算法的选择是培养个人程序设计能力的重要一环,在实际问题中往往可以用多个算法来解决,但是要找到最佳的解决算法也是一项极难的挑战。(传送门 ↓)
◉ 分治法:拆解问题,逐一解决。
◉ 递归法:自己调用自己。
◉ 贪心法:只顾当前的最佳。
◉ 动态规划法:不重复计算。
◉ 回溯法:及时回到正确的路。
1.1 分治法
1.1.1 分治法概念
在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……
1.1.2 分治法应用特征
分治法所能解决的问题一般具有以下几个特征:
1) 该问题的规模缩小到一定的程度就可以容易地解决
2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
3) 利用该问题分解出的子问题的解可以合并为该问题的解;
4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;
第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;
第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。
第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
1.1.3 经典问题解决
1.2 递归法
1.2.1 递归法概念
递归是一种特殊的算法,类似于分治法,都是将复杂问题进行分解,让规模越来越小,使得子结构问题容易求解。简单而言,其函数(子程序)不单纯只是能够被其他的函数调用,还提供了自己调用自己的功能,这种调用的功能称为“递归”。
在程序设计的角度而言,递归的定义是:假如一个函数或子程序是由自身定义或调用的,就称为递归。它至少满足两个条件:
1、可以反复调用执行的递归过程。
2、一个跳出执行程序的出口。
1.2.2 递归法简单理解的例子(阶乘函数)
阶乘函数是数学上很有名的函数,采用递归函数算法如下,请注意其中的递归基本条件:一个反复的过程;一个递归终止的条件,确保有跳出递归过程的出口。
#include <stdio.h>
int factorial(int i){
int sum=0;
if(i == 0) // 确保有跳出递归的出口
return 1;
else{
// 递推的过程
printf("Backstepping:i=%d sum=%d\n",i,sum);
sum = i * factorial(i-1);
// 回归的过程
printf("Return:i=%d sum=%d\n",i,sum);
}
return sum;
}
int main(void)
{
printf("Finally sum=%d",factorial(5));
}
函数中归为两个过程,递推过程与回归过程,通过以下的输出可以更好的理解程序运行过程。
Backstepping:i=5 sum=0
Backstepping:i=4 sum=0
Backstepping:i=3 sum=0
Backstepping:i=2 sum=0
Backstepping:i=1 sum=0
Return:i=1 sum=1
Return:i=2 sum=2
Return:i=3 sum=6
Return:i=4 sum=24
Return:i=5 sum=120
Finally sum=120
通过输出语句的顺序可以很好的观察出程序的运行流程,可以总结出以下几点:
1、当主程序调用了递归函数factorial()后,首先判断是否要结束递推,这个判断是每次进入递归函数必须要进行的。
2、在递推过程(Backsteping)中,sum的数值并没有变化,说明语句sum = i * factorial(i-1);没有发生赋值,而是进入了下一个factorial()的调用递推过程。
3、一直到 i=0的条件成立后,程序开始进入回归过程。这时 sum 的数值开始阶乘,也就是语句 sum = i * factorial(i-1) 开始赋值。
以下是对factorial(3)为例的递归过程的图解,以及程序运行流程,帮助大家更好的理解程序运行过程。
图 1:factorial(3)为例的递归过程
1.3 贪心法
1.3.1 贪心法概念
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解,不能够保证求得的最后解是最佳的,贪心法容易做出过早的决定。
贪心算法的应用范围并不大,因为算法本身具有局部最优性,且不考虑全局,所以选择该算法时要考虑每一步所作的贪心选择能否最终导致问题的整体最优解。
1.4 动态规划法
1.4.1 动态规划法概念
由美国数学家R.E.Bellman发明提出,用于研究多阶段决策的优化过程与求得一个问题的最佳解。
动态规划法的主要做法是:如果一个问题答案与子问题相关,就将大问题拆成小问题,类似与分治法,但其中与分治法的最大区别在于是可以让每一个子问题的答案被储存起来,以供下次求解时使用。这样不但能够减少再次计算的时间,并将这些解组成最后的大问题的解答,所以动态规划法可以解决重复计算的问题。
与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。
1.4.2 动态规划法的例子(斐波拉契数列)
斐波那契数列指的是这样一个数列:1、1、2、3、5、8、13、21、34、……这个数列从第3项开始,每一项都等于前两项之和。
如果使用动态规划法设计算法,那么已经计算过的数据就不必重复计算,实现提高算法性能的目的。
图 2:递归法执行路径图
从以上的递归法的执行路径图可以得出,递推调用了9次,加法计算执行了4次,Fib(1)和Fib(0)执行了3次,重复的计算影响了算法的执行效率。我们可以根据动态规划法将算法改为以下:
#include <stdio.h>
int output[100]={0}; // 数据暂存区
int fib(int n){
int result;
result = output[n]; // 将数据读取回来
printf("Recursion n=%d\n",n);
if (result == 0){ // 检查数据是否计算过
if(n == 0){
return 0;
}
if(n == 1){
return 1;
}
else
result = (fib(n-1)+fib(n-2)); // 递归过程
output[n] = result; // 将计算出来的结果保存
return result;
}
return result;
}
int main(void){
printf("Finally result=%d",fib(6));
}
在程序中出现的数据暂存区是动态规划法的重点,当在递归中的子函数得到解后,立即将其保存,并在其后的计算中使用,以减少计算量。
Recursion n=6
Recursion n=5
Recursion n=4
Recursion n=3
Recursion n=2
Recursion n=1
Recursion n=0
Recursion n=1
Recursion n=2
Recursion n=3
Recursion n=4
Finally result=8
这是以Fib(6)为例的递归调用次数,大家可以试试将程序中的第18行注释掉,再次运行观察调用次数的区别。
1.5 回溯法
1.5.1 回溯法概念
回溯法也算是枚举法的一种。对于一些问题而言,回溯法是一种可以找出所有(或者一部分)解的一般性算法,同时避免了枚举不正确的数值,一旦发现不正确的解,就不在递归到下一层,而是回溯到上一层,以节约时间。它的特点在于搜索过程中寻找问题的解,当发现不满足求解条件时,就回溯,重新尝试别的路径,避免无效搜索。
1.5.2 回溯法的例子(老鼠走迷宫)
老鼠走迷宫问题的描述是:假设把一只老鼠放在一个没有盖子的大迷宫的入口处,其中有分隔的墙挡住去路,老鼠要不断的尝试错误的方式找到出口。不过聪明的老鼠不会走已经走过的错误路。
程序中将使用堆栈结构来帮助老鼠走迷宫的路线,其中0代表墙,2代表入口,3代表出口,6代表老鼠走过的路线。
#include <stdio.h>
#include <stdlib.h>
#define EAST MAZE[x][y+1]
#define WAST MAZE[x][y-1]
#define SOUTH MAZE[x+1][y]
#define NORTH MAZE[x-1][y]
#define ExitX 8
#define ExitY 10
struct list{ // 定义了一个坐标结构体
int x,y;
struct list* next; // 一个指向自己的指针
};
typedef struct list node; // 重命名结构体
typedef node* link; // 定义一个该结构体指针
int MAZE[10][12] = {{2,1,1,1,1,0,0,0,1,1,1,1}, //迷宫地图
{1,0,0,0,1,1,1,1,1,1,1,1},
{1,1,1,0,1,1,0,0,0,0,1,1},
{1,1,1,0,1,1,0,1,1,0,1,1},
{1,1,1,0,0,0,0,1,1,0,1,1},
{1,1,1,0,1,1,0,1,1,0,1,1},
{1,1,1,0,1,1,0,1,1,0,1,1},
{1,1,1,0,1,1,0,1,1,0,1,1},
{1,1,0,0,0,0,0,0,1,0,0,1},
{1,1,1,1,1,1,1,1,1,1,1,3}};
// 压栈函数
link push(link stack, int x, int y){
link newnode;
newnode = (link)malloc(sizeof(node)); // 申请一段内存地址
if(!newnode){
printf("Error!");
return NULL;
}
newnode->x = x;
newnode->y = y;
newnode->next = stack; // 初次为NULL,之后就是上一个坐标结构体的地址
stack = newnode; // 将本次的坐标结构体指针赋出,传递给下一个
return stack;
}
// 出栈函数
link pop(link stack,int* x,int* y){
link top;
if(stack!=NULL){
top = stack;
stack = stack->next; // 将指针指向上一个坐标结构体
*x=top->x; // 将x,y通过解地址的方式直接赋值修改
*y=top->y;
free(top); // 释放内存
return stack;
}
else
*x=-1;
return stack;
}
int chkExit(int x,int y,int ex,int ey){
if(x == ex && y==ey){
if(NORTH == 1 || SOUTH == 1 ||WAST == 1 || EAST == 2)
return 1;
if(NORTH == 1 || SOUTH == 1 ||WAST == 2 || EAST == 1)
return 1;
if(NORTH == 1 || SOUTH == 2 ||WAST == 1 || EAST == 1)
return 1;
if(NORTH == 2 || SOUTH == 1 ||WAST == 1 || EAST == 1)
return 1;
}
return 0;
}
int main(void){
int i,j,x,y;
link path = NULL;
x = 1;
y = 1;
printf("0代表墙,2代表入口,3代表出口,6代表老鼠走过的路线\n");
for(i=0;i<10;i++){
for(j=0;j<12;j++){
printf("%2d",MAZE[i][j]);
}
printf("\n");
}
while(x<=ExitX && y<=ExitY){
MAZE[x][y]=6;
if(NORTH == 0){
x -= 1;
path = push(path,x,y);
}
else if(SOUTH == 0){
x += 1;
path = push(path,x,y);
}
else if(WAST == 0){
y -= 1;
path = push(path,x,y);
}
else if(EAST == 0){
y += 1;
path = push(path,x,y);
}
else if(chkExit(x,y,ExitX,ExitY) == 1) //检查是否到达出口
break;
else{
MAZE[x][y]=6;
path=pop(path,&x,&y);
}
}
printf("----------------------------\n");
for(i=0;i<10;i++){
for(j=0;j<12;j++){
printf("%2d",MAZE[i][j]);
}
printf("\n");
}
return 0;
}
输出:
0代表墙,2代表入口,3代表出口,6代表老鼠走过的路线
2 1 1 1 1 0 0 0 1 1 1 1
1 0 0 0 1 1 1 1 1 1 1 1
1 1 1 0 1 1 0 0 0 0 1 1
1 1 1 0 1 1 0 1 1 0 1 1
1 1 1 0 0 0 0 1 1 0 1 1
1 1 1 0 1 1 0 1 1 0 1 1
1 1 1 0 1 1 0 1 1 0 1 1
1 1 1 0 1 1 0 1 1 0 1 1
1 1 0 0 0 0 0 0 1 0 0 1
1 1 1 1 1 1 1 1 1 1 1 3
----------------------------
2 1 1 1 1 0 0 0 1 1 1 1
1 6 6 6 1 1 1 1 1 1 1 1
1 1 1 6 1 1 6 6 6 6 1 1
1 1 1 6 1 1 6 1 1 6 1 1
1 1 1 6 0 0 6 1 1 6 1 1
1 1 1 6 1 1 6 1 1 6 1 1
1 1 1 6 1 1 6 1 1 6 1 1
1 1 1 6 1 1 6 1 1 6 1 1
1 1 6 6 6 6 6 0 1 6 6 1
1 1 1 1 1 1 1 1 1 1 1 3