1. 程式人生 > 其它 >09_Python演算法+資料結構筆記-分數揹包-數字拼接-活動選擇-動態規劃-鋼條切割

09_Python演算法+資料結構筆記-分數揹包-數字拼接-活動選擇-動態規劃-鋼條切割

技術標籤:Python演算法+資料結構筆記python資料結構與演算法

b站視訊:路飛IT學城
https://www.bilibili.com/video/BV1mp4y1D7UP

文章目錄


個人部落格
https://blog.csdn.net/cPen_web

#81 分數揹包

###### 揹包問題
# 一個小偷在某個商店發現有N個商品,第i個商品價值Vi元,重Wi千克。他希望拿走的價值儘量高,但他的揹包最多隻能容納W千克的東西。他應該拿走哪些商品?
#注:揹包問題 往下細分 有2種不太一樣的問題:0-1揹包 和 分數揹包
#   ·0-1揹包:對於一個商品,小偷要麼把它完整拿走,要麼留下。不能只拿走一部分,或把一個商品拿走多次。(商品為金條)
#   ·分數揹包:對於一個商品,小偷可以拿走其中任意一部分。(商品為金砂)
### 舉例
# 商品 1:V1=60  W1=10         1千克 6塊錢
# 商品 2:V2=100 W2=20         1千克 5塊錢
# 商品 3:V3=120 W3=30         1千克 4塊錢  降序
# 揹包容量:W=50
# 對於0-1揹包和分數揹包,貪心演算法是否都能得到最優解?為什麼?
#注:怎麼貪心?算單位重量的商品 分別值多少錢,先把單位重量的 更貴的 先拿走,然後如果包裡還有地方 拿剩下的
#注:這個貪心演算法 對於 分數揹包 一定是 最優的 ,生活常識。包一定是滿的,一點都不剩
#注:對於0-1揹包,貪心的來做,拿走的價值是 160 (商品1和商品2 都拿走),是最好的嗎?不是最好的,比如 只拿商品1和商品2

#注:顯然 0-1揹包,不能用貪心演算法來做。因為 分數揹包裝的時候,最後肯定是滿的,但是 0-1揹包不一定,按貪心演算法來拿 最後可能剩下很多容量

#82 分數揹包實現

goods = [(60, 10), (100, 20), (120, 30)]    # 每個商品元組表示  (價格,重量)
goods.sort(key=lambda x: x[0] / x[1], reverse=True) # 按照單位價格降序排列 def fractional_backpack(goods, w): # 引數 商品、w揹包重量 # 貪心拿商品,拿單位重量更值錢的商品,所以先對goods進行排序 m = [0 for _ in range(len(goods))] # m表示每個商品拿多少,存結果的 total_v =0 # 存結果的 拿走的總價值 for i, (price,weight) in enumerate(goods): if w >= weight: m[i] = 1 # 這個商品拿多少。拿1整個,或者小數 total_v += price # 更新拿走的 價值 w -= weight # 並更新w的值 (揹包重量) else: m[i] = w / weight total_v += m[i] * price # 更新拿走的 價值 w = 0 # 揹包滿了 break return total_v, m # 最後返回 存結果的m、total_v print(fractional_backpack(goods, 50)) #結果為 # (240.0, [1, 1, 0.6666666666666666])
#注:揹包問題,貪心演算法,先拿單位重量裡最值錢的

#83 數字拼接問題

###### 拼接最大數字問題 (面試經常問)
# 切有N個非負整數,將其按照字串拼接的方式拼接為一個整數。 如何拼接可以使得得到的整數最大?
# 例:32,94,128,1286,6,71可以拼接的最大整數為 94716321286128
#注:按照字串比較的方式 來排序(先比首位,首位大的最大,首位一樣的 再往後走……直到有一個最大,那個最大;或者直到有 一個有 一個沒有,沒有的最大)
# (整數比較:32和6,32大;字串比較:32和6,6大)
#注:128 和 1286 怎麼排?(2個數拼接)
        # 1286128  1281286,1286128大
        # 7286728  7287286,7287286大
#注:這個問題怎麼解決?貪心的去做,貪頭位。但是一個串長、一個串短,並且短串還是長串的子串 (128\1286) 怎麼辦?
#------------------------------
a = "96"        # a和b長度一樣時,表示式沒問題
b = "87"

