1 条题解
-
0
题目解析
题目大意 给定一个无向连通图(可能有重边),要求给每个节点分配一个整数值,使得对于任意一条不重复经过同一条边的路径,节点值序列必须是单调递增或单调递减的。定义一个数组的多样性为数组中不同值的节点对的数量。求所有满足条件的赋值方案中,多样性的最大值。
关键性质
- 边双连通分量内值相同:在任意边双连通分量(EBCC)内,所有节点必须具有相同的值。否则,存在环会导致路径值序列非单调。
- 缩点树结构:将每个边双连通分量缩成一个点,得到一棵树(缩点树),树边对应原图中的桥。
- 赋值方案限制:在缩点树上,赋值必须满足从任意根节点出发到任何叶节点的路径上值序列单调。这意味着赋值可以看作基于树深度的函数,但值可以重复。
- 多样性最大化:多样性最大化等价于最小化值相同的节点对数。值相同的节点对数为所有值对应的节点数组合数之和,即 (\sum_{v} \binom{s_v}{2}),其中 (s_v) 是值 (v) 的节点数。因此,需要最小化 (\sum_{v} s_v^2)。
算法步骤
- 边双连通分量分解:使用 Tarjan 算法求出所有桥,并识别边双连通分量。
- 构建缩点树:将每个边双连通分量缩成一个节点,缩点树的边对应原图的桥。
- 树形动态规划:在缩点树上,通过两次 DFS 计算每个节点的子树大小,并找到最优根节点,使得按深度赋值时,同一深度的节点数平方和最小。
- 计算答案:总节点对数为 (\binom{n}{2}),减去最小相同值对数,得到最大多样性。
代码实现详细解析
算法概述
这个问题要求我们在一个无向连通图中找到一种节点赋值方案,使得任意简单路径上的节点值序列都是单调的(递增或递减),同时最大化不同值节点对的数量。
核心思路
- 边双连通分量分解:将图分解为边双连通分量(EBCC)
- 缩点建树:将每个边双连通分量缩成一个点,构建缩点树
- 树形动态规划:在缩点树上进行动态规划,找到最优的赋值方案
代码实现详细步骤
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)
关键洞察
- 边双连通分量内值相同: 由于环的存在,同一分量内的节点必须赋值相同
- 树结构保证单调性: 在缩点树上,从根到叶子的路径赋值单调即可保证所有路径单调
- 最小化相同值对: 通过树形DP找到使相同值节点对数最小的赋值方案
这个实现巧妙地结合了图论分解和动态规划,高效地解决了这个复杂的问题。
- 1
信息
- ID
- 3130
- 时间
- 3000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 17
- 已通过
- 4
- 上传者