基本算法---递归以及分治初步

本人由于水平有限,笔记难免有疏漏与不严谨的地方,请大家给予批评指正,谢谢!

一、分治

分治的本质就是把原问题划分成若干个与原问题结构相似的子问题进行递归求解,并同时在回溯时解出原问题。

具体来说,分治的过程分为以下三个步骤:

1.划分问题:将原问题划分成若干子问题;

2.递归求解:分别求解若干个子问题;

3.合并问题:通过求解若干子问题,解出原问题;

 

例题:归并排序

归并排序所运用的原理就是分治。

我们对于原序列划分成左右两个等数量的连续序列,对这些子序列进行排序,最终通过对有序的子序列处理,从而对该序列进行排序。

具体来说,我们考虑递归的边界情况:给只有两个数的序列排序。

显然,仅需比较大小即可。

对于有四个数的序列呢?

例如:9,1,6,4

先将此序列划分左右两个子序列,对它们进行排序;

此时,9>1,因此,左边子序列为1,9;同样的,右边的序列为4,6。

原序列就变成:1,9,4,6。

这时我们只需要分别从左右两个序列中从小到大依次遍历,比较大小即可。

1和4,1<4,原序列第一个为1;

9>4,原序列第二个为4;

同理,6是第三个;

那最后剩下的怎么办?直接放进排过序的新序列后面,因为此时剩下的一定同时属于左右子序列其中一个,而左右子序列都已经有序,故剩下的一定也有序。

 

