python常用算法(7)——動態規划,回溯法


引言:從斐波那契數列看動態規划

  斐波那契數列:Fn = Fn-1 + Fn-2    ( n = 1,2     fib(1) = fib(2) = 1)

練習:使用遞歸和非遞歸的方法來求解斐波那契數列的第 n 項

  代碼如下:

# _*_coding:utf-8_*_

def fibnacci(n):
    if n == 1 or n == 2:
        return 1
    else:
        return fibnacci(n - 1) + fibnacci(n - 2)

# 寫這個是我們會發現計算f(5) 要算兩邊f(4) 
# f(5) = f(4)+f(3)
# f(4) = f(3)+f(2)
# f(3) = f(2)+f(1)
# f(3) = f(2)+f(1)
# f(2) = 1
# 那么同理,算f(6),我們會計算兩次f(5),三次f(4)....
# 當然不是說所有的遞歸都會重復計算,

# 時間隨着數字越大,時間越長
print(fibnacci(10))  # 55

  簡單來說,就是想要計算f(5),我們需要先計算出子問題 f(4)  和 f(3),然后要計算 f(4) ,我們需要先計算出子問題 f(3)  和 f(2),以此類推,最后遇到 f(1) 或者 f(2) 的時候,結果已知,就能直接返回結果,遞歸樹不再向下生長了。

  遞歸算法的時間復雜度怎么計算?子問題個數乘以解決一個子問題需要的時間。

  子問題個數,即遞歸樹中節點的總數。顯然二叉樹節點總數為指數級別,所以子問題個數為 O(2^n)。

  解決一個子問題的時間,在本算法中,沒有循環,只有 f(n - 1) + f(n - 2) 一個加法操作,時間為 O(1)。

  所以,這個算法的時間復雜度為 O(2^n),指數級別,爆炸。

  觀察遞歸樹,很明顯發現了算法低效的原因:存在大量重復計算,比如 f(5) 被計算了兩次,而且你可以看到,以 f(5) 為根的這個遞歸樹體量巨大,多算一遍,會耗費巨大的時間。更何況,還不止 f(5) 這一個節點被重復計算,所以這個算法及其低效。

  這就是動態規划問題的第一個性質:重疊子問題。下面,我們想辦法解決這個問題。

  明確了問題,其實就已經把問題解決了一半。即然耗時的原因是重復計算,那么我們可以造一個「備忘錄」,每次算出某個子問題的答案后別急着返回,先記到「備忘錄」里再返回;每次遇到一個子問題先去「備忘錄」里查一查,如果發現之前已經解決過這個問題了,直接把答案拿出來用,不要再耗時去計算了。

  一般使用一個數組充當這個「備忘錄」,當然你也可以使用哈希表(字典),思想都是一樣的。

def fibnacci_n_recurision(n):
    f = [0, 1, 1]
    if n > 2:
        for i in range(n - 2):
            num = f[-1] + f[-2]
            f.append(num)
    return f[n]


print(fibnacci_n_recurision(10))

  實際上,帶「備忘錄」的遞歸算法,把一棵存在巨量冗余的遞歸樹通過「剪枝」,改造成了一幅不存在冗余的遞歸圖,極大減少了子問題(即遞歸圖中節點)的個數。

  遞歸算法的時間復雜度怎么算?子問題個數乘以解決一個子問題需要的時間。

  子問題個數,即圖中節點的總數,由於本算法不存在冗余計算,子問題就是 f(1), f(2), f(3) ... f(20),數量和輸入規模 n = 20 成正比,所以子問題個數為 O(n)。

  解決一個子問題的時間,同上,沒有什么循環,時間為 O(1)。

  所以,本算法的時間復雜度是 O(n)。比起暴力算法,是降維打擊。

  至此,帶備忘錄的遞歸解法的效率已經和動態規划一樣了。實際上,這種解法和動態規划的思想已經差不多了,只不過這種方法叫做「自頂向下」,動態規划叫做「自底向上」。

  啥叫「自頂向下」? 就是從上向下延伸,都是從一個規模較大的原問題比如說 f(5),向下逐漸分解規模,直到 f(1) 和 f(2) 觸底,然后逐層返回答案,這就叫「自頂向下」。

  啥叫「自底向上」?反過來,我們直接從最底下,最簡單,問題規模最小的 f(1) 和 f(2) 開始往上推,直到推到我們想要的答案 f(5),這就是動態規划的思路,這也是為什么動態規划一般都脫離了遞歸,而是由循環迭代完成計算。

  為了讓我們的說服更有理一些,這里寫了一個裝飾器,我們通過運行時間看。同樣對於上面兩個函數,一個遞歸,一個非遞歸,我們輸入 n=15

