Stargazer的分治讲义

cyk的分治讲义

分治讲义


请在开始前先确保您已经会了如下知识
1、数组
2、递归函数
3、线段树,树状数组
4、小学的代数知识

由于cykcyk实在是tcltcl,所以难免讲义可能存在问题
如果有请指出来大力批判我,thanksthanks~


一、一般分治

参考论文:20132013年国家集训队xhrxhr,20142014年国家集训队xyz,2016年国家集训队lzzlzz

主定理(Master Theorem):

T(n)=2T(n/2)+O(kn)>T(n)=O(knlogn)T(n)=2T(n/2)+O(kn)->T(n)=O(knlogn)

证明?
考虑递归的每层规模实际上都是O(nk)O(nk)
而每次nn规模减少一半,总共lognlogn

1、序列分治

一般询问满足某种条件的点对数量
考虑2个点之间的路径
则我们可以在中间一个点统计答案

void solve(int l,int r){
    int mid=((l+r)>>1);
    solve(l,mid),solve(mid+1,r)
    count(l,r);
    if(l==r)return;
}

先来看一道简单题
bzoj4979bzoj4979凌晨三点的宿舍
给定一个序列,每个位置一个值hih_i
定义22个位置(i,j),i<j(i,j),i<j的距离为ji+hi+hj2min(h[k],k[i,j])j-i+h_i+h_j-2*min(h[k],kin[i,j])
求有多少对节点的距离小于等于pp
n2e5n le2e5


记得校内考过一道类似的题
考虑分治

对于当前区间l,mid,rl,mid,r
mnimn_i表示iimidmid的最小值
求有多少i[l,mid],j[mid+1,r]iin[l,mid],jin[mid+1,r]满足
ji+hi+hj2min(mni,mnj)kj-i+h_i+h_j-2*min(mn_i,mn_j)le k

假设此时min=mnimin=mn_i,另一种会在相反的i,ji,j统计到
hii2mnikjhjh_i-i-2*mn_ile k-j-h_j
对每个ii插入树状数组后对jj查询就是了
复杂度O(nlog2n)O(nlog^2n)


2、整体二分

整体二分可以相当于普通二分的进化版
考虑普通二分一般是处理多个修改,单个查询
复杂度O(f(a)logc)O(f(a)*logc)cc是值域,ff是单次操作复杂度

那如果有多个询问变成O(Qf(a)logc)O(Q*f(a)*logc)就炸了
而整体二分可以做到O((Q+f(a))logc)O((Q+f(a))*logc)的复杂度
可以在一定情况下替代一些复杂数据结构
比如树套树之类的
而且时间常数还十分优秀(雾)

比如询问区间第kk大(不考虑主席树)
如果只有一次询问
我们显然可以二分答案,判断区间是否有k个大于midmid的数
虽然一次十分不优
如果有多次询问呢?
发现二分过程中有许多次信息是重复的
怎么连带统计完对所有询问的影响
这时就可以上整体二分了

算法流程

void(二分下界l,二分上界r,队首,队尾){
  if(队列空)return;
  if(l==r)队列内的所有询问答案为l,return ;
  int mid = 上下界中值;
  扫描队列,处理询问与修改值小于mid的修改操作(树状数组)。则询问结果存的是(l, m)内数的个数;
  还原树状数组。
  扫描队列:
    若是修改操作,根据修改答案的大小丢进q1与q2;
    若是询问操作,if 询问答案+当前贡献 >= 所需答案k 丢入q1;
           else 更新询问答案,丢入q2;
  solve(l,mid,q1);
  solve(mid+1,r,q2);
}

例题:WOJ2802WOJ2802
支持修改,查询区间第kk
树套树?
考虑整体二分
把初始数作为修改
假设当前处理qlql~qrqr之间的操作,数值在ll ~rr
二分一个数值midmid
每遍历一个操作
如果是修改且修改的值小于midmid,则加入树状数组,加入q1q1
否则加入q2q2
如果是询问,则查询区间内满足查询区间的数numnum
如果num>knum>k,则加入q1q1
否则knumk-num加入q2q2

