09_Python演算法+資料結構筆記-分數揹包-數字拼接-活動選擇-動態規劃-鋼條切割
阿新 • • 發佈:2020-12-29
技術標籤:Python演算法+資料結構筆記python資料結構與演算法
b站視訊:路飛IT學城
https://www.bilibili.com/video/BV1mp4y1D7UP
文章目錄
- #81 分數揹包
- #82 分數揹包實現
- #83 數字拼接問題
- #84 數字拼接問題實現
- #85 活動選擇問題
- #86 活動選擇問題實現
- #87 貪心演算法總結
- #88 動態規劃介紹
- #89 鋼條切割問題
- #90 鋼條切割問題:自頂向下實現
個人部落格
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 鋼條切割問題
###### 鋼條切割問題
# ·某公司出售鋼條,出售價格與鋼條長度之間的關係如下表:
#注:一般來說,長的鋼條貴點,而且和長度不是倍數關係
# ·問題:現有一段長度為n的鋼條和上面的價格表,求切割鋼條方案,使得總收益最大。#注:即 賣出去的錢最多
# ·長度為4的鋼條的所有切割方案如下:(c方案最優)
# ·思考:長度為n的鋼條的不同切割方案有幾種?
#注:2**(n-1) (2的(n-1)次方)種方案。長度為n的鋼條,有n-1個切割的位置,可以切可以不切,每個位置2種選擇,所以2**(n-1)種可能
#注:如果相同的結果位置不同 看成同一種方案,那麼問題很難,在組合數學裡叫做整數切割問題
#注:一個一個列出來 不太合理
#注:r[i] 最優解,最優解,最優的情況下 能拿到多少錢
#注:長度為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 遞推式
寫法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 遞推式
寫法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