整体二分学习

这大概是我目前学过的最难理解的知识点了吧(

概述

整体二分,意味着同时二分一切

这个算法适用于静态和动态区间第\(k\)大,以及一些区间询问问题.那么根据通常的思路,让我们先来介绍一下暴力,再来分析二者的区别.

静态区间第k大

让我们暴力地二分答案来做,应该怎么做呢?既然我们要求区间第\(k\)大,那么区间中就应该有\(k-1\)个数比答案大才对.所以我们二分答案\(mid\),看看序列里有多少个比它小的数,然后缩小值域.

想象一下,如果对于每一个询问操作我们都这样做,会\(T\)成什么样子.对于每个操作都要做相同的二分,会出现大量的时间浪费.那么我们最好能一次性处理所有的操作.这时候就要用到整体二分了!

机制

对于答案的二分还是依旧,只不过这次不是针对一个操作了.我们把各个操作排成一列,和二分同步进行.这里我们设计一个函数\(solve(ql,qr,l,r)\),表示我们把某些操作放到"符合答案在区间\([l,r]\)中"的区域,根据值域进行二分答案,并把它们分别标号\([ql,qr]\)来方便递归分组.现在我们来考虑怎么分组以及分组过程中的事情.

首先,如果\(l=r\)了的话,代表区间所属询问的答案在区间\([l,l]\)中,那就直接记录答案即可.然后如果还没到最后,我们就要像暴力那样二分出mid,然后扫一遍对应的数看一看有多少数大于mid,然后接下来就是看操作如何分组了.

每个操作都询问一个区间,针对一个区间的时候我们可以直接做,那么针对许多区间呢?我们可以用树状数组的方式,通过前缀和相减来迅速确认有多少个是大于mid的.如果大于k,代表答案应该还在左边,所以把这个操作放到左边,否则放到右边,具体可以通过开两个数组暂存来实现.最后像归并排序一样把这些暂存器里的操作按已经分好的左右放回操作序列,并根据分的左右来递归左边多少,右边多少.

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int sum[5000050],n,m,cnt,answer[5000050];
const int oo=1e9;
struct node
{
    int a,b,k,id,type;
}q[5000050],q1[5000050],q2[5000050];
/****************BIT*************/
int lowbit(int x)
{
    return x&(-x);
}
void add(int pos,int w)
{
    for(int i=pos;i<=n;i+=lowbit(i))sum[i]+=w;
}
int ask(int pos)
{
    int ans=0;
    for(int i=pos;i;i-=lowbit(i))ans+=sum[i];
    return ans;
}
/****************BIT**************/
void solve(int ql,int qr,int l,int r)//假设答案在[l,r]中,符合答案属于[l,r]的操作序列是[ql,qr]
{
    if(ql>qr)return;
    if(l==r)//如果答案已经确定
    {
        for(int i=ql;i<=qr;i++)//区间中所有的操作答案都已经确定
            if(q[i].type==2)
                answer[q[i].id]=l;
        return ;
    }
    int cnt1=0,cnt2=0;
    int mid=(l+r)>>1;//二分答案
    for(int i=ql;i<=qr;i++)//扫一遍这个操作和数的混合序列,进行分组和重新混合
        if(q[i].type==1)//如果是数
        {
            if(q[i].a<=mid)//考虑它应该放在哪个操作区间继续下传
                q1[++cnt1]=q[i],//q1是将要下放到左区间的暂时储存器
                add(q[i].id,1);//加到树状数组,之后求前缀和
            else
                q2[++cnt2]=q[i];//放到右边先不管
        }
        else//由于读入的顺序,数一定先于操作来处理.
        {
            int tmp=ask(q[i].b)-ask(q[i].a-1);//用树状数组统计当前有多少小于mid的
            if(q[i].k<=tmp)//如果小于tmp个,就代表答案在[l,mid]
                q1[++cnt1]=q[i];
            else
                q2[++cnt2]=q[i],//否则在[mid+1,r]
                q2[cnt2].k-=tmp;//此时应减去cmp
        }
    for(int i=1;i<=cnt1;i++){//还原树状数组
        if(q1[i].type==2)
            break;
        add(q1[i].id,-1);
    }
    for(int i=1;i<=cnt1;i++)//赋值回原数组,类似于归并排序
        q[i+ql-1]=q1[i];
    for(int i=1;i<=cnt2;i++)
        q[i+ql+cnt1-1]=q2[i];
    solve(ql,ql+cnt1-1,l,mid);//递归下去直到确定答案
    solve(ql+cnt1,qr,mid+1,r);
    return;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&q[++cnt].a),//读入序列
        q[cnt].id=i,//储存下标
        q[cnt].type=1;//代表这是一个数
    for(int i=1;i<=m;i++)
        scanf("%d",&q[++cnt].a),//离线读入所有操作
        scanf("%d",&q[cnt].b),
        scanf("%d",&q[cnt].k),
        q[cnt].id=i,//储存下标
        q[cnt].type=2;//代表这是一个操作
    solve(1,cnt,-oo,oo);
    for(int i=1;i<=m;i++)
        cout << answer[i],
        printf("\n");
    return 0;
}

