广义串并联图
定义
不存在同胚于 \(K_4\) 的子图的无向图被称为广义串并联图。
通俗地说,就是不存在四个结点,满足其两两之间都有一条路径相连,且这些路径互不相交。
举例:树与仙人掌都是广义串并联图。
性质
- 广义串并联图是平面图;
- 广义串并联图可以通过以下的操作变成一个单点:
- 删一度点(去断路):选择一个度数为 \(1\) 的点并删掉它和与它连接的边;
- 缩二度点(消串联):选择一个度数为 \(2\) 的点,用一条边替换它和与它相连的两条边;
- 叠合重边(消并联):选择两条重边,用一条边替换它们;
- 广义串并联图在进行上述操作中,每一条边都对应原图的一个子图。
- 上面的三种操作在一般图中的体现称为“广义串并联图方法”。对于一般图 \(G=(V,E)\),令 \(n=|V|\),\(m=|E|\),如果满足 \(m−n\le k\),其中 \(k\) 很小。运用前两种操作(当然三种操作都用也可以)对图进行操作可以使原图满足 \(n\le 2k\),\(m\le 3k\),然后一些题可以直接暴力做。证明考虑三种操作中 \(m−n\) 的值都不增,操作完后满足每个点的度数都不小于 \(3\),所以 \(3n\le 2m\),于是 \(n\le 2k\),\(m\le 3k\)。
P6790 [SNOI2020] 生成树
在仙人掌上,任取 \(4\) 个不同的点,两两之间最多有 \(4\) 条路径(在环上才能有 \(4\) 条),再加上一条也只有 \(5\) 条,少于 \(6\) 条。
所以原图是广义串并联图。
将贡献缩到边上,设 \(f_e\) 表示 \(e\) 代表的子图中,使得 \(e\) 的两个端点连通的生成树个数。
设 \(g_e\) 表示 \(e\) 代表的子图中,使得 \(e\) 的两个端点不连通的生成森林个数。
删一度点
设当前点为 \(u\),唯一的连边为 \((u,v)\)。
只能通过边 \((u,v)\) 的子图使得 \((u,v)\) 联通,所以将答案乘上 \(f_{(u,v)}\)。
最后删掉点 \(u\) 和边 \((u,v)\)。
缩二度点
设当前点为 \(u\),两条连边分别为 \((u,x)\) 和 \((u,y)\)。
若要让 \(x,y\) 连通,则 \(u,x\) 和 \(u,y\) 均要连通,所以有:
如果 \(x,y\) 不连通,显然只会分成两个连通块,且 \(x,y\) 不属于同一个连通块。如果有三个连通块,最后一定无法形成连通的生成树。
所以需要满足 \(u,x\) 和 \(u,y\) 恰好一组连通,所以有:
最后删掉点 \(u\) 和其连边,添加 \((u,v)\) 这一条边。
叠合重边
考虑两条边 \(e_1,e_2\),两条边的两个端点为 \(u,v\),设缩完后的边为 \(e_3\)。
如果 \(u,v\) 不连通,则 \(e_1,e_2\) 两条边对应的子图都要不连通,所以有:
如果 \(u,v\) 连通,则 \(e_1,e_2\) 两条边对应的子图恰好有一个连通,如果两个都联通肯定就不会是生成树了,所以有:
最后将 \(e_1,e_2\) 替换成 \(e_3\)。
写法
最开始图中可能会出现重边,用“叠合重边”缩掉就行。
我们考虑用队列维护度数不超过 \(2\) 的点,如果是一度点就直接“删一度点”,如果是二度点就先“缩二度点”,再判断是否可以“叠合重边”。
维护 \(f,g\) 可以用 std::map 维护。
容易发现这样的做法是有正确性的,一定可以将广义串并联图缩成一个点。
代码
#include <bits/stdc++.h>
using namespace std;
const int MaxN = 2e5 + 5, Mod = 998244353;
int n, m, ans = 1;
map<int, array<int, 2>> mp[MaxN];
queue<int> q;
void Add(int u, int v, array<int, 2> f) {
if (mp[u].count(v)) {
auto g = mp[u][v];
g[1] = (1LL * f[1] * g[0] + 1LL * f[0] * g[1]) % Mod;
g[0] = 1LL * f[0] * g[0] % Mod;
mp[u][v] = mp[v][u] = g;
} else {
mp[u][v] = mp[v][u] = f;
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m;
for (int i = 1, u, v; i <= m; i++) {
cin >> u >> v;
Add(u, v, {1, 1});
}
for (int i = 1; i <= n; i++) {
if (mp[i].size() <= 2) q.push(i);
}
for (int u; q.size(); q.pop()) {
u = q.front();
if (mp[u].size() == 1) {
ans = 1LL * ans * (mp[u].begin() -> second)[1] % Mod;
}
if (mp[u].size() == 2) {
auto [x, f] = *mp[u].begin();
auto [y, g] = *mp[u].rbegin();
array<int, 2> tmp;
tmp[0] = (1LL * f[1] * g[0] + 1LL * f[0] * g[1]) % Mod;
tmp[1] = 1LL * f[1] * g[1] % Mod;
Add(x, y, tmp);
}
for (auto [v, f] : mp[u]) {
mp[v].erase(u);
if (mp[v].size() <= 2) q.push(v);
}
mp[u].clear();
}
cout << ans;
return 0;
}
qoj856 Cactus
观察到仙人掌是广义串并联图,考虑使用广义串并联图方法做。
设 \(f_e\) 表示 \(e\) 代表的子图中满足条件,已经确定两个端点的颜色,且两个端点颜色相同的方案数。
设 \(g_e\) 表示 \(e\) 代表的子图中满足条件,已经确定两个端点的颜色,且两个端点颜色不同的方案数。
下面只讲转移,边的变化跟上一道题一摸一样。
删一度点
将答案乘上 \(f+(k-1)g\)。
这里的答案表示确定其中一个点的颜色后方案数是多少,所以最后答案还要乘以 \(k\) 才能输出。
缩二度点
设两条边分别为 \((u,v),(v,w)\),分别记为 \(e_1,e_2\),缩完后的边记为 \(e_3\)。
如果 \(u,w\) 颜色相同,要么 \(u,v,w\) 颜色均相同,或者 \(u,v\) 不相同且 \(v,w\) 不相同,所以:
如果 \(u,w\) 颜色不同,要么 \(u,v\) 颜色相同且 \(v,w\) 颜色不同(对称过去是另一种情况),或者 \(u,v\) 颜色不同且 \(v,w\) 颜色不同,所以:
叠和重边
\(f,g\) 分别相乘就行了。
代码
#include <bits/stdc++.h>
using namespace std;
const int MaxN = 3e5 + 5, Mod = 1e9 + 7;
int T, n, m, k, ans = 1;
map<int, array<int, 2>> mp[MaxN];
queue<int> q;
void Add(int u, int v, array<int, 2> f) {
if (mp[u].count(v)) {
auto g = mp[u][v];
g[0] = 1LL * f[0] * g[0] % Mod;
g[1] = 1LL * f[1] * g[1] % Mod;
mp[u][v] = mp[v][u] = g;
} else {
mp[u][v] = mp[v][u] = f;
}
}
void Clear() {
ans = 1;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
for (cin >> T; T; T--, Clear()) {
cin >> n >> m >> k;
for (int i = 1, u, v; i <= m; i++) {
cin >> u >> v, Add(u, v, {0, 1});
}
for (int i = 1; i <= n; i++) {
if (mp[i].size() <= 2) q.push(i);
}
for (int u; q.size(); q.pop()) {
u = q.front();
if (mp[u].size() == 1) {
auto [v, f] = *mp[u].begin();
ans = 1LL * ans * (f[0] + 1LL * f[1] * (k - 1) % Mod) % Mod;
}
if (mp[u].size() == 2) {
auto [x, f] = *mp[u].begin();
auto [y, g] = *mp[u].rbegin();
array<int, 2> tmp;
tmp[0] = (1LL * f[0] * g[0] + 1LL * f[1] * g[1] % Mod * (k - 1)) % Mod;
tmp[1] = (1LL * f[0] * g[1] + 1LL * f[1] * g[0] + 1LL * f[1] * g[1] % Mod * (k - 2)) % Mod;
Add(x, y, tmp);
}
for (auto [v, f] : mp[u]) {
mp[v].erase(u);
if (mp[v].size() <= 2) q.push(v);
}
mp[u].clear();
}
cout << 1LL * ans * k % Mod << '\n';
}
return 0;
}
AT_abc419_g [ABC419G] Count Simple Paths 2
AT_abc419_g [ABC419G] Count Simple Paths 2
注意到暴搜时间复杂度是 \(O(n2^k)\),所以考虑缩小点数,可以用广义串并联图的前两个操作将原图变成点数为 \(O(k)\) 的图,最后暴搜就可以得到答案,时间复杂度 \(O(k2^k)\)。

浙公网安备 33010602011771号