分治思想
算法介绍
分治,字面上的解释是"分而治之",就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
适用情况
-
该问题的规模缩小到一定的程度就可以容易地解决
-
该问题可以分解为若干个规模较小的相同问题。
-
利用该问题分解出的子问题的解可以合并为该问题的解。
实现步骤
- 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
- 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
- 合并:将各个子问题的解合并为原问题的解。
经典例题
汉诺塔
传说印度古代某寺庙有一个梵塔,塔内有三个座A、B、C,座A上放着n个大小不等的盘,其中大盘在下,小盘在上。有一个和尚想把这64个盘从座A搬到座B,但一次只能搬动一个盘,搬动的盘只允许放到其他两个座上,且大盘不能压在小盘上。
现要求用程序模拟该过程,并输出搬动步骤。
问题分析
- 如果n=1,可直接从座A搬动到座B。
- 如果n>=2,先将上面n-1个盘子搬到座C,再将最下面第n个盘子搬到座B,再将n-1个盘子从座C搬到座B。
代码实现
void Hanoi(int n,char A,char B,char C)
{
if(n==1) printf("将第1个盘从%c搬到%c\n",A,B);
//如果只有1个盘,A->B
else
{
Hanoi(n-1,A,C,B);
//先把 最上面的所有盘 A->C,移动过程会使用到B
printf("将第%d个盘从%c搬到%c\n",n,A,B);
//把最下边的盘 A->B
Hanoi(n-1,C,B,A);
//把C塔的所有盘 从C->B , 移动过程使用到座A
}
}
最近平面点对
给定平面上 n 个点,找出其中的一对点的距离,使得在这 n个点的所有点对中,该距离为所有点对中最小的。
样例输入
第一行一个整数 n,表示点的个数。接下来 n行,每行两个实数x,y ,表示一个点的行坐标和列坐标。
3
1 1
1 2
2 2
样例输出
仅一行,一个实数,表示最短距离,四舍五入保留 4位小数。
1.000
问题分析
如果直接暴力,即枚举每个点到其他点的距离并更新距离的最小值。时间复杂度为O(n^2),在数据范围较小时可以考虑,但数据范围一大就会超时。
分治法求解
- 分解:将所有的点按照x坐标从小到大排序,以中间点mid为界划分为左右两个区域。
- 解决:递归寻找两个集合的最近点对。取两个集合最近点对的最小值d=min(dis_left,dis_right)
- 合并:这里需要考虑3种情况。
1.最近点对位于左半边区域
2.最近点对位于右半边区域
3.两个点一个位于左半边,一个位于右半边。
前两种情况均可通过递归左右区域完成,对于第三种情况我们可以这样考虑:
如果存在第3种情况,则可以利用第1,2种情况得到的最小值d,以mid为界得到区间(mi
d-d,mid+d),
设P点是位于(mid-d,mid]的一个点,则在(mid,mid+d)中最多只有6个点与之匹配。
以下为证明过程
将右半边的矩形划分为6个相等的小矩形,其中每个矩形的对角线长s(即最大距离))由勾股定理得s=(25/36)d^2,s<d,由反证法,如果有7个点的话由抽屉原理必然有一个小矩形内存在两个点,那么这两个点的距离最大为s,又s<d,因此与之前所得到的左右区域最小值为d矛盾,因此最多只有6个点
时间复杂度
在分解和合并时,可能存在按照x轴、y轴进行排序的预处理O(nlogn),该问题在解决阶段只做提取的操作为Θ(n),计算后得到整体时间复杂度为:O(nlogn^2))
代码实现
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10,INF=1e10;
struct node
{
double x,y;
}points[N],temp[N];
bool cmp1(node a,node b){return a.x<b.x;}
bool cmp2(node a,node b){return a.y<b.y;}
double dist(node a,node b)
{
double dx=a.x-b.x,dy=a.y-b.y;
return sqrt(dx*dx+dy*dy);
}
double slove(int l,int r)
{
if(l>=r) return INF;//如果只有一个点,返回无穷大
int mid=l+r>>1;//每次递归找中点
double mid_x=points[mid].x;
double res=min(slove(l,mid),slove(mid+1,r));//递归找左右区域的最小值
int k=0;
for(int i=l;i<=r;i++)
if(points[i].x>=mid_x-res&&points[i].x<=mid_x+res)
//如果在x轴坐标在(mid-res,mid+res)内就加入备选数组
temp[k++]=points[i];
sort(temp,temp+k,cmp2);//排序y轴
for(int i=0;i<k;i++)
for(int j=i+1;j<k&&temp[i].y-temp[j].y<res;j++)
//如果y轴距离也在res内则更新最小值
res=min(res,dist(temp[i],temp[j]));
return res;
}
int main()
{
int n;
scanf("%d",&n);
for(int i=0;i<n;i++)
scanf("%lf%lf",&points[i].x,&points[i].y);
sort(points,points+n,cmp1);//先按x轴排序
printf("%.4lf",slove(0,n-1));
return 0;
}
如果数据范围过大,我们还可以利用归并排序来排序y轴将时间复杂度降低到O(nlogn)
分形
分形,具有以非整数维形式充填空间的形态特征。通常被定义为“一个粗糙或零碎的几何形状,可以分成数个部分,且每一部分都(至少近似地)是整体缩小后的形状”,即具有自相似的性质。
问题描述
问题分析
- 分解:对于n>=2,我们只需要得到每个图形左上角的图形,再将其放到指定的位置即可。
- 解决:当n=1时可以直接输出,n>=2时可以通过递归求解。
- 合并:最后将各个子问题的解合并得到最终解。
我们可以发现:对于第1级分形它的长度len=1,第2级为3,第3级为9,因此第n级分形的长度为3^(n-1)。只要通过左上角图形的len确定其他四个部分的坐标,最后输出即可。
代码实现
#include<bits/stdc++.h>
using namespace std;
const int N=1000;
char g[N][N];
void dfs(int n)
{
if(n==1)
{
g[0][0]='X';//如果只有1级直接返回'X'
return ;
}
dfs(n-1);//递归找它的上一级
int len=1;//确定第n-1级的长度
for(int i=0;i<n-2;i++) len*=3;//输出第n-1级的图形
int sx[4]={0,1,2,2},sy[4]={2,1,0,2};
for(int k=0;k<4;k++)
for(int i=0;i<len;i++)
for(int j=0;j<len;j++)
g[sx[k]*len+i][sy[k]*len+j]=g[i][j];
//确定剩下四个部分的位置就确定了第n级
}
int main()
{
dfs(7);
int n;
while(~scanf("%d",&n)&&n!=-1)
{
int len=1;//找到第n级的长度
for(int i=0;i<n-1;i++) len*=3;
//第1级长度为1,第2级为3,第3级为9....
for(int i=0;i<len;i++)
{
for(int j=0;j<len;j++)
{
if(g[i][j]=='X') printf("X");
else printf(" ");
}
printf("\n");
}
printf("-\n");
}
return 0;
}
CDQ分治
CDQ分治是我们处理各类问题的重要武器。它的优势在于可以顶替复杂的高级数据结构,而且常数比较小;缺点在于必须离线操作。
基本思想
- 我们要解决一系列问题,这些问题一般包含修改和查询操作,可以把这些问题排成一个序列,用一个区间[L,R]表示。
- 递归处理左边区间[L,M]和右边区间[M+1,R]的问题。
- 合并两个子问题,同时考虑到[L,M]内的修改对[M+1,R]内的查询产生的影响。即,用左边的子问题帮助解决右边的子问题。
这就是CDQ分治的基本思想。和普通分治不同的地方在于,普通分治在合并两个子问题的过程中,[L,M]内的问题不会对[M+1,R]内的问题产生影响。
基本步骤
- 将整个序列分成长度相等的两部分
- 递归处理前一部分的子问题
- 计算前一部分的子问题中的修改操作对后一部分子问题的影响
- 递归处理后一部分的子问题
经典例题
三维偏序
前置知识:
- 树状数组(用来求前缀和)
- 归并排序,树状数组求逆序对
- 前缀和
题目描述
有 n 个元素,第 i 个元素有ai,bi,ci 三个属性,设 f(i) 表示满足aj≤ai 且 bj≤bi 且cj≤ci 且 j!=i 的 j 的数量。对于d∈[0,n),求 f(i) = d 的数量。
输入格式
第一行两个整数 n,k,表示元素数量和最大属性值。接下来 n 行,每行三个整数 ai,bi,ci,分别表示三个属性值。
输出格式
n 行,第 d+1 行表示 f(i)=d 的 i 的数量。
输入样例
10 3
3 3 3
2 3 3
2 3 1
3 1 1
3 1 2
1 3 1
1 1 2
1 2 2
1 3 2
1 2 1
输出样例
3
1
3
0
1
0
1
0
0
1
我们先来考虑一下二维偏序
二维偏序也就是aj≤ai,bj≤bi,应该怎么做
二维偏序解法
先按a为第一关键字排序,再按b为第二关键字排序
于是对于每一个i,只有1—i-1的元素会对它有贡献,那么我们只要查询1—i-1里bj<bi的元素个数就好了,其中利用树状数组动态维护b即可,或者归并排序求顺序对也行。
三维偏序思路
在三维偏序下,我们需要保证前两维都已经满足的情况下(即aj<ai,bj<bi,j<i)才能考虑第三维.
我们以a为第一关键字排序,b为第二关键字排序,c为第三关键字排序,第一维保证左边小于等于右边了,为了保证第二维也是左边小于右边,我们需要对第二维进行排序,第二维排序利用CDQ分治,第三维用树状数组维护。
代码实现
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int n,tot1,k,tot2,anss[N],tree[N];
//s2排序处理第一维,cdq处理第二维,树状数组处理第三维
struct node
{
int a,b,c,cnt,ans;
}s1[N],s2[N];//原数组,去重后的数组
bool cmp1(node x,node y)//排序第一维
{
if(x.a==y.a)
{
if(x.b==y.b)return x.c<y.c;
return x.b<y.b;
}
return x.a<y.a;
}
bool cmp2(node x,node y)//排序第二维
{
if(x.b==y.b) return x.c<y.c;
return x.b<y.b;
}
int lowbit(int x){return x&(-x);}//取出x的最低位1
void add(int x,int y)//区间修改
{
while(x<=k)
{
tree[x]+=y;
x+=lowbit(x);
}
}
int query(int x)//单点查询
{
int sum=0;
while(x)
{
sum+=tree[x];
x-=lowbit(x);
}
return sum;
}
void cdq(int l,int r)
{
if(l==r) return ;//和归并排序很像
int mid=l+r>>1;
cdq(l,mid);
cdq(mid+1,r);
sort(s2+l,s2+1+mid,cmp2);//排序第二关键字
sort(s2+1+mid,s2+r+1,cmp2);
int j=l;
for(int i=mid+1;i<=r;i++)
{
while(s2[i].b>=s2[j].b&&j<=mid)
{
add(s2[j].c,s2[j].cnt);//说明s2[j]对s2[i]有贡献,所以用树状数组处理c的时候在s2[j].c的位置上加上s2[j]的个数
j++;
}
s2[i].ans+=query(s2[i].c);
//对于s2[i]的贡献只要求出在s2[i].c之前的点的贡献之和
}
for(int i=l;i<j;i++)
add(s2[i].c,-s2[i].cnt);//加上一个负的cnt从而清空树状数组
//memset会超时
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
scanf("%d%d%d",&s1[i].a,&s1[i].b,&s1[i].c);
sort(s1+1,s1+1+n,cmp1);
for(int i=1;i<=n;i++)
{
tot1++;//去重,不然会出问题
if(s1[i].a!=s1[i+1].a||s1[i].b!=s1[i+1].b||s1[i].c!=s1[i+1].c)
{
tot2++;
s2[tot2].a=s1[i].a,s2[tot2].b=s1[i].b,s2[tot2].c=s1[i].c;
s2[tot2].cnt=tot1;
tot1=0;
}
}
cdq(1,tot2);//注意不是n
for(int i=1;i<=tot2;i++)
anss[s2[i].ans+s2[i].cnt-1]+=s2[i].cnt;
/*
s2[i].ans表示的是aj<ai,bj<bi,cj<ci,但不包括aj=ai,bj=bi,cj=ci的j的个数
而a[i].cnt恰好表示了aj=ai,bj=bi,cj=ci的个数,于是a[i].ans+a[i].cnt就是去重后第i个点的答案
*/
for(int i=0;i<n;i++)
printf("%d\n",anss[i]);//输出答案为d的i的数量
return 0;
}
题目出处 | 题目类型 | 题目难度 |
---|---|---|
AcWing 107 | 分治思想 | ⭐ |
洛谷P1115 | 分治思想 | ⭐ |
洛谷P1908 | 分治思想 | ⭐⭐ |
POJ2503 | 分治思想 | ⭐⭐ |
洛谷P1228 | 分治思想 | ⭐⭐⭐ |
POJ 2366 | 分治思想 | ⭐⭐⭐ |
POJ 1836 | 分治思想 | ⭐⭐⭐ |
POJ 3122 | 分治思想 | ⭐⭐⭐ |
洛谷P1498 | 分形 | ⭐⭐ |
AcWing 118 | 分形 | ⭐⭐ |
AcWing 98 | 分形 | ⭐⭐⭐ |
POJ 3768 | 分形 | ⭐⭐⭐ |
AcWing 119 | 平面最近点对 | ⭐⭐⭐ |
洛谷P1429 | 平面最近点对 | ⭐⭐⭐ |
洛谷P7883 | 平面最近点对 | ⭐⭐⭐ |
洛谷P2717 | CDQ分治 | ⭐⭐⭐ |
洛谷P2345 | CDQ分治 | ⭐⭐⭐ |
洛谷P3810 | CDQ分治 | ⭐⭐⭐⭐ |
洛谷P4169 | CDQ分治 | ⭐⭐⭐⭐ |
洛谷P5459 | CDQ分治 | ⭐⭐⭐⭐ |