动态规划(dynamic programming)问题详解

版权声明:如需转载或引用,请注明出处。 https://blog.csdn.net/weixin_39278265/article/details/84937471

前言

最近要用到动态规划,所以这里研究一下。

1 问题描述

首先,背包问题主要有三类:
1)01背包问题
2)完全背包问题
3)多重背包问题
要了解背包问题,我可是查了很多资料,如今看来,只需要看:文献 [18], [19] 就ok。

其实我碰到的是一个完全背包问题,就是说给定的是:
一家店专门卖麦辣鸡翅,而且生意很不错,但是呢,这个价格有点奇葩,定价如下:

4个鸡翅XX块钱
5个鸡翅XX块钱
7个鸡翅XX块钱
8个鸡翅XX块钱
9 ...
10 ...
17 ...
18 ...
19 ...
20 ...
24 ...

这个列表列出了店家规定的价格(明显这家店的鸡翅不能一个一个买),而且一次买不同个数鸡翅价格不一样,比如买8个和买9个,单价就不一样。现在问道:
1)画出鸡翅和单价的曲线;
2)如果买218个鸡翅,怎么最划算?

2 解题过程

2.1 第一问:画图

import matplotlib.pyplot as plt
import pandas as pd
data=pd.read_csv('menu.csv').values

x=data[:,0].tolist()
y=[data[i][1]/data[i][0] for i in range(0,len(data))]

plt.plot(x,y)
plt.xlabel('Number of Chicken Wings')
plt.ylabel('Corresponding Unit Price')

plt.xticks(x,rotation=45,fontsize=5)
plt.yticks(y,fontsize=5)
plt.grid()

这段代码可以说是得意之作了,非常简洁(当然这个过程看了很多参考文献,不容易。还用了help函数)。

结果如下:
在这里插入图片描述

果然有点神奇。。。单价波动不小。。

2.2 第二问:动态规划求最优

一开始还没太明白动态规划,背包问题,看了[18],[19]还有很多文献后明白了,代码大概如下(比较粗糙):

# -*- coding: utf-8 -*-
"""
Created on Mon Dec 10 20:54:15 2018

@author: dehen
"""

# -*- coding: utf-8 -*-
"""
Created on Mon Dec 10 12:16:05 2018

@author: dehen
"""

#import matplotlib.pyplot as plt
#import csv
#import pandas as pd

#data1=csv.reader(open('menu.csv','r'))

#data=pd.read_csv('menu.csv').values   #header=None

#print(pd.core.frame.DataFrame
#x,y=[],[]
#for i in range(0,len(data)):#or data.shape[0]
#    x.append(float(data[i][0]))
#    y.append(float(data[i][1])/float(data[i][0]))

import matplotlib.pyplot as plt
import pandas as pd
import math
data=pd.read_csv('menu.csv').values

x=data[:,0].tolist()
y=[data[i][1]/data[i][0] for i in range(0,len(data))]

#plt.plot(x,y)
#plt.xlabel('Number of Chicken Wings')
#plt.ylabel('Corresponding Unit Price')

#plt.xticks(x,rotation=45,fontsize=5)
#plt.yticks(y,fontsize=5)
#plt.grid()

#,purchase_x
purchase_x=[0 for i in x]
x_bkup=x.copy()

wings_num=218
max_value=10000
wings=[10000]*(wings_num+1)
#wings[0]=0


def get_better(value1,value2):
#    return value1 if (value1/num1 != 0 and value1/num1 < value2/num2) else value2
    return value1 if value1<value2 else value2

x=[int(i) for i in x]

for pos in range(0,len(x)):
    wings[x[pos]]=x[pos]*y[pos]
wings_bkup=wings.copy()


for pos in range(0,len(x)):
    for num in range(x[pos],wings_num+1):
#    for num in range(wings_num,x[pos]-1,-1):
#        for num in range(wings_num,x[pos],-1):
#        if num>x[pos]:
        
#        wings[num]=get_better(wings[num],wings[num-x[pos]]+x[pos]*y[pos])
        if wings[num-x[pos]]+x[pos]*y[pos] <= wings[num]:
            wings[num]=wings[num-x[pos]]+x[pos]*y[pos]
