[洛谷-P3047] [USACO12FEB]Nearby Cows G(树形DP+换根DP)

文章描述了一种树形动态规划问题,目标是计算在给定的农场网络中,每个字段能容纳多少在最多K步距离内的奶牛。通过状态表示`f[u][k]`表示以u为根的子树内,距离u不超过k的节点权值和,以及换根DP方法`g[u][k]`表示所有到u节点不超过k距离的节点权值和。文章详细分析了状态转移方程,并提供了代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、问题

题目描述

Farmer John has noticed that his cows often move between nearby fields. Taking this into account, he wants to plant enough grass in each of his fields not only for the cows situated initially in that field, but also for cows visiting from nearby fields.

Specifically, FJ’s farm consists of N fields (1 <= N <= 100,000), where some pairs of fields are connected with bi-directional trails (N-1 of them in total). FJ has designed the farm so that between any two fields i and j, there is a unique path made up of trails connecting between i and j. Field i is home to C(i) cows, although cows sometimes move to a different field by crossing up to K trails (1 <= K <= 20).

FJ wants to plant enough grass in each field i to feed the maximum number of cows, M(i), that could possibly end up in that field – that is, the number of cows that can potentially reach field i by following at most K trails. Given the structure of FJ’s farm and the value of C(i) for each field i, please help FJ compute M(i) for every field i.

输入格式

* Line 1: Two space-separated integers, N and K.

* Lines 2…N: Each line contains two space-separated integers, i and j (1 <= i,j <= N) indicating that fields i and j are directly connected by a trail.

* Lines N+1…2N: Line N+i contains the integer C(i). (0 <= C(i) <= 1000)

第一行两个正整数 n , k n,k n,k
接下来 n − 1 n-1 n1 行,每行两个正整数 u , v u,v u,v,表示 u , v u,v u,v 之间有一条边。
最后 n n n 行,每行一个非负整数 c i c_i ci,表示点权。

输出格式

* Lines 1…N: Line i should contain the value of M(i).

输出 n n n 行,第 i i i 行一个整数表示 m i m_i mi

样例 #1

样例输入 #1

6 2 
5 1 
3 6 
2 4 
2 1 
3 2 
1 
2 
3 
4 
5 
6

样例输出 #1

15 
21 
16 
10 
8 
11

提示

There are 6 fields, with trails connecting (5,1), (3,6), (2,4), (2,1), and (3,2). Field i has C(i) = i cows.

Field 1 has M(1) = 15 cows within a distance of 2 trails, etc.

【数据范围】
对于 100 % 100\% 100% 的数据: 1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1n105 1 ≤ k ≤ 20 1 \le k \le 20 1k20 0 ≤ c i ≤ 1000 0 \le c_i \le 1000 0ci1000

二、分析

题目简单地来说就是:
给你一棵 n n n 个点的树,点带权,对于每个节点求出距离它不超过 k k k 的所有节点权值和 m i m_i mi

对于树中的某个节点而言,距离它不超过 k k k的节点主要来源于两方面,一个是该节点的子节点中距离该节点不超过距离 k k k的节点的权值和,另一个就是该节点向上沿着父节点方向不超过距离 k k k的点的权值和。

对于子节点方向的节点的权值和,我们可以先通过普通的树形DP计算出来。

因此,我们先写一个DP计算出子树中距离该点不超过 k k k的点的权值和。

1、状态表示

f [ u ] [ k ] f[u][k] f[u][k]表示以 u u u为根节点的树中,距离 u u u不超过 k k k的子节点的权值和。

2、状态转移

f [ u ] [ k ] = v a l [ u ] + ∑ f [ s o n ] [ k − 1 ] f[u][k]=val[u]+\sum f[son][k-1] f[u][k]=val[u]+f[son][k1]
到节点 u u u不超过距离 k k k,即距离 s o n son son不超过 k − 1 k-1 k1,然后加在一起即可。同时 u u u节点本身也是答案,因为 u u u节点本身是不超过距离 0 0 0的节点。

3、换根DP

这个题目本身是个无根树,如果我们认为规定编号为1的节点是根的话,那么对于祖宗节点 1 1 1来说, f [ 1 ] [ k ] f[1][k] f[1][k]就是距离 1 1 1节点不超过距离 k k k的节点的权值和。因为祖宗节点是没有父亲节点的,所以我们就不需要考虑沿着父节点方向的节点权值和。

我们不妨令: g [ u ] [ k ] g[u][k] g[u][k]表示所有到 u u u节点的不超过距离 k k k的节点的权值和。根据刚刚的分析: g [ 1 ] [ k ] = f [ 1 ] [ k ] g[1][k]=f[1][k] g[1][k]=f[1][k]

