整体二分

Meaning

所谓整体二分,就是把所有询问一起二分,神仙至极,但是常数蛮大的 \(QwQ\)

能够处理支持离线,区间,可二分达到单询问 \(nlog\) 的这类询问。

一般而言复杂度 \(nlog^2\) ,因为修改询问线性查询的缘故,所以一般会带着一个树状数组。

General

一般来讲我们会把每个有权值的位置放到一个队列里,然后所有操作也放在这个队列里,但是放在权值之后。

然后我们二分一个值,将所有这个值下仍能合法的询问丢到右区间处理 (也就是它们的二分区间肯定都变成了 \([mid+1,r]\) ) ,同时将大于等于 \(mid\) 的权值和修改权值丢到右边去,因为它们会对且仅对右区间的询问产生贡献。

左区间同理。

当我们的二分区间 \(l==r\) 时,更新所有二分区间为这个的询问的答案。

大致就是一个函数 \(Solve(L,R,l,r)\) ,代表我们现在处理的是 \([L,R]\) 的操作或权值序列,它们对应的二分区间都是 \([l,r]\) 。每次求出有 \(len1\) 个操作和权值要丢到左区间,\(len2\) 个丢到右区间,那我们就拿出这些操作并重新摆放位置,确定那 \(len1\) 个操作都在 \([L,L+len1-1]\) 这段区间,且按照操作的时间顺序摆放,另外 \(len2\) 个操作同理。

那么我们的递归处理就是 \(Solve(L,L+len1-1,l,mid)\)\(Solve(L+len,R,mid+1,r)\) 了。

Example1 - 静态区间第 \(k\)

题目的话 \(......\) 洛谷的 主席树板子题 就行。

这就是个很经典的整体二分问题了,对于每个区间,我们可以二分一个值 \(mid\) ,如果 \(mid\) 是满足小于 \(mid\) 的值有 \(k\) 个这个条件下最小的值,那么 \(mid\) 就是区间第 \(k\) 小了。

那么我们把每个位置的值先放到操作序列里,然后再按顺序把询问放到后面,就可以开始整体二分了。

对于一个值,我们直接判断是否 \(\le mid\) ,如果是的话,我们就将这个值的位置加上 \(1\) 的贡献,且它将会被丢到左区间,否则丢到右区间,对于一个询问,我们只需要查询询问区间内的贡献和是否 \(\le k\) 即可,如果是的话,就要丢到右区间,判断更大的值能不能行,否则丢到左区间,判断更小的值是否符合。

就是介么简单。

为保证篇幅不过长,只放动态区间第 \(k\) 小的代码。

不懂的话看看代码注释?

Example2 - 动态区间第 \(k\)

题目的话 \(......\) Dynamic Rankings 就是了。

其实和静态区间第 \(k\) 小没什么差别,我们将一个修改操作分解成两个部分放入操作序列即可:第一个操作是删除修改前的权值,第二个操作放入修改后的权值。

那么我们在扫到权值的时候判断是删除还是加入并判断是否 \(\le mid\) 就好了。

Code

#include <cstdio>
#include <iostream>
using namespace std;
int n, m, sum, cnt, a[200001], c[200001], tmp[400001], Ans[200001];
char opt;
struct node {
  int type, x, y, k, tmp, id;
} q[400001], q1[400001], q2[400001];
void add(int k, int x) {
  for (int i = k; i <= n; i += i & -i) c[i] += x;
}
int query(int k) {
  int ans = 0;
  for (int i = k; i > 0; i -= i & -i) ans += c[i];
  return ans;
}
void Solve(int L, int R, int l, int r) {
  if (L > R) return;  //如果没有东西了就返回
  if (l == r) {
    for (int i = L; i <= R; i++)
      if (q[i].type == 3) Ans[q[i].id] = l;
    return;
  }  //如果二分到底了就更新答案
  int mid = (l + r) >> 1;
  for (int i = L; i <= R; i++) {
    if (q[i].type == 1 && q[i].y <= mid) add(q[i].x, 1);
    if (q[i].type == 2 && q[i].y <= mid) add(q[i].x, -1);
    if (q[i].type == 3) tmp[i] = query(q[i].y) - query(q[i].x - 1);
  }  //按时间顺序扫一遍加入、删除操作并记录下每个询问内小于当前二分值的数的数量
  int len1 = 0, len2 = 0;
  for (int i = L; i <= R; i++) {
    if (q[i].type == 1 && q[i].y <= mid) add(q[i].x, -1);
    if (q[i].type == 2 && q[i].y <= mid) add(q[i].x, 1);  //清空树状数组
    if (q[i].type == 3) {
      if (q[i].tmp + tmp[i] >
          q[i].k -
              1)  // q[i].tmp
                  // 表示的是当前询问在上一个二分值下有多少个小于的数,因为我们丢进当前区间的权值已经不包括之前那些数了
        q1[++len1] = q[i];
      else {
        q[i].tmp += tmp[i];
        q2[++len2] = q[i];
      }
    }  //将询问丢到相应区间
    else if (q[i].y <= mid)
      q1[++len1] = q[i];
    else
      q2[++len2] = q[i];  //将权值丢到相应区间
  }
  for (int i = 1; i <= len1; i++) q[L + i - 1] = q1[i];
  for (int i = 1; i <= len2; i++)
    q[L + len1 + i - 1] = q2[i];  //重新整理放置操作序列
  Solve(L, L + len1 - 1, l, mid);
  Solve(L + len1, R, mid + 1, r);  //递归处理
}
int main() {
  freopen("2617.in", "r", stdin);
  freopen("2617.out", "w", stdout);
  cin >> n >> m;
  for (int i = 1; i <= n; i++) {
    scanf("%d", &a[i]);
    sum++;
    q[sum].type = 1, q[sum].x = i, q[sum].y = a[i];
  }  // 第一种操作-加入,存入序列中的值
  for (int i = 1; i <= m; i++) {
    scanf("%s", &opt);
    if (opt == 'Q') {
      q[++sum].id = ++cnt;
      scanf("%d%d%d", &q[sum].x, &q[sum].y, &q[sum].k);
      q[sum].type = 3;
    }  //第三种操作-询问,存入询问区间,询问编号,询问值
    else {
      int A, B;
      scanf("%d%d", &A, &B);
      sum++;
      q[sum].type = 2, q[sum].x = A, q[sum].y = a[A];  //第二种操作,删除
      sum++;
      q[sum].type = 1, q[sum].x = A, q[sum].y = B;
      a[A] = B;
    }
  }
  Solve(1, sum, 0, 2e9);  //整体二分,Start!
  for (int i = 1; i <= cnt; i++) printf("%d\n", Ans[i]);
}
posted @ 2019-04-01 21:54 洛水·锦依卫 阅读(...) 评论(...) 编辑 收藏