二分的进阶版。
先看一个经典问题。
区间第K大
给定一个长度为 \(n\) 的序列 \(a\) 和 \(m\) 个询问.
每次询问给定一个区间 \([l,r]\),输出该区间第 \(k\) 大的数。
\(n,m \le 30000,a_i \in [0, 2^{31})\)
对于单次询问,二分答案即可。
如何处理多组询问呢?
不难发现在处理每次询问时,我们都要重新进行一次二分,这个时间花费是比较大的。
于是,我们有一个大胆的想法:
将所有的询问离线,并将所有的询问同时进行二分。
整体二分
如上所述,所有询问同时处理。
具体来说,我们用 \((l, r, l', r')\) 来表示当前状态。
其中 \([l,r]\) 表示当前正在考虑的操作的编号区间(不一定是初始编号),\([l',r']\) 表示当前的答案区间。
考虑分治,将当前答案区间分成两部分 \([l',mid]\) 和 \([mid+1, r']\),然后对每一个询问单独考虑。
判断每一个询问的答案属于左半区间右半区间,最后向下传递,直到 \(l' =r'\) 再将答案存起来。
原序列中的元素就被当作是修改加入操作序列。
代码结构大致长这样:
void Solve(int l, int r, int _l, int _r) { // 询问区间,答案区间
if (l > r || _l > _r) return;
if (_l == _r) { // 边界
for (int i = l; i <= r; i++) {
if (!q[i].op) continue; // 如果是修改则跳过
res[q[i].id] = _l; // 存答案
}
return;
}
int mid = (_l + _r) >> 1;
int pc[2] = {0};
for (int i = l; i <= r; i++) {
if (!q[i].op) { // 是修改
if (q[i].r <= mid) { // 左半部分, 直接修改,存储
} else { // 右半部分,存储
}
} else { // 是询问
int tot = Getres(q[i].r) - Getres(q[i].l - 1);
if (tot >= q[i].k) { // 个数超过询问, 左半部分
} else { // 修改询问的k,右半部分
}
}
}
// 撤除修改,避免产生后效
for (int i = 1; i <= pc[0]; i++) {
if (p[0][i].op) continue; // 是询问,跳过
}
// 将原区间按照答案区间分成连续的两个部分
int pl = l;
for (int i = 1; i <= pc[0]; i++) {
q[pl++] = p[0][i];
}
for (int i = 1; i <= pc[1]; i++) {
q[pl++] = p[1][i];
}
// 分治
Solve(l, l + pc[0] - 1, _l, mid);
Solve(l + pc[0], r, mid + 1, _r);
}
回到原题,要求区间第 \(k\) 大。为了便于使用树状数组修改和统计答案,我们将其转化为区间第 \(k'\) 小,直接整体二分即可。
代码:
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int kmax = 3e4 + 3;
const int kmaxM = 1e5 + 3;
struct Q {
int l, r, k;
int id, op, res;
} q[kmaxM], p[2][kmaxM];
int n, m, a[kmax];
int qc;
int num, b[kmax];
int c[kmax];
int Lowbit(int x) {
return x & -x;
}
void Modify(int x, int v) {
for (; x <= n; x += Lowbit(x)) {
c[x] += v;
}
}
int Getres(int x) {
int tot = 0;
for (; x; x -= Lowbit(x)) {
tot += c[x];
}
return tot;
}
void Solve(int l, int r, int _l, int _r) {
if (l > r || _l > _r) return;
if (_l == _r) {
for (int i = l; i <= r; i++) {
if (!q[i].op)
continue;
q[i].res = _l;
}
return;
}
int mid = (_l + _r) >> 1;
int pc[2] = {0};
for (int i = l; i <= r; i++) {
if (!q[i].op) {
if (q[i].k <= mid) {
p[0][++pc[0]] = q[i];
Modify(q[i].id, 1);
} else {
p[1][++pc[1]] = q[i];
}
} else {
int tot = Getres(q[i].r) - Getres(q[i].l - 1);
if (tot >= q[i].k) {
p[0][++pc[0]] = q[i];
} else {
q[i].k -= tot;
p[1][++pc[1]] = q[i];
}
}
}
// 撤除修改
for (int i = 1; i <= pc[0]; i++) {
if (p[0][i].op) continue;
Modify(p[0][i].id, -1);
}
// 分成两部分
int pl = l;
for (int i = 1; i <= pc[0]; i++) {
q[pl++] = p[0][i];
}
for (int i = 1; i <= pc[1]; i++) {
q[pl++] = p[1][i];
}
Solve(l, l + pc[0] - 1, _l, mid);
Solve(l + pc[0], r, mid + 1, _r);
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
b[i] = a[i];
}
// 离散化,便于树状数组修改
sort(b + 1, b + n + 1);
num = unique(b + 1, b + n + 1) - b - 1;
for (int i = 1; i <= n; i++) {
a[i] = lower_bound(b + 1, b + num + 1, a[i]) - b;
}
// 将原序列转化成修改放入操作序列中
for (int i = 1; i <= n; i++) {
q[++qc].k = a[i], q[qc].id = i;
}
for (int i = 1, l, r, k; i <= m; i++) {
scanf("%d%d%d", &l, &r, &k);
// 转成区间第k小
q[++qc] = {l, r, r - l + 1 - k + 1, i, 1};
}
Solve(1, qc, 1, num); // 整体二分
sort(q + 1, q + qc + 1, [](Q p, Q q) { return p.op > q.op || p.op == q.op && p.id < q.id; });
for (int i = 1; i <= m; i++) {
printf("%d\n", b[q[i].res]);
}
return 0;
}
我们将原问题进行拓展
带修改区间第K小
如题,有两种操作。
1, x, v
表示将 \(a_x\) 修改为 \(v\)。2, l, r, k
表示询问 \([l, r]\) 的第 \(k\) 小的元素。
显然,我们只需像将序列转为修改操作那样增加修改操作即可。
代码:
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int kmax = 1e5 + 3;
const int kmaxM = 5e5 + 3;
struct Q {
int l, r, k;
int id, op;
} q[kmaxM], p[2][kmaxM];
int n, m, a[kmax];
int qc;
int num, b[kmax];
int c[kmax];
int res[kmax];
int Lowbit(int x) {
return x & -x;
}
void Modify(int x, int v) {
for (; x <= n; x += Lowbit(x)) {
c[x] += v;
}
}
int Getres(int x) {
int tot = 0;
for (; x; x -= Lowbit(x)) {
tot += c[x];
}
return tot;
}
void Solve(int l, int r, int _l, int _r) {
if (l > r || _l > _r) return;
if (_l == _r) {
for (int i = l; i <= r; i++) {
if (!q[i].op) continue;
res[q[i].id] = _l;
}
return;
}
int mid = (_l + _r) >> 1;
int pc[2] = {0};
for (int i = l; i <= r; i++) {
if (!q[i].op) {
if (q[i].r <= mid) {
p[0][++pc[0]] = q[i];
Modify(q[i].l, q[i].k);
} else {
p[1][++pc[1]] = q[i];
}
} else {
int tot = Getres(q[i].r) - Getres(q[i].l - 1);
if (tot >= q[i].k) {
p[0][++pc[0]] = q[i];
} else {
q[i].k -= tot;
p[1][++pc[1]] = q[i];
}
}
}
for (int i = 1; i <= pc[0]; i++) {
if (p[0][i].op) continue;
Modify(p[0][i].l, -p[0][i].k);
}
int pl = l;
for (int i = 1; i <= pc[0]; i++) {
q[pl++] = p[0][i];
}
for (int i = 1; i <= pc[1]; i++) {
q[pl++] = p[1][i];
}
Solve(l, l + pc[0] - 1, _l, mid);
Solve(l + pc[0], r, mid + 1, _r);
}
int main() {
while (scanf("%d", &n) != EOF) {
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
qc = 0;
for (int i = 1; i <= n; i++) {
q[++qc] = {i, a[i], 1, 1, 0};
}
scanf("%d", &m);
for (int i = 1, op, l, r, k; i <= m; i++) {
scanf("%d%d%d", &op, &l, &r);
res[i] = -1;
if (op == 1) {
q[++qc] = {l, a[l], -1, 1, 0};
q[++qc] = {l, r, 1, 1, 0};
a[l] = r;
} else {
scanf("%d", &k);
q[++qc] = {l, r, k, i, 1};
}
}
Solve(1, qc, 0, 1e9);
for (int i = 1; i <= m; i++) {
if (res[i] == -1) continue;
printf("%d\n", res[i]);
}
}
return 0;
}
[国家集训队]矩阵乘法
在矩阵上求第 \(k\) 大,做法同上,使用二维树状数组即可。
代码中没有将原问题转化为第 \(k\) 小,而是将树状数组反着做,也是可以的。
代码:
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int kmax = 503;
const int kmaxM = 1e5 + 3;
// 位置,权值
struct V {
int x, y, c;
} v[kmax * kmax];
struct Q {
int lx, ly, rx, ry;
int k;
int id;
} q[kmaxM], p[2][kmaxM];
int n, m;
int vc;
int c[kmax][kmax];
int res[kmaxM];
int Lowbit(int x) {
return x & -x;
}
// 二维树状数组反着做
void Modify(int x, int y, int v) {
for (int i = x; i <= n; i += Lowbit(i)) {
for (int j = y; j <= n; j += Lowbit(j)) {
c[i][j] += v;
}
}
}
int Getres(int x, int y) {
int tot = 0;
for (int i = x; i; i -= Lowbit(i)) {
for (int j = y; j; j -= Lowbit(j)) {
tot += c[i][j];
}
}
return tot;
}
void Solve(int l, int r, int _l, int _r) {
if (l > r || _l > _r) return;
if (_l == _r) {
for (int i = l; i <= r; i++) {
res[q[i].id] = v[_l].c;
}
return;
}
int mid = (_l + _r) >> 1;
int pc[2] = {0};
// 直接把答案区间的矩阵元素的坐标取出并修改
for (int i = _l; i <= mid; i++) {
Modify(v[i].x, v[i].y, 1);
}
for (int i = l; i <= r; i++) {
int tot = Getres(q[i].rx, q[i].ry) - Getres(q[i].lx - 1, q[i].ry) - Getres(q[i].rx, q[i].ly - 1) + Getres(q[i].lx - 1, q[i].ly - 1);
if (tot >= q[i].k) {
p[0][++pc[0]] = q[i];
} else {
q[i].k -= tot;
p[1][++pc[1]] = q[i];
}
}
// 撤除修改
for (int i = _l; i <= mid; i++) {
Modify(v[i].x, v[i].y, -1);
}
int pl = l;
for (int i = 1; i <= pc[0]; i++) {
q[pl++] = p[0][i];
}
for (int i = 1; i <= pc[1]; i++) {
q[pl++] = p[1][i];
}
Solve(l, l + pc[0] -1, _l, mid);
Solve(l + pc[0], r, mid + 1, _r);
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
for (int j = 1, w; j <= n; j++) {
scanf("%d", &w);
v[++vc] = {i, j, w};
}
}
// 按照权值排序
// 把矩阵的权值作为答案区间
sort(v + 1, v + vc + 1, [](V p, V q) { return p.c < q.c; });
for (int i = 1; i <= m; i++) {
scanf("%d%d%d%d%d", &q[i].lx, &q[i].ly, &q[i].rx, &q[i].ry, &q[i].k);
q[i].id = i;
}
// 答案区间变为答案所在的矩阵元素区间
Solve(1, m, 1, vc);
for (int i = 1; i <= m; i++) {
printf("%d\n", res[i]);
}
return 0;
}
[ZJOI2013]K大数查询
单点修改改为区间修改即可,使用线段树。
(我的树状数组不知道为什么寄了)
代码:
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#define int long long
using namespace std;
const int kmax = 2e5 + 3;
struct Q {
int l, r;
long long k;
int id, op;
} q[kmax], p[2][kmax];
int n, m;
int res[kmax];
long long c[kmax];
long long tr[kmax << 2], laz[kmax << 2];
void Pushup(int x) {
tr[x] = tr[x << 1] + tr[x << 1 | 1];
}
void Pushdown(int x, int l, int r) {
if (!laz[x]) return;
int mid = (l + r) >> 1;
laz[x << 1] += laz[x];
laz[x << 1 | 1] += laz[x];
tr[x << 1] += (mid - l + 1) * laz[x];
tr[x << 1 | 1] += (r - mid) * laz[x];
laz[x] = 0;
}
void Update(int x, int l, int r, int _l, int _r, int v) {
if (_l <= l && r <= _r) {
laz[x] += v;
tr[x] += (r - l + 1) * v;
return;
}
int mid = (l + r) >> 1;
Pushdown(x, l, r);
if (_l <= mid) {
Update(x << 1, l, mid, _l, _r, v);
}
if (_r > mid) {
Update(x << 1 | 1, mid + 1, r, _l, _r, v);
}
Pushup(x);
}
long long Query(int x, int l, int r, int _l, int _r) {
if (_l <= l && r <= _r) return tr[x];
int mid = (l + r) >> 1;
long long res = 0;
Pushdown(x, l, r);
if (_l <= mid) res += Query(x << 1, l, mid, _l, _r);
if (_r > mid) res += Query(x << 1 | 1, mid + 1, r, _l, _r);
return res;
}
void Solve(int l, int r, int _l, int _r) {
if (l > r || _l > _r) return;
if (_l == _r) {
for (int i = l; i <= r; i++) {
if (!q[i].op) continue;
res[q[i].id] = _l;
}
return;
}
int mid = (_l + _r) >> 1;
int pc[2] = {0};
for (int i = l; i <= r; i++) {
if (!q[i].op) {
if (q[i].k <= mid) {
p[0][++pc[0]] = q[i];
} else {
p[1][++pc[1]] = q[i];
Update(1, 1, n, q[i].l, q[i].r, 1); // 线段树区间修改
}
} else {
int tot = Query(1, 1, n, q[i].l, q[i].r); // 线段树区间查询
if (tot >= q[i].k) {
p[1][++pc[1]] = q[i];
} else {
q[i].k -= tot;
p[0][++pc[0]] = q[i];
}
}
}
// 撤除修改
for (int i = l; i <= r; i++) {
if (!q[i].op && q[i].k > mid) {
Update(1, 1, n, q[i].l, q[i].r, -1);
}
}
int pl = l;
for (int i = 1; i <= pc[0]; i++) {
q[pl++] = p[0][i];
}
for (int i = 1; i <= pc[1]; i++) {
q[pl++] = p[1][i];
}
Solve(l, l + pc[0] - 1, _l, mid);
Solve(l + pc[0], r, mid + 1, _r);
}
signed main() {
scanf("%lld%lld", &n, &m);
int qc = 0;
for (int i = 1; i <= m; i++) {
scanf("%lld%lld%lld%lld", &q[i].op, &q[i].l, &q[i].r, &q[i].k);
q[i].op--;
if (q[i].op) q[i].id = ++qc;
}
Solve(1, m, 0, n);
for (int i = 1; i <= qc; i++) {
printf("%lld\n", res[i]);
}
return 0;
}
完结撒花 \ / \ / \ /
标签:二分,int,整体,long,kmaxM,pc,kmax,include From: https://www.cnblogs.com/ereoth/p/17356189.html