[HNOI/AHOI2018]排列(贪心)
opening
好像之前模拟赛考过原题,不过这个题的题意是真的看不懂,还有题解区都说省选出原题,原来省选都会出原题的呀,我吐了。
outline
给数组 \(a\)(\(0 <= a_i <= n\)) 和 \(w\),它的一个排列合法当且仅当对于 \(p[j] <= p[k]\),那么不存在 \(a_{p[j]} == p[k]\),定义该排列的价值为 \(\sum_{1}^{n}i*w_{p_i}\),要求是价值最大化。
solution
反正在没有看题解之前我连题意都读不懂,u1s1,这道省选题出的是真的烂啊,不过题中的套路还是不错的。
我们可以把题意转化为对于 \(a[i] = k\),那么在 \(p\) 数组里 \(k\) 要放到 \(i\) 的前面,我们考虑建图,对于这样的情况我们 \(k->i\),即 \(k\) 连向 \(i\)。
图如果是一棵基环树,那么本题无解,否则是一棵以 \(0\) 为根的树,题目转化为给定一棵树,每个节点都有一个价值,要求选一个节点必须先选它的父亲,求最大价值。
我们不难想到经典的套路题,取出当前 \(w\) 最小的点,如果它已经没有父亲了,那么直接计算答案,否则把它和它的父亲合并成一个点,且新点的权值定义为它们权值的平均数,然后重复该过程,得到的便是答案。
对于为啥是权值的平均值,这个是可以推柿子的,因为每次合并相当于两个序列合并,随便推一下柿子就可以得到平均值的结论了。
其实这道题的实现方法还是挺值得研究的,主要是利用堆来实现,并且用并查集来维护联通块和最新的情况,方便即时排除堆中过时的元素,利用0号节点方便直接计算答案。
std
// by longdie
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 5e5 + 5;
ll ans, w[N];
int n, a[N], fa[N], siz[N];
struct node {
int id; ll val, num;
node() {}
node(int a, ll b, ll c) { id = a, val = b, num = c; }
friend bool operator < (const node &a, const node &b) {
return a.val * b.num > b.val * a.num;
}
}; priority_queue<node> q;
inline int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }
inline void merge(int x, int y) { fa[find(x)] = find(y); }
signed main() {
scanf("%d", &n);
for(register int i = 0; i <= n; ++i) fa[i] = i, siz[i] = 1;
for(register int i = 1; i <= n; ++i) {
scanf("%d", &a[i]);
if(find(a[i]) == find(i)) return puts("-1"), 0;
merge(a[i], i);
}
for(register int i = 0; i <= n; ++i) fa[i] = i, siz[i] = 1;
for(register int i = 1; i <= n; ++i)
scanf("%lld", &w[i]), q.push(node(i, w[i], 1));
while(q.size()) {
node t = q.top(); q.pop();
int u = find(t.id), v = find(a[u]);
if(t.num != siz[u]) continue;
ans += w[u] * siz[v];
w[v] += w[u], siz[v] += siz[u];
merge(u, v);
if(v) q.push(node(v, w[v], siz[v]));
}
cout << ans << '\n';
return 0;
}