整体二分学习笔记

P3834 【模板】可持久化线段树 2

这是个静态区间第 \(k\) 大问题,对于需要二分处理很多个询问并且支持离线的题目,可以使用整体二分解决。

顾名思义,它的核心思路是利用分治的思想将 答案在同一个范围内的询问 整体一起处理。

具体的,记 Sol(int L,int R,int ql,int qr) 表示当前需要处理对于 \([ql,qr]\) 的所有询问,并且这些答案在 \([L,R]\) 的范围内。

考虑继续划分下去,令 mid=(L+R)/2,把所有答案范围在 \([L,mid]\) 的询问放进左区间,在 \([mid+1,R]\) 的询问放进右区间,递归下去处理即可。

对于一个询问 \(l,r,k\),记 \(x\) 表示下标范围在 \([l,r]\),值域在 \([L,mid]\) 的数的个数。

如果 \(k \le x\),就说明该询问的答案在 \([L,mid]\) 之间。

否则,在 \([mid+1,R]\) 区间。

但是它有两个限制(下标,值域),数据结构只能处理一维(总不能写主席树吧。。。),怎么办呢?

其实很简单,你分治时就把值域在 \([L,R]\) 的数放进对应的询问数组里面混合起来就可以了,懂我意思吧?

注意,先把数放进去,再把操作放进去。

#include<bits/stdc++.h>
#define int long long
#define N 400005
using namespace std;
int n, m;
int o[N], ans[N], b[N], a[N], c[N], tot, cnt;
map<int, int>P;
struct node{
	int l, r, k, id, type;
}Q[N], q1[N], q2[N];
int lowbit(int x) {return x & (-x);}
int query(int x) {int res = 0; for(int i = x; i; i -= lowbit(i)) res += o[i]; return res;}
void add(int x, int y) {for(int i = x; i <= n; i += lowbit(i)) o[i] += y;}
int ask(int l, int r) {return l == 1 ? query(r) : query(r) - query(l - 1);}
void Sol(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].type == 2) ans[Q[i].id] = l;
		return;
	}
	int mid = (l + r) / 2, cnt1 = 0, cnt2 = 0;
	for(int i = ql; i <= qr; i++) {
		if(Q[i].type == 1) {
			if(Q[i].l <= mid) q1[++cnt1] = Q[i], add(Q[i].id, 1); //把值域在[l,mid]的数放进树状数组里面
			else q2[++cnt2] = Q[i];
		}
		else {
			int x = ask(Q[i].l, Q[i].r);
			if(Q[i].k <= x) q1[++cnt1] = Q[i];
			else {
				Q[i].k -= x;
				q2[++cnt2] = Q[i]; 
			}
		}
	}
	for(int i = 1; i <= cnt1; i++) if(q1[i].type == 1) add(q1[i].id, -1);
	for(int i = ql, j = 1; j <= cnt1; i++, j++) Q[i] = q1[j];
	for(int i = ql + cnt1, j = 1; j <= cnt2; i++, j++) Q[i] = q2[j];
	Sol(l, mid, ql, ql + cnt1 - 1); Sol(mid + 1, r, ql + cnt1, qr);
}
signed main() {
    scanf("%lld %lld", &n, &m);    
    for(int i = 1; i <= n; i++) scanf("%lld", &a[i]), b[i] = a[i];
    sort(b + 1, b + n + 1);
    for(int i = 1; i <= n; i++)
        if(i == 1 || b[i] != b[i - 1]) c[++tot] = b[i];
    for(int i = 1; i <= tot; i++) P[c[i]] = i; //离散化一下
	for(int i = 1; i <= n; i++) Q[++cnt] = (node){P[a[i]], -1, -1, i, 1};
	for(int i = 1, l, r, k; i <= m; i++) {
		scanf("%lld %lld %lld", &l, &r, &k);
		Q[++cnt] = (node){l, r, k, i, 2};
	}
    Sol(1, tot, 1, cnt);
    for(int i = 1; i <= m; i++) printf("%lld\n", c[ans[i]]);
	return 0; 
}

P1527 [国家集训队] 矩阵乘法

矩阵里面静态求第 k 大,同样也很简单。不过要使用二维树状数组,其他跟上题类似。

