递归
1,分治
概念:分治法将原问题划分成若干规模较小而结构与原问题相同或相似的子问题,然后分别解决这些子问题,最后合并子问题的解,即可得到为原问题的解。
由上可知,分治法可以分为三个步骤:
1)分解:将原问题分解为若干和原问题拥有相同或相似结构的子问题
2)解决:递归求解所有子问题,如果存在子问题的规模较小到可以直接解决,就直接解决它
3)合并:将子问题解法合并为原问题
⚠️:分治法分解出的子问题应该是相互独立、没有交叉的,如果存在两个子问题有相交部分,那么不应当使用分治法解决。
广义上来说,分治法分解成的子问题个数只要大于0即可;严格意义上,一般吧子问题个数为1的情况称为减治,而把子问题个数大于1的情况称为分治。
另外分治作为一种思想,既可以通过递归手段实现,也可以通过分递归的手段实现。
2,递归
递归很适合来实现分治思想
递归中两个重要概念:
1)递归边界——分解的尽头
2)递归式(或称递归调用)——将原问题分解为若干子问题的手段
经典例子:使用递归求解n的阶乘
n! = 1 * 2* 3*…*n,这个式子写成递归式就是n! = (n -1)! * n,
就把问题规模为n的问题转换为求解规模为n -1的问题。
如果用F(n)表示n!,就可以写成F(n) = F(n -1)*n【递归式】,
这样就把F(n)变成F(n-1),又把F(n-1)变成F(n-2),这样一直减小规模;
因为0! = 1,因此不妨以F(0)=1作为递归边界,即当规模减小至n=0的时候开始“回头”。
参考代码:
//求阶乘
#include<cstdio>
int F(int n){
if(n == 1) return 1; //达到递归边界F(0)时,返回F(0)=1
else return F(n - 1) * n; //没有达到递归边界,使用递归式递归下去
}
int main(int argc, char const *argv[])
{
int n;
scanf("%d", &n);
printf("%d\n", F(n));
return 0;
}
另一个经典案例:求斐波那契数列的第n项
Fibonacci数列是满足F(0) = 1,F(1) = 1,F(n) =F(n-1)+F(n-2)【n>=2】的数列
参考代码:
//斐波那契数列
#include<cstdio>
int F(int n){
if(n == 1 || n == 0) return 1; //递归边界F(1)和 F(0)等于1
else return F(n - 1) + F(n - 2); //递归式
}
int main(int argc, char const *argv[])
{
int n;
scanf("%d", &n);
printf("%d\n", F(n));
return 0;
}
全排列(Full Permutation)
把1~n这个n个整数按某种顺序摆放的结果称为这n个整数的一个排列,而全排列指这n个整数能形成的所有排列。
现在需要实现按字典序从小到大的顺序输出1~n的全排列,其中(a1,a2,…,an)的字典序小于(b1,b2,…,bn)是指存在一个i,使得a1=b1、a2=b2、…、ai-1=bi-1、ai<bi成立。
例如对1、2、3这三个整数来说,(1,2,3)、(1,3,2)、(2,1,3),(2,3,1)、(3,1,2)、(3,2,1)就是1~3的全排列。
参考代码:
#include<cstdio>
const int maxn =11;
int P[maxn], hashTable[maxn] = {false};//p为当前全排列,hashTable记录x是否已经在p中
int n;
void generateP(int index){
if(index == n + 1){ //递归边界已经处理完1~n
for (int i = 1; i <= n; ++i)
{
printf("%d", P[i]); //输出当前全排列
}
printf("\n");
return;
}
for (int x = 1; x <= n; ++x) //枚举1~n,试将x填入P[index]
{
if(hashTable[x] == false){ //如果x不在P[0]到P[index -1]中
P[index] = x; //令P第index位为x,即把x加入当前排列
hashTable[x] = true; //记x已在P中
generateP(index + 1); //处理排列的index+1号位
hashTable[x] = false; //已处理完P[index]为x的子问题,还原状态
/*最后全部递归结束前 x对应的数分别为 1 2 3
index对应的数为 3 2 1
hashTable[1] 0 0 0
hashTable[2] 1 0 0
hashTable[3] 1 1 0
三次递归后没有任何输出,然后结束递归调用,返回main函数
*/
}
}
}
int main(int argc, char const *argv[])
{
n = 3; //欲输出1~3的全排列
generateP(1); //从P【1】开始填
return 0;
}
令外上面代码可以参考下面这个代码理解:
#include<iostream>
using namespace std;
void Printf(int a[],int i)
{
if(i==0)
{ cout<<a[i]<<" ";
return;
}
cout<<a[i]<<" "; //1
Printf(a,i-1);
cout<<endl;
cout<<a[i]<<" "; //2
}
int main()
{
int a[4]={1,2,3,4};
Printf(a,3);
return 0;
}
一般把不使用优化算法、直接用朴素算法来解决问题的做法称为暴力法。
朴素算法:枚举所有情况,然后判断每一种情况是否合法的做法称为朴素算法。
如果在到达递归边界前的某层,由于一些事实导致已经不需要往任何一个子问题递归,就可以直接返回上一层,这种方法称为回coiifaffx法递归
1,分治
概念:分治法将原问题划分成若干规模较小而结构与原问题相同或相似的子问题,然后分别解决这些子问题,最后合并子问题的解,即可得到为原问题的解。
由上可知,分治法可以分为三个步骤:
1)分解:将原问题分解为若干和原问题拥有相同或相似结构的子问题
2)解决:递归求解所有子问题,如果存在子问题的规模较小到可以直接解决,就直接解决它
3)合并:将子问题解法合并为原问题
⚠️:分治法分解出的子问题应该是相互独立、没有交叉的,如果存在两个子问题有相交部分,那么不应当使用分治法解决。
广义上来说,分治法分解成的子问题个数只要大于0即可;严格意义上,一般吧子问题个数为1的情况称为减治,而把子问题个数大于1的情况称为分治。
另外分治作为一种思想,既可以通过递归手段实现,也可以通过分递归的手段实现。
2,递归
递归很适合来实现分治思想
递归中两个重要概念:
1)递归边界——分解的尽头
2)递归式(或称递归调用)——将原问题分解为若干子问题的手段
经典例子:使用递归求解n的阶乘
n! = 1 * 2* 3*…*n,这个式子写成递归式就是n! = (n -1)! * n,
就把问题规模为n的问题转换为求解规模为n -1的问题。
如果用F(n)表示n!,就可以写成F(n) = F(n -1)*n【递归式】,
这样就把F(n)变成F(n-1),又把F(n-1)变成F(n-2),这样一直减小规模;
因为0! = 1,因此不妨以F(0)=1作为递归边界,即当规模减小至n=0的时候开始“回头”。
参考代码:
另一个经典案例:求斐波那契数列的第n项
Fibonacci数列是满足F(0) = 1,F(1) = 1,F(n) =F(n-1)+F(n-2)【n>=2】的数列
参考代码:
全排列(Full Permutation)
把1~n这个n个整数按某种顺序摆放的结果称为这n个整数的一个排列,而全排列指这n个整数能形成的所有排列。
现在需要实现按字典序从小到大的顺序输出1~n的全排列,其中(a1,a2,…,an)的字典序小于(b1,b2,…,bn)是指存在一个i,使得a1=b1、a2=b2、…、ai-1=bi-1、ai<bi成立。
例如对1、2、3这三个整数来说,(1,2,3)、(1,3,2)、(2,1,3),(2,3,1)、(3,1,2)、(3,2,1)就是1~3的全排列。
一般把不使用优化算法、直接用朴素算法来解决问题的做法称为暴力法。
朴素算法:枚举所有情况,然后判断每一种情况是否合法的做法称为朴素算法。
如果在到达递归边界前的某层,由于一些事实导致已经不需要往任何一个子问题递归,就可以直接返回上一层,这种方法称为回coiifaffx法
3,应用
求n皇后问题
八皇后问题是一个以国际象棋为背景的问题:如何能够在8×8的国际象棋棋盘上放置八个皇后,使得任何一个皇后都无法直接吃掉其他的皇后?为了达到此目的,任两个皇后都不能处于同一条横行、纵行或斜线上。八皇后问题可以推广为更一般的n皇后摆放问题:这时棋盘的大小变为n×n,而皇后个数也变成n。当且仅当n = 1或n ≥ 4时问题有解。
上图的a的排列为24135 ,b的排列为35142
参考代码:
//n皇后问题(未优化)
#include<cstdio>
//#include<cmath> //求浮点数的abs
#include<cstdlib> //求整数的abs
int count =0;
const int maxn = 11;
int hashTable[maxn] = {false},P[maxn];
int n;
void generatP(int index){
if(index == n + 1){ //递归边界产生排列
bool flag = true; //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) count++; //如果当前方案合法,令count加1
return;
}
for (int x = 1; x <= n; ++x)
{
if(hashTable[x] == false){
P[index] = x;
hashTable[x] = true;
generatP(index + 1);
hashTable[x] = false;
}
}
}
int main(int argc, char const *argv[])
{
n = 8;
generatP(1);
printf("%d\n", count);
return 0;
}
优化后的代码(当前面放置的皇后产生冲突时【如b图的351】,无论后面怎么放也还是冲突,就没有必要继续递归了,然后回溯):
//n皇后,优化
#include<cstdio>
#include<cstdlib>
const int maxn = 11;
int P[maxn], hashTable[maxn] = {false};
int n, count = 0;
void generateP(int index){
if(index == n + 1){ // 递归边界
count++; //能达到这里的一定是合法的
return;
}
for (int x = 1; x <= n; ++x) //第x行
{
if(hashTable[x] == false){ //第x后还有皇后
bool flag = true; //flag为true表示当前皇后不会和之前的皇后冲突
for (int pre = 1; pre < index; ++pre) // 遍历之前的皇后
{
if(abs(index - pre) == abs(x - P[pre])){
//第index列皇后的行号为x,第pre列的皇后的行号P[pre]
flag = false; // 与之前的皇后在一条对角线,冲突
break;
}
}
if(flag){ //如果可以把皇后放在第x行
P[index] = x; //令第index列的皇后的行号为x
hashTable[x] = true; //第x行已被占用
generateP(index + 1); //递归处理完第index+1行皇后
hashTable[x] = false; // 递归完毕,还原第x行未占用
}
}
}
}
int main(int argc, char const *argv[])
{
n = 5;
generateP(1);
printf("%d\n", count);
return 0;
}