• 强连通分量与拓扑排序略解


    $ $强连通分量与拓扑排序

    拓扑排序

    $ $对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。

    (by 百度百科)

    $ $照个人理解,拓扑排序通常是在DAG图中寻找一个适合的解决问题的顺序。

    如何实现拓扑排序

    方法1:BFS(SPFA优化)

    1、先寻找入度为0的点,把它加入队列。

    2、搜寻队列,把队列的点G删去,则如果有点的入度有G点的话,入度- -,当发现又出现入度为0的点时,将该点加入队列。

    3、拓扑排序的结果为该队列,在执行删点操作的时候存储在一个数组及可。

    方法2:记忆化搜索

    大多数情况下,并不需要显式的拓扑排序

    考虑朴素的回溯算法

    若从一个给定的点出发,得到的结果是一样的

    因此对于每个点,计算完成后可以把结果保存起来,之后直接返回查表的结果即可

    拓扑排序伪代码(1):

    Topological_sort(G){
        统计图G中每个点的入度(可计算重边,但不可计算自环),记为degree[i]
        初始化queue和result为空的队列,并将所有degree为0的点加入queue
        while (!queue.empty()){
            u = queue.pop() // 队首
            result.push(u)
            for e 是u的出边(若上面计算了重边,这里也要算,与上面一致)
            v是e的指向的点
            degree[v]--
            if (degree[v] == 0) queue.push(v)
        }
    	return result
    }
    

    拓扑排序伪代码(2):

    calculate(u){
        if (u 已经搜索过) return table[u]
        ans = -inf
        for (v 是u的出边指向的点)
        ans = max(ans, value[u] + calculate(v))
        标记u已经搜索过
        table[u] = ans
        return ans
    }
    for (i 是G的所有节点)
    result = max(result, calculate(i))
    print(result)
    

    ps:源码在我讲完缩点后一起放出来

    强连通分量——缩点(有向有环图)

    $ $现在给出一个有向有环图,那么这个图不是一个DAG,所以不能在这种图上做拓扑排序或其他有关DAG的操作了。

    如果我们单独把1,2,3点提出来,把它们看做一个团。

    我们把这样一个“团点”叫做强连通分量(scc, strong connected component)

    通常来讲,一组互相能到达的点叫做连通分量

    当这个连通分量不能再大时,便是强连通分量

    求强连通分量

    把有向有环图抽象成一颗DFS树。

    那么每一个图上的圈圈就是一个强连通分量。在DFS树中,强连通分量一定长成这样子。

    那么问题就被化简成了确定每个强连通分量的根。

    Tarjan

    DFS时我们维护两个数组dfn,low

    dfn[i]是i点的进入时间

    low[i]是从i点出发,所能访问到的最早的进入时间

    Tarjan-scc伪码

    DFS(u)
      dfn[u] = low[u] = ++timer
      stack.push(u)
      state[u]=1 //已访问并入栈
      for v 是u的一条出边的端点
        if (state[v] == 0) //未访问
        DFS(v)
        low[u] = min(low[u],  low[v])
        if (state[v] == 1)
        low[u] = min(low[u], dfn[v])
      if (dfn[u] == low[u])
      	stack.pop() until 弹出了u //这些点构成一个强连通分量
      弹出的点的state[] = 2
      
    Tarjan_scc(G)
      timer = 0
      for u 是图G的节点
      if (state[u] == 0) DFS(u)
    

    那怎么找出一个 强连通分量的所有点

    找出scc之后,问题通常会变成两个部分

    1、scc内部

    2、scc之间,把每个scc看成一个点,则是DAG图

    新图怎么连边?

    记belong[u]为u所在的scc编号

    对于每条边u -> v

    若belong[u] != belong[v],则给新图加边 belong[u] -> belong[v]

    洛谷【P3387 缩点】

    缩点+拓扑排序+DP

    代码:

    #include<bits/stdc++.h>
    #define maxn 100001
    #define maxm 500001
    using namespace std;
    struct node{
        int to,next,from;
    }edge[maxm];
    queue <int> q;
    vector <int> cb[maxn];
    vector <int> rdr[maxn];
    int ans[maxn],totq,x,y,v,rd[maxn],u,n,m,sum,vis[maxn],dis_[maxn],dis[maxn];
    int dfn[maxn],low[maxn],f[maxn],times,cntqq;
    int stack_[maxn],heads[maxm],visit[maxn],cnt,tot,index_;
    void add(int x,int y)		//建边
    {
        edge[++cntqq].next=heads[x];
        edge[cntqq].from=x;
        edge[cntqq].to=y;
        heads[x]=cntqq;
       	return;
    }
    void tuopu()				//拓扑排序
    {
    	for(int i=1;i<=tot;i++)	//初始化 
        {
            if(rd[i]==0)
            q.push(i);			//入度为0的都进队列 
        }
        while(!q.empty())
        {
            int u=q.front();
            q.pop();
            ans[++totq]=u;
            for(int i=1;i<=cb[u].size();i++)
            {
                v=cb[u][i-1];	//因为vector是从0开始的,所以减1,下面代码的减1也一样 
                rd[v]--;
                if(rd[v]==0)q.push(v);
            }
        }
    }
    void tarjan(int x)			//tarjan求强连通分量
    {
        dfn[x]=low[x]=++times;
        stack_[++index_]=x;		//手写栈嘿嘿嘿 
        visit[x]=1;
       	for(int i=heads[x];i!=-1;i=edge[i].next)
        {
            if(!dfn[edge[i].to])
            {
               	tarjan(edge[i].to);
                low[x]=min(low[x],low[edge[i].to]);
           	}
           	else 
            if(visit[edge[i].to])
            low[x]=min(low[x],dfn[edge[i].to]);
        }
        if(low[x]==dfn[x])
       	{
       		tot++;//强连通分量编号 
       		while(1)
           	{
           		vis[stack_[index_]]=tot;	//index_所在的强连通分量编号,等于前面讲的belong 
           		dis_[tot]+=dis[stack_[index_]];	//强连通分量权值累加 
           		visit[stack_[index_]]=0;index_--;
           		if(x==stack_[index_+1])break;//手写栈嘿嘿嘿 
           	}
        }
    }
    int main(){
        memset(heads,-1,sizeof(heads));
        int n,m,x,y;
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++)
        scanf("%d",&dis[i]);
        for(int i=1;i<=m;i++){
            scanf("%d%d",&x,&y);
           	add(x,y);
        }
        for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i); 	//tarjan 
        for(int i=1;i<=cntqq;i++){			//拓扑建边
            if(vis[edge[i].from]!=vis[edge[i].to])
            {
                x=vis[edge[i].from];y=vis[edge[i].to];
    			rd[y]++;cb[x].push_back(y);rdr[y].push_back(x);
            }
        }
        tuopu();
        for(int i=1;i<=tot;i++)				//dp
        {
            int w=ans[i];
            f[w]=dis_[w];
            for(int j=1;j<=rdr[w].size();j++)
            f[w]=max(f[w],f[rdr[w][j-1]]+dis_[w]);
        }
        for(int i=1;i<=tot;i++)				//最后统计答案 
        sum=max(f[i],sum);
        printf("%d",sum); 
        return 0;
    }//刚刚好100行 
    
    
    

    无向图

    $ $现在问题又进一步升级了,有向图变成了无向图,那么完全不可能成为一个DAG了,所以,我们上面讨论的强连通分量等神奇东西在无向图是没有意义的,那么无向图有什么操作呢?

    $ $无向图一般讨论割点点双连通分量边双连通分量

    1、若删除一条边后该图不连通,则该边为

    2、若删除一个点后该图不连通,则该点为割点

    3、无割点的图称是点双连通的,极大的点双连通子图称为点双连通分量

    4、无桥的图称是边双连通的,极大的边双连通子图称为边双连通分量

    无向图的Tarjan算法

    核心仍然是求dfn和low

    主体与有向图类似,有两点注意

    节点只有两种状态:是否搜索过

    要特判是否父亲搜索过来的边

    无向图Tarjan伪码

    DFS(u)
        dfn[u] = low[u] = ++timer
        vis[u] = true
        for v 是u的一条出边(非父边)的端点
        if (!vis[v]) //未访问
            DFS(v)
            low[u] = min(low[u],  low[v])
            else
            low[u] = min(low[u], dfn[v])
    Tarjan(G)
        timer = 0
        for u 是图G的节点
        	if (!vis[u]) DFS(u)
    
    

    判断割点和桥

    割点

    存在儿子v,low[v] >= dfn[u],则u是割点

    根节点特判:若有两个或以上的儿子,则是割点

    回边不是桥

    对于树边,父亲记为u,儿子记为v,若low[v] > dfn[u],则该边是桥

    洛谷【P3388 割点】

    割点

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    struct edge{
        int next,to;
    }e[200010];
    int n,m,times,cnt,tot,a,b;
    int head[100010],dfn[100010],low[100010];
    bool vis[100010];
    void add(int x,int y)
    {
        e[++cnt].next=y;
        e[cnt].to=head[x];
        head[x]=cnt;
    }
    void tarjan(int u,int father)
    {
        dfn[u]=low[u]=++times;
        int son=0;
        for(int i=head[u];i!=0;i=e[i].to)
        {
            int v=e[i].next;
            if(!dfn[v])
            {
                tarjan(v,father);
                low[u]=min(low[u],low[v]);
                if(low[v]>=dfn[u]&&u!=father)
                vis[u]=1;
                if(u==father)
                son++;
            }
            low[u]=min (low[u],dfn[v]);
        }
        if(son>=2&&u==father)
        vis[u]=1;
    }
    int main()
    {
        scanf("%d%d",&n,&m);
        for(int i=1;i<=m;i++)
        {
            scanf("%d%d",&a,&b);
            add(a,b);add(b,a);
        }
        for(int i=1;i<=n;i++)
        if(dfn[i]==0)
        tarjan(i,i);
        for(int i=1;i<=n;i++)
        if(vis[i])
        tot++;
        printf("%d
    ",tot);
        for(int i=1;i<=n;i++)
        if(vis[i])
        printf("%d ",i);
        return 0;
    }
    

    后记

    我将这两个问题留给大家,如果实在有需要的可以私信我。

    question1:如何求点(边)双连通分量

    question2:如何记点(边)双连通分量

    随便给点题

    P2341 [HAOI2006]受欢迎的牛

    P2002 消息扩散

    P1262 间谍网络

    POJ 1236

    POJ 2186

    POJ 2762

    POJ 3687

    以上知识可以优化许多问题,希望大家掌握。

  • 相关阅读:
    golang goroutine协程泄露
    golang值传递和引用传递
    golang语法速记
    RabbitMQ管理界面介绍
    golang不定参数
    google Flutter入门
    golang context
    mysql ptarchiver
    8个顶级免费CRM客户关系管理系统
    未注册单拼域名——.top篇
  • 原文地址:https://www.cnblogs.com/hyfhaha/p/10678256.html
Copyright © 2020-2023  润新知