这个就是我们换根DP的初始化。其实受这个的启发,我们完全可以去把每个点都当作根,然后暴力跑出答案,但是这个暴力做法的时间复杂度是 O ( n 2 ) O(n^2) O(n2)的,会超时。

所以当我们将祖宗节点从节点1换为另一个节点的时候,我们只能通过数学上的关系来计算出 g g g数组元素的值。这个也是换根DP的意义。

我们看下面的图:
在这里插入图片描述
红色框是非常好理解的,直接写成 f [ u ] [ k ] f[u][k] f[u][k]即可。上面的部分,我们可以写成 g [ f a ( u ) ] [ k − 1 ] g[fa(u)][k-1] g[fa(u)][k1]。因为到 u u u不超过 k k k的距离,即距离它的父亲节点不超过 k − 1 k-1 k1的距离。

但是这么写对吗?

答案是不对的, g [ f a ( u ) ] [ k − 1 ] g[fa(u)][k-1] g[fa(u)][k1] f [ u ] [ k ] f[u][k] f[u][k]是有重复部分的。我们需要减去这段重复的部分,那么关键问题是重复部分如何表示?

重复部分肯定是出现在了红色框中,红色框中到 f a ( u ) fa(u) fa(u)不超过距离 k − 1 k-1 k1,即距离 u u u不超过 k − 2 k-2 k2,同时重复部分又恰好是节点 u u u的子节点,所以这部分可以表示为: f [ u ] [ k − 2 ] f[u][k-2] f[u][k2]

所以最终的结果就是:

g [ u ] [ k ] = f [ u ] [ k ] + g [ f a ( u ) ] [ k − 1 ] − f [ u ] [ k − 2 ] g[u][k]=f[u][k]+g[fa(u)][k-1]-f[u][k-2] g[u][k]=f[u][k]+g[fa(u)][k1]f[u][k2]

但是上述方程成立的条件是 k ≥ 2 k\geq 2 k2的。

所以我们还得想一想 k ≤ 1 k \leq 1 k1的时候。

如果 k = 0 k=0 k=0 g [ u ] [ 0 ] g[u][0] g[u][0]其实就是 v a l [ u ] val[u] val[u],因为不超过距离 0 0 0的点只有本身。

如果 k = 1 k=1 k=1,那么 g [ u ] [ 1 ] g[u][1] g[u][1]其实就是 f [ u ] [ 1 ] + v a l [ f a ( u ) ] f[u][1]+val[fa(u)] f[u][1]+val[fa(u)],因为沿着父节点方向距离 u u u不超过 1 1 1的点,只有父节点,而树中,父节点是唯一的。沿着子节点方向,其实就是 u u u的各个子节点,而这些子节点可以统统用 f [ u ] [ 1 ] f[u][1] f[u][1]表示。

三、代码

#include<bits/stdc++.h>
#define endl '\n'
#define INF 0x3f3f3f3f
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
const int N = 1e5 + 10;
vector<int>edge[N];
int f[N][25], g[N][25];
int val[N];
int n, k;

void dp(int u, int father)
{
	for(int i = 0; i <= k; i ++)
		f[u][i] = val[u];
	for(int i = 0; i < edge[u].size(); i ++ )
	{
		int son = edge[u][i];
		if(son == father)
			continue;
		dp(son, u);

		for(int j = 1; j <= k; j ++ )
		{
			f[u][j] += f[son][j - 1];
		}
	}
	return;
}

void dp2(int u, int father)
{
	for(int i = 0; i < edge[u].size(); i ++ )
	{
		int son = edge[u][i];
		if(son == father)
			continue;
		g[son][0] = val[son];
		g[son][1] = f[son][1] + val[u];
		for(int j = 2; j <= k; j ++ )
		{
			g[son][j] = g[u][j - 1] + f[son][j] - f[son][j - 2]; 
		}
		
		dp2(son, u);
	}
}

