目录
1、分治
分治,就是把原问题划分成若干个规模较小的小问题,然后一个一个解决这些小问题,最后再合并这些小问题的解,就可以解决了这个大问题啦。下面就是官方的解释啦
①分解:将原问题分解为若干和原问题拥有相同或相似结构的子问题。
②解决:递归求解所有子问题。如果存在子问题的规模小到可以直接解决,就直接解决
③合并:将子问题的解合并为原问题的解。
我们需要注意的是:分治法分解出的子问题应当是相互独立、没有交叉的。如果存在两个子问题有相交部分,那么不应当使用分治法解决。
从广义上米说,分治法分解成的子问题个数只要大于0即可。但是从严格的定义上讲,一般把子问题个数为1的情况称为减治,而把子问题个数大于1的情况称为分治,不过通常情况下不必在意这种区别。另外,分治法作为一种算法思想,既可以使用递归的手段去实现,也可以通过非递归的手段去实现,可以视具体情况而定,一般我们都用递归啦!
下面我们用n的阶乘来介绍递归思想。
2、递归
我觉得递归就是反复调用自身函数嘛,但是每次把问题的范围缩小,直到范围缩小到可以直接得到边界数据的结果,然后再在返回路上求出对应的解,是不是看出来了递归很适合用来实现分治思想。
递归逻辑中有二个重要概念:
①递归边界
②递归调用
那就来个经典的例题吧!
题目:
使用递归求解n的阶乘
代码如下:
#include<stdio.h>
int F(int n)
{
if (n == 0) return 1; //当达到递归边界F(0)时,返回F(0)==1
else return F(n - 1) * n; //没有达到递归边界使用递归式传递下去
}
int main() {
int n;
scanf("%d", &n);
printf("%d\n", F(n));
return 0;
}
思路如下:
①无妨令输入的 n 为3,然后调用 F(3),即求解3的阶乘。
②进入F(3)函数(第一层),判断 n ==0是否成立:不成立,因此返回 F(2)*3。注意,此时 F (2)还是未知的,因此程序会调用 F (2),等待得到 F (2)的结果后再计算 F (2)+3。
③进入 F(2)函数(第二层),判断 n ==0是否成立:不成立,因此返回F(1)*2。注意,注意此时 F (1)还是未知的,因此程序会调用 F (1),等待得到 F (1)的结果后再计算F(1)*2。
④进入 F (1)函数(第三层),判断 n ==0是否成立:不成立,因此返回 F (0)*1。注意,此时 F (0)还是未知的,因此程序会调用 F (0),等待得到 F (0)的结果后再计算F (0)*1。
⑤进入 F (0)函数(第四层),判断 n ==0是否成立:成立,因此返回1。到这里为止,得到了 F (0)=1,且不用再向更小范围进行递归,因此向上一层(第三层)返回 F(0)的结果。
⑤步骤④(第三层)由于等到了从第四层返回的 F (0),计算出了 F (1)= F (0)*1=1,并将 F (1)的结果返回上一层(第二层)。
⑥面步骤③(第二层)由于等到了从第三层返回的 F ( I ),计算出了 F (2)= F (1)*2=2,并将 F (2)的结果返回上一层(第一层)。
⑦步骤②(第一层)由于等到了从第二层返回的 F (2),计算出了 F (3)= F (2)*3=6,并将 F (3)的结果返回给了 main 函数。
至此,F(3)的递归过程结束,得到了F(3)=6并输出。在图4-2中,从进入第一层 F (3)开始,不断向下递归,到达递归边界后返回,在返回的过程中依次计算出F(1)、F(2)及 F (3),并把 F (3)返回给 main 函数调用 F (3)的地方。
(有点长,不过很好理解)
那再来看一道例题吧!(进阶一下)
题目:
求Fibonacci 数列的第n项。
Fibonacci 数列(即斐波那契数列)是足 F (0)=1, F ( I )=1. F(n)=F(n-1)+ F(n-2) ( n ≥2)的数列,数列的前几项为1,1,2,3,5,8,13,21,…。由于从定义中已经可以获知递归边界为F(0)=1和 F (1)=1,且递归式为F(n)=F(n-1)+ F(n-2) ( n ≥2) ,因此可以仿照求解 n 的阶乘的写法,
代码如下:
#include<stdio.h>
int F(int n)
{
if (n == 0||n==1) return 1; //当达到递归边界
else return F(n - 1)+ F(n - 2); //递归式
}
int main() {
int n;
scanf("%d", &n);
printf("%d\n", F(n));
return 0;
}
我们来看一下求解Fibonacci 数列的过程中会发现,这不就是使用递归实现分治法的一个简单例子吗,对给定的正整数n来说,把求解F(n)的问题分解为求解F(n-1)和F(n-2)这二个子问题,而F(0)=F(1)=1是当n很小时问题的直接解决,递归式F(n)=F(n-1)+F(n-2)则是问题的合并。
有上面二个例子可以知道,如果实现一个递归函数,那么就要有两样东西:递归边界和递归式。其中递归边界用来返回最简单底层的结果,递归式用来减少数据规模并向下一层递归。我们一开始学我觉得画图理解比较好,慢慢的找到规律。
再来一道例题
n皇后问题。
n皇后问题是指在一个n*n的国际象棋棋盘上放置n个皇后,使得这n个皇后两两均不在同一行、同一列、同一条对角线上。
如果我们用组合数的方式来枚举每一种情况(即从n平方个位置中选择n个位置),那n如果大,那我们根本吃不消啊!那我们换个思路,考虑到每行只能放置一个皇后、每列也只能放置一个皇后,那么如果把n列皇后所在的行号依次写出,那么就会是1~n的一个排序。那么我们就可以只枚举1~n的所有排列,查看每个是否合理进行了,通过计算可以发现比上一个方法好太多。
于是可以在全排列的代码基础上求解。由于当到达递归边界是生成了一个排列,所以需要在其内部判断是否为合法方案,即遍历每两个皇后,判断它们是否再同一条对角线上(不在同一行和同一列是显然的),若不是,则累计计数变量count即可。
代码如下:
#include <cstdio>
#include <cstdlib>
#define MAXN 12
int legal_rank_num = 0;
int N; // 棋盘边长,位数
int P[MAXN]; // 排列结果
bool hashTable[MAXN]; // 散列记录
void generateP(int index) {
// 递归边界
if (index == N + 1) {
// 内部判断是否满足要求
bool flag = true;
for (int i = 1; i <= N; i++) {
for (int j = i + 1; j <= N; j++) {
if (abs(i - j) == abs(P[i] - P[j])) { // 在同一条对角线上
flag = false;
}
}
}
if (flag == true) {
legal_rank_num += 1;
for (int i = 1; i <= N; i++) {
printf("%d", P[i]);
}
printf("\n");
}
return;
}
// 递归式
for (int j = 1; j <= N; j++) {
if (hashTable[j] == false) {
P[index] = j;
hashTable[j] = true;
generateP(index + 1);
hashTable[j] = false;
}
}
}
int main() {
N = 8;
generateP(1);
printf("\n解决方案有: %d\n", legal_rank_num);
return 0;
}
在前述的方法中,我们先枚举出所有的情况,然后再判断每一种情况是否合法,这种方法明显不是最优化的。其实当我们在已经放置了部分的皇后之后,该部分皇后已经出现呈对角线的局部排列时,后面再怎么摆放其它棋子都是徒劳。这样,我们也没有必要再往下继续递归了,直接返回上层即可,还可以减少很多的计算量。
一般我们把这种还没有达到递归边界就直接返回上层,不再进行继续递归的做法叫做回溯法。
代码如下:
#include <cstdio>
#include <cstdlib>
#define MAXN 12
int legal_rank_num = 0;
int N; // 棋盘边长,位数
int P[MAXN]; // 排列结果
bool hashTable[MAXN]; // 散列记录
void generateP(int index) {
// 递归边界
if (index == N + 1) {
// 回溯法中能够到达递归边界的都是满足条件的
legal_rank_num += 1;
for (int i = 1; i <= N; i++) {
printf("%d", P[i]);
}
printf("\n");
return;
}
// 递归式
for (int j = 1; j <= N; j++) {
if (hashTable[j] == false) {
bool flag = true;// 遍历之前的皇后,确保新皇后不会与它们形成对角线
for (int pre = 1; pre < index; pre++) {
if (abs(pre - index) == abs(P[pre] - j)) {
flag = false;
break;
}
}
if (flag == true) {
P[index] = j;
hashTable[j] = true;
generateP(index + 1);
// 递归完毕,还原第j行为未占用。
hashTable[j] = false;
}
}
}
}
int main() {
N = 8;
generateP(1);
printf("\n解决方案有: %d\n", legal_rank_num);
return 0;
}
这个n皇后问题其实我也还没太搞懂,惭愧啊!还是练的少啊! 哎
大家浅看一下吧!