整体二分学习笔记

整体二分

​ 考虑某种奇怪的问题,询问满足单调性,可以二分。但是不同的mid需要不同的状态来判定,暴力做代价过高。这时往往要使用整体二分来一起计算。此时可以带修。

整体二分实质上就是最大化询问的共用判定。若当前询问区间对应的答案为 [L,R],我们取其中点 M=L+R2,然后将数据结构的状态调整到 M 处,然后判断每个询问答案是大于还是小于等于 M。把询问分成两个部分 [L,M](M,R] 后再重复上次操作,直到 L=R。这样整体二分就省去了数据结构在每一次询问中都要改变的数量,操作次数数量级就从 O(NQlogN) 变为 O((N+Q)logN)

例题 1 Dynamic Rankings

题目描述

题目描述

给定一个含有 n 个数的序列 a1,a2an,需要支持两种操作:

  • Q l r k 表示查询下标在区间 [l,r] 中的第 k 小的数
  • C x y 表示将 ax 改为 y

输入格式

第一行两个正整数 n,m,表示序列长度与操作个数。
第二行 n 个整数,表示 a1,a2an
接下来 m 行,每行表示一个操作,都为上述两种中的一个。

输出格式

对于每一次询问,输出一行一个整数表示答案。

样例 1#Input

5 3
3 2 1 4 7
Q 1 4 3
C 2 6
Q 2 5 3

样例 1#Output

3
6

数据范围

1n,m105,1lrn,1krl+1,1xn,0ai,y109

​ 我们把所有的询问和修改时间顺序排列后放到一起去整体二分

​ 先想想如果只有一个询问,那么该怎么二分:显然是二分答案。假设当前二分的是 C,则check是看 [l,r] 中有多少个数 x 满足 xC

​ 现在我们把询问一次二分的思想放在整体二分中。假设当前二分的是 C,然后对应的修改询问序列是 A。我们遍历 A 中的元素 X,如果 X 是修改 p,且此时修改中的数 V 满足 VC,就用数据结构将 p 赋值为 1,表示 p 处的数 x 满足条件 xC。如果 X 是查询,那么就查询当前状态数据结构中 [l,r] 中的和 S,表示 [l,r] 中有 S 个数大于等于 C。如果 Sk,说明当前询问答案小于等于 C;否则答案比 C 大。我们可以用树状数组维护这个过程。然后将操作询问序列分为两半,继续递归下去。这样的时间复杂度就是O(Nlog2N)

参考代码
#include <bits/stdc++.h>
using namespace std;
static constexpr int Maxn = 300005;
static constexpr int inf = 0x3f3f3f3f;
int n, m, qnum;
int a[Maxn];
int ans[Maxn];
bool isQ[Maxn];
struct Query {
  int l, r, k, op, id;
} q[Maxn], q1[Maxn], q2[Maxn];
int bit[Maxn];
inline void add(int x, int v) { for (; x < Maxn; x += x & -x) bit[x] += v; }
inline int ask(int x) { int r = 0; for (; x; x &= x - 1) r += bit[x]; return r; }
void solve(int l, int r, int ql, int qr) {
  if (ql > qr) return ;
  if (l == r) {
    for (int i = ql; i <= qr; ++i)
      if (q[i].op == 2) ans[q[i].id] = l;
    return ;
  }
  int mid = l + r >> 1;
  int cnt1 = 0, cnt2 = 0;
  for (int i = ql; i <= qr; ++i) {
    if (q[i].op == 1) {
      if (q[i].l <= mid) q1[++cnt1] = q[i], add(q[i].id, q[i].r);
      else q2[++cnt2] = q[i];
    }
    else {
      int num = ask(q[i].r) - ask(q[i].l - 1);
      if (q[i].k <= num) q1[++cnt1] = q[i];
      else q[i].k -= num, q2[++cnt2] = q[i];
    }
  }
  for (int i = 1; i <= cnt1; ++i)
    if (q1[i].op == 1) add(q1[i].id, -q1[i].r);
  int cnt = ql;
  for (int i = 1; i <= cnt1; ++i) q[cnt++] = q1[i];
  for (int i = 1; i <= cnt2; ++i) q[cnt++] = q2[i];
  solve(l, mid, ql, ql + cnt1 - 1);
  solve(mid + 1, r, ql + cnt1, qr);
} // solve
int main(void) {
  scanf("%d%d", &n, &m); qnum = 0;
  for (int i = 1; i <= n; ++i) {
    scanf("%d", a + i);
    q[++qnum] = (Query){a[i], 1, 0, 1, i};
  }
  for (int i = 1; i <= m; ++i) {
    char op; int l, r, k;
    scanf("\n%c %d%d", &op, &l, &r);
    isQ[i] = (op == 'Q');
    if (isQ[i]) scanf("%d", &k);
    if (!isQ[i]) {
      q[++qnum] = (Query){a[l], -1, 0, 1, l};
      a[l] = r;
      q[++qnum] = (Query){a[l], 1, 0, 1, l};
    }
    else
      q[++qnum] = (Query){l, r, k, 2, i};
  }
  solve(-inf, inf, 1, qnum);
  for (int i = 1; i <= m; ++i)
    if (isQ[i]) printf("%d\n", ans[i]);
  return 0;
} // main

总结整体二分解决操作问题,操作询问序列具有时间顺序,不能随意改变内部顺序

例题 2 神仙的膜法

题目描述

题目背景

ycx是一个众所周知的神仙。他特别喜欢打怪兽。

题目描述

一个月黑风高的夜晚,ycxhsc在河边上散步。突然间,ycx发现河边上长着一个奇奇怪怪的树。

这珂树有N个节点,第i个节点上住着一个编号为i怪兽,这个怪兽有hi滴血。

对于一珂长满怪兽的树,ycx会两种魔法:

  1. 对这珂树的一条链上的所有怪兽打出v的血量

  2. 对这珂树的一珂子树中的所有怪兽打出v的血量

