1. 程式人生 > >python動態規劃若干問題

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)