# cal_time.py 函數代碼如下:

import time

def cal_time(func):
    def wrapper(*args, **kwargs):
        t1 = time.time()
        result = func(*arg, **kwargs)
        t2 = time.time()
        print("%s running time: %s secs." % (func.__name__, t2 - t1))
        return result
    return wrapper
    

運行結果:

fibnacci running time: 0.01000070571899414 secs.
fibnacci_n_recurision running time: 0.0 secs.

  總結來說,就是遞歸非常非常的慢,那非遞歸相對來說就比較快了。那為什么呢?就是為什么遞歸的效率低。我們上面代碼也說過了,就是對子問題進行重復計算了。那第二個函數為什么快呢,我們將每次的計算結果存在了函數里,直接調用,避免了重復計算(當然不是說所有的遞歸都會重復計算子問題),第二個函數我們其實可以看做是動態規划的思想,從上面的代碼來看:

  動態規划的思想==遞推式+重復子問題

  怎么理解呢,就是說動態規划遵循一套固定的流程:遞歸的暴力解法  ——>  帶備忘錄的遞歸解法  ——> 非遞歸的動態規划解法    這個過程是層次遞進的解決問題的過程,你如果沒有前面的鋪墊,直接看最終的非遞歸動態規划解法,當然覺得難。

1,什么是動態規划

  動態規划(dynamic programming)是運籌學的一個分支,是求解決策過程(decision process)最優化的數學方法。把多階段過程轉化為一系列單階段問題,利用各階段之間的關系,逐個求解,創立了解決這類過程優化問題的新方法——動態規划。

1.1,使用動態規划特征

  • 1. 求一個問題的最優解 
  • 2. 大問題可以分解為子問題,子問題還有重疊的更小的子問題 
  • 3. 整體問題最優解取決於子問題的最優解(狀態轉移方程) 
  • 4. 從上往下分析問題,從下往上解決問題 
  • 5. 討論底層的邊界問題

1.2,動態規划的基本思想

  若要解一個給定問題,我們需要解其不同部分(即子問題),再合並子問題的解以得出原問題的解。通常許多子問題非常相似,為此動態規划法試圖僅僅解決每個子問題一次,從而減少計算量:一旦某個給定子問題的解已經算出,則將其記憶化存儲,以便下次需要同一個子問題解之時直接查表。這種做法在重復子問題的數目關於輸入的規模呈指數增長時特別有效。

  動態規划最重要的有三個概念:1、最優子結構 2、邊界 3、狀態轉移方程

  所以我們在學習動態規划要明白三件事情:

1,目標問題

2,狀態的定義:opt[n]

3,狀態轉移方程:opt[n] = best_of(opt[n-1], opt[n-2])

   其實狀態轉移方差直接代表着暴力解法,千萬不要看不起暴力解,動態規划問題最難的就是寫出狀態轉移方差,即這個暴力解。

2,鋼條切割問題

  某公司出售鋼條,出售價格與鋼條長度直接的關系如下表:

   問題:現在有一條長度為 n 的鋼條和上面的價格表,求切割鋼條方案,使得總收益最大。

  分析:長度為4的鋼條的所有切割方案如下:(C方案最優)

   思考:長度為 n 的鋼條的不同切割方案有幾種?

  下面是當長度為n的時候,最優價格的表格( i 表示長度為 n ,r[i] 表示最優價格)

