目录
一、排列
1、Python 的排列函数 permutations()
2、permutations() 按什么顺序输出序列(重要⭐)
3、易错点
二、组合
1、Python的组合函数combinations()
2、注意内容
三、手写排列和组合代码
1、手写排列代码(暴力法)
2、手写组合代码(暴力法)
3、手写组合代码(二进制法)
4、输出n个数中任意m个数的组合
5、输出 n 个数的任意组合(所有子集)
四、例题讲解
1、排列序数(lanqiaoOJ题号269)
1)用 sort() 函数
2)用 sorted() 函数。sorted() 能直接在字符串的内部排序
2、拼数(lanqiaoOJ题号782)
3、火星人(lanqiaoOJ题号572)
4、带分数(lanqiaoOJ题号208)
一、排列
1、Python 的排列函数 permutations()
- itertools.permutations(iterable, r=None)
- 功能:连续返回由 iterable 序列中的元素生成的长度为 r 的排列。
- 如果 r 未指定或为 None,默认设置为 iterable 的长度,即生成包含所有元素的全排列。
from itertools import *
s=['a','b','c']
for element in permutations(s,2):
    # print(element)
    a=element[0]+element[1]
    # 或者这样写:a=''.join(element)
    print(a,end=' ')2、permutations() 按什么顺序输出序列(重要⭐)
- 答:按元素的位置顺序输出元素的排列,也就是说,输出排列的顺序是位置的字典序。例如 s = ['b','a','c'],执行 permutations(s),输出 "bac bca abc acb cba cab" ,并不是按字符的字典序输出排列,而是按位置顺序输出。
- s=['b','a','c'] 的 3 个元素的位置是 'b'=1、'a'=2、'c'=3,输出的排列 “bac bca abc acb cba cab”,按位置表示就是“123 132 213 231 312 321”,这是按从小到大的顺序输出的。
from itertools import *
s=['b','a','c']
for element in permutations(s):
    #print(element)
    a=''.join(element)
    print(a,end=' ')
如果有相同的元素,不同位置的元素被认为不同。例如 s=['a', 'a', 'c'],执行 permutations(s),输出 "ааc aca aaс аcа cаа cаа".
from itertools import *
s=['a','a','c']
for element in permutations(s):
    #print(element)
    a=''.join(element)
    print(a,end=' ')
3、易错点
初学者容易犯一个错误,把元素当成了位置。例如 s = ['1', '3', '2'],执行 permutations(s),输出“132 123 312 321 213 231",看起来很乱,实际上是按 3 个元素的位置 '1'=1、'3'=2、'2'=3 输出有序的排列的。若需要输出看起来正常的 “123 132 213 231 312 321”,可以把 s=['1','3','2'] 先用 sort() 排序为 ['1', '2', '3'],再执行 permutations()。
from itertools import *
s=['1','3','2']
for element in permutations(s):
    #print(element)
    a=''.join(element)
    print(a,end=' ')如何输出看起来正常的 “123 132 213 231 312 321”?
先把 s = ['1', '3', '2'],用 sort() 排序为 ['1', '2', '3'],再执行 permutations()
from itertools import *
s=['1','3','2']
s.sort()
for element in permutations(s):
    #print(element)
    a=''.join(element)
    print(a,end=' ')
二、组合
1、Python的组合函数combinations()
permutations() 输出的是排列,元素的排列是分先后的,“123” 和 “321” 不同。但是有时只需要输出组合,不用分先后,此时可以用 combinations() 函数。
from itertools import *
s=['1','3','2']
for element in combinations(s,2):
    #print(element)
    a=''.join(element)
    print(a,end=' ')
2、注意内容
如果序列 s 中有相同的字符,且 s 是用 [ ] 表示的数组,那么 s 中不同位置的元素被认为不同。
from itertools import *
s=['1','1','3','2']
for element in combinations(s,2):
    #print(element)
    a=''.join(element)
    print(a,end=' ')
如果要去重怎么办?用集合,s用 { } 表示。
from itertools import *
s={'1','1','3','2'}
for element in combinations(s,2):
    #print(element)
    a=''.join(element)
    print(a,end=' ')    # 多次输出的顺序是不定的
如果要去重且输出按字典序:先用 set() 去重,再转为 list,再排序
from itertools import *
s={'1','1','3','2'}
t=list(set(s))
t.sort()
#print(t)
for element in combinations(t,2):
    #print(element)
    a=''.join(element)
    print(a,end=' ')    # 多次输出的顺序是不定的
三、手写排列和组合代码
- 在某些场景下,系统排列函数并不能用,需要手写代码实现排列组合。
- 在 “DFS与排列组合” 中给出了基于 DFS 的手写方法。
- 下面给出几种简单的手写方法。
1、手写排列代码(暴力法)
从 {1,2,3,4} 中选 3 个的排列,有 24 种。最简单直接无技巧的手写排列这样写:
s=[1,2,3,4]
for i in range(4):
    for j in range(4):
        if j!=i:
            for k in range(4):
                if k!=j and k!=i:
                    print("%d%d%d"%(s[i],s[j],s[k]),end=",")【优缺点】简单且效果很好,但是非常笨拙。如果写 5 个以上的数的排列组合,代码冗长无趣。