void solve()
{
	cin >> n >> k;
	for(int i = 0; i < n - 1; i ++ )
	{
		int a, b;
		cin >> a >> b;
		edge[a].push_back(b);
		edge[b].push_back(a);
	}

	for(int i = 1; i <= n; i ++ )
		cin >> val[i];

	dp(1, 0);
	for(int i = 0; i <= k; i ++ )
	{
		g[1][i] = f[1][i];
	}
	dp2(1, 0);
	// for(int i = 1; i <= n; i ++ )
	// {
	// 	cout << f[i][k] << endl;
	// }
	// cout << endl;
	for(int i = 1; i <= n; i ++ )
	{
		cout << g[i][k] << endl;
	}
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);

	solve();
}
### 解决方案 USACO 的题目 **P2895 Meteor Shower S** 是一道经典的 BFS(广度优先搜索)问题,涉及路径规划以及动态障碍物的处理。以下是关于此题目的 C++ 实现方法及相关讨论。 #### 1. 题目概述 贝茜需要在一个二维网格上移动到尽可能远的位置,同时避开由流星造成的破坏区域。每颗流星会在特定时间落在某个位置,并摧毁其周围的五个单元格(中心及其上下左右)。目标是最小化贝茜受到的风险并计算最短到达安全地点的时间[^5]。 --- #### 2. 关键算法思路 为了高效解决这个问题,可以采用以下策略: - 使用 **BFS(广度优先搜索)** 来模拟贝茜可能的行走路线。 - 动态更新地图上的危险区域,确保在每个时刻只考虑有效的威胁。 - 提前预处理所有流星的影响范围,减少冗余计算。 由于直接在每次 BFS 中调用 `boom` 函数可能导致性能瓶颈[^4],因此可以通过优化来降低复杂度。 --- #### 3. 优化建议 为了避免重复标记已知的危险区域,可以在程序初始化阶段完成如下操作: - 创建一个数组记录每个单位时间内哪些坐标会被流星影响。 - 将 BFS 和流星爆炸事件同步进行,仅在必要时扩展新的状态。 这种方法能够显著提升运行速度,尤其对于大规模输入数据(如 $ M \leq 50,000 $),效果尤为明显。 --- #### 4. C++ 示例代码实现 下面提供了一个高效的解决方案框架: ```cpp #include <bits/stdc++.h> using namespace std; const int MAXN = 1e6; int grid[1001][1001]; // 地图大小假设为合理范围内 bool visited[1001][1001]; queue<pair<int, pair<int, int>>> q; // 存储 {time, {x, y}} // 方向向量定义 vector<pair<int, int>> directions = { {-1, 0}, {1, 0}, {0, -1}, {0, 1} }; void initializeGrid(int N, vector<tuple<int, int, int>>& meteors) { memset(grid, 0, sizeof(grid)); for(auto &[t, x, y] : meteors){ if(t >= N || t < 0) continue; // 超过最大时间或负数忽略 grid[x][y] = max(grid[x][y], t); for(auto &[dx, dy] : directions){ int nx = x + dx, ny = y + dy; if(nx >=0 && nx <1001 && ny>=0 && ny<1001){ grid[nx][ny] = max(grid[nx][ny], t); } } } } bool isValid(int time, int x, int y){ return !(grid[x][y] <= time); // 如果当前时间<=流星爆炸时间则不可通过 } int main(){ ios::sync_with_stdio(false); cin.tie(0); int T, X, Y; cin >> T >> X >> Y; vector<tuple<int, int, int>> meteors(T); for(int i=0;i<T;i++) cin >> get<0>(meteors[i]) >> get<1>(meteors[i]) >> get<2>(meteors[i]); initializeGrid(X*Y, meteors); memset(visited, false, sizeof(visited)); q.push({0,{X,Y}}); visited[X][Y]=true; while(!q.empty()){ auto current = q.front(); q.pop(); int currentTime = current.first; int cx = current.second.first, cy = current.second.second; if(isValid(currentTime,cx,cy)){ cout << currentTime; return 0; } for(auto &[dx,dy]:directions){ int nx=cx+dx,ny=cy+dy; if(nx>=0&&nx<1001&&ny>=0&&ny<1001&&!visited[nx][ny]){ if(isValid(currentTime,nx,ny)){ q.push({currentTime+1,{nx,ny}}); visited[nx][ny]=true; } } } } cout << "-1"; // 若无解返回-1 return 0; } ``` 上述代码实现了基于 BFS 的最优路径查找逻辑,并预先构建了流星影响的地图以加速查询过程。 --- #### 5. 进一步讨论 尽管本题的核心在于 BFS 及动态更新机制的应用,但在实际编码过程中仍需注意以下几个方面: - 输入规模较大时应选用快速 IO 方法(如关闭同步流 `ios::sync_with_stdio(false)` 并取消绑定 `cin.tie(NULL)`)。 - 对于超出边界或者无关紧要的数据点可以直接跳过处理,从而节省不必要的运算开销。 - 利用位掩码或其他压缩技术存储访问标志可进一步节约内存资源。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值