概论
搜索与回溯算法,就像是在迷宫里不断寻找正确前进路径的方式。
搜索与回溯也是信息学竞赛中常会有的算法(algorithm)。于是,诸位作为新生代的大佬,哪里能不好好学习一下搜索回溯算法呐?!!!
这里讲的是深度搜索策略(dfs);
dfs就是一条路走到黑,不撞南墙不回头!!!
就类似迷宫问题: 进入迷宫后,先随意选择一个前进方向,一步步向前试探前进,如果碰到死胡同,说明前进方向已无路可走,这时,首先看其它方向是否还有路可走,如果有路可走,则沿该方向再向前试探;如果已无路可走,则返回一步,再看其它方向是否还有路可走;如果有路可走,则沿该方向再向前试探。按此原则不断搜索回溯再搜索,直到找到新的出路或从原路返回入口处无解为止。
递归回溯法算法框架[一]
int Search(int k)
{
for (i=1;i<=算符种数;i++)
if (满足条件)
{
保存结果
if (到目的地) 输出解;
else Search(k+1);
恢复:保存结果之前的状态{回溯一步}
}
}
递归回溯法算法框架[二]
int Search(int k)
{
if (到目的地) 输出解;
else
for (i=1;i<=算符种数;i++)
if (满足条件)
{
保存结果;
Search(k+1);
恢复:保存结果之前的状态{回溯一步}
}
}
以上是两个框架,就是搜索回溯算法的框架,看不懂吗?没关系,结合着它一起看看题目和代码。
例题:
例题一
设有n个整数的集合{1,2,…,n},从中取出任意r个数进行排列(r<=n),试列出所有的排列。
输入:一行,n和r。
输出:所有的排列,每种占一行。
输入样例:
3 3
输出样例:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
6
我们分析一下这个题哈:首先,你要从1开始搜,搜啊搜,搜啊搜,当1的情况全部搜完事时,再从2开始搜········
以下是参考程序:
#include<iostream>
#include<cstdio>
#include<iomanip>
//库文件名
using namespace std;
int num=0,a[10001]={0},n,r;
//先定义一下全局变量 ,b数组是bool类型、用于判断,以防止搜重
bool b[100001]={0};
int search(int);
int print();
//以上是函数声明
int main()
{
cin>>n>>r;
//取到n,r得值
search(1);
//开始搜索,从"1"开始
cout<<num<<endl;
}
int search(int k){
int i;
for(i=1;i<=n;i++)//n个数,从一到n都搜索一遍,for循环内的是搜索以ni为开头的数 (定义ni是第i个数)
if(!b[i]){
a[k]=i;
//自己手动模拟,你就明白了
b[i]=1;
//防止搜重
if(k==r) print();
else search(k+1);
//没到目的地,那就继续搜
b[i]=0;
//回溯,很关键,特别关键,超超级关键(重要的词说三遍;
}
}
int print()
//这是一个输出含数
{
num++;
//看输出了多少个排列
for(int i=1;i<=r;i++)
cout<<setw(3)<<a[i];
//setw(3)是以空三个格形式输出
cout<<endl;
}
这里是这个程序运行情况
注意:搜索与回溯这一方面的算法,是需要大量的模拟 的;
模拟模拟模拟,特别重要哦!!!!!
例题二:
任何一个大于1的自然数n,总可以拆分成若干个小于n的自然数之和。
输入:n
输出:n这个数的拆分方式,并输出总共有多少种拆分方式。
输入样例:
7
输出样例:
7=1+1+1+1+1+1+1
7=1+1+1+1+1+2
7=1+1+1+1+3
7=1+1+1+2+2
7=1+1+1+4
7=1+1+2+3
7=1+1+5
7=1+2+2+2
7=1+2+4
7=1+3+3
7=1+6
7=2+2+3
7=2+5
7=3+4
total=14
以下为参考程序;
#include<iostream>
#include<fstream>
int a[10001]={1},n,total;
using namespace std;
ifstream fin ("in.in");
ofstream fout ("out.out");
int print(int t){
//这是一个输出函数
fout<<n<<"=";
for(int i=1;i<=t-1;i++) fout<<a[i]<<"+";
fout<<a[t]<<endl;
total++;
}
int search(int s,int t){
int i;
for(i=a[t-1];i<=s;i++){
if(i<n){
a[t]=i;
s-=i;
if(s==0) print(t);
//如果到达目的地的话,就输出
else search(s,t+1);
//如果没到目的地的话,就在搜索下一条路。
s+=i;
//回溯阶段,很重要很重要很重要
}
}
}
int main(){
fin>>n;
search(n,1);
//搜索
fout<<total;
}
这里是程序运行结果,用文件流方式写的
这两个的例题是很经典的,一定要自已模拟一下,手动模拟!!!!
八皇后问题:
要在国际象棋棋盘中放八个皇后,使任意两个皇后都不能互相吃。(提示:皇后能吃同一行、同一列、同一对角线的任意棋子。)
放置第i个(行)皇后的算法为:
int search(i);
{
int j;
for (第i个皇后的位置j=1;j<=8;j++ ) //在本行的8列中去试
if (本行本列允许放置皇后)
{
放置第i个皇后;
对放置皇后的位置进行标记;
if (i==8) 输出 //已经放完个皇后
else search(i+1); //放置第i+1个皇后
对放置皇后的位置释放标记,尝试下一个位置是否可行;
}
}
显然问题的关键在于如何判定某个皇后所在的行、列、斜线上是否有别的皇后;可以从矩阵的特点上找到规律,如果在同一行,则行号相同;如果在同一列上,则列号相同;如果同在/ 斜线上的行列值之和相同;如果同在\ 斜线上的行列值之差相同;从下图可验证:
考虑每行有且仅有一个皇后,设一维数组A[1..8]表示皇后的放置:第i行皇后放在第j列,用A[i]=j来表示,即下标是行数,内容是列数。例如:A[3]=5就表示第3个皇后在第3行第5列上。判断皇后是否安全,即检查同一列、同一对角线是否已有皇后,建立标志数组b[1..8]控制同一列只能有一个皇后,若两皇后在同一对角线上,则其行列坐标之和或行列坐标之差相等,故亦可建立标志数组c[1..16]、d[-7..7]控制同一对角线上只能有一个皇后。
如果斜线不分方向,则同一斜线上两皇后的行号之差的绝对值与列号之差的绝对值相同。在这种方式下,要表示两个皇后I和J不在同一列或斜线上的条件可以描述为:A[I]<>A[J] AND ABS(I-J)<>ABS(A[I]-A[J]){I和J分别表示两个皇后的行号}
下面放代码:
#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<iomanip>
using namespace std;
bool d[100]={0},b[100]={0},c[100]={0};
int sum=0,a[100];
int search(int);
int print();
int main()
{
search(1); //从第1个皇后开始放置
}
int search(int i)
{
int j;
for (j=1;j<=8;j++) //每个皇后都有8位置(列)可以试放
if ((!b[j])&&(!c[i+j])&&(!d[i-j+7])) //寻找放置皇后的位置
//由于C++不能操作负数组,因此考虑加7
{ //放置皇后,建立相应标志值
a[i]=j; //摆放皇后
b[j]=1; //宣布占领第j列
c[i+j]=1; //占领两个对角线
d[i-j+7]=1;
if (i==8) print(); //8个皇后都放置好,输出
else search(i+1); //继续递归放置下一个皇后
b[j]=0; //递归返回即为回溯一步,当前皇后退出
c[i+j]=0;
d[i-j+7]=0;
}
}
int print()
{
int i;
sum++; //方案数累加1
cout<<"sum="<<sum<<endl;
for (i=1;i<=8;i++) //输出一种方案
cout<<setw(4)<<a[i];
cout<<endl;
}