整体二分

前提条件:

  1. 题目允许离线算法
  2. 对于询问,答案具有单调性(例如,区间第k小等)

算法步骤:

  1. 对于所有的操作离线,保存在结构体中。
  2. 定义solve(MIN,MAX,st,en)表示对于操作区间 [ s t , e n ] [st,en] [st,en]中的询问,确定的答案值域范围为 [ M I N , M A X ] [MIN,MAX] [MIN,MAX],递归求解。
  3. 到达边界处 M I N = = M A X MIN==MAX MIN==MAX,则将当前操作序列中所有的查询答案赋值为MIN,回溯。

以P2617 Dynamic Rankings为例。
两种操作:

  1. 询问区间 [ L , R ] [L,R] [L,R] k k k小的数字是谁
  2. a [ i ] a[i] a[i]改为x

考虑问题的简化版:只有一次询问,那是不是直接使用nth_element函数,O(n)知晓答案。也可以二分答案为 x x x,将序列中小于等于x的数字对应的位置置为1,即可在 n ∗ l o g 2 n n*log_2n nlog2n时间内知晓答案。

再考虑去掉修改的问题:多次询问,没有修改操作,那是不是可以对每次询问都二分答案,当然如果直接这么做的话复杂度达到了 n ∗ q ∗ l o g 2 n n*q*log_2n nqlog2n这么高,那就需要用到整体二分了,二分答案,考虑他对于所有询问的贡献。
我们定义了 s o l v e ( m i n , m a x , s t , e n ) solve(min,max,st,en) solve(min,max,st,en)的意义,那么这里就可以拿来用了,设当前二分的答案为 m i d mid mid,遇到询问 ( l , r , k ) (l,r,k) (l,r,k),树状数组求得区间中 < = m i d <=mid <=mid的数字个数为 c n t cnt cnt

  1. 如果 c n t > = k cnt>=k cnt>=k,是不是说明了该次询问的答案一定是小于等于 m i d mid mid的,那么对于这种询问我们尝试缩小答案。
  2. 否则的话,说明答案 > m i d >mid >mid,那么我们将 k k k减去 c n t cnt cnt,然后增大答案。
  3. 接下来对这两种询问分别处理,那为了处理的方便,我们将这两种询问拆分为两个操作序列,就可以分别处理了。

整体二分与CDQ分治最大的区别就是一个是基于时间的分治,一个是基于整体的分治;一个是自顶向下的分治思路,一个是自底向上的。

#include
using namespace std;

const int maxn=3e5+7;

int a[maxn],b[maxn];

int m;//值域区间;
void quchong(int n){
    sort(b+1,b+1+n);
    m=unique(b+1,b+1+n)-b-1;
}

int getid(int x){
    return lower_bound(b+1,b+1+m,x)-b;
}

struct Node{
    int l,r,k;
    int op;//-1删掉,0加上,>=1查询;
}q[maxn],lq[maxn],rq[maxn];

int ans[maxn];

int sum[maxn];

void add(int x,int y){
    for(;x<=m;x+=(x&-x)) sum[x]+=y;
}

int ask(int x){
    int res=0;
    for(;x;x-=(x&-x)) res+=sum[x];
    return res;
}

void solve(int L,int R,int st,int en){
    if(L>R) return ;
    if(L==R){
        for(int i=st;i<=en;++i)
            if(q[i].op>0) ans[q[i].op]=b[L];
        return ;
    }
    int mid=(L+R)>>1;
    int lt=0,rt=0;
    for(int i=st;i<=en;++i)
        if(q[i].op==-1){
            if(q[i].l<=mid) add(q[i].r,-1),lq[++lt]=q[i];
            else rq[++rt]=q[i];
        }
        else if(q[i].op==0){
            if(q[i].l<=mid) add(q[i].r,1),lq[++lt]=q[i];
            else rq[++rt]=q[i];
        }
        else{
            int cnt=ask(q[i].r)-ask(q[i].l-1);
            if(cnt>=q[i].k) lq[++lt]=q[i];
            else q[i].k-=cnt,rq[++rt]=q[i];
        }

    for(int i=st;i<=en;++i)
        if(q[i].l<=mid)
            if(q[i].op==-1) add(q[i].r,1);
            else if(q[i].op==0) add(q[i].r,-1);

    for(int i=1;i<=lt;++i) q[st+i-1]=lq[i];
    for(int i=1;i<=rt;++i) q[st+lt+i-1]=rq[i];
    solve(L,mid,st,st+lt-1);
    solve(mid+1,R,st+lt,en);
}
bool vis[maxn];