2、手写组合代码(暴力法)
- 排列数需要分先后,组合数不分先后。
- 把求组合的代码,去掉 if,然后按从小到大打印即可。
- 从 {1,2, 3,4} 中选 3 个的组合有 4 种。
s=[1,2,3,4]
for i in range(4):  
    for j in range(i+1,4):
        for k in range(j+1,4):
            print("%d%d%d"%(s[i],s[j],s[k]),end=",")
3、手写组合代码(二进制法)
一个包含 n 个元素的集合 {a0, a1, a2, a3, ..., an-1},它的子集有 {φ}, {a0}, {a1}, {a2}, ..., {a0, a1, a2}, ..., {a0, a1, a2, a3, ..., an-1},共 2n 个。
用二进制的概念进行对照是最直观的,子集正好对应了二进制。例如 n=3 的集合 {a0, a1, a2},它的子集和二进制数的对应关系是:

每个子集对应了一个二进制数。二进制数中的每个 1,对应了子集中的某个元素。而且,子集中的元素,是不分先后的,这正符合组合的要求。
下面的代码通过处理每个二进制数中的 1,打印出了所有的子集。
#include<bits/stdc++.h>
using namespace std;
int a[]={1,2,3,4,5,6,7,8,9,10,11,12,13,14};
void print_subset(int n){
	for(int i=0;i<(1<<n);i++){
		for(int j=0;j<n;j++){  //打印一个子集,即打印 i 的二进制数中所有的 1 
			if(i&(1<<j)){	   //从 i 的最低位开始,逐个检查每一位,如果是 1,打印 
				cout<<a[j]<<" ";
			}
		}	
		cout<<"; ";
	}
}
int main() {
	int n=3;
	print_subset(n);
	return 0;
}输出:
; 1 ; 2 ; 1 2 ; 3 ; 1 3 ; 2 3 ; 1 2 3 ;
4、输出n个数中任意m个数的组合
- 根据上面子集生成的二进制方法,一个子集对应一个二进制数:一个有m个元素的子集,它对应的二进制数中有m个1。
- 所以问题转化为:查找 1 的个数为 m 个的二进制数,这些二进制数对应了需要打印的子集。
- 如何判断二进制数中 1 的个数为 m 个? 简单的方法是对这个 n 位的二进制数逐位检查,共需要检查 n 次。
有一个更快的方法,可以直接定位二进制数中 1 的位置,跳过中间的 0。
用到一个神奇操作:k = k & (k-1),功能是消除 k 的二进制数的最后一个 1。连续进行这个操作,每次消除一个 1,直到全部消除,操作次数就是 1 的个数。例如二进制数 1011,经过连续 3 次操作后,所有 1 都消除了:
1011 & (1011 - 1) = 1011 & 1010 = 1010
1010 & (1010 - 1) = 1010 & 1001 = 1000
1000 & (1000 - 1) = 1000 & 0111 = 0000
利用这个操作,可以计算出二进制数中 1 的个数。用 num 统计1的个数,具体步骤是:
1)用 k=k & (k-1) 清除 k 的最后一个 1;
2)num++;
3)继续上述操作,直到k=0。
5、输出 n 个数的任意组合(所有子集)
输出按字典序输出,从小到大。(下面的代码已经说明了二进制的精髓)
a=[1,2,3,4,5,6,7,8,9,10,11,12,13,14]
def print_set(n,m):
    for i in range(2**n):   #2**n可以写成1<<n
        num,k=0,i           #num统计i中1的个数,k用来处理i
        while k>0:
            k=k&(k-1)       #清除k的最后一个1
            num+=1
        if num==m:
            for j in range(n):
                if i&(2**j):
                    print(a[j],end="")
            print(";",end='')
n,m=4,3
print_set(n,m)
四、例题讲解
1、排列序数(lanqiaoOJ题号269)
【题目描述】
如果用 a b c d 这 4 个字母组成一个串,有 4!=24 种。现在有不多于 10 个两两不同的小写字母,给出它们组成的串,你能求出该串在所有排列中的序号吗?
【输入描述】输入一行,一个串。
【输出描述】输出一行,一个整数,表示该串在其字母所有排列生成的串中的序号。注意:最小的序号是 0。下面给出两种代码,分别用 sort() 和 sorted() 排序,然后用permutions()求排列。
1)用 sort() 函数
sort() 不能直接在字符串内部排序。可以这样:把字符串转换成数组,对数组排序后,再转换回字符串,就得到了最小字符串。
from itertools import *
olds=input()
news=list(olds)
news.sort()
cnt=0
for ele in permutations(news):
    a=''.join(ele)  #把所有元素拼回成字符串
    if olds==a:
        print(cnt)
        break
    cnt+=1
2)用 sorted() 函数。sorted() 能直接在字符串的内部排序
from itertools import *
olds=input()
news=sorted(olds)
print(olds,news)
a=[]
for ele in permutations(news):
    a.append(ele)
