基环树,也是环套树,是一种有 n 个点 n 条边的图,简单地讲就是树上在加一条边。它形如一个环,环上每个点都有一棵子树的形式。
基环内向树:每个点出度为1(因此每个环上点的子树,儿子指向父亲)
基环外向树:每个点入度为1(因此每个环上点的子树,父亲指向儿子)
基环树的关键就是找到环,可以先把环当作这个无根树的 “根” ,也就是把环当成一个点(先不管它),这样一颗基环树就变成了一个普通的树,然后我们先按照解决普通树的方法对“根”的所有子树依次处理求解答案,最后在单独对环上所有的点进行操作求解最终答案即可。
有向图找环
- inline void find(int u,int rt){
- vis[u] = 1;
- for(re int i(head[u]) ; i ; i=e[i].nxt){
- int v = e[i].to;
- if(v == rt) { r1=u; r2=v; return; } //找到了环,设为这条边的两个端点
- if(vis[v]) continue;
- find(v,rt);
- }
- }
无向图找环
- inline void find(int u,int rt){
- vis[u] = 1;
- for(re int i(head[u]) ; i ; i=e[i].nxt){
- int v = e[i].to;
- if(v == rt) continue;
- if(vis[v]){
- fl = 1;
- r1 = u;
- r2 = v;
- return;
- }
- find(v,u);
- if(fl) return;
- }
- }
洛谷 P2607 [ZJOI2008] 骑士

这道题可以算是一个例题吧,考虑如何进行连边。由于一个人可能被多个人痛恨,那么把一个人痛恨的人作为这个人的父亲节点进行连边。这样这个图就变成了一个有向的基环树。我们每一次找环,把环断开成链,设断开的边的两个端点是 u 和 v,分别对 u 进行 dfs 和 v 进行 dfs,取两个的最大值。由于相邻的两个点最多只能选其中一个,那么这道题断开环之后就变成了一个基础的树形DP。
- #include
- #include
- #include
- #include
- #include
- #define re register
- #define int long long
- #define drep(a,b,c) for(re int a(b) ; a>=(c) ; --a)
- #define rep(a,b,c) for(re int a(b) ; a<=(c) ; ++a)
- using namespace std;
- inline int read(){
- int x=0,f=1;char ch=getchar();
- while(ch<'0'||ch>'9'){if(ch == '-') f=-1 ; ch=getchar();}
- while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
- return x*f;
- }
- const int M = 2e6+10;
- int n;
- int head[M],w[M],vis[M],f[M][2];
- int cnt,sum,res1,res2,r1,r2;
- struct edge{
- int to,nxt;
- }e[M];
- inline void add(int u,int v){
- e[++cnt].to = v;
- e[cnt].nxt = head[u];
- head[u] = cnt;
- }
- inline void find(int u,int rt){ //找环
- vis[u] = 1;
- for(re int i(head[u]) ; i ; i=e[i].nxt){
- int v = e[i].to;
- if(v == rt) { r1=u; r2=v; return; }
- if(vis[v]) continue;
- find(v,rt);
- }
- }
- inline int dfs(int u,int rt){
- f[u][0] = 0,f[u][1] = w[u]; //f[u][0]表示不选该节点,f[u][1]表示选
- for(re int i(head[u]) ; i ; i=e[i].nxt){
- int v = e[i].to;
- if(v == rt) continue;
- dfs(v,rt);
- f[u][0] += max(f[v][0],f[v][1]); //不选这个点,从它子节点转移过来
- f[u][1] += f[v][0]; //选这个点就不能选它的子节点
- }
- return f[u][0]; //让这个点强制不选
- }
- signed main(){
- n=read();
- rep(i,1,n){
- w[i]=read();
- int u=read();
- add(u,i);
- }
- rep(i,1,n){
- if(!vis[i]){
- r1 = r2 = 0;
- find(i,i);
- if(r1){ //找到了环
- res1 = dfs(r1,r1);
- res2 = dfs(r2,r2);
- sum += max(res1,res2);
- }
- }
- }
- printf("%lld\n",sum);
- return 0;
- }
洛谷 P5022 [NOIP2018 提高组] 旅行

