「数据结构进阶」例题之离线分治算法

0x40「数据结构进阶」例题

CDQ分治

CDQ分治,能够将动态问题转化为静态问题求解。它将操作的时间顺序作为分治的基础,每次递归操作的两部分,回溯时计算前一半的操作对后一半的询问的影响。在实际过程中,它往往用于解决二维平面的动态偏序问题,因而要与排序和树状数组结合。

例题

4701 天使玩偶
计算距离的过程中涉及到了绝对值,为了去掉绝对值符号,我们分四类讨论,即最优解位于询问点的左下,左上,右上,右下四个方向的情况。设目前的询问点为(x_{i},y_{i}),那么dist=\min_{1\leq i\leq n}\left\{|x-x_{i}|+|y-y_{i}| \right\},四个方向上该式的化简如下:
左下:即x_{i}\leq x,y_{i}\leq y
dist=\min_{1\leq i\leq n}\left\{(x-x_{i})+(y-y_{i}) \right\}=(x+y)-\max_{1\leq i\leq n}\left\{x_{i}+y_{i} \right\},其中\;\;x_{i}\leq x,y_{i}\leq y
左上:即x_{i}\leq x,y_{i}\geq y
dist=\min_{1\leq i\leq n}\left\{(x-x_{i})+(y_{i}-y) \right\}=(x-y)-\max_{1\leq i\leq n}\left\{x_{i}-y_{i} \right\},其中\;\;x_{i}\leq x,y_{i}\geq y
右上:即x_{i}\geq x,y_{i}\geq y
dist=\min_{1\leq i\leq n}\left\{(x-x_{i})+(y-y_{i}) \right\}=-(x+y)-\max_{1\leq i\leq n}\left\{-x_{i}-y_{i} \right\},其中\;\;x_{i}\geq x,y_{i}\geq y
右下:即x_{i}\leq x,y_{i}\geq y
dist=\min_{1\leq i\leq n}\left\{(x-x_{i})+(y_{i}-y) \right\}=-x+y-\max_{1\leq i\leq n}\left\{-x_{i}+y_{i} \right\},其中\;\;x_{i}\geq x,y_{i}\leq y
化简原则是将x,y(常量)和x_{i},y_{i}(变量)分开,同时保证四个式子尽量相似(max函数前都为负号)。
因此,我们可以这样做(以左下为例):
1 将区间内操作对应的点的坐标按照横坐标升序排序。
2 扫描到一个点(x_{i},y_{i}),就在树状数组中把y_{i}个位置和x_{i}+y_{i}取max,同时维护前缀和的最大值(注意,树状数组虽然无法维护区间最值,但是当所有区间的起点都为1时,由于不需要用到区间加法,所以树状数组可以胜任)。
3 扫描到一个询问(x,y),就在树状数组中查询[1,y]上的最大值val,答案就是x+y-val。
上述做法中,排序满足了x_{i}\leq x,树状数组控制了y_{i}\leq y并维护了x_{i}+y_{i}的最大值。
另外三个方向同理,例如左上方,只要按照横坐标升序排序,然后树状数组维护x_{i}-y_{i}的最大值,修改位置为10^{6}-y_{i},查询时前缀变成[1,10^{6}-y]即可。(10^{6}也可以是\max_{1\leq i\leq n}y_{i}+1
时间复杂度:O((n+m)log^{2}(n+m))
PS:为了保证时间复杂度与询问区间长度有关,我们每次不能建立新的树状数组,而是在每次修改后,都将操作依次撤销。
代码如下

/*

*/
#define method_1
#ifdef method_1
/*

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<"  "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=1000000+5;
const int INF=0x3f3f3f3f;
struct node1{
    int x,y,op;
}a[maxn];
struct node2{
    int x,y,id;
}b[maxn];
int n,m,tot=-INF,c[maxn],ans[maxn];
bool operator<(const node2 &h,const node2 &w){
    return h.x<w.x||h.x==w.x&&h.y<w.y;
}
void add(int x,int y){
    for(int i=x;i<tot;i+=i&-i) c[i]=max(c[i],y);
}
int ask(int x){
    int ans=-INF;
    for(int i=x;i>=1;i-=i&-i) ans=max(c[i],ans);
    return ans;
}
void cal(int st,int ed,int de,int dx,int dy){
    for(int i=st;i!=ed;i+=de){
        int y=dy==1?b[i].y:tot-b[i].y;
        int temp=dx*b[i].x+dy*b[i].y;
        if(a[b[i].id].op==1) add(y,temp);
        else ans[b[i].id]=min(ans[b[i].id],temp-ask(y));
    }
    for(int i=st;i!=ed;i+=de){
        int y=dy==1?b[i].y:tot-b[i].y;
        if(a[b[i].id].op==1){
            for(int j=y;j<tot;j+=j&-j) c[j]=-INF;
        }
    }
}
void cdq(int l,int r){
    int mid=l+r>>1;
    if(l<mid) cdq(l,mid);
    if(mid+1<r) cdq(mid+1,r);
    int t=0;
    for(int i=l;i<=r;i++){
        if((a[i].op==1&&i<=mid)||(a[i].op==2&&i>mid)){
            b[++t].x=a[i].x;
            b[t].y=a[i].y;
            b[t].id=i;
        }
    }
    sort(b+1,b+t+1);
    cal(1,t+1,1,1,1);//左下
    cal(1,t+1,1,1,-1);//左上
    cal(t,0,-1,-1,1);//右下 这两行里头,xi>=x所以要倒序插入xi 
    cal(t,0,-1,-1,-1);//右上
}
int main() {
    ios::sync_with_stdio(false);
//  freopen("天使玩偶.in","r",stdin);
    cin>>n>>m;
    m+=n;
    for(int i=1;i<=n;i++){
        cin>>a[i].x>>a[i].y;
    //  a[i].y++;
        a[i].op=1;
        tot=max(a[i].y,tot);
    }
    for(int i=n+1;i<=m;i++){
        cin>>a[i].op>>a[i].x>>a[i].y;
    //  a[i].y++;
        tot=max(a[i].y,tot);
    }
    tot++;
    memset(c,0xcf,sizeof(c));
    memset(ans,INF,sizeof(ans));
    cdq(1,m);//二分范围[1,m] 
    for(int i=n+1;i<=m;i++){
        if(a[i].op==2){
            cout<<ans[i]<<endl;
        }
    }
    return 0;
}
#endif
#ifdef method_2
/*

*/

#endif
#ifdef method_3
/*

*/

#endif

另外,这里附上CDQ分治的另外两道例题(洛谷 P3810 P4390)

基于值域的整体二分

例题

POJ2104 K-th Number
这是一道模板题,可以用四种方法AC。
这里介绍树状数组实现的整体二分做法。
将询问序列作为三元组(l_{i},r{i},k_{i})表示[l_{i},r{i}]k_{i}小的数。设solve(L,R,1,t)表示值域为[L,R]的序列,对询问序列1~t作出回答。
步骤如下:
1 mid=L+R>>1
2 利用树状数组,对于当前询问序列,统计数字序列在[l_{i},r{i}]中小于等于mid的数有多少个,记为c_{i}
3 若k_{i}\leq c_{i},则将询问加入序列lq中,否则令k_{i}-c_{i},将询问加入序列rq中。
4 将lq和rq拷贝回原来的数组
5 还原树状数组
6 递归solve(L,mid,1,?)和solve(mid+1,R,?+1,t)
递归边界:L=R时,将L作为目前询问序列中所有询问的答案。
未离散化时间复杂度:O((n+m)lognlogSIZE)(SIZE为值域大小)。
离散化时间复杂度:O((n+m)log^{2}n)
代码如下

/*

*/
#define method_1
#ifdef method_1
/*

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<"  "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=2e5+5;
const int INF=0x3f3f3f3f;
struct node{
    int id,x,y,z;
}q[maxn],lq[maxn],rq[maxn];
int n,m,t=0,c[maxn],ans[maxn];
void add(int x,int y){
    for(int i=x;i<=n;i+=i&-i) c[i]+=y;
}
int ask(int x){
    int sum=0;
    for(int i=x;i>=1;i-=i&-i) sum+=c[i];
    return sum;
}
void solve(int lval,int rval,int st,int ed){
    if(st>ed) return;
    if(lval==rval){
        for(int i=st;i<=ed;i++){
            if(q[i].id>0) ans[q[i].id]=lval;
        }
        return;
    }
    int mid=lval+rval>>1;
    int lt=0,rt=0;
    for(int i=st;i<=ed;i++){
        if(q[i].id==0){
            if(q[i].y<=mid) add(q[i].x,1),lq[++lt]=q[i];
            else rq[++rt]=q[i]; 
        }
        else{
            int cnt=ask(q[i].y)-ask(q[i].x-1);
            if(cnt>=q[i].z) lq[++lt]=q[i];
            else q[i].z-=cnt,rq[++rt]=q[i];
        }
    }
    for(int i=st;i<=ed;i++){
        if(q[i].id==0&&q[i].y<=mid) add(q[i].x,-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(lval,mid,st,st+lt-1);
    solve(mid+1,rval,st+lt,ed);
}
int main() {
    ios::sync_with_stdio(false);
//  freopen("K-th Number.in","r",stdin);
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        q[++t].id=0;
        q[t].x=i;
        cin>>q[t].y;
    }
    for(int i=1;i<=m;i++){
        q[++t].id=i;
        cin>>q[t].x>>q[t].y>>q[t].z;
    }
    solve(-INF,INF,1,t);
    for(int i=1;i<=m;i++) cout<<ans[i]<<endl;
    return 0;
}
#endif
#ifdef method_2
/*

*/

#endif
#ifdef method_3
/*

*/

#endif

另外,若序列存在带修改操作(如A[x]=y),那么做如下转化:
将这个操作转化为在x位置去掉一个值为A[x]的数(记该类操作为-1),增加一个值为y的数(记该类操作为0)两次操作。然后将所有操作包括询问进行整体二分即可。(例题 BZOJ 1901/ZOJ 2112)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容