图(八):强连通分量

强连通分量

强连通分量是针对有向图来说的,当一个有向图中所有的点都能够相互到达则称这个图为强连通分量。

一、模板

int dfn[N], low[N], dfncnt, s[N], in_stack[N], tp;
int scc[N], sc;  // 结点 i 所在 SCC 的编号
int sz[N];       // 强连通 i 的大小

void tarjan(int u) {
  low[u] = dfn[u] = ++dfncnt, s[++tp] = u, in_stack[u] = 1;
  for (int i = h[u]; i; i = e[i].nex) {
    const int &v = e[i].t;
    if (!dfn[v]) {
      tarjan(v);
      low[u] = min(low[u], low[v]);
    } else if (in_stack[v]) {
      low[u] = min(low[u], dfn[v]);
    }
  }
  if (dfn[u] == low[u]) {
    ++sc;
    while (s[tp] != u) {
      scc[s[tp]] = sc;
      sz[sc]++;
      in_stack[s[tp]] = 0;
      --tp;
    }
    scc[s[tp]] = sc;
    sz[sc]++;
    in_stack[s[tp]] = 0;
    --tp;
  }
}

[模板]缩点

参考代码:

#include <iostream>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <vector>
#include <map>
#include <queue>
#define int long long
using namespace std;
typedef pair<int, int> PII;
const int N = 1e4 + 10;
int n, m, ans = -1;
int w[N];
int dfn[N], low[N], timetamp;
int stk[N], top;
bool st[N], stt[N];
int id[N];
int scc_cnt;
int res[N], dist[N];
int d[N];
vector<int> g[N];
vector<int> h[N];
map<PII, bool> p;
void tarjan(int u)
{
    dfn[u] = low[u] = ++timetamp;
    stk[++ top] = u;
    st[u] = true;
    for (int i : g[u])
    {
        if (!dfn[i])
        {
            tarjan(i);
            low[u] = min(low[u], low[i]);
        }
        else if (st[i])
        {
            low[u] = min(low[u], dfn[i]);
        }
    }
    if (low[u] == dfn[u])
    {
        int y;
        ++ scc_cnt;
        do
        {
            y = stk[top--];
            st[y] = false;
            id[y] = scc_cnt;
            res[scc_cnt] += w[y];
        } while (y != u);
    }
}
void topsort()
{
    queue<int> q;
    for (int i = 1; i <= scc_cnt; i++)
    {
        if (d[i] == 0)
        {
            q.push(i);
            dist[i] = res[i];
        }
    }
    while (q.size())
    {
        int t = q.front();
        q.pop();
        for (int i : h[t])
        {
            dist[i] = max(dist[i], dist[t] + res[i]);
            d[i]--;
            if (d[i] == 0)
            {
                q.push(i);
            }
        }
    }
    for (int i = 1; i <= scc_cnt; i++)
    {
        ans = max(ans, dist[i]);
    }
}
signed main()
{
    scanf("%lld%lld", &n, &m);
    for (int i = 1; i <= n; i++)
    {
        scanf("%lld", &w[i]);
    }
    for (int i = 0; i < m; i++)
    {
        int a, b;
        scanf("%lld%lld", &a, &b);
        g[a].push_back(b);
    }
    for (int i = 1; i <= n; i++)
    {
        if (!dfn[i])
        {
            tarjan(i);
        }
    }
    for (int i = 1; i <= n; i++)
    {
        for (int j : g[i])
        {
            if (id[i] != id[j])
            {
                h[id[i]].push_back(id[j]);
                d[id[j]]++;
            }
        }
    }
    /*
    for(int i = 1; i <= scc_cnt; i ++){
        for(int j : h[i]){
            printf("%d->%d\n",i , j);
        }
    }*/
    topsort();
    printf("%lld\n", ans);
    return 0;
}

二、应用

受欢迎的牛

思路: 当一群牛相互爱慕时,我们可以用tarjan 算法将其缩成一个点。一群牛受欢迎当且仅当其余所有牛都爱慕他,即其余所有点都能够到达他,出度为0, 若缩点之后的图中存在两个及以上的点出度为0时则没有牛是受欢迎的,因为至少有一头别的牛不认为他受欢迎。

参考代码