其中v在每一次魔法中不一定相同。

然而,ycx的魔法会随着周围的环境即时间的变化而变化,这也就是说,ycx并不能随心所欲为所欲为地使用滥用魔法了。不过呢,ycx拥有预测能力,即他珂以知道时刻i时,环境是什么样的,和他的魔法会变成什么样。

现在,ycx把他在接下来M秒内所要使用的所有M个魔法跟hsc说了,要hsc算出每一个怪兽会在接下来的几秒内死亡,或者它们能坚强地活下来。然而,hsc并不会数数,所以她向你寻求帮忙。

输入格式

N M
h_1 h_2 ... h_n
magic_1
magic_2
...
magic_Q

其中magic_i表示ycx使用的魔法,是以下两种形式中的一个:

1 u v x:表示ycx使用第一种魔法,对所有在uv这条链上的怪兽打出x的血量。

2 u x:表示ycx使用第二种魔法,对所有在u子树内的怪物打出x的血量。

输出格式

对于每一个怪兽,输出一行:若它还能再ycx的魔法中存活下来,输出alive;否则输出一个数y,表示在ycx打出前y1个魔法后,这个怪兽还活着,而打出第y个魔法后,这个怪兽就死了。

样例 1#Input

5 4
1 3 4 7 8
1 2
1 3
2 4
2 5
1 4 5 1
1 4 5 4
1 3 4 2
1 5 3 2

样例 1#Output

3
2
4
3
alive

数据范围

对于所有数据,满足:

1N,M3×105,1hi109

对于第i个魔法:

若是第一类,则满足1ui,viN,1x109

若是第二类,则满足1uiN,1x109.

题后一言

附赠ycx的魔法口诀(39子真言):
释迦牟尼 脚绽莲花 菩提达摩 你真伟大 天上天下 唯我独尊 如来佛祖 太上老君 耶稣耶稣快显灵

​ 看到要求每个怪物最早在什么时候死掉,我们想到可以使用整体二分

整体二分了之后,问题就转化为:树链加,子树加,单点查。显然可以树剖+BIT,不过复杂度是 O(Nlog3N),过不了 3×105。冷静一下,发现:所有的查询都在修改后,即这是个静态问题。然后我们就可以树上差分了吧。。。不行,整体二分每一层树上差分的复杂度是 O(N),不是 O(rl+1),然后就会被***钻的数据卡掉。于是我们的思路是树剖,然后用类似于虚树的思想,我们保留会被用到的数组的下标,对这些下标进行离散化。于是,整体二分每一层的复杂度就是 O((rl+1)logN),就能解决了。

​ 按上面思路写完后交一发上去,发现:我怎么TLE了?两个 log 还跑不过 3×105?其实是在离散化时,此时存在数组里的有用的下标个数最多是 O(NlogN) 级别,如果用快速排序(特别是C中的qsort),复杂度就会被卡成 O(Nlog2N),加上整体二分自带的 log 之后,甚至比树剖+BIT的 log3 还慢。我们考虑到下标不会超过 N,可以用桶排。但桶排的复杂度又是 O(N),出现了和之前一样的问题。所以类似"根号分治",可以设一个临界值 CNlogN,当数组元素个数大于 C 时就用桶排,因为此时桶排的复杂度是严格小于等于快排的复杂度;否则就用快排。于是这样子的复杂度就是严格 O(Nlog2N)