#            if num==wings_num:
#            print('here better',x[pos])
#        print(num)
             #x[pos]*y[pos])   wings[x[pos]]
#        print(wings[num])
             
#j = len(x)-1
j=0
res = [0]*len(x)
wn = wings_num;

sum=0
print(wings[wings_num])

while wings_num >= x[0]:
    for pos in range(0,len(x)):
        if wings[wings_num]==wings[wings_num-x[pos]]+x[pos]*y[pos]:
            wings_num-=x[pos]
            sum+=x[pos]*y[pos]
            print('choose ',x[pos],pos,wings_num,y[pos])
    if wings[wings_num]==wings_bkup[wings_num]:
        sum+=wings_bkup[wings_num]
        print('choose ',wings_num)
        break

print('sum is',sum)
#wings[wings_num]=wings[wings_num-x[pos]]+x[pos]*y[pos]  pos
#wings_num-=x[pos]


#wings[wings_num]=wings[wings_num-x[pos]]+x[pos]*y[pos]  pos


#while(j!=-1):
#while j!=len(x):
#    if wings[wn]==wings[wn-x[j]]+x[j]*y[j]:
#        print('choose ',j)
#        wn-=x[j]
#        sum+=x[j]*y[j]
#        continue
#    j+=1
#        j+=1#continue
#    j-=1

#print('sum is:',sum,sum+x[37]*y[37]==wings[wings_num])

#for pos in range(0,len(x)):
#    if
#
#while(j!=-1):
#    count = int(math.floor(wn/x[j]))
#    print('count is:',count)
#    for k in range(count,0,-1):
#        if(wings[wn] == (wings[wn-x[j]*k]+k*x[j]*y[j])):
#            print('dale',k,wn,x[j],x[j]*k,wings[wn-x[j]*k],k*x[j]*y[j],wings[wn],y[j])
#            res[j] = k
#            wn = wn - k*x[j]
#            print(wn)
#            break
#    j-=1
#print(wings[wings_num])
#for i in range(0,len(res)):
#    print(res[i])    

这么多注释行足以看出我的尝试之多。。。


242.79999999999995
choose  9 5 209 1.1333333333333333
choose  28 24 181 1.1142857142857143
choose  28 24 153 1.1142857142857143
choose  125 37 28 1.112
choose  28
sum is 242.8

动态规划的第二种实现(改变初始化的方式)

而且有意思的是,我注释掉:

for pos in range(0,len(x)):
    wings[x[pos]]=x[pos]*y[pos]

同时在前面加上:
wings[0]=0

会得到不一样的也是最优方案的结果:

The cheapest total price for buying 218 wings is : 242.8
choose  6 2 212 1.1333333333333333
choose  9 5 203 1.1333333333333333
choose  25 21 178 1.112
choose  28 24 150 1.1142857142857143
choose  50 30 100 1.112
choose  25 21 75 1.112
choose  50 30 25 1.112
choose  25 21 0 1.112
sum is 242.80000000000007

这里给出比较完整的,有注释的代码:

# -*- coding: utf-8 -*-
"""
Created on Tue Dec 11 21:55:07 2018

@author: dehen
"""


import pandas as pd

data=pd.read_csv('menu.csv').values#这里是读文件

x=data[:,0].tolist() #将numpy转成list,方便后面的数据处理
y=[data[i][1]/data[i][0] for i in range(0,len(data))] # 计算单价,得到list

wings_num=218 # 要买的鸡翅数目
max_value=10000 # 设定的初始最大值,因为我们要买最便宜的,所以当然要设置成最大啦,不能像普通的背包问题,设为0
wings=[10000]*(wings_num+1)  # 初始化 最大值, 注意:wings列表里面存的是总价格,如wings[n]表示买n个鸡翅的时候,最便宜的总价格。
wings[0]=0 # 让wings[0]设置为0,不然的话,没法迭代。而且0个鸡翅,总价格肯定是0

x=[int(i) for i in x] #转成int,方便下面的迭代