这道题如果没有环的话,就把每一个点的出边按字典序从小到大进行排序,然后每一个点先 dfs 它孩子字典序小的点,最后输出答案即可。
然后考虑有环的情况。还是跟基环树的题做法一样,找到环,断开,对断边的两个点分别进行 dfs,求出每一种情况的更优解。
要注意的是,由于这道题要排序,普通的链式前向星实现起来比较困难,用 vector 比较好实现。而且在 dfs 的过程中,我们需要记录走到该点会不会让答案更优。如果会变差,直接 return 就行,变优的话记录下来,接着往下搜索。
- #include
- #include
- #include
- #include
- #include
- #include
- #define re register
- #define drep(a,b,c) for(re int a(b) ; a>=(c) ; --a)
- #define rep(a,b,c) for(re int a(b) ; a<=(c) ; ++a)
- using namespace std;
- inline int read(){
- int x=0,f=1;char ch=getchar();
- while(ch<'0'||ch>'9'){if(ch == '-') f=-1 ; ch=getchar();}
- while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
- return x*f;
- }
- typedef pair<int,int> pii;
- const int M = 1e5+10;
- vector<int> g[M];
- pii edge[M];
- int path[M],vis[M];
- int cnt;
- int n,m,better,nu,nv;
- inline bool dfs(int u){
- if(!better){
- if(u > path[cnt]) return 1;
- if(u < path[cnt]) better = -1;
- }
- path[cnt++] = u;
- vis[u] = 1;
- for(auto v : g[u]){
- if(vis[v]) continue;
- if(v==nu && u==nv) continue;
- if(v==nv && u==nu) continue;
- if(dfs(v)) return 1;
- }
- return 0;
- }
- signed main(){
- n=read(),m=read();
- rep(i,1,m){
- int u=read(),v=read();
- edge[i].first = u,edge[i].second = v;
- g[u].push_back(v);
- g[v].push_back(u);
- }
- memset(path,0x3f,sizeof(path));
- rep(i,1,n) sort(g[i].begin(),g[i].end());
- if(m == n-1) dfs(1);
- else{
- rep(i,1,m){
- nu = edge[i].first;
- nv = edge[i].second;
- memset(vis,0,sizeof(vis));
- cnt = better = 0;
- dfs(1);
- }
- }
- rep(i,0,n-1) printf("%d ",path[i]);
- return 0;
- }
洛谷 P1399 [NOI2013] 快餐店

