前言:
说实话我对于这种没有固定板子,变化多端的算法实在是非常头疼的
但是不学不行,这也是一种很重要的思伟方式
所以趁着这几天的心情比较好(快要放大周),赶紧学一波~
鸣谢:tham,stdcall
CDQ分治,传说中是一个♀神犇创造的算法
在了解这种算法之前,我们有必要了解一下一种基本的思想:分治
分治的一个经典例子就是归并求逆序对
简单叙述一下算法:
利用归并把序列对半分,在一般的归并过程中,我们在前后两部分各设两个指针,按照大小顺序合并成一个序列
我们要做的就是在这个过程中记录逆序对个数
什么情况下会有逆序对呢?
无非是在前面的数比在后面的数大(翻译过来:在前半部分的数比在后半部分的数大)
设前半部分的指针为t1,后半部分的指针为t2
如果a[t1] > a[t2],那么t1~mid的元素一定都比t2大,一定都可以与t2形成逆序对
#include
#include
#include
using namespace std;
const int N=100010;
int a[N],b[N],n,ans=0;
void merge(int l,int r)
{
if (l==r) return;
int mid=(l+r)>>1;
merge(l,mid);
merge(mid+1,r);
int t1=l;
int t2=mid+1;
for (int i=l;i<=r;i++)
{
if ((t1<=mid&&a[t1]<=a[t2])||t2>r) {
b[i]=a[t1];
t1++;
}
else
{
b[i]=a[t2];
ans+=(mid-t1+1);
t2++;
}
}
for (int i=l;i<=r;i++) a[i]=b[i];
}
int main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++) scanf("%d",&a[i]);
merge(1,n);
printf("%d",ans);
return 0;
}
我为什么要介绍这个呢?
因为归并就是一个最简单的分治问题
我们在合并两个子区间的时候,要考虑到左边区间的对右边区间的影响
即,我们每次从右边区间的有序序列中取出一个元素的时候,要把“以这个元素结尾的逆序对的个数”(左边区间有多少个元素比他大)
这是一个典型的CDQ分治的过程
CDQ分治是我们处理各类问题的重要武器
它的优势在于可以顶替复杂的高级数据结构,而且常数比较小;
缺点在于必须离线操作
上面介绍了归并求逆序对的经典问题,我们由此引入二维偏序问题:
给定N个有序对(a,b),求对于每个(a,b),满足a0 < a且b0 < b的有序对(a0,b0)有多少个
在归并求逆序对的时候,实际上每个元素是用一个有序对(a,b)表示的,
其中a表示数组中的位置,b表示该位置对应的值
我们求的就是“对于每个有序对(a,b),有多少个有序对(a0,b0)满足a0 < a且b0 > b”,这就是一个二维偏序问题
注意到在求逆序对的问题中,a元素是默认有序的,即我们拿到元素的时候,数组中的元素是默认从第一个到最后一个按顺序排列的,所以我们才能在合并子问题的时候忽略a元素带来的影响
因为我们在合并两个子问题的过程中,左边区间的元素一定出现在右边区间的元素之前,即左边区间的元素的a都小于右边区间元素的a
那么对于二维偏序问题,我们在拿到所有有序对(a,b)的时候,先把a元素从小到大排序
这时候问题就变成了“求顺序对”,因为a元素已经有序,可以忽略a元素带来的影响,和“求逆序对”的问题是一样的。
考虑二维偏序问题的另一种解法,用树状数组代替CDQ分治,即常用的用树状数组求顺序对
在按照a元素排序之后,我们对于整个序列从左到右扫描,每次扫描到一个有序对,求出“扫描过的有序对中,有多少个有序对的b值小于当前b值”
然而当b的值非常大的时候,空间和时间上就会吃不消,便可以用CDQ分治代替,就是我们所说的“顶替复杂的高级数据结构”
给定一个N个元素的序列a,初始值全部为0,对这个序列进行以下两种操作
操作1:格式1 x k,把位置x的元素加上k
操作2:格式为2 x y,求出区间[x,y]内所有元素的和
这是一个经典的树状数组问题
但是我们就是要没事找事,我们用CDQ分治解决它——带修改和询问的问题
我们把ta转化成一个二维偏序问题,每个操作用一个有序对(a,b)表示,其中a表示操作的时间,b表示操作的位置,时间是默认有序的,所以我们在合并子问题的过程中,就按照b从小到大的顺序合并。
首先我们把原数列和1操作都看作是修改操作
询问操作[l,r]我们拆成两个:l-1,r
因为我们询问的是一个区间和,一般的思路就是前缀和相减(我们需要具备这样的思维)
实际上我们这道题也可以这样,我们按照时间顺序进行修改
记录前缀和,当遇到l-1的标记时,我们减去sum(l-1)
遇到r标记时,询问的处理就完成了
需要注意的是:
#include
#include
#include
#define ll long long
using namespace std;
const int N=5000010;
int n,m,totx=0,tot=0; //totx是操作的个数,tot询问的编号
struct node{
int type,id;
ll val;
bool operator < (const node &a) const //重载运算符,优先时间排序
{
if (id!=a.id) return idelse return typevoid CDQ(int L,int R)
{
if (L==R) return;
int M=(L+R)>>1;
CDQ(L,M);
CDQ(M+1,R);
int t1=L,t2=M+1;
ll sum=0;
for (int i=L;i<=R;i++)
{
if ((t1<=M&&A[t1]R) //只修改左边区间内的修改值
{
if (A[t1].type==1) sum+=A[t1].val; //sum是修改的总值
B[i]=A[t1++];
}
else //只统计右边区间内的查询结果
{
if (A[t2].type==3) ans[A[t2].val]+=sum;
else if (A[t2].type==2) ans[A[t2].val]-=sum;
B[i]=A[t2++];
}
}
for (int i=L;i<=R;i++) A[i]=B[i];
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++)
{
tot++;
A[tot].type=1; A[tot].id=i; //修改操作
scanf("%lld",&A[tot].val);
}
for (int i=1;i<=m;i++)
{
int t;
scanf("%d",&t);
tot++;
A[tot].type=t;
if (t==1)
scanf("%d%lld",&A[tot].id,&A[tot].val);
else
{
int l,r;
scanf("%d%d",&l,&r);
totx++;
A[tot].val=totx; A[tot].id=l-1; //询问的前一个位置
tot++; A[tot].type=3; A[tot].val=totx; A[tot].id=r; //询问的后端点
}
}
CDQ(1,tot);
for (int i=1;i<=totx;i++) printf("%lld\n",ans[i]);
return 0;
}
给定N个有序三元组(a,b,c),求对于每个三元组(a,b,c),有多少个三元组(a0,b0,c0)满足a0 < a且b0 < b且c0 < c
不用CDQ的算法,我们就不说了(太麻烦了)
类似二维偏序问题,先按照a元素从小到大排序,这样我们就可以忽略a元素的影响
然后CDQ分治,按照b元素从小到大进行归并排序
哪c元素我们要怎么处理呢?
这时候比较好的方案就是借助权值树状数组,
每次从左边取出三元组(a,b,c),根据c值在树状数组中进行修改
从右边的序列中取出三元组(a,b,c)时,在树状数组中查询c值小于(a,b,c)的三元组的个数
注意,每次使用完树状数组要把树状数组清零
平面上有N个点,每个点的横纵坐标在[0,1e7]之间,有M个询问,每个询问为查询在指定矩形之内有多少个点,矩形用(x1,y1,x2,y2)的方式给出,其中(x1,y1)为左下角坐标,(x2,y2)为右上角坐标
把每个点的位置变成一个修改操作,用三元组(时间,横坐标,纵坐标)来表示,把每个查询变成二维前缀和的查询
这样对于只有位于询问的左下角的修改,才对询问有影响
操作的时间是默认有序的,分治过程中按照横坐标从小到大排序,用树状数组维护纵坐标的信息