有向图的强连通分量

对于一个有向图有如下概念:

连通分量:对于分量中任意两点u,v,必然可以从u走到v,且从v走到u。

强连通分量:极大连通分量。(加上任一点后都不是连通分量)

强连通分量可以将任意一个有向图(通过缩点)转化成有向无环图(拓扑图,DAG)

缩点:将所有的强连通分量缩成一个点

DAG求最短路和最长路可以递推来做,时间复杂度是线性的

算法实现:

基于dfs,将所有的边分为四类:

树枝边(1):x是y的父节点

前向边(2):x是y的祖先节点

后向边(3):从子节点指向祖先节点

横叉边(4):往之前搜过的其他分支搜

判断一个点是否在强连通分量(scc)中:
情况1:通过后向边走到某一祖先节点
情况2:通过横叉边走到另一分支,再通过另一分支走到某一祖先节点

Tarjan算法求scc:
引入时间戳(按照dfs搜索顺序给每个点一个编号),具体实现:
对于每个点u,引入两个时间戳:
dfn[u]表示遍历到u的时间戳
low[u]表示从u开始遍历,所能遍历到的最小时间戳
如果u是所在强连通分量的最高点,那么dfn[u]==low[u]

代码模板:

void tarjan(int u)
{
    dfn[u]=low[u]=++timestamp;
    stk[++top]=u,in_stk[u]=1;//栈用来装当前正在遍历的强连通分量
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        if(!dfn[j])//当前点没有时间戳,证明之前没有搜到过
        {
            tarjan(j);
            low[u]=min(low[u],low[j]);
        }
        else if(in_stk[j])//当前点在栈中,证明和u同在一个强连通分量中,而且时间戳肯定小于u的时间戳,故而去更新low[u]
        {
            low[u]=min(low[u],dfn[j]);
        }
    }
    if(dfn[u]==low[u])
    {
        int y;
        ++scc_cnt;
        do{
            y=stk[top--];
            in_stk[y]=0;
            id[y]=scc_cnt;
        }while(y!=u);
    }
}

ps:最特殊的情况就是每个点自己是一个强连通分量。

时间复杂度:O(n+m)

然后缩点:

for i~n(遍历所有的点)

        for h[i]~-1(遍历i的所有邻边)

                if(i,j不在同一scc)

                        i->j(建边)

连通分量编号递减的顺序一定是拓扑序。按照上述的统计方式,在拓扑图中一个点指向另一个点的边不止一条,我们需要都记录下来。

1174. 受欢迎的牛(活动 - AcWing

 思路:这题由于喜爱关系是可以传递的,所以实际上可以用floyd算传递闭包,但是这里既然是scc的例题,那么我们就用tarjan算法来写。

如果a认为b受欢迎,那么我们就建立一条a指向b的边,那么如果得到得是一个有向无环图(DAG),答案取决于终点的数量,也即出度为0的点的数量。如果只有一个,那么肯定是这个点,如果有多个,那么这些终点之间没有指向关系,就不符合题意了。但是题目不能保证是有向无环图,所以我们可以通过计算强连通分量和缩点建立一张有向无环图。最后如果只有一个强连通分量终点,那么答案就是这个强连通分量中点的个数,我们可以在tarjan计算的时候统计一下个数。

这题其实还有一个比较特殊的点,我们只需要找终点,所以实际不用真的建图,只要统计每个分量的出度即可。

#include<bits/stdc++.h>
using namespace std;
const int N=10010,M=100010;
int n,m;
int h[N],e[M],ne[M],idx;
int scc;
int id[N],stk[N],sz[N],st[N];
int dfn[N],low[N];
int cd[N];
int tp;
int top;
void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void tarjan(int u)
{
    dfn[u]=low[u]=++tp;
    stk[++top]=u,st[u]=1;
    for(int i=h[u];i!=-1;i=ne[i])
    {
        int j=e[i];
        if(!dfn[j])
        {
            tarjan(j);
            low[u]=min(low[u],low[j]);
        }
        else if(st[j])
        {
            low[u]=min(low[u],dfn[j]);
        }
    }
    if(low[u]==dfn[u])
    {
        int y;
        ++scc;
        do{
            y=stk[top--];
            st[y]=0;
            id[y]=scc;
            sz[scc]++;
        }while(y!=u);
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof h);
    for(int i=1;i<=m;i++)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        add(a,b);
    }
    for(int i=1;i<=n;i++)//整个图可能并不连通
        if(!dfn[i])
            tarjan(i);
            
    for(int i=1;i<=n;i++)
    {
        for(int j=h[i];j!=-1;j=ne[j])
        {
            int z=e[j];
            if(id[i]!=id[z]) cd[id[i]]++;
        }
    }
    int z=0,sum=0;
    for(int i=1;i<=scc;i++)
    {
        if(!cd[i])
        {
            z++;
            sum += sz[i];
        }
    }
    if(z==1) printf("%d",sum);
    else printf("0");
}

