P3915 树的分解 (黄色 普及/提高-)
树形 DP / 拓扑性质,https://www.luogu.com.cn/problem/P3915
给出
输入格式
第一行,一个整数
第一行,两个整数
接下来
输出格式
对于每组数据,输出 YES 或 NO。
输入输出样例 #1
输入 #1
2
4 2
1 2
2 3
3 4
4 2
1 2
1 3
1 4输出 #1
YES
NO说明/提示
- 对于
的数据, ; - 对于
的数据, , 。
这是一个经典的树形 DP / 拓扑性质问题。我们可以通过分析子树大小的性质来解决。
算法分析
要将一棵大小为 NO。
如果满足整除条件,我们可以任选一个点作为根节点(例如节点
设以
- 如果我们将树切成若干个大小为
的连通块,那么每一次“切断”一条边,都会分离出一个大小为 的倍数的连通块。 - 也就是说,若能成功划分,那么原树中子树大小为
的倍数的节点个数必须正好等于 。 - 这是一个充要条件。因为如果一个子树的大小是
的倍数,我们就可以在其上方切一刀,将其独立出来,然后递归地在内部继续划分。
算法步骤
- 特判:若
,直接输出 NO。 - 建图:构建无向树。
- 拓扑排序 / BFS:由于 Python 的递归深度限制以及递归带来的额外开销,推荐使用非递归(BFS)的方式获取从根节点(假设为
)出发的拓扑序(自顶向下),然后逆序处理(自底向上)来计算子树大小。 - 统计:自底向上累加子树大小。如果某个节点
的子树大小 满足 ,则计数器 count加 1。 - 判断:若
count == N // K,输出YES,否则输出NO。
Python 实现
python
import sys
def solve():
# 使用 sys.stdin.read 快速读取所有输入
input_data = sys.stdin.read().split()
if not input_data:
return
iterator = iter(input_data)
num_test_cases = int(next(iterator))
out = []
for _ in range(num_test_cases):
n = int(next(iterator))
k = int(next(iterator))
# 构建邻接表
adj = [[] for _ in range(n + 1)]
for _ in range(n - 1):
u = int(next(iterator))
v = int(next(iterator))
adj[u].append(v)
adj[v].append(u)
# 如果 N 不能被 K 整除,直接判定为 NO
if n % k != 0:
out.append("NO")
continue
# 使用 BFS 得到自顶向下的节点顺序,并记录每个节点的父节点
order = []
parent = [0] * (n + 1)
visited = [False] * (n + 1)
queue = [1]
visited[1] = True
head = 0
while head < len(queue):
u = queue[head]
head += 1
order.append(u)
for v in adj[u]:
if not visited[v]:
visited[v] = True
parent[v] = u
queue.append(v)
# 初始化每个节点的子树大小为 1
size = [1] * (n + 1)
count = 0
# 逆序(自底向上)计算子树大小
for u in reversed(order):
if size[u] % k == 0:
count += 1
p = parent[u]
if p != 0:
size[p] += size[u]
# 如果大小为 K 的倍数的子树数量恰好为 N // K
if count == n // k:
out.append("YES")
else:
out.append("NO")
print("\n".join(out))
if __name__ == "__main__":
solve()复杂度分析
- 时间复杂度:对于每组数据,BFS 遍历所有节点和边的时间复杂度为
,自底向上计算子树大小的时间复杂度也是 。总时间复杂度为 。在 的数据规模下,该算法可以在 1 秒内运行完毕。 - 空间复杂度:主要消耗在邻接表、BFS 队列以及辅助数组上,空间复杂度为
,能够轻松通过空间限制。