a + b if a>b else b + a
#------------------------------
a = "128"
b = "1286"

a + b = "1281286"   # 長度一樣
b + a = "1286128"   # b + a 大

a + b if a+b>b+a else b+a   # 這樣寫
#---------

a = "728"
b = "7286"
a + b = "7287286"
b + a = "7286728"
#------------------------------

#84 數字拼接問題實現

from functools import cmp_to_key    # 傳一個老式的cmp函式,轉換成1個key函式
li = [32, 94, 128, 1286, 6, 71]

# Python2的cmp函式 傳2個引數的一個函式,然後根據比較函式返回它們的比較結果 (x<y返回-1, x=y返回0,x>y返回1)
def xy_cmp(x, y):                    # 寫的cmp函式
    if x+y < y+x:
        return 1                     # x>y  讓大的去前面 x和y交換,因為預設小數在前面
    elif x+y > y+x:
        return -1
    else:
        return 0

def number_join(li):
    li = list(map(str, li)) # 把數字變成字串
    # 接下來 對li 進行一個排序(2個值對比 做交換) 94在最前面
    # li.sort(cmp=lambda x,y: )   # Python2的cmp函式 傳2個引數的一個函式,然後根據比較函式返回它們的比較結果 (x<y返回-1,x=y返回0,x>y返回1)
    li.sort(key=cmp_to_key(xy_cmp))     # 執行後 li已經是排好序的了
    return "".join(li)                 # 返回字串

print(number_join(li))
# 結果為 94716321286128
#注:li.sort(key=cmp_to_key(xy_cmp)) 看不懂的話,可以寫個快排或者冒泡,冒泡:看2個元素是否交換,看a+b>b+a 還是 a+b<b+a

#85 活動選擇問題

###### 活動選擇問題 (貪心演算法)
#   ·假設有n個活動,這些活動要佔用同一片場地,而場地在某時刻只能供一個活動使用。
#   ·每個活動都有一個開始時間Si和結束時間fi(題目中時間以整數表示),表示活動在[Si, Fi)區間佔用場地。   #注:Fi這一刻 它不佔用,避免問題
#   ·問:安排哪些活動能夠使該場地舉辦的活動的個數最多?

活動

#注:11個活動,
#  Si 開始時間,
#  fi 結束時間,
# 怎麼安排?貪心的問題
### 貪心結論:最先結束的活動一定是最優解的一部分。 貪最早的點,接下來在剩下的 貪 最早的點……
#注:第一個結束的活動,一定在最優解裡面
#注:最早結束,後面剩的時間越長
### 證明:假設a是所有活動中最先結束的活動,b是最優解中最先結束的活動。
# 如果a=b,結論成⽴。
# 如果a≠b,則b的結束時間⼀定晚於a的結束時間,則此時用a替換掉最優解中的b,a⼀定不與最優解中的其他活動時間重疊,因此替換後的解也是最優解。
#注:為什麼 a⼀定不與最優解中的其他活動時間重疊? 因為a比b結束的早 那些活動都在b後面,所以不重疊

#86 活動選擇問題實現

activities = [(1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11), (8,12), (2,14), (12,16)]    # 活動,1個元組表示1個活動.開始時間、結束時間
# 保證活動是按照結束時間排好序的。      因為 貪心 是貪 最早結束的活動,排好序後 直接選第一個
activities.sort(key=lambda x:x[1])  # 按結束時間排序

def activity_selection(a):   # 傳參a 活動
    res = [a[0]]    # 最後返回的結果,一開始肯定有a[0]活動,a[0]是最早結束的
    #接下來 按照結束時間往後看,是否與前面時間衝突,不衝突 加進去
    for i in range(1,len(a)):   # 從1 開始,因為a[0] 已經進去了
        # 如果a[i]活動的開始時間>=res最後一個活動的結束時間
        if a[i][0] >= res[-1][1]:   # 當前活動的開始時間大於等於最後一個入選活動的結束時間
            # 不衝突
            res.append(a[i])
    return res

print(activity_selection(activities))
#結果為 [(1, 4), (5, 7), (8, 11), (12, 16)]   4個活動,一定是最大的

活動選擇問題 -- 精簡程式碼

