整体二分 学习笔记
整体二分 学习笔记
引入
假设有这么一道题目:
给定一个整数序列 \(a_1, a_2\dots a_n\),求 \(a_l\sim a_r\) 中第 \(k\) 小的元素。
\(1\le n\leq 10^5, |a_i|\le 10^9\)
这题实际上可以用二分答案求解。
不是二分下标,而是二分值域,对于每一个取值,检验是否有 \(k - 1\) 个数小于它,如果是则缩小值域,否则扩大值域。
整体二分
将这个题目作出扩展。
给定一个整数序列 \(a_1, a_2\dots a_n\),有 \(q\) 次询问,每次询问有 \(l_i, r_i, k_i\), 回答 \(a_{l_i}\sim a_{r_i}\) 中第 \(k_i\) 小的元素。
\(1\le n\leq 10^5,\ 1\le q\le 10^5,\ |a_i|\le 10^9\)
如果对每次询问都在线地回答,时间复杂度会爆掉。
对于这种问题,可以考虑离线地进行整体二分。
之前二分答案的时候只处理了一次询问,这次直接把所有的询问统一处理。
将当前询问集合称为 \(Q\),\(ql\sim qr\) 值域区间元素集合 \(A\)。
二分地,设 \(m = \lfloor \dfrac{ql+qr}{2}\rfloor\),于是整个元素集合分为两部分
- 代表 \([ql, m]\) 的 \(A_1\)
- 代表 \((m, qr]\) 的 \(A_2\)
接着对 \(Q\) 进行划分,对于一个询问 \((l, r, k)\),注意此处 \(l, r\) 为下标,作出以下分类:
令 满足 \(i\in[l, r](a_i\le m)\) 的 \(i\) 的个数为 \(t\),这个统计问题可以用树状数组在 \(O(\log n)\) 的时间内维护(类似逆序对的解法)。
- \(t\ge k\),代表这个问题的答案应该是在 \([ql, m]\) 这个值域内的,放入集合 \(Q_1\)
- \(t<k\),代表这个问题的答案应该是在 \([ql, m]\) 这个值域外的,放入集合 \(Q_2\)
这样就完成了集合的划分,把 \(Q_1, A_1\) 和 \(Q_2, A_2\) 两个集合再进一步进行划分,递归处理。
直到 \(ql = qr\),说明当前 \(Q\) 集合内的询问只有 \(ql(qr)\) 一种取值,记录下来就好了。
实现
把初始化也当作一种操作,分治的时候去树状数组里面更新,这样就解决了 \(t\) 的问题,注意分治下一层的时候要清空树状数组。
时间复杂度:\(O(n\log n\log C)\),\(C\) 是值域大小,离散化之后可以达到 \(O(n\log ^2n)\),不过差不多。
哎呀,好像是什么什么树的模板诶。
// Problem: P3834 【模板】可持久化线段树 2
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3834
// Memory Limit: 256 MB
// Time Limit: 1000 ms
// Author: Moyou
// Copyright (c) 2023 Moyou All rights reserved.
// Date: 2023-02-12 13:06:13
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <iostream>
#include <map>
#include <queue>
#include <stack>
#define x first
#define y second
#define speedup (ios::sync_with_stdio(0), cin.tie(0), cout.tie(0))
#define int long long
using namespace std;
typedef pair<int, int> PII;
const int N = 1e6 + 10;
const int INF = 1e9 + 7;
struct OPT
{
int l, r, k, id, type; // type = 2时代表时询问,否则是初始化操作
} q[N], q1[N], q2[N];
int cnt;
int n, m;
int ans[N];
int tr[N << 1];
int lowbit(int x) {return x & (-x); }
void add(int u, int v)
{
while(u <= n)
{
tr[u] += v;
u += lowbit(u);
}
}
int query(int u)
{
int res = 0;
while(u)
{
res += tr[u];
u -= lowbit(u);
}
return res;
}
void solve(int ql, int qr, int l, int r)
{
int mid = l + r >> 1;
if(ql > qr) return ;
if(l == r)
{
for(int i = ql; i <= qr; i ++)
if(q[i].type == 2)
ans[q[i].id] = l;
return ;
}
int cnt1 = 0, cnt2 = 0;
for(int i = ql; i <= qr; i ++)
{
if(q[i].type == 1)
{
if(q[i].l <= mid)
{
add(q[i].id, 1);
q1[++ cnt1] = q[i];
}
else q2[++ cnt2] = q[i];
}
else
{
int t = query(q[i].r) - query(q[i].l - 1);
if(t >= q[i].k) q1[++ cnt1] = q[i];
else q[i].k -= t, q2[++ cnt2] = q[i]; // 注意对于集合Q2,排名变成了k-t
}
}
for(int i = 1; i <= cnt1; i ++) // 消除影响
if(q1[i].type == 1)
add(q1[i].id, -1);
for(int i = 1; i <= cnt1; i ++)
q[i + ql - 1] = q1[i];
for(int i = 1; i <= cnt2; i ++)
q[i + ql + cnt1 - 1] = q2[i];
solve(ql, ql + cnt1 - 1, l, mid);
solve(ql + cnt1, qr, mid + 1, r);
}
signed main()
{
cin >> n >> m;
for(int i = 1, tmp; i <= n; i ++)
cin >> tmp, q[++ cnt] = {tmp, 1, INF, i, 1};
for(int i = 1, l, r, k; i <= m; i ++)
cin >> l >> r >> k, q[++ cnt] = {l, r, k, i, 2};
solve(1, cnt, -INF, INF);
for(int i = 1; i <= m; i ++) cout << ans[i] << endl;
return 0;
}
带修整体二分
给定一个含有 \(n\) 个数的序列 \(a_1,a_2 \dots a_n\),需要支持两种操作:
Q l r k
表示查询下标在区间 \([l,r]\) 中的第 \(k\) 小的数
C x y
表示将 \(a_x\) 改为 \(y\)\(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\)。
这题有解法花里胡哨,其中一个是线段树套平衡树,在线段树的树节点上挂着平衡树,并把原区间拆分成线段树区间,然后二分值域,时间复杂度:\(O(n\log^3n)\)。
整体二分应该是最简单的解法,并且可以做到 \(O(n\log ^2n)\)。
和上一个标题的思路一样,把初始化、修改、查询一起当作一个 “操作” 放进 \(Q\) 中进行分治,注意修改可以解剖为:减少原来的值然后增加新的值,然后就没有难点了。
// Author: Moyou
// Copyright (c) 2023 Moyou All rights reserved.
struct OPT
{
int l, r, k, type, id;
} q[N], q1[N], q2[N];
int cnt;
int ans[N];
int a[N];
int tr[N];
int n, m;
void add(int u, int v)
int query(int u)
void solve(int ql, int qr, int l, int r)
{
if (ql > qr || l > r)
return;
int mid = l + r >> 1;
if (l == r)
{
for (int i = ql; i <= qr; i++)
if (q[i].type == 1)
ans[q[i].id] = l;
return;
}
int cnt1 = 0, cnt2 = 0;
for (int i = ql; i <= qr; i++)
{
if (q[i].type == 0)
{
if (q[i].k <= mid)
add(q[i].l, q[i].r), q1[++cnt1] = q[i];
else
q2[++cnt2] = q[i];
}
else
{
int t = query(q[i].r) - query(q[i].l - 1);
if (t >= q[i].k)
q1[++cnt1] = q[i];
else
q[i].k -= t, q2[++cnt2] = q[i];
}
}
for (int i = 1; i <= cnt1; i++)
if (q1[i].type == 0)
add(q1[i].l, -q1[i].r);
for (int i = 1; i <= cnt1; i++)
q[i + ql - 1] = q1[i];
for (int i = 1; i <= cnt2; i++)
q[i + ql + cnt1 - 1] = q2[i];
solve(ql, ql + cnt1 - 1, l, mid);
solve(ql + cnt1, qr, mid + 1, r);
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
q[++cnt] = {i, 1, a[i], 0};
}
for (int i = 1; i <= m; i++)
{
char op;
cin >> op;
if (op == 'C')
{
int x, y;
cin >> x >> y;
q[++cnt] = {x, -1, a[x], 0, 0};
a[x] = y;
q[++cnt] = {x, 1, y, 0, 0};
}
else
{
int a, b, c;
cin >> a >> b >> c;
q[++cnt] = {a, b, c, 1, i};
}
}
for (int i = 1; i <= m; i++)
ans[i] = -1;
solve(1, cnt, 0, INF);
for (int i = 1; i <= m; i++)
if (ans[i] != -1)
cout << ans[i] << endl;
return 0;
}