print(a.index(tuple(olds)))
2、拼数(lanqiaoOJ题号782)
【题目描述】
设有 n 个正整数 a1, a2, ..., an,将它们联接成一排,相邻数字首尾相接,组成一个最大的整数。 n<20。
最简单粗暴的方法,是先得到这 n 个整数的所有排列,然后找其中最大的。但是这个方法的复杂度是 O(n!),当 n =20 时,有 20! =2×1018 种排列,超时。
from itertools import *
N=int(input())
ans=""
nums=list(map(str,input().split())) #按字符的形式读入
for ele in permutations(nums):  #每次输出一个全排列
    a="".join(ele)
    #print(a)
    if ans<a:
        ans=a       #在所有串中找最大的
print(ans)
    
暴力排列不行,可以用排序吗? 本题不能直接对数字排序然后首尾相接,例如“7, 13”,应该输出“713”,而不是“137”。注意到这其实是按两个数字组合的字典序排序,也就是把数字看成字符串来排序。本题的 n 很小,用较差的排序算法也行,例如交换排序。第 3~6 行用交换排序对所有的数 (按字符串处理) 进行排序,复杂度 O(n^2)。
n=int(input())
nums=input().split()    #按字符读入
# print(nums)   
for i in range(0,n-1):  #交换排序
    for j in range(i+1,n):
        if nums[j]+nums[i]>nums[i]+nums[j]:
            nums[j],nums[i]=nums[i],nums[j]
print(''.join(nums))
3、火星人(lanqiaoOJ题号572)
【题目描述】
给出 N 个数的排列,输出这个排列后面的第 M 个排列。
【输入描述】
第一行有一个正整数 N,1<=N<=10000。第二行是一个正整数 M。下一行是 1 到 N 个整数的一个排列,用空格隔开。
【输出描述】
输出一行,这一行含有 N 个整数,表示原排列后面第 M 个排列。每两个相邻的数中间用一个空格分开,不能有多余的空格。
用 Python 编码比较麻烦,因为 Python 的 permutations() 函数是按元素位置进行排列的输出的。只能这样编码:先把 n 个数排序成最小排列,然后从这个最小排列开始 permutations(),遇到题目给定的起始排列后,再往后数到第m个排列,输出。但是这个代码会超时,因为浪费了很多计算。
from itertools import *
from copy import *
n=int(input())
m=int(input())
nums=list(map(str,input().split()))
back=deepcopy(nums)
k=0
flag=0
nums.sort()
for ele in permutations(nums):
    if list(ele)==back:
        flag=1
    if flag==1:
        if k==m:
            a=''.join(ele)
            print(a)
            break   #退出for循环!
        k+=1一种高效的方法:从当前排列开始,暴力地寻找下一个排列。对于当前排列,从后往前比较,寻找 nums[i-1] < nums[i] 的位置,把 nums[i-1] 与 i 到末尾中比 nums[i-1] 大的最小数交换,再将 i-1 之后的数进行翻转 (从小到大排序),可以得到比当前排列大的最小排列。
n=int(input())
m=int(input())
nums=list(map(int,input().split()))
def find_next(nums):
    for i in range(n-1,0,-1):
        if nums[i]>nums[i-1]:
            for j in range(n-1,i-1,-1):
                if nums[j]>nums[i-1]:
                    nums[j],nums[i-1]=nums[i-1],nums[j]
                    return nums[:i]+nums[:i-1:-1]
for i in range(m):
    nums=find_next(nums)
print(''.join([str(i) for i in nums]))
4、带分数(lanqiaoOJ题号208)
【题目描述】
100 可以表示为带分数的形式:100 = 3 + 69258 / 714。还可以表示为:100 = 82 + 3546 / 197。
注意特征:带分数中,数字 1~9 分别出现且只出现一次(不包含0)。
类似这样的带分数,100 有 11 种表示法。输入一个整数,输出它有多少种表示法。
【输入描述】
从标准输入读入一个正整数 N (N < 1000×1000)。
【输出描述】
程序输出该数字用数码 1~9 不重复不遗漏地组成带分数表示的全部种数。
典型的排列题。题目中说 “数字 1~9 分别出现且只出现一次”,用暴力排列:对所有 1~9 的排列,验证有几个符合要求。9 个数只有 9!=362880 种排列,不会超时。
from itertools import *
n=int(input())
bit=len(str(n))     #n的位数
cnt=0
for num in permutations("123456789"):
    a,b,c=0,0,0
    for a1 in range(bit):   #a1: a的位数,a肯定比n短
        a=int("".join(num[:a1+1]))  #一个a
        bLast=(n-a)*int(num[-1])%10  #b的尾数,(n-a)c%10
        if bLast==0:        #b的尾数不可能等于0,因为只用到1~9
            continue
        b1=num.index(str(bLast))    #根据b的尾数,确定b的长度
        if b1<=a1 or b1>=8:
            continue
        b=int(''.join(num[a1+1:b1+1]))
        c=int(''.join(num[b1+1:]))
        if b%c==0 and n==a+b//c:
            cnt+=1
print(cnt)
以上,蓝桥杯Python组排列和组合、二进制讲解
祝好


















