强连通分量例题

本文探讨了强连通分量的概念及其在解决图论问题中的应用,包括明星奶牛问题、差分约束系统和拓扑排序。通过实例解析,展示了如何利用DAG图的强连通分量进行问题建模,并利用tarjan算法求解。同时,介绍了如何处理图中环的问题,以及如何统计特定类型的边对图的影响。文章还涵盖了在某些条件下确保图成为强连通分量所需的最少边数。
摘要由CSDN通过智能技术生成

强连通分量例题

带明星

题意:

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

输入:

第一行:两个用空格分开的整数:NM

接下来 M 行:每行两个用空格分开的整数:AB,表示 A 喜欢 B

$ 1≤N≤10^4, 1≤M≤5×10^4 $

输出:

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

分析:

我们先不考虑带环的情况,只有当一头奶牛的前驱总和为 n − 1 n-1 n1 时,才能被称之为明星奶牛。很自然的会想到拓扑,然后就很自然的超时了(其实普通拓扑没事,但这题要缩点,复杂度就要接近 n 2 n^2 n2 了)。

有什么东西是等价于前驱总和为 n − 1 n-1 n1 呢?因为没有一个点是单独不连边不出边的,所以出度数为0就相当于前驱总和为 n − 1 n-1 n1 (前面的爱意都传给这头牛了)。同时,出度数为0的个数当今仅当为1是才能有带明星,如果大于1的话,出度数为0的奶牛之间的爱意无法互相传达。

现在再考虑带环的情况,我们只需要维护出一个由强连通分量构成的图,每次维护单独一个强连通分量时通过遍历它的元素的连边情况统计这个强连通分量的出度数。

实现:

#include<bits/stdc++.h>
using namespace std;
#define MAXN 10005
#define MAXM 50005
#define ll long long
ll n,m;
ll dp[MAXN];

struct EDGE
{
    int to,next;
}edge[MAXM];
int head[MAXM],tot;
void add_edge1(int from,int to)
{
    edge[++tot].to=to;edge[tot].next=head[from];head[from]=tot;
}


int visit[MAXN],dfn[MAXN],scc,num,low[MAXN],key[MAXN],sccc[MAXN];
stack <int> st;
int outdgr[MAXN];

void tarjan(int x)
{
    low[x]=dfn[x]=++num;
    visit[x]=1;st.push(x);
    for(int i=head[x];i;i=edge[i].next)
    {
        int y=edge[i].to;
        if(dfn[y]==0)
        {
            tarjan(y);
            low[x]=min(low[x],low[y]);
        }
        else if(visit[y]==1)
        {
            low[x]=min(low[x],dfn[y]);
        }
    }
    if(dfn[x]==low[x])
    {
        scc++;
        int now=-1;
        vector <int> v;
        while(now!=x)
        {
            now=st.top();st.pop();
            visit[now]=0;
            key[now]=scc;
            v.emplace_back(now);
            sccc[scc]++;
        }
        for(auto x:v)
        {
            for(int i=head[x];i;i=edge[i].next)
            {
                int y=edge[i].to;
                if(key[y]!=key[x]) outdgr[key[x]]++;
            }
        }
    }
}

void output()
{
    int ans=0;
    int cnt=0;
    for(int i=1;i<=scc;i++)
    {
        if(outdgr[i]==0)
        {
            cnt++;
            ans=sccc[i];
        }
    }
    if(cnt==1) cout<<ans<<endl;
    else cout<<0<<endl;
}

void solve()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) key[i]=i;
    for(int i=1;i<=m;i++)
    {
        int u,v;
        cin>>u>>v;
        add_edge1(u,v);
    }
    for(int i=1;i<=n;i++)
        if(dfn[i]==0)
            tarjan(i);
    output();
}

int main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

校园网

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7VLilki2-1637498596124)(C:\Users\ADguy\AppData\Roaming\Typora\typora-user-images\1637460866538.png)]

结论一:

