前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >全排列生成算法:next_permutation

全排列生成算法:next_permutation

作者头像
triplebee
发布于 2018-01-12 07:08:56
发布于 2018-01-12 07:08:56
1.1K00
代码可运行
举报
运行总次数:0
代码可运行

概念

全排列的生成算法有很多种,有递归遍例,也有循环移位法等等。C++/STL中定义的next_permutation和prev_permutation函数则是非常灵活且高效的一种方法,它被广泛的应用于为指定序列生成不同的排列。本文将详细的介绍prev_permutation函数的内部算法。

按照STL文档的描述,next_permutation函数将按字母表顺序生成给定序列的下一个较大的序列,直到整个序列为减序为止。prev_permutation函数与之相反,是生成给定序列的上一个较小的序列。二者原理相同,仅遍例顺序相反,这里仅以next_permutation为例介绍算法。

下文内容都基于一个假设,即序列中不存在相同元素。对序列大小的比较做出定义:两个长度相同的序列,从两者的第一个元素开始向后比较,直到出现一个不同元素(也可能就是第它们的第一个元素),该元素较大的序列为大,反之序列为小;若一直到最后一个元素都相同,那么两个序列相等。

设当前序列为pn,下一个较大的序列为pn+1,那么不存在pm,使得pn < pm < pn+1。

问题

给定任意非空序列,生成下一个较大或较小的序列。

数学推导

根据上述概念易知,对于一个任意序列,最小的序列是增序,最大的序列为减序。那么给定一个pn要如何才能生成pn+1呢?先来看下面的例子:

我们用<a1 a2 ... am>来表示m个数的一种序列。设序列pn=<3 6 4 2>,根据定义可算得下一个序列pn+1=<4 2 3 6>。观察pn可以发现,其子序列<6 4 2>已经为减序,那么这个子序列不可能通过交换元素位置得出更大的序列了,因此必须移动最高位3(即a1)的位置,且要在子序列<6 4 2>中找一个数来取代3的位置。子序列<6 4 2>中6和4都比3大,但6大于4。如果用6去替换3得到的序列一定会大于4替换3得到的序列,因此只能选4。将4和3的位置对调后形成排列<4 6 3 2>。对调后得到的子序列<6 3 2>仍保持减序,即这3个数能够生成的最大的一种序列。而4是第1次作为首位的,需要右边的子序列最小,因此4右边的子序列应为<2 3 6>,这样就得到了正确的一个序列pn+1=<4 2 3 6>。

下面归纳分析该过程。假设一个有m个元素的序列pn,其下一个较大序列为pn+1。

1) 若pn最右端的2个元素构成一个增序子序列,那么直接反转这2个元素使该子序列成为减序,即可得到pn+1。

2) 若pn最右端一共有连续的s个元素构成一个减序子序列,令i = m - s,则有pn(i) < pn(i+1),其中pn(i)表示排列pn的第i个元素。例如pn=<1 2 5 4 3>,那么pn的右端最多有3个元素构成一个减序子集<5 4 3>,i=5-3=2,则有pn(i)=2 < 5=pn(i+1)。因此若将pn(i)和其右边的子集s {pn(i+1), pn(i+2), ..., pn(m)}中任意一个元素调换必能得到一个较大的序列(不一定是下一个)。要保证是下一个较大的序列,必须保持pn(i)左边的元素不动,并在子集s {pn(i+1), pn(i+2), ..., pn(m)}中找出所有比pn(i)大的元素中最小的一个pn(j),即不存在pn(k) ∈ s且pn(i) < pn(k) < pn(j),然后将二者调换位置。现在只要使新子集{pn(i+1), pn(i+2), ..., pn(i), ...,pn(m)}成为最小序列即得到pn+1。注意到新子集仍保持减序,那么此时直接将其反转即可得到pn+1 {pn(1), pn(2), ..., pn(j), pn(m), pn(m-1), ..., pn(i), ..., pn(i+2), pn(i+1)}。

复杂度

最好的情况为pn的最右边的2个元素构成一个最小的增序子集,交换次数为1,复杂度为O(1),最差的情况为1个元素最小,而右面的所有元素构成减序子集,这样需要先将第1个元素换到最右,然后反转右面的所有元素。交换次数为1+(n-1)/2,复杂度为O(n)。因为各种排列等可能出现,所以平均复杂度即为O(n)。

扩展

1. 能否直接算出集合{1, 2, ..., m}的第n个排列?

设某个集合{a1, a2, ..., am}(a1<a2<...<am)构成的某种序列pn,基于以上分析易证得:若as<at,那么将as作为第1个元素的所有序列一定都小于at作为第1个元素的任意序列。同理可证得:第1个元素确定后,剩下的元素中若as'<at',那么将as'作为第2个元素的所有序列一定都小于作为第2个元素的任意序列。例如4个数的集合{2, 3, 4, 6}构成的序列中,以3作为第1个元素的序列一定小于以4或6作为第1个元素的序列;3作为第1个元素的前题下,2作为第2个元素的序列一定小于以4或6作为第2个元素的序列。

推广可知,在确定前i(i<n)个元素后,在剩下的m-i=s个元素的集合{aq1, aq2, ..., aq3}(aq1<aq2<...<aqm)中,以aqj作为第i+1个元素的序列一定小于以aqj+1作为第i+1个元素的序列。由此可知:在确定前i个元素后,一共可生成s!种连续大小的序列。

根据以上分析,对于给定的n(必有n<=m!)可以从第1位开始向右逐位地确定每一位元素。在第1位不变的前题下,后面m-1位一共可以生成(m-1)!中连续大小的序列。若n>(m-1)!,则第1位不会是a1,n中可以容纳x个(m-1)!即代表第1位是ax。在确定第1位后,将第1位从原集合中删除,得到新的集合{aq1, aq2, ..., aq3}(aq1<aq2<...<aqm),然后令n1=n-x(m-1)!,求这m-1个数中生成的第n1个序列的第1位。

举例说明:如7个数的集合为{1, 2, 3, 4, 5, 6, 7},要求出第n=1654个排列。

(1654 / 6!)取整得2,确定第1位为3,剩下的6个数{1, 2, 4, 5, 6, 7},求第1654 % 6!=214个序列;

(214 / 5!)取整得1,确定第2位为2,剩下5个数{1, 4, 5, 6, 7},求第214 % 5!=94个序列;

(94 / 4!)取整得3,确定第3位为6,剩下4个数{1, 4, 5, 7},求第94 % 4!=22个序列;

(22 / 3!)取整得3,确定第4位为7,剩下3个数{1, 4, 5},求第22 % 3!=4个序列;

(4 / 2!)得2,确定第5为5,剩下2个数{1, 4};由于4 % 2!=0,故第6位和第7位为增序<1 4>;

因此所有排列为:3267514。

2. 给定一种排列,如何算出这是第几个排列呢?

和前一个问题的推导过程相反。例如3267514:

后6位的全排列为6!,3为{1, 2, 3 ,4 , 5, 6, 7}中第2个元素(从0开始计数),故2*720=1440;

后5位的全排列为5!,2为{1, 2, 4, 5, 6, 7}中第1个元素,故1*5!=120;

后4位的全排列为4!,6为{1, 4, 5, 6, 7}中第3个元素,故3*4!=72;

后3位的全排列为3!,7为{1, 4, 5, 7}中第3个元素,故3*3!=18;

后2位的全排列为2!,5为{1, 4, 5}中第2个元素,故2*2!=4;

最后2位为增序,因此计数0,求和得:1440+120+72+18+4=1654

