【笔记】分治算法
来自\(\texttt{SharpnessV}\)的省选复习计划中的分治算法。
分治,顾名思义,分而治之,一般能将 \(N^2\) 的时间复杂度优化至 \(N \log N\) 或\(N\log^2 N\)。
P7415 [USACO21FEB] Count the Cows G
观察一个对于边长为\(3^n\)的正方形,拆分成 \(9\) 块。
1 0 1
0 1 0
1 0 1
其中 \(1\) 表示边长为 \(3^{n-1}\) 的正方形,\(0\) 表示全零正方形。
我们发现这构成递归结构,我们讨论一下对角线过这个正方形的哪些块即可。
时间复杂度\(\rm O(Q\log^2_{3}\ d_i )\)。
整体二分
如果一次询问,我们可以二分在\(\rm O(N\log N)\)的时间内解决,但是有多次询问,就可以考虑整体二分。
整体二分就是把所有的询问放到一起二分。
主要思路是设计递归solve(l,r,L,R)
,表示在\([l,r]\)之间的询问,答案在\([L,R]\)中。
每次查找\([L,R]\)的中点\(mid\),\(\rm check\)一下\([l,r]\)之间的答案,如果满足条件放到左边,否则放到右边,递归下去处理。
显然每递归一次答案的区间减半,所以最多递归 \(\log T\) 层,每一层有 \(N\) 个询问需要\(\rm check\),如果\(\rm check\)的时间复杂度是\(f(N)\),则整个算法的时间复杂度为\(\rm O(Nf(N)\log T)\)。(\(\rm T\)为值域,下同)
这道题,我们可以二分天数,然后将所有询问塞到一起\(\rm check\),就可以做到\(\rm O(N\log ^2N)\)的时间复杂度。
二维矩阵第\(K\)小。
我们都知道区间第\(K\)小可持久化线段树是最优的,但是放到矩阵上时间和空间复杂度就不可接受了。
区间第\(K\)小还可以整体二分,考虑扩展到矩阵上。
考虑二分当前答案,将\(\le mid\)的数在对应的位置\(+1\),对于每个查询只需要判断矩阵和是否\(\ge k\)即可,这可以用二维树状数组维护。
时间复杂度\(\rm O((N^2+Q)\log T\log^2 N)\)。
区间带修第\(K\)小。
考虑扩展可持久化线段树,需要支持带修,只用树状数组套可持久化线段树即可,时间和空间复杂度都是\(\rm O(N\log^2N)\),空间比较卡。
考虑整体二分,我们发现修改操作,在二分时对应的是单点加减,仍然可以树状数组维护,稍加修改模板即可。
时间复杂度仍然是\(\rm O(N\log^2 N)\),空间复杂度优化为线性。
#include<bits/stdc++.h>
using namespace std;
int n,m,u[100005];
struct node{
int x,y,z,op;
}q[500005];
int T=0,tot=0,b[200005],t,o[200005],pt,c[200005],ans[200005];
void add(int x,int val){for(;x<=pt;x+=x&-x)c[x]+=val;}
int ask(int x){
int sum=0;
for(;x;x-=x&-x)sum+=c[x];
return sum;
}
node ls[500005],rs[500005];
void solve(int l,int r,int st,int ed){
if(st>ed)return;
if(l==r){
for(int i=st;i<=ed;i++)if(q[i].op)ans[q[i].op]=l;
return;
}
int mid=(l+r)>>1,lt=0,rt=0;
for(int i=st;i<=ed;i++){
if(!q[i].op){
if(q[i].y<=mid){
add(q[i].x,q[i].z);
ls[++lt]=q[i];
}
else rs[++rt]=q[i];
}
else{
int sum=ask(q[i].y)-ask(q[i].x-1);
if(sum>=q[i].z)ls[++lt]=q[i];
else{
rs[++rt]=q[i];
rs[rt].z-=sum;
}
}
}
for(int i=st;i<=ed;i++)if(!q[i].op&&q[i].y<=mid)add(q[i].x,-q[i].z);
for(int i=1;i<=lt;i++)q[st+i-1]=ls[i];
for(int i=1;i<=rt;i++)q[st+lt+i-1]=rs[i];
solve(l,mid,st,st+lt-1);solve(mid+1,r,st+lt,ed);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&u[i]),o[++t]=u[i];
for(int i=1;i<=n;i++)q[++T].op=0,q[T].x=i,q[T].y=u[i],q[T].z=1;
for(int i=1;i<=m;i++){
char opt[2];int x,y,z;
scanf("%s%d%d",opt,&x,&y);
if(opt[0]=='C'){
o[++t]=y;
q[++T].op=0;q[T].x=x;q[T].y=u[x];q[T].z=-1;
q[++T].op=0;q[T].x=x;q[T].y=y;q[T].z=1;u[x]=y;
}
else{
scanf("%d",&z);
q[++T].op=++tot;q[T].x=x;q[T].y=y;q[T].z=z;
}
}
sort(o+1,o+t+1);
for(int i=1;i<=t;i++)
if(i==1||o[i]^o[i-1])b[++pt]=o[i];
for(int i=1;i<=T;i++)
if(!q[i].op)q[i].y=lower_bound(b+1,b+pt+1,q[i].y)-b;
solve(1,pt,1,T);
for(int i=1;i<=tot;i++)printf("%d\n",b[ans[i]]);
return 0;
}
直接树剖整体二分可以做到\(\rm O(N\log^3 N)\)。
考虑优化,修改操作是单点修改,则需要查询链上和,而查询链上和并不好求。
先差分一下,链上查询可以拆成四个点到根的路径。
考虑转换一下,对树上一点修改,只会影响到它的子树中的点到根的路径。而子树修改可以用\(\rm DFS\)序转化为一个区间,这样就可以做到\(\rm O(N\log^2N)\)的时间复杂度。
转换后也可以用树状数组套可持久化线段树,本质上没有区别。
单次询问二分可做,考虑整体二分。
二分未被影响的请求中重要度的最大值,重要度小于当前值的路径\(+1\),然后单点查询判断是否所有询问都被影响。
三只\(\log\)碾过去。。。
发现只有链上加和单点查询,延续上一题的讨论,链上加改为单点加,单点查询转化为子树查询。
给定若干路径,每次查询一条路径的所有子路径中第\(k\)小的权值。
二分答案,我们只有判断一个路径有多少子路径。
反过来,需要求一个路径可以成为哪些路径的子路径。
不难发现能够包含当前路径的路径,一定是两端在当前路径两段的子树内,对应一个平面上的矩形,直接扫描线即可。
第一眼发现求集合并集非常不可做。
然后发现是可重集,所以是区间修改第\(k\)小模板,整体二分碾过去。
\(\rm CDQ\)分治
二维偏序就是我们所熟知的逆序对。可以用归并排序解决。
观察一下归并排序,发现合并过程中,左边一半的第一维一定小于右边的第一维,所以我们只用考虑第二维的限制,问题极大的简化了。
\(\rm CDQ\)分治就是基于这样的思想,我们先将所有点按第一维从小到大排序,则合并左右两段时,左边第一维一定小于右边第一维。
我们考虑第二维限制,第二维类似于归并排序,在合并的过程中满足第二维限制。
第三维,用树状数组维护,左边的点在树状数组中插入,右边的点查询前缀和。
时间复杂度\(\rm O(N\log^2 N)\)。
没有修改就是二维偏序。
存在修改,我们可以再增加一个维度,表示时间轴,这就变成了三维偏序模板,套用\(\rm CDQ\)分治即可。
对于每个点,分别考虑左上,左下,右上,右下的最近点。
支持修改后,我们加入时间轴,对于每个点,需要计算时间在它前面,且在它左下角的最近点,这就是三维偏序模板。
四个方向,我们只用将所有点每次旋转\(90\)°即可。
支持插入二维数点,三维偏序模板。
我们设\(f[i]\)表示以第\(i\)个导弹结束最多能拦截的导弹个数。
如果\(f[j]\to f[i]\),则需要满足\(j<i,h_j\ge h_i,v_j\ge v_i\)。
我们发现这和三维偏序很像,但是这是\(\rm DP\)。
所以我们用\(\rm CDQ\)分治优化\(\rm DP\),每次先递归计算\([l,mid]\),然后用\([l,mid]\)更新\([mid+1,r]\),最后递归计算\([mid+1,r]\)。
注意本题还要求一个点出现的方案数,需要正反两遍分治。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 50005
using namespace std;
int n,o[N],b[N],T,f[N],u[N];double g[N];
struct node{
int tm,h,v;
}a[N];
bool cmp(node x,node y){return x.tm<y.tm;}
bool cmp1(node x,node y){return x.h>y.h;}
bool cmp2(node x,node y){return x.h<y.h;}
int c[N];double d[N];
inline void add(int x,int p,double q){
for(;x<=T;x+=x&-x){
if(c[x]==p)d[x]+=q;
else if(c[x]<p)c[x]=p,d[x]=q;
}
}
inline void clear(int x){for(;x<=T;x+=x&-x)c[x]=0xcfcfcfcf,d[x]=0;}
typedef pair<int,double> Pr;
inline Pr ask(int x){
Pr now;now.first=0xcfcfcfcf;now.second=0;
for(;x;x-=x&-x)if(c[x]>now.first)now=make_pair(c[x],d[x]);else if(c[x]==now.first)now.second+=d[x];
return now;
}
void solve(int l,int r){
if(l==r){
if(!f[l])f[l]=1,g[l]=1;
else f[l]++;
return;
}
int mid=(l+r)>>1;
solve(l,mid);
sort(a+mid+1,a+r+1,cmp1);
int j=l;
rep(i,mid+1,r){
while(j<=mid&&a[j].h>=a[i].h)add(T-a[j].v+1,f[a[j].tm],g[a[j].tm]),j++;
Pr cur=ask(T-a[i].v+1);
if(cur.first>f[a[i].tm])f[a[i].tm]=cur.first,g[a[i].tm]=cur.second;
else if(cur.first==f[a[i].tm])g[a[i].tm]+=cur.second;
}
while(j>l)j--,clear(T-a[j].v+1);
sort(a+mid+1,a+r+1,cmp);
solve(mid+1,r);
sort(a+l,a+r+1,cmp1);
}
int f_[N];double g_[N];
void calc(int l,int r){
if(l==r){
if(!f_[l])f_[l]=1,g_[l]=1;
else f_[l]++;
return;
}
int mid=(l+r)>>1;
calc(mid+1,r);
sort(a+l,a+mid+1,cmp2);
int j=mid+1;
rep(i,l,mid){
while(j<=r&&a[j].h<=a[i].h)add(a[j].v,f_[a[j].tm],g_[a[j].tm]),j++;
Pr cur=ask(a[i].v);
if(cur.first>f_[a[i].tm])f_[a[i].tm]=cur.first,g_[a[i].tm]=cur.second;
else if(cur.first==f_[a[i].tm])g_[a[i].tm]+=cur.second;
}
while(j>mid+1)j--,clear(a[j].v);
sort(a+l,a+mid+1,cmp);
calc(l,mid);
sort(a+l,a+r+1,cmp2);
}
int main(){
memset(c,0xcf,sizeof(c));
scanf("%d",&n);
rep(i,1,n)a[i].tm=i,scanf("%d%d",&a[i].h,&a[i].v),o[i]=a[i].v;
sort(o+1,o+n+1);
rep(i,1,n)if(o[i]!=o[i-1])b[++T]=o[i];
rep(i,1,n)a[i].v=lower_bound(b+1,b+T+1,a[i].v)-b;
solve(1,n);sort(a+1,a+n+1,cmp);calc(1,n);
int mx=0;double sum=0;
rep(i,1,n)mx=max(mx,f[i]);
rep(i,1,n)if(f[i]==mx)sum+=g[i];
printf("%d\n",mx);
rep(i,1,n)if(f[i]+f_[i]-1==mx)printf("%lf ",1.00*g[i]*g_[i]/sum);else printf("0 ");
return 0;
}