http://acm.hdu.edu.cn/showproblem.php?pid=6769
正常的求直径的树形dp一般是记录一个最长链和一个次长链,然后第二遍换根dp再记录一个从父节点转移过来的最长链,然后求出这个直径。然而这题需要加一维j表示当前子树已经使用了j个a边后的最长链和次长链,这就很难搞了,因为无法保证最长链最小的同时保证最长链+次长链最小,然后思考了2小时人生。
赛后题解理解不能,还是群巨牛逼,一问就懂。群巨补题地址 http://www.koule2333.top:3000/s/H1Lv7U1A8
这题并不直接维护直径,不求出直径,而是二分直径的长度mid后,维护在直径不超过mid前提下,每个子树中所有点到子树根节点的最长距离的最小值。
那么我们对子树根节点u,它的一棵子树v进行合并,一开始u已经合并了部分子树,现在把子树v也合并到u
首先定一个临时变量数组tmp,表示合并之后的最长距离的最小值,然后枚举连通块u的已经用的过的a边数量j,再枚举子树v中已经用过的a边数量k,那么如果j+k+1<=m,就可以更新tmp[j+k+1],此时的最大值就是max(dp[u][j],dp[v][k]+a),如果u-v这条边不用,那就是upd(tmp[j+k],max(dp[v][k]+b,dp[u][k])),这样我们在转移时会丢掉一些使得直径大于mid的情况,最后留到根节点的始终是保证直径<=mid的,那么如果dp[1][m]<=inf,则存在整体直径<=mid的情况。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxl=2e4+10;
const ll inf=1ll<<60;
int n,m;
ll l,r,mid,ans;
int sz[maxl];
ll tmp[21];
ll dp[maxl][21];
struct ed{int to,a,b;};
vector<ed> e[maxl];
inline void prework()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
e[i].clear();
int u,v,a,b;l=1;r=0;
for(int i=1;i<=n-1;i++)
{
scanf("%d%d%d%d",&u,&v,&a,&b);
e[u].push_back(ed{v,a,b});
e[v].push_back(ed{u,a,b});
r+=max(a,b);
}
}
inline void upd(ll &x,ll y)
{
if(x>y) x=y;
}
inline bool dfs(int u,int fa)
{
for(int i=0;i<=m;i++)
dp[u][i]=inf;
dp[u][0]=0;
int v,up;sz[u]=0;
for(ed ee:e[u])
{
v=ee.to;
if(v==fa) continue;
dfs(v,u);up=min(m,sz[u]+sz[v]+1);
for(int j=0;j<=m;j++)
tmp[j]=inf;
for(int j=0;j<=sz[u];j++)
for(int k=0;k<=sz[v];k++)
{
if(dp[u][j]+dp[v][k]+ee.a<=mid && j+k+1<=m)
upd(tmp[j+k+1],max(dp[u][j],dp[v][k]+ee.a));
if(dp[u][j]+dp[v][k]+ee.b<=mid && j+k<=m)
upd(tmp[j+k],max(dp[u][j],dp[v][k]+ee.b));
}
for(int j=0;j<=m;j++)
dp[u][j]=tmp[j];
sz[u]=up;
}
}
inline bool jug(){dfs(1,0);return dp[1][m]<=mid;}
inline void mainwork()
{
while(l+1<r)
{
mid=(l+r)>>1;
if(jug())
r=mid;
else
l=mid;
}
mid=l;
if(jug())
ans=l;
else
ans=l+1;
}
inline void print()
{
printf("%lld\n",ans);
}
int main()
{
int t;
scanf("%d",&t);
for(int i=1;i<=t;i++)
{
prework();
mainwork();
print();
}
return 0;
}