算法设计与分析笔记(一)递归与分治策略

一》递归:直接或间接地调用自身的算法

EG:

1》阶乘定义 n!=n(n-1)! (n>0);

pubic static int factorial(int n ){
   if(n==0)  return 1;
   else
          return n*factorial(n-1);

}

2》FiBonacci数列

public static int fibo(int n){
 if(n<=1)return 1;
 else
       fibo(n-1)+fibo(n-2);
}

3》排列问题(一个集合的全排列)

设p(X)表示集合X的全排列,则rp(X)表示集合{r,X}的全排列,则一个集合的全排列可一次递归到只有一个元素的集合的全排列。

设Ri表示R-{ri},则p(R)=(r1)p(R1),(r2)p(R2),......(rn)p(Rn);

public void perm(char *list,int k,int m){
	//产生list[k:m]的全排列
	if(k==m){	//只剩一个元素的时候
		for(int =0;i<=m;i++){
			printf("%c",list[i]);
		}
		printf("\n");
	}else{	//还有多的元素,递归产生排列
		for(int i=k;i<=m;i++){
			//将集合中每个元素都放到队首排列一次,以此递归。
			swap(list,k,i);	//交换位置
			perm(list,k+1,m);	//递归
			swap(list,k,i);		//恢复原位
		}
	}

}

4》整数划分问题

将正整数n表示为一系列正整数之和,n=n1+n2+.....nk,其中n1>=n2>=....nk>=1,k>1;

这种表示成为正整数n的划分,记为p(n),将最大加数n1不大于m的划分个数记为q(n,m).

分析:

1》若n=1,不论m何值,只有一种划分{1};

2》若m=1,不论n何值,只有一种划分{1,1,1,1,1......};

3》若n

4》若n=m,则可分两种情况讨论:

a》划分中包含m(=n),所以只有一种划分即{n};

b》划分中不包含m,即最大只能是m-1,即为f(n,m-1);

5》若n>m,也分两种情况讨论

a》划分中包含了m,则为{m,{m1,m2,m3......}},即f(n-m,m);

b》划分中不好m,则为f(n,m-1);

综上:

f(n,m)=1,n=1orm=1

f(n,m)=f(n,n)n

f(n,m)=1+f(n,m-1)n=m

f(n,m)=f(n-m,m)+f(n,m-1)n>m

#include
using namespace std;