参考代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef long long int64_t;
int _swap_tmp;
#define swap(x, y) ({_swap_tmp = x; x = y; y = _swap_tmp; 0;})
#define Maxn 300005
#define Maxm 300005
#define BUC_SORT_SIZE 2200
#define _I_Buffer_Size (20 << 20)
static char _I_Buffer[_I_Buffer_Size];
char *_I_pos = _I_Buffer;
__attribute__((__always_inline__))__inline int read(void) {
  while (*_I_pos < 48) _I_pos++;
  int n = *_I_pos++ - '0';
  while (*_I_pos > 47) n = n * 10 + (*_I_pos++ - '0');
  return n;
} // read
int n, m;
int64_t a[Maxn];
int ans[Maxn];
struct Edge { int to, nxt; } e[Maxn << 1];
int head[Maxn], e_tot;
void clear_graph() { memset(head, 0, sizeof(head)); e_tot = 0; }
void add_edge(int u, int v) {
  e[++e_tot] = (struct Edge){ .to = v, .nxt = head[u] };
  head[u] = e_tot;
} // add_edge
int dep[Maxn], sz[Maxn], son[Maxn], par[Maxn];
int top[Maxn], dfn[Maxn], ind[Maxn];
void dfs1(int u, int fa, int depth) {
  sz[u] = 1, son[u] = -1;
  dep[u] = depth, par[u] = fa;
  for (int i = head[u], v; i; i = e[i].nxt) {
    if ((v = e[i].to) == fa) continue;
    dfs1(v, u, depth + 1); sz[u] += sz[v];
    if (son[u] == -1 || sz[v] > sz[son[u]]) son[u] = v;
  }
} // dfs1
void dfs2(int u, int topv) {
  static int index = 0;
  top[u] = topv; ind[index] = u; dfn[u] = index++;
  if (son[u] != -1) dfs2(son[u], topv);
  for (int i = head[u], v; i; i = e[i].nxt)
    if ((v = e[i].to) != par[u] && v != son[u]) dfs2(v, v);
} // dfs2
#define CHAIN 1
#define SUBTREE 2
#define OP_TYPE int
struct operation {
  OP_TYPE type;
  int u, v; int64_t val;
} b[Maxm];
int qry[Maxn];
int ids[Maxn * 20], ids_sz, gid[Maxn];
int64_t dif[Maxn];
__attribute__((__always_inline__)) __inline void add_range(int l, int r, int64_t val) {
  dif[gid[l]] += val, dif[gid[r + 1]] -= val;
} // add_range
void join_chain(int u, int v) {
  while (top[u] != top[v]) {
    if (dep[top[u]] > dep[top[v]]) swap(u, v);
    ids[ids_sz++] = dfn[top[v]];
    ids[ids_sz++] = dfn[v] + 1;
    v = par[top[v]];
  }
  if (dep[u] > dep[v]) swap(u, v);
  ids[ids_sz++] = dfn[u];
  ids[ids_sz++] = dfn[v] + 1;
} // join_chain
void add_chain(int u, int v, int64_t val) {
  while (top[u] != top[v]) {
    if (dep[top[u]] > dep[top[v]]) swap(u, v);
    add_range(dfn[top[v]], dfn[v], val);
    v = par[top[v]];
  }
  if (dep[u] > dep[v]) swap(u, v);
  add_range(dfn[u], dfn[v], val);
} // add_chain
__attribute__((__always_inline__)) __inline void join_subtree(int u) {
  ids[ids_sz++] = dfn[u]; ids[ids_sz++] = dfn[u] + sz[u];
} // join_subtree
__attribute__((__always_inline__)) __inline void add_subtree(int u, int64_t val) {
  add_range(dfn[u], dfn[u] + sz[u] - 1, val);
} // add_subtree
int cmp_int(const void *x, const void *y) { return *(int*)x - *(int*)y; }
__attribute__((__always_inline__)) __inline int bs_getid(int id) {
  int low = 0, high = ids_sz, ans = 0;
  while (low <= high) {
    int mid = (low + high) >> 1;
    if (ids[mid] <= id) low = mid + 1, ans = mid;
    else high = mid - 1;
  }
  return ans;
} // bs_getid
void divide(int l, int r, int ql, int qr) {
  if (l == r) {
    for (int i = ql; i <= qr; ++i) ans[qry[i]] = l;
    return ;
  }
  int mid = (l + r) >> 1;
  static int qryl[Maxn], qryr[Maxn];
  int sz_qryl = 0, sz_qryr = 0;
  ids_sz = 0; ids[ids_sz++] = 0;
  for (int i = l; i <= mid; ++i) {
    if (b[i].type == CHAIN) join_chain(b[i].u, b[i].v);
    else join_subtree(b[i].u);
  }
  if (ids_sz <= BUC_SORT_SIZE)
    qsort(ids, ids_sz, sizeof(int), cmp_int);
  else {
    static int bucket[Maxn] = { };
    for (int i = 0; i < ids_sz; ++i) bucket[ids[i]]++;
    ids_sz = 0;
    for (int i = 0; i <= n; ++i)
      for (; bucket[i]; --bucket[i]) ids[ids_sz++] = i;
  }
  int new_sz = 1;
  for (int i = 1; i < ids_sz; ++i)
    if (ids[i] != ids[i - 1]) ids[new_sz++] = ids[i];
  ids_sz = new_sz;
  for (int i = 0; i < ids_sz; ++i) gid[ids[i]] = i;
  for (int i = l; i <= mid; ++i) {
    if (b[i].type == CHAIN) add_chain(b[i].u, b[i].v, b[i].val);
    else add_subtree(b[i].u, b[i].val);
  }
  for (int i = 1; i < ids_sz; ++i) dif[i] += dif[i - 1];
  for (int i = ql; i <= qr; ++i) {
    int id = qry[i];
    int64_t sum = dif[bs_getid(dfn[id])];
    if (sum >= a[id]) qryl[sz_qryl++] = id;
    else qryr[sz_qryr++] = id, a[id] -= sum;
  }
  memset(dif, 0, ids_sz * sizeof *dif);
  int qry_start = ql;
  for (int i = 0; i < sz_qryl; ++i) qry[qry_start++] = qryl[i];
  for (int i = 0; i < sz_qryr; ++i) qry[qry_start++] = qryr[i];
  int div = ql + sz_qryl;
  divide(l, mid, ql, div - 1);
  divide(mid + 1, r, div, qr);
} // divide
int main(int argc, const char *argv[]) {
  fread(_I_Buffer, 1, _I_Buffer_Size, stdin);
  clear_graph();
  n = read(), m = read();
  for (int i = 0; i < n; ++i) a[i] = read();
  for (int i = 0; i < n - 1; ++i) {
    int u = read() - 1, v = read() - 1;
    add_edge(u, v); add_edge(v, u);
  }
  for (int i = 0; i < m; ++i) {
    int op = read(), u = read() - 1, v;
    int64_t val;
    if (op == 1) {
      v = read() - 1; val = read();
      b[i] = (struct operation) { .type = CHAIN, .u = u, .v = v, .val = val };
    }
    else {
      val = read();
      b[i] = (struct operation) { .type = SUBTREE, .u = u, .v = -1, .val = val };
    }
  }
  dfs1(0, -1, 0);
  dfs2(0, 0);
  for (int i = 0; i < n; ++i) qry[i] = i;
  divide(0, m, 0, n - 1);
  for (int i = 0; i < n; ++i) {
    if (ans[i] == m) puts("alive");
    else printf("%d\n", ans[i] + 1);
  }
  exit(EXIT_SUCCESS);
} // main

总结 整体二分每一层复杂度不能和 N 有关

例题 3 WD与地图

题目描述

题目背景

WD 整日沉浸在地图中,无法自拔……

题目描述

CX 让 WD 研究的地图可以看做是 n 个点,m 条边的有向图,由于政府正在尝试优化人民生活,他们会废弃一些无用的道路来把省下的钱用于经济建设。

城市都有各自的发达程度 si。为了方便管理,政府将整个地图划分为一些地区,两个点 u,v 在一个地区当且仅当 u,v 可以互相到达。政府希望知道一些时刻某个地区的前 k 发达城市的发达程度总和,以此推断建设的情况。

也就是说,共有三个操作:

1 a b 表示政府废弃了从 a 连向 b 的边,保证这条边存在。

2 a b 表示政府把钱用于建设城市 a,使其发达程度增加 b