activities = [(1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11), (8,12), (2,14), (12,16)]
# 保證活動是按照結束時間排好序的
activities.sort(key=lambda x:x[1])

def activity_selection(a):
    res = [a[0]]
    for i in range(1, len(a)):
        if a[i][0] >= res[-1][1]:   # 當前活動的開始時間小於等於最後一個入選活動的結束時間
            # 不衝突
            res.append(a[i])
    return res

print(activity_selection(activities))
#結果為 [(1, 4), (5, 7), (8, 11), (12, 16)]

#87 貪心演算法總結

#貪心演算法4個例子:找零問題、揹包問題、拼接數字、活動選擇
#注:知道按什麼來貪心
#注:4個問題 公共特點:
# 首選 都是 最優化問題 (什麼什麼什麼 最多、最少、最大、最小)
# 不是所有最優化問題,都能用貪心演算法來做, 比如揹包問題中的0-1揹包,貪心 不是最優的,這類問題 可以用動態規劃做
#注:貪心演算法 程式碼比較好寫,思路比較簡單,速度比較快
#注:比如活動選擇問題,不用貪心,窮舉所有情況,需要窮舉很多情況 ,n個活動  要看2**n 種方案。但是貪心演算法 程式碼複雜度 O(n)級別,很快

#88 動態規劃介紹

#注:動態規劃 是種思想,在各個演算法領域都有深入研究:基因測序、基因比對、序列的相似程度、hmm……,很多問題 都可以用動態規劃解決
###### 從斐波那契數列看動態規劃
#   ·斐波那契數列:Fn = Fn-1 + Fn-2        #注:遞推式
#   ·練習:使用遞迴和非遞迴的方法來求解斐波那契數列的第n項

#注:斐波拉契數列:後一項是前2項的和
#注:遞迴寫法
# 子問題的重複計算
def fibnacci(n):
    if n == 1 or n == 2:                        # 終止條件
        return 1
    else:
        return fibnacci(n-1) + fibnacci(n-2)    # 遞迴條件

print(fibnacci(10))
#結果為  第5項斐波拉契數列
# 55
#注:第100項斐波拉契數列 用遞迴寫法  時間很長很長。數不大,但是計算機很慢,為什麼?
#原因:子問題的重複計算,遞迴執行效率低
# f(6) = f(5) + f(4)
# f(5) = f(4) + f(3)
# f(4) = f(3) + f2)
# f(4) = f(3) + f(2)
# …………              #注:相同的問題算了很多遍
# f(3) = f(2) + f(1)
# f(3) = f(2) + f(1)
# f(2) = 1

非遞迴方法, 動態規劃(DP)的思想 = 最優子結構(遞推式) + 重複子問題

def fibnacci_no_recurision(n):
    f = [0,1,1]  # 下標是1的第一項是1,下標是2的第二項是1
    if n > 2:
        for i in range(n-2):    # 求第n項,往裡追加,追加n-2次  (求n=3,追加1次;求n=4,追加2次)
            num = f[-1] + f[-2] # 算這個數,再append到f裡去。這個數 = f最後一項 + f倒數第2項
            f.append(num)
    return f[n]             # 因為按照下標來的 所以第n項就是f[n]

print(fibnacci_no_recurision(100))
#結果為 354224848179261915075
#注:雖然這個數很大,但是運算很快
#注:為什麼遞迴方法很慢?
#注:因為遞迴執行效率低。為什麼遞迴執行效率低?
#注:子問題的重複計算
#注:為什麼 不用遞迴 算的比遞迴快
#注:因為把之算過的問題 存到了列表裡,只需要呼叫就行了 加一次。
#注:因為 不遞迴的 寫法裡 ,避免 子問題的重複計算,所以效率比 遞迴寫法 高一點

#注:不是說 所有的遞迴 慢問題 都是因為 子問題的重複計算
#注:DP思想 = 最優子結構(遞推式) + 重複子問題
#注:不想用遞迴來做,所以用迴圈方式,把需要的子問題 存起來,只算一遍,後邊的值從列表裡取這個值

#Python裡 取巧方法,遞迴函式前加裝飾器 @lru_cache,就能自動快取重複子問題的
@functools.lru_cache

精簡程式碼

