#P3672. 第3题-树的联通块
-
2000ms
Tried: 67
Accepted: 11
Difficulty: 5
所属公司 :
阿里
时间 :2025年9月13日-阿里淘天
-
算法标签>树
第3题-树的联通块
解题思路
关键结论
把树任选一点(如 1)作为根,设每条父子边 (parent, child) 的子树规模为 sz[child]。
若删除这条边,切下来的那个连通块正好是 child 的整棵子树,其点数为 sz[child]。
因此要使最终每个连通块都是偶数,当且仅当我们删除的每一条边都满足其子树规模为偶数;若 n 本身为奇数则无解。
于是问题转化为:
- 统计满足
sz[child]为偶数的边数m; - 从这些边里选出恰好
k条即可,方案数为C(m, k);若k > m或n为奇数则答案为 0。
算法步骤
- 建图,根在 1 做一次 DFS/BFS 得到拓扑顺序与父子关系;
- 逆序累加得到每个节点
sz; - 统计
m = |{ child | sz[child] 为偶数 }|(不含根); - 用组合数计算
C(m, k),模数MOD = 1e9+7。预处理阶乘与逆元,或只到n即可(m ≤ n-1)。
复杂度分析
- 时间复杂度:O(n),一次遍历与一次组合数计算。
- 空间复杂度:O(n),存图与辅助数组。
代码实现
Python
import sys
MOD = 10**9 + 7
read = sys.stdin.readline
n_k = read().strip()
if not n_k:
print(0)
sys.exit(0)
n, k = map(int, n_k.split())
g = [[] for _ in range(n + 1)]
for _ in range(n - 1):
u, v = map(int, read().split())
g[u].append(v)
g[v].append(u)
if n % 2 == 1:
print(0)
sys.exit(0)
# 迭代 DFS:求父亲、遍历顺序
parent = [0] * (n + 1)
order = []
st = [1]
parent[1] = -1
while st:
u = st.pop()
order.append(u)
for v in g[u]:
if v == parent[u]:
continue
parent[v] = u
st.append(v)
# 逆序累加子树大小
sz = [1] * (n + 1)
for u in reversed(order):
p = parent[u]
if p != -1:
sz[p] += sz[u]
# 统计偶子树边数
m = 0
for v in range(2, n + 1):
if sz[v] % 2 == 0:
m += 1
# 组合数 C(m, k)
if k > m:
print(0)
sys.exit(0)
# 预处理阶乘与逆元(到 n 即可)
fac = [1] * (n + 1)
for i in range(1, n + 1):
fac[i] = fac[i - 1] * i % MOD
def mod_pow(a, e):
r = 1
while e:
if e & 1:
r = r * a % MOD
a = a * a % MOD
e >>= 1
return r
inv_fac = [1] * (n + 1)
inv_fac[n] = mod_pow(fac[n], MOD - 2)
for i in range(n - 1, -1, -1):
inv_fac[i] = inv_fac[i + 1] * (i + 1) % MOD
def C(nv, kv):
if kv < 0 or kv > nv:
return 0
return fac[nv] * inv_fac[kv] % MOD * inv_fac[nv - kv] % MOD
print(C(m, k))
Java
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.StringTokenizer;
public class Main {
static final long MOD = 1_000_000_007L;
// 快速幂
static long modPow(long a, long e) {
long r = 1;
while (e > 0) {
if ((e & 1) == 1) r = (r * a) % MOD;
a = (a * a) % MOD;
e >>= 1;
}
return r;
}
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int n = Integer.parseInt(st.nextToken());
int k = Integer.parseInt(st.nextToken());
ArrayList<Integer>[] g = new ArrayList[n + 1];
for (int i = 1; i <= n; i++) g[i] = new ArrayList<>();
// 读边,建无向图
for (int i = 0; i < n - 1; i++) {
st = new StringTokenizer(br.readLine());
int u = Integer.parseInt(st.nextToken());
int v = Integer.parseInt(st.nextToken());
g[u].add(v);
g[v].add(u);
}
if ((n & 1) == 1) {
System.out.println(0);
return;
}
// 迭代 DFS 获取父亲与访问顺序
int[] parent = new int[n + 1];
int[] order = new int[n];
int[] stack = new int[n];
int top = 0, idx = 0;
stack[top++] = 1;
parent[1] = -1;
while (top > 0) {
int u = stack[--top];
order[idx++] = u;
for (int v : g[u]) {
if (v == parent[u]) continue;
parent[v] = u;
stack[top++] = v;
}
}
// 逆序累加子树大小
int[] sz = new int[n + 1];
for (int i = 1; i <= n; i++) sz[i] = 1;
for (int i = idx - 1; i >= 0; i--) {
int u = order[i];
int p = parent[u];
if (p != -1) sz[p] += sz[u];
}
// 统计偶子树边数 m
int m = 0;
for (int v = 2; v <= n; v++) if ((sz[v] & 1) == 0) m++;
if (k > m) {
System.out.println(0);
return;
}
// 组合数 C(m, k) 预处理到 n
long[] fac = new long[n + 1];
long[] ifac = new long[n + 1];
fac[0] = 1;
for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % MOD;
ifac[n] = modPow(fac[n], MOD - 2);
for (int i = n - 1; i >= 0; i--) ifac[i] = ifac[i + 1] * (i + 1) % MOD;
long ans = fac[m] * ifac[k] % MOD * ifac[m - k] % MOD;
System.out.println(ans);
}
}
C++
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1000000007LL;
long long modpow(long long a, long long e) {
long long r = 1;
while (e) {
if (e & 1) r = r * a % MOD;
a = a * a % MOD;
e >>= 1;
}
return r;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
if (!(cin >> n >> k)) return 0;
vector<vector<int>> g(n + 1);
for (int i = 0; i < n - 1; ++i) {
int u, v; cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
if (n % 2 == 1) { // 总点数为奇数,无解
cout << 0 << '\n';
return 0;
}
// 迭代 DFS:求父亲与访问顺序
vector<int> parent(n + 1, 0), order;
order.reserve(n);
vector<int> st; st.reserve(n);
st.push_back(1);
parent[1] = -1;
while (!st.empty()) {
int u = st.back(); st.pop_back();
order.push_back(u);
for (int v : g[u]) {
if (v == parent[u]) continue;
parent[v] = u;
st.push_back(v);
}
}
// 逆序累加子树大小
vector<int> sz(n + 1, 1);
for (int i = (int)order.size() - 1; i >= 0; --i) {
int u = order[i];
int p = parent[u];
if (p != -1) sz[p] += sz[u];
}
// 统计偶子树边数 m
int m = 0;
for (int v = 2; v <= n; ++v) if (sz[v] % 2 == 0) ++m;
if (k > m) { cout << 0 << '\n'; return 0; }
// 组合数 C(m, k) —— 预处理到 n
vector<long long> fac(n + 1), ifac(n + 1);
fac[0] = 1;
for (int i = 1; i <= n; ++i) fac[i] = fac[i - 1] * i % MOD;
ifac[n] = modpow(fac[n], MOD - 2);
for (int i = n - 1; i >= 0; --i) ifac[i] = ifac[i + 1] * (i + 1) % MOD;
auto C = [&](int N, int K)->long long{
if (K < 0 || K > N) return 0;
return fac[N] * ifac[K] % MOD * ifac[N - K] % MOD;
};
cout << C(m, k) << '\n';
return 0;
}
题目内容
给定一棵有 n 个节点的无向连通无环图(树)。节点编号为 1 ~ n 。你需要恰好删除 k 条边,将原树分成上 k+1 个连通块。要求所有连通块的节点数量均为偶数。
请你计算满足条件的删除方案总数,并对 109+7 取模输出。
【名词解释】
-
树:树 是一个无向、连通且无环的图结构。
-
连通块:连通块 指图中任意两点间均存在路径的极大顶点集合。
输入描述
第一行输入两个整数 n 和 k(1≦n≦2×105,0≦k≦n−1) ,分别表示树的节点数量和要删除的边数。
接下来 n−1 行,每行输入两个整数 ui,vi(1≦ui,vi≦n,ui=vi),表示一条边连接节点 ui 与 vi 。输入保证构成一棵树。
输出描述
输出一个整数,表示删除恰好 k 条边后,使得所有连通块节点数均为偶数的方案总数,对 109+7 取模。
样例1
输入
8 1
1 2
2 3
3 4
4 5
5 6
6 7
7 8
输出
3
样例2
输入
8 2
1 2
2 3
3 4
4 5
5 6
6 7
7 8
输出
3