实验四:动态规划算法与回溯法
用动态规划算法设计实现0-1背包问题,用回溯法设计实现0-1背包问题,并且用不同数据量进行实验对比分析,要求分析算法的时间复杂性并且提交相关代码等文档;
- 问题描述
(1)背包问题
给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。也可以将背包问题描述为决定性问题,即在总重量不超过W的前提下,总价值是否能达到V?它是在1978年由Merkle和Hellman提出的。
(2)动态规划算法
动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程。20世纪50年代初,美国数学家贝尔曼(R.Bellman)等人在研究多阶段决策过程的优化问题时,提出了著名的最优化原理,从而创立了动态规划。动态规划的应用极其广泛,包括工程技术、经济、工业生产、军事以及自动化控制等领域,并在背包问题、生产经营问题、资金管理问题、资源分配问题、最短路径问题和复杂系统可靠性问题等中取得了显著的效果
(3)回溯法
回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
- 实验目的
掌握动态规划法和回溯法设计思想、程序设计。
- 实验原理
(1)基于动态规划算法的0-1背包问题:
给定n种物品和一个背包,物品i的重量是wi,价值为vi,背包的容量为c。如何选择物品使得装进背包里的物品总价值最大。
解法就是利用动态规划和递归,将大问题分解为若干个小问题。
①最优子结构性质:
0-1背包问题具有最优子结构性质,一个原因是若干个最优解对应的物品总数就是减去一个最优解的重量后剩下的最优解物品总数。
②递归关系:
反复求最优解。下一个最优解是上一个的:
若物品不装入,则最优解直接跳过。
若物品计划装入,则取装入与不装入的价值最大值。
我的函数设计和书上的一致,但是用python实现的,原因在前一个实验已经讲明。设计如下:
Bag函数的时间复杂度是O(nc)
Show函数的时间复杂度是O(n)
(2)基于回溯法设计的0-1背包问题:
回溯法核心:
能进则进,进不了则换,换不了则退。(按照条件深度优先搜索,搜到某一步时,发现不是最优或者达不到目标,则退一步重新选择)。目前考虑的回溯法不进行算法的二次优化,只进行原始的代码编写。
解法采用面向对象的思想设计(其实是因为别的设计方案难以解决问题)
解决本问题思路:
使用0/1序列表示物品的放入情况。将搜索看做一棵二叉树,二叉树的第 i 层代表第 i 个物品,若剩余空间允许物品 i 放入背包,扩展左子树。若不可放入背包,判断限界条件,若后续继续扩展有可能取得最优价值,则扩展右子树(即此 i 物品不放入,但是考虑后续的物品)。在层数达到物品的个数时,停止继续扩展,开始回溯。
约束条件:
放入背包中物品的总质量小于等于背包容量
限界条件:
当前放入背包中物品的总价值(i及之前) + i 之后的物品总价值 < 已知的最优值 这种情况下就没有必要再进行搜索
- 实验设计
4.1 0-1背包问题(动态规划)
输入为物品的数量,书包能承受的重量,两个均为整数int
还有重量和价值数组(python 列表)
输出为最优价值,加入的物品(以编号列表的形式)
以及程序执行的时间
执行多次记录数据,图形化分析。
4.2 0-1 背包问题(回溯法解决)
使用面向对象方法解决。
因为vs code运行代码效率比较低,这次使用pycharm。
- 实验结果与分析
5.1 0-1背包问题(动态规划)
先尝试执行程序如下:
没有问题,很好,开始进行随机数测试:
首先n和c是有逻辑关系的,c过大会导致大部分的物品都被选中,c过小又会导致n的增加毫无意义。至于物品的价值和重量,为了方便就取1-n范围内的随机数。
Bag与show的时间默认取3次平均值,体现在表里了
忽略掉所有print对运行时间的影响
(1)首先不改变n,让c等差增加:
c | n | c*n | bag时间(ns) | show时间(ns) | bag1 | bag2 | bag3 | show1 | show2 | show3 |
1000 | 50 | 50000 | 23246633.33 | 1364933.333 | 22898700 | 22937500 | 23903700 | 1102600 | 996800 | 1995400 |
1200 | 50 | 60000 | 28243233.33 | 984700 | 29920300 | 25927500 | 28881900 | 999100 | 957800 | 997200 |
1400 | 50 | 70000 | 33128433.33 | 979533.3333 | 35489100 | 30984900 | 32911300 | 995900 | 928300 | 1014400 |
1600 | 50 | 80000 | 38128733.33 | 959900 | 37474800 | 39948500 | 36962900 | 1002900 | 942000 | 934800 |
1800 | 50 | 90000 | 47240066.67 | 964266.6667 | 47907200 | 45941000 | 47872000 | 960000 | 933800 | 999000 |
这是c*n与bag,show时间的图表
可以看到,c*n值的增加使得bag时间线性增长,但show的值几乎不变,这是因为bag的时间复杂度是O(cn),show的时间复杂度是O(n),而n的值被控制不变,只改变c的值。
(2)改变n的值使其等差增长,但不改变c的值:
c | n | c*n | bag时间(ns) | show时间(ns) | bag1 | bag2 | bag3 | show1 | show2 | show3 |
2000 | 50 | 100000 | 49222800 | 961433.3 | 47931200 | 47829100 | 51908100 | 936100 | 998600 | 949600 |
2000 | 55 | 110000 | 56543767 | 955700 | 50886100 | 58842000 | 59903200 | 937000 | 997300 | 932800 |
2000 | 60 | 120000 | 61846667 | 985600 | 64827900 | 63827400 | 56884700 | 999300 | 996900 | 960600 |
2000 | 65 | 130000 | 65841567 | 979866.7 | 66872700 | 64827600 | 65824400 | 944300 | 997400 | 997900 |
2000 | 70 | 140000 | 67170133 | 985833.3 | 66822400 | 65791100 | 68896900 | 956000 | 1003600 | 997900 |
Show的时间随n的增加基本呈线性关系,由于数据差太小,这种线性关系可能不是很明显。
N=50,c=1400的一次运行截图:
N=70,c=2000的一次运行截图:
5.2 0-1背包问题(回溯法)
先试运行:
进行数据随机化测量:
(注:经过理论证明和多次试运行,这个算法的效率要远低于动态规划算法,因此不取太大的数)
取背包重量200,价值和单个重量的值从1到50随机:
商品数量 | 执行时间(ns) |
15 | 16806966 |
20 | 376673867 |
25 | 2410045300 |
30 | 14938310733 |
35 | 38633429100 |
- 结论
使用动态规划算法和回溯法解决0-1背包问题,结论得到了较好的验证。在编写程序的过程中查阅了网上和书上的一些资料,在此表示感谢。
- 程序源码
7.1 动态规划算法:
import random
import time
def bag(n, c, w, v):
# 置零,表示初始状态
value = [[0 for j in range(c + 1)] for i in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, c + 1):
value[i][j] = value[i - 1][j]
# 背包总容量够放当前物体,遍历前一个状态考虑是否置换
if j >= w[i - 1] and value[i][j] < value[i - 1][j - w[i - 1]] + v[i - 1]:
value[i][j] = value[i - 1][j - w[i - 1]] + v[i - 1]
print("weight: ")
print(w)
print("value: ")
print(v)
# for x in value:
# print(x)
return value
def show(n, c, w, value):
print('the best value is ', value[n][c])
x = [False for i in range(n)]
j = c
for i in range(n, 0, -1):
if value[i][j] > value[i - 1][j]:
x[i - 1] = True
j -= w[i - 1]
print('the things in the bag: ')
for i in range(n):
if x[i]:
print('NO. ', i+1, ' thing,', end='')
n = 70 # 物品的数量,
c = 2000 # 书包能承受的重量,
w=[0 for _ in range(n)] # the weight of goods
v=[0 for _ in range(n)] # the values of goods
for k in range(n):
w[k]=random.randint(1,n)
v[k]=random.randint(1,n)
# 乱码严重,改为英文输出(utf-8 也会乱码我也是醉了)
t1=time.time_ns()
total = bag(n,c,w,v)
e1=time.time_ns()
t2=time.time_ns()
show(n,c,w,total)
e2=time.time_ns()
print("\n===============================")
print("time of function bag :"+str(e1-t1)+" ns")
print("time of function show :"+str(e2-t2)+" ns")
7.2 回溯法
import random
import time
import numpy
class BackSack(): # 定义背包类
def __init__(self, capacity): # 类的初始化
self.capacity = capacity # 背包最大容量(重量)
self.currentWeight = 0 # 背包当前重量
self.bestValue = 0 # 背包可容纳货物的最大价值,最优值
self.currentValue = 0 # 背包内当前已装货物的价值
def Backtrack(self, i): # 遍历解空间寻找最优值,I:当前搜索的深度
global length, weight, value, goods # 全局变量
if (i > length):
if self.currentValue > self.bestValue: # 更新最优值
self.bestValue = self.currentValue
self.currentCapacity = self.currentWeight # 当前最优解下的背包重量
self.bestgoods = goods[0:10]
print('best:', self.bestgoods) # 输出当前的最优解,最后一次输出即是最终的最优解
return
if self.currentWeight + weight[i] <= self.capacity: # 进入左子树,即选取goods[i]放入背包
goods[i] = 1
self.currentWeight = self.currentWeight + weight[i]
self.currentValue = self.currentValue + value[i]
self.Backtrack(i + 1)
self.currentValue = self.currentValue - value[i] # 进入右子树,即舍弃goods[i],不放入背包
self.currentWeight = self.currentWeight - weight[i]
goods[i] = 0
self.Backtrack(i + 1)
def main():
global length, weight, value, goods # 全局变量,分别表示货物数目,货物的重量数组,价值数组,货物的选取即0-1值
# currentWeight = 0
# bestValue = 0
# currentValue = 0
capacity = 200
number_goods = 35
value_max = 50
weight_max = 50
weight = [0 for _ in range(number_goods)]
for index1 in range(number_goods):
weight[index1] = random.randint(1, weight_max)
print(weight)
# weight = [2, 2, 6, 5, 4]
value = [0 for _ in range(number_goods)]
for index2 in range(number_goods):
value[index2] = random.randint(1, value_max)
print(value)
# value = [6, 3, 5, 4, 6]
goods = [0 for _ in range(number_goods)]
length = len(weight) - 1
backsack = BackSack(capacity)
start_time = time.time_ns()
backsack.Backtrack(0)
end_time = time.time_ns()
# backsack.Backtrack=Backtrack
print("===============================================")
print("Bag weight: " + str(capacity))
print("Number of goods: " + str(capacity // 2))
print("Best value: " + str(backsack.bestValue))
print("Current weight: " + str(backsack.currentCapacity)) # 输出最优值和背包内物品的总重量
print("Total time: " + str(end_time - start_time) + " ns.")
print(backsack.bestgoods) # 输出最优解
return end_time - start_time
times = [0, 0, 0]
for ix in range(3):
ctime = main()
times[ix] = ctime
print("################################################################")
print(numpy.mean(times))
项目源代码:github地址