wings_bkup=wings.copy() #备份wings,方便后面回溯出最优方案

# 这一段代码是动态规划的核心代码了
for pos in range(0,len(x)):
    for num in range(x[pos],wings_num+1): # 从x[pos]开始,到wings_num结束,因为range的原因,所以在range的时候用wings_num+1
    # 这个for循环非常讲究。具体参考  背包问题(转自背包九讲+对应题目) http://www.cnblogs.com/fht-litost/p/9204147.html  完全背包问题
       if wings[num-x[pos]]+x[pos]*y[pos] <= wings[num]: # 状态转移方程
            wings[num]=wings[num-x[pos]]+x[pos]*y[pos]  # 如果满足条件,就改变wings[num]的值

print("The cheapest total price for buying 218 wings is :",wings[wings_num]) 

sum=0 #回溯的时候,检查总价格对不对 identify the total price 其实也可以不要,只不过我自己想看下这样的sum和当时动态规划的sum有什么不一样
while wings_num >= x[0]: # 当wings_num比x[0]大,那就执行while下面的语句
    for pos in range(0,len(x)): # 回溯
        if wings[wings_num]==wings[wings_num-x[pos]]+x[pos]*y[pos]: #反向判断
            wings_num-=x[pos] # 说明这个x[pos]是被选中了,那么就改变wings_num的值,表明剩下的鸡翅数
            sum+=x[pos]*y[pos] # 计算价格
            print('choose ',x[pos],pos,wings_num,y[pos]) #输出选择信息
    if wings[wings_num]==0: # 终止条件,就是鸡翅都选完了
        break
    if wings[wings_num]==wings_bkup[wings_num]: # 如果只剩最后一次选取了,就执行这个循环。wings_bkup[wings_num]表明最初始的原始价格赋值
        sum+=wings_bkup[wings_num]
        print('choose ',wings_num)
        break

print('sum is',sum)

2.2-2 其实还可以递归求解

因为218是个比较巧妙的数字,刚好可以用贪心算法,递归求解,就是每次选单价最小的,直到选满218个鸡翅。

import matplotlib.pyplot as plt
import pandas as pd
data=pd.read_csv('menu.csv').values

x=data[:,0].tolist()
y=[data[i][1]/data[i][0] for i in range(0,len(data))]

plt.plot(x,y)
plt.xlabel('Number of Chicken Wings')
plt.ylabel('Corresponding Unit Price')

plt.xticks(x,rotation=45,fontsize=5)
plt.yticks(y,fontsize=5)
plt.grid()

#,purchase_x
purchase_x=[0 for i in x]
x_bkup=x.copy()
y_bkup=y.copy()

def purchase(total_num,x,y):
    best_price_pos=y.index(min(y))
    x_best = x[best_price_pos]
    if total_num == 0:
        return purchase_x
    if total_num < x_best:
        x.remove(x_best)
        y.remove(min(y))
        return purchase(total_num,x,y)
    while total_num >= x_best:
        total_num -= x_best
        purchase_x[best_price_pos] += 1
#        print('print',purchase_x[best_price_pos])
    x.remove(x_best)
    y.remove(min(y))
#    print(len(x),len(y))
    return purchase(total_num,x,y)

purchase(218,x,y)

total_price=0
print("\nThe best plan for buying 218 chicken wings is:\n")
while max(purchase_x) != 0:
    pos=purchase_x.index(max(purchase_x))
    print('Buy','%d' %x_bkup[pos],'wings for',purchase_x[pos])#,'times',y_bkup[pos])
    total_price+=x_bkup[pos]*purchase_x[pos]*y_bkup[pos]
    purchase_x[pos]=0
print('\nThe total cost for the best plan is : %.4f' %total_price)

输出:


The best plan for buying 218 chicken wings is:

Buy 25 wings for 8
Buy 6 wings for 3

The total cost for the best plan is : 242.8000

参考文献

[1] 动态规划. https://zh.wikipedia.org/wiki/动态规划

这里讲了DP的几个性质:

最优子结构性质。 如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
无后效性。 即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。
子问题重叠性质。 子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。