2.1,遞推式解決鋼條切割問題

  設長度為 n 的鋼條切割后最優收益值為 Rn,可以得到遞推式:

  第一個參數Pn 表示不切割

  其他 n-1個參數分別表示另外 n-1種不同切割方案,對方案 i=1,2,...n-1 將鋼條切割為長度為 i 和 n-i 兩段

  方案 i  的收益為切割兩段的最優收益之和,考察所有的 i,選擇其中收益最大的方案

2.2,最優子結構解決鋼條切割問題

  可以將求解規模為 n 的原問題,划分為規模更小的子問題:完成一次切割后,可以將產生的兩段鋼條看成兩個獨立的鋼條切割問題。

  組合兩個子問題的最優解,並在所有可能的兩段切割方案中選取組合收益最大的,構成原問題的最優解。

  鋼條切割滿足最優子結構:問題的最優解由相關子問題的最優解組合而成,這些子問題可以獨立求解。

  鋼條切割問題還存在更簡單的遞歸求解方法:

  • 從鋼條的左邊切割下長度為 i 的一段,只對右邊剩下的一段繼續進行切割,左邊的不再切割
  • 遞推式簡化為:
  • 不做切割的方案就可以描述為:左邊一段長度為 n,收益為 pn,剩下一段長度為0,收益為 r0=0.

2.3,鋼條切割問題——自頂向下遞歸代碼及其時間復雜度

  代碼如下:

def _cut_rod(p, n):
    if n == 0:
        return 0
    q = 0
    for i in range(1, n+1):
        q = max(q, p[i] + _cut_rod(p, n-i))
    return q

  如下圖所示,當鋼條的長度增加時候,切割的方案次數隨着指數增加。當n=1的時候切割1次,n=2的時候切割2次,n=3的時候切割4次,n=4的時候切割8次。。。。

  所以:自頂向下遞歸實現的時間復雜度為 O(2n)

2.4,兩種方法的代碼實現

  代碼如下:

# _*_coding:utf-8_*_
import time


# 給遞歸函數一個裝飾器,它就遞歸的裝飾!! 所以為了防止這樣我們再套一層即可
def cal_time(func):
    def wrapper(*args, **kwargs):
        t1 = time.time()
        result = func(*args, **kwargs)
        t2 = time.time()
        print('%s running time : %s secs' % (func.__name__, t2 - t1))
        return result

    return wrapper


# p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 21, 23, 24, 26, 27, 28, 30, 33, 36, 39, 40]
p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]


def cut_rod_recurision_1(p, n):
    if n == 0:
        return 0
    else:
        res = p[n]
        for i in range(1, n):
            res = max(res, cut_rod_recurision_1(p, i) + cut_rod_recurision_1(p, n - i))
        return res


# print(cut_rod_recurision_1(p, 9))


def cut_rod_recurision_2(p, n):
    if n == 0:
        return 0
    else:
        res = 0
        for i in range(1, n + 1):
            res = max(res, p[i] + cut_rod_recurision_2(p, n - i))
        return res


# print(cut_rod_recurision_2(p, 9))

@cal_time
def c1(p, n):
    return cut_rod_recurision_1(p, n)

@cal_time
def c2(p, n):
    return cut_rod_recurision_2(p, n)


print(c1(p, 10))
print(c2(p, 10))
'''
c1 running time : 0.02000117301940918 secs
30
c2 running time : 0.0010001659393310547 secs
30
'''

  我們通過計算時間,發現第二個遞歸方法明顯比第一個遞歸方法快很多。那么是否還有更簡單的方法呢?肯定有,下面學習動態規划。

2.5,動態規划解決鋼條切割問題

  遞歸算法由於重復求解相同子問題,效率極低。即使優化過后的遞歸也效率不高。那這里使用動態規划。

  動態規划的思想

  1. 每個子問題只求解一次,保存求解結果
  2. 之后需要此問題時,只需要查找保存的結果

  動態規划解法代碼:

def cut_rod_dp(p, n):
    r = [0 for _ in range(n+1)]
    for j in range(1, n+1):
        q = 0
        for i in range(1, j+1):
            q = max(q, p[i]+r[j-i])
        r[j] = q
    return r[n]

  或者便於理解這樣:

def cut_rod_dp(p, n):
    r = [0]
    for i in range(1, n+1):
        res = 0
        for j in range(1, i+1):
            res = max(res, p[j]+r[i-j])
        r.append(res)
    return r[n]

  時間復雜度: O(n2)

2.6,鋼條切割問題——重構解

  如何修改動態規划算法,使其不僅輸出最優解,還輸出最優切割方案?

  對於每個子問題,保存切割一次時左邊切下的長度

  下圖為r[i] 表示最優切割的價格,s[i]表示切割左邊的長度。

   代碼如下:

def cut_rod_extend(p, n):
    r = [0]
    s = [0]
    # 這個循環的意思是從底向上計算
    for i in range(1, n+1):
        res_r = 0  # 用來記錄價格的最優值
        res_s = 0  # 用來記錄切割左邊的最優長度
        for j in range(1, i+1):
            if p[j] + r[i-j] > res_r:
                res_r = p[j] + r[i-j]
                res_s = j
        r.append(res_r)
        s.append(res_s)
    return r[n], s

def cut_rod_solution(p, n):
    r, s = cut_rod_extend(p, n)
    ans = []
    while n>0:
        ans.append(s[n])
        n-= s[n]
    return ans


print(cut_rod_extend(p, 10))
# (30, [0, 1, 2, 3, 2, 2, 6, 1, 2, 3, 10])
print(cut_rod_solution(p, 9))
# [3, 6]

2.7,什么問題可以使用動態規划方法?

  1,最優子結構

  • 原問題的最優解中涉及多少個子問題
  • 在確定最優解使用那些子問題時,需要考慮多少種選擇

  2,重疊子問題

 

3,最長公共子序列

  一個序列的子序列是在該序列中刪去若干元素后得到的序列。例如:ABCD 和 BDF 都是 ABCDEFG 的子序列。

  在一個序列中,子串是連續的,子序列可以不連續

  最常公共子序列(LCS)問題:給定兩個序列 X 和 Y,求 X 和 Y 長度最大的公共子序列。例如 X = ABBCBDE,  Y = DBBCDB ,  LCS(X, Y) = BBCD 。

   應用場景:字符串相似度比對。

3.1,最長公共子序列的思路——暴力窮舉法

  當X的長度為m,Y的長度為n,則時間復雜度為: 2^(m+n) 

  雖然我們最先想到的時暴力窮舉法,但是很顯然,由其時間復雜度可知,這是不可取的。

3.2,最長公共子序列的思路——LCS是否具有最優子結構性質

  例如:要求 a = ABCBDAB  與 b = BDCABA 的LCS:

  由於最后一位 B!= A 

  因此LCS(a, b)應該來源於  LCS(a[: -1], b)與 LCS(a, b[: -1]) 中更大的哪一個。

   最優解的遞推式如下:

   c[i,j] 表示 Xi 和 Yj 的LCS 長度。

3.3,最長公共子序列的代碼

   代碼如下:

def lcs_length(x, y):
    m = len(x)
    n = len(y)
    c = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if x[i - 1] == y[j - 1]:  # i,j位置上的字符匹配的時候,來自於左上方
                c[i][j] = c[i - 1][j - 1] + 1
            else:
                c[i][j] = max(c[i - 1][j], c[i][j - 1])

    # 逐行打印
    for _ in c:
        print(_)
    return c[m][n]


# 最優值出來了,但是過程沒有出來,也就是只有最長,不知道公共子序列
# print(lcs_length("ABCBDAB", "BDCABA"))
'''
[0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 1, 1, 1]
[0, 1, 1, 1, 1, 2, 2]
[0, 1, 1, 2, 2, 2, 2]
[0, 1, 1, 2, 2, 3, 3]
[0, 1, 2, 2, 2, 3, 3]
[0, 1, 2, 2, 3, 3, 4]
[0, 1, 2, 2, 3, 4, 4]
4
'''


