day 11 强连通等

强连通等


定义

  • 连通分量:连通的子图
  • 极大连通分量:最大的连通分量(不能再加点)
  • 强连通分量:任意两个点之间都互相可达
  • 极大强连通分量:最大的强连通分量(不能再加点)

Tarjan

  • 求强连通分量

  • 把强连通分量缩成一个点后变成一张 DAG

  • 原理图

  • 树边:dfs 树上的边(图中黑色的边)

  • 返祖边:回到自己祖先的一条边(图中红色的边)

  • 横插边:除上面两种边外的边(图中绿色的边)

  • 因为横插边不可能形成强连通分量所以直接无视

  • dfn 表示点的 dfs 序编号, low 表示通过返祖边仅向上爬一次能到达的最浅深度

  • 在求强连通分量时爬多少次无所谓,但求割点的时候会出问题

  • 如果 dfn[u] == low[u] 就说明 u 是一个强连通分量的根(即既没有一条起点是 u 的返祖边,也没有返祖边跨过它)

  • 此时将栈里的点弹到直到弹到这个点(强连通分量的根),它们就属于一个强连通分量

  • 强连通分量性质:

割点和割边

  • 求割点:

  • 还是用 Tarjan ,但是每次求 low只能向上爬一次

  • 如图:

  • 如果可以向上爬多次,那每个点的 low 就都是 1 了,区别不出来割点是什么

  • 如果一个点被一条返祖边跨越,则这个既不是强连通分量中深度最浅的点,也不是割点

  • 但如果这个点上长者一条返祖边,则该点也可能是割点

  • 所以只能沿着返祖边跳一次

  • 所以割点有两种:要么是根,要么没有返祖边跨越他且他有孩子(不能是叶子节点)

  • 求割边(桥):

image-20230720094345852

双连通

2-SAT 问题

  • n-SATn\ge3 时这是个 NPC 问题(即复杂度不能是多项式级别的问题)
  • 2-SAT 可以用强连通分量求解

  • 这种情况就是无解,否则可以证明一定有解
  • 对建出的图求强连通分量后缩点,得出一张 DAG
  • 然后求拓扑序,对于每个变量 a,\neg a ,把拓扑序小的赋值为 0 ,大的赋值为 1
  • 2-SAT详解

例题

P4171 [JSOI2010] 满汉全席

  • 裸的 2-SAT 板子

P3825 [NOI2017] 游戏

  • 乍一看是 3-SAT 耶!
  • 其实如果当前地图不适合 x 赛车,就对剩下的 yz 去建边(要么选 y ,要么选 $z$)
  • 如果是通用赛车图? 3^8 枚举?
  • 2^8 就够了,因为选 a 车和 b 车已经包含了选 c 车的情况

