Tarjan

\(Tarjan\)

定义:

强连通:指图中任意两个节点连通

强连通分量:极大的强连通子图

割点:去掉这个点后增加强连通子图的个数(就是连接两个强连通分量的点)

作用:

求强连通分量(求环):

我们发现,每个图上的环都被缩成了一个点,这些点集即组成了环,因此记录这个点集即可找到环

求割点:

割点在代码中的定义是:

\(low[y] \geq dfn[x]\) ,即从儿子点回不到父亲点。

此时,\(x\) 即为割点。(可以理解成一个环的开始)

求桥:

就是删去一条边后,增加了强连通分量的个数。

代码定义:

\(low[y] > dfn[x]\) ,此时 \(x-y\) 这条边就是桥

缩点,将有向图变成有向无环图(进行 拓扑排序求值)

我们判断原先的边中,\(x,y\) 是否处于同一个点集中。

如果不存在,就进行 \(x \rightarrow y\) 的建边,并记录入度,边权,然后拓扑排序,这样就可以进行路径长度的计算。

节省了转移方程的过程.

实现:

void tarjan(int x){
    low[x]=dfn[x]=++cnt;
    sta[++top]=x;//在栈中,表示在同一个强连通分量
    vis[x]=1;
    for(int i=head[x];i;i=nxt[i]){
        int y=ver[i]; 
        if(!dfn[y]){
            tarjan(y); low[x]=min(low[x],low[y]);
        }
        else if(vis[y]) low[x]=min(low[x],dfn[y]);//搜索到的最小栈中节点dfs序
        //注意这里是vis[y],表示在栈内
    }
    if(dfn[x]==low[x]){
        int y; 
        while(y=sta[top--]){//缩点
            sd[y]=x; vis[y]=0; if(x==y) break;
            val[x]+=val[y];
        }
    }
}

其中,\(dfn[x]\)\(x\)\(dfs\) 序,而 \(low[x]\) 为在该强连通分量中所能找到的最小的 \(dfs\) 序的值。

如果 \(dfn[x]==low[x]\) ,那么这个点就是割点,此时栈中从 \(sta[top] \leftarrow x\) 就是这个强连通分量的点。

又因为加入栈的顺序是按边加入的,所以我们也能记录强连通分量的边,从而得到图中的环。

我们在缩点之后整个图就变成 \(DAG\) ,就能进行拓扑或者算权值等操作。

例题:

P3387 【模板】缩点
这题是模板题。

我们首先需要把所有点都缩成一个点,然后进行 \(dp\)

我们怎么进行 \(dp\) 呢,因为我们缩完点之后形成了一个有向无环图,我们可以进行拓扑排序,求出来 \(dp\) 的顺序,然后增加值就行。

代码:

//P3387 【模板】缩点
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,val[N],top;
int nxt[N],ver[N],tot,head[N],from[N];
int dfn[N],low[N],cnt;
int sta[N],vis[N];//栈表示此时是否有父子关系
int sd[N];//把环上所有点都汇总到一个点
int deg[N],dis[N];
struct node{
    int nxt,ver,edge,head,from;
}t[N]; 
void add(int x,int y){
    ver[++tot]=y;from[tot]=x;nxt[tot]=head[x];head[x]=tot;
}
void tarjan(int x){
    low[x]=dfn[x]=++cnt;
    sta[++top]=x;//在栈中,表示在同一个强连通分量
    vis[x]=1;
    for(int i=head[x];i;i=nxt[i]){
        int y=ver[i]; 
        if(!dfn[y]){
            tarjan(y); low[x]=min(low[x],low[y]);
        }
        else if(vis[y]) low[x]=min(low[x],dfn[y]);//搜索到的最小栈中节点dfs序
    }
    if(dfn[x]==low[x]){
        int y; 
        while(y=sta[top--]){//缩点
            sd[y]=x; vis[y]=0; if(x==y) break;
            val[x]+=val[y];
        }
    }
}
int tot1=0;
void add1(int x,int y,int z){
    t[++tot1].ver=y; t[tot1].nxt=t[x].head; 
    t[tot1].edge=z; t[x].head=tot1; 
    deg[y]++; 
}
void topsort(){
    queue<int> q;
    for(int i=1;i<=n;i++) if(sd[i]==i&&!deg[i]){
        q.push(i); dis[i]=val[i];
    }
    while(!q.empty()){
        int x=q.front() ; q.pop();
        for(int i=t[x].head;i;i=t[i].nxt){
            int y=t[i].ver; 
            dis[y]=max(dis[y],dis[x]+val[y]); 
            deg[y]--;
            if(deg[y]==0) q.push(y);
        }
    }
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) scanf("%d",&val[i]);
    for(int i=1,x,y;i<=m;i++){
        scanf("%d%d",&x,&y); add(x,y);
    } 
    for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i); 
    for(int i=1;i<=m;i++){
        int x=sd[from[i]],y=sd[ver[i]]; //没有缩成一个点
       
        if(x!=y) add1(x,y,val[x]); 
    }
    topsort(); int ans=0;
    for(int i=1;i<=n;i++) ans=max(ans,dis[i]);
    cout<<ans<<endl;
    system("pause");
    return 0;
}