int f(int n,int m){
	if(n==1 ||m==1)
		return 1;
	else if(n

5》汉诺塔问题。

先考虑两层塔的移动,发现需要三个动作。考虑,三层塔的移动,发现出现一定的重复性,将上面两层看成一个整体,又变层两层塔移动,以此类推发现,任何n层次的移动都可以划分为第n层与前面(n-1)层的递归。

public void hanoi(int n,int a,int b,int c){
	//n表示总的层数,从a借助c移动到b
	if(n>0){
		hanoi(n-1,a,c,b);
		move(a,b);
		hanoi(n-1,c,ba);
	}
}

总结:

1》必须有终止条件,否则陷入无限递归,最后栈溢出。

2》考虑是否可以用递归解决时,思考减小问题规模是否可以采取相同的步骤且后一步的结果依赖前一步的结果产生递归。也可以从问题最简单的情况开始,逐渐扩大规模发现规律。


二》分治:将一个规模为n的问题分解为k个规模较小的子问题且互相独立且与原问题相同,递归解决子问题,然后合并得到原问题解。

divide-and-conquer(P){
	if(|P|<=n)  adhoc(P);
	divide P into p1,p2,p3...pk;
	for(int i=1;i<=k;i++)
		yi=divide-and-conquer(pi);
	return merge(y1,y2....yk);
}
其中n时阀值,当问题规模达到阀值时,可直接得到解并返回解


1》二分搜索算法

思想:在一个有序数组中搜索一个元素,通过一次比较(取值范围的中间数,即(left+right)/2  )将范围缩小一半,直到找出元素或无解。因为每次搜索都减小一半规模,所以复杂度为O(logn).

(规模减小,求解步骤重复,阀值确定)

//二分查找
//a为有序升序数组,k为查找的元素,n为数组大小
int f(int *a,int k,int n){
	int left=0,right=n-1;
	while(left<=right){
		int mid=(left+right)/2;
		if(k == a[mid]){
			return mid;
		}
		else if(k

2》合并算法(归并算法)

思想:将待排序元素氛围两个大小大致相同的2个子集合,分别对两个子集合排序,最终将排序好的子集合合并为所要的排序集合。(将规模减小了一半n/2,且小规模的求解同上,直到只剩一个元素)

void MerSort(int *a,int n){	//n为数组长度
	int *p=(int *)malloc(sizeof(int)*(n+1));
//问题的解决需要一个临时数组,临时数组只分配一次,减小开销
	mergeSort(a,0,n-1,p);
}
//分治
void mergeSort(int *a,int left,int right,int *p){	//left,right都表示下标
	if(left

3》快速排序

思想:选取待排序的某个元素做划分,比它大的放右边,比它小的放左边,剩下的空位就是它的位置。不断减小问题规模

int p(int *a,int left,int right){
	int key=a[left];
//	printf("**** %d ***   ",a[left]);
	while(left=key)
			right--;
		a[left]=a[right];
		//比key大的都移到右边
		while(left		//递归终止条件一定不要忘记,惨痛教训
		int pp=p(a,left,right);
		qSort(a,left,pp-1);
		qSort(a,pp+1,right);
	}
}


4》大整数的乘法

思路:将大整数分为两段,每段n/2位,则x=A*2^n/2+B,y=C*2^n/2+D,相乘后去括号可得到XY=AC*2^n+(AD+BC)*2^n/2+BD;

T(n)={O(1),n=1;   4T(n/2)+O(n),n>1;}

进过数学家的改进发现XY=AC2n+[(A-B)(D-C)+AC+BD]2n/2+BD    

T(1)=1

T(n)=3T(n/2)+cn.

#define sign(num) (num)>0?1:-1;

//x与y都是n位十进制的大整数乘法模型
//要实现真正的大整数,要把数据结构改为数组存储
int Mul(int x,int y,int n){
	int s=sign(x)*sign(y);
	int x=abs(x);
	int y=abs(y);

	if(x==0 || y== 0)
		return 0;
	if(n==1){
		return x*y;
	}else{
		int xl=x/(int )pow(10,(int)n/2);
		int xr=x-x1*(int )pow(10,(int)n/2);
		int yl=y/(int )pow(10,(int)n/2);
		int yr=y-y1*(int )pow(10,(int)n/2);
	
		int m1=Mul(xl,yl,n/2);
		int m2=Mul(xr,yr,n/2);
		int m3=Mul(xl-xr,yl-yr,n/2);
		return s*(m1*(int)pow(10,n)+m3*m1*m2*(int)pow(10,n/2)+m2);
	}

}
详细内容参考博客: http://blog.chinaunix.net/uid-20563078-id-1636245.html。
PS:对于不是分治算法的解法,我们一般采用链表或数组存储数据,模仿手工竖式计算方法,但是每次仅2个一位十进制数相乘的效率太低,改进版本一是采用一种列表法(参考博客: http://blog.csdn.net/zxasqwedc/article/details/12399543)


可以采用千位进制的乘法,即将大整数分为每个元素是一个不超过1000的三位十进制数,每次相乘是三位数三位数相乘,具体查看这个博客http://www.xuebuyuan.com/1601102.html。


待解决问题:将大整数分为多段,而不是2段,复杂性有何变化?是否优于2段????


5》Stassen矩阵乘法

高深的数学问题,写出来也只是搬运工,直接贴参考博客的连接:http://blog.sina.com.cn/s/blog_6eea1bc20100mfp3.html。


6》棋盘覆盖

问题描述

在一个2^k×2^k 个方格组成的棋盘中,恰有一个方格与其他方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘。在棋盘覆盖问题中,要用图示的4种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖。

思路:这个提供关键还是要懂一点小技巧,把两个子问题变成一个子问题。按分治思想又是正方形,理当将其分成四个小正方形,即4个2^k-1*2^k-1的子棋盘。特殊方格位于其中一个,其余三个无特方格,为了将其转化成特殊方格,可以用一个L型骨牌覆盖3个棋盘的汇合处,从而转化成4个特殊方格棋盘的覆盖问题。当递归到最后一个棋盘只有一个方格时则停止递归返回。为了表示出覆盖方法,将骨牌编号。

#include
#include
#include

#define SIZE 100

int tile=0;
int board[SIZE][SIZE];	//棋盘

//tr,tc是棋盘左上角行号,列号。dr,dc是特殊方格行号、列号
//size是棋盘大小
void chessBoard(int tr,int tc,int dr,int dc,int size){

	if(size == 1)	//停止递归
		return;
	
	int t=tile++;	//骨牌标号
	int s=size/2;	//分割棋盘
	
	//左上角棋盘
	if(dr=tc+s){
		chessBoard(tr,tc+s,dr,dc,s);
	}else{
		board[tr+s-1][tc+s]=t;
		chessBoard(tr,tc+s,tr+s-1,tc+s,s);
	}

	//左下角
	if(dr>=tr+s && dc=tr+s && dc>=tc+s){
		chessBoard(tr+s,tc+s,dr,dc,s);
	}else{
		board[tr+s][tc+s]=t;
		chessBoard(tr+s,tc+s,tr+s,tc+s,s);
	}
}

void main(){
	memset(board,0,sizeof(board));
	board[3][3]=100;
	chessBoard(1,1,3,3,16);
	for(int i=1;i<=16;i++){
		for(int j=1;j<=16;j++)
			printf("%4d",board[i][j]);
		printf("\n");
	}
	printf("\ntile=%d\n",tile);

}

7》循环赛日程表

设有n=2k个选手参加比赛,要求设计一个满足一下要求的比赛日程表:

(1)每个选手必须与其他的n-1个选手个比赛一次;

(2)每个选手每天只能赛一次 。

 

按此要求可以把比赛日程表设计成一个n行n-1列的二维表,其中第i行第j列表示第i个选手在 第j天比赛的选手。 

思路仍然是要发现能重复解决的办法,这个也是看题解的

#include
#include
#include

#define SIZE 100
int a[SIZE][SIZE];

void table(int k,int a[][SIZE]){
	int temp;
	int n=2;	//一开始n=2,两个人的情况
	a[1][1]=1;	a[1][2]=2;
	a[2][1]=2;	a[2][2]=1;

	for(int t=0;t

附上两个参考博客,写法略有不同,还没有I完全搞懂

1:递归写法http://www.2cto.com/kf/201411/351886.html

2:http://blog.csdn.net/liufeng_king/article/details/8488421

下列代码详解请看博客链接2

思路:这个算法将整个安排从前面的三步变成了两步:算法一本来是填充好两个人比赛的安排,然后开始1)左上角复制到右下角,2)左上角值增加temp后复制到左下角,3)左下角复制到右上角。算法二现将第一行数据填充好,依次按1)左上角复制到右下角,2)右上角复制到左下角。

比较:算法一更为直观,便于理解。

#include
#include
#include

#define SIZE 100

int a[SIZE][SIZE];

void delay(int a[][SIZE],int k){
	int n=1;
	for(int i=1;i<=k;i++)	//棋盘总大小
		n=n*2;
	for(i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			if(a[i][j]!=0)
				printf("%4d",a[i][j]);
		}
		printf("\n");
	}
	printf("\n");
}

void table(int k,int a[][SIZE]){
	int n=1;
	for(int i=1;i<=k;i++)	//棋盘总大小
		n=n*2;

	for(i=1;i<=n;i++)	//填充第一行
		a[1][i]=i;

	int m=1;
	for(int s=1;s<=k;s++){	//每次按2倍增长,只有k次即可填充完
		n=n/2;	

		for(int t=1;t<=n;t++){	//每行每次填充m列数据,共需要n次
			for(int i=m+1;i<=2*m;i++){
				for(int j=m+1;j<=2*m;j++){
					a[i][j+(t-1)*2*m]=a[i-m][j+(t-1)*2*m-m];	//将左上角数据填充到右下角
					a[i][j+(t-1)*2*m-m]=a[i-m][j+(t-1)*2*m];	//右上角数据填充到左下角
				}
			}
		//此段代码帮助理解算法运算过程
		printf("t=%d\n",t);
		delay(a,k);
		}
		m=m*2;	//倍增
	
	}

}

void main(){
	table(3,a);
}



8》线性时间选择


9》最接近点对问题



               


你可能感兴趣的:(算法)