char s[9];
int main(){
    int n,qq,l,r,k;
    scanf("%d%d",&n,&qq);
    int t=0;
    t=n;
    int hh=n;
    for(int i=1;i<=n;++i){
        scanf("%d",&a[i]);
        b[i]=a[i];
        q[i].op=0,q[i].l=a[i],q[i].r=i;
    }

    for(int i=1;i<=qq;++i){
        scanf("%s",s);
        if(s[0]=='Q'){
            ++t;
            scanf("%d%d%d",&q[t].l,&q[t].r,&q[t].k);
            q[t].op=i;
            vis[i]=1;
        }
        else{
            scanf("%d%d",&l,&r);
            q[++t].op=-1,q[t].l=a[l],q[t].r=l;
            q[++t].op=0,q[t].l=r,q[t].r=l;
            b[++hh]=r;
            a[l]=r;
        }
    }
    quchong(hh);
//    for(int i=1;i<=m;++i) cout<
//    cout<
    for(int i=1;i<=t;++i)
        if(q[i].op<=0) q[i].l=getid(q[i].l);
//    for(int i=1;i<=t;++i)
//        cout<
    solve(1,m,1,t);
    for(int i=1;i<=qq;++i)
        if(vis[i]) printf("%d\n",ans[i]);

    return 0;
}

AcWing 268. 流星
题面见链接。
通过分析题目,有k次修改,n次询问,而且修改都发生在询问之前,同时对于每一次查询,答案都具有二分的性质,即陨石雨越多能收集到的石头越多,那么就可以整体二分,每次二分操作次数,将询问序列分为两半。
注意加法会炸long long。

#include
using namespace std;

const int maxn=3e5+7;

typedef long long ll;

struct Node{
    int l,r,x;
}a[maxn];

struct AAA{
    int need;
    int id;
}q[maxn],lq[maxn],rq[maxn];

ll sum[maxn];

int m;
void add(int x,int y){
    for(;x<=m;x+=x&-x) sum[x]+=y;
}

ll ask(int x){
    ll res=0;
    for(;x;x-=x&-x) res+=sum[x];
    return res;
}

vector<int> v[maxn];
int ans[maxn];

void solve(int L,int R,int st,int en){
    if(L>R) return ;
    //if(st>en) return ;
    if(L==R){
        for(int i=st;i<=en;++i) ans[q[i].id]=L;
        return ;
    }
    int mid=(L+R)>>1;
    int lt=0,rt=0;
    for(int i=L;i<=mid;++i){
        if(a[i].l>a[i].r) add(1,a[i].x);
        add(a[i].l,a[i].x);
        add(a[i].r+1,-a[i].x);
    }

    for(int i=st;i<=en;++i){
        long double cnt=0;
        for(int j=0;j<v[q[i].id].size();++j){
            cnt+=ask(v[q[i].id][j]);
        }
        if(cnt>=q[i].need) lq[++lt]=q[i];
        else q[i].need-=(ll)cnt,rq[++rt]=q[i];
    }
    for(int i=L;i<=mid;++i){
        if(a[i].l>a[i].r) add(1,-a[i].x);
        add(a[i].l,-a[i].x);
        add(a[i].r+1,a[i].x);
    }
    for(int i=1;i<=lt;++i) q[st+i-1]=lq[i];
    for(int i=1;i<=rt;++i) q[st+lt+i-1]=rq[i];
    solve(L,mid,st,st+lt-1);
    solve(mid+1,R,st+lt,en);
}
const int inf=0x3f3f3f3f;
int main(){
    int n,x;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;++i){
        scanf("%d",&x);
        v[x].push_back(i);
    }
    for(int i=1;i<=n;++i){
        scanf("%d",&q[i].need);
        q[i].id=i;
    }
    int k;
    scanf("%d",&k);
    for(int i=1;i<=k;++i)
        scanf("%d%d%d",&a[i].l,&a[i].r,&a[i].x);
    ++k;
    a[k].l=1,a[k].r=m,a[k].x=inf;
    solve(1,k,1,n);
    for(int i=1;i<=n;++i)
        if(ans[i]==k) printf("NIE\n");
        else printf("%d\n",ans[i]);
    return 0;
}

你可能感兴趣的:(分治)