全排列函数和自写排列

预告:我用两年写的新书《算法竞赛》,已于2022年2月交给清华大学出版社,预计于2022年7月出版。
《算法竞赛》是一本“大全”,内容覆盖“基础-中级-高级”,篇幅700页左右。部分知识点的草稿已经在本博客发表。


  很快就要 蓝桥杯省赛了,这里发一篇很有用的基础知识点。

  排列是计算机编程常用的基本技术,每一场算法竞赛都有题目用到排列。
  需要掌握两种实现排列的方法(C/C++组):
   (1)STL的next_permutation()函数。需要输出所有的全排列时,直接用这个函数。
   (2)自写排列函数。如果只需要输出排列的一部分,此时用next_permutation()函数无法做到,需要自己写代码。

1、next_permutation()

   STL中求“下一个”全排列的函数是next_permutation()。例如三个字符{a, b, c}组成的序列,next_permutation()能按字典序返回6个组合:abc,acb,bac,bca,cab,cba。
   函数next_permutation()的定义有两种形式:

bool next_permutation (BidirectionalIterator first, BidirectionalIterator last);
bool next_permutation (BidirectionalIterator first, BidirectionalIterator last, Compare comp);

   返回值:如果没有下一个排列组合,返回false,否则返回true。每执行next_permutation()一次,会把新的排列放到原来的空间里。
  注意以下事项:
(1)next_permutation()排列的范围是[first, last),包括first,不包括last。
(2)next_permutation()从当前的全排列开始,逐个输出更大的全排列,而不是输出所有的全排列。例如,初始序列是{b,c,a},它不是字典序最小的,此时只能输出3个。

#include <bits/stdc++.h>
using namespace std;
int main(){
    
    
    string s="bca";
    do{
    
     
       cout<<s<<endl;
    }while(next_permutation(s.begin(),s.end()));  //end()指向最后一个字符的下一个位置
    return 0;
}    

   代码只能输出3个全排列,而不是6个:

bca
cab
cba

(3)如果要得到所有的全排列,需要从最小的全排列开始。如果初始的全排列不是最小的,先用sort()排序,得到最小排列后,然后再执行next_permutation()。例如:

#include <bits/stdc++.h>
using namespace std;
int main(){
    
    
    string s="bca"; 
    sort(s.begin(),s.end());  //字符串内部排序,得到最小的排列“abc”
    do{
    
     
       cout<<s<<endl;
    }while(next_permutation(s.begin(),s.end()));
    return 0;
}   

   此时能输出所有的全排列,共6个:

abc
acb
bac
bca
cab
cba

(4)如果序列中有重复元素,next_permutation()生成的排列会去重。例如“aab”,代码输出3个排列{aab, aba, baa},不是6个排列。

#include <bits/stdc++.h>
using namespace std;
int main(){
    
    
    string s="aab"; 
    sort(s.begin(),s.end());  //字符串内部排序,得到最小的排列“abc”
    do{
    
     
        cout<<s<<endl;
    }while(next_permutation(s.begin(),s.end()));
    return 0;
}   

   输出3个排列:

aab
aba
baa

   STL中还有一个全排列函数prev_permutation(),求“前一个”排列组合,与next_permutation()相反,即“从大到小”。
   next_permutation()虽然很方便,但是它不能打印n个数中取m个数的部分排列,在某些场合下需要在排列过程中做处理,此时必须自己写排列函数。

2.自写排列函数

   下面自写一个全排列函数,它能实现部分排列。用递归写全排列函数,用b[]记录一个新的全排列,第一次进入bfs()时,b[0]在n个数中选一个,第二次进入bfs()时,b[1]在剩下的n-1个数中选一个,…,等等。用vis[]记录某个数是否已经被选过,选用过的数不能在后面继续选。
   代码能从小到大打印全排列,前提是a[]中的数字是从小到大的,先对a[]排序即可。

#include<bits/stdc++.h>
using namespace std;
int a[20] = {
    
    1,2,3,4,5,6,7,8,9,10,11,12,13};