基本介绍:
动态规划在查找有很多重叠子问题的情况的最优解时有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并被保存,从简单的问题直到整个问题都被解决。因此,动态规划保存递归时的结果,因而不会在解决同样的问题时花费时间。
动态规划只能应用于有最优子结构的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决。

[2] Python numpy 提取矩阵的某一行或某一列 https://blog.csdn.net/luoganttcc/article/details/74080768
这里讲了怎么提取numpy矩阵的某一列,如 a[:,0]

[3] Python使用pandas处理CSV文件的实例讲解. https://www.jb51.net/article/142430.htm
这里讲了下pandas的索引,但是我还是使用索引失败了,显示没有,很尴尬。

[4] python中利用pandas读写csv文件。 https://www.cnblogs.com/ystwyfe/p/8289044.html

[5] python之pandas简单介绍及使用(一) https://www.cnblogs.com/misswangxing/p/7903595.html
这里介绍了下series和dataframe,还是挺有科普意义的。

[6] python之pandas用法大全. https://www.jb51.net/article/136360.htm
pandas的基本用法,函数之类的,还是可以的。

[7] 使用python读写CSV文件的三种方法 https://blog.csdn.net/u013555719/article/details/79293198
在这里学到了用pandas读取csv文件。

[8] python pandas读文件(不把第一行作列属性) https://blog.csdn.net/SZU_Hadooper/article/details/78913644
这里讲了怎么跳过第一行,不过我没用上。

最后,我想纯粹得到除第一行(标签)外的数据,还是在console用的help(pd.core.frame.DataFrame)找到了它的一个values

[9] python 类属性的访问. https://blog.csdn.net/Rubik_Wong/article/details/79848809
这里科普了下__dict__方法,我试了下:pd.core.frame.DataFrame.dict

[10] python—调用类属性的方法. https://blog.csdn.net/niedongri/article/details/79669612
从这里知道.values应该是一个类属性吧,,,

[11] matplotlib设置坐标轴. https://blog.csdn.net/xtingjie/article/details/71156743
这个让我知道怎么写plt.xticks,yticks函数。
但是主要还是得靠help(plt.xticks) (注意xticks后面是没有()的,不然就变成调用函数了…)

[12] python Matplotlib画图之调整字体大小的示例. https://www.jb51.net/article/128549.htm
这个告诉我如何控制字体大小。

[13] python matplotlib 画图刻度、图例等字体、字体大小、刻度密度、线条样式设置. https://blog.csdn.net/u010358304/article/details/78906768
更细致的讲解。同[12]

[14] python中寻找list中的最值和所对应的索引 https://blog.csdn.net/sinat_32547403/article/details/53374726
告诉我这个numpy矩阵转list的函数,是aa.tolist() aa是numpy矩阵。

[15] Python命名规范 https://blog.csdn.net/zhangzeyuaaa/article/details/78574919
告诉我文件名全小写,可以加入下划线提高可读性。
函数名的话:

函数名应该为小写,可以用下划线风格单词以增加可读性。如:myfunction,my_example_function。
注意:混合大小写仅被允许用于这种风格已经占据优势的时候,以便保持向后兼容。

[16] Python List remove()方法 http://www.runoob.com/python/att-list-remove.html
该方法没有返回值但是会移除列表中的某个值的第一个匹配项。

[17] python列表的三种删除方法 https://www.cnblogs.com/wrnb/p/8846213.html
可以用del aa[i]删除列表第i个位置的元素。
也可以用pop删除最后一个元素
还可以用remove。

[18] 背包问题详解:01背包、完全背包、多重背包 https://blog.csdn.net/na_beginning/article/details/62884939
在这里学会了01、完全、多重背包问题的解法。此外,我把这些C++程序都转成了python程序。

看来做什么都还是要动手啊!!!
不要说自己没理解理论,可以边做边理解的。!!!

[19] 背包问题(转自背包九讲+对应题目) http://www.cnblogs.com/fht-litost/p/9204147.html
这个讲的很强了算。

猜你喜欢

转载自blog.csdn.net/weixin_39278265/article/details/84937471