这篇文章屯了很久了,一直想把内容总结到最好。
但由于时间关系,就先把现在能总结出来的写一下。
关于分治
基础分治
分治,意为:分而治之。
通常来说,将问题划分为若干个子问题,通过子问题的合并从而得到最优解。
一般分治都是讲问题分为左区间与右区间,划分为其他区间的分治 少之又少。
怎么通过问题的合并来得到最优解呢?
举个例子来看:
例子1:有N个数,你的任务是求出异或为0的区间有多少个?
这道题在之前写过的博客关于异或问题的总结中提到过,用map记录一下前缀即可。
但是现在任务是用分治来写(小题大做):
首先分治要分而治之,所以要分块l=1,r=n并写好分治函数
ll work(ll l,ll r){
if(r<l) return 0;
if(r==l) return num[l]==0?1:0;
ll mid=(l+r)/2;
ll sum=0;
sum+=work(l,mid);
sum+=work(mid+1,r);
}
上述代码应该都可以看懂:
首先计算左区间与右区间的贡献。
接下来将两个区间合并为一个新的区间,那么合并过程中必然又会产生新的贡献所以:
ll res=0;
map<int,int>mp;
for(int i=mid;i>=l;i--){
res^=num[i];
mp[res]++;
}
res=0;
for(int i=mid+1;i<=r;i++){
res^=num[i];
sum+=mp[res];
}
return sum;
这样一来就是整个区间[l,r]的贡献,所以主函数中输出 work(1,n)即为答案 ,复杂度 (n*lgn*c) c这个常数还蛮大的,因为用了map
例子2:平面最近点对:给出1e5个点,求最近的两点间的距离
这样枚举的复杂度会爆,所以我们考虑分治(按x坐标分治)。
我们把区间二等分,所以合并区间的时候就会有三种情况:
最小点对在左区间,最小点对在右区间,最小点对合并后产生
对于前两种没什么可说的,对于第三种:
如果我们不加优化的话,会增加[l,mid]*[mid+1,r]的复杂度*nlgn ,这个常数无疑来说太大了。
由于我们求的是最近距离,那么完全可以加一个启发式优化:
如果对mid.x的横坐标来说,如果大于当前最小距离,那么肯定不可以使得其成为最小值。
double solve(int l,int r)
{
if(l==r) return INF;//一个点距离 返回INF
if(l+1==r) return cal(save[l],save[r]);//两个点直接计算
int mid=(l+r)/2;
double d=min(solve(l,mid),solve(mid+1,r));
double minl=d;
int cnt=0;//储存两边的数
for(int i=l;i<=r;i++)//[l,r]区间内在两边 横坐标之差不能超过在两边的最小距离
if(abs((save[i].x-save[mid].x))<d)
index[++cnt]=i;//更新一下
sort(index+1,index+1+cnt,cmp);
for(int i=1;i<=cnt;i++)
for(int k=i+1;k<=cnt&&abs(save[index[i]].y-save[index[k]].y)<d;k++)//优化
minl=min(minl,cal(save[index[i]],save[index[k]]));
return minl;
}
例子3:归并排序
这个应该不需要过多的讲解了吧,分治的基础题目,没有掌握的话参见算法导论等算法书籍,基本都会有。
cdq分治
cdq分治,是陈丹琪(国际集训队队员,贼nb)发表论文里面提到的一种分治,与整体二分类似。
cdq分治的应用在于处理偏序问题。
算法核心在于,只考虑左区间对右区间的贡献。
那么对于
一维偏序问题(树状数组裸题)
二维偏序问题(排序后裸题)
三维偏序问题呢?还是裸题?
这就需要cdq分治
首先对于数组整体x坐标排序,保证了右边的不可能会对左边产生贡献
之后对于区间[l,r]进行cdq分治。
所以现在cdq分治核心就来了,左区间对右区间的贡献如何计算?
我们可以把左区间按照y坐标排序,右区间也按照y坐标排序
然后使用归并排序的思想:如果当前左边的这个y小于当前右区间的y,那么当前的左边的这个z就可以对右边指针的所有大于左边z的产生贡献,所以这一部分用树状数组去维护。
所以要求树状数组维护的区间为z的大小。
不要忘记减去贡献!!!!
void Divide(int l,int r){
if(l>=r) return;
int mid=(l+r)/2;
Divide(l,mid);Divide(mid+1,r);
sort(cop+l,cop+mid+1,cmpb);
sort(cop+mid+1,cop+r+1,cmpb);
int j=l;
for(int i=mid+1;i<=r;i++){
while(cop[i].b>=cop[j].b&&j<=mid){///当前左边的可以对之后的大于左边c的产生贡献
update(cop[j].c,cop[j].w);///树状数组加上贡献 cop[j].w 为1
j++;
}
cop[i].ans+=GetSum(cop[i].c);///然后计算一下 左边区间对当前值的贡献为多少
}
for(int i=l;i<j;i++) update(cop[i].c,-cop[i].w);
}
关于倍增
基础倍增
倍增的思想与分治如出一格,但实质又不同。
倍增思想的原理是 ,任何数字都可以用二进制数表示,那么处理距离问题,就可以使用倍增或者区间问题就可以使用倍增。
例子1:说到基础倍增不说RMQ问题就白说了~
关于RMQ问题——静态区间询问最大值与最小值
根据dp的思想,可以将静态区间的rmq数组处理出来。
dp[i][j]表示以i开始的区间长度为2^j次方的区间内的最大值或最小值。
那么很显然dp[i][j] = max(dp[i][j-1],dp[i+(1<<(j-1)][j-1])
之后询问就更为简单了,由于最大值和最小值具有区间可合并性,所以完全可以将区间拆为两个二进制的区间O1复杂度就可以得到静态区间最大值与最小值,假设区间长度为len,取log(len)即可
例子2:区间连续和问题:询问区间[x,y]内最少可以划分多少个区间使得每个区间的和都小于等于k
首先,如果单次询问的话:方法为贪心——从区间端点开始寻找这个区间最大的右边界,然后移动过去,继续找右边界。
这样贪心一定是正确的?可以通俗的想,如果跳过左边界,那么区间就会+1,而不这么做的话最多区间会-1,所以正确。
或者当作结论记住。
那么多次询问怎么处理?
首先倍增思想预处理出dp[i][j]表示,以i开始划分为2^j段的结束点+1的位置
基础上述的贪心思想我们便可以从l开始倍增判断dp[i][j]是否小于等于r,如果小于等于r,那么i=dp[i][j],同时ans=ans+(1<<j)
最后的ans要+1?为什么呢?因为剩下的要自己划分为一段而且这一段和一定小于等于k
ll ans=1;
int x,y;scanf("%d%d",&x,&y);
for(int i=21;i>=0;i--){
if(st[x][i]&&st[x][i]<=y){
x=st[x][i];
ans+=1ll<<i;
}
}
树上倍增问题
关于树上倍增问题首先想到——LCA
树上两点最近公共祖先:
首先,预处理整个树的deep数组——深度数组与f i,j 数组——i点向上2^j祖先是谁
下面分三步:
第一步:考虑什么时候两者可以同时倍增——深度相同的时,距离公共祖先的距离,就可以用二进制表示这个距离,从而使两个点一起倍增上移,所以第一步确定谁的深度大
第二步:让深度大的利用倍增,跳到与深度小的相同的高度,显然是可以的——任何数都可以用二进制表示。
第三步:两点同时向上跳,注意此时的判断条件是祖先不相同就向上跳,因为公共祖先上面的所有祖先两者一定相同。
这样LCA就解决了。
另外树上倍增还可以维护树链的最大值与树链的最小值,甚至可以维护树上路径的贸易 从u->v可以赚的最大差值。
这些题目以后总结再说吧
这次就先到这里了
完结~撒花!