1 条题解

  • 0
    @ 2025-10-20 17:55:20

    题目解析

    题目大意 给定一个无向连通图(可能有重边),要求给每个节点分配一个整数值,使得对于任意一条不重复经过同一条边的路径,节点值序列必须是单调递增或单调递减的。定义一个数组的多样性为数组中不同值的节点对的数量。求所有满足条件的赋值方案中,多样性的最大值。

    关键性质

    1. 边双连通分量内值相同:在任意边双连通分量(EBCC)内,所有节点必须具有相同的值。否则,存在环会导致路径值序列非单调。
    2. 缩点树结构:将每个边双连通分量缩成一个点,得到一棵树(缩点树),树边对应原图中的桥。
    3. 赋值方案限制:在缩点树上,赋值必须满足从任意根节点出发到任何叶节点的路径上值序列单调。这意味着赋值可以看作基于树深度的函数,但值可以重复。
    4. 多样性最大化:多样性最大化等价于最小化值相同的节点对数。值相同的节点对数为所有值对应的节点数组合数之和,即 (\sum_{v} \binom{s_v}{2}),其中 (s_v) 是值 (v) 的节点数。因此,需要最小化 (\sum_{v} s_v^2)。

    算法步骤

    1. 边双连通分量分解:使用 Tarjan 算法求出所有桥,并识别边双连通分量。
    2. 构建缩点树:将每个边双连通分量缩成一个节点,缩点树的边对应原图的桥。
    3. 树形动态规划:在缩点树上,通过两次 DFS 计算每个节点的子树大小,并找到最优根节点,使得按深度赋值时,同一深度的节点数平方和最小。
    4. 计算答案:总节点对数为 (\binom{n}{2}),减去最小相同值对数,得到最大多样性。

    代码实现详细解析

    算法概述

    这个问题要求我们在一个无向连通图中找到一种节点赋值方案,使得任意简单路径上的节点值序列都是单调的(递增或递减),同时最大化不同值节点对的数量。

    核心思路

    1. 边双连通分量分解:将图分解为边双连通分量(EBCC)
    2. 缩点建树:将每个边双连通分量缩成一个点,构建缩点树
    3. 树形动态规划:在缩点树上进行动态规划,找到最优的赋值方案

    代码实现详细步骤

    1. 数据结构和变量定义

    #include <bits/stdc++.h>
    #define ll long long
    #define pii pair<int, int>
    #define fi first
    #define se second
    #define m_p make_pair
    using namespace std;
    
    • 使用 long long 处理大数
    • pii 表示边对(目标节点,边ID)
    • 其他辅助数据结构用于存储图信息和中间结果

    2. 边双连通分量分解

    vector<int> dfn(n, 0), low(n, 0);
    vector<int> cut(m, 0), bel(n, 0), sum(n + 1, 0), sz(n + 1, 0);
    vector<vector<pii>> g(n, vector<pii>());
    vector<vector<int>> G(n + 1, vector<int>());
    
    // 构建原图
    for (int i = 0; i < m; i++) {
        g[U[i]].push_back(m_p(V[i], i));
        g[V[i]].push_back(m_p(U[i], i));
    }
    
    • dfn: DFS序
    • low: Tarjan算法中的low值
    • cut: 标记边是否为桥
    • bel: 节点所属的边双连通分量编号
    • sum: 每个边双连通分量的节点数
    • sz: 子树大小

    3. Tarjan算法找桥

    int dfncnt = 0, idx = 0;
    function<void(int, int)> tarjan = [&](int u, int pre) {
        dfn[u] = low[u] = ++dfncnt;
    
        for (auto [v, id] : g[u])
            if (pre != id) {  // 避免重复访问父边
                if (!dfn[v]) {
                    tarjan(v, id);
                    low[u] = min(low[u], low[v]);
                    
                    // 判断是否为桥
                    if (low[v] > dfn[u]) {
                        cut[id] = true;
                    }
                } else {
                    low[u] = min(low[u], dfn[v]);
                }
            }
    };
    tarjan(0, -1);
    
    • 使用DFS遍历图
    • 通过比较 low[v] > dfn[u] 判断边是否为桥
    • 标记所有的桥边

    4. 识别边双连通分量

    function<void(int)> dfs = [&](int u) {
        bel[u] = idx, ++sum[idx];  // 标记所属分量并计数
        
        for (auto [v, id] : g[u])
            if (!cut[id] && !bel[v]) {  // 只遍历非桥边
                dfs(v);
            }
    };
    
    for (int i = 0; i < n; i++)
        if (!bel[i]) {
            ++idx, dfs(i);  // 新的边双连通分量
        }
    
    • 通过DFS遍历非桥边,识别边双连通分量
    • 统计每个分量中的节点数

    5. 构建缩点树

    for (int i = 0; i < m; i++)
        if (bel[U[i]] != bel[V[i]]) {
            G[bel[U[i]]].push_back(bel[V[i]]);
            G[bel[V[i]]].push_back(bel[U[i]]);
        }
    
    • 桥连接不同的边双连通分量
    • 构建缩点树,节点为边双连通分量

    6. 树形动态规划

    auto C = [&](int x) {
        return 1ll * x * (x - 1) / 2;  // 组合数C(x,2)
    };
    
    vector<ll> dp(n + 1);
    
    // 第一次DFS:计算子树大小
    function<void(int, int)> pdfs = [&](int u, int fno) {
        sz[u] = sum[u];  // 初始化为当前分量的节点数
        
        for (int v : G[u])
            if (v != fno) {
                pdfs(v, u);
                sz[u] += sz[v];  // 累加子树大小
            }
    };
    
    // 第二次DFS:计算DP值
    function<void(int, int)> dypr = [&](int u, int fno) {
        for (int v : G[u])
            if (v != fno) {
                dp[v] = dp[u] + C(sz[u] - sz[v]);
                dypr(v, u);
            }
    };
    
    • C(x): 计算x个节点中相同值节点对的数量
    • pdfs: 预处理计算每个节点的子树大小
    • dypr: 动态规划,计算移除子树后的代价

    7. 寻找最优根节点

    pdfs(1, 0), dypr(1, 0);
    int rt = -1;
    
    for (int i = 1; i <= idx; i++)
        if (!(G[i].size() - (i != 1 ? 1 : 0))) {  // 叶子节点
            dp[i] += C(sum[i]);  // 加上当前分量的内部代价
            
            if (rt == -1 || dp[i] < dp[rt]) {
                rt = i;  // 选择代价最小的叶子作为根
            }
        }
    
    • 遍历所有叶子节点(度为1的节点)
    • 选择代价最小的叶子作为新的根节点

    8. 重新计算并找到最小代价

    dp[rt] = 0;
    pdfs(rt, 0), dypr(rt, 0);  // 以新根重新计算
    ll minn = 1e18;
    
    for (int i = 1; i <= idx; i++)
        if (!(G[i].size() - (i != rt ? 1 : 0))) {  // 叶子节点
            minn = min(minn, dp[i] + C(sum[i]));
        }
    
    • 以新选择的根节点重新进行树形DP
    • 找到所有叶子节点中的最小代价

    9. 计算最终答案

    return C(n) - minn;
    
    • 总节点对数为 C(n)
    • 最大多样性 = 总对数 - 最小相同值节点对数

    算法标签

    • 图论

    • 边双连通分量

    • 缩点

    • 树形动态规划

    • 贪心

    算法复杂度

    • 时间复杂度: O(n + m)
      • Tarjan算法: O(n + m)
      • 两次DFS: O(n)
    • 空间复杂度: O(n + m)

    关键洞察

    1. 边双连通分量内值相同: 由于环的存在,同一分量内的节点必须赋值相同
    2. 树结构保证单调性: 在缩点树上,从根到叶子的路径赋值单调即可保证所有路径单调
    3. 最小化相同值对: 通过树形DP找到使相同值节点对数最小的赋值方案

    这个实现巧妙地结合了图论分解和动态规划,高效地解决了这个复杂的问题。

    • 1

    信息

    ID
    3130
    时间
    3000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    17
    已通过
    4
    上传者