当前位置: 代码迷 >> 综合 >> P2986 [USACO10MAR]Great Cow Gathering G(带点权树重心/树形dp+换根dp)详解
  详细解决方案

P2986 [USACO10MAR]Great Cow Gathering G(带点权树重心/树形dp+换根dp)详解

热度:52   发布时间:2024-02-25 17:30:58.0

https://www.luogu.com.cn/problem/P2986


先说带点权的树重心做法:

https://blog.csdn.net/zstuyyyyccccbbbb/article/details/108558682树的重心的定义以及一些性质。

如果这个树有点权和边权怎么办?

关于边权:其实和所有边为单位1的重心一模一样。(网上搜了搜没怎么有比较详细的说明的,姑且当性质结论1)

 

关于点权:把”最大的子树节点数最少“改成“最大点权块最小”。

感性证明:

把这个问题转化成刚在关于边权的那个重心问题。去掉点权,也就是把有c[u]头牛的农场u拆成c[u]个牛数为1的农场。

那么发现,新拆出来的农场之间距离应该都是0,然后到其他一个农场的距离贡献都是一样的到该农场的边权。

这就类似建立了一个虚拟源点,这个源点到其他农场的距离等于每一个拆出来的农场的贡献和,而拆出来的农场彼此之间距离都为0.

那么此时这个图就变成了有好多边权为0和边权一致的比原来多了很多点的图。由结论1得重心和边权无关,和结点数有关,那么此时结点数变多,真实的重心应该由每一个节点的siz[i]=1结合而来,融汇到一起,就是这个虚拟源点(题目给的点)的c[i]。

由此点权将siz[i]=c[i],然后由原来树的重心去求。

由于树的重心到每个点的距离和是最小的。求出来就是最终答案了。

#include<iostream>
#include<vector>
#include<queue>
#include<cstring>
#include<cmath>
#include<map>
#include<set>
#include<cstdio>
#include<algorithm>
#define debug(a) cout<<#a<<"="<<a<<endl;
using namespace std;
const int maxn=1e5+100;
typedef long long LL;
LL n,sum=0,ans=0;
LL son[maxn],siz[maxn],dweight[maxn];
struct edge{LL to,w;
};
vector<edge>g[maxn];
void dfs(LL u,LL fa)
{siz[u]=dweight[u];for(LL i=0;i<g[u].size();i++){LL v=g[u][i].to;if(v==fa) continue;dfs(v,u);siz[u]+=siz[v];son[u]=max(son[u],siz[v]);}son[u]=max(son[u],sum-siz[u]);
}
void dfs2(LL u,LL fa,LL dis)
{
///	cout<<"g["<<u<<"]="<<g[u].size()<<endl;for(LL i=0;i<g[u].size();i++){LL to=g[u][i].to;if(to==fa) continue;cout<<u<<"---->"<<to<<endl;ans+=dweight[to]*(dis+g[u][i].w);
///		debug(ans);dfs2(to,u,dis+g[u][i].w);}
}
int main(void)
{cin.tie(0);std::ios::sync_with_stdio(false);cin>>n;for(LL i=1;i<=n;i++){cin>>dweight[i];sum+=dweight[i];}for(LL i=1;i<n;i++){LL u,v,c;cin>>u>>v>>c;g[u].push_back({v,c});g[v].push_back({u,c});}dfs(1,0);LL point=1;for(LL i=1;i<=n;i++){if(son[point]>son[i]){point=i;//带点权边权树的重心 }}/// cout<<"重心是"<<point<<endl;dfs2(point,-1,0);cout<<ans<<endl;
return 0;
}

考虑换根dp。换根dp的一个比较显然的条件是可以o(n^2)暴力求每个点。这个题的暴力原题是https://www.luogu.com.cn/problem/P1364。可以暴力快乐过题。

那么换根dp先以任意一个点为根,求出以该点为根为的答案值。再通过这个点和父亲,节点的关系来更新。

先预处理出以1为根的答案dp[1]。

举例:以2为根的答案值为以2的父亲的答案值加上以(1为根的子树大小-以2为根的子树大小)*(1~2的边权)-以2为根的子树的子节点们要减去到以1为根的节点的多出来的(1~2)的边权。

文字表述有点长。换根dp建议画图比较容易理解。

表达式子: dp[v]=dp[u]+(siz[1]-2*siz[v])*g[u][i].w;

#include<iostream>
#include<vector>
#include<queue>
#include<cstring>
#include<cmath>
#include<map>
#include<set>
#include<cstdio>
#include<algorithm>
#define debug(a) cout<<#a<<"="<<a<<endl;
using namespace std;
const int maxn=1e5+100;
typedef long long LL;
LL dp[maxn],siz[maxn],dis[maxn],c[maxn];
struct edge{LL to,w;
};
vector<edge>g[maxn];
void dfs1(LL u,LL fa)
{siz[u]=c[u];for(LL i=0;i<g[u].size();i++){LL v=g[u][i].to;if(v==fa) continue;dis[v]=dis[u]+g[u][i].w;//每个点到根节点的距离 dfs1(v,u);siz[u]+=siz[v];//以u为根的子树奶牛数量 }
}
void dfs2(LL u,LL fa)
{for(LL i=0;i<g[u].size();i++){LL v=g[u][i].to;if(v==fa) continue;dp[v]=dp[u]+(siz[1]-2*siz[v])*g[u][i].w;dfs2(v,u);}
}
int main(void)
{cin.tie(0);std::ios::sync_with_stdio(false);LL n;cin>>n;for(LL i=1;i<=n;i++) cin>>c[i];for(LL i=1;i<n;i++){LL u,v,w;cin>>u>>v>>w;g[u].push_back({v,w});g[v].push_back({u,w});}dfs1(1,-1);for(LL i=2;i<=n;i++){dp[1]+=dis[i]*c[i]; }dfs2(1,-1);LL ans=1e18;for(LL i=1;i<=n;i++) ans=min(ans,dp[i]);cout<<ans<<endl;
return 0;
}