P4630 [APIO2018] 铁人两项

  • 如果是一棵树,暴力枚举切断点,按顺序合并
  • 要用到仙人掌结构

  • 图中有 4 个环

  • 对于每个环建一个方点,留下红色的边和树边

  • 这样子剩下的图一定是棵树(圆方树

    先把所有的点双求出来

    对于每个点双都能在里面取起点、中转点和终点

    然后排列组合求出答案

    方点连接割点的边边权为 0

    后面相当于把最短路拆成边建

    每个点到方点的边权为割点到该点的最短路

    ​ —— wsr

课后题


守矢的关键路径

  • 法一:正着求一遍拓扑算出最早开始时间,反着求一遍拓扑算出最晚开始时间,如果最早开始时间与最晚开始时间相等,则该点一定在关键路径上

    #include <bits/stdc++.h>
    using namespace std;
    const int maxn = 15000 + 10;
    struct cc { int from, to, cost; } es[maxn];
    struct cc2 { int from, to, cost; } es2[maxn];
    int ru[maxn], ru2[maxn], first[maxn], nxt[maxn], dis[maxn], first2[maxn], nxt2[maxn], dis2[maxn];
    bool vis[maxn], vis2[maxn]; int tot = 0;
    void build(int ff, int tt, int pp) {
        es[++tot] = (cc) {
            ff, tt, pp
        };
        nxt[tot] = first[ff];
        first[ff] = tot;
    }
    int tot2 = 0;
    void build2(int ff, int tt, int pp) {
        es2[++tot2] = (cc2) {
            ff, tt, pp
        };
        nxt2[tot2] = first2[ff];
        first2[ff] = tot2;
    }
    int n, m;
    void TopoSort() { // 求各点最早发生时间
        for (int i = 1; i <= n; i++) {
            if (!ru[i] && !vis[i]) {
                vis[i] = 1;
                for (int j = first[i]; j; j = nxt[j]) {
                    int u = es[j].to;
                    dis[u] = max(dis[u], dis[i] + es[j].cost);
                    if (!vis[u]) {
                        ru[u]--;
                        if (!ru[u]) {
                            TopoSort();
                        }
                    }
                }
            }
        }
    }
    void TopoSort2() { // 求各点最晚发生时间
        for (int i = n; i >= 1; i--) {
            if (!ru2[i] && !vis2[i]) {
                vis2[i] = 1;
                for (int j = first2[i]; j; j = nxt2[j]) {
                    int u = es2[j].to;
                    dis2[u] = min(dis2[u], dis2[i] - es2[j].cost);
                    if (!vis2[u]) {
                        ru2[u]--;
                        if (!ru2[u]) {
                            TopoSort2();
                        }
                    }
                }
            }
        }
    }
    int main() {
        memset(dis2, 63, sizeof(dis2));
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= m; i++) {
            int x, y, z;
            scanf("%d%d%d", &x, &y, &z);
            build(x, y, z); build2(y, x, z);
            ru[y]++, ru2[x]++;
        }
        TopoSort();
        dis2[n] = dis[n];
        TopoSort2();
        int ans = 0;
        for (int i = 1; i <= n; i++) 
            if (dis[i] == dis2[i]) 
                ans++;
        printf("%d", ans);
        return 0;
    }
    
  • 法二:用 Dijkstra 建出最长路径树,记录每个点的转移前驱,最后 dfs 搜一遍最长路径上的点(我自己想的做法 qwq)

    #include<bits/stdc++.h>
    #define mk make_pair
    #define type pair<int,int>
    using namespace std;
    const int maxn = 205;
    int n,m,dis[maxn],ans; bool vis[maxn]; vector<pair<int,int> > mp[maxn]; vector<int> dfn[maxn];
    void addEdge(int u,int v,int w) { mp[u].push_back(mk(v,w)); }
    void Dijkstra() {
        priority_queue<type,vector<type>,greater<type> > q;
        memset(dis,-0x3f,sizeof(dis)); dis[1] = 0; q.push(mk(0,1));
        while (!q.empty()) {
            type top = q.top(); q.pop();
            int u = top.second; vis[u] = true;
            for (auto v : mp[u]) 
                if (dis[v.first] < dis[u] + v.second) {
                    dfn[v.first].clear(); dfn[v.first].push_back(u);
                    dis[v.first] = dis[u] + v.second;
                    if (!vis[v.first]) q.push(mk(dis[v.first],v.first));
                } else if (dis[v.first] == dis[u] + v.second)
                    dfn[v.first].push_back(u);
        }
        // for(int i = 1;i <= n;i ++) cout << dis[i] << ' ';
    }
    void dfs(int u) {
        if (vis[u]) return ;
        ans ++, vis[u] = true;
        for (auto v : dfn[u]) dfs(v);
    }
    int main() {
        scanf("%d%d",&n,&m);
        for (int i = 1,u,v,w;i <= m;i ++) {
            scanf("%d%d%d",&u,&v,&w);
            addEdge(u,v,w);
        }
        Dijkstra();
        memset(vis,false,sizeof(vis)); dfs(n);
        printf("%d",ans);
        return 0;
    }
    