这道题个人认为可以算是比较难的题了。
首先这是一道基环树的题。给定 n 个点和 n 条边,而且保证联通,那么就存在一个环。
要求出一个点到离它距离最远的点的距离最小,显然跟直径有关,求出此基环树的直径,答案为直径除以 2。
那么考虑如何求直径。有两种情况。
1.直径不经过基环。
2.直径经过基环。
在求直径之前,我们需要将哪些点在环上记录下来。我们从 1 号点开始深搜找环。具体看代码注释。
- inline bool find(int u){
- vis[u] = 1; //这个点记录为走过
- for(re int i(head[u]) ; i ; i=e[i].nxt){
- int v = e[i].to;
- if(v == fa[u]) continue; //走到了父亲不行
- fa[v] = u;
- w[v] = e[i].w; //将这条边的边权转化为儿子的点权,和树剖的转化类似
- if(!vis[v]) { if(find(v)) return 1; } //如果这个点没有走过而且后面能找到环,就一直往回return
- else{ //找到了环
- int p = u; //枚举环上的点
- while(1){
- inc[p] = 1; //inc[p]=1表示p在环上
- cv[++tot] = p; //环上的点的编号,由于后面操作,我们需要重新编号
- cw[tot] = w[p]; //环上这个编号点的点权
- p = fa[p]; //由于记录了fa[p]是p的父亲,所以可以往回找
- if(p == u) break; //环遍历完了
- }
- return 1; //记录已经找到环
- }
- }
- return 0; //没找到就继续往下找
- }
一个一个考虑,先看直径不经过基环,这个比较好求。此基环树的直径为环上挂着的点的每一棵子树的最大直径,记为 res1,枚举环上的每一个点,深搜计算子树的直径 res1=max(d[u]+d[v]+w(u,v))。顺道求出以 u 为根的子树的最大深度 d[u]。
- inline void dfs(int u,int fa){
- for(re int i(head[u]) ; i ; i=e[i].nxt){
- int v = e[i].to,w = e[i].w;
- if(inc[v] || v==fa) continue; //如果这个点在环上,或者搜到了父亲就不继续
- dfs(v,u); //往下搜
- res1 = max(res1,d[u]+d[v]+w); //求最大的直径
- d[u] = max(d[u],d[v]+w); //顺道记录下来以u为根的最大深度
- }
- }
-
- rep(i,1,tot) dfs(cv[i],0);
然后来看直径经过环。一个最简单的想法就是,暴力枚举删哪一条边, res[i] = max (r两个子树的深度 + 它们在环上的距离 )。则经过环的直径 res2 = min(res[i])。
但这样做复杂度是 n^2 的,过不去本题。考虑如何进行优化。
1.把边 (1,tot) 断开,tot 为环上有 tot 个点,预处理前缀长度。
A[i] = max( 前缀中链的长度 + 节点树的深度 )。
B[i] = max( 前缀中两棵树的深度 + 这两个节点之间的距离 )。
2.把边 (1,tot) 断开,预处理后缀长度。
C[i] = max( 后缀中链的长度 + 节点树的深度 )。
D[i] = max( 后缀中两棵树的深度 + 这两个节点之间的距离 )。
3.枚举断开环上的边 (i,i+1),拼凑答案。
res[i]=max(B[i],D[i+1],A[i]+C[i+1]+w(1,tot))
则经过环的直径 res2=min(res[i])。
注意,最后需要有个特判,断开 (1,tot) 这条边,需要加上一句话 res2=min(res2,B[tot])。
那么基环树的直径就是 max(res1,res2),这道题的答案就是 max(res1,res2)/2。
- double sum=0,mx=0;
- rep(i,1,tot){ //预处理前缀
- sum += cw[i-1]; //sum就是链长的加和
- A[i] = max(A[i-1],sum+d[cv[i]]); //根据A数组的定义来推
- B[i] = max(B[i-1],mx+d[cv[i]]+sum); //这里很巧妙
- //我们的mx记录的是i这个点之前的某一个点k的深度-k之前的链长
- //也就是说,我们要求B数组,可以用一个抵消的思想
- //sum记录的是链长的加和,再加上mx,也就把k前面的链长消掉了
- //最后剩的就是k的深度+k到i的链长+i的深度,与B数组定义相同
- mx = max(mx,d[cv[i]]-sum); //注意要更新mx
- }
- sum = mx = 0;
- double cn_1 = cw[tot]; //这里也很巧妙,把tot这个点的点权记录下来,也就是相当于把1和tot这条边断开
- cw[tot] = 0; //要把tot这个点点权清零
- drep(i,tot,1){ //预处理后缀,和前缀类似
- sum += cw[i];
- C[i] = max(C[i+1],sum+d[cv[i]]);
- D[i] = max(D[i+1],mx+d[cv[i]]+sum);
- mx = max(mx,d[cv[i]]-sum);
- }
- double res;
- rep(i,1,tot){
- res = max(max(B[i],D[i+1]),A[i]+C[i+1]+cn_1); //上述所说的三段
- res2 = min(res2,res); //使res2最小化
- }
- res2 = min(res2,B[tot]); //别忘了加上特判
- printf("%.1lf\n",max(res1,res2)/2);
- #include
- #include
- #include
- #include
- #include
- #define re register
- #define drep(a,b,c) for(re int a(b) ; a>=(c) ; --a)
- #define rep(a,b,c) for(re int a(b) ; a<=(c) ; ++a)
- using namespace std;
- inline int read(){
- int x=0,f=1;char ch=getchar();
- while(ch<'0'||ch>'9'){if(ch == '-') f=-1 ; ch=getchar();}
- while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
- return x*f;
- }
- const int M = 2e5+10;
- int n;
- int head[M],vis[M],fa[M];
- int inc[M],cv[M],cw[M],w[M];
- double d[M],A[M],B[M],C[M],D[M];
- int cnt,tot,ans;
- double res1,res2=1e18;
- struct edge{
- int to,nxt,w;
- }e[M];
- inline void add(int u,int v,int w){
- e[++cnt].to = v;
- e[cnt].w = w;
- e[cnt].nxt = head[u];
- head[u] = cnt;
- }
- inline bool find(int u){
- vis[u] = 1; //这个点记录为走过
- for(re int i(head[u]) ; i ; i=e[i].nxt){
- int v = e[i].to;
- if(v == fa[u]) continue; //走到了父亲不行
- fa[v] = u;
- w[v] = e[i].w; //将这条边的边权转化为儿子的点权,和树剖的转化类似
- if(!vis[v]) { if(find(v)) return 1; } //如果这个点没有走过而且后面能找到环,就一直往回return
- else{ //找到了环
- int p = u; //枚举环上的点
- while(1){
- inc[p] = 1; //inc[p]=1表示p在环上
- cv[++tot] = p; //环上的点的编号,由于后面操作,我们需要重新编号
- cw[tot] = w[p]; //环上这个编号点的点权
- p = fa[p]; //由于记录了fa[p]是p的父亲,所以可以往回找
- if(p == u) break; //环遍历完了
- }
- return 1; //记录已经找到环
- }
- }
- return 0; //没找到就继续往下找
- }
- inline void dfs(int u,int fa){
- for(re int i(head[u]) ; i ; i=e[i].nxt){
- int v = e[i].to,w = e[i].w;
- if(inc[v] || v==fa) continue; //如果这个点在环上,或者搜到了父亲就不继续
- dfs(v,u); //往下搜
- res1 = max(res1,d[u]+d[v]+w); //求最大的直径
- d[u] = max(d[u],d[v]+w); //顺道记录下来以u为根的最大深度
- }
- }
- signed main(){
- n=read();
- rep(i,1,n){
- int u=read(),v=read(),w=read();
- add(u,v,w),add(v,u,w);
- }
- find(1);
- rep(i,1,tot) dfs(cv[i],0);
- double sum=0,mx=0;
- rep(i,1,tot){ //预处理前缀
- sum += cw[i-1]; //sum就是链长的加和
- A[i] = max(A[i-1],sum+d[cv[i]]); //根据A数组的定义来推
- B[i] = max(B[i-1],mx+d[cv[i]]+sum); //这里很巧妙
- //我们的mx记录的是i这个点之前的某一个点k的深度-k之前的链长
- //也就是说,我们要求B数组,可以用一个抵消的思想
- //sum记录的是链长的加和,再加上mx,也就把k前面的链长消掉了
- //最后剩的就是k的深度+k到i的链长+i的深度,与B数组定义相同
- mx = max(mx,d[cv[i]]-sum); //注意要更新mx
- }
- sum = mx = 0;
- double cn_1 = cw[tot]; //这里也很巧妙,把tot这个点的点权记录下来,也就是相当于把1和tot这条边断开
- cw[tot] = 0; //要把tot这个点点权清零
- drep(i,tot,1){ //预处理后缀,和前缀类似
- sum += cw[i];
- C[i] = max(C[i+1],sum+d[cv[i]]);
- D[i] = max(D[i+1],mx+d[cv[i]]+sum);
- mx = max(mx,d[cv[i]]-sum);
- }
- double res;
- rep(i,1,tot){
- res = max(max(B[i],D[i+1]),A[i]+C[i+1]+cn_1); //上述所说的三段
- res2 = min(res2,res); //使res2最小化
- }
- res2 = min(res2,B[tot]); //别忘了加上特判
- printf("%.1lf\n",max(res1,res2)/2);
- return 0;
- }