算法
(贪心) O(n2)
如果染色没有限制,那么由排序不等式,我们应该先染权值最大的节点,再染权值第二大的节点,依此类推。
假设权值最大的节点是 b,它的父节点是 a,那么当染色有限制时,如果 a 被染色了,我们应该立即给 b 染色。所以我们就找到了在染色顺序上相邻的两个点。
然后我们再考虑这对点和其它点的关系,比如点 c,那么:
- 如果先染 a,b,再染 c,分值是 a+2b+3c;
- 如果先染 c,再染 a,b,分值是 c+2a+3b;
我们计算一下两个分值的差: a+2b+3c−(c+2a+3b)=2c−(a+b),这个差小于0,等价于 c<a+b2。
所以当且仅当 a,b 的平均值大于 c 时,我们应该先染 a,b,再染 c。
所以我们在考虑剩余点的染色顺序时,可以将 a,b 两个点当成一个点,其权值是 a,b 的均值。
进一步推广,如果有两组点:a1,a2,…an 和 b1,b2,…bm,组内的点在染色时是相邻的一段。我们现在来考虑何时应该先染第一组点:
- 如果先染 ai,则分值是 Sab=∑ni=1ai∗i+∑n+mi=n+1bi∗i;
- 如果先染 bi,则分值是 Sba=∑mi=1bi∗i+∑n+mi=m+1ai∗i;
则 Sab−Sba=n∗∑mi=1bi−m∗∑ni=1ai,所以 Sab−Sba<0⟺∑ni=1ain<∑mi=1bim。
所以我们在考虑剩余点的染色顺序时,可以将这两组点分别当成两个点,其权值分别是两组内所有点权值的平均值。
最终做法是:每次找出当前权值最大的非根节点,将其染色顺序排在紧随父节点之后的位置,然后将该点合并进父节点中,更新父节点的权值。直到将所有点都合并进根节点为止。
如果直接按上述算法做的话,最终的分值不太容易计算,我们可以在将点合并的时候,实时更新当前的权值和:
- 最初所有点各自为一组,总分值是 S=∑ni=1ai∗1;
- 接下来每次会将两组点合并,将其中一组点接在另一组点的后面。比如两组点分别是 xi 和 yi,我们将 yi 接在 xi 之后,则 yi 中每个点所乘的系数均会增加一个相同的偏移量,这个偏移量就是 xi 中点的个数,假设是 k,则合并之后,总的权值直接加上 k∗∑yi 即可;
时间复杂度
如下所示代码最多只有两重循环,所以时间复杂度是 O(n2)。
C++ 代码
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, root;
struct Node
{
int p, s, v;
double avg;
}nodes[N];
int find()
{
double avg = 0;
int res = -1;
for (int i = 1; i <= n; i ++ )
if (i != root && nodes[i].avg > avg)
{
avg = nodes[i].avg;
res = i;
}
return res;
}
int main()
{
cin >> n >> root;
int ans = 0;
for (int i = 1; i <= n; i ++ )
{
cin >> nodes[i].v;
nodes[i].avg = nodes[i].v;
nodes[i].s = 1;
ans += nodes[i].v;
}
for (int i = 0; i < n - 1; i ++ )
{
int a, b;
cin >> a >> b;
nodes[b].p = a;
}
for (int i = 0; i < n - 1; i ++ )
{
int p = find();
int father = nodes[p].p;
ans += nodes[p].v * nodes[father].s;
nodes[p].avg = -1;
for (int j = 1; j <= n; j ++ )
if (nodes[j].p == p)
nodes[j].p = father;
nodes[father].v += nodes[p].v;
nodes[father].s += nodes[p].s;
nodes[father].avg = (double)nodes[father].v / nodes[father].s;
}
cout << ans << endl;
return 0;
}
推广里最后结论的不等号反了吧……
我也觉得
是的
妙脆角~
太巧妙了😢
堆 + 并查集 可行,y总神仙!
非常清晰。感谢y总!
咋想到的啊
n-1次合并 find n-1次 把p的儿子的father更新为p的father 不应该是o(n^3)嘛
latex
# markdown
ans 不需要开 long long 吗
1000*1000*1001/2<1E9,不需要
666
可以用 自定义结构体的set存储权值和序号,小于号重载为权值小于,将每一步合并的点的权值删除,插入新的权值,每次取set 末尾点的序号,删除和插入操作O(nlogn),取点O(1),同时,可以用链式前向星存储每个结点的孩子节点,每一步将目标点向父节点合并时父节点可以O(1)继承目标节点的全部孩子
#include<iostream> #include<vector> using namespace std; const int maxn=1010; struct Node { int fa,sz,sum; double avg; }a[maxn]; struct Heap { int num; double avg; }b[maxn]; vector<int> g[maxn]; int n,root; int h_sz; void heapify(int x) //向下更新 { int L=2*x,R=2*x+1,mx=x; if(L<=h_sz&&b[L].avg>b[x].avg) mx=L; if(R<=h_sz&&b[R].avg>b[mx].avg) mx=R; if(mx!=x) { swap(b[x],b[mx]); heapify(mx); } } void update(int p) //向上更新 { int fa=p/2; while(p>1&&b[p].avg>b[fa].avg) { swap(b[p],b[fa]); p=fa; fa=p/2; } } int main() { cin>>n>>root; int ans=0; for(int i=1;i<=n;i++) { cin>>a[i].sum; a[i].sz=1; a[i].avg=a[i].sum; ans+=a[i].sum; } for(int i=1,x,y;i<n;i++) { cin>>x>>y; a[y].fa=x; g[x].push_back(y); } for(int i=1;i<=n;i++) { if(i==root) continue; b[++h_sz].num=i; b[h_sz].avg=a[i].avg; } for(int i=h_sz/2;i>=1;i--) //建堆 heapify(i); for(int i=1;i<n;i++) { int mx=b[1].num; swap(b[1],b[h_sz--]); heapify(1); int fa=a[mx].fa; ans+=a[mx].sum*a[fa].sz; for(int j=0;j<g[mx].size();j++) // 1 g[fa].push_back(g[mx][j]),a[g[mx][j]].fa=fa; a[fa].sum+=a[mx].sum; a[fa].sz+=a[mx].sz; a[fa].avg=(double)a[fa].sum/a[fa].sz; for(int i=1;i<=h_sz;i++) //2 if(b[i].num==fa) { b[i].avg=a[fa].avg; heapify(i); update(i); } } cout<<ans; return 0; }
码了半天,帮忙看看1和2有没有更优化的方法?
这个代码实现能力,处理方式很强%%%
find()里avg和res的赋值反了吧,虽然对结果没什么影响
没反呀hh
%%%%%%%%%%%%%%%%%
在find函数里面限制了i!=root,永远都不会取到root,这是把root放在最后了吗?
这道题的根节点必须第一个染色,否则树中其他点都不能染色。
明白了,题目中的根节点随时可以染色有点干扰了。谢谢y总
MarkDown测试
所以测试成功了嘛hh
其实这题应该可以用堆或者其他数据结构做到nlogn的复杂度吧。(缩点用并查集)
看起来是可以的,用堆维护最大值,用并查集合并点,这样可以把时间复杂度降至 O(nlogn)。
可以用堆,但是需要手写,因为需要改变father节点属性
可以的。
Sab - Sba > 0的等价条件应该写错了吧∑bi / m > ∑ai / n吧
已修正~
p s v能不能单词写全啊
写全的话就太长啦,详细讲解在这里:AcWing 115. 给树染色。
### 祝大雪菜老师直播讲课月入百万