算法:基环树DP

这道题是我学完基环树后自己做出来的第一道题目hh,虽然调了很久 和这道题目有些类似的题目有IOI2008 Island 这题

https://www.luogu.com.cn/problem/P4381

下面来说这一题的思路:

n点n边,任意两点之间都有一条双向边,因此这是一颗基环树,对于基环树DP的题目,我们首先做的应该是找到环并断开

  • 1.找环
void dfs_s(int u,int fa){
    st[u] = ins[u] = true;
    for(int i=h[u];~i;i=ne[i]){
        //if(i == (from^1)) continue; //用边判重会导致有三个点RE
        int j = e[i];
        if(j==fa) continue;
        fu[j] = u,fw[j] = w[i];
        
        if(!st[j]) dfs_s(j,u);
        else if(ins[j]){
           //debug(u);debug(j);
	       brk = w[i];
	       LL sum = 0;
	       for(int k = u;k!=j;k=fu[k]){
	           s[k] = sum;
	           sum += fw[k];
	           cir[++ed] = k;
	           
	       }
	       s[j] = sum, cir[++ed] = j;
        }
    }
    ins[u] = false;
}

找环有一个技巧在dfs的时候存储每个结点的访问状态,然后当遇到一个结点已经被访问过了,说明找到了环。然后同时根据记录的反向边而新开的数组fu[]就可以找出完整的环了

!注意在判断是否重复走一个点的时候不能用边判重,需要用是否为父节点来判重,不然可能只有70pts

这个时候我们断环成链,需要依次计算出在链上每一条边断开的形成的最长直径,在所有的直径中取最小的。之所以要取最小的是因为我们依次断开的那条边也是可以走的,但是在我们断开他之后不一定会被算到;

因此需要预处理四个数组来协助我们在O(n)的复杂度内完成最长直径的寻找

  • 处理数组找直径
//处理出后缀和和A[]数组,起点出发到i点子树或者左边子树的最大值
    for(int j=1;j<=sz-1;j++){
        sub[j] = sum[sz] - sum[j];
        if(j==1) A[j] = d[j];
        else A[j] = max(A[j-1] , d[j] + sum[j]);
    	//debug(j);debug(A[j]);
    }
    //处理B[]数组
    for(int j=sz-1;j>=1;j--){
        if(j==sz-1) B[j] = d[j+1];
        else B[j] = max(B[j+1],d[j+1] + sub[j+1]);
        //debug(sub[j]);
    }
    mn=-0x3f3f3f3f;
    //处理C[]数组
    for(int j=1;j<=sz-1;j++){
        if(j==1) C[j] = max(d[j],tr[j]);
        else C[j] = max({C[j-1],tr[j],d[j]+sum[j]+mn});
        mn = max(mn,d[j]-sum[j]);
    }
    mn=-0x3f3f3f3f;
    //处理D[]数组
    for(int j=sz-1;j>=1;j--){
        if(j==sz-1) D[j] = max(tr[j+1],d[j+1]);
        else D[j] = max({D[j+1],tr[j+1],d[j+1]+sub[j+1]+mn});
        mn = max(mn,d[j+1]-sub[j+1]);
    }
    //debug(brk);
    for(int i=1;i<=sz-1;i++){
  		 //debug(A[i]);debug(B[i]);debug(C[i]);debug(D[i]);debug(A[i]+B[i]+brk);debug(ans);
        ans=min(ans,max({A[i],B[i],C[i],D[i],A[i]+B[i]+brk}));
    }

下面是完整的代码:

#include<bits/stdc++.h>
using namespace std;
//================================
#define debug(a) cout << #a": " << a << endl;
#define rep(i, ll,rr) for(int i = ll; i <= rr; ++i)
const int N =1e5+10,INF = 1e9;
//================================
typedef pair<int,int> pii;
#define x first
#define y second
typedef long long LL; typedef unsigned long long ULL; typedef long double LD;
inline LL read() { LL s = 0, w = 1; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') w = -1; for (; isdigit(ch); ch = getchar())    s = (s << 1) + (s << 3) + (ch ^ 48); return s * w; }
inline void print(LL x, int op = 10) { if (!x) { putchar('0'); if (op)  putchar(op); return; }  char F[40]; LL tmp = x > 0 ? x : -x; if (x < 0)putchar('-');  int cnt = 0;    while (tmp > 0) { F[cnt++] = tmp % 10 + '0';     tmp /= 10; }    while (cnt > 0)putchar(F[--cnt]);    if (op) putchar(op); }
//================================= 
int n,m,a,b;
LL e[N*2],ne[N*2],h[N],idx=0,fu[N*2],tr[N*2],brk;  //tr[]维护当前树上的直径
LL w[N*2],fw[N],d[N*2],s[N*2],sum[N*2],c;
LL A[N],B[N],C[N],D[N],sub[N],mn=-0x3f3f3f3f;//mn维护d[x]-s[x]的最大值
int cir[N],ed=0,q[N*2];
bool st[N*2],ins[N*2];
LL res,ans=1e18;
/*
sum为前缀和
1.A[]:环上第一个点出发到i点的子树或者左边子树的最大距离 d[i] + sum[i]
2.B[]:环上最后一个点出发到i或者右边子树的最大距离 d[i] + sub[i]
3.C[]:i之前的直径 d[x] + d[y] - sum[x] + sum[y]
4.D[]:i之后的直径 d[x] + d[y] + sum[x] - sum[y]
*/
void add(int a,int b,LL c){
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}

void dfs_s(int u,int fa){
    st[u] = ins[u] = true;
    for(int i=h[u];~i;i=ne[i]){
        //if(i == (from^1)) continue; //用边判重会导致有三个点RE
        int j = e[i];
        if(j==fa) continue;
        fu[j] = u,fw[j] = w[i];
        
        if(!st[j]) dfs_s(j,u);
        else if(ins[j]){
           //debug(u);debug(j);
	       brk = w[i];
	       LL sum = 0;
	       for(int k = u;k!=j;k=fu[k]){
	           s[k] = sum;
	           sum += fw[k];
	           cir[++ed] = k;
	           
	       }
	       s[j] = sum, cir[++ed] = j;
        }
    }
    ins[u] = false;
}

LL dfs_d(int u,int id){
    st[u] = true;
    LL d1 = 0 , d2 = 0 ;
    for(int i=h[u];~i;i=ne[i]){
        int j = e[i];
        if(st[j]) continue;
        LL length = dfs_d(j,id)+w[i];
        if(length > d1) d2 = d1,d1 = length;
        else if(length > d2) d2 = length;
    }
    tr[id] = d1 + d2;
    return d1;
}

signed main(){
    memset(h,-1,sizeof h);
    n=read();
    rep(i,1,n){
        a=read(),b=read(),c=read();
        add(a,b,c),add(b,a,c);
    }
    for(int i=1;i<=n;i++)
        if(!st[i])
            dfs_s(i,-1);
    
    memset(st,0,sizeof st);
    for(int i=1;i<=ed;i++) st[cir[i]] = true;  //将圆上的点全部标记一下,防止被找过
    
//     ans = 0.0;
    int sz = 1;
    for(int j = 1;j <= ed; j++){
        int k = cir[j];
        d[sz] = dfs_d(k,sz);
        sum[sz] = s[k];
        sz ++ ;
    }sz--;
    //debug(sz);debug(sum[sz]);
    //处理出后缀和和A[]数组,起点出发到i点子树或者左边子树的最大值
    for(int j=1;j<=sz-1;j++){
        sub[j] = sum[sz] - sum[j];
        if(j==1) A[j] = d[j];
        else A[j] = max(A[j-1] , d[j] + sum[j]);
    	//debug(j);debug(A[j]);
    }
    //处理B[]数组
    for(int j=sz-1;j>=1;j--){
        if(j==sz-1) B[j] = d[j+1];
        else B[j] = max(B[j+1],d[j+1] + sub[j+1]);
        //debug(sub[j]);
    }
    mn=-0x3f3f3f3f;
    //处理C[]数组
    for(int j=1;j<=sz-1;j++){
        if(j==1) C[j] = max(d[j],tr[j]);
        else C[j] = max({C[j-1],tr[j],d[j]+sum[j]+mn});
        mn = max(mn,d[j]-sum[j]);
    }
    mn=-0x3f3f3f3f;
    //处理D[]数组
    for(int j=sz-1;j>=1;j--){
        if(j==sz-1) D[j] = max(tr[j+1],d[j+1]);
        else D[j] = max({D[j+1],tr[j+1],d[j+1]+sub[j+1]+mn});
        mn = max(mn,d[j+1]-sub[j+1]);
    }
    //debug(brk);
    for(int i=1;i<=sz-1;i++){
  		 //debug(A[i]);debug(B[i]);debug(C[i]);debug(D[i]);debug(A[i]+B[i]+brk);debug(ans);
        ans=min(ans,max({A[i],B[i],C[i],D[i],A[i]+B[i]+brk}));
    }
    
    ans & 1 ? printf("%lld.5",ans/2) : printf("%lld.0",ans/2);
    
    return 0;
}