蓝桥杯省赛基础知识点 | 全排列函数和自写排列

梁老师
梁老师 北京小升初老师~

0 人点赞了该文章 · 55 浏览





正值蓝桥杯省赛,这里发一篇很有用的基础知识点。

排列是计算机编程常用的基本技术,每一场算法竞赛都有题目用到排列。需要掌握两种实现排列的方法(C/C++组):

(1)STL的next_permutation()函数。需要输出所有的全排列时,直接用这个函数。

(2)自写排列函数。如果只需要输出排列的一部分,此时用next_permutation()函数无法做到,需要自己写代码。


01

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个数的部分排列,在某些场合下需要在排列过程中做处理,此时必须自己写排列函数。


02

自写排列函数

下面自写一个全排列函数,它能实现部分排列。用递归写全排列函数,用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


03

例题

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


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

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

#include <bits/stdc++.h>
using namespace std;
int a[20] = {12345678910111213};
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

图片


添加 家长论坛微信 



发布于 2024-04-26 14:03

免责声明:

本文由 梁老师 原创发布于 家长帮 ,著作权归作者所有。

登录一下,更多精彩内容等你发现,贡献精彩回答,参与评论互动

登录! 还没有账号?去注册

暂无评论

广告
All Rights Reserved Powered BY WeCenter V4.1.0 © 2025 京ICP备20005761号-2