对于DAG图而言,通过所有入度数为 0 0 0 的点即可遍历到所有的点。

反证:如果存在一个不能被遍历到的点,说明它的入度为 0 0 0 ,而我们是将该点作为起始点push进队列的,所以这种情况不可能发生。

所以对于第一问,只要统计出强联通分量缩点后入度为 0 0 0 的点的个数即可。

第二问,加边后任意学校都可以传给所有学校,显然是一个强连通分量。

这里有一个奇奇怪怪的结论,证明看不懂,链接摆在这

结论二:

对于一个DAG图,至少需要加 m a x ( 入 度 数 为 0 的 点 的 个 数 , 出 度 数 为 0 的 点 的 个 数 ) max(入度数为0的点的个数,出度数为0的点的个数) max(00) 条边,就能使得这张图成为强连通分量

特判一下整张图缩点后是不是只有一个点(原图就是强连通分量),如果是输出 0 0 0

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MAXN 105
#define MAXM MAXN*MAXN
typedef pair<int,int> pii;
#define INF 0x3f3f3f3f
int n;
int low[MAXN],dfn[MAXN],vis[MAXN],key[MAXN],sccc,num;//sccc为强联通分量个数,num为时间戳
int head[MAXN];int tot;
int indgr[MAXN],oudgr[MAXN];
stack <int> st;
struct EDGE
{
    int to,next;
}edge[MAXM];
void add_edge(int from,int to)
{
    edge[++tot].to=to;edge[tot].next=head[from];head[from]=tot;
}
void tarjan(int x)
{
    low[x]=dfn[x]=++num;
    st.push(x);vis[x]=1;
    for(int i=head[x];i;i=edge[i].next)
    {
        int y=edge[i].to;
        if(!dfn[y])
        {
            tarjan(y);
            low[x]=min(low[x],low[y]);
        }
        else if(vis[y])//在栈中
        {
            low[x]=min(low[x],dfn[y]);
        }
    }
    if(dfn[x]==low[x])
    {
        sccc++;
        int now=-1;
        while(now!=x)
        {
            now=st.top();st.pop();
            vis[now]=0;//出栈标记
            key[now]=sccc;
        }
    }

}

void solve()
{
    cin>>n;
    for(int i=1;i<=n;i++)
        key[i]=i;
    for(int i=1;i<=n;i++)
    {
        int j;
        while(cin>>j)
        {
            if(!j) break;
            add_edge(i,j);
        }
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i);
    for(int now=1;now<=n;now++)
    {
        for(int i=head[now];i;i=edge[i].next)
        {
            int to=edge[i].to;
            if(key[now]==key[to]) continue;
            indgr[key[to]]++;oudgr[key[now]]++;
        }
    }
    int cnts=0;int cnte=0;
    for(int i=1;i<=sccc;i++)
    {
        if(!indgr[i]) 
            cnts++;
        if(!oudgr[i]) 
            cnte++;
    }
    cout<<cnts<<endl;
    if(sccc==1)
        cout<<0<<endl;
    else    
        cout<<max(cnts,cnte)<<endl;
}

int main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

缩点+拓扑方案统计

题面过于毒瘤就不放了。大概意思为给出一张DAG图(可能有重边,自环),给它完成一遍强连通分量缩点后,图中最长链的大小和构成最长链的方案数。缩点后点的权值为一个强连通分量内点的个数。

一直到 t a r j a n tarjan tarjan 完成都是模板。因为可能有重边的存在,而且最后还要通过遍历边的方式统计方案数,所以在缩点时要有意识地判断重边。方法有很多,可以哈希判重,可以 s o r t sort sort 后判重。

set <ll> s;
    for(int now=1;now<=n;now++)
    {
        for(int i=hh[now];i;i=edge[i].next)
        {
            int to=edge[i].to;
            if(key[now]==key[to]) continue;
            ll hash = key[now] * 1000000ll + key[to];
            if(s.count(hash)) continue;//判重边
            add_edge(head,key[now],key[to]);
            indgr[key[to]]++;
            s.insert(hash);
        }
    }

