关于回溯算法的递归与非递归解法

摘要:本文简要描述了回溯算法的基本思路,并给出了几个典型实例的源码

关键字:回溯,搜索,非递归,全排列,组合,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;
}

你可能感兴趣的:(c,算法,String,面试,System,input)