整体二分 学习笔记
整体二分
整体二分是一种离线算法,它可以以较少的码量来完成一些主席树的操作。
运用较广,拓展性较高。
大致思想
整体二分基于一种分治的思想,我的理解它就是一种二分和分治的结合,运用分治去搜索,利用二分来优化判断。
以主席树例题为例。
首先考虑单个二分如何解决。
可将区间 \(l\) 至 \(r\) 进行排序,二分查找。
复杂度 \(O(nm\log_{n})\)。
妥妥的超时,接下来考虑整体二分。
顾名思义,我们可以将所有的询问进行离线,统一进行二分。
0.初始化
由于我们需要将询问离线,这里的离线是要将序列和询问都离线。
什么叫做把序列离线呢?
我们可以把序列的初始化,看作是一种修改。
这样理解起来就不会特别困难。
为了减小常数,我们可以现进行离散化。
struct edge
{
int x , y , k , id , type;
}e[maxn * 2];
for(int i = 1;i <= n;i++)
b[i].first = c[i] = a[i] = read() , b[i].second = i;
sort(b + 1 , b + n + 1);
for(int i = 1;i <= n;i++) a[b[i].second] = i;
//离散化
for(int i = 1;i <= n;i++) e[++cnt] = (edge){a[i] , 1 , 1 , i , 1};
for(int i = 1;i <= m;i++)
{
e[++cnt].x = read() , e[cnt].y = read();
e[cnt].k = read() , e[cnt].type = 2 , e[cnt].id = i;
}
//type为1,表示修改,2为查询。
//修改的x存的为值,id为位置。
//查询的x,y为左右边界,k为第几小,id为询问编号。
1.调用
由于整体二分是二分,所以必然需要一个左边界和一个右边界。
solve(1 , n , 1 , cnt);
但可以看到,调用入口运用了两个左右边界。
它的含义为,值域的左右边界为 \(1 - n\),操作的左右边界为 \(1 - cnt\)。
2.二分实现
既然已经有了左右边界,那么我们就可以根据这一点来实现判断合法。
我们考虑将所有值小于 \(mid\) 的修改全部丢到一个数据结构来进行维护。
进行单点修改,区间查询的数据结构。
这里我们采用的是常数小的树状数组。
我们可以思考一下,数据结构里维护的是什么,是区间内小于 \(mid\) 的数的个数。
那么我们分治的内容是什么,不就是去查询小于 \(mid\) 的数是不是有 \(k\) 个吗。
根据二分的正确性,我们发现,最后二分出的结果,一定会是一个序列中的数。
有没有一种恍然大雾的感觉。
inline 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(e[i].type == 2) ans[e[i].id] = l;
//记录答案
return;
}
int mid = (l + r) >> 1;
int cnt1 = 0 , cnt2 = 0;
for(int i = ql;i <= qr;i++)
{
if(e[i].type == 1)
{
if(e[i].x <= mid) a1[++cnt1] = e[i] , t.update(e[i].id , 1);
else a2[++cnt2] = e[i];
//修改树状数组
}
if(e[i].type == 2)
{
int res = t.ask(e[i].y) - t.ask(e[i].x - 1);
if(res >= e[i].k) a1[++cnt1] = e[i];
if(res < e[i].k) a2[++cnt2] = e[i] , a2[cnt2].k -= res;
//注意,如果询问到它小于k,那么k要减去res,这一点和主席树是一样的。
}
}
for(int i = ql;i <= qr;i++)
if(e[i].type == 1 && e[i].x <= mid) t.update(e[i].id , -1);
//清除当前的修改
for(int i = 1;i <= cnt1;i++) e[ql + i - 1] = a1[i];
for(int i = 1;i <= cnt2;i++) e[ql + cnt1 + i - 1] = a2[i];
//更新序列
solve(l , mid , ql , ql + cnt1 - 1);
solve(mid + 1 , r , ql + cnt1 , qr);
}
以上,我们就可以过掉模板题了。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 10;
int n , m , cnt;
int ans[maxn] , a[maxn] , c[maxn];
pair<int , int> b[maxn];
struct edge
{
int x , y , k , id , type;
}e[maxn * 2] , a1[maxn * 2] , a2[maxn * 2];
inline int read()
{
int asd = 0, qwe = 1; char zxc;
while (!isdigit(zxc = getchar())) if (zxc == '-') qwe = -1;
while (isdigit(zxc)) asd = asd * 10 + zxc - '0', zxc = getchar();
return asd * qwe;
}
inline void solve(int l , int r , int ql , int qr);
int main()
{
n = read() , m = read();
for(int i = 1;i <= n;i++)
b[i].first = c[i] = a[i] = read() , b[i].second = i;
sort(b + 1 , b + n + 1);
for(int i = 1;i <= n;i++) a[b[i].second] = i;
for(int i = 1;i <= n;i++) e[++cnt] = (edge){a[i] , 1 , 1 , i , 1};
for(int i = 1;i <= m;i++)
{
e[++cnt].x = read() , e[cnt].y = read();
e[cnt].k = read() , e[cnt].type = 2 , e[cnt].id = i;
}
// return 0;
solve(1 , n , 1 , cnt);
for(int i = 1;i <= m;i++)
printf("%d\n" , c[b[ans[i]].second]);
return 0;
}
struct Tree
{
int sum[maxn];
inline int lowbit(int x) { return x & (-x); }
inline void update(int x , int y) { while(x <= n) sum[x] += y , x += lowbit(x); }
inline int ask(int x) { int res = 0; while(x) res += sum[x] , x -= lowbit(x); return res; }
}t;
inline 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(e[i].type == 2) ans[e[i].id] = l;
return;
}
int mid = (l + r) >> 1;
int cnt1 = 0 , cnt2 = 0;
for(int i = ql;i <= qr;i++)
{
if(e[i].type == 1)
{
if(e[i].x <= mid) a1[++cnt1] = e[i] , t.update(e[i].id , 1);
else a2[++cnt2] = e[i];
}
if(e[i].type == 2)
{
int res = t.ask(e[i].y) - t.ask(e[i].x - 1);
if(res >= e[i].k) a1[++cnt1] = e[i];
if(res < e[i].k) a2[++cnt2] = e[i] , a2[cnt2].k -= res;
}
}
for(int i = ql;i <= qr;i++)
if(e[i].type == 1 && e[i].x <= mid) t.update(e[i].id , -1);
for(int i = 1;i <= cnt1;i++) e[ql + i - 1] = a1[i];
for(int i = 1;i <= cnt2;i++) e[ql + cnt1 + i - 1] = a2[i];
solve(l , mid , ql , ql + cnt1 - 1);
solve(mid + 1 , r , ql + cnt1 , qr);
}
它甚至比我的主席树还快。
关于时间复杂度
写的时候,有没有这样一种感觉。
明明感觉会T,但最后快到飞起。
我们可以用我们良好的初赛素养,来证明一下时间复杂度。
由于程序主体是递归。
我们可以设:
\(n\log(n)\) 为修改时间复杂度,我们姑且认为,每次递归,它递归了一半。
设:
那么有:
可知:
那么:
即:
省掉常数:
带回 \(2^t=n\)
所以,我们就较为严谨的证明了它的复杂度,为 \(O(n(\log_{n})^2)\)。
一些例题
一道很经典的整体二分模板题。
我们还是考虑以一种二分答案的方式去进行解决。
题意
每次给区间加上一个值,问几次以后,一些位置的和可以大于 \(k\), 如果不能,输出NIE。
应该是比较模板的一道题。
思路
二分多少次以后,一些位置的和可以大于 \(k\),如果当前已经大于,就丢到左边,不然就丢到右边。
Code
#include<bits/stdc++.h>
#define int unsigned long long
using namespace std;
const int inf = 1e15;
const int maxn = 300010;
int n , m , k , now , o[maxn] , p[maxn] , ans[maxn];
int lc[maxn] , rc[maxn] , a[maxn] , c[maxn] , c1[maxn] , c2[maxn];
vector<int> hav[maxn];
inline int read()
{
int asd = 0 , qwe = 1; char zxc;
while(!isdigit(zxc = getchar())) if(zxc == '-') qwe = -1;
while(isdigit(zxc)) asd = asd * 10 + zxc - '0' , zxc = getchar();
return asd * qwe;
}
namespace Tree
{
struct TREE
{
int sum[maxn];
inline int lowbit(int x) { return x & (-x); }
inline void add(int x , int y) { for(int i = x;i <= m;i += lowbit(i)) sum[i] += y; }
inline int ask(int x) { int res = 0; for(int i = x;i;i -= lowbit(i)) res += sum[i]; return res; }
}t;
inline void update(int l , int r , int x)
{
if(l <= r)
t.add(l , x) , t.add(r + 1 , -x);
else
t.add(l , x) , t.add(m + 1 , -x),
t.add(1 , x) , t.add(r + 1 , -x);
}
inline int ask(int x)
{
return t.ask(x);
}
}
using namespace Tree;
inline void solve(int l , int r , int ls , int rs);
signed main()
{
n = read() , m = read();
for(int i = 1;i <= n;i++) c[i] = i;
for(int i = 1;i <= m;i++) o[i] = read();
for(int i = 1;i <= n;i++) p[i] = read();
for(int i = 1;i <= m;i++) hav[o[i]].push_back(i);
k = read();
for(int i = 1;i <= k;i++)
lc[i] = read() , rc[i] = read() , a[i] = read();
k++ , lc[k] = 1 , rc[k] = m , a[k] = inf;
solve(1 , k , 1 , n);
for(int i = 1;i <= n;i++)
{
if(ans[i] == k) puts("NIE");
else printf("%lld\n" , ans[i]);
}
return 0;
}
inline void solve(int l , int r , int ls , int rs)
{
if(l == r || ls > rs)
{
for(int i = ls;i <= rs;i++) ans[c[i]] = l;
return;
}
int mid = (l + r) >> 1 , cnt1 = 0 , cnt2 = 0;
while(now < mid) now++ , update(lc[now] , rc[now] , a[now]);
while(now > mid) update(lc[now] , rc[now] , -a[now]) , now--;
for(int i = ls;i <= rs;i++)
{
int sum = 0;
for(auto j : hav[c[i]]) sum += ask(j);
if(sum >= p[c[i]]) c1[++cnt1] = c[i];
if(sum < p[c[i]]) c2[++cnt2] = c[i];
}
for(int i = 1;i <= cnt1;i++) c[ls + i - 1] = c1[i];
for(int i = 1;i <= cnt2;i++) c[ls + cnt1 + i - 1] = c2[i];
solve(l , mid , ls , ls + cnt1 - 1);
solve(mid + 1 , r , ls + cnt1 , rs);
}
同样的例题,带修区间第 \(k\) 大。
由于我们在主席树板子里已经讲过了,将序列的初始化,看作修改,那么现在有了修改,我们同样可以看做是现删除曾经的,再加上现在的。
Code
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int n , m , cnt , tot , a[maxn] , ans[maxn];
struct edge
{
int x , y , k , id , type;
}e[maxn * 3] , a1[maxn * 3] , a2[maxn * 3];
inline int read()
{
int asd = 0 , qwe = 1; char zxc;
while(!isdigit(zxc = getchar())) if(zxc == '-') qwe = -1;
while(isdigit(zxc)) asd = asd * 10 + zxc - '0' , zxc = getchar();
return asd * qwe;
}
namespace Tree
{
struct TREE
{
int sum[maxn];
inline int lowbit(int x) { return x & (-x); }
inline void add(int x , int y) { for(int i = x;i <= n;i += lowbit(i)) sum[i] += y; }
inline int ask(int x) { int res = 0; for(int i = x;i;i -= lowbit(i)) res += sum[i]; return res; }
}t;
inline int ask(int x) { return t.ask(x); }
inline void update(int l , int x) { t.add(l , x); }
}
using namespace Tree;
inline void solve(int l , int r , int ql , int qr);
int main()
{
n = read() , m = read();
for(int i = 1;i <= n;i++)
a[i] = read() , e[++cnt] = (edge){a[i] , 0 , 1 , i , 1};
for(int i = 1;i <= m;i++)
{
char ch; cin >> ch; int l = read() , r = read() , k = (ch == 'Q' ? read() : 0);
if(ch == 'Q') e[++cnt] = (edge){l , r , k , ++tot , 2};
if(ch == 'C') e[++cnt] = (edge){a[l] , 0 , -1 , l , 1} , e[++cnt] = (edge){r , 0 , 1 , l , 1} , a[l] = r;
}
solve(0 , 1e9 , 1 , cnt);
for(int i = 1;i <= tot;i++) cout << ans[i] << endl;
return 0;
}
inline 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(e[i].type == 2) ans[e[i].id] = l;
return;
}
int mid = (l + r) >> 1;
int cnt1 = 0 , cnt2 = 0;
for(int i = ql;i <= qr;i++)
{
if(e[i].type == 1)
{
if(e[i].x <= mid) a1[++cnt1] = e[i] , update(e[i].id , e[i].k);
else a2[++cnt2] = e[i];
}
if(e[i].type == 2)
{
int res = ask(e[i].y) - ask(e[i].x - 1);
if(res >= e[i].k) a1[++cnt1] = e[i];
if(res < e[i].k) a2[++cnt2] = e[i] , a2[cnt2].k -= res;
}
}
for(int i = ql;i <= qr;i++)
if(e[i].type == 1 && e[i].x <= mid) update(e[i].id , e[i].k * -1);
for(int i = 1;i <= cnt1;i++) e[ql + i - 1] = a1[i];
for(int i = 1;i <= cnt2;i++) e[ql + cnt1 + i - 1] = a2[i];
solve(l , mid , ql , ql + cnt1 - 1);
solve(mid + 1 , r , ql + cnt1 , qr);
}
这应该也是一道区间第 \(k\) 小的裸题吧。
思路
考虑整体二分,将所有的询问全部离线。
我们可以将每一个子弹赋值,值可以取他们射击的时间。
这样,被第 \(k\) 颗子弹击碎,就相当于查询第 \(k\) 小了。
这里与其它整体二分的不同的点是。
它的统计答案,要这样写。
if(lc > rc || l == r)
{
for(int i = lc;i <= rc;i++)
if(e[i].type == 2) ans[l]++;
return ;
}
因为我们二分的是时间,而时间又和子弹的编号相等。
所以查询到这些板子会被这颗子弹击碎。
相当与这颗子弹击碎的数量增加。
一个细节
要注意,这里的板子并没有说一定会被击碎,所以初始化时,操作要多加一个。
这样,没有被击碎的板子二分到的就是,最后一颗子弹的下一颗子弹,而不是最后一颗子弹。
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 300010;
int n , m , cnt , ans[maxn];
struct edge
{
int x , y , k , id , type;
}e[maxn * 2] , a1[maxn * 2] , a2[maxn * 2];
inline int read()
{
int asd = 0 , qwe = 1; char zxc;
while(!isdigit(zxc = getchar())) if(zxc == '-') qwe = -1;
while(isdigit(zxc)) asd = asd * 10 + zxc - '0' , zxc = getchar();
return asd * qwe;
}
namespace TREE
{
int l = 200010;
int sum[maxn];
inline int lowbit(int x) { return x & (-x); }
inline void update(int x , int y) { while(x <= l) sum[x] += y , x += lowbit(x); }
inline int ask(int x) { int res = 0; while(x) res += sum[x] , x -= lowbit(x); return res; }
}
using namespace TREE;
inline void solve(int l , int r , int lc , int rc);
signed main()
{
n = read() , m = read();
for(int i = 1;i <= n;i++)
e[++cnt].x = read() , e[cnt].y = read() , e[cnt].k = read() , e[cnt].id = i , e[cnt].type = 2;
for(int i = 1;i <= m;i++)
e[++cnt].x = read() , e[cnt].y = i , e[cnt].type = 1;
e[++cnt].x = 1 , e[cnt].y = ++m , e[cnt].type = 1;
//多加一颗子弹。
solve(1 , m , 1 , cnt);
for(int i = 1;i < m;i++)
cout << ans[i] << endl;
return 0;
}
inline void solve(int l , int r , int lc , int rc)
{
if(lc > rc || l == r)
{
for(int i = lc;i <= rc;i++)
if(e[i].type == 2) ans[l]++;
return ;
}
int mid = (l + r) >> 1 , cnt1 = 0 , cnt2 = 0;
for(int i = lc;i <= rc;i++)
if(e[i].type == 1)
{
if(e[i].y <= mid) update(e[i].x , 1) , a1[++cnt1] = e[i];
else a2[++cnt2] = e[i];
}
for(int i = lc;i <= rc;i++)
if(e[i].type == 2)
{
int sum = ask(e[i].y) - ask(e[i].x - 1);
if(sum >= e[i].k) a1[++cnt1] = e[i];
else a2[++cnt2] = e[i] , a2[cnt2].k -= sum;
}
for(int i = lc;i <= rc;i++)
if(e[i].type == 1 && e[i].y <= mid)
update(e[i].x , -1);
for(int i = 1;i <= cnt1;i++) e[i + lc - 1] = a1[i];
for(int i = 1;i <= cnt2;i++) e[i + lc + cnt1 - 1] = a2[i];
solve(l , mid , lc , lc + cnt1 - 1);
solve(mid + 1 , r , lc + cnt1 , rc);
}