Just Do IT !

Python算法学习:全排列的回溯实现与二进制枚举

字数统计: 1.8k阅读时长: 7 min
2020/03/07 Share

全排列

LeetCode 46. 全排列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
给定一个没有重复数字的序列,返回其所有可能的全排列。

示例:

输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

方法1:使用库函数itertools.permutations
Python 中文文档3.7.2rc1itertools

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 方法一
'''
输入:
1 2 3
输出:
(1, 2, 3)
(1, 3, 2)
(2, 1, 3)
(2, 3, 1)
(3, 1, 2)
(3, 2, 1)
'''
import itertools
n = list(map(int, input().split()))
list = list(itertools.permutations(n))
for i in range(len(list)):
print(list[i])

方法二:回溯

1
2
3
4
5
6
7
8
9
10
nums = [1,2,3]
res = []
def backtrack(nums, tmp):
if not nums:
res.append(tmp)
return
for i in range(len(nums)):
backtrack(nums[:i] + nums[i+1:], tmp + [nums[i]])
backtrack(nums, [])
print(res)

回溯算法框架:

解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:

1、路径:也就是已经做出的选择。

2、选择列表:也就是你当前可以做的选择。

3、结束条件:也就是到达决策树底层,无法再做选择的条件。

代码方面,回溯算法的框架:

1
2
3
4
5
6
7
8
9
10
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return

for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择

其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」
在这里插入图片描述

二进制枚举

定义

先给出子集的定义:子集是一个数学概念:如果集合A的任意一个元素都是集合B的元素,那么集合A称为集合B的子集。

用途

在写程序的时候,有时候我们可能需要暴力枚举出所有的情况,这时可以考虑通过二进制来枚举子集来尝试解决问题。

解释

假设我们现在有5个小球,上面分别标号了0,1,2,3,4代表这些小球的权值,现在要像你求出这些小球的权值可以组成的所有情况。

假设我们现在有5个小球,上面分别标号了0,1,2,3,4代表这些小球的权值,现在要像你求出这些小球的权值可以组成的所有情况。

我们用二进制的思维来考虑这个问题,因为有5个小球,所以我们用5个比特位来分别标记小球存在还是不存在,对于这样一种情况,比如我们现在要选择3个小球,分别是0,3,4号小球,那么我们用二进制1表是当前的小球存在,用0表示当前小球不存在

二进制下标 4 3 2 1 0
二进制 1 1 0 0 1

小球状态| 存在| 存在| 不存在 |不存在| 存在|

我们可以用5个比特位来表示这种情况,如果小球全部选择的话那么二进制表示就是11111,二进制的11111转化为十进制数字就是31,这个数字正好就是2^5 -1,那么我们可以用从0~(2^5−1)这些数表示完所有的选取状态(因为这个范围内的二进制数情况正好包括了这些选取状况).

所以我们遍历每一个集合:

for i in range(1<<n):

设s = 1(二进制为00001)代表我们选0位置上的数值;

那么我们如何找到每个位置上的数值呢?

我们遍历的是二进制的十进制表示,我们当然可以转化为二进制再枚举每一位,但是,这很麻烦;

一个很巧妙的方式就是利用位运算。

1<<0=1(0);

1<<1=2(10);

1<<2=4(100);

1<<3=8(1000);

1<<4=16(10000);

...

1<<7=128(10000000);

...

看出来了吧!我们只需要将n&(1<<i)我们便可以得到每一位是不是1 (1<< i 除了那一位,剩余的都是0,所以我们就可以得到那一位是不是1)

按位与运算符(&)

参加运算的两个数据,按二进制位进行“与”运算。

运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;

即:两位同时为“1”,结果才为“1”,否则为0

例如:3&5 即 0000 0011& 0000 0101 = 00000001 因此,3&5的值得1。

左移运算(<<)

a << b就表示把a转为二进制后左移b位(在后面添b个0)。例如100的二进制为1100100,而110010000转成十进制是400,那么100 << 2 = 400。
可以看出,a << b的值实际上就是a乘以2的b次方,因为在二进制数后添一个0就相当于该数乘以2(这样做要求保证高位的1不被移出)。

题目举例:

忧郁的JM,借酒消愁。略微喝醉的他,和下酒花生聊起了天。

JM:“你知道质数是什么吗?”

花生:“……”

JM:“质数是指在大于11的自然数中,除了11和它本身以外不再有其他因数的自然数。”

花生:“……”

JM:“现在我有一个质数集合{3, 5, 7, 11, 13, 19, 23, 29, 31, 37, 41, 53, 59, 61, 67, 71, 97, 101, 127, 197, 211, 431}3,5,7,11,13,19,23,29,31,37,41,53,59,61,67,71,97,101,127,197,211,431,你可以从中挑出任意多个(0-12个)不同的数出来构成一个新数(取出数的和)”

JM:“构成的新数从小到大依次为:0, 3, 5, 7, 8, 10, 11, 12, 13…,0,3,5,7,8,10,11,12,13…,你知道[0, 1694][0,1694]中有多少个数是没法构成的吗?”
花生:”……“

JM:“例如:1,2,4…1,2,4…均是不能够从质数集合中挑数构成”

你来帮帮花生吧~

思路:

总共 22 个数,选择其中的 0 -12 个数,加上来组成一个新数。

我们可以用二进制枚举,对于 22 个数,每一个数,只有拿或不拿两种情况,也就是 0 或者 1。所以总共有 2 ^ 22 约等于 4e6。不会超时。

因为我们用二进制枚举,每一位对应这个数要不要取,如果取,那就累和。还要注意,最后只能取 12 个,所以我们要判断,这种取法中 1 的个数,如果是 >12 ,那这种方案不成立。

然后算出所有情况的数,用 set 统计(可能有重复的,去重)。

最后答案是问,无法构成的个数,因此答案是 : 总数(1695) - set 中的数(可以构成了这么多数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
nums = [3,5,7,11,13,19,23,29,31,37,41,53,59,61,67,71,97,101,127,197,211,431]
ans = [] #存放所有答案

for i in range(1<<22): # 2^22-1种情况(这里是取0或者取22个数的全部可能情况)
cnt = 0 # 计数 控制取数不超过12
res = 0 # 结果
tmp = i
for j in range(22): # 查看22位中都有哪一位放了数字,即是1
if (tmp >> j) & 1: # 如果第j位是1,则符合
cnt += 1
res += nums[j]
if cnt <= 12: # 不超过12位
ans.append(res)
ans = set(ans) # 使用集合的特性,去重
cnt = 1695
print(ans)
print(cnt - len(ans))
CATALOG
  1. 1. 全排列
    1. 1.1. LeetCode 46. 全排列
  2. 2. 二进制枚举
    1. 2.0.1. 定义
    2. 2.0.2. 用途
    3. 2.0.3. 解释
    4. 2.0.4. 按位与运算符(&)
    5. 2.0.5. 左移运算(<<)
  3. 2.1. 题目举例: