求一棵n点树的k个点,使得任意两点距离和最小。
关键字:树形分组背包、换角度求和
原题要求求任意两点的距离之和,不易处理,因此我们可以从边的角度,计算每条边计算了多少次。一条边<u,v>可以将一棵树分为两部分,每部分有x,k-x个点,则该边被计算了2*x*(k-x)次。因此,该边的贡献是2*x*(k-x)*w<u,v>,若u是v的父亲,且已知v中x条边对整体的最小贡献,则可以通过分组背包模型解决该问题。dp[u][k][i]:u节点遍历到前m个孩子节点时,点容量为i的最小距离和。每个孩子可以占据1,2...i个点和贡献相应的距离。dp[u][m][i]={dp[u][m-1][i],dp[u][m-1][i-j]+(dp[v][j]+2*w<u,v>*(k-j)*j)}
完全背包模型:一共有n件物品,和容量为v的背包。第i件物品的容量是Ci,价值是Wi。背包被分为k组,每组至多选1件物品。求一种选择方案,使得物品总容量不超过背包容量,且价值最大。
状态转移方程:F [k; v] = max{ F [k - 1; v]; F [k - 1; v - Ci ] + Wi | item i ∈ group k}
伪代码:
for k <—1 to K 枚举所有的组 (在树中即为每个子节点)
for v <— V to 0 枚举所有的容量 从大到小,保证每组仅有一个进入背包(在树中即为每个孩子仅进入一次)
for all item i in group k 每组选择一件物品(每个子节点进行一次选择)
F [v] = max{ F [v], F [v - Ci ] + Wi }
#include<stdio.h>
#include<iostream>
#include<string.h>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
#define ll long long
#define sf scanf
#define pf printf
#define maxn 10000
#define inf 0x3f3f3f
#define INF 1ll<<60
#define mem(a,b) memset(a,b,sizeof(a))
#define lowbit(x) x&(-x)
using namespace std;
int t,n,k;
struct Edge{
int to,next;
ll w;
}edge[maxn];
int head[maxn],tot;
ll dp[maxn][55];
void add(int u,int v,ll w){
edge[tot].to=v,edge[tot].next=head[u],edge[tot].w=w;
head[u]=tot++;
}
void init(){
mem(head,-1);
tot=0;
for(int i=1;i<=n;i++)
for(int j=0;j<=k;j++)
dp[i][j]=(j==1||j==0)?0:INF;
}
void dfs(int u,int fa){//树形分组背包
dp[u][0]=dp[u][1]=0;
for(int l=head[u];l!=-1;l=edge[l].next){//每个孩子 <=> 每个组
int v=edge[l].to;
ll w=edge[l].w;
if(v==fa) continue;
dfs(v,u);//1.递归求得每个孩子的最优值
//2.对每个孩子进行完全背包
for(int i=k;i>=1;i--){//容量
for(int j=1;j<=i;j++){//枚举每组的物品
ll cost=dp[v][j]+(ll)w*j*(k-j)*2;
dp[u][i]=min(dp[u][i],dp[u][i-j]+cost);
}
}
}
}
int main(){
//freopen("a.txt","r",stdin);
scanf("%d",&t);
while(t--){
scanf("%d%d",&n,&k);
init();
for(int i=0;i<n-1;i++){
int a,b;
ll c;
scanf("%d%d%lld",&a,&b,&c);
add(a,b,c),add(b,a,c);
}
dfs(1,0);
printf("%lld\n",dp[1][k]);
}
return 0;
}
bug:分组背包必须先枚举组,再枚举容量,不能先枚举容量!
long long的上限取1ll<<60即可