#include<bits/stdc++.h>
#define int long long
#define N 4000005
using namespace std;
int n, m;
int o[510][510], ans[N], b[N], a[510][510], c[N], tot, cnt, num;
map<int, int>P;
struct node{
	int X1, Y1, X2, Y2, k, id, type;
}Q[N], q1[N], q2[N], Z[N];
int lowbit(int x) {return x & (-x);}
int query(int x, int y) {
	if(x == 0 || y == 0) return 0;
	int res = 0;
	for(int i = x; i; i -= lowbit(i))
	for(int j = y; j; j -= lowbit(j)) res += o[i][j];
	return res;
}
void add(int x, int y, int z) {
	for(int i = x; i <= n; i += lowbit(i))
	for(int j = y; j <= n; j += lowbit(j)) o[i][j] += z;
}
int ask(int X1, int Y1, int X2, int Y2) {
	return query(X2, Y2) - query(X2, Y1 - 1) - query(X1 - 1, Y2) + query(X1 - 1, Y1 - 1);
}
void Sol(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].type == 2) ans[Q[i].id] = l;
		return;
	}
	int mid = (l + r) / 2, cnt1 = 0, cnt2 = 0;
	for(int i = ql; i <= qr; i++) {
		if(Q[i].type == 1) {
			if(Q[i].X1 <= mid) {
				q1[++cnt1] = Q[i];
				add(Q[i].X2, Q[i].Y2, 1);
			} 
			else q2[++cnt2] = Q[i];
		}
		else {
			int x = ask(Q[i].X1, Q[i].Y1, Q[i].X2, Q[i].Y2);
			if(Q[i].k <= x) q1[++cnt1] = Q[i];
			else {
				Q[i].k -= x;
				q2[++cnt2] = Q[i]; 
			}
		}
	}
	for(int i = 1; i <= cnt1; i++) if(q1[i].type == 1) add(q1[i].X2, q1[i].Y2, -1);
	for(int i = ql, j = 1; j <= cnt1; i++, j++) Q[i] = q1[j];
	for(int i = ql + cnt1, j = 1; j <= cnt2; i++, j++) Q[i] = q2[j];
	Sol(l, mid, ql, ql + cnt1 - 1); Sol(mid + 1, r, ql + cnt1, qr);
}
signed main() {
    scanf("%lld %lld", &n, &m);    
    for(int i = 1; i <= n; i++) 
	for(int j = 1; j <= n; j++) scanf("%lld", &a[i][j]), b[++num] = a[i][j];
	for(int i = 1, X1, Y1, X2, Y2, k; i <= m; i++) {
		scanf("%lld %lld %lld %lld %lld", &X1, &Y1, &X2, &Y2, &k);
		Z[i] = (node){X1, Y1, X2, Y2, k, i, 2};
		b[++num] = X1; b[++num] = Y1; b[++num] = X2; b[++num] = Y2; 
	}
    sort(b + 1, b + num + 1);
    for(int i = 1; i <= num; i++)
        if(i == 1 || b[i] != b[i - 1]) c[++tot] = b[i];
    for(int i = 1; i <= tot; i++) P[c[i]] = i;
	for(int i = 1; i <= n; i++) 
	for(int j = 1; j <= n; j++) Q[++cnt] = (node){P[a[i][j]], -1, i, j, -1, -1, 1};
	for(int i = 1; i <= m; i++) {
		Z[i].X1 = P[Z[i].X1]; Z[i].Y1 = P[Z[i].Y1]; Z[i].X2 = P[Z[i].X2]; Z[i].Y2 = P[Z[i].Y2];  
		Q[++cnt] = Z[i];
	}
    Sol(1, tot, 1, cnt);
    for(int i = 1; i <= m; i++) printf("%lld\n", c[ans[i]]);
	return 0; 
}

P2617 Dynamic Rankings

题意:

给定一个含有 \(n\) 个数的序列 \(a_1,a_2 \dots a_n\),需要支持两种操作:

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

分析:

问题变成动态了,怎么办?

对于一个询问 \(l,r,k\),同样需要求下标范围在 \([l,r]\),值域在 \([L,mid]\) 的数的个数。

并且还要保证这些数都要在这个询问之前出现过。

只需要顺序加入修改与操作就能保证了!

对于一个修改操作,一个显而易见的操作是:

scanf("%d %d", &x, &y);
Q[++cnt] = (node){a[x], -1, -1, x, 1};
a[x] = y;
Q[++cnt] = (node){a[x], 1, -1, x, 1};