3 a b 表示政府希望知道 a 城市所在地区发达程度前 b 大城市的发达程度之和。如果地区中的城市不足 b 个输出该地区所有城市的发达程度总和。

输入格式

第一行两个数 n,m,q,表示共 n 个点,m 条边,q 次询问。

第二行 n 个正整数,表示 si,即每个城市的发达程度。

接下来 m 行每行两个数 u,v,表示初始时有一条从 u 连向 v 的边。

接下来 q 行,表示 q 组询问,格式如题目描述。

输出格式

对于每个询问操作,输出一个数,表示发达程度之和。

样例 1#Input

5 8 8
4 2 1 1 3
2 5
4 2
5 3
1 3
4 5
5 1
1 5
1 4
3 3 1
1 4 5
3 3 3
3 4 1
3 1 5
3 2 4
1 5 3
2 3 4

样例 1#Output

1
1
4
10
10

数据范围

subtask 1 (19 pts): n105,m2×105,q2×105,删除操作个数×m106;

subtask 2 (39 pts): n5×103,m8×103,q2×105;

subtask 3 (42 pts): n105,m2×105,q2×105.

保证任何时刻发达程度 109,无重边(反向边不算重边)无自环。

​ 首先套路性地将删边变为加边,于是问题就变成了:动态加边,改变一个点权值大小,和那个奇怪的查询。

​ 先考虑如果是无向图怎么做。无向图很显然加边就是把两个连通块合并,那么可以对于每一个连通块维护一个动态开点线段树。线段树下标即为权值,查询时就在线段树上二分,加边时就把两个连通块所代表的线段树合并一下。于是这样可以做到 O(NlogN)

​ 然后考虑有向图。我们发现无向图的好处就是每加一条边,都可以把两个连通块合并;而有向图上就不行了。所以我们现在要求出:对于每条边,什么时候可以把这条边所连接的两个端点所在的强连通块合并起来。求出了这个之后就可以像无向图一样直接线段树合并了。

​ 那么问题时怎么找出这些时间点呢?这里就可以用到整体二分的技巧。不难发现,每条边的存在是有时间的。先考虑暴力对于每一条边都二分,那么就是二分时间 t ,把所有 t 时间内就存在的边加入到当前的图里,然后跑一边tarjan,看这条边所连接的两个端点是否在同一个强连通块中。如果在,说明答案在 t 之前;否则在 t 之后。然后发现,对于不同的时间 t 所对应的图是不同的;但对于不同的边且相同的 t,此时的图是相同的。所以我们可以用整体二分的技巧来优化暴力二分。注意在整体二分过程中,我们无法承受每次都加入出现时间在 [0,t] 内的边;于是可以考虑利用上一次递归的结果,使用可撤销并查集来维护当前区间tarjan缩点的结果,这样总时间复杂度就是 O(Nlog2N)O(NlogNα(N))

