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)

以上存在只能找出一个子集的问题,通常这类问题可以通过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)

 

 

 

你可能感兴趣的:(python动态规划若干问题)