# 子問題的重複計算	#注:遞迴寫法
def fibnacci(n):
    if n == 1 or n == 2:
        return 1
    else:
        return fibnacci(n-1) + fibnacci(n-2)

# 動態規劃(DP)的思想 = 遞推式 + 重複子問題
def fibnacci_no_recurision(n):		#注:DP思想
    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_no_recurision(100))
#結果為
# 354224848179261915075

#89 鋼條切割問題

###### 鋼條切割問題
#   ·某公司出售鋼條,出售價格與鋼條長度之間的關係如下表:

price

#注:一般來說,長的鋼條貴點,而且和長度不是倍數關係
#   ·問題:現有一段長度為n的鋼條和上面的價格表,求切割鋼條方案,使得總收益最大。#注:即 賣出去的錢最多

#   ·長度為4的鋼條的所有切割方案如下:(c方案最優)

方案

#   ·思考:長度為n的鋼條的不同切割方案有幾種?
#注:2**(n-1) (2的(n-1)次方)種方案。長度為n的鋼條,有n-1個切割的位置,可以切可以不切,每個位置2種選擇,所以2**(n-1)種可能
#注:如果相同的結果位置不同 看成同一種方案,那麼問題很難,在組合數學裡叫做整數切割問題
#注:一個一個列出來 不太合理

圖示

#注:r[i]  最優解,最優解,最優的情況下 能拿到多少錢

price

#注:長度為2時  5、1+1
#注:長度為3    8、6      不切 8塊;切1刀變成2和1或1和2 但不考慮 2不管切不切 最多能賣5塊錢,所以6塊錢
#注:長度為4    9、10     不切 9塊;切1刀 3和1 不管3切不切這個問題 給個3價格是8,所以9塊;切成2和2 ,2價格是5,所以 10
# ………………
#注:長度為9         不切 24塊;切1刀,1和8,1+22=23 不用管8切不切;切2刀,2和7,5+18=23 不用管7切不切;3和6,17+8=25;4和5,23;5和4…………
        #注:所以最後 3和6 最大,17+8=25,所以 9 最大值是25
#注:動態規劃 需要 1個遞推式 (即 最優子結構)
###### 鋼條切割問題 -- 遞椎式
#   ·設⻓度為n的鋼條切割後最優收益值為rn,可以得出遞推式:

遞推式

#   ·第⼀個引數Pn表示不切割
#   ·其他n-1個引數分別表示另外n-1種不同切割方案,對方案i=1,2,...,n-1
#       ·將鋼條切割為⻓度為i和n-i兩段
#       ·⽅案i的收益為切割兩段的最優收益之和
#注:比如說長度為9。9 、1+8 、2+7 、……、8+1 ,這些所有情況 要一個最大值 max
#   ·考察所有的i,選擇其中收益最大的方案
#注:切一刀 (n-1)種方案 和 不切,每一個方案的 收益 是多少。n-1種方案的收益 和不切割的 收益 做對比,選最大值  就是結果
#注:為什麼 這是對的? 最優子結構
###### 鋼條切割冋題 -- 最優子結構
# 可以將求解規模為n的原問題,劃分為規模更小的子問題:完成⼀次切割後,可以將產生的兩段鋼條看成兩個獨⽴的鋼條切割問題。
# 組合兩個子問題的最優解,並在所有可能的兩段切割⽅案中選取組合收益最⼤的,構成原問題的最優解。
# 鋼條切割滿足最優子結構:問題的最優解由相關⼦問題的最優解組合而成,這些子問題可以獨⽴求解。

#注:最優子結構:子問題的最優解 能夠算大的問題的最優解
#注:切1刀,只要算 2部分的最優解,不管這2部分 怎麼切的,所有方案中,取最大值max,就是最優解

#注:最優子結構 就是一個 遞推式,DP中 最重要
###### 鋼條切割問題 -- 最優子結構
# 鋼條切割問題還存在更簡單的⽅法
# 從鋼條的左邊切割下⻓度為i的⼀段,只對右邊剩下的⼀段繼續進⾏切割,左邊的不再切割
# 遞推式簡化為

遞推式

