强连通分量习题随笔

1.强连通分量

通过强连通分量的缩点,抢一个普通的有向图变成有向无环图

习题1 有向图缩点

给定一个 \(n\) 个点 \(m\) 条边的有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。你只需要求出这个权值和。

允许多次经过一条边或者一个点,但是重复经过的点,权值只计算一次。

(标明粗体的是重点)

首先允许多次经过显然就是说可以走环了,但是如果按照普通的最短路算法,走环这样的情况显然是不允许的,所以我们考虑用 \(tarjan\) 算法缩点,然后重建图,用拓扑序进行一次

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, M = 5e5 + 10;
int h[N], e[M], ne[M], idx;
int nwh[N], nwe[M], nwne[M], nidx;
int dfn[N], low[N], timestamp;
int stk[N], top, f[N], size[N], SCC, id[N], tt[N];
bool instk[N];
int dis[N], n, m, din[N], p[N];
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void add1(int a, int b)
{
    nwe[nidx] = b, nwne[nidx] = nwh[a], nwh[a] = nidx++;
}

void tarjan(int u)
{
    low[u] = dfn[u] = ++timestamp;
    stk[++top] = u, instk[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 (instk[j])
            low[u] = min(low[u], dfn[j]);
    }
    if (low[u] == dfn[u])
    {
        int y = -1;
        SCC++;
        while (y = stk[top--])
        {
            tt[y] = u;
            instk[y] = 0;
            size[SCC]++;
            id[y] = SCC;
            if (y == u)
                break;
            p[u] += p[y];
        }
    }
}

int topo()
{
    queue<int> q;
    for (int i = 1; i <= n; i++)
        if (tt[i] == i && !din[i])
            q.push(i), dis[i] = p[i];
    while (q.size())
    {
        int t = q.front();
        q.pop();
        for (int i = nwh[t]; ~i; i = nwne[i])
        {
            int j = nwe[i];
            dis[j] = max(dis[j], dis[t] + p[j]);
            din[j]--;
            if (!din[j])
                q.push(j);
        }
    }
    int ans = 0;
    for (int i = 1; i <= n; i++)
        ans = max(ans, dis[i]);
    return ans;
}

int main()
{
    memset(h, -1, sizeof(h));
    memset(nwh, -1, sizeof(nwh));
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        scanf("%d", &p[i]);
    for (int i = 1, x, y; i <= m; i++)
    {
        scanf("%d%d", &x, &y);
        add(x, y);
    }
    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; j = ne[j])
        {
            int a = tt[i], b = tt[e[j]];
            if (a != b)
            {
                add1(a, b);
                din[b]++;
            }
        }
    cout << topo() << endl;
    return 0;
}

习题2 受欢迎的牛

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

同样的,如果直接去看奶牛们的关系图,我们可能会想到一个暴力解——以每个点为终点,然后再看一下是不是每一个点都能到达它( 并查集),时间复杂度 \(O(n^2)\) 显然过不了了

那我们考虑把环缩点,然后重构图,至于判断明星奶牛的数量,我们只需要遍历所有的强连通分量,通过模拟可以发现,如果新图中,出度为0的大于等于2个,就说明根本就不存在明星奶牛(因为这两个出度为0的连通分量无法互相喜欢),否则,明星奶牛的个数就是出度为 0 的强连通分量的节点个数。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, M = 5e5 + 10;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool instk[N];
int id[N], SCC, size[N];
int dout[N], zero = 0;
void add(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, instk[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 (instk[j])
            low[u] = min(low[u], dfn[j]);
    }
    if (dfn[u] == low[u])
    {
        int y = -1;
        ++SCC;
        do
        {
            y = stk[top--];
            instk[y] = 0;
            id[y] = SCC;
            size[SCC]++;
        } while (y != u);
    }
}