动态区间K大

相比于静态,动态只需要加一点操作,就是当扫到修改的时候,如果修改后的值\(\le mid\),才能放到左区间,而且要先执行以下免得后面的判断不符合事实.

其他问题[POI2011]MET-Meteors

在该问题中,我们首先要做的是断环成链,使得问题变成序列问题.操作区间右端点小于左端点时,将右端点+m即可.

这次我们二分的答案是每个国家最早什么时候收集到足够的陨石.因此我们二分出答案mid后,要让\([l,mid]\)的陨石雨都落下来(也就是在树状数组上上传),然后按照哪些国家收集足够了为标准,对询问分类即可.值得注意的是,因为一个国家的空间站可能是分散在环上的,所以我们可以通过连边的方式来统一.同时因为是区间加,所以树状数组应维护差分数组.

在判断某个国家是否被满足的时候,应当扫过它的每个空间站\(j\),然后将\(sum(j)\)与\(sum(j+m)\)相加来作为判断它有没有收集足够的标准.为什么呢?因为每场流星雨代表数列上一个点加\(a\),一个点减去\(a\),如果左端点小于右端点,那么\(sum(j+m)\)在后半段,一定是\(0\),此时答案是\(sum(j)\).如果左端点大于右端点,那么右端点在后半段,左右端点相差不到\(m\).那么如果\(j\)在左端点右边,\(j+m\)一定在右端点右边,统计同上一种情况.如果\(j\)在左端点左边,\(j+m\)要么在右端点右边,\(sum(j)=sum(j+m)\),符合这次陨石雨没有影响到这个空间站的情况,要么在右端点左边,此时\(sum(j)\)一定位0,答案是\(sum(j+m)\).

#include <cmath>
#include <queue>
#include <deque>
#include <cctype>
#include <string>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define N 600005
#define int long long
using namespace std;

template<class T> inline void read(T &x) {
    x = 0;
    char ch = getchar(), w = 0;
    while (!isdigit(ch))
        w = (ch =='-'), ch = getchar();
    while (isdigit(ch))
        x = (x << 1) + (x << 3) + (ch ^ 48), ch = getchar();
    x = w ? -x : x;
    return;
}

struct node {
    int head, ind, tot;
}sta[N], stal[N], star[N];

struct NODE {
    int l, r, a;
}eve[N];

int n, m, noe, k;
int nxt[N], to[N];
int bit[N], ans[N];

inline int lowbit(int x) {
    return -x & x;
}

inline void add(int x, int v) {
    while (x <= m + m)
        bit[x] += v, x += lowbit(x);
    return;
}

