回溯法”也称“试探法”,是深度搜索的一种。它是从问题的某一状态出发,不断“试探”着往前走一步,当一条路走到“尽头”,不能再前进(拓展出新状态)的时候,再倒回一步或者若干步,从另一种可能的状态出发,继续搜索,直到所有的“路径(状态)”都一一试探过。这种不断前进、不断回溯,寻找解的方法,称为“回溯法”。
他的基本思想是:为了求得问题的解,先选择某一种可能情况向前搜索,在搜索过程中,一旦发现原来的选择是错误的,就退回一步重新选择,继续向前探索,如此反复进行,直到得到解或证明无解。
【组合】
例1. 给定两个整数n和k,返回1 ... n中所有可能的k个数的组合。
示例:
输入:n=4,k=2
输出:
1 2
1 3
1 4
2 3
2 4
3 4
思路
本题这是回溯法的经典题⽬。
直接的解法当然是使⽤for循环,例如示例中r为2,很容易想到⽤两个for循环,这样就可以输出和示例中⼀样的结果。
int n = 4;
for(int i=1; i<=n; i++){
for(int j=i+1; j<=n; j++){
cout<<i<<" "<<j<<endl;
}
}
输⼊:n=100,k=3那么就三层for循环,代码如下:
int n = 100;
for(int i=1; i<=n; i++){
for(int j=i+1; j<=n; j++){
for(int u=j+1; u<=n; n++){
cout<<i<<" "<<j<<" "<<u<<endl;
}
}
}
如果n为100,k为50呢,那就50层for循环...
此时就会发现虽然想暴力搜索,但是⽤for循环嵌套连暴力都写不出来!
回溯搜索法来了,虽然回溯法也是暴⼒,但⾄少能写出来,不像for循环嵌套k层让⼈绝望。那么回溯法怎么暴⼒搜呢?
上⾯我们说了要解决n为100,k为50的情况,暴力写法需要嵌套50层for循环,那么回溯就用递归来解决嵌套层数的问题。
递归来做层叠嵌套(可以理解是开k层for循环),每⼀次的递归中嵌套⼀个for循环,那么递归就可以⽤于解决多层嵌套循环的问题了。
回溯法解决的问题都可以抽象为树形结构(N叉树),⽤树形结构来理解回溯就容易多了。
那么我把组合问题抽象为如下树形结构:
可以看出这个棵树,⼀开始集合是1,2,3,4,从左向右取数,取过的数,不在重复取。
第⼀次取1,集合变为2,3,4,因为k为2,我们只需要再取⼀个数就可以了,分别取2,3,4,得到集合[1,2] [1,3] [1,4],以此类推。
每次从集合中选取元素,可选择的范围随着选择的进⾏⽽收缩,调整可选择的范围。
图中可以发现n相当于树的宽度,k相当于树的深度。
==回溯法三部曲==
- 递归函数的返回值以及参数
- 回溯函数终止条件
- 单层搜索的过程
==框架:==
本题程序如下:
//组合:不重复输出
#include<iostream>
using namespace std;
int a[50];
int n,r;
void search(int t){ //t表示第几个数
if(t>r){ //满足2个数之后就输出并终止当前搜索,回溯到上一层
for(int i=1;i<=r;i++)
cout<<a[i]<<" ";
cout<<endl;
return;
}
for(int i=1; i<=n; i++){
if(i>a[t-1]){ //判断当前是否满足条件 :不重复输出
a[t]=i;
search(t+1); //查找下一个数
}
}
}
int main()
{
cin>>n>>r;
search(1);
return 0;
}
练习1. 排列与组合是常用的数学方法,其中组合就是从n个元素中抽出r个元素(不分顺序且r<n)。 我们可以简单地将n个元素理解为自然数1,2,...,n从中任取 r 个数。
现要求你输出所有组合,例如 n=5,r=3 所有组合为:
123,124,125,134,135,145,234,235,245,345
【输入格式】
一行两个自然数n,r (1<n<21,0<r<n)。
【输出格式】
所有的组合,每一个组合占一行且其中的元素按由小到大的顺序排列,每个元素占三个字符的位置,所有的组合也按字典顺序。
程序如下:
//组合--从小到大输出
#include<iostream>
using namespace std;
int a[1005];
int n,r;
void search(int t){
if(t>r){
for(int i=1;i<=r;i++)
cout<<a[i]<<" ";
cout<<endl;
return;
}
for(int i=1; i<=n; i++){
if(i > a[t-1]){ //判断当前是否满足条件: 从小到大,新数要比上一个数大
a[t]=i; //保存结果
search(t+1);
}
}
}
int main()
{
cin>>n>>r;
search(1);
return 0;
}
练习2. 素数环:现有10个位置,要求从1到10这十个数字摆成一个环,要求相邻的数字和为素数。输出所有排列方案和方案总数。
如:1 2 3 4 7 6 5 8 9 10
题目分析:
1.总共有10个位置放置数字,那么问题就分为10步,每步确定一个位置的数字。
2.每一步有10种可能。
参考程序:
#include<bits/stdc++.h>
using namespace std;
int a[50],v[50];
int n,r,ans;
bool isPrime(int x){
for(int i=2;i<=sqrt(x);i++){
if(x%i==0) return false;
}
return true;
}
void search(int t){
if(t>10){
if(isPrime(a[1]+a[10])){ //首尾相邻也需判断
for(int i=1;i<=10;i++)
cout<<a[i]<<" ";
cout<<endl;
ans++;
}
return;
}
for(int i=1; i<=10; i++){
if(!v[i] && isPrime(i+a[t-1])){
a[t]=i;
v[i]=1;
search(t+1);
v[i]=0;
}
}
}
int main()
{
search(1);
cout<<ans;
return 0;
}
练习3. 输入n个从小到大的数,输出 r 个数的所有组合方案,无顺序区别。
【输入样例】
4 3
4 5 6 7
【输出样例】
4 5 6
4 5 7
4 6 7
5 6 7
【参考程序】
#include<iostream>
using namespace std;
int a[1005],b[1005],c[1005];
int n,r;
void search(int t){
if(t>r){
for(int i=1;i<=r; i++)
cout<<c[i];
cout<<endl;
return;
}
for(int i=1; i<=n; i++){
if(a[i]>c[t-1]){
c[t]=a[i];
search(t+1);
c[t]=0;
}
}
}
int main()
{
cin>>n>>r;
for(int i=1;i<=n;i++)
cin>>a[i];
search(1);
return 0;
}
完成练习:
信息学奥赛一本通 题号1317-1318
【排列】
例1. 给定两个整数n和k,返回1 ... n中k个数的全排列。
输入:3 2
输出:
1 2
1 3
2 1
2 3
3 1
3 2
程序如下:
#include<iostream>
using namespace std;
int a[1005],v[1005];
int n,k;
void search(int t){
if(t>k){
for(int i=1;i<=k; i++)
cout<<a[i]<<" ";
cout<<endl;
return;
}
for(int i=1; i<=n; i++){
if(!v[i]){ //不能充分输出相同的数
a[t]=i;
v[i]=1;
search(t+1); //搜索下一个数
v[i]=0; //回溯,恢复可使用状态
}
}
}
int main()
{
cin>>n>>k;
search(1);
return 0;
}
练习2. 输入n个从小到大的数,输出 r 个数的所有排列方案。
【输入样例】
3 3
7 8 9
【输出样例】
7 8 9
7 9 8
8 7 9
8 9 7
9 7 8
9 8 7
#include<iostream>
using namespace std;
int b[1005],a[1005],v[1005];
int n,k;
void search(int t){
if(t>k){
for(int i=1;i<=k; i++)
cout<<a[i]<<" ";
cout<<endl;
return;
}
for(int i=1; i<=n; i++){
if(!v[i]){ //不能充分输出相同的数
a[t]=b[i];
v[i]=1;
search(t+1); //搜索下一个数
v[i]=0; //回溯,恢复可使用状态
}
}
}
int main()
{
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>b[i];
}
search(1);
return 0;
}
例2. 八皇后问题:要在国际象棋棋盘中放八个皇后,皇后可以在横、竖、斜线上不限步数地吃掉其他棋子。如何将8个皇后放在棋盘上(有8 × 8个方格),使它们任意两个皇后不会被互相吃掉。
输出所有的方案。
【输出样例】
sum=1
1 0 0 0 0 0 0 0
0 0 0 0 0 0 1 0
0 0 0 0 1 0 0 0
0 0 0 0 0 0 0 1
0 1 0 0 0 0 0 0
0 0 0 1 0 0 0 0
0 0 0 0 0 1 0 0
0 0 1 0 0 0 0 0
sum=2
1 0 0 0 0 0 0 0
0 0 0 0 0 0 1 0
0 0 0 1 0 0 0 0
0 0 0 0 0 1 0 0
0 0 0 0 0 0 0 1
0 1 0 0 0 0 0 0
0 0 0 0 1 0 0 0
0 0 1 0 0 0 0 0
...以下省略
放置第i个(行)皇后的算法为:
int search(i){
if(i>8) 输出。
for(第i个皇后的位置 j =1; j<= 8; j++){ //在本行的8列中去试
if(本行本列允许放置皇后){
放置第i个皇后;
对放置皇后的位置进行标记;
放置第i+1个皇后
对放置皇后的位置释放标记,尝试下一个位置是否可行;
}
}
}
【算法分析】
确定三个问题:
(1)递归函数的返回值和参数(参数是第几个皇后i)
(2)回溯搜索的终止条件(皇后数i>8即终止)
(3)单层搜索的过程
搜索过程中的关键问题在于如何判定行、列、斜线上是否有别的皇后;可以从矩阵的特点上找到规律,同行行号相同,同列列号相同,同斜线行列值之和相同(行列值之差相同)。
考虑每行有且仅有一个皇后,设一维数组a[1.. 8]表示皇后的放置:第i行皇后放在第j列,用a[i]=j来表示;b[1..8]数组标识哪一列被存放过,下一个皇后
必须不同列;c[1..16]和d[-7..7]数组标识对角线上是否有皇后,下一个皇后必须不同斜线,由算法思路转换成程序如下:
【参考程序】
#include<iostream>
#include<iomanip>
using namespace std;
int a[100]; //存放放置皇后列数
bool b[100]; //标志第x列已被存放
bool c[100], d[100]; //标志斜对角是否被存放
int sum;
void search(int i){ //t表示第几个皇后
if(i>8){
sum++;
cout<<"sum="<<sum<<endl;
for(int j=1;j<=8;j++){
for(int k=1;k<=8;k++){
if(j==a[k]) cout<<1<<" ";
else cout<<0<<" ";
}
cout<<endl;
}
return;
}
for(int j=1; j<=8; j++){
if(!b[j] && !c[i+j] && !d[i-j+7]){ //不在同行 同列 对角线
a[i]=j; //摆放皇后
b[j]=1; //宣布占领第j列
c[i+j]=1; //占领两对角线
d[i-j+7]=1;
search(i+1); //继续递归放置下一个皇后
b[j]=0; //回溯
c[i+j]=0;
d[i-j+7]=0;
}
}
}
int main()
{
search(1);
return 0;
}