int main()
{
    memset(h, -1, sizeof(h));
    cin >> n >> m;
    while (m--)
    {
        int a, b;
        cin >> 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; j = ne[j])
        {
            int k = e[j];
            int a = id[i], b = id[k];
            if (a != b)
                dout[a]++;
        }
    int sum = 0;
    for (int i = 1; i <= SCC; i++)
    {
        if (!dout[i])
        {
            zero++;
            sum += size[i];
            if (zero > 1)
            {
                sum = 0;
                break;
            }
        }
    }
    cout << sum << endl;
    return 0;
}

习题3 最大半连通子图

image-20240127145250381

所谓半连通图,就是指在半连通图中任选两点 \((u,v)\) 满足,\(u\) 能到 \(v\) 或者 \(v\) 能到 \(u\) , 从含义可以知道,我们的强连通分量显然也是半联通的,然后我们发现,缩点建新图之后,如果强联通分量 \(SCC_1\)\(SCC_2\) 之间有一条路线,那么显然这两个东西合到一起符合半联通子图的条件,问题就变成了信徒中寻找最长链的问题。

#include <iostream>
#include <cstring>
#include <cstdio>
#include <unordered_set>
using namespace std;
typedef long long LL;
const int N = 100010 , M = 2000010;
int h[N],sh[N],ne[M],e[M],idx;
int stk[N],top,times;
bool in_stk[N];
int dfn[N],low[N];
int id[N],scc_cnt,Size[N];
int f[N],g[N];
int n,m,mod;
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] = ++ times ;
    stk[++ top] = u,in_stk[u] =true;
    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]) low[u] = min(low[u] , dfn[j]);
    }

    if(dfn[u] == low[u])
    {
        scc_cnt ++;
        int y;
        do{
            y = stk[top --];
            in_stk[y] = false;
            id[y] = scc_cnt;
            Size[scc_cnt] ++;           
        }while(y != u);
    }
}

int main()
{
    scanf("%d%d%d",&n,&m,&mod);
    memset(h,-1,sizeof h);
    memset(sh,-1,sizeof sh);
    while(m -- )
    {
        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 <LL> S;
    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];
            LL hash = a * 1000000ll + b;
            if(a != b && !S.count(hash))
            {
                add(sh,a,b);
                S.insert(hash);
            }
        }
    }


    for(int i = scc_cnt; i ;i --)
    {
        if(! f[i])
        {
            f[i] = Size[i];
            g[i] = 1;
        }
        for(int j = sh[i]; ~j ; j = ne[j])
        {
            int k = e[j];
            if(f[k] < f[i] + Size[k])
            {
                f[k] = f[i] + Size[k];
                g[k] = g[i];
            }
            else if(f[k] == f[i] + Size[k]) 
                    g[k] = (g[k] + g[i]) % mod;
        }
    }

    int maxn = 0 ,sum = 0;
    for(int i = 1;i <= scc_cnt;i ++)
    if(f[i] > maxn)
    {
        maxn = f[i];
        sum = g[i];
    }
    else if(f[i] == maxn) sum = (sum + g[i]) % mod;

    printf("%d\n%d\n",maxn,sum);
    return 0;
}

习题4 网络传输

给定 \(n\) 个点 \(m\) 条边的带边权有向图,沿边方向传输信息的代价为该边的边权,特殊的,同一个强联通分量内的点间传输代价为 0,求点 \(1\) 到点 \(n\) 的最小传输代价。

简单,实际上就是缩点+最短路即可

#include <bits/stdc++.h>
using namespace std;
const int N = 6e5 + 10, M = N * 2;
int h[N], e[M], ne[M], w[M], idx;                       //原图
int hs[M], es[M], nes[M], sw[M], nidx;                  //缩点后的图
int dfn[N], SCC, low[N], stk[N], top, timestamp, id[N]; //强连通分量
bool vis[N], instk[N];
int dis[N]; //最短路径
int n, m;

