linux 数组排列组合,排列组合与回溯算法

排列组合与回溯算法

KuiBing

感谢Bamboo、LeeMaRS的帮助

[关键字] 递归 DFS

[前言] 这篇论文主要针对排列组合对回溯算法展开讨论,在每一个讨论之后,还有相关的推荐题。在开始之前,我们先应该看一下回溯算法的概念,所谓回溯:就是搜索一棵状态树的过程,这个过程类似于图的深度优先搜索(DFS),在搜索的每一步(这里的每一步对应搜索树的第i层)中产生一个正确的解,然后在以后的每一步搜索过程中,都检查其前一步的记录,并且它将有条件的选择以后的每一个搜索状态(即第i+1层的状态节点)。

需掌握的基本算法:

排列:就是从n个元素中同时取r个元素的排列,记做P(n,r)。(当r=n时,我们称P(n,n)=n!为全排列)例如我们有集合OR = {1,2,3,4},那么n = |OR| = 4,切规定r=3,那么P(4,3)就是:

{1,2,3}; {1,2,4}; {1,3,2}; {1,3,4};{1,4,2};{1,4,3};{2,1,3};{2,1,4}; {2,3,1}; {2,3,4}; {2,4,1}; {2,4,3}; {3,1,2}; {3,1,4}; {3,2,1}; {3,2,4}; {3,4,1}; {3,4,2}; {4,1,2}; {4,1,3}; {4,2,1}; {4,2,3}; {4,3,1}; {4,3,2}

算法如下:

int  n, r;

char used[MaxN];

int  p[MaxN];

void permute(int pos)

