整体二分学习笔记
整体二分
考虑某种奇怪的问题,询问满足单调性,可以二分。但是不同的mid需要不同的状态来判定,暴力做代价过高。这时往往要使用整体二分来一起计算。此时可以带修。
整体二分实质上就是最大化询问的共用判定。若当前询问区间对应的答案为 \([L,R]\),我们取其中点 \(M=\left\lfloor\frac{L+R}{2}\right\rfloor\),然后将数据结构的状态调整到 \(M\) 处,然后判断每个询问答案是大于还是小于等于 \(M\)。把询问分成两个部分 \([L,M]\) 和 \((M,R]\) 后再重复上次操作,直到 \(L=R\)。这样整体二分就省去了数据结构在每一次询问中都要改变的数量,操作次数数量级就从 \(\mathcal O(NQ\log N)\) 变为 \(\mathcal O((N+Q)\log N)\)。
例题 1 Dynamic Rankings
题目描述
题目描述
给定一个含有 \(n\) 个数的序列 \(a_1,a_2 \dots a_n\),需要支持两种操作:
Q l r k表示查询下标在区间 \([l,r]\) 中的第 \(k\) 小的数C x y表示将 \(a_x\) 改为 \(y\)输入格式
第一行两个正整数 \(n,m\),表示序列长度与操作个数。
第二行 \(n\) 个整数,表示 \(a_1,a_2 \dots a_n\)。
接下来 \(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数据范围
\(1\le n,m\le 10^5,\,1\le l\le r\le n,\,1\le k\le r-l+1,\,1\le x\le n,\,0\le a_i,y\le 10^9\)
我们把所有的询问和修改按时间顺序排列后放到一起去整体二分。
先想想如果只有一个询问,那么该怎么二分:显然是二分答案。假设当前二分的是 \(C\),则check是看 \([l,\,r]\) 中有多少个数 \(x\) 满足 \(x \ge C\)。
现在我们把询问一次二分的思想放在整体二分中。假设当前二分的是 \(C\),然后对应的修改询问序列是 \(A\)。我们遍历 \(A\) 中的元素 \(X\),如果 \(X\) 是修改 \(p\),且此时修改中的数 \(V\) 满足 \(V\ge C\),就用数据结构将 \(p\) 赋值为 \(1\),表示 \(p\) 处的数 \(x\) 满足条件 \(x\ge C\)。如果 \(X\) 是查询,那么就查询当前状态数据结构中 \([l,\,r]\) 中的和 \(S\),表示 \([l,\,r]\) 中有 \(S\) 个数大于等于 \(C\)。如果 \(S\le k\),说明当前询问答案小于等于 \(C\);否则答案比 \(C\) 大。我们可以用树状数组维护这个过程。然后将操作询问序列分为两半,继续递归下去。这样的时间复杂度就是\(\mathcal O(N\log^2 N)\)。
参考代码
#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 神仙的膜法
题目描述
题目背景
\(\mathtt{\color{red}y\color{black}cx}\)是一个众所周知的神仙。他特别喜欢打怪兽。
题目描述
一个月黑风高的夜晚,\(\mathtt{\color{red}y\color{black}cx}\)和\(\mathtt{\color{gray}hsc}\)在河边上散步。突然间,\(\mathtt{\color{red}y\color{black}cx}\)发现河边上长着一个奇奇怪怪的树。
这珂树有\(N\)个节点,第\(i\)个节点上住着一个编号为\(i\)怪兽,这个怪兽有\(h_i\)滴血。
对于一珂长满怪兽的树,\(\mathtt{\color{red}y\color{black}cx}\)会两种魔法:
对这珂树的一条链上的所有怪兽打出\(v\)的血量
对这珂树的一珂子树中的所有怪兽打出\(v\)的血量
其中\(v\)在每一次魔法中不一定相同。
然而,\(\mathtt{\color{red}y\color{black}cx}\)的魔法会随着周围的环境即时间的变化而变化,这也就是说,\(\mathtt{\color{red}y\color{black}cx}\)并不能随心所欲
为所欲为地使用滥用魔法了。不过呢,\(\mathtt{\color{red}y\color{black}cx}\)拥有预测能力,即他珂以知道时刻\(i\)时,环境是什么样的,和他的魔法会变成什么样。现在,\(\mathtt{\color{red}y\color{black}cx}\)把他在接下来\(M\)秒内所要使用的所有\(M\)个魔法跟\(\mathtt{\color{gray}hsc}\)说了,要\(\mathtt{\color{gray}hsc}\)算出每一个怪兽会在接下来的几秒内死亡,或者它们能坚强地活下来。然而,\(\mathtt{\color{gray}hsc}\)并不会数数,所以她向你寻求帮忙。
输入格式
N M h_1 h_2 ... h_n magic_1 magic_2 ... magic_Q其中
magic_i表示\(\mathtt{\color{red}y\color{black}cx}\)使用的魔法,是以下两种形式中的一个:
1 u v x:表示\(\mathtt{\color{red}y\color{black}cx}\)使用第一种魔法,对所有在\(u\rightarrow v\)这条链上的怪兽打出\(x\)的血量。
2 u x:表示\(\mathtt{\color{red}y\color{black}cx}\)使用第二种魔法,对所有在\(u\)子树内的怪物打出\(x\)的血量。输出格式
对于每一个怪兽,输出一行:若它还能再\(\mathtt{\color{red}y\color{black}cx}\)的魔法中存活下来,输出
alive;否则输出一个数\(y\),表示在\(\mathtt{\color{red}y\color{black}cx}\)打出前\(y-1\)个魔法后,这个怪兽还活着,而打出第\(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数据范围
对于所有数据,满足:
\(1\le N,M\le 3\times 10^5,1\le h_i\le 10^9\)
对于第\(i\)个魔法:
若是第一类,则满足\(1\le u_i,v_i\le N,1\le x\le 10^9\);
若是第二类,则满足\(1\le u_i\le N,1\le x\le 10^9\).
题后一言
附赠\(\mathtt{\color{red}y\color{black}cx}\)的魔法口诀(\(39\)子真言):
释迦牟尼 脚绽莲花 菩提达摩 你真伟大 天上天下 唯我独尊 如来佛祖 太上老君 耶稣耶稣快显灵
看到要求每个怪物最早在什么时候死掉,我们想到可以使用整体二分。
整体二分了之后,问题就转化为:树链加,子树加,单点查。显然可以树剖+BIT,不过复杂度是 \(\mathcal O(N\log^3N)\),过不了 \(3\times 10^5\)。冷静一下,发现:所有的查询都在修改后,即这是个静态问题。然后我们就可以树上差分了吧。。。不行,整体二分每一层树上差分的复杂度是 \(\mathcal O(N)\),不是 \(\mathcal O(r-l+1)\),然后就会被刁钻的数据卡掉。于是我们的思路是树剖,然后用类似于虚树的思想,我们保留会被用到的数组的下标,对这些下标进行离散化。于是,整体二分每一层的复杂度就是 \(\mathcal O((r-l+1)\log N)\),就能解决了。
按上面思路写完后交一发上去,发现:我怎么TLE了?两个 \(\log\) 还跑不过 \(3\times 10^5\)?其实是在离散化时,此时存在数组里的有用的下标个数最多是 \(\mathcal O(N\log N)\) 级别,如果用快速排序(特别是C中的qsort),复杂度就会被卡成 \(O(N\log^2 N)\),加上整体二分自带的 \(\log\) 之后,甚至比树剖+BIT的 \(\log^3\) 还慢。我们考虑到下标不会超过 \(N\),可以用桶排。但桶排的复杂度又是 \(\mathcal O(N)\),出现了和之前一样的问题。所以类似"根号分治",可以设一个临界值 \(C\approx \frac{N}{\log N}\),当数组元素个数大于 \(C\) 时就用桶排,因为此时桶排的复杂度是严格小于等于快排的复杂度;否则就用快排。于是这样子的复杂度就是严格 \(\mathcal O(N\log^2 N)\)。
参考代码
#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\) 条边的有向图,由于政府正在尝试优化人民生活,他们会废弃一些无用的道路来把省下的钱用于经济建设。
城市都有各自的发达程度 \(s_i\)。为了方便管理,政府将整个地图划分为一些地区,两个点 \(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\) 个正整数,表示 \(s_i\),即每个城市的发达程度。
接下来 \(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数据范围
\(\text{subtask 1 (19 pts): } n \le 10^5,\, m \le 2\times 10^5,\, q\le 2\times 10^5\),删除操作个数\(\times m\le 10^6\);
\(\text{subtask 2 (39 pts): }n\le 5\times 10^3,\,m\le 8\times 10^3,\,q\le 2\times 10^5\);
\(\text{subtask 3 (42 pts): }n\le 10^5,\, m\le 2\times 10^5,\, q\le 2\times 10^5\).
保证任何时刻发达程度 \(\le 10^9\),无重边(反向边不算重边)无自环。
首先套路性地将删边变为加边,于是问题就变成了:动态加边,改变一个点权值大小,和那个奇怪的查询。
先考虑如果是无向图怎么做。无向图很显然加边就是把两个连通块合并,那么可以对于每一个连通块维护一个动态开点线段树。线段树下标即为权值,查询时就在线段树上二分,加边时就把两个连通块所代表的线段树合并一下。于是这样可以做到 \(\mathcal O(N\log N)\)。
然后考虑有向图。我们发现无向图的好处就是每加一条边,都可以把两个连通块合并;而有向图上就不行了。所以我们现在要求出:对于每条边,什么时候可以把这条边所连接的两个端点所在的强连通块合并起来。求出了这个之后就可以像无向图一样直接线段树合并了。
那么问题时怎么找出这些时间点呢?这里就可以用到整体二分的技巧。不难发现,每条边的存在是有时间的。先考虑暴力对于每一条边都二分,那么就是二分时间 \(t\) ,把所有 \(t\) 时间内就存在的边加入到当前的图里,然后跑一边tarjan,看这条边所连接的两个端点是否在同一个强连通块中。如果在,说明答案在 \(t\) 之前;否则在 \(t\) 之后。然后发现,对于不同的时间 \(t\) 所对应的图是不同的;但对于不同的边且相同的 \(t\),此时的图是相同的。所以我们可以用整体二分的技巧来优化暴力二分。注意在整体二分过程中,我们无法承受每次都加入出现时间在 \([0,\,t]\) 内的边;于是可以考虑利用上一次递归的结果,使用可撤销并查集来维护当前区间tarjan缩点的结果,这样总时间复杂度就是 \(\mathcal O(N\log^2 N)\) 或 \(\mathcal O(N\log N\cdot\alpha(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可以看出,整体二分可以用来解决一些存在时间类型的问题。
题目描述
You are given an integer array \(a_1,\ldots,a_n\) and an integer array \(b_1,\ldots,b_n\).
You have to calculate the array \(c_1,\ldots,c_n\) defined as follows: $$c_k=\max\limits_{\gcd(i,j)=k}|a_i-b_j|$$.
Input
The first line of input contains a single integer \(n\) (\(1\le n\le 10^5\)).
The second line of input contains \(n\) integers \(a_1,\ldots,a_n\) (\(1\le a_i\le 10^9\)).
The third line of input contains \(n\) integers \(b_1,\ldots,b_n\) (\(1\le b_i\le 10^9\)).
Output
Output \(n\) integers \(c_1,\ldots,c_n\).
Example 1#Input
8 1 2 3 4 5 6 7 8 8 7 6 5 4 3 2 1Example 1#Output
7 5 3 3 1 3 5 7
一眼没有明显的做法,我们尝试着先化简式子。首先可以把绝对值拆掉,拆成两块分别算。
然后每一块的式子是这样的:
\(\begin{align*}c_k&=\max\limits_{\gcd(i,j)=k}\left\{a_i+b_j\right\}=\max\limits_{k|i,i\le n}\left\{a_i+\max\limits_{\gcd(i,j)=k}b_j\right\}\\&=\max\limits_{k|i,i\le n}\left\{a_i+\max\limits_{k|j,j\le n}b_j[\gcd(i,j)=k]\right\}\\&=\max\limits_{1\le i\le \lfloor\frac nk\rfloor}\left\{a_{ik}+\max\limits_{1\le j\le\lfloor\frac nk\rfloor}b_{jk}[\gcd(i,j)=1]\right\}\end{align*}\)
我们枚举 \(k\),每个 \(k\) 中询问是 \(m=\left\lfloor\dfrac nk\right\rfloor\) 次 \(\max\limits_{1\le i\le m}A_i[\gcd(i,T)=1]\),其中 \(A_i=b_{ik},1\le T\le m\)。我们发现 \(\max\) 根本不好用数论方式化简。
于是我们考虑整体二分。用一次整体二分,将 \(\max\) 变为 \(\sum\)。这样式子就变为了 \(\sum\limits_{1\le i\le m}B_i[\gcd(i,T)=1]\) 其中 \(B_i=[C\le A_i]\),\(C\) 是每一次整体二分的数值。这个式子我们用一下 \(\gcd\) 反演之后就变为了:\(\sum\limits_{1\le i\le m}B_i[\gcd(i,T)=1]=\sum\limits_{1\le i\le m}B_i\sum\limits_{d|i,d|T}\mu(d)=\sum\limits_{d|T}\mu(d)\sum\limits_{d|i,i\le m}B_i\)。我们现在要算的就是 \(\sum\limits_{d|i,i\le m}B_i\)。这玩意儿显然可以记忆化后暴力。于是,整体二分里面的复杂度就是 \(\mathcal O(m\log^2 m)\)。
所以,总的时间复杂度是 \(\mathcal O\left(\sum\limits_{1\le k\le n}\left\lfloor\dfrac nk\right\rfloor\log^2 \left\lfloor\dfrac nk\right\rfloor\right)=\mathcal O(n\log^3n)\),常数较小,可以通过。
参考代码
#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
总结 整体二分可以将多次询问无法解决的 \(\min\) 或 \(\max\) 式变为 \(\sum\),然后推式子优化解法。
例题 5 [FJOI2015]火星商店问题
题目描述
题目描述
火星上的一条商业街里按照商店的编号 \(1 \sim n\) ,依次排列着 \(n\) 个商店。商店里出售的琳琅满目的商品中,每种商品都用一个非负整数 \(\text{val}\) 来标价。每个商店每天都有可能进一些新商品,其标价可能与已有商品相同。
火星人在这条商业街购物时,通常会逛这条商业街某一段路上的所有商店,譬如说商店编号在区间 \([l,r]\) 中的商店,从中挑选一件自己最喜欢的商品。每个火星人对商品的喜好标准各不相同。
通常每个火星人都有一个自己的喜好密码 \(x\)。对每种标价为 \(\text{val}\) 的商品,喜好密码为 \(x\) 的火星人对这种商品的喜好程度与 \(\text{val}\) 异或 \(x\) 的值成正比。也就是说,\(\text{val xor }x\) 的值越大,他就越喜欢该商品。
每个火星人的购物卡在所有商店中只能购买最近 \(d\) 天内(含当天)进货的商品。另外,每个商店都有一种特殊商品不受进货日期限制,每位火星人在任何时刻都可以选择该特殊商品。每个商店中每种商品都能保证供应,不存在商品缺货的问题。
对于给定的按时间顺序排列的事件,计算每个购物的火星人的在本次购物活动中最喜欢的商品,即输出 \(\text{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,10^5]\) 范围内。
考虑到询问是给定一定范围内的集合 \(S\) 与一个整数 \(x\),求 \(\max\limits_{w\in S}w\bigoplus x\)。我们可以考虑用trie树做。但是trie树的构建需要把所有元素都放到一个trie树里面再查询,朴素的实现肯定是不行的。
注意到trie树中的查询其实是一个二分的过程:对于一个 \(x\),假设现在从高往低算到了第 \(i\) 位,则这一层二分的值的后 \(i-1\) 位应该都是 1,若 \(x\) 的第 \(i\) 位是 \(0\) 且当前范围内存在 \(w\ge 2^{i-1}\),或者 \(x\) 的第 \(i\) 位是 \(1\) 且当前范围内存在 \(w<2^{i-1}\),则答案应该加上 \(2^{i-1}\),否则就不加。
若我们对于每一个询问这样暴力二分,时间复杂度就是 \(\mathcal O(过不去)\)。但是注意到,在对于不同的询问,每一个二分里面相同的二分值的判定是相同的 (因为原始的序列并没有发生任何变化)。于是我们可以用整体二分优化二分的过程。接着我们考虑二分中,对于二分值 \(C\) 判定的范围是啥。
显然,我们可以把所有的商品分为两部分:\(w<C\) 的,和 \(w\ge C\) 的。对于一个询问,它要满足的商品 \(p\) 的条件是:\(L\le id_p\le R,\;T_l\le time_p\le T_r\)。这显然就变成了一个静态二维数点问题了。所以我们在整体二分中把这一层的所有的询问和商品缓存下来,然后做两次二维数点就可以了。时间复杂度为 \(\mathcal O(N\log ^2 N)\)。
注意,还需要额外考虑每一个商店的特殊标价。这个怎么做都可以了。时间复杂度 \(\mathcal O(N\log N)\) 或 \(\mathcal O(N\log^2N)\)。
所以总时间复杂度为 \(\mathcal O(N\log^2 N)\),空间复杂度为 \(\mathcal 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
浙公网安备 33010602011771号