题目描述
有 n 个蚁巢,这
n 个蚁巢形成一颗树形结构,第 i 个蚁巢有ai 只蚂蚁。现在蚂蚁们想举行一个大型的聚会。但是这些蚂蚁比较懒惰,都不想走太远,每只蚂蚁最多只愿意走 X 步(每一步就是走一条边)。
它们要计算:如果选择第
i 个蚁巢作为举行聚会的地点,可以有多少只蚂蚁参加聚会?记该数量为 pi 。你的任务就是帮助计算: p1,p2,p3,…,pn 。
输入格式 1790.in
第一行,两个整数: n 和
x 。 1≤n≤100000 , 1≤x≤20 。接下来有 n−1 行,每行两个整数: a ,
b ,表示蚁巢 a 和蚁巢b 有一双向条边相连。接下来有 n 行,第
i 行是一个整数 ai 。 0≤ai≤1000 。
输出格式 1790.out
共 n 行,第
i 行是 pi 。
输入样例 1790.in
6 2
5 1
3 6
2 4
2 1
3 2
1
2
3
4
5
6
输出样例 1790.out
15
21
16
10
8
11
这题真的是一个好题。
题意:有一棵
n
个结点的树,每个结点都有一个权值
求 max{v′i} 。
拿样例解释一下:
样例中 x=2 ,能到达 1 的结点有 1、2、3、4 和 5,故 v′1=v1+v2+v3+v4+v5=15 。其他点同理。
做法一:暴力
枚举每一个点,做一遍 BFS,多于
x
步则停止搜索,即可直接求得
时间复杂度上限: O(n2) 。(想象一棵树,高度为 2,除了根结点之外其他所有结点的父亲都是根结点)
得分:60。(似乎可以优化到 70,但我不会)
代码:
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <queue>
#include <utility>
#include <vector>
using namespace std;
typedef pair <int, int> pii;
const int maxn = 1e5 + 100;
int n, x;
vector <int> g[maxn];
int a[maxn];
bool vis[maxn];
int bfs(int start) {
memset(vis, false, sizeof vis);
queue <pii> q;
while (!q.empty()) q.pop();
int ans = a[start];
vis[start] = true;
for (q.push(make_pair(start, 0)); !q.empty(); q.pop()) {
pii cur = q.front();
if (cur.second >= x) break;
for (int i = 0; i < g[cur.first].size(); i++)
if (!vis[g[cur.first][i]]) {
vis[g[cur.first][i]] = true;
q.push(make_pair(g[cur.first][i], cur.second + 1));
ans += a[g[cur.first][i]];
}
}
return ans;
}
int main(void) {
freopen("1790.in", "r", stdin);
freopen("1790.out", "w", stdout);
scanf("%d%d", &n, &x);
for (int i = 1; i < n; i++) {
int a, b;
scanf("%d%d", &a, &b);
g[a].push_back(b); g[b].push_back(a);
}
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= n; i++) printf("%d\n", bfs(i));
return 0;
}
做法二:树型 DP
因为移动的过程中既可以往上,也可以往下,似乎不能直接做 DP,但其实通过一点小技巧处理就可以了。
定义结点
r
的深度为
当 depth(i)<depth(j) 时,
1.
i
是
2.
i
不是
当 depth(i)=depth(j) 时,
3.
i
和
归纳一下,其实情况 2 和 3 可以合并。总的来看无非是:直接上下爬到达,或者通过某一个点中转。
为了方便,我们先只考虑每个结点往下的情况。不妨定义
(为什么不定义为“与
显然
归纳一下,有
于是到目前为止我们就解决了由下往上爬的这种情况。可以确定了 v′i=f[i][x] 。
但是还剩下从上往下爬的情况和以某个祖先中转的情况。
这两种情况其实是一致的。
不妨这样想,当前要作为中转点的祖先
k
的深度
于是
i
到
若
i
和
由上面的推论我们可以知道,若
i
和
(这里就可以回答上面的疑问。对于一个确定范围的子问题,用“在
而
其实如果直接用上面那个深度的式子反而还麻烦,我们不妨这样考虑吧。
有结点
i
和它的某个祖先
于是也就得到了
k
的另一个后裔
而直接从上往下爬的情况,其实相当于 depth(i)−depth(k)=x ,所以说它们是一致的。
但是要注意,直接加会有重复。因为
k
的后裔也包含了以
因此要再减去重复的部分,即
得分:100。
参考代码:
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>
using namespace std;
const int maxn = 1e5 + 100;
const int maxx = 20 + 10;
int n, x;
int a[maxn];
vector <int> g[maxn];
int father[maxn]; //记录每个结点的父亲,方便后面不断往上
int dp[maxn][maxx];
void dfs(int root, int pre) { //从下往上的情况
father[root] = pre;
for (int i = 0; i <= x; i++) dp[root][i] = a[root]; //必定包含根结点的权值
for (int i = 0; i < g[root].size(); i++)
if (g[root][i] != pre) {
dfs(g[root][i], root);
for (int j = 1; j <= x; j++) dp[root][j] += dp[g[root][i]][j - 1];
}
}
int main(void) {
freopen("1790.in", "r", stdin);
freopen("1790.out", "w", stdout);
scanf("%d%d", &n, &x);
for (int i = 1; i < n; i++) {
int a, b;
scanf("%d%d", &a, &b);
g[a].push_back(b); g[b].push_back(a);
}
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
memset(father, 0, sizeof father);
dfs(1, 0);
for (int i = 1; i <= n; i++) {
int ans = dp[i][x];
int cur = i; //当前结点
for (int j = 1; j <= x && father[cur]; j++) { //最多往上爬 x 步,若已经爬到根结点则停止,j 即为上文所述的 depth(i) - depth(k)
ans += dp[father[cur]][x - j]; //通过 father[cur] 中转
if (j < x) ans -= dp[cur][x - j - 1]; //要注意对于一直往上爬 x 步的情况是不会多算的
cur = father[cur]; //爬到它的父亲
}
printf("%d\n", ans);
}
return 0;
}