先扔张图:

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

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

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

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

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