摘要:本文简要描述了回溯算法的基本思路,并给出了几个典型实例的源码
关键字:回溯,搜索,非递归,全排列,组合,N皇后,整数划分,0/1背包
回溯是按照某种条件在解空间中往前试探搜索,若前进中遭到失败,则回过头来另择通路继续搜索。
符号声明:
解空间:[a1,a2,a3,...,an];
x[k]为解空间元素的索引, 0 <= x[k] < n;k为数组x的索引;
a[x[0~n-1]]表示一组解。
//判断解空间中的a[x[k]]是否满足条件
bool CanbeUsed(int k)
{
if(x[k] 不满足条件) return false;
else return true;
}
算法描述如下:
(1) k = 0; x[k] = -1;
(2)while( k >= 0 )
a. x[k] = x[k] + 1;
b. while(x[k] < n && ( ! CanbeUsed(k) ))//遍历解空间,直到找到可用的元素
x[k] = x[k] + 1;
c. if(x[k] > n -1)//x[k]超出了解空间a的索引范围
k = k - 1; //回溯
d. else if( k == n -1)//找到了n - 1个元素
输出一组解
e. else //当前元素可用,更新变量准备寻找下一个元素
k = k + 1;
x[k] = -1;
回溯的这种实现方式非常适合于在解空间中搜索特定长度的序列!
实例源码:
1.回溯之全排列(VC6.0/VS2005)==============================================
////////////////////////////////
//回溯搜索之全排列
#include<iostream>
#include<string>
using namespace std;
#define N 100
string str;
int x[N];
bool IsPlaced(int n)
{
for(int i = 0; i < n ; ++i)
{
if(x[i] == x[n])
return false;
}
return true;
}
void PrintResult()
{
for(int i = 0; i < str.length(); ++i)
cout<<str[x[i]];
cout<<endl;
}
void Arrange()
{
int k = 0; x[k] = -1;
while(k >= 0)
{
x[k] = x[k] + 1;
while(x[k] < str.length() && !IsPlaced(k))
{
x[k] = x[k] + 1;
}
if(x[k] > str.length() - 1)
{
k = k - 1;
}
else if( k == str.length() - 1)
{
PrintResult();
}
else
{
k = k + 1;
x[k] = -1;
}
}
}
int main()
{
cout<<"input:"<<endl;
while(cin>>str)
{
cout<<str<<" arrange......"<<endl;
Arrange();
cout<<"input:"<<endl;
}
return 0;
}
2.八皇后(N皇后)============================================================
////////////////////////////////////////
//回溯之N皇后问题[ 4<=N<=100]
#include <iostream>
using namespace std;
#define N 8
//用于防置皇后的棋盘
//0表示未放置,1表示已放置
int board[N][N]={
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0
};
int x[N];
//按列摆放
bool CanbePlaced(int k)
{
for(int i = 0; i < k ; ++i)
{
if(x[i] == x[k] || abs(x[i] - x[k]) == abs(i - k))
return false;
}
return true;
}
void PrintResult()
{
for(int i = 0; i < N; ++i)
for(int j = 0; j < N; ++j)
board[i][j] = 0;
for(int i = 0; i < N; ++i)
board[i][x[i]] = 1;
for(int i = 0; i < N; ++i)
{
for(int j = 0; j < N; ++j)
{
if(board[i][j] == 1)
cout<<"* ";
else
cout<<"- ";
}
cout<<endl;
}
cout<<endl;
}
int count = 0;
void NQ()
{
int k = 0;
x[k] = -1;
while( k >= 0 )
{
x[k] = x[k] + 1;
while(x[k] < N && !CanbePlaced(k))
x[k] = x[k] + 1;
if(x[k] > N - 1)
{
k = k - 1;
}
else if( k == N - 1)
{
PrintResult();
count ++;
}
else
{
k = k + 1;
x[k] = - 1;
}
}
}
int main()
{
NQ();
cout<<"一共:"<<count<<"组摆放方法"<<endl;
system("pause");
return 0;
}
3.回溯之整数划分==========================================================
/////////////////////////
//回溯之整数划分
#include<iostream>
using namespace std;
#define N 100
int x[N];
int result[N];//保存一组解
int count = 0;//解的组数
int sum(int k)
{
int sum = 0;
for(int i = 0; i <= k; ++i)
sum += result[x[i]];
return sum;
}
//a1>=a2>=...>=an
//a1+a2+...+an = n
bool IsSuit(int n,int k)
{
if(sum(k) > n)
return false;
if(k > 0 && result[x[k]] > result[x[k-1]] )
return false;
return true;
}
void PrintResult(int n,int k)
{
if(sum(k) == n)
{
for(int i = 0; i <= k; ++i)
cout<<result[x[i]]<<" ";
cout<<endl;
count++;
}
}
void SplitInt(int n)
{
//解空间[n,n-1,n-2,...,1]
for(int i = 0; i < n; ++i)
{
result[i] = n - i;
}
for(int m = 1; m <= n; ++m)
{
int k = 0;
x[k] = -1;
while( k >= 0 )
{
x[k] = x[k] + 1;
while(x[k] < n && !IsSuit(n,k) )
x[k] = x[k] + 1;
if(x[k] > n - 1)
k = k - 1;
else if( k == m - 1)
{
PrintResult(n,k);
}
else
{
k = k + 1;
x[k] = -1;
}
}
}
}
int main()
{
int num;
while(cin>>num)
{
count = 0;
SplitInt(num);
cout<<"共:"<<count<<"组"<<endl;
}
return 0;
}
4.回溯之组合===============================================================
/////////////////////////////////////////
//回溯之组合
//找出所有从m个元素中选取n(n<=m)元素的组合
#include<iostream>
using namespace std;
#define M 5
#define N 3
char elements[M]={'a','b','c','d','e'};
int x[N];
bool CanbeUsed(int k)
{
for(int i = 0; i < k; ++i)
if(x[i] == x[k])
return false;
if(k > 0 && elements[x[k]] < elements[x[k-1]])
{
return false;
}
return true;
}
void PrintResult()
{
for(int i = 0; i < N; ++i)
{
cout<<elements[x[i]]<< " ";
}
cout<<endl;
}
void Compose()
{
int k = 0;
x[k] = -1;
while( k >= 0 )
{
x[k] = x[k] + 1;
while(x[k] < M && !CanbeUsed(k))
x[k] = x[k] + 1;
if(x[k] > M - 1)
k = k - 1;
else if( k == N - 1)
{
PrintResult();
}
else
{
k = k + 1;
x[k] = -1;
}
}
}
int main()
{
Compose();
system("pause");
return 0;
}
5.回溯之0/1背包=============================================================
//////////////////////////////////////////////////
//回溯之0/1背包问题
//.0/1背包
//一个旅行者有一个最多能用m公斤的背包,现在有n件物品,它们的重量分别是W1,W2,...,Wn,它们的价值分别为C1,C2,...,Cn.
//若每种物品只有一件求旅行者能获得最大总价值。
#include<iostream>
using namespace std;
#define M 50
#define N 5
int weight[N] = {10,15,12,19,18};
int value[N] = {5,2,2,1,1};
int x[N]={-1,-1,-1,-1,-1};
int max_weight = 0;
int max_value = 0;
bool CanbeUsed(int k)
{
for(int i = 0; i < k ; ++i)
{
if(x[i] == x[k] )
return false;
}
return true;
}
void CalResult(int k)
{
int totalValue = 0;
int totalWeight= 0;
for(int i = 0 ; i <= k; ++i)
{
totalValue += value[x[i]];
totalWeight += weight[x[i]];
}
if(totalValue > max_value && totalWeight <= M )
{
max_value = totalValue;
max_weight = totalWeight;
cout<< totalWeight << " "<<totalValue<<endl;
}
}
void Bag()
{
//分别计算去1~N个物品的情况
for(int n = 1; n <= N; ++n)
{
int k = 0;
x[k] = -1;
while( k >= 0)
{
x[k] = x[k] + 1;
while(x[k] < n && !CanbeUsed(k))
x[k] = x[k] + 1;
if(x[k] > n - 1)
{
k = k - 1;
}
else if( k == n - 1)
{
CalResult(k);
}
else
{
k = k + 1;
x[k] = -1;
}
}
}
}
int main()
{
Bag();
cout<<"最优解为weight:" << max_weight << ",value:" <<max_value<<endl;
system("pause");
return 0;
}
八皇后问题是一个古老而著名的问题,是回溯算法的典型例题。该问题是19世纪著名的数学家高斯1850年提出:在8×8格的国际象棋盘上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。[英国某著名计算机图形图像公司面试题]
解析:递归实现n皇后问题。
算法分析:
数组a、b、c分别用来标记冲突,a数组代表列冲突,从a[0]~a[7]代表第0列到第7列。如果某列上已经有皇后,则为1,否则为0。
数组b代表主对角线冲突,为b[i-j+7],即从b[0]~b[14]。如果某条主对角线上已经有皇后,则为1,否则为0。
数组c代表从对角线冲突,为c[i+j],即从c[0]~c[14]。如果某条从对角线上已经有皇后,则为1,否则为0。
C++代码如下:
/* 实现N皇后问题*/
#include <stdio.h>
#define QUEENNUM 8
#define QCROSSNUM (QUEENNUM*2-1)
static char QueenArray[QUEENNUM][QUEENNUM];
static int a[QUEENNUM];
static int b[QCROSSNUM];
static int c[QCROSSNUM];
static int iQueenNum=0;//记录皇后问题总共有多少中摆法
void Queen(int i);//i为行数
int main()
{
int iLine,iColumn;
for (iLine=0;iLine<QUEENNUM;iLine++)
{
a[iLine]=0;
for(iColumn=0;iColumn<QUEENNUM;iColumn++)
QueenArray[iLine][iColumn]='*';
}
for(iLine=0;iLine<QCROSSNUM;iLine++)
b[iLine] = c[iLine]=0;
Queen(0);
return 0;
}
void Queen(int i)
{
int iColumn;
for(iColumn=0;iColumn<QUEENNUM;iColumn++)
{
if(a[iColumn]==0&&b[i-iColumn+7]==0&&c[i+iColumn]==0)
{
QueenArray[i][iColumn]='@';//皇后标志
a[iColumn]=1;
b[i-iColumn+7]=1;
c[i+iColumn]=1;
if (i<(QUEENNUM-1))
Queen(i+1);
else
{
int iLine,iColumn;
printf("The %d th state is :/n ",++iQueenNum);
for(iLine=0;iLine<QUEENNUM;iLine++)
{
for(iColumn=0;iColumn<QUEENNUM;iColumn++)
printf("%c",QueenArray[iLine][iColumn]);
printf("/n");
}
printf("/n/n");
}
//后面无论如何也无法放置皇后,则回溯重置
QueenArray[i][iColumn]='*';
a[iColumn]=0;
b[i-iColumn+7]=0;
c[i+iColumn]=0;
}
}
}
这段代码与一般的N!之类的递归大不相同, 以往都是从大到小的基本递归,如N!、打靶等等。这些方法都是采用嵌套方法, 中间没有循环,没有回溯的出现。八皇后问题显然不同,中间不但有循环,而且还有很严谨的回溯。切入点也不同,是设置行数.
程序中,改变QUEENNUM的数值,就能得到N皇后的摆法。递归结束后的处理,包括清理本行的皇后,以及相关数据,即列的皇后信息清除、主从对角线的标志设置0。回溯法中,回溯后数据清理是有一定深度和难度的。学习的好方法就是多写写采用回溯法的递归算法,多尝试用回溯的方法做一些数据清理工作。
递归算法步骤:
1.方法的选定,基本递归、分治法、动态规划和回溯法的选择哪种?
2.考虑不满足什么样的条件递归结束?
3.考虑满足条件,并且最后一次调用递归,如何处理?
4.考虑中间的满足条件状态如何处理?如递归函数要传入什么参数,处理哪些数据?调用递归函数后,要清理哪些数据,得到的数据如何处理?
基本递归法:
一个打靶问题的代码如下:
/*10枪打中90环的有多少种可能 ---- by zhaquanmin*/
#include <stdio.h>
long int sum = 0;
int storeArray[10];
void Comput(int score,int num)
{
if(score<0 || score>(num+1)*10)
return ;
if (num==0)
{
storeArray[num]=score;
for(int i=0;i<10;i++)
printf("%d "storeArray[i]);
printf("/n");
++sum;
return ;
}
for(int i=0;i<=10;++i)
{
storeArray[num]=i;
Comput(score-i,num-1);
}
}
int main()
{
Comput(90,9);
cout<<"sum="<<sum<<endl;
return 0;
}