整体二分
例题
MKTHNUM - K-th Number
考虑如果我们对每个操作进行二分怎么做。
显然是对这个区间不大于二分值 \(mid\) 的数统计个数,看个数 \(num\) 和 \(k\) 的大小关系。如果 \(num\) 更大,证明 \(mid\) 大了,如果 \(num\) 更小,证明 \(mid\) 小了。
然后我们考虑推广这种思路。
首先我们询问的答案肯定是原来数组中的一个数,所以我们可以直接把原数组排序,然后用下标代表其权值。这里我们的排序事实上可以用离散化完成,那样做的效果相同,但是排序更方便。
接下来,我们要实现整体二分的核心函数 \(solve(l,r,L,R)\),表示当前对于编号为 \(id,id\in[l,r]\) 的询问,可能的答案为 \([L,R]\) 内的一个数。
当然,给出的询问不会这么巧合正好是上面那种情况的,所以我们在整体二分时需要对询问排序,当然这个后面会说。
因为答案在 \([L,R]\) 内,所以考虑找到中点 \(mid\),然后把下标在 \([L,mid]\) 内的数加到树状数组里(事实上如果权值不大可以加权值,但这题只能使用下标,要不然空间会炸)。
然后考虑如果对于一个询问 \((l,r,k)\),树状数组在 \([l,r]\) 内的数的数量 \(num\) 不小于 \(k\),证明对于这个询问 \(mid\) 太大了,把他扔到数组 \(q1\) 中;否则就是 \(mid\) 太小了,于是先 \(k\leftarrow k-num\),然后把这个询问扔到数组 \(q2\) 内。
这里说一下为什么要 \(k\leftarrow k-num\)。因为我们接下来对这类 \(mid\) 太小了的询问会单独处理,而后面统计的是在新区间内不大于他的数的数量。我们不能忽略之前比他小的数带来的贡献,所以要做这样一个操作。
然后做完这个类似于归并排序的操作后,我们把之前树状数组里的东西清空掉,然后把 \(q1,q2\) 数组内的东西放回询问数组接着递归处理(事实上就是做了个分类,然后对两类询问继续递归)。
边界情况:\(L=R\),此时 \([l,r]\) 内的询问的答案结尾 \(val_L\),注意不是 \(L\),因为我们一开始用下标代替了权值。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n,m,c[N],res[N];
struct node{
int id,x;
bool operator<(const node &t)const{
return x<t.x;
}
}a[N];
struct ask{
int id,l,r,k;
}q[N],q1[N],q2[N];
int lowbit(int x){
return x&-x;
}
void modify(int x,int v){
while(x<=n){
c[x]+=v;
x+=lowbit(x);
}
}
int qry(int x){
int res=0;
while(x){
res+=c[x];
x-=lowbit(x);
}
return res;
}
void solve(int l,int r,int L,int R){
if(l>r)return;
if(L==R){
for(int i=l;i<=r;i++){
res[q[i].id]=a[L].x;
}
return;
}
int mid=L+R>>1;
for(int i=L;i<=mid;i++){
modify(a[i].id,1);
}
int cnt1=0,cnt2=0;
for(int i=l;i<=r;i++){
int sum=qry(q[i].r)-qry(q[i].l-1);
if(q[i].k<=sum){
q1[++cnt1]=q[i];
}
else{
q[i].k-=sum;
q2[++cnt2]=q[i];
}
}
for(int i=L;i<=mid;i++){
modify(a[i].id,-1);
}
for(int i=1;i<=cnt1;i++){
q[i+l-1]=q1[i];
}
for(int i=1;i<=cnt2;i++){
q[i+l+cnt1-1]=q2[i];
}
solve(l,l+cnt1-1,L,mid);
solve(l+cnt1,r,mid+1,R);
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i].x;
a[i].id=i;
}
for(int i=1;i<=m;i++){
cin>>q[i].l>>q[i].r>>q[i].k;
q[i].id=i;
}
sort(a+1,a+n+1);
solve(1,m,1,n);
for(int i=1;i<=m;i++){
cout<<res[i]<<'\n';
}
return 0;
}
K大数查询
上一道题的动态版。这里说一句,如果没有第 \(c\) 大的数,输出 -1
。
注意这道题需要支持区间加区间查询,所以我们需要线段树或者两个树状数组进行维护。这里笔者采用了两个树状数组。
就比方说要求 \(\displaystyle\sum_{i=1}^{k}a_i\),我们设 \(d_i\) 为其差分数组。然后原式就是 \(\displaystyle\sum_{i=1}^{k}a_i\times (k-i+1)\),再化简一下就是 \(\displaystyle k\times\sum_{i=1}^{k}d_i-\sum_{i=1}^{k}(i-1)\times d_i\)。所以我们用两个树状数组维护前缀和即可
然后剩下的东西就跟上一道题一样。代码:
#include<bits/stdc++.h>
#define int long long
#define N 50005
using namespace std;
int n,m,res[N],c1[N],c2[N];
struct node{
int op,l,r,c,id;
}q[N],q1[N],q2[N];
int lowbit(int x){
return x&-x;
}
void bit_modify(int c[],int x,int v){
while(x<=n){
c[x]+=v;
x+=lowbit(x);
}
}
int bit_qry(int c[],int x){
int res=0;
while(x){
res+=c[x];
x-=lowbit(x);
}
return res;
}
void modify(int x,int v){
bit_modify(c1,x,v);
bit_modify(c2,x,v*(x-1));//这里是树状数组区间加区间查询模板
}
int qry(int x){
return bit_qry(c1,x)*x-bit_qry(c2,x);//这里是树状数组区间加区间查询模板
}
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==2){
res[q[i].id]=L;
}
}
return;
}
int mid=L+R>>1;
int cnt1=0,cnt2=0;
for(int i=l;i<=r;i++){
if(q[i].op==1){
if(q[i].c<=mid){
q1[++cnt1]=q[i];
}
else{
modify(q[i].l,1);
modify(q[i].r+1,-1);
q2[++cnt2]=q[i];
}
}
else{
int sum=qry(q[i].r)-qry(q[i].l-1);
if(q[i].c<=sum){
q2[++cnt2]=q[i];
}
else{
q[i].c-=sum;
q1[++cnt1]=q[i];
}
}
}
for(int i=l;i<=r;i++){
if(q[i].op==1){
if(q[i].c>mid){
modify(q[i].l,-1);
modify(q[i].r+1,1);
}
}
}
for(int i=1;i<=cnt1;i++){
q[i+l-1]=q1[i];
}
for(int i=1;i<=cnt2;i++){
q[i+l+cnt1-1]=q2[i];
}
solve(l,l+cnt1-1,L,mid);
solve(l+cnt1,r,mid+1,R);
}
signed main(){
cin>>n>>m;
int cnt=0;
for(int i=1;i<=m;i++){
cin>>q[i].op>>q[i].l>>q[i].r>>q[i].c;
if(q[i].op==2)q[i].id=++cnt;
}
solve(1,m,0,n);
for(int i=1;i<=cnt;i++){
if(res[i]==0)res[i]=-1;
cout<<res[i]<<'\n';
}
return 0;
}
小马和路
本题可以使用 \(LCT\) 维护 \(MST\),但这里不再赘述。
考虑对于每条边,二分一个值,为如果以这条边作为最小边权的边加入所选边集,那么使得 \(1,n\) 连通所需加入的最大边权的边的边权最小能是多少。
发现二分 \(m\) 次时间过于爆炸,所以我们使用整体二分。
我们首先把待处理的边权倒序排序,然后每次使用可撤销并查集维护信息(不能路径压缩,故使用按秩合并),每次只需要扫一遍加边,判断加完边之后 \(1,n\) 是否连通,最后撤销即可。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 200005
#define pii pair<int,int>
#define x first
#define y second
using namespace std;
int n,m,fa[N],siz[N];
int q[N],q1[N],q2[N],res[N];
pii e[N];
stack<pii>s;
int find(int x){
return fa[x]==x?x:find(fa[x]);
}
void merge(int x,int y){
x=find(x);y=find(y);
if(x==y)return;
if(siz[x]<siz[y])swap(x,y);
fa[y]=x;
siz[x]+=siz[y];
s.push({x,y});
}
void undo(){
int x=s.top().x,y=s.top().y;
s.pop();
siz[x]-=siz[y];
fa[y]=y;
}
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]]=L;
}
return;
}
int mid=L+R>>1;
int cnt1=0,cnt2=0;
int now=mid+1;
for(int i=r;i>=l;i--){
if(q[i]>mid)q2[++cnt2]=q[i];
else{
while(now>q[i]){
now--;
merge(e[now].x,e[now].y);
}
if(find(1)==find(n))q1[++cnt1]=q[i];
else q2[++cnt2]=q[i];
}
}
while(!s.empty())undo();
reverse(q1+1,q1+cnt1+1);
reverse(q2+1,q2+cnt2+1);
for(int i=1;i<=cnt1;i++)q[i+l-1]=q1[i];
for(int i=1;i<=cnt2;i++)q[i+l-1+cnt1]=q2[i];
solve(l,l+cnt1-1,L,mid);
solve(l+cnt1,r,mid+1,R);
}
signed main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>e[i].x>>e[i].y;
q[i]=i;
}
for(int i=1;i<=n;i++){
fa[i]=i;
siz[i]=1;
}
solve(1,m,1,m+1);
int mn=2e18;
for(int i=1;i<=m;i++){
if(res[i]==m+1)continue;
mn=min(mn,res[i]-i+1);
}
if(mn==2e18)mn=-1;
cout<<mn;
return 0;
}
作业
矩阵乘法
首先考虑显然第 \(k\) 小是可以二分的,但是我们发现每次都二分的时间复杂度很高,不能接受,于是使用整体二分。
首先使用经典技巧,用下标代替权值。每次我们二分一个值 \(mid\),然后把值在 \([L,mid]\) 内的数加入二维树状数组中,然后设 \(num\) 为二位树状数组中的数在查询 \(i\) 中的子矩阵的数量,于是分类讨论:
-
\(num\ge k_i\),归到左边。
-
否则先 \(k_i\leftarrow k_i-num\),然后归到右边。
剩下的就是整体二分和二维树状数组的板子了,这里不多赘述。
混合果汁
首先还是先把 \(d\) 降序排序,然后把下标当作权值进行整体二分。
考虑二分如何进行判断。可以建立一棵以单价为下标的权值线段树,节点存储单价在 \([l,r]\) 的所有果汁的总体积与总价格(注意区分单价与总价)。
然后就是简单的了,每次只需要先判断总体积够不够。如果够,考虑权值线段树上二分,查找在体积够的情况下的最小花费(显然选单价更低的更优)。