值得一提的是, t a r j a n tarjan tarjan 的强连通分量的求解顺序的逆序正是一个拓扑序。可以通过倒着遍历强连通分量编号的写法来代替拓扑,代码如下

for (int now = scc; now; now--)
    {
        if(!dp[now])
            dp[now]=sccc[now],cnt[now]=1;
        for (int i = head[now]; i; i = edge[i].next)
        {
            int to = edge[i].to;
            int val = sccc[to];
            if (dp[to] == dp[now] + val)
            {
                cnt[to] = (cnt[to] + cnt[now]) % mod;
            }
            else if (dp[to] < dp[now] + val)
            {
                dp[to] = dp[now] + val;
                cnt[to] = cnt[now];
    
            }
        }
    }

完整代码如下(正经拓扑):

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MAXN 100005
#define MAXM 2000005
typedef pair<int,int> pii;
int mod;
int n,m;
ll dp[MAXN],cnt[MAXN];//包含节点的个数,方案的个数,经典统计方案
int head[MAXN];int hh[MAXN];int tot;
int vis[MAXN],dfn[MAXN],low[MAXN],sccc[MAXN],key[MAXN],indgr[MAXN],num,scc;//强联通分量维护强联通分量节点的个数
stack <int> st;
struct EDGE
{
    int to,next;
}edge[MAXM];
void add_edge(int h[],int from,int to)
{
    edge[++tot].to=to;edge[tot].next=h[from];h[from]=tot;
}

void tarjan(int x)
{
    dfn[x]=low[x]=++num;
    st.push(x);vis[x]=1;
    for(int i=hh[x];i;i=edge[i].next)
    {
        int y=edge[i].to;
        if(!dfn[y])
        {
            tarjan(y);
            low[x]=min(low[x],low[y]);
        }
        else if(vis[y])
        {
            low[x]=min(dfn[y],low[x]);
        }
    }
    if(low[x]==dfn[x])
    {
        scc++;
        int now=-1;
        while(now!=x)
        {
            now=st.top();st.pop();
            vis[now]=0;
            key[now]=scc;
            sccc[scc]++;
        }
    }
}

void build()
{
    set <ll> s;
    for(int now=1;now<=n;now++)
    {
        for(int i=hh[now];i;i=edge[i].next)
        {
            int to=edge[i].to;
            if(key[now]==key[to]) continue;
            ll hash = key[now] * 1000000ll + key[to];
            if(s.count(hash)) continue;//判重边
            add_edge(head,key[now],key[to]);
            indgr[key[to]]++;
            s.insert(hash);
        }
    }
}

void topu()
{   queue <int> q;
    for(int i=1;i<=scc;i++)
    {
        if(indgr[i]==0)
        {
            dp[i]=sccc[i];
            cnt[i]=1;
            q.push(i);
        }
        
    }
    while(!q.empty())
    {
        int now=q.front();q.pop();
        for(int i=head[now];i;i=edge[i].next)
        {
            int to=edge[i].to;
            int val=sccc[to];
            if(dp[to]==dp[now]+val)
            {
                cnt[to]=(cnt[to]+cnt[now])%mod;
            }
            else if(dp[to]<dp[now]+val)
            {
                dp[to]=dp[now]+val;
                cnt[to]=cnt[now];
                
            }
            indgr[to]--;
            if(!indgr[to])
                q.push(to);
        }
    }
}

void output()
{
    int mx=-1;
    int mxcnt=0;
    for(int i=1;i<=scc;i++)
    {
        if(dp[i]>mx)
        {
            mx=dp[i];
            mxcnt=cnt[i];
        }
        else if(dp[i]==mx)
        {
            mxcnt=(mxcnt+cnt[i])%mod;
        }
    }
    cout<<mx<<endl;
    cout<<mxcnt<<endl;
}

