JZOJ 3567. 【GDKOI2014】石油储备计划

题目

解析

多种解法:有上下界费用流(nb),树形DP等
而由于我太菜,前者待日后再补
下面介绍树形DP的解法

首先我们等发现一些性质:
最后使方差最小,树的每个点权值必然在 ([sum/n..sum/n+1]) 之间,其中 (sum) 指石油总和
那么我们可不可以试试枚举最后有多少个点为 (sum/n+1)
当然可以

(f_{i,j}) 表示以 (i) 为根的子树有 (j) 个点权值为 (sum/n+1)
那么转移时枚举它的儿子,它子树的 (j),它儿子子树的 (j)
(f_{i,j}=f_{son,k}+f_{i,j-k}+c*|s[v] - (ave + 1)*k - ave*(sz[v] - k)|)
(c) 为子树到 (i) 的边权,(s_v) 表示子树的油的总和,那么后面一截就是必须流出去或流进来的,经过 (c) 这条边。
注意我们枚举时是先用某一个儿子来更新原来的 (f),而转移时又要借助 (father) 子树的 (j),所以我们不能直接修改,而是应该在所有 (j) 都枚举完后再修改,具体可见代码

总共是 (O(n^3))

#include<cstdio>
#include<cmath>
#include<cstring>
#include<iostream>
using namespace std;
typedef long long LL;

const int N = 105;
int n , num , sz[N] , h[N] , tot;
LL f[N][N] , g[N] , s[N] , ave;

struct edge{
	int to , w , nxt;
}e[N << 1];

inline void add(int x , int y , int z){e[++tot] = edge{y , z , h[x]} , h[x] = tot;}
inline void dfs(int x , int fa)
{
	sz[x] = 1;
	f[x][0] = f[x][1] = 0;
	for(register int i = h[x]; i; i = e[i].nxt)
	{
		int v = e[i].to;
		if (v == fa) continue;
		dfs(v , x) , s[x] += s[v] , sz[x] += sz[v];
	}
	for(register int i = h[x]; i; i = e[i].nxt)
	{
		int v = e[i].to;
		if (v == fa) continue;
		memset(g , 0x3f3f3f3f , sizeof g);
		for(register int j = 0; j <= num && j <= sz[x]; j++)
			for(register int k = 0; k <= j && k <= sz[v]; k++)
				g[j] = min(g[j] , f[v][k] + f[x][j - k] + (LL)abs(s[v] - (ave + 1)*k - ave*(sz[v] - k)) * e[i].w);
		for(register int j = 0; j <= num && j <= sz[x]; j++) f[x][j] = g[j];
	}
}

int main()
{
	int T;
	scanf("%d" , &T);
	for(; T; T--)
	{
		scanf("%d" , &n);
		ave = 0;
		for(register int i = 1; i <= n; i++) scanf("%lld" , &s[i]) , ave += s[i];
		num = ave % n , ave = ave / n , tot = 0 , memset(h , 0 , sizeof h);
		int x , y , z;
		for(register int i = 1; i < n; i++) 
		{
			scanf("%d%d%d" , &x , &y , &z);
			add(x , y , z) , add(y , x , z);
		}
		memset(f , 0x3f3f3f3f , sizeof f);
		dfs(1 , 0);
		printf("%lld
" , f[1][num]);
	}
}
原文地址:https://www.cnblogs.com/leiyuanze/p/13498231.html