bool vis[20];         //记录第i个数是否用过
int b[20];            //生成的一个全排列
void dfs(int s,int t){
    
    
    if(s == t) {
    
          //递归结束,产生一个全排列
        for(int i = 0; i < t; ++i)  
            cout << b[i] << " ";    //输出一个排列            
        cout<<endl;
        return;
    }
    for(int i=0;i<t;i++)
        if(!vis[i]){
    
    
            vis[i] = true;   
            b[s] = a[i];
            dfs(s+1,t);
            vis[i] = false;
        }
}
int main(){
    
    
    int n = 3;
    dfs(0,n);     //前n个数的全排列
    return 0;
}

   输出:

1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

   如果需要打印n个数中任意m个数的排列,例如在4个数中取任意3个数的排列,把21行改为n = 4,然后在dfs()中修改第7行,得下面的代码:

#include<bits/stdc++.h>
using namespace std;
int a[20] = {
    
    1,2,3,4,5,6,7,8,9,10,11,12,13};
bool vis[20];        //记录第i个数是否用过
int b[20];           //生成的一个全排列
void dfs(int s,int t){
    
    
    if(s == 3) {
    
         //递归结束,取3个数产生一个排列
       for(int i = 0; i < 3; ++i)     //打印4个数中3个数的排列
            cout << b[i] << " ";
        cout<<endl;
        return;
    }
    for(int i=0;i<t;i++)
        if(!vis[i]){
    
    
            vis[i] = true;
            b[s] = a[i];
            dfs(s+1,t);
            vis[i] = false;
        }
}
int main(){
    
    
    int n = 4;
    dfs(0,n);     //前n个数的全排列
    return 0;
}

输出:

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

3、例题

   下面给出一个需要自写全排列,不能用next_permutation()的例子。


寒假作业 (蓝桥杯2016年省赛C++A组第6题)
题目描述: 加减乘除四种运算:
□ + □ = □
□ - □ = □
□ × □ = □
□ ÷ □ = □
每个方块代表1~13中的某一个数字,但不能重复。
问:一共有多少种方案?


题目是一个13!的全排列问题,如果用next_permutation(),容易写出下面的代码:

#include <bits/stdc++.h>
using namespace std;
int a[20] = {
    
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
int main() {
    
    
  int ans=0;
  do{
    
     
     if(a[0]+a[1]==a[2] && a[3]-a[4]==a[5] &&a[6]*a[7]==a[8] && a[11]*a[10]==a[9])
          ans++;
  }while(next_permutation(a,a+13));
  cout<<ans<<endl;
}

   可惜,上述代码严重超时,因为13! = 6,227,020,800,运行代码,很久很久都无法结束。
   由于next_permutation()每次都必须生成一个完整的排列,而不能在中间停止(只生成全排列的一部分,例如5个数的排列只输出排列的前3个),所以在这种场合下并不好用。
   分析题目可知,实际上并不用生成一个完整排列。例如一个排列的前3个数,如果不满足“□ + □ = □”,那么后面的9个数不管怎么排列都不对。这种提前终止搜索的技术叫“剪枝”,剪枝是搜索中常见的优化技术。下面的代码,在自写全排列的基础上加上了剪枝。

#include<bits/stdc++.h>
using namespace std;
int a[20]={
    
    1,2,3,4,5,6,7,8,9,10,11,12,13};
bool vis[20]; 
int b[20];    
int ans=0;
void dfs(int s,int t){
    
    
    if(s==12) {
    
    
        if(b[9]*b[10] == b[11])   ans++;  
        return;
    }
    if(s==3 && b[0]+b[1]!=b[2]) return; //剪枝
    if(s==6 && b[3]-b[4]!=b[5]) return; //剪枝
    if(s==9 && b[6]*b[7]!=b[8]) return; //剪枝
    for(int i=0;i<t;i++)
        if(!vis[i]){
    
    
            vis[i]=true;
            b[s]=a[i];  //本题不用a[],改成b[s]=i+1也行
            dfs(s+1,t);
            vis[i]=false;
        }
}
int main(){
    
    
    int n=13;
    dfs(0,n); //前n个数的全排列
    cout<<ans;
    return 0;
}

   运行代码,很快就能输出答案:

64

4、习题

以下求排列的习题,来自蓝桥杯官网。
https://www.lanqiao.cn/problems/782/learning
https://www.lanqiao.cn/problems/572/learning
https://www.lanqiao.cn/problems/269/learning
https://www.lanqiao.cn/problems/208/learning

猜你喜欢

转载自blog.csdn.net/weixin_43914593/article/details/123652919