把原来的贡献变成 -1,新数的贡献改成 1 就好了。

注意!如果你不离散化的话,由于有负数,就不能用 (l+r)/2,而是用 (l+r)>>1

代码:

#include<bits/stdc++.h>
#define N 300005
using namespace std;
int n, m;
int o[100005], ans[100005], a[100005], cnt;
struct node{
	int l, r, k, id, type;
}Q[N], q1[N], q2[N];
int lowbit(int x) {return x & (-x);}
int query(int x) {if(x == 0) return 0; int res = 0; for(int i = x; i; i -= lowbit(i)) res += o[i]; return res;}
void add(int x, int y) {for(int i = x; i <= n; i += lowbit(i)) o[i] += y;}
int ask(int l, int r) {return l == 1 ? query(r) : query(r) - query(l - 1);}
void Sol(int l, int r, int ql, int qr) {
	//cout << l << " " << r << " " << ql << " " << qr << endl;
	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 mid = (l + r) >> 1, cnt1 = 0, cnt2 = 0;
	for(int i = ql; i <= qr; i++) {
		if(Q[i].type == 1) {
			if(Q[i].l <= mid) q1[++cnt1] = Q[i], add(Q[i].id, Q[i].r);
			else q2[++cnt2] = Q[i];
		}
		else {
			int x = ask(Q[i].l, Q[i].r);
			if(Q[i].k <= x) q1[++cnt1] = Q[i];
			else {
				Q[i].k -= x;
				q2[++cnt2] = Q[i]; 
			}
		}
	}
	for(int i = 1; i <= cnt1; i++) if(q1[i].type == 1) add(q1[i].id, -q1[i].r);
	for(int i = ql, j = 1; j <= cnt1; i++, j++) Q[i] = q1[j];
	for(int i = ql + cnt1, j = 1; j <= cnt2; i++, j++) Q[i] = q2[j];
	Sol(l, mid, ql, ql + cnt1 - 1); Sol(mid + 1, r, ql + cnt1, qr);
}
signed main() {
    scanf("%d %d", &n, &m);    
    for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
	for(int i = 1; i <= n; i++) Q[++cnt] = (node){a[i], 1, -1, i, 1};
	for(int i = 1; i <= m; i++) ans[i] = 1e9 + 5;
	for(int i = 1, l, r, k, x, y; i <= m; i++) {
		char c;
		cin >> c;
		if(c == 'Q') {
			scanf("%d %d %d", &l, &r, &k);
		    Q[++cnt] = (node){l, r, k, i, 2};
		}
        else {
        	scanf("%d %d", &x, &y);
        	Q[++cnt] = (node){a[x], -1, -1, x, 1};
        	a[x] = y;
        	Q[++cnt] = (node){a[x], 1, -1, x, 1};
		}
	}
    Sol(-1e9, 1e9, 1, cnt);
    for(int i = 1; i <= m; i++) if(ans[i] != 1e9 + 5) printf("%d\n", ans[i]);
	return 0; 
}
/*
1 1
0
Q 1 1 1
*/

P3527 [POI2011] MET-Meteors

题意:

\(n\) 个成员国。现在它发现了一颗新的星球,这颗星球的轨道被分为 \(m\) 份(第 \(m\) 份和第 \(1\) 份相邻),第 \(i\) 份上有第 \(a_i\) 个国家的太空站。

这个星球经常会下陨石雨。BIU 已经预测了接下来 \(k\) 场陨石雨的情况。

BIU 的第 \(i\) 个成员国希望能够收集 \(p_i\) 单位的陨石样本。你的任务是判断对于每个国家,它需要在第几次陨石雨之后,才能收集足够的陨石。

分析:

整体二分典中典。

同样用Sol(int L,int R,int ql,int qr) 表示当前需要处理 \([ql,qr]\) 的所有国家,并且这些国家在 \([L,R]\) 波陨石雨能够收集到足够的陨石样本。

如何知道哪些国家只需要在 \([L,mid]\) 波陨石雨能够收集到足够的陨石样本呢?

利用树状数组差分一下,假如需要修改 \([x,y]\) 这个区间,只需要 add(x,s)add(y+1,-s)即可。

