zoukankan      html  css  js  c++  java
  • Tarjan 总结

    Tarjan 基础

      dfn[i]: 在dfs中该节点被搜索的次序(时间戳)。

      low[i]: 为i或i的子树能够追溯到的最早的栈中节点的次序号。

       dfn[i] == low[i] 时,为i或i的子树可以构成一个强连通分量。

    void tarjan(int x)
    {
        id++;
        dfn[x] = id;
        low[x] = id;
        vis[x] = 1;//是否在栈中
        stk[++top] = x;//入栈
        for(int i = head[x]; i != 0; i = edge[i].nxt){
            int temp = edge[i].to;
            if(!dfn[temp]){
                tarjan(temp);
                low[x] = min(low[x],low[temp]);
            }
            else if(vis[temp]){
                low[x] = min(low[x],dfn[temp]);
            }
        }
        if(dfn[x] == low[x]){//构成强连通分量,进行染色
            vis[x] = 0;
            color[x] = ++col;
            while(stk[top] != x){
                color[stk[top]] = col;
                vis[stk[top--]] = 0;
            }
            top--;
        }
    }
    View Code

     割边、割点

    一、基本概念

        桥:无向连通图中,如果删除某边后,图变成不连通,则称该边为桥。

        割点:无向连通图中,如果删除某点后,图变成不连通,则称该点为割点。

    二、Tarjan算法求解桥和割点

        1.割点:1)当前节点为树根的时候,条件是要有至少两颗子树

                2)当前节点u不是树根的时候,条件是存在u的一个子节点v使得 low[v]>=dfn[u]

        2.桥:当且仅当无向边(u,v)是树枝边的时候,条件是 dfn[u]<low[v]

    #include<bits/stdc++.h>
    
    using namespace std;
    const int N = 201;
    vector<int>G[N];
    int n,m,low[N],dfn[N];
    bool is_cut[N];
    int father[N],tim;
    void input()
    {
        scanf("%d%d",&n,&m);
        int a,b;
        for(int i=1;i<=m;++i)
        {
            scanf("%d%d",&a,&b);
            G[a].push_back(b);
            G[b].push_back(a);
        }
    }
    void Tarjan(int i,int Father)
    {
        father[i]=Father;
        dfn[i]=low[i]=tim++;
        for(int j=0;j<G[i].size();++j)
        {
            int k=G[i][j];
            if(dfn[k]==-1)
            {
                Tarjan(k,i);
                low[i]=min(low[i],low[k]);
            }
            else if(Father!=k)/*假如k是i的父亲的话,那么这就是无向边中的重边,有重边那么一定不是桥*/
                low[i]=min(low[i],dfn[k]);
        }
    }
    void _count()
    {
        int rootson=0;
        Tarjan(1,0);
        for(int i=2;i<=n;++i)
        {
            int v=father[i];
            if(v==1)
            rootson++;
            else{
                if(low[i]>=dfn[v])/*割点的条件*/
                is_cut[v]=true;
            }
        }
        if(rootson>1)
        is_cut[1]=true;
        for(int i=1;i<=n;++i)
        if(is_cut[i])
        printf("%d
    ",i);
        for(int i=1;i<=n;++i)
        {
            int v=father[i];
            if(v>0&&low[i]>dfn[v])/*桥的条件*/
            printf("%d,%d
    ",v,i);
        }
    
    }
    int main()
    {
        input();
        memset(dfn,-1,sizeof(dfn));
        memset(father,0,sizeof(father));
        memset(low,-1,sizeof(low));
        memset(is_cut,false,sizeof(is_cut));
        _count();
        return 0;
    }
    View Code

     有向图缩点

    思想:将一个有向图强连通分量缩点为一个点去代替一堆点,要修改两个属性,一个是边,一个是点。

    方法:运用Tarjan算法找出一个强连通分量,每次找出一个强连通分量,我们就用其中的一个点去代表这一堆点。记这个点为代表点( ̄□ ̄||只是自己这么叫),那么这堆点的contract[x] = 代表点。

        对于非代表点:

       ①、将它的出边全部复制给代表点。

       ②、将它的点权加给代表点。

       ③、所有指向它的点在使用时用指向 contract[x] 代替即可,不需要做修改。

    #include<bits/stdc++.h>
    
    using namespace std;
    typedef long long ll;
    const int maxn = 200005;
    vector<int> e[maxn];
    int ins[maxn], dfn[maxn], low[maxn], contract[maxn];
    ll w[maxn];
    int ind;
    stack<int> s;
    void tarjan(int u)
    {
        dfn[u] = low[u] = ++ind;
        ins[u] = 1;
        s.push(u);
        for(int i = 0; i < e[u].size(); i++) {
            int v = e[u][i];
            if(!dfn[v])
            {
                tarjan(v);
                low[u] = min(low[u], low[v]);
            }
            else if(ins[v]) low[u] = min(low[u], dfn[v]);
        }
        if(dfn[u] == low[u]) {
            int v;
            do {
                v = s.top();
                s.pop();
                ins[v] = 0;
                contract[v] = u;
                if(u != v) {
                    w[u] += w[v];
                    while(!e[v].empty()) {
                        e[u].push_back(e[v].back());
                        e[v].pop_back();
                    }
                }
            } while(u != v);
        }
    }
    
    ll dfs(int u, ll cnt)
    {
        cnt += w[u];
        ll ret = cnt;
        for(int i = 0; i < e[u].size(); i++) {
            int v = contract[e[u][i]];
            if(v != u) ret = max(ret, dfs(v, cnt));
        }
        return ret;
    }
    int main()
    {
        int n, m;
        scanf("%d%d",&n,&m);
        for(int i = 1; i <= n; i++) {
            scanf("%d",&w[i]);
        }
        for(int i = 1; i <= m; i++) {
            int u, v;
            scanf("%d%d",&u,&v);
            e[u].push_back(v);
        }
        tarjan(1);
        ll ans = dfs(1,0);
        printf("%lld
    ",ans);
        return 0;
    }
    View Code

    向图的双连通分量

    一、点双连通分量

      定义:对于一个连通图,如果任意两点至少存在两条点不重复路径,则称这个图为点双连通的(简称双连通)。点双连通图的定义等价于任意两条边都同在一个简单环中。对一个无向图,点双连通的极大子图称为点双连通分量(简称双连通分量)。

    #include<bits/stdc++.h>
    
    using namespace std;
    const int maxn = 1000;
    
    struct Edge {
        int u,v;
        Edge(int uu,int vv)
        {
            u = uu;
            v = vv;
        }
    };
    stack<Edge> s;
    
    struct edge  //链式前向星建图的边结构
    {
        int v,next;
    }edges[maxn];
    
    int n,m;        //节点的数目,无向边的数目
    int e,head[maxn];
    int dfn[maxn];           //第一次访问的时间戳
    int dfs_clock;           //时间戳
    int iscut[maxn];         //标记节点是否为割点
    int bcc_cnt;             //点_双连通分量的数目
    int bccno[maxn];         //节点属于的点_双连通分量的编号
    vector<int> bcc[maxn];   //点_双连通分量
    
    void addedges(int u,int v)  //加边
    {
        edges[e].v = v;
        edges[e].next = head[u];
        head[u] = e++;
        edges[e].v = u;
        edges[e].next = head[v];
        head[v] = e++;
    }
    
    int dfs(int u,int fa)
    {
        int low = dfn[u] = ++dfs_clock;
        int child = 0;
        for(int i=head[u];i!=-1;i=edges[i].next)
        {
            int v = edges[i].v;
            Edge e = (Edge){u,v};
            if(!dfn[v])
            {
                s.push(e);
                child++;
                int lowv = dfs(v,u);
                low = min(low,lowv); //用后代更新low
                if(lowv >= dfn[u])     //找到了一个子树满足割顶的条件
                {
                    iscut[u] = 1;
                    bcc_cnt++;
                    bcc[bcc_cnt].clear();
                    for(;;)            //保存bcc信息
                    {
                        Edge x = s.top(); s.pop();
                        if(bccno[x.u] != bcc_cnt) {bcc[bcc_cnt].push_back(x.u); bccno[x.u] = bcc_cnt;}
                        if(bccno[x.v] != bcc_cnt) {bcc[bcc_cnt].push_back(x.v); bccno[x.v] = bcc_cnt;}
                        if(x.u == u && x.v == v) break;
                    }
                }
            }
            else if(dfn[v] < dfn[u] && v != fa)   //用反向边更新low
            {
                s.push(e);
                low = min(low,dfn[v]);
            }
        }
        if(fa < 0 && child == 1) iscut[u] = 0;    //对于根节点若只有一个子树则不是割顶
        return low;
    }
    
    void init()
    {
        memset(dfn,0,sizeof(dfn));
        memset(iscut,0,sizeof(iscut));
        memset(head,-1,sizeof(head));
        memset(bccno,0,sizeof(bccno));
        e = 0; dfs_clock = 0; bcc_cnt = 0;
    }
    
    int main()
    {
        int u,v;
        while(scanf("%d%d",&n,&m)!=EOF)
        {
            init();
            for(int i=0;i<m;i++)
            {
                scanf("%d%d",&u,&v);
                addedges(u,v);
            }
            dfs(1,-1);
            for(int i=1;i<=bcc_cnt;i++)
            {
                for(int j=0;j<bcc[i].size();j++)
                    cout<<bcc[i][j]<<" ";
                cout<<endl;
            }
    
        }
        return 0;
    }
    View Code

    二、边双连通分量

      定义:对于一个连通图,如果任意两点至少存在两条边不重复路径,则称该图为边双连通的。边双连通图的定义等价于任意一条边至少在一个简单环中。对一个无向图,边双连通的极大子图称为边双连通分量。

    #include<bits/stdc++.h>
    
    using namespace std;
    
    const int maxn = 1005;
    struct Edge
    {
        int no,v,next;      //no:边的编号
    }edges[maxn];
    
    int n,m,ebcnum;         //节点数目,无向边的数目,边_双连通分量的数目
    int e,head[maxn];
    int dfn[maxn];          //第一次访问的时间戳
    int dfs_clock;          //时间戳
    int isbridge[maxn];     //标记边是否为桥
    vector<int> ebc[maxn];  //边_双连通分量
    
    void addedges(int num,int u,int v)    //加边
    {
        edges[e].no = num;
        edges[e].v = v;
        edges[e].next = head[u];
        head[u] = e++;
        edges[e].no = num++;
        edges[e].v = u;
        edges[e].next = head[v];
        head[v] = e++;
    }
    
    int dfs_findbridge(int u,int fa)    //找出所有的桥
    {
        int lowu = dfn[u] = ++dfs_clock;
        for(int i=head[u];i!=-1;i=edges[i].next)
        {
            int v = edges[i].v;
            if(!dfn[v])
            {
                int lowv = dfs_findbridge(v,u);
                lowu = min(lowu,lowv);
                if(lowv > dfn[u])
                {
                    isbridge[edges[i].no] = 1; //
                }
            }
            else if(dfn[v] < dfn[u] && v != fa)
            {
                lowu = min(lowu,dfn[v]);
            }
        }
        return lowu;
    }
    
    void dfs_coutbridge(int u,int fa)     //保存边_双连通分量的信息
    {
        ebc[ebcnum].push_back(u);
        dfn[u] = ++dfs_clock;
        for(int i=head[u];i!=-1;i=edges[i].next)
        {
            int v = edges[i].v;
            if(!isbridge[edges[i].no] && !dfn[v]) dfs_coutbridge(v,u);
        }
    }
    
    void init()
    {
        memset(dfn,0,sizeof(dfn));
        memset(isbridge,0,sizeof(isbridge));
        memset(head,-1,sizeof(head));
        e = 0; ebcnum = 0;
    }
    
    int main()
    {
        int u,v;
        while(scanf("%d%d",&n,&m)!=EOF)
        {
            init();
            for(int i=0;i<m;i++)
            {
                scanf("%d%d",&u,&v);
                addedges(i,u,v);
            }
            dfs_findbridge(1,-1);
            memset(dfn,0,sizeof(dfn));
            for(int i=1;i<=n;i++)
            {
                if(!dfn[i])
                {
                    ebc[ebcnum].clear();
                    dfs_coutbridge(i,-1);
                    ebcnum++;
                }
            }
            for(int i=0;i<ebcnum;i++)
            {
                for(int j=0;j<ebc[i].size();j++)
                    cout<<ebc[i][j]<<" ";
                cout<<endl;
            }
        }
        return 0;
    }
    View Code

    三、点双连通分量和边双连通分量的区别和联系

      ①、二者都是基于无向图。

      ②、边双连通分量是删边后还连通,而后者是删点。

      ③、点双连通分量一定是边双连通分量(除两点一线的特殊情况),反之不一定。

      ④、点双连通分量可以有公共点,而边双连通分量不能有公共边。

     Tarjan离线算法求LCA

    思路dfs... 

    #include<bits/stdc++.h>
    
    using namespace std;
    const int N = 500005;
    struct EDGE{
        int next;
        int to;
        int lca;
    };
    EDGE edge[N];//树的链表
    EDGE qedge[N];//需要查询LCA的两节点的链表
    int n,m,p,x,y;
    int num_edge,num_qedge,head[N],qhead[N];
    int father[N];
    int visit[N];//判断是否被找过
    void add_edge(int from,int to){//建立树的链表
        edge[++num_edge].next=head[from];
        edge[num_edge].to=to;
        head[from]=num_edge;
    }
    void add_qedge(int from,int to){//建立需要查询LCA的两节点的链表
        qedge[++num_qedge].next=qhead[from];
        qedge[num_qedge].to=to;
        qhead[from]=num_qedge;
    }
    int fin(int z){//找爹函数
        if(father[z]!=z)
            father[z]=fin(father[z]);
        return father[z];
    }
    int dfs(int x){//把整棵树的一部分看作以节点x为根节点的小树
        father[x]=x;//由于节点x被看作是根节点,所以把x的father设为它自己
        visit[x]=1;//标记为已被搜索过
        for(int k=head[x];k;k=edge[k].next)//遍历所有与x相连的节点
            if(!visit[edge[k].to]){//若未被搜索
                dfs(edge[k].to);//以该节点为根节点搞小树
                father[edge[k].to]=x;//把x的孩子节点的father重新设为x
            }
        for(int k=qhead[x];k;k=qedge[k].next)//搜索包含节点x的所有询问
            if(visit[qedge[k].to]){//如果另一节点已被搜索过
                qedge[k].lca=fin(qedge[k].to);//把另一节点的祖先设为这两个节点的最近公共祖先
                if(k%2)//由于将每一组查询变为两组,所以2n-1和2n的结果是一样的
                    qedge[k+1].lca=qedge[k].lca;
                else
                    qedge[k-1].lca=qedge[k].lca;
            }
    }
    int main(){
        scanf("%d%d%d",&n,&m,&p);//输入节点数,查询数和根节点
        for(int i=1;i<n;++i){
            scanf("%d%d",&x,&y);//输入每条边
            add_edge(x,y);
            add_edge(y,x);
        }
        for(int i=1;i<=m;++i){
            scanf("%d%d",&x,&y);//输入每次查询,考虑(u,v)时若查找到u但v未被查找,所以将(u,v)(v,u)全部记录
            add_qedge(x,y);
            add_qedge(y,x);
        }
        dfs(p);//进入以p为根节点的树的深搜
        for(int i=1;i<=m;i++)
            printf("%d ",qedge[i*2].lca);//两者结果一样,只输出一组即可
        return 0;
    }
    View Code
  • 相关阅读:
    Git批量删除的方法
    第五课 森さんは 七時に 起きます
    第一课 李さんは  中国人です
    如何设置上传文件控件 input type="file" 的 默认值
    如何用程序提交一个上传文件的请求
    MVC Beta 做的网站实践总结(上)
    泛型单一模式
    提高web站点性能的最佳实践
    扩展GridView实现的一个自定义无刷新分页,排序,支持多种数据源的控件TwfGridView
    [导入]将字符串中连续的空格转换为一个空格
  • 原文地址:https://www.cnblogs.com/solvit/p/9636831.html
Copyright © 2011-2022 走看看