JZOJ 6653. 【2020.05.27省选模拟】树(权值线段树)

JZOJ 6653. 【2020.05.27省选模拟】树

题目大意

  • 一棵以 1 1 1为根大小为 n n n的树,要求父亲编号小于儿子, 2 n − 2 2n-2 2n2个数,可任意分配使它们作为树的边权和每个点的父亲编号,求各种分配下 1 1 1 n n n路径长度分别为 [ 1 , n ) [1,n) [1,n)时路径的最大边权和。
  • n ≤ 1 0 5 n\le10^5 n105

题解

  • 树的形态确定后,最大边权和自然为剩余未选的若干个数之和。
  • 统计每个数出现的个数 c i c_i ci,求出前缀和,若存在 s u m i − 1 < i − 1 sum_{i-1}<i-1 sumi1<i1则必然无解,因为可选的父亲个数小于儿子个数;若存在 s u m i − 1 = i − 1 sum_{i-1}=i-1 sumi1=i1则它 i i i必然要作为 1 1 1 n n n路径上的点,不然没有多余的父亲编号让 i i i后某个点连向 i i i前某个点;其他的点都是在或不在路径上皆可。
  • 否则,先考虑从 1 1 1 n n n路径最长的情况,自然可以把所有 c i ≠ 0 c_i\not=0 ci=0作为中间点连接起来,然后剩下的点从前往后选择编号较小的儿子,即可求出当前最大的边权和。
  • 记录下作为答案的数字集合 D D D,以及剩余可选的数字集合 C C C
  • 接着考虑减小路径长度,为了剩出更大的数,则从后往前删去可不在路径上的点 x x x
  • 此时因为边数减少了,则从 D D D中挑出最小的数删去,放入 C C C,重复做两遍这个过程,因为接下来的操作可能可以加入更大的数。
  • x x x和父亲断开了,则把它父亲放入 C C C,再把它连向 C C C中最小的点,
  • 最后找到 C C C中最大的数加入 D D D,并从 D D D中删去。
  • C C C D D D用权值线段树维护。

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 100010
#define ll long long
ll dis[N], F[N];
int n, a[N * 2], c[N], d[N], tp[N], fr[N], q[N];
struct {
	int q[N * 4];
	void add(int v, int l, int r, int x, int c) {
		if(l == r) q[v] += c;
		else {
			int mid = (l + r) / 2;
			if(x <= mid) add(v * 2, l, mid, x, c); else add(v * 2 +1, mid + 1, r, x, c);
			q[v] = q[v * 2] + q[v * 2 + 1];
		}
	}
	int find_mx(int v, int l, int r) {
		if(l == r) return l;
		int mid = (l + r) / 2;
		if(q[v * 2 + 1] > 0) return find_mx(v * 2 + 1, mid + 1, r);
		return find_mx(v * 2, l, mid);
	}
	int find_mi(int v, int l, int r) {
		if(l == r) return l;
		int mid = (l + r) / 2;
		if(q[v * 2] > 0) return find_mi(v * 2, l, mid);
		return find_mi(v * 2 + 1, mid + 1, r);
	}
	void ins(int k, int c) {
		add(1, 1, n, k, c);
	}
	int ans_mx() {
		return find_mx(1, 1, n);
	}
	int ans_mi() {
		return find_mi(1, 1, n);
	}
}di, ci;
int main() {
	int i, j, k;
	scanf("%d", &n);
	for(i = 1; i <= 2 * n - 2; i++) scanf("%d", &a[i]), c[a[i]]++;
	memset(dis, 255, sizeof(dis));
	sort(a + 1, a + n * 2 - 1);
	for(i = 1; i <= n; i++) {
		F[i] = F[i - 1] + c[i];
		if(F[i - 1] < i - 1 || (i < n && F[i - 1] == i - 1 && !c[i])) {
			for(j = 1; j < n; j++) printf("-1 ");
			return 0;
		}
	}
	int sum = 0, s = 0, la = 1;
	for(i = 2; i <= n; i++) {
		if(F[i - 1] == i - 1 || i == n) tp[i] = 1, s++;
		if(c[i] || i == n) fr[i] = la, c[la]--, sum++, la = i;
	}
	j = 1;
	for(i = 2; i < n; i++) if(!fr[i]) {
		while(!c[j]) j++;
		c[j]--, fr[i] = j;
	}
	int t = sum; 
	dis[sum] = 0;
	for(i = n - 1; i; i--) while(c[i] && t) dis[sum] += i, c[i]--, d[i]++, t--;
	for(i = 1; i < n; i++) di.ins(i, d[i]), ci.ins(i, c[i]);
	j = n, k = 1;
	while(--sum >= s) {
		while(tp[fr[j]]) j = fr[j];
		int x = fr[j];
		dis[sum] = dis[sum + 1];
		
		k = di.ans_mi();
		di.ins(k, -1);
		ci.ins(k, 1);
		dis[sum] -= k;
		
		k = di.ans_mi();
		di.ins(k, -1);
		ci.ins(k, 1);
		dis[sum] -= k;
		
		fr[j] = fr[x];
		ci.ins(x, 1);
		
		k = ci.ans_mi();
		ci.ins(k, -1);
		fr[x] = k;
		
		k = ci.ans_mx();
		di.ins(k, 1);
		ci.ins(k, -1);
		dis[sum] += k;
	}
	for(i = 1; i < n; i++) printf("%lld ", dis[i]);
	return 0;
}

自我小结

  • 尽管这题有 O ( n ) O(n) O(n)的做法,但这种解法优化到 O ( n ) O(n) O(n)并不容易,另外因为 C C C同时需要最小值和最大值,所以用堆维护也比较麻烦,因此在尝试上花费了较多的时间。
  • 维护最大最小值,使用权值线段树虽然时间并非最优,但想法自然,写起来也方便。
posted @ 2021-03-02 21:46  AnAn_119  阅读(94)  评论(0编辑  收藏  举报