NOIP专题复习3 图论-强连通分量

栏目: 数据库 · 发布时间: 7年前

内容简介:在前两节内容中,大家应该已经大致了解了图论这一块的算法。今天我们要讲的是强连通分量。强连通分量是一个再OI中非常重要的算法,我们需要对强联通分量进行缩点,使得一个大的有向图变成一个有向无环图。说了这么多,我们切入今天要讲的正题。在有向图G中,如果有两个顶点存在路径均可互达,就说这两个点强连通。如果在有向图上任意两点均强连通,我们则称图G为强连通图。

一、知识概述

在前两节内容中,大家应该已经大致了解了图论这一块的算法。今天我们要讲的是强连通分量。强连通分量是一个再OI中非常重要的算法,我们需要对强联通分量进行缩点,使得一个大的有向图变成一个有向无环图。

说了这么多,我们切入今天要讲的正题。

1、强连通&强连通图

在有向图G中,如果有两个顶点存在路径均可互达,就说这两个点强连通。如果在有向图上任意两点均强连通,我们则称图G为强连通图。

2、极大强连通子图&强连通分量

极大强连通子图:在有向图中最大(不能再大)的强连通图。

而强连通分量就是有向非强连通图中的极大强连通子图。

3、举个栗子

下图中,子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。

NOIP专题复习3 图论-强连通分量

二、典型例题

1、[HAOI2006]受欢迎的牛

题目描述

每头奶牛都梦想成为牛棚里的明星。被所有奶牛喜欢的奶牛就是一头明星奶牛。所有奶牛都是自恋狂,每头奶牛总是喜欢自己的。奶牛之间的“喜欢”是可以传递的——如果A喜欢B,B喜欢C,那么A也喜欢C。牛栏里共有$N$头奶牛,给定一些奶牛之间的爱慕关系,请你算出有多少头奶牛可以当明星。

输入输出格式

输入格式:

第一行:两个用空格分开的整数:$N$和$M$

第二行到第$M + 1$行:每行两个用空格分开的整数:A和B,表示A喜欢B

输出格式:

第一行:单独一个整数,表示明星奶牛的数量

输入输出样例

输入样例#1:

3 3
1 2
2 1
2 3
 

输出样例#1:

1

说明

只有 3 号奶牛可以做明星

【数据范围】

10%的数据$N<=20$, $M<=50$

30%的数据$N<=1000$,$M<=20000$

70%的数据$N<=5000,M<=50000$

100%的数据$N<=10000,M<=50000$

2、校园网络【[USACO]Network of Schools加强版】

题目描述

一些学校连入一个电脑网络。那些学校已订立了协议:每个学校都会给其它的一些学校分发软件(称作“接受学校”)。注意即使 B 在 A 学校的分发列表中, A 也不一定在 B 学校的列表中。

你要写一个程序计算,根据协议,为了让网络中所有的学校都用上新软件,必须接受新软件副本的最少学校数目(子任务 A)。更进一步,我们想要确定通过给任意一个学校发送新软件,这个软件就会分发到网络中的所有学校。为了完成这个任务,我们可能必须扩展接收学校列表,使其加入新成员。计算最少需要增加几个扩展,使得不论我们给哪个学校发送新软件,它都会到达其余所有的学校(子任务 B)。一个扩展就是在一个学校的接收学校列表中引入一个新成员。

输入输出格式

输入格式:

输入文件的第一行包括一个整数 N:网络中的学校数目($2 <= N <= 10000$)。学校用前 N 个正整数标识。

接下来 $N$ 行中每行都表示一个接收学校列表(分发列表)。第 $i+1$ 行包括学校 $i$ 的接收学校的标识符。每个列表用 0 结束。空列表只用一个 0 表示。

输出格式:

你的程序应该在输出文件中输出两行。

第一行应该包括一个正整数:子任务 A 的解。

第二行应该包括子任务 B 的解。

输入输出样例

输入样例#1:

5
2 4 3 0
4 5 0
0
0
1 0
 

输出样例#1:

1
2
 

说明

poj题目。$2<=n<=10000$.

三、算法分析

直接根据定义,用双向遍历取交集的方法求强连通分量,时间复杂度为$O(N^2+M)$。更好的方法是Kosaraju算法或Tarjan算法,两者的时间复杂度都是$O(N+M)$。本文介绍的是Tarjan算法。

(一)Tarjan算法

Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。

简介

NOIP专题复习3 图论-强连通分量 NOIP专题复习3 图论-强连通分量 定义DFN(u)为节点u搜索的次序编号(时间戳),Low(u)为u或u的子树能够追溯到的最早的栈中节点的次序号。由定义可以得出,

Low(u)=Min
{
    DFN(u),
    Low(v),(u,v)为树枝边,u为v的父节点
    DFN(v),(u,v)为指向栈中节点的后向边(非横叉边)
}
 

当DFN(u)=Low(u)时,以u为根的搜索子树上所有节点是一个强连通分量。

