python動態規劃若干問題
轉載自點此這個人的系列文章。動態規劃和分治演算法有點類似,分治一般用於子問題互相獨立的情況,動態規劃一般用於子問題重疊的情況。
首先上個簡單的斐波那契數列,如果用遞迴:
def digui(n):
if n<1:
return -1
elif n==1 or n==2:
return 1
else:
return digui(n-1)+digui(n-2)
計算n=38時耗時11.5s
再看動態規劃:
def dongtai(n): pre=1 cur=1 if n<1: return -1 elif n==1 or n==2: return 1 for _ in range(n-2):#n=3時迴圈一次,4時迴圈2次。。。 value=pre+cur pre=cur cur=value return value
n=38時耗時為0
遞迴的時候,由於子問題重疊了,比如n=5時求解了digui(3) 3次,其耗時是指數上升的,動態規劃每次更新2個值,再計算新值,相當於空間換時間了。
問題一:求整數集合S的一個子集,使得子集元素和等於M
解答過程:假設在集合S的前i個元素中找子集,令解為set(i,M),如果S[i-1]>M(第i個元素大於M),則其肯定不在要找的子集中;如果S[i-1]<=M,那麼可能在也可能不在,如果不在,就在剩下的i-1個元素中繼續找子集,問題變為set(i-1,M),如果在,問題變為set(i-1,M-S[i-1])
程式碼:
def fun(s,n,M): a=np.array([[True]*(M+1)]*(n+1))#n+1行M+1列 for i in range(n+1): a[i,0]=True for i in range(1,M+1): a[0,i]=False #第0行從第2個開始都為False #從第一行開始,找前1個元素和等於1~M的解 #第i行,找前i個元素和分別為1~M的解 for i in range(1,n+1): for j in range(1,M+1): if s[i-1]>j: a[i,j]=a[i-1,j]#一定不在 else: a[i,j]=a[i-1,j] or a[i-1,j-s[i-1]]#可能在也可能不在 if a[n,M]:#這裡只能找到從左往右第一個子集 result=[] i=n while i>=0: if a[i,M] and not a[i-1,M]:#判斷第i個元素存在的根據 result.append(s[i-1]) M-=s[i-1] if M==0: break i=i-1 print(result) else: print('not found') print(a) S=[1,2,3,4,5,7] fun(S,len(S),7)
問題二:揹包問題,若干物體,已知每個重量和其價值,求重量不超過W時如何選擇物體使得總價值最大。
解答過程:依次求前i個物體不超過1~W重量的最大價值,當i=n,重量=W時,此元素即為最大價值。令解f(i,W)表示前i個物體重量不超過W的最大價值解,如果第i個物體重量大於W,不予考慮,如果不大於W,對應2種情況,一是放進揹包f(i-1,W-w[i-1]),一是不放進揹包f(i-1,W),對這2種情況取最大值即可。
程式碼如下:
import numpy as np #行李數n,不超過的重量W,重量列表w和價值列表p def fun(n,W,w,p): a=np.array([[0]*(W+1)]*(n+1)) #依次計算前i個行李的最大價值,n+1在n的基礎上進行 for i in range(1,n+1): for j in range(1,W+1): if w[i-1]>j: a[i,j]=a[i-1,j] else: a[i,j]=max(a[i-1,j],p[i-1]+a[i-1,j-w[i-1]])#2種情況取最大值 #print(a) print('max value is'+str(a[n,W])) findDetail(p,n,a[n,W]) #找到價值列表中的一個子集,使得其和等於前面求出的最大價值,即為選擇方案 def findDetail(p,n,v): a=np.array([[True]*(v+1)]*(n+1)) for i in range(0,n+1): a[i][0]=True for i in range(1,v+1): a[0][i]=False for i in range(1,n+1): for j in range(1,v+1): if p[i-1]>j: a[i,j]=a[i-1,j] else: a[i,j]=a[i-1,j] or a[i-1,j-p[i-1]] if a[n,v]: i=n result=[] while i>=0: if a[i,v] and not a[i-1,v]: result.append(p[i-1]) v-=p[i-1] if v==0: break i-=1 print(result) else: print('error') weights=[1,2,5,6,7,9] price=[1,6,18,22,28,36] fun(len(weights),13,weights,price)
問題三:找零錢問題,已經零錢面額為1、3、4,求找零n所用零錢數最少的方案
解答過程:對於找零n的最少零錢數f(n),它和f(n-1),f(n-3),f(n-4)有關,即它等於這3者中最小的值加1.
程式碼:
# 找零錢字典,key為面額,value為最少硬幣數
change_dict = {}
def rec_change(M, coins):
change_dict[0] = 0
s = 0
for money in range(1, M+1):
num_of_coins = float('inf')
#意思是要求50的最少找零數,在46,47,49的最少找零數中找到最小的即可
for coin in coins:
if money >= coin:
# 記錄每次所用的硬幣數量
if change_dict[money-coin]+1 < num_of_coins:
num_of_coins = change_dict[money-coin]+1
s = coin #記錄每次找零的面額
change_dict[money] = num_of_coins
return change_dict[M],s
# 求出具體的找零方式
# 用path變數記錄每次找零的面額
def method(M, coins):
print('Total denomination is %d.'%M)
nums, path = rec_change(M, coins)#path為最少硬幣數方案中的一個面額值
print('The smallest number of coins is %d.'%nums)
print('%s'%path, end='')
while M-path > 0:
M -= path
nums, path = rec_change(M, coins)
print(' -> %s'%path, end='')
print()
coins = (1, 3, 4)
method(50, coins)
問題四:鋼條切割,已經各長度的鋼條和對應的收益,問長度為n的鋼條怎麼切割收益最大。
要求長度為n的鋼條切割最大收益,則在n-1最大收益+長度1的收益,n-2最大收益+長度2最大收益……中取最大者。那麼依次求長度1~n的鋼條最大收益即可。
程式碼如下:
# 鋼條長度與對應的收益
length = (1, 2, 3, 4,5, 6, 7, 8, 9, 10)
profit = (1, 5, 8, 9,10, 17, 17, 20, 24, 30)
# 引數:profit: 收益列表, n: 鋼條總長度
def bottom_up_cut_rod(profit, n):
r = [0] # 收益列表
s = [0]*(n+1) # 切割方案列表
for j in range(1, n+1):
q = float('-inf')
#每次迴圈求出長度為j的鋼條切割最大收益r[j],s[j]則儲存切割方案中最長的那一段長度
for i in range(1, j+1):
if max(q, profit[length.index(i)]+r[j-i]) == profit[length.index(i)]+r[j-i]:#元組index從1開始
s[j] = i#如果切割方案為1和2,那麼2會覆蓋1,即儲存最長的一段
q = max(q, profit[length.index(i)]+r[j-i])
r.append(q)
#r[n]儲存長度為n鋼條最大切割收益
return r[n], s[n]
# 切割方案
def rod_cut_method(profit, n):
how = []
while n != 0:
t,s = bottom_up_cut_rod(profit, n)
how.append(s)
n -= s
return how
#輸出長度1~10鋼條最大收益和最佳切割方案
for i in range(1, 11):
t1 = time.time()
money,s = bottom_up_cut_rod(profit, i)
how = rod_cut_method(profit, i)
t2 = time.time()
print('profit of %d is %d. Cost time is %ss.'%(i, money, t2-t1))
print('Cut rod method:%s\n'%how)
問題五:水杯摔碎問題,有n個水杯和k層樓,求最少測試幾次可以確定水杯剛好在哪一層樓摔碎。
解答過程:假設從x層樓開始扔為f(n,x),如果水杯碎了水杯數量-1需要探測的樓層為x-1層,則為f(n-1,x-1),如果沒碎水杯還是n個需要探測k-x層,則為f(n,k-x)
程式碼:
import numpy as np
#n個水杯k層樓,最少需要幾次測試確定水杯在幾層樓剛好摔破
def solvepuzzle(n, k):
numdrops = np.array([[0]*(k+1)]*(n+1))
for i in range(k+1):
numdrops[1, i] = i#只有一個水杯,最壞情況是跟樓層數一樣
for i in range(2, n+1):#2到n個水杯
for j in range(1, k+1):#樓層1到k
minimum = float('inf')
#每次迴圈得出一種(i,j)下的最少次數
for x in range(1, j+1):
minimum = min(minimum, (1+max(numdrops[i, j-x], numdrops[i-1, x-1])))
numdrops[i, j] = minimum
print(numdrops)
return numdrops[n,k]
t = solvepuzzle(3, 10)
print(t)
問題六:給定n個水杯和d次嘗試機會,求最多能探測多少樓層。
解答過程:f(d,n)=f(d-1,n)+f(d-1,n-1),令g(d,n)=f(d,n+1)-f(d,n)=g(d-1,n)+g(d-1,n-1),這跟二項式C(n,k)=C(n-1,k)+C(n-1,k-1)相似,故g(d,n)=C(d,n)-->f(d,n)=求和(C(d,i)) i從1到n-1,i>=d時C(d,i)=0
程式碼:
#n個水杯d次嘗試機會,最多探測多少層樓?
#f(d,n)=求和i=1~n-1{C(d,i)} 對所有d>=1 and i<d
def assist(d,i):#C(d,i)
sum=1
sum2=1
for k in range(d-i+1,d+1):
sum*=k
for j in range(1,i+1):
sum2*=j
sum=sum/sum2
return sum
def f(d,n):
if d<1 or n<1:
return 0
elif d==1:
return 1
sum=0
for i in range(1,n+1):
if i<d:
sum+=assist(d,i)
return int(sum)
print(f(3,3))
#這裡可以逆向求解n個水杯k層樓至少需要多少次測試
def reverseFun(n,k):
d=1
while f(d,n)<k:
d+=1
print(d)
return d
reverseFun(3,10)
問題七:最大子陣列問題,給定一個數組,求其元素之和最大的子陣列
下面給出3種方法求解:
#對於全是正數(可能有分數)的陣列,如果求最大乘積子陣列,取對數後就變成了最大子陣列問題
#1,Kanade演算法:最簡潔
def maxSubArraySum(a, size):
max_so_far = float("-inf")
max_ending_here = 0
for i in range(size):
max_ending_here = max_ending_here + a[i]
if (max_so_far < max_ending_here):
max_so_far = max_ending_here
if max_ending_here < 0:#只要小於0就重新開始
max_ending_here = 0
return max_so_far
a=[-1,2,3,-5,6,7,-6,2,3,-5]
# value=maxSubArraySum(a,len(a))
# print(value)
#2,動態規劃
def DP_maximum_subarray(arr):
t = len(arr)
MS = [0]*t
MS[0] = arr[0]
for i in range(1, t):
MS[i] = max(MS[i-1]+arr[i], arr[i])
return MS#這個陣列第i項的意思是前i項的最大子陣列的值
# 3,分治演算法
import math
def find_max_crossing_subarray(A, low, mid, high):
max_left, max_right = -1, -1
# left part of the subarray
left_sum = float("-Inf")
sum = 0
for i in range(mid, low - 1, -1):
sum += A[i]
if (sum > left_sum):
left_sum = sum
max_left = i
# right part of the subarray
right_sum = float("-Inf")
sum = 0
for j in range(mid + 1, high + 1):
sum += A[j]
if (sum > right_sum):
right_sum = sum
max_right = j
return max_left, max_right, left_sum + right_sum
# using divide and conquer to solve maximum subarray problem
# time complexity: n*logn
def find_maximum_subarray(A, low, high):
if (high == low):
return low, high, A[low]
else:
mid = math.floor((low + high) / 2)
#以中間為分界,最大子陣列可能在左邊、右邊或跨越中點
left_low, left_high, left_sum = find_maximum_subarray(A, low, mid)
right_low, right_high, right_sum = find_maximum_subarray(A, mid + 1, high)
cross_low, cross_high, cross_sum = find_max_crossing_subarray(A, low, mid, high)
if (left_sum >= right_sum and left_sum >= cross_sum):
return left_low, left_high, left_sum
elif (right_sum >= left_sum and right_sum >= cross_sum):
return right_low, right_high, right_sum
else:
return cross_low, cross_high, cross_sum
from math import log,pow
#最大乘積子陣列
def maxMultipy(arr):
a=[log(i) for i in arr]
value=maxSubArraySum(a,len(a))
return pow(math.e,value)
b=[1,3,5,1/15,8,3,2]
result=maxMultipy(b)
print(result)