inline int sum(int x) {
    int ret = 0;
    while (x)
        ret += bit[x], x -= lowbit(x);
    return ret;
}

inline void addedge(int from, int t) {
    nxt[++noe] = sta[from].head;
    to[noe] = t;
    sta[from].head = noe;
    return;
}

void solve(int l, int r, int ql, int qr) {
    if (l > r) return;
    if (l == r) {
        for (int i = ql; i <= qr; ++i)
            ans[sta[i].ind] = l;
        return;
    }
    int mid = l + r >> 1, tl = 0, tr = 0;
    for (int i = l; i <= mid; ++i) {
        add(eve[i].l, eve[i].a);
        add(eve[i].r + 1, -eve[i].a);
    }
    for (int i = ql; i <= qr; ++i) {
        int temp = 0;
        for (int j = sta[i].head; j && temp <= sta[i].tot; j = nxt[j])
            temp += sum(to[j] + m) + sum(to[j]);
        if (temp >= sta[i].tot)
            stal[++tl] = sta[i];
        else
            star[++tr] = sta[i],
            star[tr].tot -= temp;
    }
    for (int i = l; i <= mid; ++i) {
        add(eve[i].l, -eve[i].a);
        add(eve[i].r + 1, eve[i].a);
    }
    for (int i = 1; i <= tl; ++i)
        sta[ql + i - 1] = stal[i];
    for (int i = 1; i <= tr; ++i)
        sta[ql + tl + i - 1] = star[i];
    solve(l, mid, ql, ql + tl - 1),
    solve(mid + 1, r, ql + tl, qr);
    return;
}

signed main() {
    read(n), read(m);
    for (int i = 1, x; i <= m; ++i)
        read(x), addedge(x, i);
    for (int i = 1; i <= n; ++i)
        read(sta[i].tot), sta[i].ind = i;
    read(k);
    for (int i = 1; i <= k; ++i) {
        read(eve[i].l), read(eve[i].r), read(eve[i].a);
        if (eve[i].r < eve[i].l)
            eve[i].r += m;
    }
    solve(1, k + 1, 1, n);
    for (int i = 1; i <= n; ++i)
        if (ans[i] > k)
            printf("NIE\n");
        else
            printf("%d\n", ans[i]);
    return 0;
}

感谢@SWK@Tian-Xing两位dalao的帮助和陪伴

原文地址:https://www.cnblogs.com/i-cookie/p/11623071.html

时间: 2024-10-13 17:47:58

整体二分学习的相关文章

【cdq分治】cdq分治与整体二分学习笔记Part1.整体二分

之所以把cdq分治和整体二分放在一起学习,是因为他们两个实在太像了-不管是做法还是代码- 感觉整体二分可能会比cdq分治稍微简单那么一点点?所以先学整体二分.(感觉他们的区别在于整体二分是对每个操作二分答案,cdq是分治了操作序列) 整体二分是对答案进行二分,其具体操作如下: (比如以ZJOJ2013K大数查询为例) 具体过程 Step1.从(L,R)二分答案.mid=(L+R)>>1,用线段树维护原序列中(a,b)位置比mid大的数有多少个,同时记录对序列的操作分别是什么操作. Step2.

[整体二分]【学习笔记】【更新中】

先小结一下吧 主要为个人理解 整体二分 理解 $zyz:$整体二分是在权值上进行$CDQ$分治 我觉得更像是说$:$整体二分是在答案上进行$CDQ$分治 整体二分是二分答案在数据结构题上的扩展 因为数据结构题二分的答案通常是第几个操作之后,需要进行一些操作(预处理)之后才能判断,所以每次询问二分还不如从前往后暴力找 整体二分可以解决这样的问题 核心就是维护一个$cur$数组保存当前的影响,分治到$[l,r]$时只需要计算$[l,mid]$的影响再与$cur$里的合并就好了 这样分治里的操作就只与

CDQ分治与整体二分总结

