题目大意:
你有一棵 n 个点的有根树,每条边都是带权的,其中 1 号点是这棵树的根。你开始你把一个棋子放在 1 号点
位置。接下来你可以做如下的事情:
• 将这个棋子移向它的某个儿子。代价为这条边的权值。
• 将这个棋子跳回 1 号节点。这个操作不需要任何代价。
• 在这个棋子所在的位置设立存档点。注意你只能在整棵树上设立一个存档点,如果之前在某个节点上设立过
存档点,那么原来的存档点会消失。这个操作不需要任何代价。
• 将棋子跳回到设立的存档点。这个操作不需要任何代价。
你想要访问每个节点至少一次,问最小的代价是多少。
首先可以想到是个树状dp,但是怎么设状态和转移方法实在是太难想了。本来以为每个子节点状态要分设不设存档点讨论,其实不用。
dp[x]表示x的子树,在x处设一个存档点(最终操作完成后存档点落到x或是他的任意一个子节点无所谓),将整个子树都遍历一遍(最后一步走到哪里无所谓,反正都要跳回1再走到x的father)的最小代价。
转移的确是神仙操作。对于每个节点,它的子节点y对应子树有三种遍历方法
1:存档点始终在x不动
2:存档点下推到y,这样的话最后走完所有节点后,直接跳回1,再从1走到x(再次走到x时肯定贪心设存档点),产生一个多余代价
3:可以发现最终存档点落到哪里无所谓,所以遍历最后一个子树时,可以在最后一个子树中把存档点下推下去,而不用加上再从1号点到x的额外代价,设这个子树为特殊子树
#include<iostream>
#include<cstdio>
#include<cstring>
#define LL long long
using namespace std;
int n,h[2020],m1;
struct edge{
int next,to;
LL val;
void Add(int N,int T,LL v){
next=N; to=T; val=v;
}
}q[2020];
LL dep[2020],dp[2020],sum_dis[2020],sum_lv[2020];
LL Min(LL a,LL b){
if (a<b) return a; return b;
}
void Add(){
int x,y;
LL Val;
scanf("%d %d %lld",&x,&y,&Val);
q[++m1].Add(h[x],y,Val); h[x]=m1;
q[++m1].Add(h[y],x,Val); h[y]=m1;
}
void Dfs(int x,int fa){
int i,y;
bool lv=1;
for (i=h[x];i;i=q[i].next){
y=q[i].to;
if (y==fa) continue;
lv=0;
dep[y]=dep[x]+q[i].val;
Dfs(y,x);
sum_lv[x]+=sum_lv[y];
sum_dis[x]+=sum_dis[y];
}
if (lv){
sum_dis[x]=dep[x];
sum_lv[x]=1;
}
}
void Dp(int x,int fa){
int i,y;
LL k=0,k1,k2;
for (i=h[x];i;i=q[i].next){
y=q[i].to;
if (y==fa) continue;
Dp(y,x);
k1=sum_dis[y]-dep[x]*sum_lv[y];
//存档点不动,两个sum简化求子树各个叶子到x距离
k2=dep[y]+dp[y];
//存档点推下去再走回x
k2=Min(k1,k2);
dp[x]+=k2;
k=Min(k,dp[y]+q[i].val-k2);
//把一个子树改造成特殊子树能减小的最大代价
}
dp[x]+=k;
}
void Work(){
scanf("%d",&n); m1=0;
memset(dp,0,sizeof(dp)); memset(dep,0,sizeof(dep));
memset(sum_dis,0,sizeof(sum_dis));
memset(sum_lv,0,sizeof(sum_lv));
memset(h,0,sizeof(h)); memset(q,0,sizeof(q));
int i;
for (i=1;i<n;i++) Add();
Dfs(1,0);
Dp(1,0);
cout<<dp[1]<<endl;
}
int main(){
int num; cin>>num;
while (num--) Work();
}