预备知识
说实话,基环树一般比较综合,所以一般只要就要具有图论基本知识便可以开始学习.
警告
认识
基环树,原体是树,有树上任意连一条边,就成了一个环,即基环树,一般特征,个点条边;
由于有树的特征,所以经常会用一些树的算法来计算基环树;
基础的知识
找环
寻找环,这是基环树的基础,也很简单.
考虑记录每个点是否访问过,当再次经过这个点时,那么这个点就能找出这个环了.
我们只要在dfs时记录点的前继即可.
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; }//返回值为是否找到环
这个是根据原理自己构造的,需要注意的是,环处理出来的前缀和终点不是该点,即为从0点到点的距离,所以记录环中第2个for循环就是讲数组循环移动一次,当然这是我自己构造而导致的,不过也并不碍事;
子树DP找直径以及最大深度
这里应该是比较基础的,确实,两遍bfs或dfs理解很容易,但是相比之下,DP有更优的时间以及优秀的代码长度,所以DP找直径是必要的;
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; }//DP找直径以及最大深度
进阶结合
基环树,最终都是在环上处理问题,通常处理路径长度问题,因为博主实力不足,目前见过两种,都总结下来;
set维护
王国有座城市与条有长度的街道,保证所有城市直接或间接联通,我们定义王国的直径为所有点对最短距离中的最大值,现因财政危机需拆除一条道路并同时要求所有城市仍然联通,求所有拆除方案中王国直径的最小值
显然,这是一道有基环树特征的题,考虑枚举拆除道路,即环上道路(不在环上就不连通了),那么如何快速求出直径是我们所需要的;
我们可以先考虑直径不过环,那么可以先DP预处理出子树上直径,并同时处理出最大深度(后面要用).
之后考虑环上直径,我们可以维护环上前缀和,注意前缀和必须以点结尾;那么,直径公式就很显然了;
那么,我们可以用两个set维护和,每次取出最大值,相加即可;
还有一些细节问题需要注意:
-
显然,如果相等,我们可以取次小值最大的更新
-
万一不在一个环上怎么办,我们知道,环上有两种求直径,和,那么我们如何保证一定是第一个公式呢.考虑我们在存前缀和时,是点与点之间的距离,那么我们可以先枚举这条边,那么,此时一定是第一个公式,这个可以想一想环上前缀和的特点;那么我们就可以在枚举点,枚举的边即为它与之间的边,每次到下一个点,使我们这个点的加上,即相当于变成了前缀和最后一个点,然后在set中加入,并删除以前的;
-
如何保证,考虑一下,如果,但是前缀和中,那么显然,而我们求出的是最优的,所以一定是后者,所以只要注意一下第一条所说的的情况,其他情况下.
这样应该就能解决了,对了,multiset的结构体删除请注意一下,重载运算符可能会出差错;
Code
#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; }//DP找直径以及最大深度 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)); }
单调队列优化
你准备浏览一个公园,该公园由 个岛屿组成,当地管理部门从每个岛屿 出发向另外一个岛屿建了一座长度为的桥,不过桥是可以双向行走的。同时,每对岛屿之间都有一艘专用的往来两岛之间的渡船。相对于乘船而言,你更喜欢步行。你希望经过的桥的总长度尽可能长,但受到以下的限制:
可以自行挑选一个岛开始游览。
任何一个岛都不能游览一次以上。
无论任何时间,你都可以由当前所在的岛 去另一个从未到过的岛 。从 到
有如下方法:
步行:仅当两个岛之间有一座桥时才有可能。对于这种情况,桥的长度会累加到你步行的总距离中。
渡船:你可以选择这种方法,仅当没有任何桥和以前使用过的渡船的组合可以由 走到 (当检查是否可到达时,你应该考虑所有的路径,包括经过你曾游览过的那些岛)。
注意,你不必游览所有的岛,也可能无法走完所有的桥。
我们很容易分析出这是一个基环树森林,分别计算每一个基环树上的直径,但是我们不能排除环的干扰,难道要暴力枚举环上边,博主并没有尝试,决定口胡;
与set的连接
以下为博主口胡部分(并不是主流的正解做法):
根据公式;
我们可以用上面的方法枚举断边,然后用set维护,求出最大直径,之后对于每一个基环树进行操作,总时间复杂度
,估计可以通过此题,但是因为这道题时间太过久远,博主在之前写过单调队列,已经不想再写一遍了,不过这种方法预测可行(还挺具有普适性的);
主流的单调队列
显然,我们可以枚举点,不过这样会T,但是考虑当时,我们可以用单调队列维护的最大值,然后断环成链,出队条件即为,那么就可以很快地操作了.时间复杂度.
这里不再附上代码,因为当时代码太丑,有很大一部分是与他人代码雷同(因为不会写),为了防止误导,就不再附上代码;