#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 1e4 + 10;
int n, m;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool st[N];
int id[N], scc_cnt, scc_size[N];
int dout[N];
vector<int> g[N];
void tarjan(int u){
    dfn[u] = low[u] = ++ timestamp;
    stk[++ top] = u;
    st[u] = true;
    for(int son : g[u]){
        if(!dfn[son]){
            tarjan(son);
            low[u] = min(low[u], low[son]);
        }
        else if(st[son]){
            low[u] = min(low[u], dfn[son]);
        }
    }
    if(dfn[u] == low[u]){
        ++scc_cnt;
        int y;
        do{
            y = stk[top --];
            st[y] = false;
            id[y] = scc_cnt;
            scc_size[scc_cnt] ++;
        }while(y != u);
    }
}
int main(){
    scanf("%d%d",&n, &m);
    for(int i = 0; i < m ; i ++){
        int a, b;
        scanf("%d%d",&a, &b);
        g[a].push_back(b);
    }
    for(int i = 1; i <= n ; i ++){
        if(!dfn[i]){
            tarjan(i);
        }
    }
    for(int i = 1; i <= n ; i ++){
        for(int j: g[i]){
            int a = id[i];
            int b = id[j];
            if(a != b){
                dout[a] ++;
            }
        }
    }
    int zeors = 0, sum = 0;
    for(int i = 1; i <= scc_cnt; i++){
        if(!dout[i]){
            zeors ++;
            sum += scc_size[i];
            if(zeors > 1){
                sum = 0;
                break;
            }
        }
    }
    printf("%d\n", sum);
    return 0;
}

The Cow Prom

很裸的强连通分量问题,直接套模板即可。

参考代码:

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<vector>
using namespace std;
const int N = 1e5 + 10;
int dfn[N], low[N], timetamp;
int stk[N], top;
bool st[N];
int n, m, ans;
int scc_cnt, scc_size[N];
int id[N];
vector<int> g[N];
void tarjan(int u){
    dfn[u] = low[u] = ++ timetamp;
    stk[++ top] = u;
    st[u] = true;
    for(int i : g[u]){
        if(!dfn[i]){
            tarjan(i);
            low[u] = min(low[u], low[i]);
        }
        else if(st[i]){
            low[u] = min(low[u], dfn[i]);
        }
    }
    if(low[u] == dfn[u]){
        int y; 
        ++ scc_cnt;
        do{
            y = stk[top --];
            st[y] = false;
            id[y] = scc_cnt;
            scc_size[scc_cnt] ++;
        }while(y != u);
    }
}
int main(){
    scanf("%d%d",&n, &m);
    for(int i = 0; i < m ; i ++){
        int a, b;
        scanf("%d%d",&a, &b);
        g[a].push_back(b);
    }
    for(int i = 1; i <= n ; i ++){
        if(!dfn[i]){
            tarjan(i);
        }
    }
    for(int i = 1; i <= scc_cnt; i ++){
        if(scc_size[i] > 1){
            ans ++;
        }
    }
    printf("%d\n", ans);
    return 0;
}

Network of Schools

思路: 由题意可得所给接收学校关系为一条有向边。首先将原图通过tarjan缩点得到有向无环图,统计每个强连通分量的出度入度,起点数量为 a,终点数量为 b。对于一个强连通分量,其中只要有一所学校获得新软件那么整个分量都能获得。对于任务A,只需要把软件交给这a个起点学校就能保证所有学校都接收到软件。对于任务B,我们只需将b个终点与a个起点随意相连就能将整张图强连通,所以答案为max(a, b), 当然若原图本来就是一个强连通分量则不再需要添加边,答案为0。

参考代码:

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<vector>
using namespace std;
const int N = 110;
vector<int> g[N];
int dfn[N], low[N], timestamp;
int stk[N], top;
int din[N], dout[N];
int id[N], n;
int scc_cnt, scc_size[N];
bool st[N];
void tarjan(int u){
    dfn[u] = low[u] = ++timestamp;
    stk[++ top] = u;
    st[u] = true;
    for(int j : g[u]){
        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]){
        ++ scc_cnt;
        int y;
        do{
            y = stk[top --];
            st[y] = false;
            id[y] = scc_cnt;
            scc_size[scc_cnt] ++;
        }while(y != u);
    }
}
int main(){
    scanf("%d",&n);
    for(int i = 1; i <= n ; i ++){
        int x;
        while(~scanf("%d",&x)){
            if(x == 0){
                break;
            }
            g[i].push_back(x);
        }
    }
    for(int i = 1; i <= n ;i ++){
        if(!dfn[i]){
            tarjan(i);
        }
    }
    for(int i = 1; i <= n ; i ++){
        for(int j : g[i]){
            int a = id[i];
            int b = id[j];
            if(a != b){
                dout[a] ++;
                din[b] ++;
            }
        }
    }
    int a = 0, b = 0;
    for(int i = 1; i <= scc_cnt; i ++){
        if(!dout[i]){
            a ++;
        }
        if(!din[i]){
            b ++;
        }
    }
    printf("%d\n", b);
    if(scc_cnt == 1){
        printf("0\n");
    }
    else{
        printf("%d\n", max(a, b));
    }
    return 0;
}

