JZOJ 6653. 【2020.05.27省选模拟】树(权值线段树)
JZOJ 6653. 【2020.05.27省选模拟】树
题目大意
- 一棵以 1 1 1为根大小为 n n n的树,要求父亲编号小于儿子, 2 n − 2 2n-2 2n−2个数,可任意分配使它们作为树的边权和每个点的父亲编号,求各种分配下 1 1 1到 n n n路径长度分别为 [ 1 , n ) [1,n) [1,n)时路径的最大边权和。
- n ≤ 1 0 5 n\le10^5 n≤105
题解
- 树的形态确定后,最大边权和自然为剩余未选的若干个数之和。
- 统计每个数出现的个数 c i c_i ci,求出前缀和,若存在 s u m i − 1 < i − 1 sum_{i-1}<i-1 sumi−1<i−1则必然无解,因为可选的父亲个数小于儿子个数;若存在 s u m i − 1 = i − 1 sum_{i-1}=i-1 sumi−1=i−1则它 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同时需要最小值和最大值,所以用堆维护也比较麻烦,因此在尝试上花费了较多的时间。
- 维护最大最小值,使用权值线段树虽然时间并非最优,但想法自然,写起来也方便。
哈哈哈哈哈哈哈哈哈哈