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

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

拓扑排序

$ ??????$对一个有向无环图(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\n",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

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

原文地址:https://www.cnblogs.com/hyfhaha/p/10678256.html

时间: 2024-10-08 06:27:53

强连通分量与拓扑排序略解的相关文章

POJ 2762判断单联通(强连通缩点+拓扑排序)

Going from u to v or from v to u? Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 14789   Accepted: 3915 Description In order to make their sons brave, Jiajia and Wind take them to a big cave. The cave has n rooms, and one-way corridors

POJ2762 Going from u to v or from v to u?(强连通缩点+拓扑排序)

Going from u to v or from v to u? Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 15196   Accepted: 4013 Description In order to make their sons brave, Jiajia and Wind take them to a big cave. The cave has n rooms, and one-way corridors

图论-拓扑排序详解

拓扑排序(topsort)详解 这篇随笔就信息学奥林匹克竞赛中图论的一个知识点--拓扑排序进行讲解.拓扑排序的内容比较基础,只要求读者学习过并了解信息学中图的相关定义和一些专业名词,但是拓扑排序的变形题目比较多,希望读者在看完本随笔后认真体会练习,掌握拓扑排序. 上课! 拓扑排序的定义 顾名思义,这是一种排序,确切地说,是一种图上排序,在一张有向无环图(注解:有向无环图即很多参考书和题解中所说的DAG)上进行排序,把其中的所有节点排成一个序列,使得图中的任意一对有边相连的节点(u,v)u要出现在

转:【拓扑排序详解】+【模板】

转自:http://www.cnblogs.com/skywang12345/p/3711489.html 拓扑排序介绍 拓扑排序(Topological Order)是指,将一个有向无环图(Directed Acyclic Graph简称DAG)进行排序进而得到一个有序的线性序列. 这样说,可能理解起来比较抽象.下面通过简单的例子进行说明! 例如,一个项目包括A.B.C.D四个子部分来完成,并且A依赖于B和D,C依赖于D.现在要制定一个计划,写出A.B.C.D的执行顺序.这时,就可以利用到拓扑

拓扑排序详解与实现

目录 介绍 拓扑排序算法分析 拓扑排序代码实现 @(目录) 介绍 拓扑排序,很多人都可能听说但是不了解的一种算法.或许很多人只知道它是图论的一种排序,至于干什么的不清楚.又或许很多人可能还会认为它是一种啥排序.而实质上它是对有向图的顶点排成一个线性序列. 至于定义,百科上是这么说的: 对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前.通常

拓扑排序详解

拓扑排序看起来很难,其实了解后不算难(思想非常清楚) 关键掌握思想后需要学会应用到具体的题目中去.(从入度为0到出度为0) 1.拓扑排序的介绍 对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前.     拓扑排序对应施工的流程图具有特别重要的作用,它可以决定哪些子工程必须要先执行,哪些子工程要在某些工程执行后才可以执行.为了形象地反映出整个

USACO network of school 强连通分量

这个题的意思是有一个有向图, 每个顶点可以发送软件到与其相连的顶点上, 现在问1,至少发送给几个顶点能满足所有顶点都收到软件, 2:如果想让这个图变成强连通图,至少添几条边.  特例是给定的图是一个强连通图的话答案是1, 0. 一般情况下我们先将这个图的强连通分量求出来缩成一个点然后统计入度为0的点和出度为0的点的个数, 答案一就是入度为0的点的个数, 答案就是他们两个之间的最大值.代码如下: /* ID: m1500293 LANG: C++ PROG: schlnet */ #include

强连通分量算法模板

const int max_v = 120; struct Scc { int V; //图的顶点数 vector<int> G[max_v]; //原始图 vector<int> rG[max_v]; //反向边的图 vector<int> vs; //后序遍历顶点列表 bool used[max_v]; //访问标记 int cmp[max_v]; //所属强连通分量的拓扑排序 void init() { for(int i=0; i<=V; i++) G[i

ZOJ_3795 Grouping(强连通分量 拓扑)

题目请点我 题解: 这是我的第一道强连通分量,虽然参考了别人的代码,还是很有收获.强连通分量的查找和处理是很多图论题目的第一步,所以还是很重要,这道题只是有向图的强连通处理. 这道题因为题目有讲每组关系都是不小于,那么如果出现环的话那只有一种情况,就是环上所有人都是相等的年龄,则这个环上所有的人的比较关系都是可以等价的,这就是为什么我们要先对强连通分量尽行缩点处理,将每一个强连通分量作为一个整体,之后再依据层次关系构造拓扑序,利用拓扑序找到最长的一条序列. 算法讲解 代码参考 测试数据: 4 4