1. 程式人生 > 實用技巧 >JZOJ 3567. 【GDKOI2014】石油儲備計劃

JZOJ 3567. 【GDKOI2014】石油儲備計劃

題目

解析

多種解法:有上下界費用流(nibi),樹形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\n" , f[1][num]);
	}
}