367. 学校网络(367. 学校网络 - AcWing题库) 

这里我们先看第一问,需要给几个学校才能使得每个学校都有,既然是有向图,那么我们就来考虑最特殊的拓扑图,反正非拓扑图也能通过缩点转化成有向图。显然对于一个拓扑图,我们只要给所有的起点,那么就可以传到所有的终点去。所以对于第一问,我们只需要把原图转化成拓扑图,然后统计起点个数即可。对于第二问,最少添加几条边可以使得我们提供给任何一个学校,其他学校都可以获得,我们还是按照拓扑图来看。

 

我们令起点各个数为怕p,终点的个数为q。我们先证p<=q的时候的情况,大于等于的时候类似:

如果p==1,那么我们只用从所有的终点连一条指向起点的边即可,因为起点可以到任何一个点

如果p>1,那么q>=p>=1,必然可以找到两个不同的起点p1,p2,可以走到两个不同的终点q1,q2,如下图:

 我们可以在去q1和p2之间连一条边:

那么p2和q1就变成中间节点了,可以从起点和终点的集合中去掉,那么大小同时减1,然后可以去p-1次,变成p==1的情况,那么还需要加q-(p-1)条边,那么总共就加了q条边。

所以是max(p,q). 

即使对于这种图也没关系,我们可以条边1和边6,同样满足上述条件,从不同的起点到不同的终点,那么去掉后还剩一个起点和两个终点,再从两个终点向起点连两条有向边即可。

#include<bits/stdc++.h>
using namespace std;
const int N=120,M=10010;
int n;
int h[N],e[M],ne[M],idx;
int dfn[N],low[N];
int stk[N],top,scc,id[N],tp,st[N];
int cd[N],rd[N];
void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void tarjan(int u)
{
    dfn[u]=low[u]=++tp;
    stk[++top]=u,st[u]=1;
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        if(!dfn[j])
        {
            tarjan(j);
            low[u]=min(low[u],low[j]);
        }
        else if(st[j]) low[u]=min(low[u],dfn[j]);
    }
    if(dfn[u]==low[u])
    {
        int y;
        scc++;
        do{
            y=stk[top--];
            st[y]=0;
            id[y]=scc;
        }while(y!=u);
    }
}
int main()
{
    scanf("%d",&n);
    memset(h,-1,sizeof h);
    for(int i=1;i<=n;i++)
    {
        int j;
        while(~scanf("%d",&j))
        {
            if(!j) break;
            add(i,j);
        }
    }
    for(int i=1;i<=n;i++)
    {
        if(!dfn[i]) tarjan(i);
    }
    for(int u=1;u<=n;u++)
    {
        for(int i=h[u];~i;i=ne[i])
        {
            int j=e[i];
            if(id[u]!=id[j])
            {
                cd[id[u]]++,rd[id[j]]++;
            }
        }
    }
    int p=0,q=0;
    for(int i=1;i<=scc;i++)
    {
        if(!cd[i]) q++;
        if(!rd[i]) p++;
    }
    printf("%d\n",p);
    if(scc==1) printf("0");//这里需要特判,如果只有一个强连通分量的话,那么自然不需要再加边
    else printf("%d",max(p,q));
}

 1175. 最大半连通子图(活动 - AcWing

我们之前求的都是强连通分量,u可以到v,那么v也可以到u。这里要求一个半连通分量,显然对于一个强连通分量来说去它的子集一定是半连通分量,取整个强连通分量也一定是半连通分量,所以我们不如取整个强连通分量。然后我们把原图缩点就可以得到一个拓扑图了,对于拓扑图中的一条链,显然它也是半连通分量,不过不能有分叉。那么答案就出来了,先求所有的强连通分量,然后缩点得到一个拓扑图,对于拓扑图,我们可以通过dp来统计答案。

#include<bits/stdc++.h>
using namespace std;
const int N=100010,M=2000010;
int n,m,mod;
int h[N],hs[N],e[M],ne[M],idx;
int dfn[N],low[N],stk[N],st[N],id[N],sz[N],top,scc,tp;
int f[N],g[N];
void add(int h[],int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void tarjan(int u)
{
    dfn[u]=low[u]=++tp;
    stk[++top]=u,st[u]=1;
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        if(!dfn[j])
        {
            tarjan(j);
            low[u]=min(low[u],low[j]);
        }
        else if(st[j]) low[u]=min(low[u],dfn[j]);
    }
    if(low[u]==dfn[u])
    {
        int y;
        scc++;
        do{
            y=stk[top--];
            id[y]=scc;
            sz[scc]++;
            st[y]=0;
        }while(u!=y);
    }
}
int main()
{
    scanf("%d%d%d",&n,&m,&mod);
    memset(h,-1,sizeof h);
    for(int i=1;i<=m;i++)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        add(h,a,b);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i);
    unordered_set<long long>s;
    memset(hs,-1,sizeof hs);
    for(int u=1;u<=n;u++)
    {
        for(int i=h[u];~i;i=ne[i])
        {
            int j=e[i];
            int a=id[u],b=id[j];
            long long tmp=a*100000ll+b;
            if(a!=b&&!s.count(tmp))//两个条件都不能少
            {
                add(hs,a,b);
                s.insert(tmp);
            }
        }
    }
    for(int i=scc;i>=1;i--)
    {
        if(!f[i])
        {
            f[i]=sz[i];
            g[i]=1;//方案数
        }
        for(int j=hs[i];~j;j=ne[j])
        {
            int k=e[j];
            if(f[k]<f[i]+sz[k])
            {
                f[k]=f[i]+sz[k];
                g[k] = g[i];
            }
            else if(f[k]==f[i]+sz[k]) g[k]=(g[k]+g[i])%mod;
        }
    }
    int mx=0,sum=0;
    for(int i=1;i<=scc;i++)
    {
        if(f[i]>mx) 
        {
            mx=f[i];
            sum=g[i];
        }
        else if(f[i]==mx) sum = (sum+g[i])%mod;
    }
    printf("%d\n%d",mx,sum);
}