最大半连通子图

思路: 首先对有向图进行tarjan缩点,得到有向无环图。缩点之后的所有点是强连通分量,满足半连通性质,再在缩点之后的拓扑图中找到最长连,记录所有点数即可。

参考代码:

#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
#include<unordered_set>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10, M = 3e6 + 10;
int stk[N], top;
int st[N], in_stk[N];
int scc_size[N], scc_cnt;
int id[N];
int n, m, mod;
int f[N], g[N];
int dfn[N], low[N], timestamp;
int h1[N], h2[N], e[M], ne[M], idx;


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] = ++ timestamp;
    stk[++ top] = u;
    in_stk[u] = true;
    for(int i = h1[u]; i != -1; i = ne[i]){
        int j = e[i];
        if(!dfn[j]){
            tarjan(j);
            low[u] = min(low[u], low[j]);
        }
        else if(in_stk[j]){
            low[u] = min(low[u], dfn[j]);
        }
    }
    if(low[u] == dfn[u]){
        int y;
        ++ scc_cnt;
        do{
            y = stk[top --];
            in_stk[y] = false;
            scc_size[scc_cnt] ++;
            id[y] = scc_cnt;
        }while(y != u);
    }
}

int main(){
    memset(h1, -1, sizeof(h1));
    memset(h2, -1,sizeof(h2));
    scanf("%d%d%d",&n, &m, &mod);
    for(int i = 0; i < m ; i ++){
        int a, b;
        scanf("%d%d",&a, &b);
        add(h1, a, b);
    }
    for(int i = 1; i <= n; i ++){
        if(!dfn[i]){
            tarjan(i);
        }
    }
    unordered_set<LL> p;
    for(int i = 1; i <= n ; i ++){
        for(int j = h1[i]; j != -1; j = ne[j]){
            int k = e[j];
            int a = id[i];
            int b = id[k];
            if(a != b){
                int hash = a * 1000000ll + b;
                if(!p.count(hash)){
                    p.insert(hash);
                    add(h2, a, b);
                }
            }
        }
    }
    for(int i = scc_cnt; i >= 0; i --){
        if(!f[i]){
            f[i] = scc_size[i];
            g[i] = 1;
        }
        for(int j = h2[i]; j != -1; j = ne[j]){
            int k = e[j];
            if(f[k] < f[i] + scc_size[k]){
                f[k] = f[i] + scc_size[k];
                g[k] = g[i];
            }
            else if(f[k] == f[i] + scc_size[k]){
                g[k] = (g[k] + g[i]) % mod;
            }
        }
    }
    int maxf = -1, sum = 0;
    for(int i = 1; i <= scc_cnt; i ++){
        if(f[i] > maxf){
            maxf = f[i];
            sum = g[i];
        }
        else if(f[i] == maxf ){
            sum = (sum + g[i]) % mod;
        }
    }
    printf("%d\n%d\n",maxf, sum);
    return 0;
}

糖果

思路: 乍一看很像差分约束,但是当差分约束中所有边权为整数时可以通过缩点求解,进一步简化,若所有边权为1时可以直接用拓扑排序求解。首先根据差分约束的思路建图,在图上跑tarjan缩点,若得到的拓扑图中的某一连通分量中有一个边权大于0的边时,则没有答案。否则按照拓扑序逆序求最最短路(所有连通分量的距离的初始值为1,因为每个小朋友至少要分到1个糖果), 跑完一边最短路之后求和即为答案。