需要注意的是,对于 q2 里(经过 \([L,mid]\) 波未能收集满)的国家,需要将它的期望收集数量减掉 \([L,mid]\) 波给它贡献的陨石数量。

本题要开 __int128

时间复杂度 \(O(k \log k \log m +n)\)

代码:

#include<bits/stdc++.h>
#define int __int128
#define N 600005 
using namespace std;
inline int read(){
    register int x = 0, t = 1;
    register char ch=getchar(); // 读入单个字符到寄存器
    while(ch<'0'||ch>'9'){
        if(ch=='-')
            t=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        x=(x<<1)+(x<<3)+(ch^48);  // 移位与异或
      	// 第十行可以换成 x = x * 10 + ch - '0'
        ch=getchar();
    }
    return x*t;
}
inline void write(int x)
{
    if(x<0){
    	putchar('-');
		x=-x;
	}
    if(x>9) 
		write(x/10);
    putchar(x%10+'0');
}
int n, m, k;
int o[N], p[N], ans[N];
int c[N], l[N], r[N], s[N], Q[N], q1[N], q2[N];
vector<int>G[N];
int lowbit(int x) {return x & (-x);}
int query(int x) {int res = 0; for(int i = x; i; i -= lowbit(i)) res += c[i]; return res;}
void add(int x, int y) {for(int i = x; i <= m; i += lowbit(i)) c[i] += y;}
void update(int L, int R, int x) {add(L, x); add(R + 1, -x);}
void Sol(int L, int R, int ql, int qr) {
	if(ql > qr) return;
	if(L == R) {
		for(int i = ql; i <= qr; i++) ans[Q[i]] = L;
		return;
	}
	int mid = (L + R) / 2, cnt1 = 0, cnt2 = 0;
	for(int i = L; i <= mid; i++) { //让[L,mid]场陨石落下来 
		if(l[i] <= r[i]) update(l[i], r[i], s[i]);
		else update(l[i], m, s[i]), update(1, r[i], s[i]);
	}
	for(int i = ql; i <= qr; i++) {
		int res = 0; 
		for(auto x : G[Q[i]]) res += query(x);
		if(p[Q[i]] <= res) q1[++cnt1] = Q[i];
		else p[Q[i]] -= res, q2[++cnt2] = Q[i];  //对答案在[mid+1,R]范围内的国家的需求减掉res 
	}
	for(int i = L; i <= mid; i++) { //让[L,mid]场陨石复原 
		if(l[i] <= r[i]) update(l[i], r[i], -s[i]);
		else update(l[i], m, -s[i]), update(1, r[i], -s[i]);
	}
	for(int i = ql, j = 1; j <= cnt1; i++, j++) Q[i] = q1[j];
	for(int i = ql + cnt1, j = 1; j <= cnt2; i++, j++) Q[i] = q2[j];
	Sol(L, mid, ql, ql + cnt1 - 1); Sol(mid + 1, R, ql + cnt1, qr);
}
signed main() {
    n = read(), m = read();
    for(int i = 1; i <= m; i++) {
    	o[i] = read();
    	G[o[i]].push_back(i);
	}
    for(int i = 1; i <= n; i++) p[i] = read(), Q[i] = i;
    k = read();
    for(int i = 1; i <= k; i++) l[i] = read(), r[i] = read(), s[i] = read();
    k++; l[k] = 1; r[k] = m; s[k] = 1e9;
	Sol(1, k, 1, n); 
	for(int i = 1; i <= n; i++) {
		if(ans[i] == k) printf("NIE");
		else write(ans[i]);
		printf("\n");
	}
	return 0;
}

P4602 [CTSC2018] 混合果汁

题意:

\(N\) 种物品,每种各有 \(l_i\) 件,单价为 \(p_i\) 元,单件美味度为 \(d_i\)。给出 \(m\) 个询问,每次询问用至多 \(G\) 元且买到 \(W\) 件物品的情况下,所有物品的最小美味度最大能为多少。

分析:

\(a_i\) 表示每种物品选的件数,简单归纳下条件

  • \(a_i \le l_i\)
  • \(\sum a_ip_i \le G\)
  • \(\sum a_i \ge W\)

要求所有 \(a_i \ge 1\)\(d_i\) 的最小值最大。

很明显的二分题。

考虑 check 一个 x,显然只能保留 美味度大于等于 \(x\) 的物品。

观察条件二,三。发现它的意思是让你在拿到 \(W\) 件物品的前提下,总花费尽可能小。