算法伪代码如下

tarjan(u)
{
    DFN[u]=Low[u]=++Index                      为节点u设定次序编号和Low初值
    Stack.push(u)                               将节点u压入栈中
    for each (u, v) in E                        枚举每一条边
        if (v is not visted)                如果节点v未被访问过
            tarjan(v)                   继续向下找
            Low[u] = min(Low[u], Low[v])
        else if (v in S)                    如果节点v还在栈内
            Low[u] = min(Low[u], DFN[v])
    if (DFN[u] == Low[u])                       如果节点u是强连通分量的根
        repeat
            v = S.pop                   将v退栈,为该强连通分量中一个顶点
            print v
        until (u== v)
}
 

接下来是对算法流程的演示。

从节点1开始DFS,把遍历到的节点加入栈中。搜索到节点u=6时,DFN[6]=LOW[6],找到了一个强连通分量。退栈到u=v为止,{6}为一个强连通分量。

NOIP专题复习3 图论-强连通分量

返回节点5,发现DFN[5]=LOW[5],退栈后{5}为一个强连通分量。

NOIP专题复习3 图论-强连通分量

返回节点3,继续搜索到节点4,把4加入堆栈。发现节点4向节点1有后向边,节点1还在栈中,所以LOW[4]=1。节点6已经出栈,(4,6)是横叉边,返回3,(3,4)为树枝边,所以LOW[3]=LOW[4]=1。

NOIP专题复习3 图论-强连通分量

继续回到节点1,最后访问节点2。访问边(2,4),4还在栈中,所以LOW[2]=DFN[4]=5。返回1后,发现DFN[1]=LOW[1],把栈中节点全部取出,组成一个连通分量{1,3,4,2}。

NOIP专题复习3 图论-强连通分量

至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},{5},{6}。

可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(N+M)。

求有向图的强连通分量还有一个强有力的算法,为Kosaraju算法。Kosaraju是基于对有向图及其逆图两次DFS的方法,其时间复杂度也是O(N+M)。与Trajan算法相比,Kosaraju算法可能会稍微更直观一些。但是Tarjan只用对原图进行一次DFS,不用建立逆图,更简洁。在实际的测试中,Tarjan算法的运行效率也比Kosaraju算法高30%左右。此外,该Tarjan算法与求无向图的双连通分量(割点、桥)的Tarjan算法也有着很深的联系。学习该Tarjan算法,也有助于深入理解求双连通分量的Tarjan算法,两者可以类比、组合理解。

求有向图的强连通分量的Tarjan算法是以其发明者Robert Tarjan命名的。Robert Tarjan还发明了求双连通分量的Tarjan算法,以及求最近公共祖先的离线Tarjan算法,在此对Tarjan表示崇高的敬意。

附:tarjan算法的C++程序

void tarjan(int i)
{
    int j;
    DFN[i]=LOW[i]=++Dindex;
    instack[i]=true;
    Stap[++Stop]=i;
    for (edge *e=V[i];e;e=e->next)
    {
        j=e->t;
        if (!DFN[j])
        {
            tarjan(j);
            if (LOW[j]<LOW[i])
                LOW[i]=LOW[j];
        }
        else if (instack[j] && DFN[j]<LOW[i])
            LOW[i]=DFN[j];
    }
    if (DFN[i]==LOW[i])
    {
        Bcnt++;
        do
        {
            j=Stap[Stop--];
            instack[j]=false;
            Belong[j]=Bcnt;
        }
        while (j!=i);
    }
}
void solve()
{
    int i;
    Stop=Bcnt=Dindex=0;
    memset(DFN,0,sizeof(DFN));
    for (i=1;i<=N;i++)
        if (!DFN[i])
            tarjan(i);
}
 

以下是fstqwq写的

void dfs(int x) {
    dfn[x] = low[x] = ++cnt, sta[++top] = x, vis[x] = 1;
    for (int now = head[x]; now; now = e[now].next) {
        if (!dfn[e[now].to]) {
            dfs(e[now].to);
            low[x] = min(low[x], low[e[now].to]);
        }
        else if (vis[e[now].to]) low[x] = min(low[x], low[e[now].to]);
    }
    if (dfn[x] == low[x]) {
        siz[++scc] = 0;
        do {
            vis[sta[top]] = 0;
            bel[sta[top]] = scc;
            siz[scc]++;
        } while (sta[top--] != x);
    }
}
 

(二)解决问题

例题1、[HAOI2006]受欢迎的牛

这就是一道tarjan的模板题,前面讲的够清楚了吧。注释什么的就不放了。

本题只要把互相喜欢的牛缩成一点。最后统计出度为0的即为受欢迎的牛。如果存在多个点出度为0,则没有受欢迎的牛。

