[APIO2012] 派遣

题意

在一棵有根树选择某些节点使得在它们的费用和在不超过某个值的条件下选择节点的个数与它们的某个公共祖先的领导力的乘积最大。

解析

即使最开始的时候先想的是贪心,在手玩一下之后我们也可以很自然的想到树形dp。

那么我们考虑怎么将子节点的信息转移到当前节点上。

我们设 \(dp_x\) 表示在以 \(x\) 为根的子树中的最优解。

那么在向上合并的时候,显然有两种情况:

  1. 管理员在当前节点的子节点中

  2. 当前节点是管理员。

设当前节点的子节点的集合为 \(y\)

对于第一种情况我们直接取 \(max_{i \in y}(dp_i)\)

而对于第二种情况,因为领导力是固定的 \(L_x\),我们显然要在以当前节点为根的子树中根据花费的高低从小到大的选择节点,让人数最多,这样才能保证最优。

现在假设我们能在我们可以接受的时间内的求出这个最多的人数,我们就有如下状态转移方程:

\[dp_x = max(people_{max} \times L_x, max_{i \in y}(dp_i)) \]

所以我们现在要解决的问题就是如何快速求出这个 \(people_{max}\)

显然这是一种能够支持快速合并和访问最值的数据结构。

不难想到左偏树和平衡树。

根据我们前面的找 \(people_{max}\) 的策略,我最开始想的是小根堆,但是这样访问下一个元素是 \(log\) 的,而且还要写对顶堆。然后又考虑平衡树,但是合并操作又会使我们的时间大大增加。

所以我们要转换一下思维。

平衡树快速维护这个我在做的时候没有想出来,加上我是来练左偏树的,所以我想的是怎么优化左偏树维护这个的方法。

不难发现上述方法的局限性在于我们在访问下一个元素的时候必须删除堆顶元素,但又不是真正的删除,因为这一部分是我们需要的,所以要写对顶堆,但是这样时间复杂度又假了。

正难则反,我们考虑维护大根堆,记录堆中的和,如果和大于预算,那么我们就删除堆顶元素,那么最后剩下的一定是最多的,这个意思也和我们先前的策略一样。而至于删除的,因为我们在向上合并的之后,在更新最优解的时候,我们的最优解肯定是每棵子树内的最优解构成的集合中再重新选出的最优解。那么显然已经删除的节点,是不可能在向上合并更新答案的过程中被选中的。所以被删除的节点我们就可以不用管了。

因为每个节点至多会被删除一次,左偏树的合并是 \(logn\) 级别的,所以整个维护 \(people_max\) 的过程是 \(nlogn\) 的。

而树形dp的过程则是 \(O(n)\) 级别的(因为边数是 \(n\) 级别的),所以总的时间是 \(O(nlogn)\) 级别的。

不过这道题的左偏树要加并查集保证时间复杂度,不然单次操作会被卡成 \(O(n)\)

代码

#include<cstdio>
#include<iostream>
#include<cctype>

using namespace std;

typedef long long LL;

const int N = 1e5 + 5;

struct Leftist {
	int val[N], dis[N], ls[N], rs[N], rt[N];
	Leftist () { dis[0] = -1; }
	inline void swap(int &x, int &y) { x ^= y ^= x ^= y; }
	int merge(int x, int y) {
		if(!x || !y) return x | y;
		if(val[x] < val[y] || (val[x] == val[y] && x > y)) swap(x, y);
		rs[x] = merge(rs[x], y);
		if(dis[rs[x]] > dis[ls[x]]) swap(rs[x], ls[x]);
		dis[x] = dis[rs[x]] + 1;
		return x;
	}
	inline int pop(int x) { return rt[ls[x]] = rt[rs[x]] = rt[x] = merge(ls[x], rs[x]); }
	
	inline int Find(int x) { return x == rt[x] ? x : rt[x] = Find(rt[x]); }
}tr;

int n, m, rt, a[N], L[N], siz[N]; LL dp[N], sum[N];

int head[N], nex[N << 1], to[N << 1], tot = 1;

inline void add(int u, int v) {
	nex[++tot] = head[u], to[tot] = v, head[u] = tot;
	nex[++tot] = head[v], to[tot] = u, head[v] = tot;
}

void dfs(int x, int f) {
	sum[x] = a[x]; siz[x] = 1;
	for(int i = head[x], y; i; i = nex[i]) {
		y = to[i];
		if(y == f) continue;
		dfs(y, x);
		siz[x] += siz[y], sum[x] += sum[y];
		int f1 = tr.Find(x), f2 = tr.Find(y);
		if(x != y) tr.rt[f1] = tr.rt[f2] = tr.merge(f1, f2);
		dp[x] = max(dp[x], dp[y]);
	}
	int root = tr.rt[tr.Find(x)];
	while(sum[x] > m) sum[x] -= tr.val[root], siz[x]--, root = tr.pop(root);
	dp[x] = max(dp[x], 1LL * siz[x] * L[x]);
}

inline void read(int &x) {
	x = 0; int c = getchar();
	for(; !isdigit(c); c = getchar());
	for(; isdigit(c); c = getchar())
		x = x * 10 + c - 48;
}

int main() {
	read(n), read(m);
	for(int i = 1, f; i <= n; i++) {
		read(f), read(a[i]), read(L[i]);
		if(!f) rt = i; else add(f, i);
	}
	for(int i = 1; i <= n; i++) tr.val[i] = a[i], tr.rt[i] = i;
	dfs(rt, 0);
	printf("%lld\n", dp[rt]);
	return 0;
}
posted @ 2021-09-09 14:22  init-神眷の樱花  阅读(49)  评论(0)    收藏  举报