参考代码
#include <bits/stdc++.h>
using namespace std;
static constexpr int Maxn = 1e5 + 5;
static constexpr int Maxm = 2e5 + 5;
static constexpr int Maxq = 2e5 + 5;
static mt19937 __gen(std::chrono::steady_clock::now().time_since_epoch().count());
#define int int64_t
int n, m, q;
int val[Maxn];
struct Query {
  int op, a, b;
  Query() { }
  ~Query() = default;
  Query(const Query &__other) = default;
  Query(int op, int a, int b) : op(op), a(a), b(b) { }
};
Query qry[Maxq];
struct Edge {
  int u, v, t;
  Edge() { }
  ~Edge() = default;
  Edge(const Edge &__other) = default;
  Edge(int u, int v, int t) : u(u), v(v), t(t) { }
};
vector<Edge> edges, tedge[Maxq];
int fa[Maxn];
int fnd(int u) { return fa[u] == u ? u : fa[u] = fnd(fa[u]); }
void dsu_merge(int u, int v) {
  u = fnd(u), v = fnd(v);
  if (u != v) fa[u] = v;
} // dsu_merge
struct di_edge { int to, nxt; } de[Maxm << 1];
int head[Maxn], tot;
inline void add_edge(int u, int v) {
  de[++tot] = (di_edge){v, head[u]}; head[u] = tot;
} // add_edge
int dfn_time;
int dfn[Maxn], low[Maxn];
stack<int> stk;
bool instk[Maxn];
void tarjan(int u) {
  dfn[u] = low[u] = ++dfn_time;
  instk[u] = true;
  stk.push(u);
  for (int ei = head[u]; ei; ei = de[ei].nxt) {
    int v = de[ei].to;
    if (!dfn[v]) tarjan(v), low[u] = min(low[u], low[v]);
    else if (instk[v]) low[u] = min(low[u], dfn[v]);
  }
  if (dfn[u] == low[u]) {
    do {
      int v = stk.top(); stk.pop(); instk[v] = false;
      dsu_merge(u, v);
      if (v == u) break;
    } while (true);
  }
} // tarjan
void divide(int l, int r, const vector<Edge> &e) {
  if (e.empty()) return ;
  if (l == r) {
    for (const auto &x: e) tedge[l].push_back(x);
    return ;
  }
  if (int(e.size()) == 1) {
    int time = q + 1;
    const auto &[u, v, t] = e.front();
    if (fnd(u) == fnd(v)) time = t;
    tedge[time].push_back(e.front());
    return ;
  }
  int mid = (l + r) >> 1;
  vector<Edge> e_l, e_r;
  vector<pair<int, int>> e_edges;
  vector<int> e_nodes;
  for (const auto &E: e) {
    if (E.t <= mid) {
      int u = fnd(E.u), v = fnd(E.v);
      e_edges.push_back({u, v});
      head[u] = head[v] = 0;
      e_nodes.push_back(u);
      e_nodes.push_back(v);
    }
  }
  tot = 0;
  for (const auto &E: e_edges) {
    int u = E.first, v = E.second;
    add_edge(u, v);
    dfn[u] = low[u] = 0;
    dfn[v] = low[v] = 0;
    instk[u] = instk[v] = false;
  }
  dfn_time = 0;
  while (!stk.empty()) stk.pop();
  for (const int &u: e_nodes) if (!dfn[u]) tarjan(u);
  int now_id = 0;
  vector<pair<int, int>> cpar;
  for (const auto &E: e) {
    if (E.t <= mid) {
      int u = e_nodes[now_id++];
      int v = e_nodes[now_id++];
      if (fnd(u) == fnd(v)) e_l.push_back(E);
      else e_r.push_back(E);
      cpar.push_back({u, fa[u]});
      cpar.push_back({v, fa[v]});
    } else {
      e_r.push_back(E);
    }
  }
  for (const int &u: e_nodes) fa[u] = u;
  divide(l, mid, e_l);
  for (auto [u, fau]: cpar) fa[u] = fau;
  divide(mid + 1, r, e_r);
} // divide
namespace sgt {
  static constexpr int Maxs = ::Maxm * 50;
  int tot, N, ls[Maxs], rs[Maxs];
  int cnt[Maxs];
  long long sum[Maxs];
  inline int newnode(void) { return ++tot; }
  void init(int n) {
    tot = 0, N = n;
    ls[0] = rs[0] = 0;
    cnt[0] = sum[0] = 0;
  } // init
  void update(int &p, int pos, int val, int l = 1, int r = N) {
    if (!p) p = newnode();
    cnt[p] += val, sum[p] += pos * val;
    if (l == r) return ;
    int mid = (l + r) >> 1;
    if (pos <= mid) update(ls[p], pos, val, l, mid);
    else update(rs[p], pos, val, mid + 1, r);
  } // update
  long long query(int p, int k, int l = 1, int r = N) {
    if (cnt[p] <= k) return sum[p];
    if (l == r) return sum[p] / cnt[p] * k;
    int mid = (l + r) >> 1;
    if (cnt[rs[p]] >= k) return query(rs[p], k, mid + 1, r);
    else return sum[rs[p]] + query(ls[p], k - cnt[rs[p]], l, mid);
  } // query
  int merge(int u, int v) {
    if (!u || !v) return u | v;
    cnt[u] += cnt[v], sum[u] += sum[v];
    ls[u] = merge(ls[u], ls[v]);
    rs[u] = merge(rs[u], rs[v]);
    return u;
  } // merge
} // namespace sgt
int root[Maxn];
int32_t main(void) {
  ios_base::sync_with_stdio(false);
  cin.tie(nullptr), cout.tie(nullptr);
  cout << fixed << setprecision(12);
  cin >> n >> m >> q;
  for (int i = 1; i <= n; ++i) cin >> val[i];
  set<pair<int, int>> _edge;
  for (int i = 1; i <= m; ++i) {
    int u, v; cin >> u >> v;
    _edge.insert({u, v});
  }
  for (int i = q; i >= 1; --i) {
    cin >> qry[i].op >> qry[i].a >> qry[i].b;
    if (qry[i].op == 1) edges.push_back(Edge(qry[i].a, qry[i].b, i)), _edge.erase({qry[i].a, qry[i].b});
    if (qry[i].op == 2) val[qry[i].a] += qry[i].b;
  }
  for (auto [u, v]: _edge) edges.push_back(Edge(u, v, 0));
  _edge.clear();
  assert((int)edges.size() == m);
  iota(fa + 1, fa + n + 1, 1);
  divide(0, q + 1, edges);
  iota(fa + 1, fa + n + 1, 1);
  int maxv = *max_element(val + 1, val + n + 1);
  sgt::init(maxv);
  for (int i = 1; i <= n; ++i) {
    root[i] = sgt::newnode();
    sgt::update(root[i], val[i], 1);
  }
  vector<int> ans;
  for (int i = 0; i <= q; ++i) {
    if (!i || qry[i].op == 1) {
      for (auto [u, v, t]: tedge[i]) {
        u = fnd(u), v = fnd(v);
        if (u == v) continue;
        fa[v] = u;
        root[u] = sgt::merge(root[u], root[v]);
      }
    } else if (qry[i].op == 2) {
      int u = qry[i].a;
      int &w = val[u], rt = fnd(u);
      sgt::update(root[rt], w, -1);
      w -= qry[i].b;
      sgt::update(root[rt], w, 1);
    } else {
      int u = qry[i].a, k = qry[i].b;
      int rt = fnd(u);
      int res = sgt::query(root[rt], k);
      ans.push_back(res);
    }
  }
  for (; !ans.empty(); ans.pop_back())
    cout << ans.back() << endl;
  exit(EXIT_SUCCESS);
} // main

总结 从例题2,3可以看出,整体二分可以用来解决一些存在时间类型的问题。

例题 4 Yet Another Convolution

题目描述

You are given an integer array a1,,an and an integer array b1,,bn.

You have to calculate the array c1,,cn defined as follows: ck=maxgcd(i,j)=k|aibj|.

Input

The first line of input contains a single integer n (1n105).

The second line of input contains n integers a1,,an (1ai109).

The third line of input contains n integers b1,,bn (1bi109).

Output

Output n integers c1,,cn.

Example 1#Input

8
1 2 3 4 5 6 7 8
8 7 6 5 4 3 2 1

Example 1#Output

7 5 3 3 1 3 5 7

​ 一眼没有明显的做法,我们尝试着先化简式子。首先可以把绝对值拆掉,拆成两块分别算。

​ 然后每一块的式子是这样的:

ck=maxgcd(i,j)=k{ai+bj}=maxk|i,in{ai+maxgcd(i,j)=kbj}=maxk|i,in{ai+maxk|j,jnbj[gcd(i,j)=k]}=max1ink{aik+max1jnkbjk[gcd(i,j)=1]}

