CDQ分治
CDQ分治
总体来讲,学过CDQ分治的人会说这个算法是“偏序的魔术师”。
CDQ分治是cdq发明的算法,它的前身是“分治求逆序对”。
换句话说,整个算法来自于“归并排序”的扩展,就是在归并排序的基础上多做了一点而已。
求逆序对
点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define pa pair<ll,int>
#define u64 unsigned long long
using namespace std;
mt19937_64 mrand(random_device{}());
const int maxn=5e6+10101;
const int MOD=998244353;
const int inf=2147483647;
const double pi=acos(-1);
const double eps=1e-12;
int read(){
int x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
int n,a[maxn],t[maxn];
ll ans;
void solve(int l,int r){
if(l==r)return ;
int mid=(l+r)>>1;
solve(l,mid);solve(mid+1,r);
int rc=mid,tot=0;
for(int i=l;i<=mid;i++){
while(rc<r && a[rc+1]<a[i])t[++tot]=a[++rc];
ans+=(ll)(rc-mid);
t[++tot]=a[i];
}
while(rc<r)t[++tot]=a[++rc];
for(int i=l;i<=r;i++)a[i]=t[i-l+1];
return ;
}
int main(){
n=read();
for(int i=1;i<=n;i++)a[i]=read();
solve(1,n);
printf("%lld",ans);
return 0;
}
类似偏序的dp
[NOIP1999]拦截导弹
其实就是对右边进行状态转移
由于是做动态规划,所以还必须注意状态转移的阶段性。
最长上升子序列的其中一种划分阶段的方法是按照下标递增的顺序进行划分求解。
在处理上,借助“中序遍历”的思路就可以一遍分治,一遍满足状态转移的阶段性。
为什么要用中序遍历?
因为就像上图,我们按照后序遍历,在第三层上从5会更新到4,但是在第二层8或7又有可能更新到5,而5却不能重新更新到4
但是“中序遍历”又不能满足归并排序(归并排序必须是后序遍历)。所以在实际处理上先进行一遍朴素的归并排序并记录归并过程,然后模拟归并(已经排好了,不需要真的再排一遍),采取“中序遍历”的递归模式,在过程中进行状态转移。
点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define pa pair<ll,int>
#define u64 unsigned long long
using namespace std;
mt19937_64 mrand(random_device{}());
const int maxn=2e5+10101;
const int MOD=998244353;
const int inf=2147483647;
const double pi=acos(-1);
const double eps=1e-12;
int read(){
int x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
int n,a[maxn],ans;
pair<int,int> tmp[maxn][21];
void getsolve(int l,int r,int deep,int flag){
//flag=-1 对序列进行降序归并,flag=1对序列进行升序归并排序
if(l==r){
tmp[l][deep].first=a[l];
tmp[l][deep].second=l;
return ;
}
int mid=(l+r)>>1;
getsolve(l,mid,deep-1,flag);getsolve(mid+1,r,deep-1,flag);
int rc=mid,tot=l-1;
for(int i=l;i<=mid;i++){
while(rc<r && (tmp[rc+1][deep-1].first-tmp[i][deep-1].first)*flag<0)tmp[++tot][deep]=tmp[++rc][deep-1];
tmp[++tot][deep]=tmp[i][deep-1];
}
while(rc<r)tmp[++tot][deep]=tmp[++rc][deep-1];
return ;
}
int dp[maxn];
void dosolve(int l,int r,int deep,int flag){
if(l==r){
dp[l]=max(dp[l],1);
ans=max(ans,dp[l]);
return ;
}
int mid=(l+r)>>1;
dosolve(l,mid,deep-1,flag);
int lr=l-1,now=-1;
for(int i=mid+1;i<=r;i++){
if(flag==-1){
while(lr<mid && tmp[lr+1][deep-1].first>=tmp[i][deep-1].first)lr++,now=max(now,dp[tmp[lr][deep-1].second]);
}
else while(lr<mid && tmp[lr+1][deep-1].first<tmp[i][deep-1].first)lr++,now=max(now,dp[tmp[lr][deep-1].second]);
dp[tmp[i][deep-1].second]=max(dp[tmp[i][deep-1].second],now+1);
ans=max(ans,dp[tmp[i][deep-1].second]);
}
dosolve(mid+1,r,deep-1,flag);
return ;
}
void solve(int pos){
ans=0; memset(dp,0,sizeof(dp));
getsolve(1,n,18,pos);
dosolve(1,n,18,pos);
printf("%d\n",ans);
return ;
}
int main(){
while(scanf("%d",&a[++n])!=EOF){}
n--;
solve(-1); //-1是第一问
solve(1); //1是第二问
return 0;
}
当然这道题条件比较少,可以用别dp来做
对于其他类似的,但条件较多,cdq就比较好用
用cdq做dp问题要注意遍历顺序,一般中序遍历
注意
使用CDQ处理复杂结构体时尽量不要封装,频繁拷贝可能会导致大常数。
最好要类似如下写法,排序的时候交换id
借鉴CDQ分治