目录
一、排列
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组排列和组合、二进制讲解
祝好