说到递归,我会想到汉诺塔,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 大的下一个字母,拿它作为首字母,这时候,和上面的递归一样,先从小到大排序,从新首字母+最小的字典序开始,重新排列,直到字典序达到最大,再换下一个首字母,重置最小字典序。。。。