参考代码:

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#define int long long
using namespace std;
const int N = 1e5 + 10, M = 6e5 +10;
int h1[N], h2[N], e[M], ne[M], w[M], idx;
int stk[N], top;
bool st[N];
int dfn[N], low[N], timetamp;
int id[N];
int n, m;
int f[N], g[N], dist[N];
int scc_cnt, scc_size[N];
void add(int h[],int a, int b, int c){
    e[idx] = b;
    w[idx] = c;
    ne[idx] = h[a];
    h[a] = idx++;
}
void tarjan(int u){
    dfn[u] = low[u] = ++ timetamp;
    stk[++ top] = u;
    st[u] = true;
    for(int i = h1[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(dfn[u] == low[u]){
        int y;
        ++ scc_cnt;
        do{
            y = stk[top --];
            st[y] = false;
            id[y] = scc_cnt;
            scc_size[scc_cnt] ++;
        }while(y != u);
    }
}
signed main(){
    memset(h1, -1, sizeof(h1));
    memset(h2, -1, sizeof(h2));
    scanf("%lld%lld",&n, &m);
    for(int i = 0; i < m ; i ++){
        int a, b, x;
        scanf("%lld%lld%lld",&x, &a, &b);
        if(x == 1){
            add(h1, a, b, 0);
            add(h1, b, a, 0);
        }
        else if(x == 2){
            add(h1, a, b, 1);
        }
        else if(x == 3){
            add(h1, b, a, 0);
        }
        else if(x == 4){
            add(h1, b, a, 1);
        }
        else{
            add(h1, a, b, 0);
        }
    }
    for(int i = 1; i <= n ; i ++){
        add(h1, 0, i, 1);   
    }
    tarjan(0);
    bool flag = true;
    for(int i = 0; i <= n ; i ++){
        for(int j = h1[i]; j != -1; j = ne[j]){
            int k = e[j];
            int a = id[i];
            int b = id[k];
            if(a == b){
                if(w[j] > 0){
                    flag = false;
                    break;
                }
            }
            else{
                add(h2, a, b, w[j]);
            }
        }
        if(!flag){
            break;
        }
    }
    if(!flag){
        printf("-1\n");
    }
    else{
        for(int i = scc_cnt; i > 0; i --){
            for(int j = h2[i]; j != -1; j = ne[j]){
                int k = e[j];
                dist[k] = max(dist[k], dist[i] + w[j]);
            }
        }
        /*
        printf("%d\n", scc_cnt);
        for(int i = 1; i <= scc_cnt; i ++){
            printf("%lld ", dist[i]);
        }*/
        int ans = 0;
        for(int i = 1; i <= scc_cnt; i ++){
            ans += (long long)dist[i] * scc_size[i];
        }
        printf("%lld\n", ans);
    }
}

Game Master

思路:

对于任意两个选手i, j, 若 i 在两场比赛中的至少一场得分大于 j 就向连 j 连一条边,后进行tarjan缩点,统计每个点的出入度。入度为0的所有点中的所有选手均有可能赢得比赛。

参考代码:

#include <iostream>
#include <cstring>
#include <vector>
#include <cstdio>
#include <algorithm>
#define int long long
using namespace std;
const int N = 1e6 + 10;
typedef pair<int, int> PII;
struct node
{
    int x, id;
    bool operator<(const node &t) const
    {
        return x < t.x;
    }
} a[N];
vector<int> g[N];
int n;
int dfn[N], low[N], timetamp;
int stk[N], top;
int d[N];
int id[N], scc_cnt;
bool st[N];
void tarjan(int u)
{
    dfn[u] = low[u] = ++timetamp;
    stk[++top] = u;
    st[u] = true;
    for (int son : g[u])
    {
        if (!dfn[son])
        {
            tarjan(son);
            low[u] = min(low[u], low[son]);
        }
        else if (st[son])
        {
            low[u] = min(low[u], dfn[son]);
        }
    }
    if (dfn[u] == low[u])
    {
        ++scc_cnt;
        int y;
        do
        {
            y = stk[top--];
            st[y] = false;
            id[y] = scc_cnt;
        } while (y != u);
    }
}
signed main()
{
    int T;
    scanf("%lld", &T);
    while (T--)
    {
        scanf("%lld", &n);
        for(int i = 0; i <= n ; i ++){
            g[i].clear();
        }
        memset(dfn, 0, sizeof(dfn));
        memset(st, false, sizeof(st));
        memset(d, 0, sizeof(d));
        memset(id, 0, sizeof(id));
        memset(low, 0, sizeof(low));
        timetamp = top = scc_cnt = 0;
        for (int j = 0; j < 2; j++)
        {
            for (int i = 1; i <= n; i++)
            {
                scanf("%lld", &a[i].x);
                a[i].id = i;
            }
            sort(a + 1, a + 1 + n);
            for (int i = 2; i <= n; i++)
            {
                g[a[i].id].emplace_back(a[i - 1].id);
            }
        }
        for (int i = 1; i <= n; i++)
        {
            if (!dfn[i])
            {
                tarjan(i);
            }
        }
        for (int i = 1; i <= n; i++)
        {
            for (int j : g[i])
            {
                if (id[i] != id[j])
                {
                    d[id[j]]++;
                }
            }
        }
        for (int i = 1; i <= n; i++)
        {
            if (d[id[i]])
            {
                printf("0");
            }
            else
            {
                printf("1");
            }
        }
        printf("\n");
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值