题目描述
难度分:1600
输入T(≤5000)表示T组数据。所有数据的n之和≤3×105。
每组数据输入n(2≤n≤3×105)和一棵n个节点的二叉树的n−1条双向边。节点编号从1开始。
二叉树的根节点是1。一开始,根节点被感染。然后执行如下过程n次:
- 首先,选择一个未被感染也未被删除的点,将其删除(相连的边也删除);或者什么也不做。
- 然后,任何与被感染节点相邻的节点,都被感染。
输出最多可以让多少个节点不被感染也不被删除。
输入样例
4
2
1 2
4
1 2
2 3
2 4
7
1 2
1 5
2 3
2 4
5 6
5 7
15
1 2
2 3
3 4
4 5
4 6
3 7
2 8
1 9
9 10
9 11
10 12
10 13
11 14
11 15
输出样例
0
2
2
10
算法
树形DP
这个感染过程还是挺形象的,我们的目的其实就是想要未感染的节点最多。
状态定义
dp[u]表示以u为根的子树中能保留的最多不会被感染的节点数目。在这个定义下,答案就应该是dp[1]。
状态转移
对于某个子树的根节点u:
- 假设它有两个孩子lch和rch,此时只能阻断u向一个子树感染,如果阻断lch,就有状态转移dp[u]=cnt[lch]−1+dp[rch],如果阻断rch,则有状态转移dp[u]=cnt[rch]−1+dp[lch],两者取较大值转移。
- 只有一个孩子ch的话,则有dp[u]=cnt[ch]−1。
- 如果没有孩子,则有dp[u]=0。
其中cnt[u]表示以u为根的子树中有多少个节点,很典,也可以用树形DP
求出来。整个算法只需要DFS
两遍做两次树形DP
。
复杂度分析
时间复杂度
树形DP
两遍,相当于DFS
遍历整棵树两遍,时间复杂度为遍历二叉树的时间复杂度,即O(n)。
空间复杂度
树的邻接表空间消耗为O(n+m),其中m是边数,n是顶点数,由于是树,所以顶点数和边数是同一级别,空间复杂度为O(n)。记录子树节点树的cnt数组空间消耗也是O(n)。在DFS
的过程中递归深度在最差情况下为O(n),即整棵树退化成链的情况。因此,整个算法的额外空间复杂度就是O(n)。
C++ 代码
#include <bits/stdc++.h>
using namespace std;
const int N = 300010;
int T, n, cnt[N];
vector<int> graph[N];
void dfs1(int u, int fa) {
cnt[u] = 1;
for(int v: graph[u]) {
if(v == fa) continue;
dfs1(v, u);
cnt[u] += cnt[v];
}
}
int dfs2(int u, int fa) {
vector<int> child;
for(int v: graph[u]) {
if(v != fa) {
child.push_back(v);
}
}
if(child.empty()) return 0;
if(child.size() == 1) return cnt[child[0]] - 1;
int lch = child[0], rch = child[1];
return max(cnt[lch] - 1 + dfs2(rch, u), cnt[rch] - 1 + dfs2(lch, u));
}
int main() {
scanf("%d", &T);
while(T--) {
scanf("%d", &n);
for(int i = 1; i <= n; i++) {
cnt[i] = 0;
graph[i].clear();
}
for(int i = 1; i < n; i++) {
int u, v;
scanf("%d%d", &u, &v);
graph[u].push_back(v);
graph[v].push_back(u);
}
dfs1(1, 0);
printf("%d\n", dfs2(1, 0));
}
return 0;
}