预备知识 说实话,基环树一般比较综合,所以一般只要就要具有图论基本知识便可以开始学习.
警告 博主实力不足,如果出错,请用力D他 .
认识 基环树,原体是树,有树上任意连一条边,就成了一个环,即基环树,一般特征,$n$个点$n$条边;
由于有树的特征,所以经常会用一些树的算法来计算基环树;
基础的知识 找环 寻找环,这是基环树的基础,也很简单.
考虑记录每个点是否访问过,当再次经过这个点时,那么这个点就能找出这个环了.
我们只要在dfs时记录点的前继即可.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void get_cir (int x,int y,int f) { int temp=x;stk[++tot]=x;id[x]=tot;w[y]=f; do { temp=pre[temp]; stk[++tot]=temp; id[temp]=tot; }while (temp!=y); sum[1 ]=w[y]; for (int i=2 ;i<=tot;i++) sum[i]=sum[i-1 ]+w[stk[i-1 ]]; } bool dfs1 (int x,int fa) { vis[x]=1 ; for (int i=head[x];i;i=edge[i].next){ int y=edge[i].to; if (y==fa) continue ; if (!vis[y]) { pre[y]=x,w[y]=edge[i].w; if (dfs1(y,x)) return 1 ; }else { get_cir(x,y,edge[i].w); return 1 ; } } return 0 ; }
这个是根据原理自己构造的,需要注意的是,环处理出来的前缀和终点不是该点,即$sum[i]$为从0点到$i+1$点的距离,所以记录环中第2个for循环就是讲$sum$数组循环移动一次,当然这是我自己构造而导致的$bug$,不过也并不碍事;
子树DP找直径以及最大深度 这里应该是比较基础的,确实,两遍bfs或dfs理解很容易,但是相比之下,DP有更优的时间以及优秀的代码长度,所以DP找直径是必要的;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ll f; void dfs2 (int x,int fa) { ll other=0 ; for (int i=head[x];i;i=edge[i].next){ int y=edge[i].to; if (y==fa||id[y]) continue ; dfs2(y,x); f=max(f,root[x]+root[y]+edge[i].w); f=max(f,other+root[y]+edge[i].w); other=max(other,root[y]+edge[i].w); dep[x]=max(dep[x],dep[y]+edge[i].w); } root[x]+=other; }
进阶结合 基环树,最终都是在环上处理问题,通常处理路径长度问题,因为博主实力不足,目前见过两种,都总结下来;
set维护 Roads in the Kingdom
王国有$n$座城市与$n$条有长度的街道,保证所有城市直接或间接联通,我们定义王国的直径为所有点对最短距离中的最大值,现因财政危机需拆除一条道路并同时要求所有城市仍然联通,求所有拆除方案中王国直径的最小值
显然,这是一道有基环树特征的题,考虑枚举拆除道路,即环上道路(不在环上就不连通了),那么如何快速求出直径是我们所需要的;
我们可以先考虑直径不过环,那么可以先DP预处理出子树上直径,并同时处理出最大深度$dep$(后面要用).
之后考虑环上直径,我们可以维护环上前缀和,注意前缀和$sum[i]$必须以点$i$结尾;那么,直径公式就很显然了;
那么,我们可以用两个set维护$sum[i]+dep[i]$和$-sum[i]+dep[i]$,每次取出最大值,相加即可;
还有一些细节问题需要注意:
1.显然$i\not =j$,如果相等,我们可以取次小值最大的更新.
2.万一不在一个环上怎么办,我们知道,环上有两种求直径, $sum[j]-sum[i]$ 和 $sum[tot]-sum[j]+sum[i]$,那么我们如何保证一定是第一个公式呢.考虑我们在存前缀和时,$sum[1]$是点$1$与点$tot$之间的距离,那么我们可以先枚举这条边,那么,此时一定是第一个公式,这个可以想一想环上前缀和的特点;那么我们就可以在枚举点,枚举的边即为它与$i-1$之间的边,每次到下一个点,使我们这个点的$sum$加上$sum[tot]$,即相当于变成了前缀和最后一个点,然后在set中加入,并删除以前的;
3.如何保证$j>i$,考虑一下,如果 $jsum[j]$ ,那么显然
而我们求出的是最优的,所以一定是后者,所以只要注意一下第一条所说的$i=j$的情况,其他情况下$i<j$.
这样应该就能解决了,对了,multiset的结构体删除请注意一下,重载运算符可能会出差错;
Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 #include <bits/stdc++.h> #define maxn 400007 #define ll long long #define mp(x,y) (d){x,y} #define st multiset<d>::iterator using namespace std ;int n,head[maxn],pre[maxn],stk[maxn],id[maxn],tot;int cent,vis[maxn],w[maxn];ll sum[maxn],ans,dep[maxn],root[maxn]; struct node { int next,to,w; }edge[maxn<<1 ]; template <typename type_of_scan>inline void scan (type_of_scan &x) { type_of_scan f=1 ;x=0 ;char s=getchar(); while (s<'0' ||s>'9' ){if (s=='-' )f=-1 ;s=getchar();} while (s>='0' &&s<='9' ){x=x*10 +s-'0' ;s=getchar();} x*=f; } template <typename top,typename ... tops>inline void scan (top &x,tops&... X) { scan(x),scan(X...); } inline void add (int u,int v,int w) { edge[++cent]=(node){head[u],v,w};head[u]=cent; edge[++cent]=(node){head[v],u,w};head[v]=cent; } void get_cir (int x,int y,int f) { int temp=x;stk[++tot]=x;id[x]=tot;w[y]=f; do { temp=pre[temp]; stk[++tot]=temp; id[temp]=tot; }while (temp!=y); sum[1 ]=w[y]; for (int i=2 ;i<=tot;i++) sum[i]=sum[i-1 ]+w[stk[i-1 ]]; } bool dfs1 (int x,int fa) { vis[x]=1 ; for (int i=head[x];i;i=edge[i].next){ int y=edge[i].to; if (y==fa) continue ; if (!vis[y]) { pre[y]=x,w[y]=edge[i].w; if (dfs1(y,x)) return 1 ; }else { get_cir(x,y,edge[i].w); return 1 ; } } return 0 ; } ll f; void dfs2 (int x,int fa) { ll other=0 ; for (int i=head[x];i;i=edge[i].next){ int y=edge[i].to; if (y==fa||id[y]) continue ; dfs2(y,x); f=max(f,root[x]+root[y]+edge[i].w); f=max(f,other+root[y]+edge[i].w); other=max(other,root[y]+edge[i].w); dep[x]=max(dep[x],dep[y]+edge[i].w); } root[x]+=other; } struct d { ll val,pos; bool operator <(const d &x) const { return val>x.val; } }; multiset <d>a,b;int main () { scan(n);ans=2305843009213693652 ; for (int i=1 ,u,v,w;i<=n;i++) scan(u,v,w),add(u,v,w); dfs1(1 ,0 ); for (int i=1 ;i<=tot;i++) dfs2(stk[i],0 ); for (int i=1 ;i<=tot;i++) a.insert(mp(sum[i]+dep[stk[i]],i)),b.insert(mp(-sum[i]+dep[stk[i]],i)); for (int i=1 ;i<=tot;i++){ st x=a.begin(),y=b.begin(); ll ol=0 ; if (x->pos==y->pos){ st dir=a.begin();dir++; ol=dir->val+y->val; dir=b.begin();dir++; ol=max(ol,dir->val+x->val); }else ol=x->val+y->val; ans=min(ans,ol); x=a.find(mp(sum[i]+dep[stk[i]],i)),y=b.find(mp(-sum[i]+dep[stk[i]],i)); a.erase(x),b.erase(y); a.insert(mp(sum[i]+sum[tot]+dep[stk[i]],i)),b.insert(mp(-sum[i]-sum[tot]+dep[stk[i]],i)); } printf ("%I64d\n" ,max(f,ans)); }
单调队列优化 P4381 [IOI2008]Island
你准备浏览一个公园,该公园由 $N$ 个岛屿组成,当地管理部门从每个岛屿 $i$ 出发向另外一个岛屿建了一座长度为$L_i$的桥,不过桥是可以双向行走的。同时,每对岛屿之间都有一艘专用的往来两岛之间的渡船。相对于乘船而言,你更喜欢步行。你希望经过的桥的总长度尽可能长,但受到以下的限制:
注意,你不必游览所有的岛,也可能无法走完所有的桥。
我们很容易分析出这是一个基环树森林,分别计算每一个基环树上的直径,但是我们不能排除环的干扰,难道要暴力枚举环上边,博主并没有尝试,决定口胡;
set 以下为博主口胡部分(并不是主流的正解做法):
根据公式$d=sum[j]-sum[i]+dep[i]+dep[j]$;
我们可以用上面的方法枚举断边,然后用set维护,求出最大直径,之后对于每一个基环树进行操作,累加和即可.
总时间复杂度$O(nlongn)$,估计可以通过此题,但是因为这道题时间太过久远,博主在之前写过单调队列,已经不想再写一遍了,不过这种方法预测可行(还挺具有普适性的);
主流的单调队列 显然,我们可以枚举点$i,j$,不过这样会$T$,
但是考虑当$i<j$时,我们可以用单调队列维护$-sum[i]+dep[i]$的最大值,然后断环成链,更新即可
至于出队条件,即为$pos[j]-pos[front]>len$($len$为环长度).时间复杂度$O(n)$.
这里不再附上代码,因为当时代码太丑,有很大一部分是与他人代码雷同(因为当时不会写),为了防止误导,就不再附上代码;
ps:当遇到这类题时会再次update.