void solve()
{
    cin>>n>>m>>mod;
    for(int i=1;i<=n;i++)
        key[i]=i;
    for(int i=1;i<=m;i++)
    {
        int u,v;
        cin>>u>>v;
        add_edge(hh,u,v);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i);
    build();
   
    topu();
   
    output();
}

int main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

用拓扑解决差分约束问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lgUzOSec-1637498596125)(C:\Users\ADguy\AppData\Roaming\Typora\typora-user-images\1637463147604.png)]

这道题之前没学过差分约束的时候写了一遍,用差分约束的思想可以很简单的解决这个问题。现在学完了差分约束再看一遍想法就清晰了很多。

一共有三类问题:

  • 两点权值必须相同
  • 两点权值可以不相同
  • 两点权值必须不相同

差分约束系统约束的是未知数之间的相对关系。我们先将所有权值能够相同的点都用 t a r j a n tarjan tarjan 缩成一个点,每个压缩完成后的点对应的就是差分约束系统下的未知数。

接着我们开始考虑加入要求两个值必须不相同的约束关系。

如果两个点在同一个强连通分量内,也就是之前定义为相同,现在我们又要求在他们之间连边,也就是这两个点必须不同。构成了矛盾,输出 − 1 -1 1

void build()
{
    for(int i=1;i<=n;i++)
    {
        for(int j=original_head[i];j;j=original_edge[j].next)
        {
            int jj=original_edge[j].to;
            if(key[i]==key[jj]) continue;
            add_edge(key[i],key[jj],true);
            indgr[key[jj]]++;
        }
    }
    for(int i=1;i<=m;i++)
    {
        if(relation[i].f==2)
        {
            if(key[relation[i].a]==key[relation[i].b])//之前已经定义为糖果相同,现在又要求糖果不同
            {
                cout<<-1<<endl;
                exit (0);
            }
            add_edge(key[relation[i].a],key[relation[i].b],false);
            indgr[key[relation[i].b]]++;
        }
        else if(relation[i].f==4)
        {
            if(key[relation[i].a]==key[relation[i].b])//之前已经定义为糖果相同,现在又要求糖果不同
            {
                cout<<-1<<endl;
                exit (0);
            }
            add_edge(key[relation[i].b],key[relation[i].a],false);
            indgr[key[relation[i].a]]++;
        }
    }
}

接着就是跑一遍拓扑了。

如果两边的糖果数量可以相同(这种情况是存在的,因为必须相同的点既连了前向边也连了后向边;而可以相同的点只连了前向边而没有连后向边。所以即使两个点权值相同,也有可能不在同一个强联通分量内),就取两者中的 m a x max max ,否则取 m a x ( n o w . v a l + 1 , t o . v a l ) max(now.val+1,to.val) max(now.val+1,to.val)

完整代码:

#include<bits/stdc++.h>
using namespace std;
#define MAXN 100005
#define MAXM 200005
#define ll long long

ll n,m;

struct EDGE
{
    int to,next;
    bool same;//记录该边是否允许两边相等
}edge[MAXM];
int head[MAXN],hh[MAXN],tot;

void add_edge(int h[],int from,int to,bool same)
{
    tot++;edge[tot].to=to;edge[tot].next=h[from];h[from]=tot;
    edge[tot].same=same;
}

struct relation
{
    int f;int a;int b;
}relation[MAXM];

int scc;//强连通分量个数
int num;//时间戳
int vis[MAXN],low[MAXN],dfn[MAXN],key[MAXN];
stack <int> st;
struct node
{
    ll size=0;//糖果相同的人的数量
    ll candy=0;//糖果的数量
}sccc[MAXN];
int indgr[MAXN];
void tarjan(int x)
{
    num++;
    low[x]=num;dfn[x]=num;
    st.push(x);vis[x]=1;
    for(int i=hh[x];i;i=edge[i].next)
    {
        int j=edge[i].to;
        if(dfn[j]==0)
        {
            tarjan(j);
            low[x]=min(low[x],low[j]);
        }
        else if(vis[j]==1)
        {
            low[x]=min(low[x],dfn[j]);
        }
    }
    if(low[x]==dfn[x])
    {
        int now=-1;
        scc++;
        while(now!=x)
        {
            now=st.top();st.pop();
            sccc[scc].size++;
            key[now]=scc;
            vis[now]=0;       
        }
    }
}

