n个元素的全排列共有n!个排列结果,所以算法复杂度至少不小于O(n!)。解这类问题的一般方法通常有:
1. 深度搜索:因为解空间已知,相当于搜索一个高度为n满n叉树,树上每一条不出现重复元素的路径即为一个合法的排列,路径总数为2**n次
2. 顺序搜索:问题规模已知,如果所有的排列能够映射成一个有序的集合,并且给定任意一个排列,都有方法得到下一个排列,那么就可以搜索所有的解
3. 子问题跟全局问题的关系:如果全局问题的解能够通过子问题的解构造,那么可以从最小规模解不停加大问题规模来求解
1. 深度搜索
典型的回溯状态树的过程,每个节点都有n个选择,选择n个元素中的任何一个。因为一条路径不能出现重复的元素,所以需要给已选元素打上标签。代码:
n = int(input('enter a number:'))
nums = [x for x in range(1,n+1)]
ls = []
visited = [0]*n
def permu_dfs(nums,n,dep):
if dep >=n:
print(ls)
return
for i in range(n):
if visited[i] == 0:
ls.append(nums[i])
visited[i] = 1
permu_dfs(nums,n,dep + 1)
visited[i] = 0
ls.pop()
2. 顺序搜索1--映射状态树
如果把状态树上每条路径映射成一个整数,比如[0, 1] 2个元素的全排列的状态树映射如下:
0 0 --> 0 = 0*2**1 + 0*2**0 = 0
0 1 --> 1 = 0*2**1 + 1*2**0 = 1
1 0 --> 2 = 1*2**1 + 0*2**0 = 2
1 1--> 3 = 1*2**1 + 1*2**0 = 3
其中,红色的是正确的排列(没有重复的元素),再如[0, 1, 2]3个元素的全排列状态树映射如下:
0 0 0--> 0 = 0*3**2 + 0*3**1 + 0*3**0 = 0
0 0 1--> 1 = 0*3**2 + 0*3**1 + 1*3**0 = 1
0 0 2--> 2 = 0*3**2 + 0*3**1 + 2*3**0 = 2
0 1 0--> 3 = 0*3**2 + 1*3**1 + 0*3**0 = 3
....
0 1 2--> 5 = 0*3**2 + 1*3**1 + 2*3**0 = 5
0 2 0--> 6 = 0*3**2 + 2*3**1 + 2*3**0 = 6
....
2 1 0-->21 = 2*3**2 + 1*3**1 + 0*3**0 = 21
....
2 2 2-->21 = 2*3**2 + 2*3**1 + 2*3**0 = 26
可以看出,n个元素组成的一个序列(不一定是合法的排列)映射成一个10进制整数,就是把这个排列看成一个n进制的整数,转换公式如下:
d = p[0]*n**(n-1) + p[1]*n**(n-2) + ... + p[n-1]*n**0
但是没有什么必要做进制转换,比如给定任意一个序列,对其模拟n进制的加1操作,则结果为另外一个序列,比如:2 0 2 + ·1 = 2 1 0
那么算法可以这样描述:
1. 将n个元素排序得到[0,1,2,...,n-1](顺序排列必须从最小序开始,代码假设从最小序开始,省略排序过程)
2. 判断当前序列是否是一个排列(没有重复元素)
3. 对当前序列进行n进制+1操作
4. 重复2-3,直到终止条件:p[0]>=n
n = int(input('enter a number:'))
nums = [x for x in range(1,n+1)]
def addOne(nums):
i = n - 1
nt = 0
while True:
if i==n-1:
nums[i] = nums[i] + 1 + nt
else:
nums[i] = nums[i] + nt
if i !=0 and nums[i] > n:
nums[i] = 1
nt = 1
i-=1
else:
break
def isUnique(nums):
s = set(nums)
return len(s) == len(nums)
def permu(nums,n):
while True:
if isUnique(nums):
print(nums)
addOne(nums)
if nums[0] > n:
break
permu(nums,n)
input('pause')
3. 顺序搜索2--康托展开
上诉方法可以看成f: D--->R的一个映射, 其中D跟R的规模都为n**n,而实际问题的复杂度为 n!,也就是我们为了解问题把问题规模扩大了然后再进行筛选。而康托展开则是n!到n!的一一映射。对于[0, 1, 2]的一个排列1 2 0用康托展开对应的整数计算方法如下:
1. 首先最高位p[0]=1,由于0比1小,所以120前面必定有0xx这样的排列,其个数为1*2!
2. 再看p[1]=2, 第一步已经计算0xx这样的排列,但是还是存在10x这样的排列排除120前面。0跟1都比2小,但是1已经排在了最高位,所以比2小的只有0,则个数为1*1!
3. 最后一位不能考虑。
得到公式: d = a0*(n-1)! + a1*(n-2)! + an-1*0!,其中ai表示还有p[i+1:n]比p[i]小的元素的个数,所以:
0 1 2 = 0*2! + 0*1! + 0*0! = 0
0 2 1 = 0*2! + 1*1! + 0*0! = 1
1 0 2 = 1*2! + 0*1! + 0*0! = 2
1 2 0 = 1*2! + 1*1! + 0*0! = 3
2 0 1 = 2*2! + 0*1! + 0*0! = 4
2 1 0 = 2*2! + 1*1! + 0*0! = 5
def factorial(n):
facs = [1]*(n+1)
for i in range(1,n+1):
facs[i]=facs[i-1]*i
return facs
def cantorExpand(nums):
n = len(nums)
facs = factorial(n)
r = 0
for i in range(0,n):
#统计比nums[i]小的数的个数
t = 0
for j in range(i+1,n):
if nums[i] > nums[j]:
t+=1
r+=t*facs[n-i-1]
return r
求全排列刚好跟康托展开相反,所以需要一个函数,对于给定任意一个0到n!-1的整数生成一个全排列,这个过程称为
康托逆展开。
在[0,1,2]的一个整数4,映射成一个排列方法如下:
1. 4/2! = 2,说明有2个数比p[0]小,所以p[0]=2,还剩[0,1]两个数
2. 4%2!=0, 0/1!= 0, 说明p[1]要在[0,1]中选择一个数并且[0,1]没有其他数比它小,所以p[1]=0, 剩下[1
3. p[2]=1,所以结果为201
def cantorInv(n, seq, facs):
l=[]
vst=[0]*n
i = n-1
while(i>=0):
t = seq // facs[i] #有t个数比l[n-i-1]小
count = -1 #要比t多一个,所以设为-1
for j in range(n):
if vst[j] == 0:
count+=1
if t==count:
break
l.append(j)
vst[j]=1
seq%=facs[i]
i-=1
return l
def permu_cantor(n):
facs = factorial(n)
l=1
for i in range(1,n+1):
l*=i
for i in range(l):
print(cantorInv(n,i,facs))
4. 顺序搜索3--字典序--交换元素
利用字典序,将每个排列看成一个序数,如123的排列有123<132<213<231<312<321。那么只要知道了一个最小的排列(利用排序)就可以得出所有的排列结果。给定一个排列132,通过交换元素为主得到它的下一个排列的过程:
1. 如果从高位开始交换,则会漏掉排列。比如123,1跟2交换漏掉了132.所以需要从低位开始交换。
2. 对于132,从后面开始扫描,发现2比3小,交换了只会变小,所以继续往前扫描,发现1比3小。则说明1这个数可以被交换到后面。那么在1后面有2个数可以选择[3,2]。很显然需要一个比1大的中数中最小的一个,所以是2。交换后结果为231
3. 发现交换后漏掉了213,原因是
1. 假设原来序列为p0p1...pipi+1....pj.....pn-1,设pi是交换点,则p[n-1,..,i+1]是一个递增序列并且pi+1>pi,设pi需要跟pj交换,则pj>pi并且pi>p[j+1,..,n-1]中的任何数,则交换后结果p0p1...pj, pi+1....pi.....pn-1。可以得到p[n-1,...i,...pi+1]是一个递增序列,如果把p[n-1,...i,...pi+1]反转则可得到想要的排列
4. 31反转,得结果213。
n = int(input('enter a number:'))
nums = [x for x in range(1,n+1)]
def findPmax(nums, p, n):
pMax = n - 1
while pMax > p:
if nums[pMax] > nums[p]:
break
pMax = pMax -1
return pMax
def permu_ord(nums,n):
s,p = 0,n-1
print(nums)
while s != p:
q = p
p=p-1
if nums[p] < nums[q]:
pMax = findPmax(nums,p,n)
nums[p],nums[pMax] = nums[pMax],nums[p]
nums[q:] = nums[-1:q-1:-1]
print(nums)
p = n-1
permu_ord(nums,n)
input('pause')
5. 从子问题构造
利用公式:
p{123}=1p{23} + 2p{13}+3p{23},其中p{23}=2p{3}+3p{2}=23+32。
n = int(input('enter a number:'))
nums = [x for x in range(1,n+1)]
def permu_swap(nums,n,dep):
if dep==n:
print(nums)
else:
for i in range(dep,n):
nums[dep],nums[i]=nums[i],nums[dep]
permu_swap(nums,n,dep+1)
nums[dep],nums[i]=nums[i],nums[dep]