[算法]——全排列(Permutation)以及next_permutation

Posted eudiwffe

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[算法]——全排列(Permutation)以及next_permutation相关的知识,希望对你有一定的参考价值。

排列(Arrangement),简单讲是从N个不同元素中取出M个,按照一定顺序排成一列,通常用A(M,N)表示。当M=N时,称为全排列(Permutation)。从数学角度讲,全排列的个数A(N,N)=(N)*(N-1)*...*2*1=N!,但从编程角度,如何获取所有排列?那么就必须按照某种顺序逐个获得下一个排列,通常按照升序顺序获得下一个排列。

例如对于一个集合A={1,2,3,},首先获取全排列a1: 1,2,3,;然后获取下一个排列a2: 1,3,2,;按此顺序,A的全排列如下:

a1: 1,2,3;  a2: 1,3,2;  a3: 2,1,3;  a4: 2,3,1;  a5: 3,1,2;  a6: 3,2,1;  共6种。

1)下一个全排列(Next Permutation)

对于给定的任意一种全排列,如果能求出下一个全排列的情况,那么求得所有全排列情况就容易了。好在STL中的algorithm已经给出了一种健壮、高效的方法,下面进行介绍。

设目前有一个集合的一种全排列情况A : 3,7,6,2,5,4,3,1,求取下一个排列的步骤如下:

/** Tips: next permuation based on the ascending order sort
 * sketch :
 * current: 3   7  6  2  5  4  3  1  .
 *                    |  |     |     |
 *          find i----+  j     k     +----end
 * swap i and k :
 *          3   7  6  3  5  4  2  1  .
 *                    |  |     |     |
 *               i----+  j     k     +----end
 * reverse j to end :
 *          3   7  6  3  1  2  4  5  .
 *                    |  |     |     |
 *          find i----+  j     k     +----end
 * */

具体方法为:

a)从后向前查找第一个相邻元素对(i,j),并且满足A[i] < A[j]。

易知,此时从j到end必然是降序。可以用反证法证明,请自行证明。

b)在[j,end)中寻找一个最小的k使其满足A[i]<A[k]。

由于[j,end)是降序的,所以必然存在一个k满足上面条件;并且可以从后向前查找第一个满足A[i]<A[k]关系的k,此时的k必是待找的k。

c)将i与k交换。

此时,i处变成比i大的最小元素,因为下一个全排列必须是与当前排列按照升序排序相邻的排列,故选择最小的元素替代i。

易知,交换后的[j,end)仍然满足降序排序。因为在(k,end)中必然小于i,在[j,k)中必然大于k,并且大于i。

d)逆置[j,end)

由于此时[j,end)是降序的,故将其逆置。最终获得下一全排序。

注意:如果在步骤a)找不到符合的相邻元素对,即此时i=begin,则说明当前[begin,end)为一个降序顺序,即无下一个全排列,STL的方法是将其逆置成升序。

2)Next Permutation代码

// STL next permutation base idea
int next_permutation(int *begin, int *end)
{
	int *i=begin, *j, *k;
	if (i==k || ++i==k) return 0;	// 0 or 1 element, no next permutation
	for (i=end-1; i!=begin;) {
		j = i--;	// find last increasing pair (i,j)
		if (!(*i < *j)) continue;
		// find last k which not less than i,
		for (k=end; !(*i < *(--k)););
		iter_swap(i,k);
		// now the range [j,end) is in descending order
		reverse(j,end);
		return 1;
	}
	// current is in descending order
	reverse(begin,end);
	return 0;
}

 上面仅仅是STL中next_permutation的主要思路,原版是C++迭代器版,这里为了便于理解,改成了C的指针版本。

当返回为1时,表示找到了下一全排列;返回0时,表示无下一全排列。注意,如果从begin到end为降序,则表明全排列结束,逆置使其还原到升序。

3)使用next_permutation

如何获取所有全排列情况?STL中的代码非常精妙,利用next_permutation的返回值,判断是否全排列结束(否则将死循环)。对于给定的一个数组,打印其所有全排列只需如下:

// Display All Permutation
void all_permutation(int arr[], int n)
{
	sort(arr,arr+n);	// sort arr[] in ascending order
	do{
		for(int i=0; i<n; printf("%d ",arr[i++]));
		printf("\n");
	}while(next_permutation(arr,arr+n));
}

如果一个数组arr[]中存在重复元素,next_permutation是否工作正常呢?注意第8和10行,STL使用“!(*i < *j)”进行判断大小,若相等则继续寻找,这样就会跳过重复的元素,进而跳过重复的全排列(如:1,2,2; 和1,2,2)。有人会认为直接使用“*i>=*j”更清晰,对于int这种进本数据类型而言,这并没问题。然而,对于结构体甚至C++而言,元素是一个用户自定义数据类型,如何判断其大小?再退一步讲,如何进行排序?STL追求健壮、高效和精妙,对于用户自定义数据类型的排序,可以增加函数指针或者仿函数(Functional),只需要给定“a<b”的方法(如less(a,b))即可。如需求“a>b”可以转化成“b<a”;求“a==b”可以转化成“!(a<b) && !(b<a)”;求“a>=b”可以转化成“!(a<b)”。因此,一般自定义比较器只需要给定less()即可(对于C++而言,即重载操作符operator<)。

有了全排列,那么排列问题A(M,N)则解决了一半,直接从A中选择选择M个元素,然后对这M个元素进行全排列。其中前一步为组合(Combination),记为(M,N),感兴趣的可以自己解决。

4)前一个全排列(prev_permutation)

与next_permutation类似,STL也提供一个版本:

// STL prev permutation base idea
int prev_permutation(int *begin, int *end)
{
	int *i=begin, *j, *k;
	if (i==k || ++i==k) return 0;	// 0 or 1 element, no prev permutation
	for (i=end-1; i!=begin;) {
		j = i--;	// find last decreasing pair (i,j)
		if (!(*i > *j)) continue;
		// find last k which less than i,
		for (k=end; !(*i > *(--k)););
		iter_swap(i,k);
		// now the range [j,end) is in ascending order
		reverse(j,end);
		return 1;
	}
	// current is in ascending order
	reverse(begin,end);
	return 0;
}

这里不再详细介绍。

以上是关于[算法]——全排列(Permutation)以及next_permutation的主要内容,如果未能解决你的问题,请参考以下文章

全排列permutation

STL中关于全排列next_permutation以及prev_permutation的用法

简单算法汇总

next_permutation(全排列算法)

简单算法汇总

next_permutation(全排列算法)