void build()
{
    for(int i=1;i<=n;i++)
    {
        for(int j=hh[i];j;j=edge[j].next)
        {
            int jj=edge[j].to;
            if(key[i]==key[jj]) continue;
            add_edge(head,key[i],key[jj],true);
            indgr[key[jj]]++;
        }
    }
    for(int i=1;i<=m;i++)
    {
        if(relation[i].f==2)
        {
            if(key[relation[i].a]==key[relation[i].b])//之前已经定义为糖果相同,现在又要求糖果不同
            {
                cout<<-1<<endl;
                exit (0);
            }
            add_edge(head,key[relation[i].a],key[relation[i].b],false);
            indgr[key[relation[i].b]]++;
        }
        else if(relation[i].f==4)
        {
            if(key[relation[i].a]==key[relation[i].b])//之前已经定义为糖果相同,现在又要求糖果不同
            {
                cout<<-1<<endl;
                exit (0);
            }
            add_edge(head,key[relation[i].b],key[relation[i].a],false);
            indgr[key[relation[i].a]]++;
        }
    }
}

void topp()
{
    queue <int> q;
    for(int i=1;i<=scc;i++)
    {
        if(indgr[i]==0)
        {
            sccc[i].candy=1;
            q.push(i);
        }
    }
    while(!q.empty())
    {
        int now=q.front();q.pop();
        for(int i=head[now];i;i=edge[i].next)
        {
            int j=edge[i].to;
            if(edge[i].same==true)//允许两边糖果数量相等
            {
                sccc[j].candy=max(sccc[j].candy,sccc[now].candy);
            }
            else if(edge[i].same==false)//不允许两个强连通分量糖果相等
            {
                sccc[j].candy=max(sccc[now].candy+1,sccc[j].candy);
            }
            indgr[j]--;
            if(indgr[j]==0) q.push(j);
        }
    }
    for(int i=1;i<=scc;i++)
    {
        if(indgr[i]!=0)//出现了自环
        {
            cout<<-1<<endl;
            exit (0);
        }
    }
}

void output()
{
    ll ans=0;
    for(int i=1;i<=scc;i++) 
    {
        ans=ans+sccc[i].candy*sccc[i].size;
    }
    if(!ans)
        cout<<-1<<endl;
    cout<<ans<<endl;
}

void solve()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        cin>>relation[i].f>>relation[i].a>>relation[i].b;
        if(relation[i].f==1)//将所有权值相同的点缩成一点
        {
            add_edge(hh,relation[i].a,relation[i].b,true);
            add_edge(hh,relation[i].b,relation[i].a,true);
        }
        else if(relation[i].f==3)
        {
            add_edge(hh,relation[i].b,relation[i].a,true);
        }
        else if(relation[i].f==5)
        {
            add_edge(hh,relation[i].a,relation[i].b,true);
        }
    }
    for(int i=1;i<=n;i++)
    {
        if(dfn[i]==0) tarjan(i);
    }
    build();
    topp();
    output();
}   

int main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

{
cin>>relation[i].f>>relation[i].a>>relation[i].b;
if(relation[i].f1)//将所有权值相同的点缩成一点
{
add_edge(hh,relation[i].a,relation[i].b,true);
add_edge(hh,relation[i].b,relation[i].a,true);
}
else if(relation[i].f
3)
{
add_edge(hh,relation[i].b,relation[i].a,true);
}
else if(relation[i].f==5)
{
add_edge(hh,relation[i].a,relation[i].b,true);
}
}
for(int i=1;i<=n;i++)
{
if(dfn[i]==0) tarjan(i);
}
build();
topp();
output();
}

int main()
{
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
solve();
return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值