def lcs(x, y):
    m = len(x)
    n = len(y)
    c = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
    # 1左上方 2上方 3 左方
    b = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if x[i - 1] == y[j - 1]:  # i,j位置上的字符匹配的時候,來自於左上方
                c[i][j] = c[i - 1][j - 1] + 1
                b[i][j] = 1
            elif c[i - 1][j] > c[i][j - 1]:  # 來自於上方
                c[i][j] = c[i - 1][j]
                b[i][j] = 2
            else:
                c[i][j] = c[i][j - 1]
                b[i][j] = 3

    return c[m][n], b


# c, b = lcs("ABCBDAB", "BDCABA")
# for _ in b:
#     print(_)
'''
[0, 0, 0, 0, 0, 0, 0]
[0, 3, 3, 3, 1, 3, 1]
[0, 1, 3, 3, 3, 1, 3]
[0, 2, 3, 1, 3, 3, 3]
[0, 1, 3, 2, 3, 1, 3]
[0, 2, 1, 3, 3, 2, 3]
[0, 2, 2, 3, 1, 3, 1]
[0, 1, 2, 3, 2, 1, 3]
'''


def lcs_trackback(x, y):
    c, b = lcs(x, y)
    i = len(x)
    j = len(y)
    res = []
    while i > 0 and j > 0:
        if b[i][j] == 1:  # 來自左上方 =》匹配
            res.append(x[i - 1])
            i -= 1
            j -= 1
        elif b[i][j] == 2:  # 來自上方=》 不匹配
            i -= 1
        else:  # ==3  來自左方 =》不匹配
            j -= 1
    # 因為是回溯法,所以倒着寫的,我們最后需要reverse回來
    return "".join(reversed(res))

print(lcs_trackback("ABCBDAB", "BDCABA"))
# BDAB

  

4,最大子序和

  給定一個整數數組 nums ,找到一個具有最大和的連續子數組(子數組最少包含一個元素),返回其最大值。

  示例:輸入:[-2, 1, -3, 4, -1, 2, 1, -5, 4]   輸出:輸出:6

  思路:我們首先分析題目,為什么最大和的連續子數組不包括其他的元素而是這幾個呢?如果我們想在現有的基礎上去擴展當前連續子數組,相鄰的元素是一定要被加入的,而相鄰元素可能會減損當前的和。

4.1,遍歷法

  算法過程:遍歷數組,用 sum 去維護當前元素加起來的和,當 sum 出現小於0的情況時,我們把它設為0,然后每次都更新全局最大值。

 def maxSubArray(self, nums: List[int]) -> int:
        sum = 0
        MaxSum = nums[0]
        for i in range(len(nums)):
            sum += nums[i]
            MaxSum = max(sum, MaxSum)
            if sum <0:
                sum = 0
        return MaxSum

  那看起來這么簡單,如何理解呢?一開始思考數組是個空的,我們每次選一個 nums[i] 加入當前數組中新增了一個元素,也就是用動態的眼光去考慮。代碼簡單為什么就能達到效果呢?

  我們進行的加和是按照順序來的,當我們i 選出來后,加入當前 sum,這時候有兩種情況:

1,假設我們當前 sum 一致都大於零,那每一次計算的 sum 都是包括開頭元素的一端子序列。

2,出現小於0的時候,說明我們當前子序列第一次小於零,所以我們現在形成的連續數組不能包括之前的連續子序了,於是拋棄他們,重新開始新的子序。

4.2,動態規划

  設sum[i]為以第i個元素結尾的最大的連續子數組的和。假設對於元素i,所有以它前面的元素結尾的子數組的長度都已經求得,那么以第i個元素結尾且和最大的連續子數組實際上,要么是以第i-1個元素結尾且和最大的連續子數組加上這個元素,要么是只包含第i個元素,即sum[i]= max(sum[i-1] + a[i], a[i])。可以通過判斷sum[i-1] + a[i]是否大於a[i]來做選擇,而這實際上等價於判斷sum[i-1]是否大於0。由於每次運算只需要前一次的結果,因此並不需要像普通的動態規划那樣保留之前所有的計算結果,只需要保留上一次的即可,因此算法的時間和空間復雜度都很小。

  代碼如下:

def maxSubArray4(self, nums: List[int]) -> int:
    length = len(nums)
    for i in range(1, length):
        # 當前值的大小與前面的值之和比較,若當前值更大,則取當前值,舍棄前面的值之和
        subMaxSum = max(nums[i]+nums[i-1], nums[i])
        # 將當前和最大的賦給 nums[i], 新的nums 存儲的為何值
        nums[i] = subMaxSum
    return max(nums)

  只要遍歷一遍。nums[i]表示的是以當前這第i號元素結尾(看清了一定要包含當前的這個i)的話,最大的值無非就是看以i-1結尾的最大和的子序能不能加上我這個nums[i],如果nums[i]>0的話,則加上。注意我代碼中沒有顯式地去這樣判斷,不過我的Max表達的就是這個意思,然后我們把nums[i]確定下來。

4.3  總結

  計算機解決問題其實沒有任何奇技淫巧,它唯一的解決辦法就是窮舉,窮舉所有可能性。算法設計無法就是先思考“如何窮舉”,然后再追求“如何聰明的窮舉”。

  列出動態轉移方差,就是在解決“如何窮舉”的問題,之所以說他難,一是因為很多窮舉需要遞歸實現,二是因為有的問題本身的解空間復雜,不難容易窮舉完整。

  備忘錄,DP table 就是在追求“如何聰明地窮舉”。用空間換時間的思路,是降低時間復雜度的不二法門。

  動態規划需要和回溯法搭配着使用,動態規划只負責求最優解,而回溯法則可以找到最優值的路徑。

5,回溯法

  回溯法是一種選優搜索法,按選優條件向前搜索,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步重新選擇,這種走不通就退回再走的技術為回溯法,而滿足回溯條件的某個狀態的點稱為 “回溯點”。許多復雜的,規模較大的問題都可以使用回溯法,有“通用解題方法”的美稱。回溯法有“通用的解題法”之稱,也叫試探法,它是一種系統的搜索問題的解的方法。簡單來說,回溯法采用試錯的方法解決問題,一旦發現當前步驟失敗,回溯方法就返回上一個步驟,選擇另一種方案繼續試錯。

5.1  回溯法的基本思想

  從一條路往前走,能進則進,不能進則退回來,換一條路再試。

5.2  回溯法的一般步驟

1,定義一個解空間(子集樹,排序樹二選一)

2,利用適用於搜索的方法組織解空間

3,利用深度優先法搜索解空間

4,利用剪枝函數避免移動到不可能產生解的子空間

5.3  回溯法針對問題的特點

  回溯算法針對的大多數問題有以下特點:問題的答案有多個元素(可向下成走迷宮是有多個決定),答案需要一些約束(比如數獨),尋找答案的方式在每一個步驟相同。回溯算法逐步構建答案,並在確定候選元素不滿足約束后立刻放棄候選元素(一旦碰牆就返回),直到找到答案的所有元素。  

5.4回溯法題目——查找單詞

  問題描述:你玩過報紙上那種查找單詞的游戲嗎?就是那種在一堆字母中橫向或豎向找出單詞的游戲。小明在玩一個和那個很像的游戲,只不過現在不僅可以上下左右連接字母,還可以拐彎。如圖所示,輸入world,就會輸出“找到了”。

 

 

5.5  回溯法題目——遍歷所有的排列方式

  問題描述:小米最近有四本想讀的書:《紅色的北京》,《黃色的巴黎》,《藍色的夏威夷》,《綠色的哈薩里》,如果小明每次只能從圖書館借一本書,他一共有多少種借書的順序呢?

   回溯法是一種通過探索所有可能的候選解來找出所欲的解的算法。如果候選解被確認,不是一個解的話(或者至少不是最后一個解),回溯算法會通過在上一步進行一些變換排期該解。即回溯並且再次嘗試。

  這里有一個回溯函數,使用第一個整數的索引作為參數  backtrack(first)。

1,如果第一個整數有索引 n,意味着當前排列已完成。

