转载自点此这个人的系列文章。动态规划和分治算法有点类似,分治一般用于子问题互相独立的情况,动态规划一般用于子问题重叠的情况。
首先上个简单的斐波那契数列,如果用递归:
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)
以上存在只能找出一个子集的问题,通常这类问题可以通过bfs或dfs解决,下面是java版递归和回溯法解决。
也可以用递归实现:可分别求解子集和为capacity,或和的范围在一定区间内。
static void findExact(int from, int capacity, int[] arr, String result) {
if (from == arr.length || capacity < 0)
return;
if (capacity == 0) {
System.out.println(result);
return;
}
findExact(from + 1, capacity - arr[from], arr, result + arr[from] + " ");
findExact(from + 1, capacity, arr, result);
}
//capacityRange,表示寻找范围toCapacity-capacityRange到toCapacity
static void findRange(int from, int capacityRange, int toCapacity, int[] a, String s) {
if (from == a.length || toCapacity < 0)
return;
if (toCapacity >= 0 && toCapacity <= capacityRange) {
System.out.println(s);
return;
}
findRange(from + 1, capacityRange, toCapacity - a[from], a, s + a[from] + " ");
findRange(from + 1, capacityRange, toCapacity, a, s);
}
还有一个经典的回溯法求解:参考文章
static class Node{
int cur;
int remain;
Node left;
Node right;
Node(int cur,int remain){
this.cur=cur;
this.remain=remain;
}
}
//回溯法
static void findExact(int[] a,int m){
//预排序,减少回溯过程
Arrays.sort(a);
//计算总和
int sum=Arrays.stream(a).reduce(0,(t,total)->t+total);
Node root=new Node(0,sum);
Node cur=root;
fun(cur,m,a,0, sum);
Node tmp=root;
printNode(tmp,m,"");
}
/**
* 顺序是排序好后的,如001表示排序好后第三个元素为所求
* @param tmp
* @param m
* @param s
*/
private static void printNode(Node tmp,int m,String s) {
if(tmp==null)
return;
if(tmp.cur==m){
System.out.println(s);
return;
}
printNode(tmp.left,m,s+1);
printNode(tmp.right,m,s+0);
}
/**
* 构造树
* @param cur
* @param m
* @param a
* @param from
* @param sum
*/
private static void fun(Node cur,int m,int[] a,int from,int sum) {
if(cur==null)
return;
if(from
问题二:背包问题,若干物体,已知每个重量和其价值,求重量不超过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
问题七:最大子数组问题,给定一个数组,求其元素之和最大的子数组
下面给出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)