把树根定为 1,每条边传播一次,每个点按孩子数计算需要的翻倍次数。
OJ: usaco
题目 ID: 1062
难度:普及-
标签:树形结构数学贪心usaco
日期: 2026-07-11 20:15
题意
有一棵
一天只能做下面两种事件之一:
- 某个农场内感染牛数量翻倍;
- 一头感染牛沿一条边移动到相邻农场。
问最少多少天后,每个农场都至少有一头感染牛。
思路
先看一个直接按有根树模拟的写法:
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-11 20:15
* update_at: 2026-07-11 20:16
*/
// brute.cpp:小数据朴素解,把树根定为 1 后逐点模拟“翻倍再传播”。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
int n;
vector<int> g[MAXN];
long long ans;
int need_double_count(int children) {
int cows = 1;
int cnt = 0;
while (cows < children + 1) {
cows *= 2;
cnt++;
}
return cnt;
}
void dfs(int u, int fa) {
int children = 0;
for (int i = 0; i < (int)g[u].size(); i++) {
int v = g[u][i];
if (v != fa) children++;
}
// 先在当前农场翻倍到足够:给每个孩子 1 头,还要自己留下 1 头。
ans += need_double_count(children);
ans += children;
for (int i = 0; i < (int)g[u].size(); i++) {
int v = g[u][i];
if (v != fa) {
dfs(v, u);
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n - 1; i++) {
int a, b;
cin >> a >> b;
g[a].push_back(b);
g[b].push_back(a);
}
dfs(1, 0);
cout << ans << '\n';
return 0;
}把树以 1 号点为根。一个点的任务很固定:
- 它最终要给每个孩子送 1 头感染牛;
- 送完以后,它自己也要留下至少 1 头感染牛;
- 所以如果它有
children个孩子,就至少需要准备children + 1头感染牛。
初始到达某个点时,我们只需要考虑它有 1 头感染牛。每次翻倍后数量变成原来的 2 倍,所以需要的翻倍次数是最小的
然后还要向每个孩子移动一次,所以移动次数就是 children。
例如样例是一颗星形树:
| 点 | 孩子数 | 需要牛数 | 翻倍次数 | 移动次数 |
|---|---|---|---|---|
| 1 | 3 | 4 | 2 | 3 |
| 2 | 0 | 1 | 0 | 0 |
| 3 | 0 | 1 | 0 | 0 |
| 4 | 0 | 1 | 0 | 0 |
总天数为
官方解析还给出一个更简洁的实现:不需要真正 DFS 建根树。
对于 1 号点,孩子数就是它的度数;对于其它点,孩子数等于度数减去父亲这条边,也就是:
text
children(1) = degree(1)
children(i) = degree(i) - 1, i != 1移动事件总数一定是
所以答案就是:
代码里不用真的算浮点对数,直接从 1 开始不断乘 2,直到不少于 children + 1。
代码
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-11 20:15
* update_at: 2026-07-11 20:16
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
int n;
int degree_cnt[MAXN]; // 无根树上的度数
int need_double_count(int children) {
int cows = 1;
int cnt = 0;
while (cows < children + 1) {
cows *= 2;
cnt++;
}
return cnt;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n - 1; i++) {
int a, b;
cin >> a >> b;
degree_cnt[a]++;
degree_cnt[b]++;
}
long long ans = n - 1; // 每个非 1 号农场都需要一次移动感染。
for (int i = 1; i <= n; i++) {
int children = degree_cnt[i];
if (i != 1) children--;
if (children > 0) {
ans += need_double_count(children);
}
}
cout << ans << '\n';
return 0;
}复杂度
只需要统计每个点的度数,再枚举所有点。
时间复杂度为
空间复杂度为
总结
本题的关键是把传播过程看成一棵从 1 号点向外扩张的有根树。
每条边只需要传播一次;每个点只需要在向孩子传播之前,先翻倍到足够的牛数。这样整题就变成按每个点的孩子数求翻倍次数。