Luogu P4516 [JSOI2018] 潜入行动

题目描述

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

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

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

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

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

输入格式

输入第一行包含两个整数 n , k n,k n,k ,表示母舰节点的数量 n n n 和监听设备的数量 k k k
接下来 n − 1 n-1 n1 行,每行两个整数 u , v u,v u,v ( 1 ≤ u , v ≤ n ) (1\le u,v\le n) (1u,vn),表示树中的一条边。

输出格式

输出一行,表示满足条件的方案数。因为答案可能很大,你只需要输出答案 mod 1,000,000,007 \text{mod 1,000,000,007} mod 1,000,000,007 的余数即可。

样例 #1

样例输入 #1

5 3
1 2
2 3
3 4
4 5

样例输出 #1

1

提示

样例 1 解释

样例数据是一条链 1 − 2 − 3 − 4 − 5 1-2-3-4-5 12345 。首先,节点 2 2 2 4 4 4 必须放置监听设备,否则 1 , 5 1,5 1,5 将无法被监听(放置的监听设备无法监听它所在的节点)。剩下一个设备必须放置在 3 3 3 号节点以同时监听 2 , 4 2,4 2,4 。因此在 2 , 3 , 4 2,3,4 2,3,4 节点放置监听设备是唯一合法的方案。

数据范围

存在 10 % 10\% 10% 的数据, 1 ≤ n ≤ 20 1 \le n \le 20 1n20

存在另外 10 % 10\% 10% 的数据, 1 ≤ n ≤ 100 1 \le n \le 100 1n100

存在另外 10 % 10\% 10% 的数据, 1 ≤ k ≤ 10 1 \le k \le 10 1k10

存在另外 10 % 10\% 10% 的数据,输入的树保证是一条链;

对于所有数据, 1 ≤ n ≤ 1 0 5 1\le n\le 10^5 1n105​ , 1 ≤ k ≤ min ⁡ { n , 100 } 1\le k\le \min\{n,100\} 1kmin{n,100}

树形dp - 20pts TLE & MLE

明显是一个树形dp。


状态定义:

考虑定义 d p ( u , i ) dp(u,i) dp(u,i)表示 u u u的子树上(不含 u u u)有 i i i个监听设备,子树上的点(不含 u u u)被全部监听,的方案数。
然而,dp要求无后效性。一个点,除了被儿子,也可以被它的爸爸监听;同时,它也可以监听它的爸爸。对于前者,需要加一维 0 / 1 0/1 0/1表示点 u u u是否被儿子监听;对于后者,还要再加一维 0 / 1 0/1 0/1表示点 u u u上是否有监听器。
d p ( u , i , 0 / 1 , 0 / 1 ) dp(u,i,0/1,0/1) dp(u,i,0/1,0/1).


状态转移:

这像是一个树形背包。与 d p ( u , i , 0 / 1 , 0 / 1 ) , d p ( v , j , 0 / 1 , 0 / 1 ) dp(u,i,0/1,0/1),dp(v,j,0/1,0/1) dp(u,i,0/1,0/1),dp(v,j,0/1,0/1)有关.

在推导状态转移方程时,可以先找一些性质,这样推得轻松些。
显然,不论怎么转移,状态的第三维(点u有没有监听器)一定不会改变。
如果点u没有监听器,它的儿子一定被它儿子的儿子监听。
如果点u不被儿子监听,那么所有儿子都没有监听器。
下面是 i i i j j j的关系:
在这里插入图片描述

初始化: d p ( u , 0 , 0 , 0 ) = d p ( u , 0 , 1 , 0 ) = 1 dp(u,0,0,0)=dp(u,0,1,0)=1 dp(u,0,0,0)=dp(u,0,1,0)=1. 一开始,可以视作只有u一个点。

u没有,不被儿子。儿子全部没有,被儿子的儿子。
d p ( u , i , 0 , 0 ) + = d p ( v , j , 0 , 1 ) × d p ( u , i − j , 0 , 0 ) dp(u,i,0,0)+=dp(v,j,0,1)\times dp(u,i-j,0,0) dp(u,i,0,0)+=dp(v,j,0,1)×dp(u,ij,0,0)