贪心就很明显了。优先取 \(p_i\) 小的即可。

但有 \(m\) 个询问怎么做呢?暴力贪心时间复杂度达到了 \(O(mn \log n)\)

一.整体二分做法:

虽然没写这种做法,但并不影响我口胡。

多次二分查询这不妥妥的整体二分吗?

同样用Sol(int L,int R,int ql,int qr) 表示当前需要处理 \([ql,qr]\) 的所有小朋友,并且这些小朋友能喝到的最高美味度在 \([L,R]\) 的范围内。

建一棵以单价为下标的线段树,上文说过,这棵线段树只保留美味度大于等于 \(x\) 的物品。

\(mid\) 是会变的,总不能每次都重新建线段树吧。用类似于莫队的移动方式,在 \(mid\) 的移动过程中更新线段树即可。

线段树要维护两个东西,一个是这个价位的物品的个数,一个是要买下这个价位的所有物品的总钱数。

我们对于每个小朋友单独在线段树上贪心一下,就可以划分成两拨小朋友了。

二.主席树做法

同样搞一棵线段树,注意到在二分时有美味度的限制,为了使时间复杂度正确,这里开个主席树。

\(R[i]\) 表示美味度为 \([i,Maxn]\) 的物品的线段树(也是以线段树为下标),然后就可以只用 \(\log 10^5\) 的时间复杂度贪心了!

总结

时间复杂度都是两只 log 的,但整体二分空间复杂度为 \(O(n)\)

做法二代码:

#include<bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n, m, cnt, G, W, INF = 0, Maxn = 0;
int rt[N], R[N]; //R[i]表示美味度大于等于i的主席树 
struct LJM{
	int d, p, l;
}a[N];
struct node{
	int ls, rs, w1, w2;
}t[N * 20];
bool cmp(LJM x, LJM y) {return x.d < y.d;}
void pushup(int u) {
	t[u].w1 = t[t[u].ls].w1 + t[t[u].rs].w1;
	t[u].w2 = t[t[u].ls].w2 + t[t[u].rs].w2;
}
void update(int lsto, int &o, int L, int R, int x, int y) { //下标为价格,但按照美味度塞进主席树里面 
	o = ++cnt;
	t[o] = t[lsto];
	if(L == R) {
		t[o].w1 += a[y].l * a[y].p;
		t[o].w2 += a[y].l;
		return;
	}
	int mid = (L + R) / 2;
	if(x <= mid) update(t[lsto].ls, t[o].ls, L, mid, x, y);
	else update(t[lsto].rs, t[o].rs, mid + 1, R, x, y);
	pushup(o);
}
bool TT;
int query(int o, int L, int R, int W) {
	if(L == R) {
		if(t[o].w2 < W) TT = 0;
		return W * L;
	}
	int mid = (L + R) / 2;
	int W2 = t[t[o].ls].w2;
	if(W <= W2) return query(t[o].ls, L, mid, W);
	else return query(t[o].rs, mid + 1, R, W - W2) + t[t[o].ls].w1;
}
bool check(int x) {
	TT = 1;
	int S = query(R[x], 1, INF, W);
	if(TT == 0) return 0;
	return S <= G;
}
signed main() {
    scanf("%lld %lld", &n, &m);
	for(int i = 1; i <= n; i++) scanf("%lld %lld %lld", &a[i].d, &a[i].p, &a[i].l), INF = max(INF, a[i].p), Maxn = max(Maxn, a[i].d);
	sort(a + 1, a + n + 1, cmp); 
	for(int i = n; i >= 1; i--) {
		update(rt[i + 1], rt[i], 1, INF, a[i].p, i);
		R[a[i].d] = rt[i];
	}
	for(int i = Maxn; i >= 1; i--) 
	    if(R[i] == 0) R[i] = R[i + 1];
	while(m--) {
		scanf("%lld %lld", &G, &W);
		int L = 1, R = Maxn, mid, ans = -1;
		while(L <= R) {
			mid = (L + R) / 2;
			if(check(mid)) {
				L = mid + 1;
				ans = mid;
			}
			else R = mid - 1;
		}
		printf("%lld\n", ans);
	}
	return 0;
}
posted @ 2023-12-07 15:11  小超手123  阅读(39)  评论(0编辑  收藏  举报