Stargazer的分治讲义
文章目录
分治讲义
请在开始前先确保您已经会了如下知识
1、数组
2、递归函数
3、线段树,树状数组
4、小学的代数知识
由于实在是,所以难免讲义可能存在问题
如果有请指出来大力批判我,~
一、一般分治
参考论文:年国家集训队,年国家集训队xyz,2016年国家集训队
主定理(Master Theorem):
证明?
考虑递归的每层规模实际上都是的
而每次规模减少一半,总共层
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;
}
先来看一道简单题
凌晨三点的宿舍
给定一个序列,每个位置一个值
定义个位置的距离为
求有多少对节点的距离小于等于
记得校内考过一道类似的题
考虑分治
对于当前区间
令表示到的最小值
求有多少满足
假设此时,另一种会在相反的统计到
则
对每个插入树状数组后对查询就是了
复杂度
2、整体二分
整体二分可以相当于普通二分的进化版
考虑普通二分一般是处理多个修改,单个查询
复杂度,是值域,是单次操作复杂度
那如果有多个询问变成就炸了
而整体二分可以做到的复杂度
可以在一定情况下替代一些复杂数据结构
比如树套树之类的
而且时间常数还十分优秀(雾)
比如询问区间第大(不考虑主席树)
如果只有一次询问
我们显然可以二分答案,判断区间是否有k个大于的数
虽然一次十分不优
如果有多次询问呢?
发现二分过程中有许多次信息是重复的
怎么连带统计完对所有询问的影响
这时就可以上整体二分了
算法流程
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);
}
例题:
支持修改,查询区间第大
树套树?
考虑整体二分
把初始数作为修改
假设当前处理~之间的操作,数值在 ~
二分一个数值
每遍历一个操作
如果是修改且修改的值小于,则加入树状数组,加入
否则加入
如果是询问,则查询区间内满足查询区间的数
如果,则加入
否则加入
核心代码
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);
}
再来一道
支持区间每个数之间插入一个数,询问区间第大
和上一道差不多吧,单点修改变成区间修改就可以了
用线段树维护
3、CDQ分治
前置
关于二/三维数点
二维数点是一个常见的模型
一般解决方法有:
主席树:带修在线,否则(要离散化)
二维树状数组(值域很小)线段树(动态开点,常数超大):(在线)
分治:(离线)
我不会
而如果上升到三维时,一般就用或者了
引入
逆序对Lis归并Bitcdq做cdq
进入正题
分治最早是在陈丹琦的集训队作业中,大致思想是将操作区间分成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)
举个栗子:
对序列单点加,区间求和()
我们可以用来做
将区间加变成两个前缀加
考虑先将左右区间分别排序
那我们就只需要用一个双指针统计前半段修改对后半段询问的贡献
左右区间分别排序可以在分治左右区间的时候归并实现
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];
}
复杂度
虽然并没有什么区别,速度慢一点,空间还要大几倍
大家应该都会了吧
来一道简单的例题:
:
给定一个大小的棋盘
支持单点加,矩形求和
复杂度要求
怎么做?
离散化后二维线段树?
二维维护?
常数过大,莫名(亲身体验)
二进制分组套主席树
考虑分治
定义操作处理之间的修改对的询问的影响
考虑将按照排序
显然我们可以用双指针来保证的有序
那我们相当于只用对于一个查询查找的个数
一个就搞定了
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);
}
再来一道:陌上菊开
题意:
有个人,每个人有3个属性
定义一个人比强当且仅当
对每个人求他比多少个人强
关于归并排序优化一个减少常数
维护动态凸包
优化
我们发现其实就是通过分割区间计算左边对右边的贡献
本质就是按照时间将动态的操作变成静态的
那对于一些递推的(一般是斜率),也可以来优化转移
有AB两种货币,第天可以花一定的钱,买到券和券,且,也可以卖掉%的券和券,每天价值为和。
开始有S元,n天后手中不能有AB券,问最大获益。
考虑令为第天得到的最多的券,为第天得到的最多的券
则
那显然有个的,暴力枚举前面每一天转移
怎么优化
考虑如果前面两天对于决策的影响,如果比优的话
则
即
注意特判一个的情况
如果将作为平面上的点,那也就是我们维护一个上凸壳
则对于每一个,我们找到最后一个满足相邻2点斜率小于的点,并把这个点更新当前节点的答案
但我们发现这个凸包在不断变化
考虑splay动态维护 分治维护凸包
具体的我们可以使保持有序
就可以一边扫凸包一边更新了
分治中途把凸包和顺带归并排序
复杂度
更深入的讨论
前提:忽略(虽然应该也没人会……),后面我会讲的
例1: 有个人,每个人有种能力
定义一个人比另一个有能力当且仅当
对每个人求出他比多少人有能力
树套树?二维?
分治
首先按照排序,把视为二维坐标
就变成了单点加,区间求和
和上一道类似,
例2: 有个人,每个人有种能力
定义一个人,比另一个人有能力当且仅当
对每个人求出他比多少个人有能力
树套树套树?三维维护?
套
可以先对排序
考虑当前
将左边和右边双指针排序后记录一下每个原来是在
那现在已经保证有序了
我们在内层的可以继续套一个双指针
保证当前有序
将在当前序列左边求在原来的一边的以为下标记录
右边在原来的的用树状数组查询就可以了
复杂度
是不是其实就是用一个的代价保证了一维的有序
但我们发现一个问题:每多一维我们都要花费一个的代价来稳定这一维
但是暴力只需要多1的常数
考虑维偏序
,暴力
时2种的效率已经几乎没有区别了(实际运行效率)
考虑有没有别的做法?
当然有
引进一个东西-
基本原理是用每32位连续的数字压成一个(似乎是这样吧)
相当于一堆,
而且可以做类似集合一样取交集,并集之类的
查询0/1的个数之类的
常数是一般运算的(似乎有人说集合的操作是?)
某些题目就可以用做到过十万(比如)
那考虑这个怎么用?
我们可以分别对于每一维求出比当前一个数小的元素集合
那是不是取一个交集就是答案了?
复杂度大约是?
然而当你写完这个程序兴致勃勃交上去,愉快
bitset不要空间?
怎么办?考虑分块
将每维每个分一组,建一个
每次暴力跳次得到当前答案集合
复杂度O(能过)
详见
当然如果你觉得慢也可以手写
参见某位dalao的blog
4、二进制分组
一般在2种情况下会用到:
1、要求强制在线(否则呗)
2、支持将信息合并(也就是说如果要的答案,可以通过信息合并在以内左右的时间得到)
二进制分组也是一个奇♂妙的东西
考虑到某些数据结构在解决某些问题的时候是不能修改的
比如主席树解决区间第大,如果带一个修改就完全不可做了
又或者主席树可以解决二维数点(实际上二维数点和区间第k大是类似的模型),
但是带加点/修改操作就不可做了
而二进制分组则可以以一个的代价将动态操作变成静态的
考虑将当前操作数拆成二的幂次从大到小的和的形式
比如:
这有什么用的?
不如对修改的操作分组按照这样分组
那我们对于每一组分别用数据结构来维护,查询在每一组分别查询就是了
考虑新加入一个修改操作
如果在二进制下可以发现如果已经有“当前位”的数就会向前进一位
那对于新的修改操作处理就很简单了
void insert(now){
build(now),siz[++top]=1;
while(siz[top]==siz[top-1]){
merge(top,top-1),siz[top-1]+=siz[top],top--;
}
}
考虑时空复杂度证明:
对于合并,显然任何一次建出的数据结构都只会被合并次
实际上仔细分析会发现
添加第个元素的时候,合并的总元素个数是,也就是在二进制位下最低一位的权值
那复杂度就是
考虑询问,显然同时存在的组数是不会超过的
那总共就只有不到次询问
因此通过二进制分组,我们以一个的代价将动态操作变成了静态
例题:
二维数点,支持加点
如果没有加点操作我们显然可以主席树解决(怎么做),或者做到也不赖
考虑对加点操作二进制分一下组
那就变成了平面有一堆点,然后单纯的二维数点了
复杂度
既然可以二进制分组,那有没有三进制分组?四进制分组之类的?
显然可以发现随着进制的增大,我们合并的操作会越来越少
但相应的,一次询问的复杂度会上升
在对于一些特殊的题目(比如什么都是修改之类的)
也许就可以用到更高的分组(雾)
5、线段树分治
大致思想就是把修改建成一颗线段树
然后考虑询问,考虑如果左区间的修改对该询问有影响
就将这次询问加入队列递归询问左区间
右区间对该询问有影响类似
和二进制分组其实很类似
只是将每次的合并状态都记录着
想象一下线段树的结构
而且一般的二进制分组只能解决全局的询问,面对某些只询问一些修改的时候就难以为力了
比如
而这时候就有一种补救方法
用类似于线段树一样的结构,每次向上存储一下当前状态
每次多一个修改就是填一个节点
(与二进制分组的时空复杂度区别)
那如果加一个撤销某次修改的操作呢?
例题
这些更深入的就不讲了,因为我也不会 估计没人想听了
可以参见2016年国家集训队论文
难♂题: 思考熊(最短)
咕咕咕
咕咕咕
二、树上分治
既然序列可以通过分治处理重复信息做到
那显然我们也可以通过同样的方法提到树上来做
6、点分治
序列上可以通过来保证复杂度每次减半
树上呢?
树的重心:如果一个点满足其所有的子树中最大的子树节点数最少,那么这个点就是这棵树的重心
因为我们可以发现这样可以保证去掉这个点之后形成的森林的点数最为平均
可以证明:每次去掉重心后树的规模减少
这很显然,否则我们重心肯定能走到这颗更大的树内
为什么要点而不是边?
也有边分治,但是特殊情况较多
在菊花图上还要特殊处理
而且最主要的是
我们发现去掉一个点之后会出现多个子树,但是去掉一条边却只会出现2颗子树
那由主定理可得点分治复杂度为
事实上我们可以发现这个很小
而在一条链上时达到上界
例题::求树上距离的点对个数
直接枚举显然是的
考虑点分治
对于当前分治中心
我们只需要统计经过了当前分治中心的路径个数(没经过的肯定会在其他分治中心被统计)
对于每一个子树出当前子树所有点到中心的距离
假设我们已经得到了前面所有子树到当前中心的深度
考虑新加入一个点,那是不是前面所有和这个点距离和小于等于的点都有贡献
那将每次子树的距离排个序,双指针统计就可以了
复杂度
在一些比较特殊的情况中,我们是没办法直接对于子树分别统计的
而需要将所有信息放在一块处理
但比如说统计路径的时候,会出现同一颗子树中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);
}
}
求树上所有距离为质数的路径个数
考虑只能处理表示距离为的点的个数,枚举统计
复杂度是不如暴力
似乎不好统计了
考虑处理出表示到当前中心距离为的点的个数
发现这是一个卷积的形式,可以优化得到每一个,再枚举质数统计答案
复杂度
—
关于操作树上点分(论文)
7、边分治
暂时咕咕
8、动态点分治
9、链分治
三、根号类算法
国集,,,
1、分块
1.一般分块
2.树上分块
3.均值法
4.重构块
2、莫队
1.普通莫队
2.带修莫队
3.树上莫队
3、莫队与分块结合
四、有关的数据结构
1、KD-Tree
练习题:
一、分治
1、整体二分
1、 动态区间第k大
2、 K大数查询
3、 Meteors
4、 接水果
关键词: 扫描线
传送门
5、 混合果汁
关键词: 线段树
传送门
6、 Attack
关键词: 整体二分套
2、分治
1、 Mokia
2、 陌上花开
3、 数列
4、Cash
5、四维偏序
6、天使玩偶
7、动态逆序对
8、五维偏序
9、城市建设
传送门
分治+最小生成树好题
10、共点圆
11、BZOJ4237稻草人
12、WOJ2257拦截导弹
二、树上分治
1、点分治
1、poj1741
2、BZOJ2152
3、BZOJ1316
4、洛谷P4149
5、Codechef
关键词:点分治+
6、BZOJ1758
关键词:点分治+分数规划