一道经典贪心。
本题的贪心策略是:令 a i a_i ai 表示节点 i i i 的权值, f a i fa_i fai 表示节点 i i i 的父亲,可以发现,假如 f a i fa_i fai 被染色了,且 a i ≥ ∀ a j a_i\ge ∀a_j ai≥∀aj 且不为根节点,则接下来必定染 a i a_i ai。
既然规定了顺序,我们就可以将 f a i fa_i fai 与 i i i 合并,用并查集维护更方便些。
但是在处理节点之间的比较时,我们需要进行一些特殊处理。
在《算进》中,其提到一种“等效权值”,令 s u m sum sum 为该点包含的原始权值总和, n u m num num 为该点包含的原始点数,其定义为 s u m n u m \frac{sum}{num} numsum。但是如果直接用此比较大小,会出现浮点误差。
因此,我们选择规避掉。
明显地,设两个节点为 x , y x,y x,y,则有 s u m x n u m x \frac{sum_x}{num_x} numxsumx 与 s u m y n u m y \frac{sum_y}{num_y} numysumy,两者乘上 n u m x × n u m y num_x\times num_y numx×numy,得 s u m x × n u m y sum_x \times num_y sumx×numy 与 s u m y × n u m x sum_y \times num_x sumy×numx。
于是这个处理就结束了。
具体见代码。
AC Code:
#include
#define int long long
using namespace std;
struct node{
int sum,num,fa,ans;//sum,num 意义见上,fa 表示该节点的父亲,ans 表示到当前节点为止的答案
}a[1005];
int n,r,fa[1005];
bool operator<(const node &x,const node &y)//比较
{
return x.sum*y.num<y.sum*x.num;
}
int find(int x)//并查集的 find()
{
return fa[x]==x?x:fa[x]=find(fa[x]);
}
signed main()
{
ios::sync_with_stdio(0);
cin>>n>>r;
while(n&&r)
{
for(int i=1;i<=n;i++)
{
cin>>a[i].sum;
a[i].num=1;
fa[i]=i;
a[i].ans=a[i].sum;//初始化
}
for(int i=1;i<n;i++)
{
int x,y;
cin>>x>>y;
a[y].fa=x;
}
int t=n;//t 为节点数
while(t>2)//当结点数还未合并到两个时执行
{
node maxn={0,1,0};//找最大非根子节点
int j=0;
for(int i=1;i<=n;i++)
{
int x=find(i);
if(x==r) continue;//为根节点
if(maxn<a[x]) maxn=a[x],j=x;
}
int k=find(maxn.fa);
a[k].sum+=a[j].sum;//合并时相应值也要合并
a[k].ans+=a[k].num*a[j].sum+a[j].ans;//处理答案
a[k].num+=a[j].num;
fa[j]=k;//合并节点
t--;
}
if(t==1) cout<<a[1].ans<<endl;//单个根节点的特判
else
{
int j=0;
for(int i=1;i<=n;i++)
{
int x=find(i);
if(x==r) continue;
j=x;
break;
}
cout<<a[r].num*a[j].sum+a[j].ans+a[r].ans<<endl;//最后将答案合并输出
}
cin>>n>>r;
}
return 0;
}