最大半连通子图

  • 思路在代码里 qwq

    /*
    首先观察到这个图中如果有某个部分是强连通的,那就不会对半连通子图产生贡献
    所以首先对这个图进行t缩点
    并记录下每个连通块的大小和每个点所在的连通块
    然后对图求出最长链个数和长度
    按照拓扑序进行 dp: 
        f[i] 表示到第 i 个连通块的最长链大小, g[i] 表示个数。
    很容易就想到了转移的方法:
    1.{g[i]=g[i]+g[j]}(条件:f[j]+number[i]==f[i])
    2.{g[i]=g[j],f[i]=f[j]+number[i]}(条件:f[j]+number[i]>f[i])。
    按照这个转移状态就可以啦。
    但是要注意缩点之后会出现大量重边拓扑排序之前要去重。
    */
    #include<bits/stdc++.h>
    #define mk make_pair
    using namespace std;
    const int maxn = 1000005;
    int n,m,P,dfn[maxn],low[maxn],tot,top,cnt,f[maxn],g[maxn],len[maxn],d[maxn],siz[maxn],st[maxn],b[maxn]; 
    bool vis[maxn]; vector<int> mp[maxn],nmp[maxn]; vector<pair<int,int> > edge;
    void addEdge(int u,int v) { mp[u].push_back(v); }
    void addnewEdge(int u,int v) { nmp[u].push_back(v); d[v] ++; }
    void Tarjan(int u) {
        dfn[u] = low[u] = ++ tot, vis[u] = true, st[++ top] = u;
        for (auto v : mp[u]) 
            if (!dfn[v]) {
                Tarjan(v);
                low[u] = min(low[u],low[v]);
            } else if (vis[v]) low[u] = min(low[u],dfn[v]);
        if (low[u] == dfn[u]) {
            siz[++ cnt] = 1, b[u] = cnt;
            while (st[top] != u) 
                b[st[top]] = cnt, siz[cnt] ++, vis[st[top]] = false, top --;
            top --, vis[u] = false;
        }
    }
    void build() {
        for (int u = 1;u <= n;u ++)
            for (auto v : mp[u])
                if (b[u] != b[v])
                    edge.push_back(mk(b[u],b[v]));
        sort(edge.begin(),edge.end()); int i = 0;
        for (auto E : edge) {
            int u = E.first, v = E.second;
            if (i > 0 && edge[i - 1].first == u && edge[i - 1].second == v) { i ++; continue; }
            addnewEdge(u,v); i ++;
        }
    }
    void Solve() {
        queue<int> q;
        for (int i = 1;i <= cnt;i ++) {
            if (d[i] == 0) q.push(i);
            f[i] = siz[i], g[i] = 1;
        }
        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (auto v : nmp[u]) {
                d[v] --;
                if (f[u] + siz[v] == f[v]) g[v] = (g[u] + g[v]) % P;
                else if (f[u] + siz[v] > f[v]) f[v] = f[u] + siz[v], g[v] = g[u];
                if (!d[v]) q.push(v); 
            }
        }
    }
    int main() {
        scanf("%d%d%d",&n,&m,&P);
        for (int i = 1,u,v;i <= m;i ++) {
            scanf("%d%d",&u,&v);
            addEdge(u,v);
        }
        memset(low,0x3f,sizeof(low));
        for (int i = 1;i <= n;i ++)
            if (!dfn[i])
                Tarjan(i);
        build(); Solve(); int pos = 0,k = 0;
        for (int i = 1;i <= cnt;i ++)
            if (f[i] > f[pos]) pos = i, k = g[pos];
            else if (f[i] == f[pos]) k = (k + g[i]) % P;
        printf("%d\n%d",f[pos],k);
        return 0;
    }
    

