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

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

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: p109 [noip2004 提高组] 合并果子: 这道题目是一道经典的贪心算法题目,题目大意是给定n个果子,每个果子的重量为wi,现在需要将这n个果子合并成一个果子,每次合并需要消耗的代价为合并的两个果子的重量之和,求最小的代价。 我们可以使用贪心算法来解决这个问题,每次选择两个最小的果子进行合并,然后将合并后的果子的重量加入到集合中,重复这个过程直到只剩下一个果子为止。 这个算法的正确性可以通过反证法来证明,假设存在一种更优的合并方案,那么这个方案一定会在某一步将两个比当前选择的两个更小的果子进行合并,这样就会得到一个更小的代价,与当前选择的方案矛盾。 usaco06nov fence repair: 这道题目是一道经典的贪心算法题目,题目大意是给定n个木板,每个木板的长度为li,现在需要将这n个木板拼接成一块长度为L的木板,每次拼接需要消耗的代价为拼接的两个木板的长度之和,求最小的代价。 我们可以使用贪心算法来解决这个问题,每次选择两个最小的木板进行拼接,然后将拼接后的木板的长度加入到集合中,重复这个过程直到只剩下一个木板为止。 这个算法的正确性可以通过反证法来证明,假设存在一种更优的拼接方案,那么这个方案一定会在某一步将两个比当前选择的两个更小的木板进行拼接,这样就会得到一个更小的代价,与当前选择的方案矛盾。 ### 回答2: 题目描述: 有n个果子需要合并,合并任意两个果子需要的代价为这两个果子的重量之和。现在有一台合并机器,可以将两个果子合并成一堆并计算代价。问将n个果子合并成一堆的最小代价。 这个问题可以用贪心算法来解决,我们可以使用一个最小堆来存储所有果子的重量。每次从最小堆中取出两个最小的果子,将它们合并成为一堆,并将代价加入答案中,将新堆的重量加入最小堆中。重复以上步骤,直到最小堆中只剩下一堆为止。这样得到的代价就是最小的。 证明如下: 假设最小堆中的果子按照重量从小到大依次为a1, a2, ..., an。我们按照贪心策略,每次都将重量最小的两个果子合并成为一堆,设合并的过程为b1, b2, ..., bn-1。因此,可以发现,序列b1, b2, ..., bn-1必然是一个前缀和为a1, a2, ..., an的 Huffman 树变形。根据哈夫曼树的定义,这个树必然是最优的,能够得到的代价最小。 因此,使用贪心策略得到的答案必然是最优的,而且时间复杂度为O(n log n)。 对于[usaco06nov] fence repair g这道题,其实也可以用相同的思路来解决。将所有木板的长度存储在一个最小堆中,每次取出最小的两个木板长度进行合并,代价即为这两个木板的长度之和,并将合并后木板的长度加入最小堆中。重复以上步骤,直到最小堆中只剩下一块木板。得到的代价就是最小的。 因此,贪心算法是解决这类问题的一种高效、简单但有效的方法,可以应用于很多有贪心性质的问题中。 ### 回答3: 这两个题目都需要对操作进行模拟。 首先是合并果子。这个题目先将所有果子放进一个优先队列中。每次取出来两个果子进行合并,直到只剩下一个果子即为答案。合并的代价为两个果子重量之和。每次合并完之后再将新的果子放入优先队列中,重复上述过程即可。 再来看fence repair。这个题目需要用到贪心和并查集的思想。首先将所有板子的长度放入一个最小堆中,每次取出堆顶元素即为最短的板子,将其与其相邻的板子进行合并,合并的长度为这两块板子的长度之和。操作完之后再将新的板子长度放入最小堆中,重复上述过程直到只剩下一块板子。 关于合并操作,可以使用并查集来实现。维护每个板子所在的集合,每次操作时合并两个集合即可。 最后,需要注意的是题目中给出的整数都很大,需要使用long long来存储避免溢出。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值