语言漩涡

thirtiseven 的博客

0%

树形DP PA2007 Barricades

题意

给一棵无向树,问最少去掉几条边可以形成一个大小为k的子树,多次询问。n ≤ 3000,m ≤ n

输入输出

输入一棵树和m个询问,输出m个数字,表示答案,如果不可能,输出-1。

样例

1
2
3
4
5
6
7
8
9
10
7
1 2
1 3
2 4
2 5
3 6
3 7
2
2
3
1
2
2
1

解法

为了简化问题,我们先随便取一个节点作为树根,假设这棵树是一个有向树。用 表示以 为节点的子树, 表示这个子树的大小。

考虑暴力的动态规划做法。对于一个节点和任意整数满足 表示要让包含 的联通分量的大小恰好为 i 的话我们最少需要删除的边的个数。显然,如果我们有所有的 的值,那么我们就能在 时间内回答一次询问。

如果 是一个叶子结点,那么以 为根的子树只有一个节点,所以 。接着,我们假设一个节点 个孩子 ,并且对于所有 ,所有的 都已经被计算过了,那么要怎么计算出 的每一个值呢?

首先描述以 为根的更大的子树 ,它表示以 为根,取前 个孩子的子树(下图为)。设 表示树 中要包含节点 的连通分量恰好为 最少需要删除的边数。我们观察到

Barricades

我们从 开始。 是一个只有一个节点的树,所以 。现在,我们准备去为更大的 计算数组 的值。可以列出递推方程: 其中 .

这个方程的每一步包含两种情况,一种是删除边 ,不取 整棵子树;另一种情况是不删除这条边,枚举在 中所有的删除边数相加等于 的可能的最小值。

分析一下复杂度。显然,最耗时的操作是求出所有的 ,它的复杂度看上去是 的,因为对于 个数组 我们执行了 次操作。实际上,我们可以证明它的复杂度是 的。

引理:对于一个节点 ,我们需要求出所有 的值( 是子树 中的节点),只需要执行 次操作。

证明:如果 是一个叶子节点,那么引理显然成立。否则,假设节点 恰好存在 个孩子节点 ,设 ,当执行合并第 个孩子节点的操作时,我们要对所有可能的 遍历 的值有 种可能, 种可能。所以一共需要执行: 次操作。所以,计算出数组 中所有的元素需要 次操作。把所有操作加起来,要计算出 中包含的所有节点 需要的时间为: 这个证明也有另一个组合意义上的解释。由于一共需要执行 次操作,我们可以把这个数字看作一个和以 为最近公共祖先的节点对的个数成正比的数,既然一棵树中的每一对点都只存在一个最近公共祖先,所以每一对点只会被计算一次,所以算出 数组的复杂度为

于是,我们就可以在 时间内解决整个问题。

需要注意的是,要获得 的复杂度,递推方程只能最多执行 个操作。特别是,我们不能允许自己在取 的时候迭代得太宽,如果我们在每次合并的时候产生 复杂度,那我们的算法就将需要 的最坏运行时间了。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <iostream>
#include <algorithm>
#include <vector>
#include <cstring>
#include <cassert>

const int maxn = 2005;

int n, u, v, m, k;
std::vector<int> T[maxn];
int t[maxn][maxn], siz[maxn];

int get_siz(int cur, int fa) {
siz[cur] = 1;
for (auto it: T[cur]) {
if (it != fa) {
siz[cur] += get_siz(it, cur);
}
}
return siz[cur];
}

void dfs(int cur, int fa) {
t[cur][1] = 0;
for (auto it: T[cur]) {
if (it != fa) {
dfs(it, cur);
for (int i = std::min(siz[cur]+siz[it], n); i >= 1; i--) {
int temp = t[cur][i] + 1;
for (int j = 1; j <= std::min(siz[cur], i); j++) {
temp = std::min(temp, t[cur][j] + t[it][i-j]);
}
t[cur][i] = temp;
}
}
}
}

int main(int argc, char *argv[]) {
std::ios::sync_with_stdio(false);
std::cin.tie(0);
memset(t, 0x3f, sizeof t);
std::cin >> n;
// assert(n <= 2000);
for (int i = 0; i < n-1; i++) {
std::cin >> u >> v;
T[u].push_back(v);
T[v].push_back(u);
}
get_siz(1, -1);
dfs(1, -1);
std::cin >> m;
for (int i = 0; i < m; i++) {
std::cin >> k;
int ans = t[1][k];
for (int i = 2; i <= n; i++) {
ans = std::min(ans, t[i][k]+1);
}
std::cout << ans << '\n';
}
}

吐槽

写完交上去卡了空间。折腾了半天,然后我 assert(n<=2000) , 竟然没 RE,乱标数据范围,不讲武德,这好吗,这不好。

然后最后一组 T 了,我大力改了改选 a b 时候的迭代范围,卡过去了。题解的温馨提示诚不欺我。

虽然道理我都懂,但是这个真的不是 吗((

发现这个东西实际上就是树上背包,我竟然连树上背包都不会!呜呜