稳定婚姻

这道题就是问你两者是否处于同一个强连通分量内,但是有一些技巧。

  1. 建边时,我们前 \(n\) 条从女建到男,为了形成环,我们后 \(m\) 条边从男到女。

  2. 在处理栈中元素,要将最后一个元素 \(x\) 也标上号。

接下来就是代码:

#include<bits/stdc++.h>
using namespace std;
const int N=5e4+5;
int nxt[N],ver[N],tot,head[N];
string girl,boy;
map<string,int> a;
int n,m,dfn[N],low[N],cnt,sta[N],top,sd[N],idx;
bool vis[N];
void add(int x,int y){
    ver[++tot]=y;nxt[tot]=head[x];head[x]=tot;
}
void tarjan(int x){
    dfn[x]=low[x]=++cnt;
    sta[++top]=x; vis[x]=1;
    for(int i=head[x];i;i=nxt[i]){
        int y=ver[i]; 
        if(!dfn[y]){
            tarjan(y); low[x]=min(low[x],low[y]);
        } 
        else if(vis[y]) low[x]=min(low[x],dfn[y]);
    }
    if(low[x]==dfn[x]){ ++idx;
        int y=0;
        while(y=sta[top--]){
            sd[y]=idx; 
            vis[y]=0; if(y==x) break;
        }
    }
}
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>girl>>boy; 
        a[girl]=i; a[boy]=i+n;
        add(i,i+n);
    } cin>>m;
    for(int i=1;i<=m;i++){
        cin>>girl>>boy; add(a[boy],a[girl]);
    }
    for(int i=1;i<=n*2;i++) if(!dfn[i]) tarjan(i);
    for(int i=1;i<=n;i++){
        if(sd[i]==sd[i+n]) puts("Unsafe");
        else puts("Safe");
    }
    system("pause");
    return 0;
}

P2746 [USACO5.3]校园网Network of Schools

我们把任务 \(A,B\) 来转换一下:

任务\(A\): 求缩点后入度为 \(0\) 的个数

任务\(B\):求入度为 \(0\) 和出度为 \(0\) 的点的最大值

int main(){
    cin>>n;
    for(int i=1,x,y;i<=n;i++){
        cin>>x; 
        while(x!=0){
            add(i,x);
            X[++now]=i,Y[now]=x; 
            scanf("%d",&x);
        }
    }
    for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i); 
    for(int i=1;i<=now;i++){//同种颜色已经成环,因此将不同种颜色之间连接
        if(sd[X[i]]!=sd[Y[i]]){//同种颜色不需要统计,不同颜色更改出度和入度。
            out[sd[X[i]]]++; in[sd[Y[i]]]++;
        }
    } int ans1=0,ans2=0;
    for(int i=1;i<=idx;i++){
        if(in[i]==0) ans1++; 
        if(out[i]==0) ans2++;
    }
    if(idx==1) cout<<1<<endl<<0<<endl;
    else  cout<<ans1<<endl<<max(ans1,ans2)<<endl;
    system("pause");
    return 0;
}

P2941 [USACO09FEB]Surround the Islands S

本题把问题转换成:求通过所有点的最短路径,每条路经过两次

不同岛屿就代表着不同的联通块,缩点,然后求最小生成树(直接暴力枚举就行)。

int main(){
    cin>>n;
    for(int i=1,x,y;i<=n;i++){
        scanf("%d%d",&x,&y); add(x,y);add(y,x);
    }
    for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
    memset(dp,0x3f,sizeof(dp));
    for(int i=1;i<=n;i++) for(int j=1,x;j<=n;j++){
        scanf("%d",&x); if(i==j) continue;
        dp[sd[i]][sd[j]]=min(dp[sd[i]][sd[j]],x);
    }
    for(int i=1;i<=idx;i++){//暴力枚举以联通块i为根,到j的路径长度
        int res=0; 
        for(int j=1;j<=idx;j++) if(i!=j) res+=dp[i][j];
        ans=min(ans,res);
    }
    cout<<ans*2<<endl;//每条边走两次
    system("pause");
    return 0;
}
posted @ 2021-07-12 18:50  Evitagen  阅读(73)  评论(0编辑  收藏  举报