图论学习笔记 4 - 仙人掌图
先扔张图:

为了提前了解我们采用的方法,请先阅读《图论学习笔记 3》。
仙人掌图的定义:一个连通图,且每条边只出现在至多一个环中。
这个图就是仙人掌图。
这个图也是仙人掌图。
而这个图就不是仙人掌图了。
很容易发现,仙人掌图就是在树上连了若干条边(\(\ge 1\) 条)。所以可以视为仙人掌图是基环树的扩展。
众所周知,我们通过想象基环树的深搜树形态解决了基环树的一些问题,所以也考虑想象仙人掌图的深搜树。
这里就直接给图了:

很容易发现以下性质:
-
仙人掌图中,每一条回边互不相交且与环一一对应,环由回边与祖先到子孙的链构成。(这个比较显然,可以简单理解)
-
任何一个环的 \(up\) 点或者是 \(dn\) 点,其子树一定包含的是完整的环。
很容易感性理解。\(up\) 的子树一定包含所有的环点,\(dn\) 的子树一定不包含所有的环点。所以就可以证了。
- 在每一个点的子树中,至多有一个没有遍历到其对应的 \(up\) 点的 \(dn\) 点。
考虑反证法,设我们有一个点 \(x\),其子树里面有两个没有遍历到其对应的 \(up\) 点的 \(dn\) 点。设两个 \(up\) 点为 \(up1,up2\),设两个 \(dn\) 点为 \(dn1,dn2\)。
很容易发现,两个 \(up\) 点一定是 \(x\) 的祖先。不妨这里设 \(up1\) 是 \(up2\) 的祖先。

容易发现,\(up1\) 到 \(x\) 的一整条路径都出现在了两个环中,所以这样是矛盾的,原结论得证。
T425915 仙人掌图最大独立集
首先默认已经做过基环树版本的 骑士 那道题了。
回顾一下那道题的做法,可以发现对于一条回边 \(up \to dn\) 构成的环,我们是在原有的 没有上司的舞会那道题 \(dp\) 状态上进行了一个升维,在记录子树根结点有没有选的同时还记录了 \(dn\) 有没有选。
考虑将这种方法扩展到仙人掌图上面,但是我们发现一个结点的子树里面可能有很多的 \(dn\)(并不是只有一个环了),而且数量是会变化的,而我们又不可能 \(2^n\) 记录所有的 \(dn\) 选没选,所以在状态设计方面遇到了一点“困难”。
但是我们发现,上述结论 3 就是为我们量身定制的,因为其他已经被考虑过的环已经不用再考虑(这是仙人掌图,不会出现环套环的情况),所以只需要把目光放在这个没有走到 \(up\) 的 \(dn\) 点就行了。
所以就可以设计状态了。至此思路已经成型,直接把那道题的代码拿过来改改就行了。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 50010;
int n;
vector<int> v[N];
int dp[N][4];
bool stk[N], vis[N];
int up[N];
int m;
void dfs(int u, int pre) {
vis[u] = stk[u] = 1;
dp[u][1] = dp[u][3] = 1, dp[u][0] = dp[u][2] = 0;
for (auto i : v[u])
if (!vis[i]) {
dfs(i, u);
if (up[i] != 0 && up[i] != u)
up[u] = up[i];
if (u == up[i]) {
dp[u][0] += max(max(dp[i][0], dp[i][1]), max(dp[i][2], dp[i][3]));
dp[u][1] += dp[i][0];
dp[u][2] += max(max(dp[i][0], dp[i][1]), max(dp[i][2], dp[i][3]));//很容易发现,对于一个环结束的时候,可以取 dp[2] 和 dp[0] 的增量相同,dp[3] 和 dp[1] 的增量相同
dp[u][3] += dp[i][0];
} else {
dp[u][0] += max(dp[i][0], dp[i][1]);
dp[u][1] += dp[i][0];
dp[u][2] += max(dp[i][2], dp[i][3]);
dp[u][3] += dp[i][2];
}
} else if (i != pre && stk[i])
up[u] = i, dp[u][1] = dp[u][2] = -1e16;
stk[u] = 0;
}
signed main() {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y;
v[x].push_back(y), v[y].push_back(x);
}
dfs(1, 0);
cout << max(dp[1][0], dp[1][1]) << endl;
return 0;
}
可以发现,这份代码和骑士那道题的那份代码是差不多了,主要改动就是把原来的单个元素 \(up\) 变成了一个数组 up[]。
最大独立集时间复杂度
学了这么多独立集,来总结一下各种图的求最大独立集的时间复杂度。
首先对于一般图,求独立集属于 NP 完全问题,也就是只能暴力枚举。
对于二分图,设点数 \(n\),边数 \(m\),则可以使用网络流将复杂度变成 \(O(m \sqrt n)\) 的级别(网络流还没学过qwq)。
对于仙人掌图,也包含了树和基环树,可以使用深搜树 + dp 的方式把复杂度变成 \(O(n+m)\) 的级别。
对仙人掌图进行缩点
发现仙人掌是一堆环通过一堆边拼在一起,两两之间彼此不相交。显然会发现,这个时候若把每一个环都看作是一个点,那么最终就会变成一棵树,在上面可以跑各种各样的科技。
那么怎么看作是一个点呢???通过 P5236 的圆方树做法,我们想到了可以配合点双连通分量进行缩点。
考虑仙人掌图中的每一个点双,不难发现是这样子的:
- 每一个点双恰好是一个简单环,或者是恰为一条非环边。
显然简单环一定是极大的点双连通分量,但是剩下的边中的每一条边也会变成一个点双连通分量,所以上面的那句话是正确的。
例如这个仙人掌图的深搜树,树边用实线,回边用虚线:

所有的点双连通分量现在已经用彩色线圈出来了,在旁边写上了新的编号。
最终得到的园方树如下:

以前我们就知道圆方树有着求必经点和可经点的作用,但是在对仙人掌图缩点的时候它会有更大的作用。
观察绿色点双连通分量,发现其 \(up\) 结点为 \(3\) 号结点(这里 \(up\) 结点为点双连通分量最高的结点,而 \(dn\) 结点为点双连通分量最低的结点),而对应到圆方树里面,发现 \(3\) 就是其父亲!
整理一下可得:
新性质:圆方树里方点的父亲恰好是深搜树中其对应的点双里最高的点。
考虑另一件事情:如何处理环上两点之间的最短距离。
不妨在圆方树里面,针对 \(14\) 号方点进行举例,即原深搜树中的绿色点双连通分量的部分。后面的抽象文字如果有不懂的可以对照着图片想想为什么。
因为获取距离的方法较为套路,这里就简要讲讲思考在这个方法的过程。
发现一个环一定是一条链加上一条回边,这是不由分说地。而且还可以发现两点之间的距离要么是在这条构成环的链上的距离(也就是直接从深度小的点通过走链走到深度大的点),要么是从另一个方向过来的距离(也就是先从深度小的点走到 \(up\),然后再走 \(dn\),再有 \(dn\) 走到深度较大的点)
显然两点之间的最短距离就是上面两片粗体字得到答案的 \(\min\) 值。但是这两个答案太难算了,有没有一些突破口呢?
显然是有的,因为我们可以发现第一托路径的答案 \(+\) 第二托路径的答案 \(=\) 整个环的路径权值和。
那么就可以通过路径权值和来快速通过前者得到后者。而且路径权值和是一个定值,因为其他地方没地方放了,就直接把环里面的路径权值和作为这个环对应的方点的点权即可。
而对于前面提到的第一托可能的路径,可以使用在链上 \(up\) 到这个点的距离来记录。这样就做完了。最终还有每一个方点到其 \(up\) 的距离设为 \(0\)。
总结一下:
-
方点到其 \(up\) 的边权设为 \(0\)。
-
圆点到方点的边权,设为在深搜树中方点的 \(up\) 到圆点的链上距离。
-
将方点的点权设为环内所有边权的总和。

浙公网安备 33010602011771号