#include
#include
#define maxn 500001
using namespace std;
int n,a[maxn]={},x[maxn]={};
void merge_sort(int s,int t){
        //边界
    if(s==t)return;
    int mid=s+(t-s)/2,i=s,j=mid+1,k=s;
        //求解子序列,分治
    merge_sort(s,mid);
    merge_sort(mid+1,t);
        //合并,算法之精髓
    while(i<=mid||j<=t){
        if(j>t||(i<=mid&&a[i]];
        else x[k++]=a[j++];
    }
        //将排好序的x数组一一对应赋值a数组
    for(k=s;k<=t;k++)a[k]=x[k];
    return;
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    merge_sort(1,n);
    for(int i=1;i<=n;i++)printf("%d ",a[i]);
    return 0;
}

 

 

 

 

 

练习:洛谷P1908 逆序对

题目描述

对于给定的一段正整数序列,逆序对就是序列中ai>aj且i

输入格式

第一行,一个数n,表示序列中有n个数。

第二行n个数,表示给定的序列。序列中每个数字不超过10^9109

输出格式

给定序列中逆序对的数目。

输入输出样例

输入
 6
5 4 2 6 3 1
输出
  11

说明/提示

对于25%的数据,n2500

对于50%的数据,n4×104

对于所有数据,n5×105

这道题可以在归并排序同时,顺便记一下数量。

为什么可以这样呢?

对于一个序列来讲,可以统计划分成的左右子序列各自的逆序对个数。

但是,对于左子序列而言,序列中每一个数的原下标都是小于右子序列的。

因此,在对于两子序列的合并时,由于我们在排序时需要比较大小,所以当右边的数小于左边的数时,这时逆序对数就又加了mid-i+1个。

#include
#include
using namespace std;
long long num=0,a[1000000]={},t1[1000000]={};
void T(int s,int t){
    if(s==t)return;
    int mid=(s+t)/2,i=s,j=mid+1,k=s;
    T(s,mid);
    T(mid+1,t);
    while(i<=mid||j<=t){
        if(j>t||(i<=mid&&a[i]<=a[j]))t1[k++]=a[i++];
        else{
            t1[k++]=a[j++];
            num+=mid-i+1;
        }
    }
    for(i=s;i<=t;i++)a[i]=t1[i];
}
int main(){
    int n;
    cin>>n;
    for(int i=0;i>a[i];
    T(0,n-1);
    cout<endl;
    return 0;
}

 

例题:平面最近点对

 

题目描述:

给定平面上n个点,找出其中的一对点的距离,使得在这n个点的所有点对中,该距离为所有点对中最小的

输入格式:

第一行:n;2≤n≤200000

接下来n行:每行两个实数:x y,表示一个点的行坐标和列坐标,中间用一个空格隔开。

输出格式:

仅一行,一个实数,表示最短距离,精确到小数点后面4位。

输入样例:

3
1 1
1 2
2 2
输出样例:
1.0000

说明/提示:

0<=x,y<=10^9

 

 

此题先考虑朴素做法:1、对点的横坐标进行排序。

2、枚举每一个点到它之后点之距离。

时间复杂度为O(n2)

#include
#include
#include
#include
#define maxn 10001
using namespace std;
//double不会WA的
struct pos{
    int x;
    int y;
}a[maxn];
int n;
double ans=999999990;
double min(double a,double b){
    return a>b?b:a; 
}
int main(){
    scanf("%d",&n);
    for(int i=0; i"%d%d",&a[i].x,&a[i].y);
    for(int i=0; i1; ++i){
        for(int j=i+1; jj){
            double dist=sqrt((a[i].x-a[j].x)*(a[i].x-a[j].x)+(a[i].y-a[j].y)*(a[i].y-a[j].y));
            ans=min(ans,dist);
        }
    }
    printf("%0.4f\n",ans);
    
    return 0;
}

对于该算法,好像无法再优了。

我们换一种枚举方式:

1、对点的横坐标排序。

2、对任意一个集合,从中间将点的集合分两个子集,分别为左子集和右子集,对子集进行平面最近点对求解。

3、将左子集中元素一一与右子集元素求距离并不断修改更新答案。

此算法时间复杂度仍为O(n2)。

不过没有关系,该算法已然看出了分治的影子。

这时,我们发现在枚举的方面,在子问题合并的时候,仍然可以优化。设左子集平面最近点对之距离为d1,右子集为d2;

在元素合并时,具体地,中间有一条分界线l,等价于需要在l左侧枚举每一个点,用到l右侧每一个点的距离来更新答案;

因此,若左侧到右侧任意一个点的距离大于答案,我们可以不考虑枚举这个点。

可是,怎么知道该点不用考虑呢?

将条件放宽,若该点到分界线l距离大于答案,则不考虑该点。

枚举的范围缩短了。由于已经对点的横坐标进行排序,因此向右及向左的枚举便可以达到O(n),最优可达到O(log n);

可以将枚举改为二分查找或倍增查找,时间复杂度可降至O(log n)。综上,该算法时间复杂度是O(n log n)。

代码未用到二分以及倍增。。

#include
#include
#include
#include
#include
#define maxn 200001
#define INF 2143483647
using namespace std;
struct pos {
    long long x,y;
} a[maxn];
int n;
double mid(double p,double q){
    return pp:q;
}
bool cmp(struct pos q,struct pos p) {
    return q.x<p.x;
}
double dist(struct pos s,struct pos t) {
    return sqrt((s.x-t.x)*(s.x-t.x)+(s.y-t.y)*(s.y-t.y));
}
double solve(int s,int t) {
    if(s-t>=0)return INF;
    int mid=s+((t-s)>>1),l,r;
    double ans,ans_Left=solve(s,mid),dis=a[mid].x,ans_Right=solve(mid+1,t);
    ans=min(ans_Left,ans_Right);
    l=r=mid;
//找考虑的结点
for(int i=mid-1; i>=s; --i) if(dis-a[i].xi; else break; for(int i=mid+1; i<=t; ++i) if(a[i].x-disi; else break;
//求解、合并
for(int i=l; i<=mid; ++i) for(int j=mid+1; j<=r; ++j) ans=min(ans,dist(a[i],a[j])); return ans; } int main() { scanf("%d",&n); for(int i=1; i<=n; ++i)scanf("%lld%lld",&a[i].x,&a[i].y); sort(a+1,a+n+1,cmp); printf("%0.4f\n",solve(1,n)); return 0; }

 

 

 

 

 

例题:循环日程表问题

输入正整数k表示有n=2^k个运动员进行循环比赛,需要设计比赛日程表。每个选手与其他n-1个选手各赛一次;每个选手一天只能赛一次;循环赛一共进行n-1天。

按此要求设计一张比赛日程表,该表有n行和n-1列,第i行第j列表示第i个选手第j天遇到的选手。

我们讨论k=1的情况,只有两个运动员;

再看k=2的情况,可先安排1和2比赛,3和4比赛,划分成两个更小的问题进行求解。

然后将左上复制到右下,右上复制到左下,即可得到k=2的循环表

以此类推,对于任意一个正整数k,都可以考虑它的k/2的循环表,再进行相应的复制。

这题就迎刃而解了。

 

 例题:洛谷P1498 南蛮图腾

题目描述

自从到了南蛮之地,孔明不仅把孟获收拾的服服帖帖,而且还发现了不少少数民族的智慧,他发现少数民族的图腾往往有着一种分形的效果,在得到了酋长的传授后,孔明掌握了不少绘图技术,但唯独不会画他们的图腾,于是他找上了你的爷爷的爷爷的爷爷的爷爷……帮忙,作为一个好孙子的孙子的孙子的孙子……你能做到吗?

输入格式

每个数据一个数字,表示图腾的大小(此大小非彼大小) n<=10

输出格式

这个大小的图腾

输入输出样例

输入  
3

输出

/\ /__\ /\ /\ /__\/__\ /\ /\ /__\ /__\ /\ /\ /\ /\ /__\/__\/__\/__\

这道题和上一题一样,将大图形划分成三个小图形进行求解,注意边界条件以及二维数组的横纵坐标之间的关系
#include
#include
#include
#include
using namespace std;
char a[2049][2049]={};
int n;
inline void _read(int &x){
    char ch=getchar();bool mark=false;
    for(;!isdigit(ch);ch=getchar())if(ch=='-')mark=true;
    for(x=0;isdigit(ch);ch=getchar())x=(x<<1)+(x<<3)+ch-'0';
    if(mark)x=-x;
}
void solve(int x,int y,int k){
    if(k==4){
    a[x][y]=a[x+1][y-1]='/';
    a[x+1][y]=a[x+2][y]='_';
    a[x+2][y-1]=a[x+3][y]='\\';
    return;    
    }
    solve(x,y,k>>1);
    solve(x+(k>>2),y-(k>>2),k>>1);
    solve(x+(k>>1),y,k>>1);
    return;
}
int main(){
    memset(a,' ',sizeof(a));
    _read(n);
    n=1<<(n+1);
    solve(1,n>>1,n);
    for(int j=1;j<=(n>>1);j++){
    for(int i=1;i<=n;i++)printf("%c",a[i][j]);
    puts("");
    }
    return 0;
}

练习:分形

分形,具有以非整数维形式充填空间的形态特征。

通常被定义为“一个粗糙或零碎的几何形状,可以分成数个部分,且每一部分都(至少近似地)是整体缩小后的形状”,即具有自相似的性质。

现在,定义“盒子分形”如下:

一级盒子分形:

   X

二级盒子分形:

   X X
    X
   X X

如果用B(n - 1)代表第n-1级盒子分形,那么第n级盒子分形即为:

  B(n - 1)        B(n - 1)

          B(n - 1)

  B(n - 1)        B(n - 1)

你的任务是绘制一个n级的盒子分形。

输入格式:

输入包含多个测试用例。

输入的每一行包含一个不大于7的正整数n,代表要输出的盒子分形的等级。

输入的最后一行为-1,代表输入结束。

输出格式:

对于每个测试用例,使用“X”符号输出对应等级的盒子分形。

请注意’X’是一个大写字母。

每个测试用例后输出一个独立一行的短划线。

输入样例:

1
2
3
4
-1

输出样例

X
-
X X
 X
X X
-
X X   X X
 X     X
X X   X X
   X X
    X
   X X
X X   X X
 X     X
X X   X X
-
X X   X X         X X   X X
 X     X           X     X
X X   X X         X X   X X
   X X               X X
    X                 X
   X X               X X
X X   X X         X X   X X
 X     X           X     X
X X   X X         X X   X X
         X X   X X
          X     X
         X X   X X
            X X
             X
            X X
         X X   X X
          X     X
         X X   X X
X X   X X         X X   X X
 X     X           X     X
X X   X X         X X   X X
   X X               X X
    X                 X
   X X               X X
X X   X X         X X   X X
 X     X           X     X
X X   X X         X X   X X
-


一道基本的分治问题,解法同“南蛮图腾”。
注意输出图形,要再输出“-”。
#include
#include
#include
#include
using namespace std;
int n,p[8]= {};
char ch[730][730];
void solve(int x,int y,int cur) {
    if(cur==1) {
        ch[x][y]='X';
        return;
    }
    solve(x-p[cur-2],y-p[cur-2],cur-1);
    solve(x-p[cur-2],y+p[cur-2],cur-1);
    solve(x,y,cur-1);
    solve(x+p[cur-2],y-p[cur-2],cur-1);
    solve(x+p[cur-2],y+p[cur-2],cur-1);
}
int main() {
    p[0]=1;
    for(int i=1; i<=7; ++i)p[i]=p[i-1]*3;//预处理,方便表示
    while(1) {
        memset(ch,' ',sizeof(ch));
        scanf("%d",&n);
        if(n==-1)return 0;
        solve((p[n-1]+1)/2,(p[n-1]+1)/2,n);
        for(int i=1; i<=p[n-1]; ++i) {
            for(int j=1; j<=p[n-1]; ++j)printf("%c",ch[i][j]);
            puts("");
        }
        puts("-");//不要忘记!!
    }
    return 0;
}

 

 

 

经典例题:分形之城

城市的规划在城市建设中是个大问题。

不幸的是,很多城市在开始建设的时候并没有很好的规划,城市规模扩大之后规划不合理的问题就开始显现。

而这座名为 Fractal 的城市设想了这样的一个规划方案,如下图所示:

基本算法---递归以及分治初步_第1张图片

当城区规模扩大之后,Fractal 的解决方案是把和原来城区结构一样的区域按照图中的方式建设在城市周围,提升城市的等级。

对于任意等级的城市,我们把正方形街区从左上角开始按照道路标号。

虽然这个方案很烂,Fractal 规划部门的人员还是想知道,如果城市发展到了等级 N,编号为 A 和 B 的两个街区的直线距离是多少。

街区的距离指的是街区的中心点之间的距离,每个街区都是边长为 10 米的正方形。

输入格式:

第一行输入正整数n,表示测试数据的数目。

以下n行,输入n组测试数据,每组一行。

每组数据包括三个整数 N,A,B 表示城市等级以及两个街区的编号,整数之间用空格隔开。

输出格式:

一共输出n行数据,每行对应一组测试数据的输出结果,结果四舍五入到整数。

数据范围:

1N31,
1A,B22N,
1n1000

输入样例:

3 
1 1 2 
2 16 1 
3 4 33 

输出样例:

10 
30 
50 

你可能感兴趣的:(基本算法---递归以及分治初步)