CCF 2019:城市规划(树形dp,贡献)

在这里插入图片描述


比较容易想到的一种做法是:设dp[i][j] 表示 i 为根,选 j 个点的答案。在子树合并时,枚举子树选取的点的个数和当前选取的结点个数,加上统计当前这条边对答案的贡献更新。

转移式子为:dp[u][i+j] = min(dp[u][i+j],dp[u][i]+dp[v][j] + i * j * w)

一跑会发现样例都过不了,原因是这条边的贡献只在选的点超过一个时才会计算,答案必然会漏算某些贡献。

一个补救的方法是:另开一个数组tp[i][j] 记录dp[i][j] 取最小值时,选的点到当前i点的距离之和。

转移方程为:dp[u][i+j] =min(dp[u][i+j],dp[u][i] + dp[v][j] + tp[u][i] * j + tp[v][j] * i + i * j * w

要注意在 dp[u][i+j] 相同的情况下,tp[u][i+j] 要取最小值,因为dp[u][i+j]相同的情况下,显然tp[u][i]越小对后面的转移越有利。

一看样例过了,似乎是一种可行的方案。交上去发现只有30分(还不如暴力打满40)。

原因是dp转移式子中,转移值不只取决于 dp[i][j],还涉及到tp[i][j],而 tp[i][j] 只在 dp[i][j] 尽量小的前提下尽量小。这相当于是在忽略tp[i][j]在转移中带来的影响,数据足够强的话很容易卡掉。

正确的做法是:
还是从贡献的角度考虑,一个很明确的思路是,如果在子树中确定好选的点,产生的新贡献和当前这条边权有关。观察到题目一定有解,并且我们只关注最后的答案。考虑直接用树形dp维护对最终答案的贡献。

dp[i][j] 表示 i 为根选 j个结点 对最终答案的贡献。显然dp[1][k] 会是正确的答案。

转移方程为:dp[u][i+j] = min(dp[u][i+j],dp[u][i] + dp[v][j] + w * j * (k - j))
当决定在子树v中选 j 个点时,最终答案连向这颗子树的边权一定会经过 j * (k - j) 次

细算一下复杂度上界是5e8,需要在转移时加一些剪枝降低上界。


代码:

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
#define pii pair<int,int>
#define fir first
#define sec second
typedef long long ll;
vector<pii> g[maxn];
int vis[maxn],sum[maxn],m,n,k;
ll dp[maxn][300],tmp[1000];
void dfs(int u,int fa) {
	for(int i = 0; i <= k; i++) {
		dp[u][i] = 1e15;
	}
	dp[u][0] = 0;
	if(vis[u]) sum[u] = 1,dp[u][1] = 0;
	for(auto it : g[u]) {
		if(it.fir == fa) continue;
		dfs(it.fir,u);
		for(int i = 0; i <= k; i++)
			tmp[i] = 1e15;
		for(int i = 0; i <= min(sum[u],k); i++) {
			for(int j = 0; j <= sum[it.fir] && i + j <= k; j++) {
				ll w = dp[u][i] + dp[it.fir][j] + 1ll * (k - j) * j * it.sec;
				tmp[i+j] = min(tmp[i+j],w);
			}
		}
		sum[u] += sum[it.fir];
		for(int i = 0; i <= min(k,sum[u]); i++) {
			dp[u][i] = min(dp[u][i],tmp[i]);
		}
	}
}

int main() {
	scanf("%d%d%d",&n,&m,&k);
	for(int i = 1,x; i <= m; i++) {
		scanf("%d",&x);
		vis[x] = 1;
	}
	for(int i = 1,u,v,w; i < n; i++) {
		scanf("%d%d%d",&u,&v,&w);
		g[u].push_back(pii(v,w));
		g[v].push_back(pii(u,w));
	}
	dfs(1,0);
	printf("%lld\n",dp[1][k]);
	return 0;
}
/*
5 3 2
1 3 5
1 2 4
1 3 5
1 4 3
4 5 1

11 6 2
2 5 7 8 9 10
1 2 2
1 3 4
2 4 3
2 5 6
3 6 8
3 7 11
4 8 9
6 9 2
6 10 1
7 11 4
*/
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值