基环树初步


预备知识

说实话,基环树一般比较综合,所以一般只要就要具有图论基本知识便可以开始学习.

警告

博主实力不足,如果出错,请用力D他 .

认识

基环树,原体是树,有树上任意连一条边,就成了一个环,即基环树,一般特征,个点条边;

由于有树的特征,所以经常会用一些树的算法来计算基环树;

基础的知识

 

找环

寻找环,这是基环树的基础,也很简单.

考虑记录每个点是否访问过,当再次经过这个点时,那么这个点就能找出这个环了.

我们只要在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维护

Roads in the Kingdom

王国有座城市与条有长度的街道,保证所有城市直接或间接联通,我们定义王国的直径为所有点对最短距离中的最大值,现因财政危机需拆除一条道路并同时要求所有城市仍然联通,求所有拆除方案中王国直径的最小值

显然,这是一道有基环树特征的题,考虑枚举拆除道路,即环上道路(不在环上就不连通了),那么如何快速求出直径是我们所需要的;

我们可以先考虑直径不过环,那么可以先DP预处理出子树上直径,并同时处理出最大深度(后面要用).

之后考虑环上直径,我们可以维护环上前缀和,注意前缀和必须以点结尾;那么,直径公式就很显然了;

 

那么,我们可以用两个set维护,每次取出最大值,相加即可;

还有一些细节问题需要注意:

  1. 显然,如果相等,我们可以取次小值最大的更新

  2. 万一不在一个环上怎么办,我们知道,环上有两种求直径,,那么我们如何保证一定是第一个公式呢.考虑我们在存前缀和时,是点与点之间的距离,那么我们可以先枚举这条边,那么,此时一定是第一个公式,这个可以想一想环上前缀和的特点;那么我们就可以在枚举点,枚举的边即为它与之间的边,每次到下一个点,使我们这个点的加上,即相当于变成了前缀和最后一个点,然后在set中加入,并删除以前的;

  3. 如何保证,考虑一下,如果,但是前缀和中,那么显然,而我们求出的是最优的,所以一定是后者,所以只要注意一下第一条所说的的情况,其他情况下.

这样应该就能解决了,对了,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
",max(f,ans));
}

单调队列优化

P4381 [IOI2008]Island

你准备浏览一个公园,该公园由 个岛屿组成,当地管理部门从每个岛屿 出发向另外一个岛屿建了一座长度为的桥,不过桥是可以双向行走的。同时,每对岛屿之间都有一艘专用的往来两岛之间的渡船。相对于乘船而言,你更喜欢步行。你希望经过的桥的总长度尽可能长,但受到以下的限制:

  • 可以自行挑选一个岛开始游览。

  • 任何一个岛都不能游览一次以上。

  • 无论任何时间,你都可以由当前所在的岛 去另一个从未到过的岛 。从

有如下方法:

  • 步行:仅当两个岛之间有一座桥时才有可能。对于这种情况,桥的长度会累加到你步行的总距离中。

  • 渡船:你可以选择这种方法,仅当没有任何桥和以前使用过的渡船的组合可以由 走到 (当检查是否可到达时,你应该考虑所有的路径,包括经过你曾游览过的那些岛)。

注意,你不必游览所有的岛,也可能无法走完所有的桥。

我们很容易分析出这是一个基环树森林,分别计算每一个基环树上的直径,但是我们不能排除环的干扰,难道要暴力枚举环上边,博主并没有尝试,决定口胡;

与set的连接

以下为博主口胡部分(并不是主流的正解做法):

根据公式;

我们可以用上面的方法枚举断边,然后用set维护,求出最大直径,之后对于每一个基环树进行操作,总时间复杂度

,估计可以通过此题,但是因为这道题时间太过久远,博主在之前写过单调队列,已经不想再写一遍了,不过这种方法预测可行(还挺具有普适性的);

主流的单调队列

显然,我们可以枚举点,不过这样会T,但是考虑当时,我们可以用单调队列维护的最大值,然后断环成链,出队条件即为,那么就可以很快地操作了.时间复杂度.

这里不再附上代码,因为当时代码太丑,有很大一部分是与他人代码雷同(因为不会写),为了防止误导,就不再附上代码;

 

ps:当遇到这类题时会再次update

原文地址:https://www.cnblogs.com/waterflower/p/11741989.html