C++/STL实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <algorithm>
#include <iostream>
#include <string>
using namespace std;
//主函数,算法详见相关说明
int main(void) {
    //循环处理输入的每一个字符串
    for (string str; cin >> str;) {
        if (str.empty()) {
            continue;
        }
        //如果字符串只有1个字符,则直接输出结束
        if (str.length() <= 1) {
            cout << "No more Permutation" << endl;
        }
        //iPivot为右边最大减序子集左边相邻的一个元素
        string::iterator iPivot = str.end(), iNewHead;
        //查找右边最大的减序子集
        for (--iPivot; iPivot != str.begin(); --iPivot) {
            if (*(iPivot - 1) <= *iPivot ) {
                break;
            }
        }
        //如果整个序列都为减序,则重排结束。
        if (iPivot == str.begin()) {
            cout << "No more Permutation" << endl;
        }
        //iPivot指向子集左边相邻的一个元素
        iPivot--;
        //iNewHead为仅比iPivot大的元素,在右侧减序子集中寻找
        for (iNewHead = iPivot + 1; iNewHead != str.end(); ++iNewHead) {
            if (*iNewHead < *iPivot) {
                break;
            }
        }
        //交换iPivot和iNewHead的值,但不改变它们的指向
        iter_swap(iPivot, --iNewHead);
        //反转右侧减序子集,使之成为最小的增序子集
        reverse(iPivot + 1, str.end());
        //本轮重排完成,输出结果
        cout << str << endl;
    }
    return 0;
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2016-09-04 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
全排列 next_permutation的使用
给定一个由不同的小写字母组成的字符串,输出这个字符串的所有全排列。 我们假设对于小写字母有'a' < 'b' < ... < 'y' < 'z',而且给定的字符串中的字母已经按照从小到大的顺序排列。
喜欢ctrl的cxk
2019/11/07
5550
排列类算法问题大总结全排列分析带重复元素的全排列代码下一个排列分析上一个排列分析第k个排列分析排列序号分析排列序号II分析
[ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
desperate633
2018/08/22
1.3K0
排列类算法问题大总结全排列分析带重复元素的全排列代码下一个排列分析上一个排列分析第k个排列分析排列序号分析排列序号II分析
【C++航海王:追寻罗杰的编程之路】STL—next_permutation函数
next_permutation函数会按照字母表顺序生成给定序列的下一个较大的排列,直到整个序列为降序为止。与其相对的还有一个函数——prev_permutation函数。
枫叶丹
2024/06/04
1210
【C++航海王:追寻罗杰的编程之路】STL—next_permutation函数
C++版 - 剑指offer面试题28: 字符串的排列
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。 结果请按字母顺序输出。
Enjoy233
2019/03/05
6310
自从有了她,再也不怕面试官问我排列问题了
生成一个序列的重排列,是所有可能的字典序中的下一个排列,默认使用 < 运算符实现。
用户9831583
2022/06/16
1800
自从有了她,再也不怕面试官问我排列问题了
全排列(permutation)
显然,对于具有n个元素的集合R,R={r1,r2,r3…rn},其排列方式有n!种。 如:R = {1,2,3},其全排列如下: 1,2,3 1,3,2 2,1,3 2,3,1 3,1,2 3,2,1
lexingsen
2022/02/24
5270
全排列(permutation)
一个易于理解的C++全排列(permutation)实现
通常我们用这两条语句可以得到一个数组的全排列: sort(nums.begin(),nums.end()); //调用next_permutation求全排列的时候必须先给容器排序 do{ get_pirnt(nums) //这里是一个可以打印输出nums的函数 }while(next_permutation(nums.begin(),nums.end()); //调用该C++内置函数可以输出字典序大于当前nums的所有排列。 还可以自己写一个函数实现同样的功能,下面的函数使用递归,每次取出
kalifa_lau
2018/04/26
1.8K0
蓝桥杯之全排列函数next_permutation()运用
在蓝桥杯的题目中大多数都可以运用到全排列函数 充分运用可以节省很多的时间。话不多说来刷题
Max超
2019/01/21
4040
盘点算法竞赛中C++常用的stl库函数
我们都知道,C++中有许多内置的库函数,我们可以直接调用它们,在蓝桥杯,ACM等比赛中,通过使用这些常用的库函数可以大大提高我们的效率,而不用自己去再重新去手写一些函数,那么本篇文章就为大家盘点了一些比较常用的库函数,并附带了例题帮助大家运用理解。
2的n次方
2024/10/15
4940
盘点算法竞赛中C++常用的stl库函数
HDU 6628 (2019杭电第五场 1005) permutation 1 (全排列)
题意:求 n的 全排列 差值序列(后一项减前一项 n-1项) 第 k 小的全排列,2 <=n<=20, 1<= k <=min(10000,n!)
用户2965768
2019/08/14
5500
nyoj------擅长排列的小明
擅长排列的小明 时间限制:1000 ms  |           内存限制:65535 KB 难度:4 描述小明十分聪明,而且十分擅长排列计算。比如给小明一个数字5,他能立刻给出1-5按字典序的全排列,如果你想为难他,在这5个数字中选出几个数字让他继续全排列,那么你就错了,他同样的很擅长。现在需要你写一个程序来验证擅长排列的小明到底对不对。 输入第一行输入整数N(1<N<10)表示多少组测试数据, 每组测试数据第一行两个整数 n m (1<n<9,0<m<=n)输出在1-n中选取m个字符进行全排列,按字典
Gxjun
2018/03/21
6550
【算法竞赛】水CF构造题
我太弱了,水水构造tag的题去。 大概只写写思路(毕竟构造题) 打*的是自己想没直接出来的。 发布时间,最早为20220814-14:14,现在为最新水题时间。
Livinfly
2022/10/26
4650
LeetCode 31. 下一个排列(线性扫描)
实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
Michael阿明
2020/07/13
3270
每日算法刷题Day16-和为S的两个数字、数字排列、二进制中1的个数
此题可以先sort将数组从小到大排序,然后定义结构vector<vector<int>> res,将结果不断地排下一组和直到返回false为止。
timerring
2022/11/02
2700
每日算法刷题Day16-和为S的两个数字、数字排列、二进制中1的个数
向前字典排序
          next_permutation算法对区间元素进行一次组合排序,使之字典顺序大于原来的排序,有如下两个使用原形,对迭代器区间[first,last)元素序列进行组合排序。当新排序的字典顺序大于原排序时,返回true,否则返回false,利用该算法也可以进行元素排序,但是速度较慢,排序的算法时间复杂度为n!阶乘.          对应的有向后字典排序 prev_permutation算法用于选择一个字典序更小的排序。有如下两个使用原形,对迭代器区间[first,last)元素序列进行组合
Gxjun
2018/03/21
1.3K0
next_permutation(全排列算法)
 STL提供了两个用来计算排列组合关系的算法,分别是next_permutation和prev_permutation。首先我们必须了解什么是“下一个”排列组合,什么是“前一个”排列组合。考虑三个字符所组成的序列{a,b,c}。       这个序列有六个可能的排列组合:abc,acb,bac,bca,cab,cba。这些排列组合根据less-than操作符做字典顺序(lexicographical)的排序。也就是说,abc名列第一,因为每一个元素都小于其后的元素。acb是次一个排列组合,因为它是固定了a(
Angel_Kitty
2018/04/08
9220
next_permutation(全排列算法)
HDOJ 1716 排列2 next_permutation函数
Problem Description Ray又对数字的列产生了兴趣: 现有四张卡片,用这四张卡片能排列出很多不同的4位数,要求按从小到大的顺序输出这些4位数。
谙忆
2021/01/20
4110
排列汇总
。即。将每一个组合与一个二进制数相应起来。枚举二进制的同一时候,枚举每一个组合。如字符串:abcde,则有 00000———null 00001———a 00010 ——–b 00011———ab 00100———c … …
全栈程序员站长
2022/07/18
4340
排列组合公式及排列组合算法[通俗易懂]
公式P是指排列,从N个元素取M个进行排列。 公式C是指组合,从N个元素取M个进行组合,不进行排列。 N-元素的总个数 M参与选择的元素个数 !-阶乘,如 9!=9*8*7*6*5*4*3*2*1
全栈程序员站长
2022/07/22
26.3K0
排列组合公式及排列组合算法[通俗易懂]
HDOJ 1716 排列2(next_permutation函数)
例题: Problem B Time Limit : 1000/1000ms (Java/Other) Memory Limit : 32768/32768K (Java/Other) Total Submission(s) : 27 Accepted Submission(s) : 10 Problem Description Ray又对数字的列产生了兴趣: 现有四张卡片,用这四张卡片能排列出很多不同的4位数,要求按从小到大的顺序输出这些4位数。
谙忆
2021/01/20
3840
推荐阅读
相关推荐
全排列 next_permutation的使用
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验