1. 程式人生 > >bzoj 5249 [2018多省省隊聯測] IIIDX

bzoj 5249 [2018多省省隊聯測] IIIDX

bzoj 5249 [2018多省省隊聯測] IIIDX

Solution

首先想到貪心,直接按照從大到小的順序在後序遍歷上一個個填

但是這樣會有大問題,就是有相同的數的時候,會使答案不優

比如考慮 \((1, 2)(1, 3)(2, 4)\) 這樣一棵樹,並且點權是 \({1,1,1,2}\)

那麼直接貪心會使得答案為 \(v_1=1,v_2=1,v_3=1,v_4=2\),但是實際上最優解為 \(v_1=1,v_2=1,v_3=2,v_4=1\)

問題出在我們先考慮 \(v_2\) 的時候,直接填上了第三個 \(1\),導致他的兒子 \(v_4\) 只有第四個數 \(2\)

一種填法,而事實上最優解裡面 \(v_2\) 對應的是第二個 \(1\)

所以做出修改:將權值按照從大到小的順序排的時候,直接貪心當前填的數為 \(v\) 時,我們選擇最靠右的 \(v\) 填入,也就是填入最小的 \(v\)

那麼怎麼維護這個玩意呢?本來因為所有子樹對應的區間都連續,只需要記錄 \(l\)\(r\) 即可,現在不連續了,我們只能對每個數記錄下它左側(大於等於它的數)有多少個被預定了,那麼考慮當前點的時候這些預定的位置就不能填

所以具體操作如下:

  1. 使用線段樹維護每個位置左側還有幾個數可以使用,區間的值為其左子區間和右子區間權值的 \(\min\),記錄 \(\text{nxt}\)
    陣列表示當前位置距離和他值相等且最靠右的位置的距離,如果當前這個位置就是最靠右的,那麼它的 \(\text{nxt}\) 表示它左側第一個未被選中的和它值相同的位置與它之間的距離
  2. 設當前點為 \(v\),當前點的父親為 \(u\)
  3. 假如 \(v\)\(u\) 的第一個兒子,那麼處理 \(u\) 的時候為 \(u\) 的子樹預定的 \(size_u\) 個位置即將被使用了,我們需要把預定取消,也就是在 \(ans_u\) 及其右側所有數的值加上 \(size_u - 1\)
  4. 線上段樹上二分,找到第一個權值大於等於 \(size_v\) 的數,下面調整位置
  5. 根據上面的做法,我們需要把當前數對應上最右側的未被選中的位置,我們用 ans += nxt[ans]
    \(\text{ans}\) 先放到它最右側的位置上,再使用 ans -= nxt[ans] ,將 \(\text{ans}\) 放到當前的值中最靠右的未被選中的位置上,最後 nxt[ans]++ 表示這個位置及其右側所有和它相等的一段都已經被選了,所以下次再找到這個位置的時候只能走到下一段值上去
  6. 預定它的子樹,就是給 \(ans_v\) 及其右側的所有數的值減去 \(size_v\)

Code

// Copyright lzt
#include<stdio.h>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<vector>
#include<map>
#include<set>
#include<cmath>
#include<iostream>
#include<queue>
#include<string>
#include<ctime>
using namespace std;
typedef long long ll;
typedef std::pair<int, int> pii;
typedef long double ld;
typedef unsigned long long ull;
typedef std::pair<long long, long long> pll;
#define fi first
#define se second
#define pb push_back
#define mp make_pair
#define rep(i, j, k)  for (register int i = (int)(j); i <= (int)(k); i++)
#define rrep(i, j, k) for (register int i = (int)(j); i >= (int)(k); i--)
#define Debug(...) fprintf(stderr, __VA_ARGS__)

inline ll read() {
  ll x = 0, f = 1;
  char ch = getchar();
  while (ch < '0' || ch > '9') {
    if (ch == '-') f = -1;
    ch = getchar();
  }
  while (ch <= '9' && ch >= '0') {
    x = 10 * x + ch - '0';
    ch = getchar();
  }
  return x * f;
}

#define lc (i << 1)
#define rc (i << 1 | 1)
const double eps = 1e-8;
const int maxn = 500500;
struct Node {
  int l, r, val, tag;
} tr[maxn << 2];
int n; double k;
int a[maxn], ans[maxn], fa[maxn], sz[maxn], nxt[maxn];

inline void build(int i, int l, int r) {
  tr[i].l = l; tr[i].r = r;
  if (l == r) {
    tr[i].val = l; tr[i].tag = 0;
    return;
  }
  int md = (l + r) >> 1;
  build(lc, l, md); build(rc, md + 1, r);
  tr[i].val = min(tr[lc].val, tr[rc].val);
}
inline void pushdown(int i) {
  tr[lc].val += tr[i].tag;
  tr[rc].val += tr[i].tag;
  tr[lc].tag += tr[i].tag;
  tr[rc].tag += tr[i].tag;
  tr[i].tag = 0;
}
inline void add(int i, int p, int v) {
  if (p <= tr[i].l) {
    tr[i].tag += v; tr[i].val += v;
    return;
  }
  pushdown(i);
  add(rc, p, v);
  if (tr[lc].r >= p) add(lc, p, v);
  tr[i].val = min(tr[lc].val, tr[rc].val);
}
int ask(int i, int x) {
  if (tr[i].l == tr[i].r) return tr[i].val >= x ? tr[i].l : tr[i].l + 1;
  pushdown(i);
  if (tr[rc].val >= x) return ask(lc, x);
  else return ask(rc, x);
}

void work() {
  scanf("%d %lf", &n, &k);
  build(1, 1, n);
  rep(i, 1, n) a[i] = read();
  sort(a + 1, a + n + 1, greater<int>());
  rrep(i, n - 1, 1) if (a[i] == a[i + 1]) nxt[i] = nxt[i + 1] + 1;
  rrep(i, n, 1) {
    fa[i] = (int)(i / k + eps);
    sz[i]++; sz[fa[i]] += sz[i];
  }
  rep(i, 1, n) {
    if (fa[i] && fa[i] != fa[i - 1]) add(1, ans[fa[i]], sz[fa[i]] - 1);
    ans[i] = ask(1, sz[i]);
    ans[i] += nxt[ans[i]]; ans[i] -= nxt[ans[i]];
    nxt[ans[i]]++;
    add(1, ans[i], -sz[i]);
  }
  rep(i, 1, n) printf("%d ", a[ans[i]]);
}

int main() {
  #ifdef LZT
    freopen("in", "r", stdin);
  #endif

  work();

  #ifdef LZT
    Debug("My Time: %.3lfms\n", (double)clock() / CLOCKS_PER_SEC);
  #endif
}

Review

貪心可以得到 \(60\) 分,正解比較難想到,JS考場上只有yzl過了這道題

關鍵在於發現貪心的錯誤在於沒有選擇最靠右的相同的數,就是條件被加緊了,我們需要放鬆條件

然後用線段樹維護的過程比較自然,\(\text{nxt}\) 陣列很精妙,完成了找到最靠右的未被選中的值得任務