void add(int a, int b, int c)
{
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

void add1(int a, int b, int c)
{
    es[nidx] = b, nes[nidx] = hs[a], sw[nidx] = c, hs[a] = nidx++;
}

void tarjan(int u) //强连通分量模板
{
    dfn[u] = low[u] = ++timestamp;
    stk[++top] = u, instk[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 (instk[j])
            low[u] = min(low[u], dfn[j]);
    }
    if (low[u] == dfn[u])
    {
        int y = -1;
        SCC++;
        do
        {
            y = stk[top--];
            instk[y] = 0;
            id[y] = SCC;
        } while (y != u);
    }
}

void dij() //迪杰斯特拉最短路
{
    memset(dis, 0x3f3f3f3f, sizeof(dis));
    dis[id[1]] = 0;
    priority_queue<pair<int, int>> q;
    q.push({0, id[1]});
    while (q.size())
    {
        int t = q.top().second;
        q.pop();
        if (vis[t])
            continue;
        vis[t] = 1;
        for (int i = hs[t]; ~i; i = nes[i])
        {
            int j = es[i];
            if (dis[j] > dis[t] + sw[i])
            {
                // cout << t << ' ' << dis[t] << endl;
                dis[j] = dis[t] + sw[i];
                q.push({-dis[j], j});
            }
        }
    }
}

int main()
{
    memset(h, -1, sizeof(h));
    memset(hs, -1, sizeof(hs));
    cin >> n >> m;
    for (int i = 1, u, v, w; i <= m; i++)
    {
        cin >> u >> v >> w;
        add(u, v, w);
    }
    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; j = ne[j])
        {
            int a = id[i], b = id[e[j]];
            if (a != b)
                add1(a, b, w[j]); //建图应该建的边权是w[j]而不是w[i]
        }
    if (id[1] == id[n])
    {
        puts("0");
        return 0;
    }
    dij();
    cout << dis[id[n]] << endl;
    return 0;
}

习题5 通讯问题

image-20240127151411556

缩完点后我们可以这样考虑,由于每一个点都会被走过,所以对于到达每一个点的最小代价,就是这个点的最小入边,毕竟题目中并不是让我们求最短路径

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10, M = N * 3;
int h[N], e[M], ne[M], w[M], idx, n, m, ans;
int hs[N], es[M], nes[M], sw[M], nidx;
int dfn[N], low[N], timestamp, stk[N], top, id[N], SCC;
bool instk[N];
int minn[N], out[N];

void add(int a, int b, int c)
{
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

void add1(int a, int b, int c)
{
    es[nidx] = b, nes[nidx] = hs[a], sw[nidx] = c, hs[a] = nidx++;
}

void tarjan(int u)
{
    low[u] = dfn[u] = ++timestamp;
    stk[++top] = u, instk[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 (instk[j])
            low[u] = min(low[u], dfn[j]);
    }
    if (low[u] == dfn[u])
    {
        int y = -1;
        SCC++;
        do
        {
            y = stk[top--];
            instk[y] = 0;
            id[y] = SCC;
        } while (y != u);
    }
}

int main()
{
    while (cin >> n >> m && !(n == 0 && m == 0))
    {
        idx = nidx = top = ans = SCC = timestamp = 0;
        memset(h, -1, sizeof(h));
        memset(hs, -1, sizeof(hs));
        memset(e, 0, sizeof(e));
        memset(es, 0, sizeof(es));
        memset(w, 0, sizeof(w));
        memset(nes, 0, sizeof(nes));
        memset(ne, 0, sizeof(ne));
        memset(minn, 0x3f, sizeof(minn));
        memset(dfn, 0, sizeof(dfn));
        memset(low, 0, sizeof(low));
        memset(instk, 0, sizeof(instk));
        memset(stk, 0, sizeof(stk));
        for (int i = 1; i <= m; i++)
        {
            int u, v, w;
            cin >> u >> v >> w;
            add(u + 1, v + 1, w);
        }
        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; j = ne[j])
            {
                int a = id[i], b = id[e[j]];
                if (a != b)
                    minn[b] = min(minn[b], w[j]);
            }
        int ans = 0;
        for (int i = 1; i <= SCC; i++)
        {
            if (i == id[1])
                continue;
            ans += minn[i];
        }
        cout << ans << endl;
    }
    return 0;
}
posted @ 2024-01-27 15:17  ljfyyds  阅读(99)  评论(0)    收藏  举报