全排列

说到递归,我会想到汉诺塔,n 皇后,还有就是全排列了。当初第一次看这个全排列,看不懂,这么短的代码竟然可以实现全排列的功能,今天就当复习一下。

#include <bits/stdc++.h>
using namespace std ;
int n ;
int a[105] ;

void DFS( int l ){
	if( l == n ){
		for( int i = 0 ; i < n ; ++i )
			cout << a[i] << "  " ;
		cout << endl ;
		return ;
	}
	for( int i = l ; i < n ; ++i ){
		swap( a[l] , a[i] ) ;
		DFS( l+1 ) ;
		swap( a[l] , a[i] ) ;
	}
}

int main(){
	cin >> n ;
	for( int i = 0 ; i < n ; ++i )
		cin >> a[i] ;
	sort( a , a+n ) ;
	DFS( 0 ) ;
	return 0 ;
}

主要就是递归函数的 for 循环了。可以这样看,先看递归的下一层,也就是

for( int i = l ; i < n ; ++i ){
	swap( a[l] , a[i] ) ;
}
这个 for 循环,当前是在 l 处进行全排列,把 “不在 l 前面” 的数字都和 l 处的数字交换,作为这段的首字母,进行全排列,然后开始递归
for( int i = l ; i < n ; ++i ){
	swap( a[l] , a[i] ) ;
	DFS( l+1 ) ;
	swap( a[l] , a[i] ) ;
}
然后一路递归,然后回溯,递归出口是当前排列的数字已经到了末尾,完成一个全排列,紧接着往回走,把首字母换回来,再换下一个数字作为首字母。

这个 for 循环整体的意义就是,把所有的数字都和第一个数字进行交换,那么所有的数字都可以作为当前全排列的首字母,一次全排列之后,返回上一层,把首字母换回来,再换下一个字母。

如果是 1  2  3  4 ,for 循环的含义就是 (1 开头,234的全排列 )+ (2 开头,134的全排列 )+ (3 开头,214的全排列)+ (4 开头,231的全排列)。

递归体现在 234 的全排列 = (2 开头,34的全排列) + (3开头,24的全排列)+(4开头,32的全排列)

34的全排列 = (3开头,4的全排列)+(4开头,3的全排列)

3 的全排列  =  3 的全排列,

到这里当前排列已经到了足够的长度,就输出一组全排列。

然后,返回上一层,换回首字母,换下一个字母作为首字母做尝试。

要注意的是,每次深搜回来之后,要恢复首字母,看接下来的 1 2 2 更明显。

如下图:姑且叫做 "递归树" 吧 , 以 1 2 3 为例




那如果要排列的是  1 2  2 , 用上面的代码会得到重复的结果,两个 2 带有小号标记。



从这个图,更容易看出交换首字母,第二个排列时,1 2 2 ,带有小2的在第二位,下次 for 循环的时候,要交换成首字母的不是这个带有小2 的2,而是带有小1的2 , 因为递归回到上一层之前,要把首字母换回来。

可以看到,全排列存在重复,为什么会存在重复呢?因为在 带有小2的 2 作为首字母之前,已经有了一个带小1的2 作为首字母,这次再让 2 做首字母,就会出现重复,依次类推。

如果之前已经有了这个字母作为首字母,这次就不再让这个字母做从当前到  第n个的首字母了。

void DFS( int l ){
	if( l == n ){
		for( int i = 0 ; i < n ; ++i )
			cout << a[i] << "  " ;
		cout << endl ;
		return ;
	}
	map<int,int> One ;
	for( int i = l ; i < n ; ++i ){
		if( One[a[i]] ) continue ;   // 前面出现了 a[i] ,这次就不重复 a[i] 了
		One[a[i]] = 1 ;
		swap( a[l] , a[i] ) ;
		DFS( l+1 ) ;
		swap( a[l] , a[i] ) ;
	}
}
同时,我们可以注意到,递归得到的全排列不是按照字典序的。原因出在哪里呢?



看到 2 3  1 这个组合,在这个组合之后,换回首字母就是 123 , 然后把 3 作为首字母,交换得到 3 2 1,然后开始递归 3 开头的全排列,就在这里, 2 出现在了 1 的前面,所以 3 2 1 出现在 3 1 2 前面。

解决也很简单,在交换得到 3 2 1 时,递归 3 开头的全排列,就对后面的 2 1 进行排序,就可以满足字典序。

因为每次新换一个首字母时,以这个首字母开头的最小字典序就是首字母+后面从小到大排序,下一个字典序就是从后往前,逐渐增大字典序,直到后面的字典序到最大,也就是从大到小排序,这时候就可以换下一个字母作为首字母了,因为以原首字母开头的字典序已经全部排列完毕。

void DFS( int l ){
	if( l == n ){
		for( int i = 0 ; i < n ; ++i )
			cout << a[i] << "  " ;
		cout << endl ;
		return ;
	}
	sort( a+l , a+n ) ;   // 交换之后,回到上一层,换回首字母,后面的换成最小的字典序
	map<int,int> One ;
	for( int i = l ; i < n ; ++i ){
		if( One[a[i]] ) continue ;
		One[a[i]] = 1 ;
		swap( a[l] , a[i] ) ;
		DFS( l+1 ) ;
		swap( a[l] , a[i] ) ;
	}
}

以上的字典序也有非递归的解法。

#include <bits/stdc++.h>
using namespace std ;
int n ;
int a[105] ;

void Display( int * a ){
	for( int i = 0 ; i < n ; ++i )
		cout << a[i] << "  " ;
	cout << endl ;
}

bool Next( int *a ){            // 求当前序列的下一个字典序
	int border = n-1 , i ;
	while( border && a[border-1] >= a[border] )  // 从后往前找第一个递减的位置 border 
		--border ;
	if( border == 0 )       // 序列的所有字母都是从大到小排序,首字母最大的序列已经是最大的字典序
		return 0 ;
	i = border ;
	while( i+1 < n && a[i+1] >= a[border-1] )    // 从后面找最小的大于 a[border-1]的数
		++i ;
	swap( a[border-1] , a[i] ) ;                 // a[border-1] 就是刚递减的那个小的数字
	reverse( a+border , a+n ) ;                  // 交换之后,后面的序列已经是最大字典序,
	return 1 ;                                   // 逆转数组,就变成当前段首字母开头的最小字典序。
}

int main(){
	cin >> n ;
	for( int i = 0 ; i < n ; ++i )
		cin >> a[i] ;
	sort( a , a+n ) ;
	Display( a ) ;
	while( Next( a ) )    // 如果存在下一个字典序
		Display( a ) ;
	return 0 ;
}
假设当前首字母是 cur , cur 后面的(以cur 开头的)序列都已经是最大字典序了,从后往前看,在 cur 处会变小,这时候,从后面找一个最小的大于 cur 的数,其实很好理解,就是字典序比 cur 大的下一个字母,拿它作为首字母,这时候,和上面的递归一样,先从小到大排序,从新首字母+最小的字典序开始,重新排列,直到字典序达到最大,再换下一个首字母,重置最小字典序。。。。




  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值