核心代码

void solve(int l,int r,int st,int des){
	if(l>r||st>des)return;
	if(l==r){
		for(int i=st;i<=des;i++)if(q[i].op)ans[q[i].pos]=l;
		return;
	}
	int mid=(l+r)>>1,cnt1=0,cnt2=0;
	for(int i=st;i<=des;i++){
		if(q[i].op){
			int tmp=query(q[i].r)-query(q[i].l-1);
			if(tmp>=q[i].k)q1[++cnt1]=q[i];
			else q[i].k-=tmp,q2[++cnt2]=q[i];
		}
		else{
			if(q[i].l<=mid){
				q1[++cnt1]=q[i],add(q[i].pos,q[i].val);
			}
			else q2[++cnt2]=q[i]; 
		}
	}
    for(int i=1;i<=cnt1;i++) if(!q1[i].op) add(q1[i].pos, -q1[i].val);
    for(int i=1;i<=cnt1;i++) q[st+i-1]=q1[i];
    for(int i=1;i<=cnt2;i++) q[st+cnt1+i-1]=q2[i];
    solve(l, mid, st, st+cnt1-1); solve(mid+1, r, st+cnt1, des);
}

再来一道
WOJ1776WOJ1776

支持区间每个数之间插入一个数,询问区间第kk
和上一道差不多吧,单点修改变成区间修改就可以了
用线段树维护


3、CDQ分治

前置

关于二/三维数点

二维数点是一个常见的模型
一般解决方法有:
主席树:带修//在线nlog2nnlog^2n,否则nlognnlogn(要离散化)
二维树状数组(值域很小)//线段树(动态开点,常数超大):nlog2nnlog^2n(在线)
cdqcdq分治:nlog2nnlog^2n(离线)
kdtree:kd-tree:我不会

而如果上升到三维时,一般就用kdtreekd-tree或者cdqcdq


引入

逆序对&gt;-&gt;Lis&gt;-&gt;归并//Bit&gt;-&gt;cdq做&gt;-&gt;cdq


进入正题

cdqcdq分治最早是在陈丹琦的集训队作业中,大致思想是将操作区间分成2半,统计左边对右边的贡献,并继续递归求解

大致代码如下:

void cdq(int l,int r){
    if(l==r)return;
    cdq(l,mid),cdq(mid+1,r);
    calc(effect:l~mid->mid+1~r);
}

一般在计算贡献的时候会用数据结构来维护
并在这次统计完后消除影响(不是直接memset)

