暗的连锁
题目描述
Dark 是一张无向图,图中有 N 个节点和两类边,一类边被称为主要边,而另一类被称为附加边。Dark 有 N−1 条主要边,并且 Dark 的任意两个节点之间都存在一条只由主要边构成的路径。另外,Dark 还有 M 条附加边。
你的任务是把 Dark 斩为不连通的两部分。一开始 Dark 的附加边都处于无敌状态,你只能选择一条主要边切断。一旦你切断了一条主要边,Dark 就会进入防御模式,主要边会变为无敌的而附加边可以被切断。但是你的能力只能再切断 Dark 的一条附加边。
现在你想要知道,一共有多少种方案可以击败 Dark。注意,就算你第一步切断主要边之后就已经把 Dark 斩为两截,你也需要切断一条附加边才算击败了 Dark。
输入描述
第一行包含两个整数 N 和 M;
之后 N - 1 行,每行包括两个整数 A 和 B,表示 A 和 B 之间有一条主要边;
之后 M 行以同样的格式给出附加边。
输出描述
输出一个整数表示答案。
样例 #1
样例输入 #1
4 1
1 2
2 3
1 4
3 4
样例输出 #1
3
提示
对于 100% 的数据, 1 ≤ N ≤ 1 0 5 , 1 ≤ M ≤ 2 × 1 0 5 1≤N≤10^5,1≤M≤2×10^5 1≤N≤105,1≤M≤2×105。数据保证答案不超过 2 31 − 1 2^{31}−1 231−1。
原题
思路
题目求的是切断一条主要边和一条附加边后使得整张图变为不连通的两部分的方案数。由于题目保证所有主要边构成一棵树,我们可以枚举所有主要边,然后判断有多少个附加边与该主要边组合后能使得整张图变为不连通的两部分。事实上,当我们枚举每个点到其父亲的主要边时,一共有三种情况:
- 当除去直接相连的主要边之外的连通两点的路径个数 = 0 =0 =0 时,删去该主要边即可破坏整张图,任意选择一条次要边都满足条件,贡献为 m m m。
- 当除去直接相连的主要边之外的连通两点的路径个数 = 1 =1 =1 时,删去该主要边后必须删去连通两点的唯一次要边,贡献为 1 1 1。
- 当除去直接相连的主要边之外的连通两点的路径个数 > 1 >1 >1 时,删去该主要边和任意一条次要边都无法破坏整张图,贡献为 0 0 0。
问题就只剩下如何求某个点到其父亲的路径个数。我们可以采用树上差分的方法,对于任意一条连接节点 u 和节点 v 的附加边,我们令 d i f [ u ] + 1 , d i f [ v ] + 1 , d i f [ L C A ( u , v ) ] − 2 dif[u]+1,dif[v]+1,dif[LCA(u,v)]-2 dif[u]+1,dif[v]+1,dif[LCA(u,v)]−2。然后再对树上差分求前缀和,这样所得到的 d i f [ i ] dif[i] dif[i] 就记录了节点 i i i 与它父亲之间的路径个数(除去直接相连的主要边之外)。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
typedef long long ll;
const int MAX = 2e5 + 6;
int n, m;
struct TREE
{
vector<int> e[MAX];
int depth[MAX];
int fa[MAX][26];
int dif[MAX];
void add(int u, int v) // 添加无向边
{
e[u].push_back(v);
e[v].push_back(u);
}
void getDepth(int u, int fath) // 获得每个节点的深度及父亲
{
fa[u][0] = fath;
depth[u] = depth[fath] + 1;
for (auto v : e[u])
{
if (v != fath)
getDepth(v, u);
}
}
void init()
{
getDepth(1, 1);
for (int i = 1; i <= 20; i++)
{
for (int j = 1; j <= n; j++)
{
fa[j][i] = fa[fa[j][i - 1]][i - 1]; // 记录每个节点的倍增祖先
}
}
}
int lca(int x, int y)
{
if (depth[x] < depth[y])
swap(x, y);
// 先将x和y的深度调节成一致的
for (int i = 20; i >= 0; i--)
{
if (depth[fa[x][i]] >= depth[y])
{
x = fa[x][i];
}
}
// 再一起向低深度跳跃,从而找到最近公共祖先
if (x == y)
return x;
for (int i = 20; i >= 0; i--)
{
if (fa[x][i] != fa[y][i])
{
x = fa[x][i];
y = fa[y][i];
}
}
return fa[x][0];
}
void insert(int u, int v) // 树上差分
{
dif[u]++;
dif[v]++;
dif[lca(u, v)] -= 2;
}
void dfs(int u) // 用dfs求出树上差分的前缀和
{
for (auto v : e[u])
{
if (v != fa[u][0])
{
dfs(v);
dif[u] += dif[v];
}
}
}
} tree;
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m;
int u, v;
for (int i = 1; i < n; i++)
{
cin >> u >> v;
tree.add(u, v); // 添加主要边
}
tree.init();
for (int i = 1; i <= m; i++)
{
cin >> u >> v;
tree.insert(u, v); // 添加次要边
}
tree.dfs(1); // 树上差分求前缀和,从而求出每个节点到其父亲节点(除去直接相连的主要边之外的)的路径个数
i64 ans = 0;
for (int i = 2; i <= n; i++)
{
if (tree.dif[i] == 0) // 此时删去主要边即可破坏整张图,所以任意选择一条次要边都满足条件,贡献为m
ans += m;
if (tree.dif[i] == 1) // 此时删去主要边后必须删去连通两点的唯一次要边,贡献为1
ans++;
// 当除去直接相连的主要边之外的路径个数大于1时,删去该主要边和任意一条次要边都无法破坏整张图,贡献为0
}
cout << ans << '\n';
return 0;
}