图论学习笔记 4 - 仙人掌图

先扔张图:


为了提前了解我们采用的方法,请先阅读《图论学习笔记 3》。

仙人掌图的定义:一个连通图,且每条边只出现在至多一个环中。

这个图就是仙人掌图。

这个图也是仙人掌图。

而这个图就不是仙人掌图了。

很容易发现,仙人掌图就是在树上连了若干条边(≥1\ge 11 条)。所以可以视为仙人掌图是基环树的扩展。


众所周知,我们通过想象基环树的深搜树形态解决了基环树的一些问题,所以也考虑想象仙人掌图的深搜树。

这里就直接给图了:

很容易发现以下性质:

  • 仙人掌图中,每一条回边互不相交且与环一一对应,环由回边与祖先到子孙的链构成。(这个比较显然,可以简单理解)

  • 任何一个环的 upupup 点或者是 dndndn 点,其子树一定包含的是完整的环。

很容易感性理解。upupup 的子树一定包含所有的环点,dndndn 的子树一定不包含所有的环点。所以就可以证了。

  • 在每一个点的子树中,至多有一个没有遍历到其对应的 upupup 点的 dndndn 点。

考虑反证法,设我们有一个点 xxx,其子树里面有两个没有遍历到其对应的 upupup 点的 dndndn 点。设两个 upupup 点为 up1,up2up1,up2up1,up2,设两个 dndndn 点为 dn1,dn2dn1,dn2dn1,dn2

很容易发现,两个 upupup 点一定是 xxx 的祖先。不妨这里设 up1up1up1up2up2up2 的祖先。

容易发现,up1up1up1xxx 的一整条路径都出现在了两个环中,所以这样是矛盾的,原结论得证。


T425915 仙人掌图最大独立集

首先默认已经做过基环树版本的 骑士 那道题了。

回顾一下那道题的做法,可以发现对于一条回边 up→dnup \to dnupdn 构成的环,我们是在原有的 没有上司的舞会那道题 dpdpdp 状态上进行了一个升维,在记录子树根结点有没有选的同时还记录了 dndndn 有没有选。

考虑将这种方法扩展到仙人掌图上面,但是我们发现一个结点的子树里面可能有很多的 dndndn(并不是只有一个环了),而且数量是会变化的,而我们又不可能 2n2^n2n 记录所有的 dndndn 选没选,所以在状态设计方面遇到了一点“困难”。

但是我们发现,上述结论 3 就是为我们量身定制的,因为其他已经被考虑过的环已经不用再考虑(这是仙人掌图,不会出现环套环的情况),所以只需要把目光放在这个没有走到 upupupdndndn 点就行了。

所以就可以设计状态了。至此思路已经成型,直接把那道题的代码拿过来改改就行了。

#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;
}

可以发现,这份代码和骑士那道题的那份代码是差不多了,主要改动就是把原来的单个元素 upupup 变成了一个数组 up[]

最大独立集时间复杂度

学了这么多独立集,来总结一下各种图的求最大独立集的时间复杂度。

首先对于一般图,求独立集属于 NP 完全问题,也就是只能暴力枚举。

对于二分图,设点数 nnn,边数 mmm,则可以使用网络流将复杂度变成 O(mn)O(m \sqrt n)O(mn) 的级别(网络流还没学过qwq)。

对于仙人掌图,也包含了树和基环树,可以使用深搜树 + dp 的方式把复杂度变成 O(n+m)O(n+m)O(n+m) 的级别。

对仙人掌图进行缩点

发现仙人掌是一堆环通过一堆边拼在一起,两两之间彼此不相交。显然会发现,这个时候若把每一个环都看作是一个点,那么最终就会变成一棵树,在上面可以跑各种各样的科技。

那么怎么看作是一个点呢???通过 P5236 的圆方树做法,我们想到了可以配合点双连通分量进行缩点。

考虑仙人掌图中的每一个点双,不难发现是这样子的:

  • 每一个点双恰好是一个简单环,或者是恰为一条非环边。

显然简单环一定是极大的点双连通分量,但是剩下的边中的每一条边也会变成一个点双连通分量,所以上面的那句话是正确的。

例如这个仙人掌图的深搜树,树边用实线,回边用虚线:

所有的点双连通分量现在已经用彩色线圈出来了,在旁边写上了新的编号。

最终得到的园方树如下:

以前我们就知道圆方树有着求必经点和可经点的作用,但是在对仙人掌图缩点的时候它会有更大的作用。

观察绿色点双连通分量,发现其 upupup 结点为 333 号结点(这里 upupup 结点为点双连通分量最高的结点,而 dndndn 结点为点双连通分量最低的结点),而对应到圆方树里面,发现 333 就是其父亲!

整理一下可得:

新性质:圆方树里方点的父亲恰好是深搜树中其对应的点双里最高的点。


考虑另一件事情:如何处理环上两点之间的最短距离。

不妨在圆方树里面,针对 141414 号方点进行举例,即原深搜树中的绿色点双连通分量的部分。后面的抽象文字如果有不懂的可以对照着图片想想为什么。

因为获取距离的方法较为套路,这里就简要讲讲思考在这个方法的过程。

发现一个环一定是一条链加上一条回边,这是不由分说地。而且还可以发现两点之间的距离要么是在这条构成环的链上的距离(也就是直接从深度小的点通过走链走到深度大的点),要么是从另一个方向过来的距离(也就是先从深度小的点走到 upupup,然后再走 dndndn,再有 dndndn 走到深度较大的点)

显然两点之间的最短距离就是上面两片粗体字得到答案的 min⁡\minmin 值。但是这两个答案太难算了,有没有一些突破口呢?

显然是有的,因为我们可以发现第一托路径的答案 +++ 第二托路径的答案 === 整个环的路径权值和。

那么就可以通过路径权值和来快速通过前者得到后者。而且路径权值和是一个定值,因为其他地方没地方放了,就直接把环里面的路径权值和作为这个环对应的方点的点权即可。

而对于前面提到的第一托可能的路径,可以使用在链上 upupup 到这个点的距离来记录。这样就做完了。最终还有每一个方点到其 upupup 的距离设为 000

总结一下:

  • 方点到其 upupup 的边权设为 000

  • 圆点到方点的边权,设为在深搜树中方点的 upupup 到圆点的链上距离。

  • 将方点的点权设为环内所有边权的总和。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值