算法之回溯与剪枝(Backtracking and pruning)

  • 思想:将回溯法与分支限界法原理结合、应用访问,用剪枝来排除不满足解的情况来提高算法的执行效率
  • 算法总结:回溯法的优点是可以遍历所有的解的空间,容易编程,也可以在遍历的过程中除去不满足解空间的路径,大大的减少了深度遍历所带来的资源的巨大消耗。
回溯法的步骤:
1.找出能解决问题的所有的解
2.用递归的思想来一个解一个解的进行遍历和计算
3.在计算各个解的过程中根据要求,记录题目要求的解
4.对不满足解的路径进行剔除
5.最后回溯到起点

下面是一些例题,可以帮助大家理解和学习回溯键值的算法思想

  1. 走迷宫

在一个 m×n 的迷宫里,从起点开始,依次按东(右)、南(下)、西(左)、北(上) 4 个方向探索通路,直至达到终点为止。迷宫由字符组成,W表示墙,. 表示空地,请编写程序,输出你找到的首条通道。
思路:从起点时开始出发依次按照右下左上的顺序进行深度探索(用一个数组来存储依次遍历的顺序),如果在遍历的过程中出现坐标越界或者进入“死胡同”(其它的三个方向都是墙)那么就回退到上一个位置,再继续探索下一个位置,直到走到迷宫出口。
样例1:
5 7
W W . W W W W
W . . . W . .
W . W W W . W
W . W . . . W
W W W . W W W
0 2
4 3
输出:
None
样例2:
5 7
W W . W W W W
W . . . W . .
W . W W W . W
W . . . W . W
W W W . W W W
0 2
4 3
输出:
W W * W W W W
W * * o W . .
W * W W W . W
W * * * W . W
W W W * W W W
代码如下:

#include<bits/stdc++.h>
using namespace std;
#define check(i,j,m,n) i<1||j<1||i>m||j>nconst int ma=1000;
char my_place[ma][ma];
int is_cross[ma][ma];
int m,n,x,y,x2,y2,t;
int my_move[4][2]={{0,1},{1,0},{0,-1},{-1,0}};void walk(int i,int j);
bool no_path(int i,int j);int main(){cin>>m>>n;for(int i=1;i<=m;i++)for(int j=1;j<=n;j++)cin>>my_place[i][j];cin>>x>>y>>x2>>y2;x+=1;y+=1;x2+=1;y2+=1;walk(x,y);if(t==0) cout <<"None"<<endl;else{for(int i=1;i<=m;i++){for(int j=1;j<=n;j++)if(j==1) cout<<my_place[i][j];else cout<<" "<<my_place[i][j];cout<<endl;}}return 0;
}bool no_path(int i,int j){if(is_cross[i-1][j]==1&&my_place[i][j+1]=='W'&&my_place[i+1][j]=='W'&&my_place[i][j-1]=='W')return true;else if(is_cross[i][j+1]==1&&my_place[i+1][j]=='W'&&my_place[i][j-1]=='W'&&my_place[i-1][j]=='W')return true;else if(is_cross[i+1][j]==1&&my_place[i][j+1]=='W'&&my_place[i-1][j]=='W'&&my_place[i][j-1]=='W')return true;else if(is_cross[i][j-1]==1&&my_place[i-1][j]=='W'&&my_place[i][j+1]=='W'&&my_place[i+1][j]=='W')return true;return false;
}
void walk(int i,int j){if(i==x2&&j==y2){is_cross[i][j]=1;my_place[i][j]='*';t=1;return;}if(no_path(i,j)) {my_place[i][j]='o';return;}for(int j1=0;j1<4;j1++){int newx=i+my_move[j1][0];int newy=j+my_move[j1][1];if(t==1||check(newx,newy,m,n)) return;if(my_place[newx][newy]!='W'&&is_cross[newx][newy]==0) {is_cross[i][j]=1;my_place[i][j]='*';walk(newx,newy);}}
}
  1. 0/1背包问题

0/1背包问题。给定一载重量为W的背包及n个重量为wi、价值为vi的物体,1≤i≤n,要求而且重量和恰好为W具有最大的价值。
思路:这道题本质是全排列的问题,即罗列出所有的可能的选择的组合,然后计算出每个组合的价值,最后求出最大的价值即可。
输入样例1:
5 10
2 6
2 3
6 5
5 4
4 6
输出样例1:
1 2 5
15
输入样例2:
2 10
11 2
13 100
输出样例2:
No
0
代码如下:

// Created by Chenglong Shi on 2021/11/3.
// Only can use to study
// Once found commercial or illegal use will be pursued to the end
// Banning plagiarism
// Email:2230307855@qq.com
// 内部可能含有拼音和汉语注释
// by 史成龙
// 方法:
//
#include<bits/stdc++.h>
using namespace std;const int ma=50;struct goods{int weight,space;goods(){weight=space=0;}
};goods things[ma];
int path[ma];
int dp[ma][ma];
int w,n,top,best;void put_goods();
void get_path();int main(){cin>>n>>w;for(int i=1;i<=n;i++) cin>>things[i].space>>things[i].weight;put_goods();best=dp[n][w];get_path();if(!best){cout<<"No"<<endl;cout<<"0";}else{for(int i=top-1;i>=0;i--)cout<<path[i]<<" ";cout<<"\n"<<best;}return 0;
}void put_goods(){for(int i=1;i<=n;i++){for(int j=1;j<=w;j++){if(things[i].space>j) dp[i][j]=dp[i-1][j];elsedp[i][j]=max(dp[i-1][j],dp[i-1][j-things[i].space]+things[i].weight);}}
}
void get_path(){int k=w;for(int i=n;i>=1;i--) {if (dp[i][k] == (dp[i - 1][k - things[i].space] + things[i].weight)) {path[top++] = i;k = w - things[i].space;}}
}
  1. 集合的划分

集合的划分
思路:这是一个贝尔数的应用例子,
1
1 2
2 3 5
5 5 8 13….
用数组进行存储a[n][n]就是要求的解
输入样例:
5
输出样例:
52
代码如下:

#include<bits/stdc++.h>
using namespace std;const int ma=5e3+1;int bell[ma][ma];
int n;void init();int main(){cin>>n;init();//打表cout<<bell[n][n]; return 0;
}void init(){bell[1][1]=1; bell[2][1]=1; bell[2][2]=2;bell[3][1]=2; bell[3][2]=3; bell[3][3]=5;for(int i=4;i<=n;i++){int begin=bell[i-1][i-1];int be=0,en=0;for(int j=1;j<=i;j++){if(j==1) {bell[i][j]=begin;be=begin;}else if(j==i){bell[i][j]=be+en;}else{bell[i][j]=bell[i][j-1]+bell[i-1][j-1];if(j==(i-1)){en=bell[i][j];} }}}
}
  1. 半数集

给定一个自然数n,由n 开始可以依次产生半数集set(n)中的数如下(注意半数集是多重集)。
1. n∈set(n);
2. 在n 的左边加上一个自然数,但该自然数不能超过最近添加的数的一半;
3. 按此规则进行处理,直到不能再添加自然数为止。
例如,set(6)={6,16,26,126,36,136}。半数集set(6)中有6 个元素。

思路:每一个数的半数集都与前面的数到此数的一半相对应,然后加上1就是当前数的半数集的个数
输入样例:
6
输出样例:
6
代码如下:

#include<bits/stdc++.h>
using namespace std;int n;
const int ma=61;
int key[ma];void init();//打表计算int main(){init();cin>>n;cout<<key[n];return 0;
}void init(){key[1]=1;for(int i=2;i<=ma;i++){for(int j=1;j<=i/2;j++)key[i]+=key[j];key[i]++;}
}
  1. 排列问题

(0<m≤26) 个大写字母中任意选出 n (0<n≤m) 个字母排成一行,一共有多少种排列?请编写程序,输入 m 和 n,输出从 A 开始的连续 m 个字母中任取 n 个字母的所有排列。
要求:每行输出一个排列,按字典序输出.

思路:先用char数组存储A到N的数据,然后用一个全局变量来表示当前读到第几个字母,如果读到N就回退再读其它的方案,直到所有的方案读取完毕。
输入样例:
3 2
输出样例:
AB
AC
BA
BC
CA
CB
代码如下:

// Created by Chenglong Shi on 2021/10/31.
// Only can use to study
// Once found commercial or illegal use will be pursued to the end
// Banning plagiarism
// Email:2230307855@qq.com
// 内部可能含有拼音和汉语注释
// by 史成龙
// 方法:回溯+全局变量控制
//
#include<stdio.h>const int ma=30;
int vis[ma],m,n,my_index=0;
char val[ma];
char re[ma];void show_permutation(int num);
void show_result();int main(){scanf("%d %d",&m,&n);for(int i=65;i<(65+m);i++) val[i-65]=(char)i;show_permutation(0);return 0;
}void show_result(){for(int i=0;i<n;i++) printf("%c",re[i]);printf("\n");
}
void show_permutation(int num){if(num>=n){show_result();return;}for(int i=0;i<m;i++){if(!vis[i]){re[my_index++]=val[i];vis[i]=1;show_permutation(num+1);vis[i]=0;my_index--;}}
}