前言
有了前面的基础,现在正式开始学习基本算法。今天介绍递归和回溯。本文主要介绍python和c++。
一、递归思想
递归的思想是把一个大型复杂问题层层转化为一个与原问题规模更小的问题,问题被拆解成子问题后,递归调用继续进行,直到子问题无需进一步递归就可以解决的地步为止。
这里给出基本框架:
python:
# python
def back(a,b):
if a==0 or b==0: # 出口
return
else:
return back(a,b)+back(a-1,b) # 子问题的拆分
c++:
//c++
int back(int a,int b){
if(a==0 || b==0) return 1;
return back(a,b-1)+ back(a-1,b);
}
求1-100的和:
python:
# python
# 1-100的和
def sumf(n):
if n == 1: # 出口
return 1
return n + sumf(n - 1) # 问题拆分
print(sumf(100))
# 输出5050
c++:
//c++
#include "iostream"
using namespace std;
int back(int n){
if(n==1) return 1;
return n+ back(n-1);
}
int main(){
cout<< back(100);
return 0;
}
//结果5050
又列如求n的阶乘:
python:
# python
# n的阶乘
def sumf(n):
if n == 1:
return 1
return n * sumf(n - 1)
print(sumf(5))
# 输出120
c++:
//c++
#include "iostream"
using namespace std;
int back(int n){
if(n==1) return 1;
return n* back(n-1);
}
int main(){
cout<< back(5);
return 0;
}
//结果120
Fibonacci
再举一个例子斐波拉契数列,1,1,2,3,5,8,13,21,34…这样的数列称为斐波拉契数列,递推式f[n]=f[n-1]+f[n-2],那么知道了递推式就可以用递归来解决了
python:
# python
# 斐波拉契数列第n项
def Fibonacci(n):
if n == 1 or n == 2:
return 1
return Fibonacci(n - 1) + Fibonacci(n - 2)
print(Fibonacci(10))
# 第10项55
# 输出55
c++:
//c++
#include "iostream"
using namespace std;
int Fibonaccik(int n){
if(n==1 || n==2 ) return 1;
return Fibonaccik(n-1)+ Fibonaccik(n-2);
}
int main(){
cout<< Fibonaccik(10);
return 0;
}
//结果55
最大公约数
这里有个经典的就是用辗转相除法求两个数的最大公约数
python:
#最大公约数
def gcd(n, m):
if m <= n and m == 0:
return n
elif n < m:
return gcd(m, n)
else:
return gcd(m, n % m)
n, m = map(int, input('请输入两个不为0的数:').split())
print('最大公约数:', end='')
print(gcd(n, m))
c++:
#include "iostream"
using namespace std;
int gcd(int x,int y){
if(y<=x && y==0) return x;
else if(x<y) return gcd(y,x);
else return gcd(y,x%y);
}
int main(){
int x,y;
cout<<"输入两个不为0的数:";
cin>>x>>y;
cout<<"最大公约数:"<< gcd(x,y);
return 0;
}
这里简单说下递归是用栈来存的,数据过大可能会导致栈爆了,而且不难发现在递归的时候会进行大量的重复运算,所以时间复杂度也是很高的
二、回溯思想
回溯就是回头的意思。比如我们要到一个终点,而去这个终点的路有很多条,那么程序就会一条一条的去尝试,如果走不通再回到原点继续走。一般的解题方法是把问题描述成树的形式,通过树的关系找出子问题的递归表达式,求可行解,在找可行解的过程中可以进行剪枝提高效率,剪枝就是在找解的过程中如果明显不满足可行解就直接终止。那这里介绍几种常见的模板,排列与组合。
全排列:
python:
# 全排列 例如我们生成1-4的全排列
vis = [False] * 5 # 标记数组,看是否被搜索过
res = [] # 结果,将找到的每一种存放到res中
def backtrack(t, path): # k表示深度,path表示临时存放一种组合的数组
if t > 4: # 出口,也就是叶子节点
res.append(path[:]) # 将一种结果存放到res中
return # 结束
for i in range(1, 5): # 1-4的全排列
if not vis[i]: # 如果没有使用过当前数字
vis[i] = True # 选择
path.append(i) # 将该数添加到path中
backtrack(t + 1, path) # 下一步搜索
path.pop()
vis[i] = False # 回溯
backtrack(1, [])
for i in res:
print(i)
结果:
[1, 2, 3, 4]
[1, 2, 4, 3]
[1, 3, 2, 4]
[1, 3, 4, 2]
[1, 4, 2, 3]
[1, 4, 3, 2]
[2, 1, 3, 4]
[2, 1, 4, 3]
[2, 3, 1, 4]
[2, 3, 4, 1]
[2, 4, 1, 3]
[2, 4, 3, 1]
[3, 1, 2, 4]
[3, 1, 4, 2]
[3, 2, 1, 4]
[3, 2, 4, 1]
[3, 4, 1, 2]
[3, 4, 2, 1]
[4, 1, 2, 3]
[4, 1, 3, 2]
[4, 2, 1, 3]
[4, 2, 3, 1]
[4, 3, 1, 2]
[4, 3, 2, 1]
c++:
#include "iostream"
using namespace std;
int vis[5],res[5];
void backtrack(int k){
if(k>=5){
for(int i=1;i<5;i++)
cout<<res[i]<<" ";
cout<<endl;
return;
}
for(int i=1;i<=4;i++){
if(vis[i]) continue;
vis[i]++;
res[k]=i;
backtrack(k+1);
vis[i]--;
}
}
int main(){
backtrack(1);
return 0;
}
结果:
1 2 3 4
1 2 4 3
1 3 2 4
1 3 4 2
1 4 2 3
1 4 3 2
2 1 3 4
2 1 4 3
2 3 1 4
2 3 4 1
2 4 1 3
2 4 3 1
3 1 2 4
3 1 4 2
3 2 1 4
3 2 4 1
3 4 1 2
3 4 2 1
4 1 2 3
4 1 3 2
4 2 1 3
4 2 3 1
4 3 1 2
4 3 2 1
组合(放回抽样)
我们发现这样求出来的结果是有顺序的,那怎样实现组合问题呢?也就是从m个排列中选择n个数,那其实我们只需要把出口改下基于可以了
第一种:
# 组合
vis = [False] * 5 # 标记数组,看是否被搜索过
res = [] # 结果,将找到的每一种存放到res中
def backtrack(t, path, n): # k表示深度,path表示临时存放一种组合的数组,n表示选择的个数
if t >n:
res.append(path[:])# 将一种结果存放到res中
return
if t > 4: # 叶子节点直接退出
return # 结束
for i in range(1, 5): # 1-4的全排列
if not vis[i]: # 如果没有使用过当前数字
vis[i] = True # 选择
path.append(i) # 将该数添加到path中
backtrack(t + 1, path,n) # 下一步搜索
path.pop()
vis[i] = False # 回溯
backtrack(1, [], 3) # 例如从1-4中选三个数
for i in res:
print(i)
第二种:
```cpp
vis = [False] * 5 # 标记数组,看是否被搜索过
res = [] # 结果,将找到的每一种存放到res中
def backtrack(t, path, n): # k表示深度,path表示临时存放一种组合的数组,n表示选择的个数
if t > 4: # 叶子节点直接退出
res.append(path[:n]) # 选择的n个数
return # 结束
for i in range(1, 5): # 保证前面的数小于后面的数
if not vis[i]: # 如果没有使用过当前数字
vis[i] = True # 选择
path.append(i) # 将该数添加到path中
backtrack(t + 1, path,n) # 下一步搜索
path.pop()
vis[i] = False # 回溯
backtrack(1, [], 3) # 例如从1-4中选三个数
for i in res:
print(i)
结果一样的
```cpp
[1, 2, 3]
[1, 2, 4]
[1, 3, 2]
[1, 3, 4]
[1, 4, 2]
[1, 4, 3]
[2, 1, 3]
[2, 1, 4]
[2, 3, 1]
[2, 3, 4]
[2, 4, 1]
[2, 4, 3]
[3, 1, 2]
[3, 1, 4]
[3, 2, 1]
[3, 2, 4]
[3, 4, 1]
[3, 4, 2]
[4, 1, 2]
[4, 1, 3]
[4, 2, 1]
[4, 2, 3]
[4, 3, 1]
[4, 3, 2]
c++:
#include "iostream"
using namespace std;
int vis[5],res[5];
void backtrack(int k,int n){
if(k>n){
for(int i=1;i<=n;i++)
cout<<res[i]<<" ";
cout<<endl;
return;
}
for(int i=1;i<5;i++){//1到4的全排列
if(vis[i]) continue;
vis[i]++;
res[k]=i;
backtrack(k+1,n);
vis[i]--;
}
}
int main(){
backtrack(1,3); //从1-4中选3个
return 0;
}
结果:
1 2 3
1 2 4
1 3 2
1 3 4
1 4 2
1 4 3
2 1 3
2 1 4
2 3 1
2 3 4
2 4 1
2 4 3
3 1 2
3 1 4
3 2 1
3 2 4
3 4 1
3 4 2
4 1 2
4 1 3
4 2 1
4 2 3
4 3 1
4 3 2
组合(不放回抽样)
这里我们发现其中的数字是又重复的,也就是放回抽样,那么我们应该怎样让它不放回抽样呢?,答案很简单,只需要让前面的数小于后面的数就可以了
不放回抽样的组合:
# 从1-4种选3个数的不重复组合问题
vis = [False] * 5 # 标记数组,看是否被搜索过
res = [] # 结果,将找到的每一种存放到res中
def backtrack(t, path, n): # k表示深度,path表示临时存放一种组合的数组,n表示选择的个数
if len(path) == n:
res.append(path[:]) # 选择的n个数
return # 结束
for i in range(t, 5): # 保证前面的数小于后面的数
if not vis[i]: # 如果没有使用过当前数字
vis[i] = True # 选择
path.append(i) # 将该数添加到path中
backtrack(i + 1, path, n) # 下一步搜索
path.pop()
vis[i] = False # 回溯
backtrack(1, [], 3) # 例如从1-4中选三个数
for i in res:
print(i)
结果
[1, 2, 3]
[1, 2, 4]
[1, 3, 4]
[2, 3, 4]
c++:
#include "iostream"
#include "vector"
using namespace std;
int vis[5];
vector<int>res;
void backtrack(int k,int n){
if(res.size()==n){
for(int i=0;i<n;i++)
cout<<res[i]<<" ";
cout<<endl;
return;
}
for(int i=k;i<5;i++){//1到4的全排列
if(vis[i]) continue;
vis[i]++;
res.emplace_back(i);
backtrack(i+1,n); //保证前面的数小于后面的数
vis[i]--;
res.pop_back();
}
}
int main(){
backtrack(1,3); //从1-4中选3个
return 0;
}
结果:
1 2 3
1 2 4
1 3 4
2 3 4
小结
这里的话差不多能理解递归和回溯基本算法了,接下来我会给出列题和蓝桥杯真题关于递归和回溯的。下一篇递归和回溯(续)