u没有,被儿子。儿子被儿子的儿子。
本来就被儿子,加了一个没有的新儿子。 d p ( u , i , 0 , 1 ) + = d p ( v , j , 0 , 1 ) × d p ( u , i − j , 0 , 1 ) dp(u,i,0,1)+=dp(v,j,0,1)\times dp(u,i-j,0,1) dp(u,i,0,1)+=dp(v,j,0,1)×dp(u,ij,0,1).
本来随便被不被儿子,反正加了一个有的新儿子。 d p ( u , i , 0 , 1 ) + = d p ( v , j , 1 , 1 ) × d p ( u , i − j − 1 , 0 , 0 / 1 ) dp(u,i,0,1)+=dp(v,j,1,1)\times dp(u,i-j-1,0,0/1) dp(u,i,0,1)+=dp(v,j,1,1)×dp(u,ij1,0,0/1).

另外两种情况同理,以及最终答案,都不再赘述。

注意,这是一个计数类树形背包dp,状态相当于被省去一维。类似于滚动数组,记得转移之前初始化 d p ( u , i , 0 / 1 , 0 / 1 ) dp(u,i,0/1,0/1) dp(u,i,0/1,0/1)为0。
同时,也要注意,由于这题的特殊性,不能直接将它初始化为0,因为转移时会用到长得一样的状态。应该另用一个东西记录 d p ( u , i , 0 / 1 , 0 / 1 ) dp(u,i,0/1,0/1) dp(u,i,0/1,0/1)的结果(万恶啊,卡了我半个多小时)。


时间复杂度好像是 O ( n k 2 ) O(nk^2) O(nk2),要超时。
可是我没想到,竟然只能得20pts。

int n, k;
ll dp[MAXN][MAXK][2][2], res[2][2];
vector<int> g[MAXN];

void dfs1(int u, int par) {
	dp[u][0][0][0] = dp[u][0][1][0] = 1;
	for (int v : g[u]) {
		if (v == par) continue;
		dfs1(v, u);
		for (int i = k; i >= 0; --i) {
			memset(res, 0, sizeof(res));
			for (int j = 0; j <= i; ++j) {
				plusmod(res[0][0], mod(dp[v][j][0][1] * dp[u][i - j][0][0]));
				
				plusmod(res[0][1], mod(dp[v][j][0][1] * dp[u][i - j][0][1]));
				if (j < i) plusmod(res[0][1], mod(dp[v][j][1][1] * mod(dp[u][i - j - 1][0][0] + dp[u][i - j - 1][0][1])));
				
				plusmod(res[1][0], mod((dp[v][j][0][0] + dp[v][j][0][1]) * dp[u][i - j][1][0]));
				
				plusmod(res[1][1], mod((dp[v][j][0][0] + dp[v][j][0][1]) * dp[u][i - j][1][1]));
				if (j < i) plusmod(res[1][1], mod(mod(dp[v][j][1][0] + dp[v][j][1][1]) * mod(dp[u][i - j - 1][1][0] + dp[u][i - j - 1][1][1])));
			}
			dp[u][i][0][0] = res[0][0]; dp[u][i][0][1] = res[0][1]; dp[u][i][1][0] = res[1][0]; dp[u][i][1][1] = res[1][1];
		}
	}
}

int main() {
	scanf("%d%d", &n, &k);
	for (int i = 1; i < n; ++i) {
		int a, b; scanf("%d%d", &a, &b);
		g[a].push_back(b); g[b].push_back(a);
	}
	
	dfs1(1, 0);
	printf("%lld\n", dp[1][k][0][1] + dp[1][k - 1][1][1]);
	
	return 0;
}

删除冗余枚举 - 70pts TLE & MLE

对于树形背包dp,一个常规的优化是,
u的子树上只有 s i z ( u ) siz(u) siz(u)个点。 i > s i z ( u ) i>siz(u) i>siz(u)的情况显然不用管。
v的子树上只有 s i z ( v ) siz(v) siz(v)个点。 j > s i z ( v ) j>siz(v) j>siz(v)的情况显然不用管。

本来以为这个优化力度不大,没想到一下从20pts跳到了70pts。

