算法
(贪心) $O(n^2)$
如果染色没有限制,那么由排序不等式,我们应该先染权值最大的节点,再染权值第二大的节点,依此类推。
假设权值最大的节点是 $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 < \frac{a + b}{2}$。
所以当且仅当 $a, b$ 的平均值大于 $c$ 时,我们应该先染 $a, b$,再染 $c$。
所以我们在考虑剩余点的染色顺序时,可以将 $a, b$ 两个点当成一个点,其权值是 $a, b$ 的均值。
进一步推广,如果有两组点:$a_1, a_2, … a_n$ 和 $b_1, b_2, … b_m$,组内的点在染色时是相邻的一段。我们现在来考虑何时应该先染第一组点:
- 如果先染 ${a_i}$,则分值是 $S_{ab} = \sum_{i=1}^n a_i * i + \sum_{i=n+1}^{n+m} b_i * i$;
- 如果先染 ${b_i}$,则分值是 $S_{ba} = \sum_{i=1}^m b_i * i + \sum_{i=m+1}^{n+m} a_i * i$;
则 $S_{ab} - S_{ba} = n * \sum_{i=1}^m b_i - m * \sum_{i=1}^n a_i$,所以 $S_{ab} - S_{ba} < 0 \Longleftrightarrow \frac{\sum_{i=1}^n a_i}{n} < \frac{\sum_{i=1}^m b_i}{m}$。
所以我们在考虑剩余点的染色顺序时,可以将这两组点分别当成两个点,其权值分别是两组内所有点权值的平均值。
最终做法是:每次找出当前权值最大的非根节点,将其染色顺序排在紧随父节点之后的位置,然后将该点合并进父节点中,更新父节点的权值。直到将所有点都合并进根节点为止。
如果直接按上述算法做的话,最终的分值不太容易计算,我们可以在将点合并的时候,实时更新当前的权值和:
- 最初所有点各自为一组,总分值是 $S = \sum_{i=1}^n a_i * 1$;
- 接下来每次会将两组点合并,将其中一组点接在另一组点的后面。比如两组点分别是 ${x_i}$ 和 ${y_i}$,我们将 ${y_i}$ 接在 ${x_i}$ 之后,则 ${y_i}$ 中每个点所乘的系数均会增加一个相同的偏移量,这个偏移量就是 ${x_i}$ 中点的个数,假设是 $k$,则合并之后,总的权值直接加上 $k * \sum y_i$ 即可;
时间复杂度
如下所示代码最多只有两重循环,所以时间复杂度是 $O(n^2)$。
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)继承目标节点的全部孩子
码了半天,帮忙看看1和2有没有更优化的方法?
这个代码实现能力,处理方式很强%%%
find()里avg和res的赋值反了吧,虽然对结果没什么影响
没反呀hh
%%%%%%%%%%%%%%%%%
在find函数里面限制了i!=root,永远都不会取到root,这是把root放在最后了吗?
这道题的根节点必须第一个染色,否则树中其他点都不能染色。
明白了,题目中的根节点随时可以染色有点干扰了。谢谢y总
$$\texttt{MarkDown测试}$$
所以测试成功了嘛hh
其实这题应该可以用堆或者其他数据结构做到nlogn的复杂度吧。(缩点用并查集)
看起来是可以的,用堆维护最大值,用并查集合并点,这样可以把时间复杂度降至 $O(nlogn)$。
可以用堆,但是需要手写,因为需要改变father节点属性
可以的。
Sab - Sba > 0的等价条件应该写错了吧∑bi / m > ∑ai / n吧
已修正~
p s v能不能单词写全啊
写全的话就太长啦,详细讲解在这里:AcWing 115. 给树染色。
### 祝大雪菜老师直播讲课月入百万
# 资瓷