2,遍歷索引 first 到索引 n-1 的所有整數 ,則:

  • 在排列中放置第 i 個整數,即 swap(nums[first], nums[i])
  • 繼續生成從第 i 個整數開始的所有排列:backtrack(first +1)
  • 現在回溯,通過 swap(nums[first], nums[i]) 還原。

   代碼如下:

class Solution:
    def permute(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        def backtrack(first = 0):
            # if all integers are used up
            if first == n:  
                output.append(nums[:])
            for i in range(first, n):
                # place i-th integer first 
                # in the current permutation
                nums[first], nums[i] = nums[i], nums[first]
                # use next integers to complete the permutations
                backtrack(first + 1)
                # backtrack
                nums[first], nums[i] = nums[i], nums[first]
        
        n = len(nums)
        output = []
        backtrack()
        return output

  便於理解的代碼如下:

class solution:
    def solvepermutation(self, array):
        self.helper(array, [])

    def helper(self, array, solution):
        if len(array) == 0:
            print(solution)
            return
        for i in range(len(array)):
            newarray = array[:i] + array[i + 1:]  # 刪除書本
            newsolution = solution + [array[i]]  # 加入新書
            self.helper(newarray, newsolution)  # 尋找剩余對象的排列組合


solution().solvepermutation(["紅", "黃", "藍", "綠"])

  方法二:走捷徑(直接使用Python的庫函數,迭代函數itertools)

li = ['A', 'B', 'C', 'D']
def solutoin(li):
    import itertools
    res = list(itertools.permutations(li))
    return len(res)

  

5.6  回溯法問題——經典問題的組合

  問題描述:小明想上兩門選修課,他有四種選擇:A微積分,B音樂,C烹飪,D設計,小明一共有多少種不同的選課組合?

   當然第一個方法就是走捷徑!,直接使用python的庫函數itertools進行迭代:

li = ['A', 'B', 'C', 'D']
def solutoin(li):
    import itertools
    res = list(itertools.permutations(li, 2))
    return len(res)

  下面開始回溯法的學習。

class solution():
    def solvecombination(self, array, n):
        self.helper(array, n, [])

    def helper(self, array, n, solution):
        if len(solution) == n:
            print(solution)
            return
        for i in range(len(array)):
            newarray = array[i + 1:]  # 創建新的課程列表,更新列表,即選過的課程不能再選
            newsolution = solution + [array[i]]  # 將科目加入新的列表組合
            self.helper(newarray, n, newsolution)


solution().solvecombination(["A", "B", "C", "D"], 2)

  

5.7  回溯法問題——八皇后問題

  問題描述:保安負責人小安面臨一個難題,他需要在一個8x8公里的區域里修建8個保安站點,並確保每一行、每一列和每一條斜線上都只有一個保安站點。苦惱的小安試着嘗試布置了很多遍,但每一次都不符合要求。小安求助程序員小明,沒過多久小明就把好幾個布置方案(實際上,這個問題有92種答案)發給了小安,其中包括下面執行結果截圖,試問小明是怎么做到的。

 

6,算法綜合作業

  這是所有的算法學完后的綜合作業,當然這也是算法學習的一個總結。當然下面的問題我都有涉及,這里不做一一解答。

1. 實現以下算法並且編寫解題報告,解題報告中需要給出題目說明、自己對
題目的理解、解題思路、對算法的說明和理解、以及算法復雜度分析等內容

2. 實現冒泡排序、插入排序、快速排序和歸並排序

3. 以盡可能多的方法解決2-sum問題並分析其時間復雜度:給定一個列表和
一個整數,從列表中找到兩個數,使得兩數之和等於給定的數,返回兩個數
的下標。題目保證有且只有一組解

4. 封裝一個雙鏈表類,並實現雙鏈表的創建、查找、插入和刪除

5. 使用至少一種算法解決迷宮尋路問題

6. 使用動態規划算法實現最長公共子序列問題

  

傳送門:代碼的GitHub地址:https://github.com/LeBron-Jian/BasicAlgorithmPractice 

 

參考分治與動態規划參考文獻:https://blog.csdn.net/weixin_41250910/article/details/94502136

https://blog.csdn.net/weixin_43482259/article/details/97996658


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM