「Luogu P4516」[JSOI2018] 潜入行动

题目

外星人又双叒叕要攻打地球了,外星母舰已经向地球航行!这一次,JYY 已经联系好了黄金舰队,打算联合所有 JSOIer 抵御外星人的进攻。

在黄金舰队就位之前,JYY 打算事先了解外星人的进攻计划。现在,携带了监听设备的特工已经秘密潜入了外星人的母舰,准备对外星人的通信实施监听。

外星人的母舰可以看成是一棵 \(n\) 个节点、 \(n-1\) 条边的无向树,树上的节点用 \(1,2,\cdots,n\) 编号。JYY 的特工已经装备了隐形模块,可以在外星人母舰中不受限制地活动,可以神不知鬼不觉地在节点上安装监听设备。

如果在节点 \(u\) 上安装监听设备,则 JYY 能够监听与 \(u\) 直接相邻所有的节点的通信。换言之,如果在节点 \(u\) 安装监听设备,则对于树中每一条边 \((u,v)\) ,节点 \(v\) 都会被监听。特别注意放置在节点 \(u\) 的监听设备并不监听 \(u\) 本身的通信,这是 JYY 特别为了防止外星人察觉部署的战术。

JYY 的特工一共携带了 \(k\) 个监听设备,现在 JYY 想知道,有多少种不同的放置监听设备的方法,能够使得母舰上所有节点的通信都被监听?为了避免浪费,每个节点至多只能安装一个监听设备,且监听设备必须被用完

Luogu

分析

求解以 \(u\) 为根的子树的答案时,如果是考虑整个子树都被覆盖的话,那么求解 \(fa[u]\) 的答案时,显然如果在 \(fa[u]\) 放置监听设备,那么 \(u\) 未被覆盖的情况也应该计入 \(fa[u]\) 的答案,因此要分别求解 \(u\) 是否被覆盖的答案,此外,用儿子结点 \(v\) 的答案更新 \(u\) 的答案时,若 \(u\) 不放置监听设备,要覆盖 \(u\) 的话至少有一个儿子结点要放置监听设备,而儿子结点不放置监听设备的答案也会更新 \(u\) 的答案,因此还需要用一维枚举是否在该店放置监听设备。

综上,我们设 \(f[u][i][0 / 1][0 / 1]\) 表示当前考虑到结点 \(u\)、在 \(u\) 为根的子树内一共放置 \(i\) 个监听设备、\(u\) 结点是否放置监听设备、\(u\) 结点是否被儿子结点监听到。于是可以得到以下状态转移方程(较复杂,细节有点小多):

\[\begin{aligned}f[u][i+j][0][0] = &\sum f[u][i][0][0] \times f[v][j][0][1]\\ f[u][i+j][0][1] = &\sum f[u][i][0][0] \times f[v][j][1][1]\\ &+ f[u][i][0][1] \times (f[v][j][0][1] + f[v][j][1][1])\\ f[u][i+j][1][0] = &\sum f[u][i][1][0] \times (f[v][j][0][1] + f[v][j][0][0])\\ f[u][i+j][1][1] = &\sum f[u][i][1][1] \times (f[v][j][1][0]+f[v][j][1][1]\\ &+ f[v][j][0][0] + f[v][j][0][1])\\ &+ f[u][i][1][0] \times (f[v][j][1][0] + f[v][j][1][1])\end{aligned} \]

这时候会发现复杂度是 \(O(nk^2)\),显然没法过。由于 \(k\) 比较小,而且如果 \(size[u]<k\) 的话,枚举第二维的时候只需要枚举到(还没用当前枚举的儿子结点的 \(size[v]\) 更新的)\(size[u]\) 就好了,这个优化看似是常数级别的,其实它实际的复杂度只有 \(O(nk)\). 考虑结点 \(u\), 它的儿子结点中,最多只有 \(size[u] / k\)\(size[v]>k\) 的结点,对于这一部分儿子结点,我们枚举 \(j\) 这一部分的总次数加起来是 \(\sum_{v \in son[u]} size[v] / k \cdot k=\sum_{v\in son[u]}size[v]\), 对于 \(size[v]\leq k\) 这部分的结点,枚举 \(j\) 的次数加起来也是 \(\sum_{v \in son[u]}size[v]\) 的,合起来就是 \(size[u]\), 而枚举 \(i\) 的次数显然是 \(\leq k\) 的,于是在结点 \(u\) 这一局部的贡献是 \(\leq k \cdot |son[u]|\) 的,相当于这个优化直接去掉了一维 \(k\), 从而最后整棵树的枚举次数是 \(O(k \cdot \sum_{u \in V}|son[u]|) = O(nk)\).

代码

#include <bits/stdc++.h>

using i64 = long long;

constexpr int N = 1e5 + 5;
constexpr i64 mod = 1e9 + 7;

int n, k;
std::vector<int> g[N];
int sz[N];
int f[N][101][2][2], t[N][101][2][2];

int mul(int x, int y) {
	return (int)((1ll * x * y) % mod);
}

int add(int x, int y) {
	return (int)((1ll * x + 1ll * y) % mod);
}

void dfs(int u, int fa) {
	sz[u] = 1;
	f[u][0][0][0] = f[u][1][1][0] = 1;

	for (int v : g[u]) {
		if (v == fa) continue;
		dfs(v, u);

		for (int i = 0; i <= std::min(sz[u], k); ++i) { // 注意更新顺序导致需要考虑的细节:备份枚举 v 之前的答案然后再用它来计算
			t[u][i][0][0] = f[u][i][0][0], f[u][i][0][0] = 0;
			t[u][i][0][1] = f[u][i][0][1], f[u][i][0][1] = 0;
			t[u][i][1][0] = f[u][i][1][0], f[u][i][1][0] = 0;
			t[u][i][1][1] = f[u][i][1][1], f[u][i][1][1] = 0;
		}

		for (int i = 0; i <= std::min(sz[u], k); ++i) {
			for (int j = 0; j <= std::min(sz[v], k - i); ++j) {
				f[u][i+j][0][0] = add(f[u][i+j][0][0],
					mul(t[u][i][0][0], f[v][j][0][1]));

				f[u][i+j][0][1] = add(f[u][i+j][0][1],
					add(mul(t[u][i][0][0], f[v][j][1][1]),
					mul(t[u][i][0][1],
						add(f[v][j][0][1], f[v][j][1][1]))));

				f[u][i+j][1][0] = add(f[u][i+j][1][0],
					mul(t[u][i][1][0], add(
					f[v][j][0][0], f[v][j][0][1])));

				f[u][i+j][1][1] = add(f[u][i+j][1][1],
					mul(t[u][i][1][1], add(
					add(f[v][j][1][0], f[v][j][1][1]),
					add(f[v][j][0][1], f[v][j][0][0]))));
				f[u][i+j][1][1] = add(f[u][i+j][1][1],
					mul(t[u][i][1][0], 
					add(f[v][j][1][0], f[v][j][1][1])));
			}
		}

		sz[u] += sz[v];
	}
}

int main() {
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);

	std::cin >> n >> k;
	for (int i = 1, u, v; i < n; ++i) {
		std::cin >> u >> v;
		g[u].push_back(v), g[v].push_back(u);
	}

	dfs(1, 0);

	std::cout << add(f[1][k][1][1], f[1][k][0][1]);
	return 0;
}
posted @ 2024-11-26 14:35  小蒟蒻hlw  阅读(12)  评论(0)    收藏  举报