目录
题目
题目描述
翻译:
Bob对树十分感兴趣。树是一种有向图,其中有一个特殊的节点被称为树的“根”。并且“根”与任意节点之间的可达路径是唯一的。
Bob想要给树的所有节点进行上色。一颗树拥有N个节点,这些节点编号为1,2,....,N。假设Bob为一个节点上色需要1个单元时间,并且Bob上色的操作是逐个进行的,即完成一个节点的上色后,可以为另一个节点上色。除此之外,每一个节点被上色之前,其父节点已被上色。显而易见的,Bob的一次尝试有且只能对“根”节点进行操作。
每一个节点拥有一个“上色成本”。“上色成本”依赖于编号i个节点的权值和其完成的时间。例如编号i的节点权值为Ci并且在Fi时间完成,那么它的上色成为Ci*Fi。
(中间给了一个例子的答案,不翻译了...)
请你给出Bob给树上色的可行最小代价。
输入
翻译:
输入由一系列的测试样例构成。每一组测试样例的第一行由两个数字N和R,其中N代表树的节点个数,同时R代表根节点的编号。这第二行包含N个整数,第i个整数是第i号个节点的权值。接下来N-1行个由两个数组成,它们分别代表两个节点V1和V2。其中V1节点是V2节点的父亲。没有任何一条边被连接两次,并且所有边都被表达。
特别地,有一组样例为N = 0, R = 0.这是结束检测的标志。
输出
翻译:
这就不用翻了吧...就是给出答案,每一组答案占一行。
思路解析
看到 Fi*Ci,我们很容易想到“逐步调整法(微扰技术)”,或者说是排序不等式(逆序积最小)。哦~以上说法只是专业一些,其实人们的直觉已经足够看到这一点了。
但是这样子的“贪心”是错误的。因为我们忽略了限制条件--每一个节点被上色之前,其父节点必然已经被上色。所以,我们很容易知道上述的“贪心”是错误的。当然,你也可以举出一个反例。例如构造一个树使得小权值节点拥有大权值子节点。但是究其本源,这个反例在说明限制条件的作用。
但是这样的想法不是毫无价值的。我们会发现,最大权值的节点,在其父节点被上色后应该立马进行上色。这样才符合我们排序不等式。
至此我们得到一个全新的看法:将最大权值节点和其父节点进行合并。因为操作是连续的。
但是问题来了,合并后的值是什么样的?
假如我们的现在由三个节点需要上色,它们的权值分别为z,x,y。其中x,y的上色是关联的。x上色完,马上对y上色。
那么,我们只有两种情况:
z先上色:代价 = z + 2 * x + 3 * y
x,y先上色: 代价 = x + 2 * y + 3 * z
考虑到我们想知道那个代价更小,换句话说我们只想知道两种情况的大小关系。所以我们可以适当变形,判定大小关系。不妨在两式同 + (z - y)
那么有 2*z + 2*x + 2*y 和 x + y + 4 * z 的大小关系。再移项得到 (x + y) / 2 和 z 的大小关系。
我们忽而想到,合并之后的值与“平均值”的关系。当然我们目前不能排除是 / 2 的关系。
所以我们再来一般化。
假定我们现在又M个节点的上色是关联的,N个节点的上色是关联的。同上推理:
W1 = 先对M个点上色, W2 = 先对N个节点上色
从以上推导,我们正式敲定合并后的权值是 “平均值”。
因为,我们每次都在贪心选取最大均值节点。所以可以使用大顶堆维护。
但是,我们以上的操作只能知道上色次序,所以我们需要记录上色次序,并按照次序求值就能获取答案。这里需要注意树的序列化,这道题目中子序列应当添加再父序列之后,小均值节点序列应该添加在大均值序列之后。这样的添加序列只需要知道链表的头尾即可。这里需要数组模拟链表。
AC代码
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>
#include <queue>
using namespace std;
const int N = 1e3 + 5;
int n, r, fa[N], nxt[N], lst[N], num[N];
double c[N], d[N], tot[N];
bool vis[N];
typedef pair<double, int> PII;
int main() {
while (scanf("%d%d", &n, &r) && n && r) {//多组数据
priority_queue <PII, vector<PII>, less<PII> > q;//大顶堆,默认就是大顶堆
for (int i = 1; i <= n; i++) {//O(n)
scanf("%lf", &c[i]);
nxt[i] = lst[i] = i;
num[i] = 1;
tot[i] = c[i];
if (i != r) q.push({ c[i], i });//将非根节点入队
}
memcpy(d, c, sizeof d);//拷贝一份原数据,最后求知用
for (int i = 1; i < n; i++) {//O(n)
int a, b;
scanf("%d%d", &a, &b);
fa[b] = a;
}
memset(vis, 0, sizeof vis);//每个节点只能上色一次
for (int i = 1; i < n; i++) {
int p;
p = q.top().second; q.pop();
while (vis[p] || p == r) {
p = q.top().second;
q.pop();
}//取出等效权值最大且合法的结点
int f = fa[p];
while (vis[f]) fa[p] = f = fa[f];
//记录操作次序
//小均值节点应该后上色,所以加入到当前序列的最末端,并更新父亲节点的序列末端值
nxt[lst[f]] = p;
lst[f] = lst[p];
num[f] += num[p], tot[f] += tot[p];
c[f] = tot[f] / num[f];//合并
vis[p] = 1;
q.push({ c[f], f });//将更新的结果入队
}
int ans = 0;
for (int i = 1; i <= n; i++) {
ans += i * d[r];
r = nxt[r];
}//计算代价
printf("%d\n", ans);
}
return 0;
}