# 不做切割的⽅案就可以描述為:左邊⼀段⻓度為n,收益為Pn,剩餘⼀段⻓度為0,收益為r0=0。
#注:切1刀,但是左邊不切了,只切右邊。把右邊r n-1 換成P n-1
#注:即 如果長度為9,9不切 24; 1+8 1不切的價格 + 8最優解22; 2+7 2不切的價格 + 7最優解17……
#注:左邊不切,右邊繼續切  即左邊原價,右邊選擇最優解
#注:為什麼可以這樣算?為什麼這樣做最簡單? (即p + r)
#注:假如說 9 ,切成2 3 2 2 價格最大。
#注:在原來的 遞推式裡 可以看成 5 + 4  價格最大
#注:左邊 5 再切成 2和3,右邊 4 再切成 2和2
#注:但是 這個問題 也可以看成 2 3 2 2 ,切成 2 和 7,在 7 裡面再接著切
#注:所以 遞推式 看成一邊切、一邊不切 相當於看成  一邊是左邊一段,右邊 可以接著切,沒有漏的情況
#注:而 之前的 遞推式 可能會算重複行為。如果 9 切成 2 3 2 2 最優,那麼 看成 5 + 4 最優,2 + 7 最優,重複了
#注:第2個遞推式,看成2 部分 ,只對 右邊的 進行切割,左邊不切

#90 鋼條切割問題:自頂向下實現

寫法1 遞推式
遞推式1
寫法2 遞推式
遞推式2

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

# 寫法1
# 價格表  長度1 賣1塊錢;長度2 賣5塊錢
p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]
def cut_rod_recurision_1(p, n):   # 引數n  鋼條長度 ; p 價格表
    if n == 0:               # 遞迴 終止項 (長度0 賣 0塊錢)
        return 0
    else:                    # 接下來 學會看公式
        res = p[n]
        for i in range(1, n):   # 從1 到 n-1 的 n-1 種情況
            res = max(res, cut_rod_recurision_1(p, i) + cut_rod_recurision_1(p, n-i))   # 函式是求r n 的,遞迴
            # 每次迴圈 都是res自己跟另一個取最大,取完了n-1次方案,res就是所有方案中最大的那個數
        return res
#注:沒有問題 因為每次迴圈的時候 res已經取了最大值

print(cut_rod_recurision_1(p, 9))
#結果為 25
#注:第一種寫法 肯定很慢:重複計算 ,而且重複計算兩

# 寫法2  還是遞迴來寫,左邊不切割 右邊切割的式子
def cut_rod_recurision_2(p, n):
    if n == 0:  # 終止條件,= 0時返回0元
        return 0
    else:
        res = 0
        for i in range(1, n+1):    # 遞推式下標告訴了範圍 從1到n
            res = max(res,p[i] + cut_rod_recurision_2(p, n-i))  #p[i] 代表左邊 不切割的部分,r n-i  這個函式就是求r的
        # for迴圈完了 res就是最大值
        return res

print(cut_rod_recurision_2(p, 9))
#結果為 25
#注:給遞迴函式 加 裝飾器 ,會遞迴的裝飾
#注:解決方法:套一層馬甲  return 原函式
#注:方法1比方法2慢,因為方法1遞迴了2次,而且n需要算 比n小的所有子問題,子問題都會重複計算。這2個 演算法 實際上覆雜度很高,慢
#注:方法2 只算1個,還是會 重複子問題,最後問題會越來越細
#注:遞迴演算法的問題  自頂向下的遞迴實現,會出現效率差的問題
###### 鋼條切割問題 -- 自頂向下遞迴實現
# 為何⾃頂向下遞迴實現的效率會這麼差?
# 時間複雜度 O(2**n)

圖示

#注:那怎麼辦?答:動態規劃的解法
###### 鋼條切割問題 -- 動態規劃解法
# 遞迴演算法由於重複求解相同子問題,效率極低
# 動態規劃的思想:
# 每個子問題只求解一次,儲存求解結果段之後需要此問題時,只需查詢儲存的結果
#注:動態規劃需要2點:1、最優子結構(遞推式)    2、重複子問題
#注:自底向上的算 ,就不會重複求解;而 遞迴是 直接來r(n)來算,不好。
# 算r(1),算完存到列表裡不需要重複算,算r(2)……
#注:這就是 不用遞迴,用 迴圈、迭代、動態規劃的思想 解決問題

遞迴寫法 精簡程式碼

寫法1 遞推式

遞推式1

寫法2 遞推式

遞推式2

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

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