​ 我们枚举 k,每个 k 中询问是 m=nkmax1imAi[gcd(i,T)=1],其中 Ai=bik,1Tm。我们发现 max 根本不好用数论方式化简。

​ 于是我们考虑整体二分。用一次整体二分,将 max 变为 。这样式子就变为了 1imBi[gcd(i,T)=1] 其中 Bi=[CAi]C 是每一次整体二分的数值。这个式子我们用一下 gcd 反演之后就变为了:1imBi[gcd(i,T)=1]=1imBid|i,d|Tμ(d)=d|Tμ(d)d|i,imBi。我们现在要算的就是 d|i,imBi。这玩意儿显然可以记忆化后暴力。于是,整体二分里面的复杂度就是 O(mlog2m)

​ 所以,总的时间复杂度是 O(1knnklog2nk)=O(nlog3n),常数较小,可以通过。

参考代码
#include <bits/stdc++.h>
using namespace std;
static constexpr int Maxn = 1e5 + 5;
namespace Solve {
  int N, T, mu[Maxn], prime[Maxn], sz;
  bool notprime[Maxn];
  int ans[Maxn], arr[Maxn], arrk[Maxn];
  int lens[Maxn], facs[Maxn][200];
  int storage[Maxn];
  void init(int n = Maxn - 1) {
    memset(storage, -1, sizeof(storage));
    memset(lens, 0, sizeof(lens));
    for (int i = 1; i <= n; ++i)
      for (int j = 1; j * j <= i; ++j) {
        if (i % j == 0) {
          facs[i][++lens[i]] = j;
          if (j * j != i) facs[i][++lens[i]] = i / j;
        }
      }
    memset(notprime, false, sizeof(notprime));
    notprime[0] = notprime[1] = true;
    memset(mu, 0, sizeof(mu));
    mu[1] = 1;
    for (int i = 2; i <= n; ++i) {
      if (!notprime[i]) prime[++sz] = i, mu[i] = -1;
      for (int j = 1; j <= sz && i * prime[j] <= n; ++j) {
        notprime[i * prime[j]] = true;
        if (i % prime[j] == 0) break;
        mu[i * prime[j]] = -mu[i];
      }
    }
  } // Solve::init
  void divide(int l, int r, int vl, int vr) {
    if (l > r) return ;
    if (vl == vr) {
      for (int i = l; i <= r; ++i) ans[arrk[i]] = vl;
      return ;
    }
    int vm = (1LL * vl + 1LL * vr + 1) >> 1;
    static int arrlk[Maxn], arrrk[Maxn];
    int cnt1 = 0, cnt2 = 0, _c = l - 1;
    int i, j, T, _, sum, d;
    for (i = l; i <= r; ++i) {
      for (T = arrk[i], sum = 0, _ = 1; _ <= lens[T]; ++_) {
        if (storage[d = facs[T][_]] == -1) {
          int &res = storage[d]; res = 0;
          for (j = d; j <= N; j += d) res += (vm <= arr[j]);
        }
        sum += storage[d] * mu[d];
      }
      ((sum == 0) ? arrlk[++cnt1] : arrrk[++cnt2]) = T;
    }
    for (i = l; i <= r; ++i)
      for (T = arrk[i], _ = 1; _ <= lens[T]; ++_)
        storage[facs[T][_]] = -1;
    for (int i = 1; i <= cnt1; ++i) arrk[++_c] = arrlk[i];
    for (int i = 1; i <= cnt2; ++i) arrk[++_c] = arrrk[i];
    divide(l, l + cnt1 - 1, vl, vm - 1);
    divide(l + cnt1, r, vm, vr);
  } // Solve::divide
  void Main(int n, int a[], int b[], int c[]) {
    for (int k = 1; k <= n; ++k) {
      c[k] = -2e9; N = (int)(n / k);
      static int vals[Maxn] = { };
      int ll = vals[0] = 0;
      for (int i = 1; i <= N; ++i)
        vals[++ll] = arr[i] = b[i * k], arrk[i] = i;
      sort(vals + 1, vals + ll + 1);
      ll = unique(vals + 1, vals + ll + 1) - vals - 1;
      for (int i = 1; i <= N; ++i)
        arr[i] = lower_bound(vals + 1, vals + ll + 1, arr[i]) - vals;
      divide(1, N, 1, N);
      for (int i = 1; i <= N; ++i)
        c[k] = max(c[k], a[i * k] + vals[ans[i]]);
    }
  } // Solve::main
} // namespace Solve
int main(void) {
  int n; cin >> n;
  Solve::init(n);
  static int a[Maxn] = { }, b[Maxn] = { };
  for (int i = 1; i <= n; ++i) cin >> a[i];
  for (int i = 1; i <= n; ++i) cin >> b[i];
  static int c1[Maxn] = { }, c2[Maxn] = { };
  for (int i = 1; i <= n; ++i) a[i] = -a[i];
  Solve::Main(n, a, b, c1);
  for (int i = 1; i <= n; ++i) a[i] = -a[i];
  for (int i = 1; i <= n; ++i) b[i] = -b[i];
  Solve::Main(n, a, b, c2);
  for (int i = 1; i <= n; ++i) b[i] = -b[i];
  for (int i = 1; i <= n; ++i)
    printf("%d%c", max(c1[i], c2[i]), " \n"[i == n]);
  exit(EXIT_SUCCESS);
} // main

总结 整体二分可以将多次询问无法解决的 minmax 式变为 ,然后推式子优化解法。

例题 5 [FJOI2015]火星商店问题

题目描述

题目描述

火星上的一条商业街里按照商店的编号 1n ,依次排列着 n 个商店。商店里出售的琳琅满目的商品中,每种商品都用一个非负整数 val 来标价。每个商店每天都有可能进一些新商品,其标价可能与已有商品相同。

火星人在这条商业街购物时,通常会逛这条商业街某一段路上的所有商店,譬如说商店编号在区间 [l,r] 中的商店,从中挑选一件自己最喜欢的商品。每个火星人对商品的喜好标准各不相同。

通常每个火星人都有一个自己的喜好密码 x。对每种标价为 val 的商品,喜好密码为 x 的火星人对这种商品的喜好程度与 val 异或 x 的值成正比。也就是说,val xor x 的值越大,他就越喜欢该商品。

每个火星人的购物卡在所有商店中只能购买最近 d 天内(含当天)进货的商品。另外,每个商店都有一种特殊商品不受进货日期限制,每位火星人在任何时刻都可以选择该特殊商品。每个商店中每种商品都能保证供应,不存在商品缺货的问题。

对于给定的按时间顺序排列的事件,计算每个购物的火星人的在本次购物活动中最喜欢的商品,即输出 val xor x 的最大值。这里所说的按时间顺序排列的事件是指以下两种事件:

0 s v,表示编号为 s 的商店在当日新进一种标价为 v 的商品。

1 l r x d,表示一位火星人当日在编号在 [l,r] 的商店购买 d 天内的商品,该火星人的喜好密码为 x

输入格式

第一行两个正整数 n,m,分别表示商店总数和事件总数。

第二行中有 n 个整数,第 i 个整数表示商店 i 的特殊商品标价。

接下来的 m 行,每行表示一个事件。每天的事件按照先事件 0,后事件 1 的顺序排列。

输出格式

对于每个事件 1,输出一行一个整数表示答案。

样例 1#Input

4 6
1 2 3 4
1 1 4 1 0
0 1 4
0 1 3
1 1 1 1 0
1 1 1 1 1
1 1 2 1 2

样例 1#Output

5
0
2
5

数据范围

对于 100% 的数据,所有输入的整数在 [0,105] 范围内。

​ 考虑到询问是给定一定范围内的集合 S 与一个整数 x,求 maxwSwx。我们可以考虑用trie树做。但是trie树的构建需要把所有元素都放到一个trie树里面再查询,朴素的实现肯定是不行的。

​ 注意到trie树中的查询其实是一个二分的过程:对于一个 x,假设现在从高往低算到了第 i 位,则这一层二分的值的后 i1 位应该都是 1,若 x 的第 i 位是 0 且当前范围内存在 w2i1,或者 x 的第 i 位是 1 且当前范围内存在 w<2i1,则答案应该加上 2i1,否则就不加。

​ 若我们对于每一个询问这样暴力二分,时间复杂度就是 O()。但是注意到,在对于不同的询问,每一个二分里面相同的二分值判定是相同的 (因为原始的序列并没有发生任何变化)。于是我们可以用整体二分优化二分的过程。接着我们考虑二分中,对于二分值 C 判定的范围是啥。

​ 显然,我们可以把所有的商品分为两部分:w<C 的,和 wC 的。对于一个询问,它要满足的商品 p 的条件是:LidpR,TltimepTr。这显然就变成了一个静态二维数点问题了。所以我们在整体二分中把这一层的所有的询问和商品缓存下来,然后做两次二维数点就可以了。时间复杂度为 O(Nlog2N)

​ 注意,还需要额外考虑每一个商店的特殊标价。这个怎么做都可以了。时间复杂度 O(NlogN)O(Nlog2N)

​ 所以总时间复杂度为 O(Nlog2N),空间复杂度为 O(N)

参考代码
#include <bits/stdc++.h>
using namespace std;
static constexpr int Maxn = 1e5 + 5;
static constexpr int LOG = 17;
static constexpr int Maxw = (1 << LOG) - 1;
int n, m, cn, qn;
int ans1[Maxn], ans2[Maxn];
struct Goods {
  int w, id, time;
} sw[Maxn], c[Maxn];
struct Query {
  int wl, wr, w;
  int id, ql, qr;
} q[Maxn];
struct fenwick_tree {
  int a[Maxn];
  void add(int x, int v) { for (; x < Maxn; x += x & -x) a[x] += v; }
  int ask(int x) const { int r = 0; for (; x; x -= x & -x) r += a[x]; return r; }
  int qry(int l, int r) const { return ask(r) - ask(l - 1); }
} bit_left, bit_right;
struct Data {
  int x, y, w, id;
  Data() { memset(this, 0, sizeof(*this)); }
  Data(const Data &x) { *this = x; }
  Data(int x, int y, int w, int id)
  : x(x), y(y), w(w), id(id) { }
  friend bool operator < (const Data &x, const Data &y) {
    if (x.x != y.x) return x.x < y.x;
    return x.id < y.id;
  }
};
int cnt_point[Maxn];
void calc(Goods points[], int p_sz, Query qry[], int q_sz) {
  memset(cnt_point, 0, (q_sz + 1) << 2);
  int sz = 0;
  static Data dd[Maxn << 3] = { };
  for (int i = 1; i <= p_sz; ++i)
    dd[++sz] = Data(points[i].id, points[i].time, 1, -1);
  for (int i = 1; i <= q_sz; ++i) {
    dd[++sz] = Data(qry[i].qr, qry[i].wr, 1, i);
    dd[++sz] = Data(qry[i].qr, qry[i].wl - 1, -1, i);
    dd[++sz] = Data(qry[i].ql - 1, qry[i].wr, -1, i);
    dd[++sz] = Data(qry[i].ql - 1, qry[i].wl - 1, 1, i);
  }
  sort(dd + 1, dd + sz + 1);
  for (int i = 1; i <= sz; ++i) {
    if (dd[i].id == -1) bit_left.add(dd[i].y, dd[i].w);
    else cnt_point[dd[i].id] += bit_left.ask(dd[i].y) * dd[i].w;
  }
  for (int i = 1; i <= sz; ++i)
    if (dd[i].id == -1) bit_left.add(dd[i].y, -dd[i].w);
} // calc
Goods w_left[Maxn], w_right[Maxn];
Query q_left[Maxn], q_right[Maxn];
void divide1(int wl, int wr, int l, int r, int ql, int qr, int dep) {
  if (l > r || ql > qr) return ;
  if (wl == wr) return ;
  int wm = (wl + wr) >> 1; // [wl, wm]: "0...", (wm, wr]: "1..."
  int w_left_sz, w_right_sz, q_left_sz, q_right_sz;
  w_left_sz = w_right_sz = 0, q_left_sz = q_right_sz = 0;
  for (int i = l; i <= r; ++i) {
    if (sw[i].w <= wm) {
      w_left[++w_left_sz] = sw[i];
      bit_left.add(sw[i].id, 1);
    }
    else {
      w_right[++w_right_sz] = sw[i];
      bit_right.add(sw[i].id, 1);
    }
  }
  for (int i = ql; i <= qr; ++i) {
    if (((q[i].w >> dep) & 1) != 0) { // simulate trie tree
      if (bit_left.qry(q[i].ql, q[i].qr) != 0) q_left[++q_left_sz] = q[i], ans1[q[i].id] |= (1 << dep);
      else q_right[++q_right_sz] = q[i];
    }
    else {
      if (bit_right.qry(q[i].ql, q[i].qr) != 0) q_right[++q_right_sz] = q[i], ans1[q[i].id] |= (1 << dep);
      else q_left[++q_left_sz] = q[i];
    }
  }
  for (int i = l; i <= r; ++i) {
    if (sw[i].w <= wm) bit_left.add(sw[i].id, -1);
    else bit_right.add(sw[i].id, -1);
  }
  for (int i = 1; i <= w_left_sz; ++i) sw[l + i - 1] = w_left[i];
  for (int i = 1; i <= w_right_sz; ++i) sw[l + w_left_sz + i - 1] = w_right[i];
  for (int i = 1; i <= q_left_sz; ++i) q[ql + i - 1] = q_left[i];
  for (int i = 1; i <= q_right_sz; ++i) q[ql + q_left_sz + i - 1] = q_right[i];
  const int w_cnt = w_left_sz, q_cnt = q_left_sz;
  divide1(wl, wm, l, l + w_cnt - 1, ql, ql + q_cnt - 1, dep - 1);
  divide1(wm + 1, wr, l + w_cnt, r, ql + q_cnt, qr, dep - 1);
} // divide1
void divide2(int wl, int wr, int l, int r, int ql, int qr, int dep) {
  if (l > r || ql > qr) return ;
  if (wl == wr) return ;
  int wm = (wl + wr) >> 1; // [wl, wm]: "0...", (wm, wr]: "1..."
  int w_left_sz, w_right_sz, q_left_sz, q_right_sz;
  w_left_sz = w_right_sz = 0, q_left_sz = q_right_sz = 0;
  int ql_top = ql - 1, qr_top = qr + 1;
  for (int i = l; i <= r; ++i) {
    if (c[i].w <= wm) w_left[++w_left_sz] = c[i];
    else w_right[++w_right_sz] = c[i];
  }
  for (int i = ql; i <= qr; ++i) {
    if (((q[i].w >> dep) & 1) != 0) q_left[++q_left_sz] = q[i];
    else q_right[++q_right_sz] = q[i];
  }
  calc(w_left, w_left_sz, q_left, q_left_sz);
  for (int i = 1; i <= q_left_sz; ++i) {
    if (cnt_point[i] != 0) q[++ql_top] = q_left[i], ans2[q_left[i].id] |= (1 << dep);
    else q[--qr_top] = q_left[i];
  }
  calc(w_right, w_right_sz, q_right, q_right_sz);
  for (int i = 1; i <= q_right_sz; ++i) {
    if (cnt_point[i] != 0) q[--qr_top] = q_right[i], ans2[q_right[i].id] |= (1 << dep);
    else q[++ql_top] = q_right[i];
  }
  assert(ql_top + 1 == qr_top);
  for (int i = 1; i <= w_left_sz; ++i) c[l + i - 1] = w_left[i];
  for (int i = 1; i <= w_right_sz; ++i) c[l + w_left_sz + i - 1] = w_right[i];
  const int w_cnt = w_left_sz, q_cnt = ql_top - ql + 1;
  divide2(wl, wm, l, l + w_cnt - 1, ql, ql + q_cnt - 1, dep - 1);
  divide2(wm + 1, wr, l + w_cnt, r, ql + q_cnt, qr, dep - 1);
} // divide2
int main(void) {
  scanf("%d %d", &n, &m);
  for (int i = 1; i <= n; ++i) {
    scanf("%d", &sw[i].w);
    sw[i].id = i, sw[i].time = 0;
  }
  int day = 0;
  for (int i = 1; i <= m; ++i) {
    int op; scanf("%d", &op);
    if (op == 0) {
      ++day, ++cn;
      scanf("%d %d", &c[cn].id, &c[cn].w);
      c[cn].time = day;
    }
    else {
      ++qn; int d;
      scanf("%d %d %d %d", &q[qn].ql, &q[qn].qr, &q[qn].w, &d);
      q[qn].wr = day, q[qn].wl = q[qn].wr - d + 1;
      q[qn].id = qn;
    }
  }
  divide1(0, Maxw, 1, n, 1, qn, LOG - 1);
  divide2(0, Maxw, 1, cn, 1, qn, LOG - 1);
  for (int i = 1; i <= qn; ++i)
    printf("%d\n", max(ans1[i], ans2[i]));
  exit(EXIT_SUCCESS);
} // main

总结 整体二分可用于在数据结构上的二分 ( 例如trie树的二分过程, 线段树二分等 )。

习题 1 [ZJOI2013] K大数查询

习题 2 [CTSC2018] 混合果汁

习题 3 [HNOI2015] 接水果

习题 4 [国家集训队] 矩阵乘法

习题 5 [POI2011]MET-Meteors

习题 6 [HNOI2016]网络

习题 7 Pastoral Oddities

posted @   cutx64  阅读(2)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示