{ int i;

/*如果已是第r个元素了,则可打印r个元素的排列 */

if (pos==r) {

for (i=0; icout << (p[i]+1);

cout << endl;

return;

}

for (i=0; iif (!used[i]) { /*如果第i个元素未用过*/

/*使用第i个元素,作上已用标记,目的是使以后该元素不可用*/

used[i]++;

/*保存当前搜索到的第i个元素*/

p[pos] = i;

/*递归搜索*/

permute(pos+1);

/*恢复递归前的值,目的是使以后改元素可用*/

used[i]--;

}

}

相关问题

UVA 524 Prime Ring Problem

可重排列:就是从任意n个元素中,取r个可重复的元素的排列。例如,对于集合OR={1,1,2,2}, n = |OR| = 4, r = 2,那么排列如下:

{1,1}; {1,2}; {1,2}; {1,1}; {1,2}; {1,2}; {2,1}; {2,1}; {2,2}; {2,1}; {2,1}; {2,2}

则可重排列是:

{1,1}; {1,2}; {2,1}; {2,2}.

算法如下:

#define FREE -1

int n, r;

/*使元素有序*/

int E[MaxN] = {0,0,1,1,1};

int P[MaxN];

char used[MaxN];

void permute(int pos)

{

int i;

/*如果已选了r个元素了,则打印它们*/

if (pos==r)  {

for (i=0; i

全排列

设R={r1,r2,...,rn}是要进行排列的n个元素,Ri = R-{ri}. 集合 X 中元素的全排列记为Perm(X)。(ri)Perm(X)表示在全排列Perm(X)的每一个排列上加前缀ri得到的排列。R的全排列可归纳定义如下:

当 n = 1 时, Perm(R) = (r),其中r 是集合R中唯一的元素;

当 n >1 时, Perm(R)有 (r1)Perm(R1),(r2)Perm(R2),.......,(rn)Perm(Rn)构成

依此递归定义,可设计产生Perm(R)的递归算法如下:

template void Perm(Type list[], int k, int m){

if ( k == m ){

for ( int i = 0; i <= m; i++)

cout << list[i];

cout << endl;

}

else{

for ( int i = k; i <= m; i ++){

Swap( list[k],list[i] );

Perm( list,k + 1, m ) ;

Swap( list[k], list[i] );

}

}

}

template < class Type >

inline void Swap ( Type &a ,Type & b)

{

Type temp = a; a = b; b = temp;

}

常用排列组合算法

尽管排列组合是生活中经常遇到的问题,可在程序设计时,不深入思考或者经验不足都让人无从下手。由于排列组合问题总是先取组合再排列,并且单纯的排列问题相对简单,所以本文仅对组合问题的实现进行详细讨论。以在n个数中选取m(01. 首先从n个数中选取编号最大的数,然后在剩下的n-1个数里面选取m-1个数,直到从n-(m-1)个数中选取1个数为止。

2. 从n个数中选取编号次小的一个数,继续执行1步,直到当前可选编号最大的数为m。

很明显,上述方法是一个递归的过程,也就是说用递归的方法可以很干净利索地求得所有组合。

下面是递归方法的实现:

/// 求从数组a[1..n]中任选m个元素的所有组合。

/// a[1..n]表示候选集,n为候选集大小,n>=m>0。

/// b[1..M]用来存储当前组合中的元素(这里存储的是元素下标),

/// 常量M表示满足条件的一个组合中元素的个数,M=m,这两个参数仅用来输出结果。

void combine( int a[], int n, int m, int b[], const int M )

{

for(int i=n; i>=m; i--)   // 注意这里的循环范围

{

b[m-1] = i - 1;

if (m > 1)

combine(a,i-1,m-1,b,M);

else                     // m == 1, 输出一个组合

{

for(int j=M-1; j>=0; j--)

cout << a[b[j]] << " ";

cout << endl;

}

}

}

因为递归程序均可以通过引入栈,用回溯转化为相应的非递归程序,所以组合问题又可以用回溯的方法来解决。为了便于理解,我们可以把组合问题化归为图的路径遍历问题,在n个数中选取m个数的所有组合,相当于在一个这样的图中(下面以从1,2,3,4中任选3个数为例说明)求从[1,1]位置出发到达[m,x] (m<=x<=n)位置的所有路径:

1 2 3 4

2 3 4

3 4

上图是截取n×n右上对角矩阵的前m行构成,如果把矩矩中的每个元素看作图中的一个节点,我们要求的所有组合就相当于从第一行的第一列元素[1,1]出发,到第三行的任意一列元素作为结束的所有路径,规定只有相邻行之间的节点,并且下一行的节点必须处于上一行节点右面才有路径相连,其他情况都无路径相通。显然,任一路径经过的数字序列就对应一个符合要求的组合。

下面是非递归的回溯方法的实现:

/// 求从数组a[1..n]中任选m个元素的所有组合。

/// a[1..n]表示候选集,m表示一个组合的元素个数。

/// 返回所有排列的总数。

int combine(int a[], int n, int m)

{

m = m > n ? n : m;

int* order = new int[m+1];

for(int i=0; i<=m; i++)

order[i] = i-1;            // 注意这里order[0]=-1用来作为循环判断标识

int count = 0;

int k = m;

bool flag = true;           // 标志找到一个有效组合

while(order[0] == -1)

{

if(flag)                   // 输出符合要求的组合

{

for(i=1; i<=m; i++)

cout << a[order[i]] << " ";

cout << endl;

count++;

flag = false;

}

order[k]++;                // 在当前位置选择新的数字

if(order[k] == n)          // 当前位置已无数字可选,回溯

{

order[k--] = 0;

continue;

}

if(k < m)                  // 更新当前位置的下一位置的数字

{

order[++k] = order[k-1];

continue;

}

if(k == m)

flag = true;

}

delete[] order;

return count;

}

下面是测试以上函数的程序:

int main()

{

const int N = 4;

const int M = 3;

int a[N];

for(int i=0;ia[i] = i+1;

// 回溯方法

cout << combine(a,N,3) << endl;

// 递归方法

int b[M];

combine(a,N,M,b,M);

return 0;

}

由上述分析可知,解决组合问题的通用算法不外乎递归和回溯两种。在针对具体问题的时候,因为递归程序在递归层数上的限制,对于大型组合问题而言,递归不是一个好的选择,这种情况下只能采取回溯的方法来解决。

n个数的全排列问题相对简单,可以通过交换位置按序枚举来实现。STL提供了求某个序列下一个排列的算法next_permutation,其算法原理如下:

1. 从当前序列最尾端开始往前寻找两个相邻元素,令前面一个元素为*i,后一个元素为*ii,且满足*i2. 再次从当前序列末端开始向前扫描,找出第一个大于*i的元素,令为*j(j可能等于ii),将i,j元素对调;

3. 将ii之后(含ii)的所有元素颠倒次序,这样所得的排列即为当前序列的下一个排列。

其实现代码如下:

template bool next_permutation(BidirectionalIterator first, BidirectionalIterator last)

{

if (first == last) return false;   // 空範圍

BidirectionalIterator i = first;

++i;

if (i == last) return false;       // 只有一個元素

i = last;                          // i 指向尾端

--i;

for(;;)

{

BidirectionalIterator ii = i;

--i;

// 以上,鎖定一組(兩個)相鄰元素

if (*i < *ii)                     // 如果前一個元素小於後一個元素

{

BidirectionalIterator j = last; // 令 j指向尾端

while (!(*i < *--j));            // 由尾端往前找,直到遇上比 *i 大的元素

iter_swap(i, j);                 // 交換 i, j

reverse(ii, last);               // 將 ii 之後的元素全部逆向重排

return true;

}

if (i == first)                   // 進行至最前面了

{

reverse(first, last);            // 全部逆向重排

return false;

}

}

}

下面程序演示了利用next_permutation来求取某个序列全排列的方法:

int main()

{

int ia[] = {1,2,3,4};

vectoriv(ia,ia+sizeof(ia)/sizeof(int));

copy(iv.begin(),iv.end(),ostream_iterator(cout," "));

cout << endl;

while(next_permutation(iv.begin(),iv.end()))

{

copy(iv.begin(),iv.end(),ostream_iterator(cout," "));

cout << endl;

}

return 0;

}

注意:上面程序中初始序列是按数值的从小到大的顺序排列的,如果初始序列无序的话,上面程序只能求出从当前序列开始的后续部分排列,也就是说next_permutation求出的排列是按排列从小到大的顺序进行的。

n取r的组合

#i nclude const int N = 9;

int a[N] = {1,2,3,4,5,6,7,8,9};

int b[N];

int r = 4;

char used[N];

int count;

void combine(int pos)

{

int i;

if(pos==r)

{

for(i = 0;i{

printf("%d ",a[b[i]]);

}

printf("\n");

count++;

return;

}

//这里是关键,第i个位置上的数,只能是从排在第i位的数开始的N-r个中的一个,后面的数一定要比前面的大

for(i = (pos>0?(b[pos-1]+1):pos);i<=pos+N-r;i++) {

if(!used[i])

{

b[pos] = i;

used[i]++;

combine(pos+1);

used[i]--;

}

}

}

int main()

{

combine(0);

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值