int n, k, siz[MAXN];
ll dp[MAXN][MAXK][2][2], res[2][2];
vector<int> g[MAXN];

void dfs1(int u, int par) {
	dp[u][0][0][0] = dp[u][0][1][0] = 1;
	for (int v : g[u]) {
		if (v == par) continue;
		dfs1(v, u);
		siz[u] += siz[v] + 1;
		for (int i = min(k, siz[u]); i >= 0; --i) { //这里
			res[0][0] = res[0][1] = res[1][0] = res[1][1] = 0;
			for (int j = 0; j <= min(i, siz[v]); ++j) { //这里
				plusmod(res[0][0], mod(dp[v][j][0][1] * dp[u][i - j][0][0]));
				
				plusmod(res[0][1], mod(dp[v][j][0][1] * dp[u][i - j][0][1]));
				if (j < i) plusmod(res[0][1], mod(dp[v][j][1][1] * mod(dp[u][i - j - 1][0][0] + dp[u][i - j - 1][0][1])));
				
				plusmod(res[1][0], mod((dp[v][j][0][0] + dp[v][j][0][1]) * dp[u][i - j][1][0]));
				
				plusmod(res[1][1], mod((dp[v][j][0][0] + dp[v][j][0][1]) * dp[u][i - j][1][1]));
				if (j < i) plusmod(res[1][1], mod(mod(dp[v][j][1][0] + dp[v][j][1][1]) * mod(dp[u][i - j - 1][1][0] + dp[u][i - j - 1][1][1])));
			}
			dp[u][i][0][0] = res[0][0]; dp[u][i][0][1] = res[0][1]; dp[u][i][1][0] = res[1][0]; dp[u][i][1][1] = res[1][1];
		}
	}
}

int main() {
	scanf("%d%d", &n, &k);
	for (int i = 1; i < n; ++i) {
		int a, b; scanf("%d%d", &a, &b);
		g[a].push_back(b); g[b].push_back(a);
	}
	
	dfs1(1, 0);
	printf("%lld\n", dp[1][k][0][1] + dp[1][k - 1][1][1]);
	
	return 0;
}

把long long改为int - 90pts TLE

仔细算一下空间。用long long存dp数组, 1 × 1 0 5 × 100 × 4 × 8 b y t e s = 305 M B 1\times 10^5\times 100\times 4 \times 8bytes=305MB 1×105×100×4×8bytes=305MB,而内存限制是 250 M B 250MB 250MB,所以爆空间了。
将long long改为int,变成 153 M B 153MB 153MB,稳过。但是,由于涉及到两个大int的乘法,又不能改成long long。
这时,我突然想起一年前做一道dp,同样的情况,老杨告诉我,就用int存,但是做乘法时,给它暂时转成long long,然后取模,变成int
这样,所有的MLE点都没了,只剩一个TLE。

int n, k, siz[MAXN], dp[MAXN][MAXK][2][2], res[2][2];
vector<int> g[MAXN];

void dfs1(int u, int par) {
	dp[u][0][0][0] = dp[u][0][1][0] = 1;
	for (int v : g[u]) {
		if (v == par) continue;
		dfs1(v, u);
		siz[u] += siz[v] + 1;
		for (int i = min(k, siz[u]); i >= 0; --i) {
			res[0][0] = res[0][1] = res[1][0] = res[1][1] = 0;
			for (int j = 0; j <= min(i, siz[v]); ++j) {
				plusmod(res[0][0], mod(1LL * dp[v][j][0][1] * dp[u][i - j][0][0]));
				
				plusmod(res[0][1], mod(1LL * dp[v][j][0][1] * dp[u][i - j][0][1]));
				if (j < i) plusmod(res[0][1], mod(1LL * dp[v][j][1][1] * mod(dp[u][i - j - 1][0][0] + dp[u][i - j - 1][0][1])));
				
				plusmod(res[1][0], mod(1LL * (dp[v][j][0][0] + dp[v][j][0][1]) * dp[u][i - j][1][0]));
				
				plusmod(res[1][1], mod(1LL * (dp[v][j][0][0] + dp[v][j][0][1]) * dp[u][i - j][1][1]));
				if (j < i) plusmod(res[1][1], mod(1LL * mod(dp[v][j][1][0] + dp[v][j][1][1]) * mod(dp[u][i - j - 1][1][0] + dp[u][i - j - 1][1][1])));
			}
			dp[u][i][0][0] = res[0][0]; dp[u][i][0][1] = res[0][1]; dp[u][i][1][0] = res[1][0]; dp[u][i][1][1] = res[1][1];
		}
	}
}

int main() {
	scanf("%d%d", &n, &k);
	for (int i = 1; i < n; ++i) {
		int a, b; scanf("%d%d", &a, &b);
		g[a].push_back(b); g[b].push_back(a);
	}
	
	dfs1(1, 0);
	printf("%d\n", mod(dp[1][k][0][1] + dp[1][k - 1][1][1]));
	
	return 0;
}

进一步删除冗余枚举 - AC(补)

接下来,我试了各种卡常方法,开了O2,都无法改变那个TLE。在题解区翻了半天,也没找到什么方法。很多题解用的是刷表法,难道会比我的查表法快些?但我懒得重新写。

直到我在讨论版里找到一个解决方案。
在“删除冗余状态”子标题中,我只考虑了u的子树大小,v的子树大小。事实上,还要考虑转移前u原来的子树的大小,即须 i − j − 1 ≤ s i z ( u ) , j ≥ i − 1 − s i z ( u ) i-j-1\leq siz(u),j\geq i-1-siz(u) ij1siz(u),ji1siz(u),这里 s i z [ u ] siz[u] siz[u]是原来的子树大小。

int n, k, siz[MAXN], dp[MAXN][MAXK][2][2], res[2][2];
vector<int> g[MAXN];

void dfs1(int u, int par) {
	dp[u][0][0][0] = dp[u][0][1][0] = 1;
	for (int v : g[u]) {
		if (v == par) continue;
		dfs1(v, u);
		for (int i = min(k, siz[u] + siz[v] + 1); i >= 0; --i) {
			res[0][0] = res[0][1] = res[1][0] = res[1][1] = 0;
			for (int j = max(0, i - 1 - siz[u]); j <= min(i, siz[v]); ++j) { //这里
				plusmod(res[0][0], mod(1LL * dp[v][j][0][1] * dp[u][i - j][0][0]));
				
				plusmod(res[0][1], mod(1LL * dp[v][j][0][1] * dp[u][i - j][0][1]));
				if (j < i) plusmod(res[0][1], mod(1LL * dp[v][j][1][1] * mod(dp[u][i - j - 1][0][0] + dp[u][i - j - 1][0][1])));
				
				plusmod(res[1][0], mod(1LL * (dp[v][j][0][0] + dp[v][j][0][1]) * dp[u][i - j][1][0]));
				
				plusmod(res[1][1], mod(1LL * (dp[v][j][0][0] + dp[v][j][0][1]) * dp[u][i - j][1][1]));
				if (j < i) plusmod(res[1][1], mod(1LL * mod(dp[v][j][1][0] + dp[v][j][1][1]) * mod(dp[u][i - j - 1][1][0] + dp[u][i - j - 1][1][1])));
			}
			dp[u][i][0][0] = res[0][0]; dp[u][i][0][1] = res[0][1]; dp[u][i][1][0] = res[1][0]; dp[u][i][1][1] = res[1][1];
		}
		siz[u] += siz[v] + 1;
	}

关于时间复杂度(补)

第一反应是 O ( n k 2 ) O(nk^2) O(nk2)。事实上,是 O ( n k ) O(nk) O(nk),稳过的。
用子树合并来看待树形背包dp。出现 k 2 k^2 k2枚举,是两棵大小为k的子树合并。这种合并最多出现 n / k n/k n/k次。所以时间复杂度是 O ( n k ) O(nk) O(nk).

总结

这题倒是简单的,套路的,但状态转移有些麻烦。过程中也暴露出了很多关于dp常见的,我却不熟悉的技巧。
我几乎花了一整个晚自习。要是能在考场上流畅地做出来,也是一个本事吧。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值