举个栗子:
对序列单点加,区间求和(woj1684woj1684
我们可以用cdqcdq来做
将区间加变成两个前缀加
考虑先将左右区间分别排序
那我们就只需要用一个双指针统计前半段修改对后半段询问的贡献
左右区间分别排序可以在分治左右区间的时候归并实现

void cdq(int l,int r){
	if(l==r)return;
	cdq(l,mid),cdq(mid+1,r);
	int cnt1=l,cnt2=mid+1;
	ll res=0;
	for(int i=l;i<=r;i++){
		if((cnt1<=mid&&q1[cnt1].l<q1[cnt2].l)||cnt2>r){
			if(q1[cnt1].op==1)res+=q1[cnt1].val;	
			q2[i]=q1[cnt1++];
		}
		else{
			if(q1[cnt2].op==3)ans[q1[cnt2].pos]+=res;
			if(q1[cnt2].op==2)ans[q1[cnt2].pos]-=res;
			q2[i]=q1[cnt2++];
		}
	}
	for(int i=l;i<=r;i++)q1[i]=q2[i];
}

复杂度O(nlogn)O(nlogn)
虽然并没有什么区别,速度慢一点,空间还要大几倍


大家应该都会了吧
来一道简单的例题:

boi2007Mokiaboi2007-Mokia
给定一个2e62e62e6*2e6大小的棋盘
支持单点加,矩形求和
复杂度要求O(nlog2n)O(nlog^2n)以内

怎么做?
离散化后二维线段树?
TLE+MLETLE+MLE
二维mapmap维护BITBIT
常数过大,莫名ReRe(亲身体验)
二进制分组套主席树

考虑cdqcdq分治
定义操作solve(l,r)solve(l,r)处理(l,mid)(l,mid)之间的修改对(mid+1,r)(mid+1,r)的询问的影响
考虑将(l,mid),(mid+1,r)(l,mid),(mid+1,r)分别按照xx排序
显然我们可以用双指针来保证xx的有序
那我们相当于只用对于一个查询查找y&lt;askyy&lt;ask_y的个数
一个BitBit就搞定了

inline void cdq(int l,int r){
	if(l==r)return;
	cdq(l,mid),cdq(mid+1,r);
	sort(a+l,a+mid+1,compl),sort(a+mid+1,a+r+1,compl);
	int i=l;
	for(int j=mid+1;j<=r;j++){
		for(;i<=mid&&a[i].l<=a[j].l;i++)
			if(a[i].op==1)update(a[i].r,a[i].val);
		if(a[j].op==2)ans[a[j].pos]+=query(a[j].r);
	}
	for(int j=l;j<i;j++)if(a[j].op==1)update(a[j].r,-a[j].pos);
}

再来一道:陌上菊开

题意:
nn个人,每个人有3个属性a,b,ca,b,c
定义一个人iijj强当且仅当ai&gt;aj,bi&gt;bj,ci&gt;cja_i&gt;a_j,b_i&gt;b_j,c_i&gt;c_j
对每个人求他比多少个人强


关于归并排序优化一个log/log/减少常数


cdqcdq维护动态凸包


cdqcdq优化dpdp

我们发现cdqcdq其实就是通过分割区间计算左边对右边的贡献
本质就是按照时间将动态的操作变成静态的
那对于一些递推的dpdp(一般是斜率dpdp),也可以来优化转移


NOI2007CashNOI2007-Cash

有AB两种货币,第ii天可以花一定的钱,买到AA券和BB券,且A:B=RateiA:B=Rate_i,也可以卖掉OPOP%的AA券和BB券,每天ABAB价值为AiA_iBiB_i

开始有S元,n天后手中不能有AB券,问最大获益。n1e5nle 1e5

考虑令f[i]f[i]为第ii天得到的最多的AA券,g[i]g[i]为第ii天得到的最多的BB
g[i]=f[i]/rate[i]g[i]=f[i]/rate[i]
那显然有个n2n^2dpdp,暴力枚举前面每一天转移
怎么优化
考虑如果前面i,ji,j两天对于决策nownow的影响,如果iijj优的话
(f[i]f[j])a[now](g[i]g[j])b[now]&gt;0(f[i]-f[j])*a[now]-(g[i]-g[j])*b[now]&gt;0
g[i]g[j]f[i]f[j]&lt;a[now]b[now]frac{g[i]-g[j]}{f[i]-f[j]}&lt;-frac{a[now]}{b[now]}
注意特判一个f[i]=f[j]f[i]=f[j]的情况
如果将(f[i],g[i])(f[i],g[i])作为平面上的点,那也就是我们维护一个上凸壳
则对于每一个nownow,我们找到最后一个满足相邻2点斜率小于a[now]b[now]-frac{a[now]}{b[now]}的点,并把这个点更新当前节点的答案
但我们发现这个凸包在不断变化
考虑splay动态维护 cdqcdq分治维护凸包
具体的我们可以使nownow保持有序
就可以一边扫凸包一边更新了
cdqcdq分治中途把凸包和a[now]b[now]-frac{a[now]}{b[now]}顺带归并排序
复杂度O(nlogn)O(nlogn)


更深入的讨论

前提:忽略KDTreeKD-Tree(虽然应该也没人会……),后面我会讲的

例1:nn个人,每个人有33种能力a,b,ca,b,c
定义一个人ii比另一个jj有能力当且仅当aj&lt;ai,bj&lt;bi,cj&lt;cia_j&lt;a_i,b_j&lt;b_i,c_j&lt;c_i
对每个人求出他比多少人有能力(n1e5)(nle 1e5)

树套树?二维BitBit?

cdqcdq分治
首先按照aa排序,把(bi,ci)(b_i,c_i)视为二维坐标
就变成了单点加,区间求和
和上一道类似,O(nlog2n)O(nlog^2n)


例2:nn个人,每个人有44种能力a,b,c,da,b,c,d
定义一个人ii,比另一个人jj有能力当且仅当
aj&lt;ai,bj&lt;bi,cj&lt;ci,dj&lt;dia_j&lt;a_i,b_j&lt;b_i,c_j&lt;c_i,d_j&lt;d_i
对每个人求出他比多少个人有能力

树套树套树?三维mapmap维护BitBit

cdqcdqcdqcdq
可以先对aa排序
考虑当前cdq(l,r)cdq(l,r)
将左边和右边双指针排序后记录一下每个原来是在(l,mid)(mid+1,r)(l,mid)还是(mid+1,r)
那现在已经保证a,ba,b有序了
我们在内层的cdqcdq可以继续套一个双指针
保证当前cc有序
将在当前序列左边求在原来的(l,mid)(l,mid)一边的以dd为下标记录
右边在原来的(mid+1,r)(mid+1,r)的用树状数组查询就可以了

复杂度O(nlog3n)O(nlog^3n)

是不是cdqcdq其实就是用一个loglog的代价保证了一维的有序
但我们发现一个问题:每多一维我们都要花费一个loglog的代价来稳定这一维
但是暴力只需要多1的常数
考虑55维偏序
cdqcdqcdq:nlog4ncdq套cdq套cdq:nlog^4n,暴力5n25*n^2
n=2e4n=2e4时2种的效率已经几乎没有区别了(实际运行效率)


考虑有没有别的做法?
当然有
引进一个东西-bitsetbitset
基本原理是用每32位连续的数字压成一个intint(似乎是这样吧)
相当于一堆0/10/1
而且可以做类似集合一样取交集,并集(&amp;,)(&amp;,|)之类的
查询0/1的个数之类的
常数是一般运算的132frac 1{32}(似乎有人说集合的操作是size/64size/64?)
某些题目就可以用bitsetbitset做到n2n^2过十万(比如woj4302woj4302
那考虑这个怎么用bitsetbitset?
我们可以分别对于每一维求出比当前一个数小的元素集合
那是不是取一个交集就是答案了?
复杂度大约是O(n2/64)O(n^2/64)?

然而当你写完这个程序兴致勃勃交上去,mlemle愉快
bitset不要空间?
怎么办?考虑分块
将每维每nsqrt n个分一组,建一个bitsetbitset
每次暴力跳nsqrt n次得到当前答案集合
复杂度O(能过) O(kqn)?O(k*qsqrt n)?
详见woj3230woj3230

当然如果你觉得bitsetbitset慢也可以手写
参见某位dalao的blog


4、二进制分组

一般在2种情况下会用到:
1、要求强制在线(否则cdqcdq呗)
2、支持将信息合并(也就是说如果要(a,b)(a,b)的答案,可以通过(a,k],(k,b)(a,k],(k,b)信息合并在log(log)以内左右的时间得到)

二进制分组也是一个奇♂妙的东西
考虑到某些数据结构在解决某些问题的时候是不能修改的
比如主席树解决区间第kk大,如果带一个修改就完全不可做了
又或者主席树可以解决二维数点(实际上二维数点和区间第k大是类似的模型),
但是带加点/修改操作就不可做了
而二进制分组则可以以一个loglog的代价将动态操作变成静态的


考虑将当前操作数拆成二的幂次从大到小的和的形式
比如:
21=16+4+1{ }21=16+4+1
22=16+4+222=16+4+2
23=16+4+2+123=16+4+2+1
24=16+824=16+8
这有什么用的?
不如对修改的操作分组按照这样分组
那我们对于每一组分别用数据结构来维护,查询在每一组分别查询就是了

考虑新加入一个修改操作
如果在二进制下可以发现如果已经有“当前位”的数就会向前进一位
那对于新的修改操作处理就很简单了

void insert(now){
    build(now),siz[++top]=1;
    while(siz[top]==siz[top-1]){
        merge(top,top-1),siz[top-1]+=siz[top],top--;
    }
}

考虑时空复杂度证明:
对于合并,显然任何一次建出的数据结构都只会被合并O(logn)O(logn)20,21,22......(2^0,2^1,2^2......)
实际上仔细分析会发现
添加第ii个元素的时候,合并的总元素个数是lowbit(i)lowbit(i),也就是ii在二进制位下最低一位的权值
那复杂度就是
i=1mO(f(lowbit(i)))sum_{i=1}^{m}O(f(lowbit(i)))
=i=1logmn2i+1+0.5f(2i)i=1logmO(f(n))=sum_{i=1}^{logm}lfloor {frac {n}{2^{i+1}}+0.5} floor *f(2^i)le sum_{i=1}^{logm}O(f(n))
=O(f(n)logn)=O(f(n)logn)

考虑询问,显然同时存在的组数是不会超过loglog
那总共就只有不到loglog次询问

因此通过二进制分组,我们以一个loglog的代价将动态操作变成了静态


例题:
二维数点,支持加点
如果没有加点操作我们显然可以主席树nlognnlogn解决(怎么做),或者cdq/bitcdq/二维bit做到nlog2nlog^2也不赖

考虑对加点操作二进制分一下组
那就变成了平面有一堆点,然后单纯的二维数点了

复杂度O(nlog2n)O(nlog^2n)


既然可以二进制分组,那有没有三进制分组?四进制分组之类的?
显然可以发现随着进制kk的增大,我们合并的操作会越来越少
但相应的,一次询问的复杂度会上升
在对于一些特殊的题目(比如什么98%98\%都是修改之类的)
也许就可以用到更高的分组(雾)

练手题:CF710F String Set Queries

5、线段树分治

大致思想就是把修改建成一颗线段树
然后考虑询问,考虑如果左区间的修改对该询问有影响
就将这次询问加入队列递归询问左区间
右区间对该询问有影响类似

和二进制分组其实很类似
只是将每次的合并状态都记录着

想象一下线段树的结构

而且一般的二进制分组只能解决全局的询问,面对某些只询问一些修改的时候就难以为力了
比如woj4408woj4408
而这时候就有一种补救方法
用类似于线段树一样的结构,每次向上pushuppushup存储一下当前状态
每次多一个修改就是填一个节点

(与二进制分组的时空复杂度区别)

那如果加一个撤销某次修改的操作呢?
例题UOJ#191UnknownUOJ# 191-Unknown

这些更深入的就不讲了,因为我也不会 估计没人想听了
可以参见2016年lzzlzz国家集训队论文

练手题:woj4299woj4299,woj4408woj4408

难♂题:UR#14UR#14 思考熊(最短4k+4k+

咕咕咕
咕咕咕

二、树上分治

既然序列可以通过分治处理重复信息做到nlognnlogn
那显然我们也可以通过同样的方法提到树上来做

6、点分治

序列上可以通过midmid来保证复杂度每次减半
树上呢?
树的重心:如果一个点满足其所有的子树中最大的子树节点数最少,那么这个点就是这棵树的重心

因为我们可以发现这样可以保证去掉这个点之后形成的森林的点数最为平均

可以证明:每次去掉重心后树的规模减少12frac 1 2
这很显然,否则我们重心肯定能走到这颗更大的树内

为什么要点而不是边?
也有边分治,但是特殊情况较多
在菊花图上还要特殊处理
而且最主要的是
我们发现去掉一个点之后会出现多个子树,但是去掉一条边却只会出现2颗子树

那由主定理可得点分治复杂度为O(f(n)logn)O(f(n)logn)
事实上我们可以发现这个loglog很小
而在一条链上时loglog达到上界

例题:POJ1741POJ1741:求树上距离kle k的点对个数
直接枚举显然是O(n2)O(n^2)
考虑点分治
对于当前分治中心
我们只需要统计经过了当前分治中心的路径个数(没经过的肯定会在其他分治中心被统计)
对于每一个子树dfsdfs出当前子树所有点到中心的距离
假设我们已经得到了前面所有子树到当前中心的深度
考虑新加入一个点,那是不是前面所有和这个点距离和小于等于kk的点都有贡献
那将每次子树的距离排个序,双指针统计就可以了
复杂度T(n)=ST(ns)+O(nlogn)&gt;T(n)=O(nlog2n)T(n)=S*T(frac n s)+O(nlogn)-&gt;T(n)=O(nlog^2n)

在一些比较特殊的情况中,我们是没办法直接对于子树分别统计的
而需要将所有信息放在一块处理
但比如说统计路径的时候,会出现同一颗子树中2个点拼在一起的不合法的情况
这时候就需要再分别递归子树求出对于单独一颗子树的答案减去
就是这样

void calc(int u,int dep,int f){
	ans+=f*query(u,dep);
}
void solve(int u){
	calc(u,0,1);
	for(v->son[u]){
		calc(v,1,-1);
		solve(v);
	}
}

Codechef:Prime Distance On TreeCodechef:Prime Distance On Tree
求树上所有距离为质数的路径个数
考虑只能处理cnt[i]cnt[i]表示距离为ii的点的个数,枚举统计
复杂度是O(nlognnum)=O(n2)O(nlogn*num)=O(n^2)不如暴力
似乎不好统计了

考虑处理出cnt[i]cnt[i]表示到当前中心距离为ii的点的个数
ans=j=primeijcnt[i]cnt[ji]=j=primef[j]ans=sum_{j=prime}sum_{i}^{j}cnt[i]*cnt[j-i]=sum_{j=prime}f[j]
发现这是一个卷积的形式,可以fftfft优化得到每一个ff,再枚举质数统计答案
复杂度O(nlog2n)O(nlog^2n)

IOI2011IOI2011RaceRace

关于操作树上点分(论文)

7、边分治

暂时咕咕

8、动态点分治

+++++++码量++,调试难度+++++

9、链分治

三、根号类算法

20142014国集lzylzy,2013ljq2013ljq,2017xmk2017xmk,2015zxy2015zxy

1、分块

1.一般分块

2.树上分块

3.均值法

4.重构块

2、莫队

1.普通莫队

2.带修莫队

3.树上莫队

3、莫队与分块结合

四、有关的数据结构

1、KD-Tree

练习题:

一、分治

1、整体二分

1、woj2802woj2802 动态区间第k大

传送门

2、woj1776woj1776 K大数查询

传送门

3、woj4034woj4034 Meteors

传送门

4、woj2885woj2885 接水果

关键词: 扫描线
传送门

5、woj3532woj3532 混合果汁

关键词: 线段树
传送门

6、woj4402woj4402 Attack

关键词: 整体二分套cdqcdq

2、CDQCDQ分治

1、woj3964woj3964 Mokia

传送门

2、woj3101woj3101 陌上花开

传送门

3、woj4326woj4326 数列

传送门

4、woj2063woj2063Cash

传送门

5、woj4403woj4403四维偏序

传送门

6、woj3059woj3059天使玩偶

传送门

7、woj3607woj3607动态逆序对

传送门

8、woj3230woj3230五维偏序
9、woj2018woj2018城市建设

传送门
cdqcdq分治+最小生成树好题

10、woj4426woj4426共点圆

传送门

11、BZOJ4237稻草人
12、WOJ2257拦截导弹

二、树上分治

1、点分治

1、poj1741
2、BZOJ2152
3、BZOJ1316
4、洛谷P4149
5、Codechef

关键词:点分治+fftfft

6、BZOJ1758

关键词:点分治+分数规划

原文地址:https://www.cnblogs.com/stargazer-cyk/p/11145549.html