\#include<bits/stdc++.h>
using namespace std;
int tot,head[100005],Next[100005],low[100005],vis[100005],bel[100005],siz[100005],dfn[100005],sta[100005],du[100005];
int to[100005];
int n,m,scc,cnt,top;
void addage(int x,int y)
{
    tot++;
    to[tot]=y;
    Next[tot]=head[x];
    head[x]=tot;
}
void dfs(int x)
{
    dfn[x]=low[x]=++cnt;
    sta[++top]=x;
    vis[x]=1;
    for (int i=head[x];i;i=Next[i])
    {
        if (!dfn[to[i]])
        {
            dfs(to[i]);
            low[x]=min(low[to[i]],low[x]);
        }
        else if (vis[to[i]]) low[x]=min(dfn[to[i]],low[x]);
    }
    if (dfn[x]==low[x])
    {
        siz[++scc]=0;
        do
        {
            vis[sta[top]]=0;
            bel[sta[top]]=scc;
            siz[scc]++;
        }
        while (sta[top--]!=x);
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    for (int i=1;i<=m;i++)
    {
        int x,y=0;
        scanf("%d%d",&x,&y);
        addage(x,y);
    }
    for (int i=1;i<=n;i++)
    if (!dfn[i]) dfs(i);
 
    for (int j=1;j<=n;j++)
    {
        for (int i=head[j];i;i=Next[i])
        {
            int u=to[i];
            if (bel[j]!=bel[u])
            du[bel[j]]++;
        }
    }
    int tt=0;
    for(int i=1;i<=scc;i++)
    if(!du[i])
    {
        if(tt)
        {
            puts("0");
            return 0;
        }
        tt=i;
    }
    printf("%d\n",siz[tt]);
    return 0;
}
 

例题2、校园网络【[USACO]Network of Schools加强版】

NOIP专题复习3 图论-强连通分量

* 本题需注意之处:当整张图都为连通图时,不需要再添加路线 *

如数据:

input

10
2 0
3 0
4 0
5 0
6 0
7 0
8 0
9 0
10 0
1 0
 

output

1

code:

\#include<bits/stdc++.h>
using namespace std;
int tot,head[100005],Next[100005],low[100005],vis[100005],bel[100005],siz[100005],dfn[100005],sta[100005],chudu[100005],rudu[100005];
int to[100005];
int n,m,scc,cnt,top;
void add(int x,int y)
{
    tot++;
    to[tot]=y;
    Next[tot]=head[x];
    head[x]=tot;
}
void dfs(int x)
{
    dfn[x]=low[x]=++cnt;
    sta[++top]=x;
    vis[x]=1;
    for (int i=head[x];i;i=Next[i])
    {
        if (!dfn[to[i]])
        {
            dfs(to[i]);
            low[x]=min(low[x],low[to[i]]);
        } else
        if (vis[to[i]]) low[x]=min(low[x],dfn[to[i]]);
    }
    if (dfn[x]==low[x])
    {
        siz[++scc]=0;
        do
        {
            vis[sta[top]]=0;
            bel[sta[top]]=scc;
            siz[scc]++;
        }while (sta[top--]!=x);
    }
}
int main()
{
    scanf("%d",&n);
    for (int i=1;i<=n;i++)
    {
        int x,y=0;
        do
        {
            scanf("%d",&x);
            if (x!=0) 
                add(i,x);
        }while (x!=0);
    }
    for (int i=1;i<=n;i++)
    if (!dfn[i]) dfs(i);
    for (int j=1;j<=n;j++)
    {
        for (int i=head[j];i;i=Next[i])
        {
            int u=to[i];
            if (bel[j]!=bel[u]) 
            chudu[bel[j]]++;
        }
    }
    for (int j=1;j<=n;j++)
    {
        for (int i=head[j];i;i=Next[i])
        {
            int u=to[i];
            if (bel[j]!=bel[u]) 
                rudu[bel[u]]++;
        }
    }
    int ans1=0; int ans2=0;
    for (int i=1;i<=scc;i++) //注意:这里一定要填scc。因为是tarjan缩点之后的点,而不是一开始的点
    {
        if (chudu[i]==0) ans1++;
        if (rudu[i]==0) ans2++;
    }
    bool p=false;
    for (int i=2;i<=n;i++)
    if (bel[i]!=bel[i-1]) p=true;//当整张图都为连通图时,不需要再添加路线 
    printf("%d\n",ans2);
    if (p==false) puts("0"); else//当整张图都为连通图时,不需要再添加路线
    printf("%d\n",max(ans1,ans2));
    return 0;
}
 

四、课后习题

  • NOIP2015 信息传递
  • NOIP2009 最优贸易

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Host Your Web Site In The Cloud

Host Your Web Site In The Cloud

Jeff Barr / SitePoint / 2010-9-28 / USD 39.95

Host Your Web Site On The Cloud is the OFFICIAL step-by-step guide to this revolutionary approach to hosting and managing your websites and applications, authored by Amazon's very own Jeffrey Barr. "H......一起来看看 《Host Your Web Site In The Cloud》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具