Python算法學習:全排列的回溯實現與二進制枚舉

全排列

LeetCode 46. 全排列

給定一個沒有重複數字的序列,返回其所有可能的全排列。

示例:

輸入: [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
輸出:
(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])

方法二:回溯

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、結束條件:也就是到達決策樹底層,無法再做選擇的條件。

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

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 中的數(可以構成了這麼多數)

具體可以參考我的這篇博客:Python算法學習:全排列的回溯實現與二進制枚舉

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))
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章