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

    科技2024-07-24  13

    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; }

     

     

     

    Processed: 0.010, SQL: 8