[解题报告] [九省联考2018]IIIDX(线段树二分)

传送🚪

题意

\(n\) 首歌曲,分别有一个权值 \(d_i\),第 \(i\) 首歌曲的前驱为 \(\lfloor \frac{i}{k} \rfloor\),求一个权值字典序最大的歌曲序列,满足每首歌曲的权值都大于等于它前驱的权值。

\(n \le 5 \times 10^5\)

解法

连写两个假做法,于是三个小时就过去了……论 wz 做题量这么少的原因

显然这 \(n\) 首歌曲会构成一个树结构(把 \(0\) 看做根节点),那么会想到从前往后贪心,发现对于每个点 \(u\),只要我们为它的子树留够 \(size_u\) 个位置,它的子树就一定存在一种合法序列,所以直接把权值从小到大排序,从前往后考虑每个点,对于同一层的点的策略就是能往后取就尽量往后取。

\(d_i\) 互不相同时,这个贪心是对的,但当 \(d_i\) 相同时可能出现错误。

例如对于下面这种情况

n = 3, k = 4
d = {1, 1, 2}

这时的树结构为

0->1
0->2
1->3

sz[1] = 2, sz[2] = 1, sz[3] = 1

我们用 \(a\) 表示最终的权值序列,那么按照刚才的策略,我们会取到 \(a_1 = d_2\),那么 \(a_2\) 就只能取到 \(d_1\) 了(因为要为 \(3\) 预留下位置)。而更优的情况应该是 \(a_1 = d_1, a_2 = d_3, a_3 = d_2\)

也就是说,当一个节点 \(i\) 确定了 \(a_i = d_x\) 后,若存在一个 \(y\) 使得 \(y < x\)\(d_y = d_x\),那么将 \(i\) 的状态改为 \(a_i = d_y\) 不会使得答案更劣(并且有可能使得答案更优)。

那么我们考虑确定了先找到一个尽量大的 \(d_x\) 使得 \(a_i = d_x\),然后再把 \(a_i\) 移动到最小的 \(y\) 使得 \(d_y = d_x\)(并且 \(y\) 之前没有被取过),然后再在 \(y\) 后面 \(size_i\) 位为 \(i\) 的子树预留下位置。

我首先想到的操作是把所有取过的 或 预留的位置都标记为 1,然后在处理点 \(i\) 的时候把它的前驱预留的位置变回 0,已经取掉了的位置依然为 1。用线段树区间覆盖 + 线段树二分貌似可以解决,但问题是每次预留的位置会被先前取过的位置分成若干个块,对每个块需要单独做一次赋值,那么时间复杂度就退化为 \(O(n^2)\)了。

一种可行的方式是对每个位置维护它右边的可行位置数量 \(num_i\),初始时 \(num_i = n - i + 1\),对于节点 \(i\),我们在确定了 \(i\) 的位置 \(y\) 后,就把 \(1 \sim y\)\(num\) 全部减去 \(size_i\),然后下一个点 \(j\) 找位置的时候就找到最大的满足 \(\min_{i = 1}^x num_i \ge size_j\) 的位置 \(x\),用线段树维护最小值 + 线段树二分可以解决。

需要注意的一点是,在处理完一些前驱相同的点时,不能马上把它们为子树预留的位置给去掉,必须要等到处理子节点时再把对应父节点预留的位置去掉(否则有可能导致不满足权值大于等于前驱权值的要求),并且每个节点都只能去除一次。

代码

#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

const int _ = 5e5 + 7;
const int __ = 1e7 + 7;

int n, d[_], pre[_], sz[_], fir[_], bel[_], pos[_];
bool b[_];
double K;

int gi() {
  int x = 0; char c = getchar();
  while (!isdigit(c)) c = getchar();
  while (isdigit(c)) x = (x << 3) + (x << 1) + c - '0', c = getchar();
  return x;
}

namespace SGT {
#define mid ((l + r) >> 1)
#define ls(k) (k << 1)
#define rs(k) (k << 1 | 1)

  int minx[__], tag[__];

  void Build(int k, int l, int r) {
    if (l == r) return (void)(minx[k] = n - l + 1);
    Build(ls(k), l, mid);
    Build(rs(k), mid + 1, r);
    minx[k] = min(minx[ls(k)], minx[rs(k)]);
  }

  void upd(int k, int w) { minx[k] += w, tag[k] += w; }

  void psd(int k) { if (tag[k]) upd(ls(k), tag[k]), upd(rs(k), tag[k]), tag[k] = 0; }

  void Modify(int k, int l, int r, int x, int y, int w) {
    if (l >= x and r <= y) return upd(k, w);
    psd(k);
    if (x <= mid) Modify(ls(k), l, mid, x, y, w);
    if (y > mid) Modify(rs(k), mid + 1, r, x, y, w);
    minx[k] = min(minx[ls(k)], minx[rs(k)]);
  }

  int Find(int k, int l, int r, int w) {
    if (l == r) { assert(minx[k] < w or l == n); return minx[k] < w ? l : n + 1; }
    psd(k);
    return minx[ls(k)] >= w ? Find(rs(k), mid + 1, r, w) : Find(ls(k), l, mid, w);
  }

#undef mid
#undef ls
#undef rs
}

void Init() {
  cin >> n >> K;
  for (int i = 1; i <= n; ++i) d[i] = gi(), pre[i] = floor(i / K), sz[i] = 1;
  sort(d + 1, d + n + 1);
  for (int i = 1; i <= n; ++i) fir[i] = d[i] == d[i - 1] ? fir[i - 1] : i, bel[i] = fir[i];
  for (int i = n; i; --i) sz[pre[i]] += sz[i];
}

void Run() {
  SGT::Build(1, 1, n);
  for (int i = 1, j = i; i <= n; i = j) {
    while (j <= n and pre[j] == pre[i]) ++j;
    for (int k = i; k < j; ++k) {
      if (pre[k] and !b[pre[k]]) SGT::Modify(1, 1, n, 1, pos[pre[k]], sz[pre[k]] - 1), b[pre[k]] = 1;
      int x = SGT::Find(1, 1, n, sz[k]) - 1;
      x = fir[bel[x]], ++fir[bel[x]];
      pos[k] = x;
      SGT::Modify(1, 1, n, 1, x, -sz[k]);
    }
  }
  for (int i = 1; i <= n; ++i) printf("%d ", d[pos[i]]); putchar('\n');
}

int main() {
  Init();
  Run();
  return 0;
}
posted @ 2021-01-19 22:13  BruceW  阅读(152)  评论(0编辑  收藏  举报