Grass Cownoisseur

  • 先把强连通分量缩起来,记录每个连通块的大小

  • 正反跑两遍最长路,最后枚举每个连通块并更新答案

    #include<bits/stdc++.h>
    #define mk make_pair
    using namespace std;
    const int maxn = 1e5 + 5;
    int n,m,dis1[maxn],dis2[maxn]; vector<int> mp[maxn],nmp1[maxn],nmp2[maxn]; bool vis[maxn];
    int dfn[maxn],low[maxn],tot,cnt,st[maxn],top,b[maxn],siz[maxn]; vector<pair<int,int> > e;
    void addEdge(int u,int v) { mp[u].push_back(v); }
    void Tarjan(int u) {
        dfn[u] = low[u] = ++ tot, st[++ top] = u, vis[u] = true;
        for (auto v : mp[u]) 
            if (!dfn[v]) { Tarjan(v); low[u] = min(low[u],low[v]); }
            else if (vis[v]) low[u] = min(low[u],dfn[v]);
        if (dfn[u] == low[u]) {
            siz[++ cnt] = 1, b[u] = cnt;
            while (st[top] != u) b[st[top]] = cnt, siz[cnt] ++, vis[st[top]] = false, top --;
            vis[u] = false, top --;
        }
    }
    void addnewEdge(int u,int v) { nmp1[u].push_back(v); nmp2[v].push_back(u); }
    void build() {
        for (int i = 1;i <= n;i ++)
            for (auto j : mp[i])
                if (b[i] != b[j])
                    e.push_back(mk(b[i],b[j]));
        sort(e.begin(),e.end()); int i = 0;
        for (auto E : e) {
            int u = E.first, v = E.second;
            if (i > 0 && e[i].first == u && e[i].second == v) { i ++; continue; }
            addnewEdge(u,v);
        }
    }
    void SPFA1() {
        queue<int> q; q.push(b[1]); memset(vis,false,sizeof(vis)); memset(dis1,-0x3f,sizeof(dis1)); dis1[b[1]] = siz[b[1]];
        while (!q.empty()) {
            int u = q.front(); q.pop(); vis[u] = false;
            for (auto v : nmp1[u]) {
                if (dis1[v] < dis1[u] + siz[v]) {
                    dis1[v] = dis1[u] + siz[v];
                    if (!vis[v]) { vis[v] = true; q.push(v); }
                }
            }
        }
    }
    void SPFA2() {
        queue<int> q; q.push(b[1]); memset(vis,false,sizeof(vis)); memset(dis2,-0x3f,sizeof(dis2)); dis2[b[1]] = siz[b[1]];
        while (!q.empty()) {
            int u = q.front(); q.pop(); vis[u] = false;
            for (auto v : nmp2[u]) {
                if (dis2[v] < dis2[u] + siz[v]) {
                    dis2[v] = dis2[u] + siz[v];
                    if (!vis[v]) { vis[v] = true; q.push(v); }
                }
            }
        }
    }
    int main() {
        scanf("%d%d",&n,&m);
        for (int i = 1,u,v;i <= m;i ++) {
            scanf("%d%d",&u,&v);
            addEdge(u,v);
        }
        for (int i = 1;i <= n;i ++)
            if (!dfn[i]) Tarjan(i);
        build(); SPFA1(); SPFA2(); int ans = siz[b[1]];
        memset(vis,false,sizeof(vis));
        for (int i = 1;i <= n;i ++)
            if (!vis[b[i]] && dis1[b[i]]) {
                int u = b[i]; vis[u] = true;
                for (auto v : nmp2[u]) {
                    if (!dis2[v]) continue;
                    ans = max(ans,dis1[u] + dis2[v] - siz[b[1]]);
                }
            }
        printf("%d",ans);
        return 0;
    }
    
3 个赞

泰剧了,支持 :heart_eyes:

2 个赞

Orz

3 个赞

@金杭东 @栗子酱 回老帖!