题解:AcWing 1175 最大半连通子图

【题目来源】

AcWing:1175. 最大半连通子图 - AcWing题库

【题目描述】

一个有向图 \(G=(V,E)\) 称为半连通的(Semi-Connected),如果满足:\(∀u,v∈V\),满足 \(u→v\) 或 \(v→u\),即对于图中任意两点 \(u,v\),存在一条 \(u\) 到 \(v\) 的有向路径或者从 \(v\) 到 \(u\) 的有向路径。

若 \(G'=(V',E')\) 满足,\(E'\) 是 \(E\) 中所有和 \(V′\) 有关的边,则称 \(G'\) 是 \(G\) 的一个导出子图。

若 \(G'\) 是 \(G\) 的导出子图,且 \(G'\) 半连通,则称 \(G'\) 为 \(G\) 的半连通子图。

若 \(G'\) 是 \(G\) 所有半连通子图中包含节点数最多的,则称 \(G'\) 是 \(G\) 的最大半连通子图。

给定一个有向图 \(G\),请求出 \(G\) 的最大半连通子图拥有的节点数 \(K\),以及不同的最大半连通子图的数目 \(C\)

由于 \(C\) 可能比较大,仅要求输出 \(C\) 对 \(X\) 的余数。

【输入】

第一行包含三个整数 \(N,M,X\)\(N,M\) 分别表示图 \(G\) 的点数与边数,\(X\) 的意义如上文所述;

接下来 \(M\) 行,每行两个正整数 \(a,b\),表示一条有向边 \((a,b)\)

图中的每个点将编号为 \(1\) 到 \(N\),保证输入中同一个 \((a,b)\) 不会出现两次。

【输出】

应包含两行。

第一行包含一个整数 \(K\),第二行包含整数 \(C\ mod\ X\)

【输入样例】

6 6 20070603
1 2
2 1
1 3
2 4
5 6
6 4

【输出样例】

3
3

【核心思想】

  1. 问题分析:给定有向图 \(G=(V,E)\),需要找到最大的半连通子图。半连通图的定义是:对于任意两点 \(u,v\),存在 \(u \to v\)\(v \to u\) 的有向路径。关键在于理解强连通分量(SCC)内部的任意两点互相可达,天然满足半连通性;而SCC之间需要形成链式结构(即缩点后的DAG是一条链)才能保持半连通性。

  2. 算法选择

    • Tarjan算法:用于求强连通分量,时间复杂度 \(O(N+M)\)
    • 缩点(Condensation):将每个SCC缩成一个点,构建DAG
    • DAG上动态规划:在缩点后的DAG上求最长链(按节点数加权)及方案数
  3. 关键步骤

    • Step 1 - Tarjan求SCC

      • 使用DFS遍历图,维护 \(dfn[x]\)(时间戳)和 \(low[x]\)(能追溯到的最早栈中节点)
      • \(dfn[x] = low[x]\) 时,找到SCC的根,弹出栈中节点构成一个SCC
      • 记录每个SCC的编号 \(scc[i]\) 和大小 \(siz[cnt]\)
    • Step 2 - 缩点建DAG

      • 遍历原图所有边 \((u,v)\),若 \(scc[u] \neq scc[v]\),则在DAG中添加边
      • 使用哈希集合去重,避免重边影响DP
    • Step 3 - DAG上DP求最长链

      • 状态定义:\(f[i]\) 表示以SCC \(i\) 结尾的最大半连通子图节点数;\(g[i]\) 表示达到 \(f[i]\) 的方案数
      • 初始化:对于入度为0的SCC,\(f[i] = siz[i]\)\(g[i] = 1\)
      • 状态转移:对于边 \(i \to j\)\(f[j] = \max(f[j], f[i] + siz[j])\)
      • 方案数更新:若 \(f[j]\) 更新,则 \(g[j] = g[i]\);若相等,则 \(g[j] = (g[j] + g[i]) \mod X\)
      • 按SCC编号逆序处理(拓扑序)
    • Step 4 - 统计答案

      • 遍历所有SCC,找到最大的 \(f[i]\) 作为 \(K\)
      • 累加所有达到 \(K\)\(g[i]\),对 \(X\) 取模得到 \(C\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N + M)\),Tarjan算法 \(O(N+M)\),缩点和DP均为 \(O(N+M)\)
    • 空间复杂度:\(O(N + M)\),存储原图、DAG、以及各种辅助数组
  5. 强连通分量与缩点DP的核心思想

    • SCC的半连通性:强连通分量内部任意两点互相可达,天然满足半连通条件
    • 缩点转化:将复杂的有向图转化为DAG,把问题转化为求DAG上的最长链
    • 半连通子图的结构:缩点后的半连通子图必须形成一条链(无分支),因为DAG中若存在分支则无法保证分支间的可达性
    • DAG上最长链:按拓扑序进行DP,利用DAG的无环性质保证状态转移的正确性
    • 方案数统计:注意去重边,避免重复计算方案;使用逆序处理确保拓扑序
    • 适用于有向图压缩、可达性分析、复杂图论问题的简化

【算法标签】

强连通分量

【代码详解】

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

// ================= 常量与全局变量 =================

const int N = 100005;

int n, m, mod;                  // n: 点数, m: 边数, mod: 取模值

vector<int> e[N];               // 原图邻接表
vector<int> e2[N];              // 缩点后的 DAG 邻接表

// ---------- Tarjan 相关 ----------
int dfn[N];                     // 时间戳:节点首次被访问的顺序
int low[N];                     // 能追溯到的最早栈中节点时间戳
int tot;                        // 时间戳计数器

int stk[N];                     // 手写栈,存储当前 DFS 路径上的节点
int instk[N];                   // 标记节点是否在栈中
int top;                        // 栈顶指针

// ---------- 强连通分量(SCC)相关 ----------
int scc[N];                     // 每个节点所属的强连通分量编号
int cnt;                        // 强连通分量的总数
int siz[N];                     // 每个强连通分量包含的节点数

// ---------- DAG 上 DP 相关 ----------
int f[N];                       // f[i]: 从某个入度为0的点到达SCC i的最大节点数
int g[N];                       // g[i]: 达到最大节点数f[i]的方案数

// ================= Tarjan 算法 =================
void tarjan(int x)
{
    dfn[x] = low[x] = ++tot;    // 初始化时间戳
    stk[++top] = x;             // 当前节点入栈
    instk[x] = 1;               // 标记为在栈中

    // 遍历 x 的所有出边
    for (int y : e[x])
    {
        if (!dfn[y])            // y 尚未访问
        {
            tarjan(y);          // 递归处理 y
            low[x] = min(low[x], low[y]);   // 用子树更新 low
        }
        else if (instk[y])      // y 仍在栈中(发现返祖边)
        {
            low[x] = min(low[x], dfn[y]);   // 用回边更新 low
        }
    }

    // 若 x 是强连通分量的根节点
    if (dfn[x] == low[x])
    {
        int y;
        ++cnt;                  // 新建一个强连通分量
        do
        {
            y = stk[top--];     // 弹出栈顶节点
            instk[y] = 0;       // 取消栈中标记
            scc[y] = cnt;       // 将节点归属到当前 SCC
            siz[cnt]++;         // 当前 SCC 节点数加1
        }
        while (y != x);         // 直到弹出 x 为止
    }
}

// ================= 主函数 =================
int main()
{
    // 读取点数、边数和取模值
    cin >> n >> m >> mod;

    // 读入 m 条有向边
    while (m--)
    {
        int u, v;
        cin >> u >> v;
        e[u].push_back(v);
    }

    // 对每个未访问的节点执行 Tarjan 算法
    for (int i = 1; i <= n; i++)
        if (!dfn[i])
            tarjan(i);

    // ========= 缩点建 DAG(去重边) =========
    unordered_set<ll> S;        // 哈希集合,用于判断边是否重复
    for (int x = 1; x <= n; x++)
    {
        for (int y : e[x])
        {
            int a = scc[x];     // x 所属 SCC
            int b = scc[y];     // y 所属 SCC
            ll hash = 1LL * a * 1000000 + b;   // 构造哈希值避免重复边

            // 如果 a 和 b 属于不同 SCC,且该边尚未添加
            if (a != b && !S.count(hash))
            {
                e2[a].push_back(b);     // 在 DAG 中添加一条有向边
                S.insert(hash);         // 标记该边已添加
            }
        }
    }

    // ========= DAG 上 DP(逆序处理) =========
    for (int i = cnt; i >= 0; i--)      // 从大到小遍历 SCC 编号
    {
        // 如果 f[i] 仍为 0,说明 i 是入度为 0 的起点
        if (!f[i])
        {
            f[i] = siz[i];              // 从自身开始,节点数为当前 SCC 的大小
            g[i] = 1;                   // 方案数为 1
        }

        // 从 i 转移到后继节点 j
        for (int j : e2[i])
        {
            // 如果能获得更大的节点数
            if (f[j] < f[i] + siz[j])
            {
                f[j] = f[i] + siz[j];   // 更新最大节点数
                g[j] = g[i];            // 方案数继承自 i
            }
            // 如果节点数相等,累加方案数
            else if (f[j] == f[i] + siz[j])
            {
                g[j] = (g[j] + g[i]) % mod;     // 方案数相加并取模
            }
        }
    }

    // ========= 统计最终答案 =========
    int maxf = 0;       // 最大节点数
    int sum = 0;        // 达到最大节点数的方案数之和

    for (int i = 1; i <= cnt; i++)
    {
        if (f[i] > maxf)            // 找到更大的节点数
        {
            maxf = f[i];
            sum = g[i];             // 重置方案数
        }
        else if (f[i] == maxf)      // 节点数相等,累加方案数
        {
            sum = (sum + g[i]) % mod;
        }
    }

    // 输出最大节点数和对应方案数
    cout << maxf << endl;
    cout << sum << endl;

    return 0;
}

【运行结果】

6 6 20070603
1 2
2 1
1 3
2 4
5 6
6 4
3
3
posted @ 2026-06-27 14:20  团爸讲算法  阅读(6)  评论(0)    收藏  举报