368. 银河(368. 银河 - AcWing题库

思路:这题跟糖果那道题几乎一摸一样(详见spfa的特殊用法-CSDN博客) ,但是这里我们还是用强连通分量来写。

我们建边的时候就跟糖果那道题一样建边,如果a>=b,那么就建一条b指向a,长度为0的边,如果a>b,那么就建一条b指向a,长度为1的边,然后计算强连通分量,再缩点成有向图,起点的亮度是1,递推求总和。另外,一个强连通分量的各个点亮度应该是相同的,所以所有的边权都应该是0,否则就不成立,输出-1,我们在缩点的时候记录一下

还有,这里因为最暗的边为1,同时为了方便更新,我们把0点放进去。那么第二次建边的时候就一定要记得把0点算上。

#include<bits/stdc++.h>
using namespace std;
const int N=100010,M=400010;
int n,m;
int h[N],hs[N],ne[M],e[M],w[M],idx;
int dfn[N],low[N],id[N],st[N],stk[N],sz[N],top,scc,tp;
int f[N];
void add(int h[],int a,int b,int c)
{
    w[idx]=c,e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void tarjan(int u)
{
    dfn[u]=low[u]=++tp;
    stk[++top]=u,st[u]=1;
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        if(!dfn[j])
        {
            tarjan(j);
            low[u]=min(low[u],low[j]);
        }
        else if(st[j]) low[u]=min(low[u],dfn[j]);
    }
    if(low[u]==dfn[u])
    {
        int y;
        ++scc;
        do{
            y=stk[top--];
            id[y]=scc;
            st[y]=0;
            sz[scc]++;
        }while(y!=u);
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof h);
    for(int i=1;i<=n;i++) add(h,0,i,1);
    for(int i=1;i<=m;i++)
    {
        int t,a,b;
        scanf("%d%d%d",&t,&a,&b);
        if(t==1) add(h,a,b,0),add(h,b,a,0);
        else if(t==2) add(h,a,b,1);
        else if(t==3) add(h,b,a,0);
        else if(t==4) add(h,b,a,1);
        else add(h,a,b,0);
    }
   tarjan(0);
    int flag=1;
    memset(hs,-1,sizeof hs);
    for(int i=0;i<=n;i++)//把0点算进去
    {
        for(int j=h[i];~j;j=ne[j])
        {
            int k=e[j];
            int a=id[i],b=id[k];
            if(a==b)
            {
                if(w[j]) 
                {
                    flag=0;
                    break;
                }
            }
            else add(hs,a,b,w[j]);
        }
        if(!flag) break;
    }
    if(!flag) printf("-1");
    else 
    {
        for(int i=scc;i>=1;i--)
        {
            for(int j=hs[i];~j;j=ne[j])
            {
                int k=e[j];
                f[k]=max(f[k],f[i]+w[j]);
            }
        }
        long long res=0;
        for(int i=1;i<=scc;i++) res += sz[i]*f[i];
        cout<<res;
    }
}

糖果那道题的时间复杂度实际有点紧,但是这里就能保证时间复杂度是线性的。 

  • 18
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值