Cdq分治和整体二分是两个很奇妙的东西.他们都是通过离线的思想来进行优化,从而更快的求出解. 整体二分通俗的讲就是二分答案,但是它了不起的地方是一下子把所有的答案都二分出来了,从而可以一下子得出所有查询. CDQ分治通俗的讲就是二分查询.通常的做法是把所有的查询分成两半,然后通过递归先计算出左边一半的所有的查询,然后通过这些已知的左半边的值来更新右半边的值.这里,最最重要的思想是通过左半边来更新右半边.更具体一点,就是用左半边的修改来更新右半边的查询. 重要的事情说话三遍: CDQ分治就是通过左

HDU 5412 CRB and Queries (整体二分)

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=5412 题目大意:对一个数组A[N](N<= 10^5)进行两种操作: 1 l v:将第A[l]的值修改成v 2 l r k:求l~r区间的第k小值 操作总数为10^5. 2015年多校第十场的题目,当时乱套主席树什么的模板,发现并不能过,赛后学习了一种叫做整体二分的方法,感觉很是厉害. 整体二分是二分答案.大致方法如下: 1.先把所有对数组的操作保存起来,包括赋值. 2.从0~inf二分一个数值,并

[Poi2011] Meteors(从不知所措到整体二分)

Byteotian Interstellar Union (BIU) has recently discovered a new planet in a nearby galaxy. The planet is unsuitable for colonisation due to strange meteor showers, which on the other hand make it an exceptionally interesting object of study. The mem

整体二分浅谈

整体二分浅谈 一.前置知识 在学习整体二分之前,要学会二分,以及二分的分治思想. 二.整体二分浅谈及例题 例题:bzoj2527: [Poi2011]Meteors 对于这道题是整体二分的经典例题,我们先抛开整体二分,思考二分怎么做.对于一个询问,因为答案有单调性,如果$x$时刻为最小可以时刻,则比$x$小的时刻都不可以,比$x$大的时刻都可以,所以我们可以进行二分答案,并加以验证.先不说怎样验证,就单是时间复杂度就不能接受,$O(nmlog_2^n)$. 如果一个一个进行二分时间复杂度不允许,

CDQ分治与整体二分小结

前言 这是一波强行总结. 下面是一波瞎比比. 这几天做了几道CDQ/整体二分,感觉自己做题速度好慢啊. 很多很显然的东西都看不出来 分治分不出来 打不出来 调不对 上午下午晚上的效率完全不一样啊. 完蛋.jpg 绝望.jpg. 关于CDQ分治 CDQ分治,求的是三维偏序问题都知道的. 求法呢,就是在分治外面先把一维变成有序 然后分治下去,左边(l,mid)关于右边(mid+1,r)就不存在某一维的逆序了,所以只有两维偏序了. 这个时候来一波"树状数组求逆序对"的操作搞一下二维偏序 就可

BZOJ 3110: [Zjoi2013]K大数查询 [整体二分]

有N个位置,M个操作.操作有两种,每次操作如果是1 a b c的形式表示在第a个位置到第b个位置,每个位置加入一个数c如果是2 a b c形式,表示询问从第a个位置到第b个位置,第C大的数是多少. N,M<=50000,N,M<=50000a<=b<=N1操作中abs(c)<=N2操作中c<=Maxlongint 之前用树套树抄过一次...然而我并不适合写那玩意儿... 加上时间序的整体二分 普通的整体二分先处理了所有$[l,mid]$的影响因子在计算询问的答案来分组

bzoj1146整体二分+树链剖分+树状数组

其实也没啥好说的 用树状数组可以O(logn)的查询 套一层整体二分就可以做到O(nlngn) 最后用树链剖分让序列上树 1 #include<cstdio> 2 #include<cstring> 3 #include<iostream> 4 #include<algorithm> 5 using namespace std; 6 inline int read() 7 { 8 int x=0,f=1,ch=getchar(); 9 while(ch<