题解:AcWing 352 闇の連鎖
【题目来源】
AcWing:352. 闇の連鎖 - AcWing题库
【题目描述】
传说中的暗之连锁被人们称为 Dark。
Dark 是人类内心的黑暗的产物,古今中外的勇者们都试图打倒它。
经过研究,你发现 Dark 呈现无向图的结构,图中有 \(N\) 个节点和两类边,一类边被称为主要边,而另一类被称为附加边。
Dark 有 \(N – 1\) 条主要边,并且 Dark 的任意两个节点之间都存在一条只由主要边构成的路径。
另外,Dark 还有 \(M\) 条附加边。
你的任务是把 Dark 斩为不连通的两部分。
一开始 Dark 的附加边都处于无敌状态,你只能选择一条主要边切断。
一旦你切断了一条主要边,Dark 就会进入防御模式,主要边会变为无敌的而附加边可以被切断。
但是你的能力只能再切断 Dark 的一条附加边。
现在你想要知道,一共有多少种方案可以击败 Dark。
注意,就算你第一步切断主要边之后就已经把 Dark 斩为两截,你也需要切断一条附加边才算击败了 Dark。
【输入】
第一行包含两个整数 \(N\) 和 \(M\)。
之后 \(N – 1\) 行,每行包括两个整数 \(A\) 和 \(B\),表示 \(A\) 和 \(B\) 之间有一条主要边。
之后 \(M\) 行以同样的格式给出附加边。
【输出】
输出一个整数表示答案。
【输入样例】
4 1
1 2
2 3
1 4
3 4
【输出样例】
3
【核心思想】
-
问题分析:给定一棵由 \(N-1\) 条主要边构成的树,以及 \(M\) 条附加边。需要选择一条主要边切断,再选择一条附加边切断,使得图不连通。求满足条件的方案数。这是一个树上差分 + LCA问题,关键在于利用差分数组统计每条主要边被多少条附加边覆盖,然后根据覆盖数量计算方案数。
-
算法选择:
- 树上差分:将路径统计问题转化为单点更新,通过
d[u]++, d[v]++, d[lca]-=2标记路径 - LCA(最近公共祖先):使用倍增法在 \(O(\log N)\) 时间内求两个节点的LCA
- DFS统计:通过后序遍历累加子树差分值,得到每条边被覆盖的次数
- 树上差分:将路径统计问题转化为单点更新,通过
-
关键步骤:
- 建树:读取 \(N-1\) 条主要边,构建树的邻接表
- LCA预处理:使用BFS预处理每个节点的深度和倍增祖先数组
fa[i][j] - 差分标记(处理 \(M\) 条附加边):
- 对于每条附加边 \((u, v)\),计算 \(p = lca(u, v)\)
d[u]++:路径起点加1d[v]++:路径终点加1d[p] -= 2:LCA处减2(差分思想,抵消重复计算)
- DFS统计:
- 后序遍历累加子树的差分值:
res += d[u] - 对于每条主要边(连接父节点 \(fa\) 和子节点 \(u\)):
- 若子树覆盖次数 \(s = 0\):该边未被任何附加边覆盖,切断后需切断任意一条附加边,贡献 \(M\) 种方案
- 若子树覆盖次数 \(s = 1\):该边被恰好1条附加边覆盖,切断后需切断那条附加边,贡献 \(1\) 种方案
- 若子树覆盖次数 \(s \geq 2\):该边被多条附加边覆盖,切断后无法通过切断一条附加边使图不连通,贡献 \(0\) 种方案
- 后序遍历累加子树的差分值:
- 累加所有方案数得到答案
-
时间/空间复杂度:
- 时间复杂度:\(O((N + M) \log N)\),LCA预处理 \(O(N \log N)\),每条附加边处理 \(O(\log N)\),DFS统计 \(O(N)\)
- 空间复杂度:\(O(N \log N)\),倍增祖先数组
fa[N][17]和邻接表
-
树上差分的核心思想:
- 路径统计降维:将树路径上的统计问题转化为端点的单点更新,通过
d[u]++, d[v]++, d[lca]-=2实现 \(O(1)\) 路径标记 - 差分还原:通过DFS后序遍历累加子树差分值,将差分数组还原为每条边的实际覆盖次数
- LCA抵消技巧:在LCA处减2是为了抵消从LCA到根路径上的重复累加,确保只有 \(u\) 到 \(v\) 的路径被正确统计
- 边权转化:通过将边权下放到子节点,将边的问题转化为节点的问题,方便差分处理
- 适用于树上路径统计、路径覆盖次数查询等问题
- 路径统计降维:将树路径上的统计问题转化为端点的单点更新,通过
【算法标签】
树上差分
【代码详解】
#include <bits/stdc++.h>
using namespace std;
// 定义数组最大容量
const int N = 100005, M = 200005 * 2;
// 全局变量声明
int n; // 节点数量
int m; // 操作次数(查询次数)
int ans; // 最终答案
int h[N]; // 邻接表头结点
int e[M]; // 存储边的终点
int ne[M]; // 存储下一条边的索引
int idx; // 边的编号计数器
int d[N]; // 差分数组,记录节点被访问次数的变化
int dep[N]; // 节点深度
int fa[N][17]; // 倍增法求LCA的祖先数组,fa[i][j]表示节点i的第2^j个祖先
queue<int> q; // BFS队列
// 添加一条无向边 a <-> b
void add(int a, int b)
{
e[idx] = b; // 存储边的终点
ne[idx] = h[a]; // 头插法
h[a] = idx++; // 更新头结点
}
// 预处理节点深度和祖先信息(BFS)
void bfs(int root)
{
// 初始化深度数组为无穷大
memset(dep, 0x3f, sizeof(dep));
dep[0] = 0; // 虚拟节点0的深度为0
dep[root] = 1; // 根节点深度为1
q.push(root); // 根节点入队
// BFS遍历整棵树
while (!q.empty())
{
int t = q.front(); // 取出队首节点
q.pop();
// 遍历当前节点的所有邻接节点
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i]; // 邻接节点
// 如果节点j尚未计算深度
if (dep[j] > dep[t] + 1)
{
dep[j] = dep[t] + 1; // 更新深度
q.push(j); // 入队
fa[j][0] = t; // j的第2^0=1个祖先是其父节点
// 倍增预处理:计算j的所有2^k级祖先
for (int k = 1; k <= 16; k++)
{
// j的第2^k级祖先 = j的第2^(k-1)级祖先的第2^(k-1)级祖先
fa[j][k] = fa[fa[j][k - 1]][k - 1];
}
}
}
}
}
// LCA倍增算法:计算节点x和节点y的最近公共祖先
int lca(int x, int y)
{
// 保证x是深度较大的节点
if (dep[x] < dep[y])
swap(x, y);
// 步骤1:将节点x和节点y调整到同一深度
// 例如深度相差11,先向上走2^3=8,再走2^1=2,最后走2^0=1
for (int k = 16; k >= 0; k--) // 从最大的跳跃步数开始
if (dep[fa[x][k]] >= dep[y])
x = fa[x][k];
// 如果调整后x和y重合,说明y就是LCA
if (x == y)
return x;
// 步骤2:两个节点同时向上跳,直到它们的父节点相同
// 例如向上跳11,先向上走2^3=8,再走2^1=2,最后走2^0=1
for (int k = 16; k >= 0; k--)
{
// 如果向上跳2^k后不相同,说明还没有到达LCA的下一层
if (fa[x][k] != fa[y][k])
{
x = fa[x][k];
y = fa[y][k];
}
}
// 返回最近公共祖先(x或y的父节点)
return fa[x][0];
}
// DFS遍历,计算子树中被覆盖的节点数并返回给父节点
int dfs(int u, int fa)
{
int res = d[u]; // 当前节点的覆盖次数
// 遍历所有子节点
for (int i = h[u]; i != -1; i = ne[i])
{
int v = e[i]; // 子节点
if (v == fa) // 跳过父节点,防止回到上一层
continue;
int s = dfs(v, u); // 递归计算子树
if (s == 0) // 如果子树中没有被覆盖的节点
ans += m; // 需要新建一个集合覆盖该子树
else if (s == 1) // 如果子树中只有1个被覆盖的节点
ans++; // 需要额外的一条边
res += s; // 累加到父节点
}
return res; // 返回当前子树总的覆盖次数
}
// 主函数入口
int main()
{
// 读取节点数和操作次数
cin >> n >> m;
// 初始化邻接表
memset(h, -1, sizeof(h));
// 读取n-1条边,构建树
for (int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
add(u, v); // 添加无向边
add(v, u);
}
// 从节点1开始BFS,预处理深度和祖先信息
bfs(1);
// 处理m次操作,使用差分思想
for (int i = 1; i <= m; i++)
{
int u, v;
cin >> u >> v;
int p = lca(u, v); // 计算最近公共祖先
d[u]++; // 路径起点加1
d[v]++; // 路径终点加1
d[p] -= 2; // 公共祖先处减2(差分思想)
}
// DFS遍历整棵树,计算结果
dfs(1, 0);
// 输出最终答案
cout << ans << endl;
return 0;
}
【运行结果】
4 1
1 2
2 3
1 4
3 4
3
浙公网安备 33010602011771号