动态规划-背包问题(跳跃点解法)

 

对于 0-1 背包问题,DP的解法很普遍。还有一种“跳跃点”的解法,该方法的提出,是根据背包求解过程中的记录表 v(i,j)的函数性特点而来的。(v(i,j)表记录的是前 i 种物品,达到总重量 j 时的最大利益)

可以Dp 求解一下,然后打印一下表进行观察,也可以根据这个求解原理,可以很自然的想到,v(i,j)的函值, 当 i 确定时,这是一个关于 j 的非递减函数,且由“跳跃点”将函数分段儿了,类似于上取整/下取整的函数图像,以跳跃点为分界非递减的一段一段儿“平”的函数图像。在求解过程中,每一个 i 都会有对应着一个这样的函数图像,并且最后一个求解时,刚好,最高的跳跃点对应的函数线即为解。

根据 v(i,j) = max(v(i-1,j) , v(i-1,j-w[i])+v[i]),即对于一个函数图像 v(i)是可以有其“前驱” v(i-1)得出的。求解过程是 由前导后的,并且根据公式,本质上,新的跳跃点,新的函数图像就是在已有图像的基础上得出的(即 i 状态本身就是在 i-1 的状态下得出的)。实际求解过程 :(过程中需要打表记录,表中的数据记录是一个structure,即占用 w 时最大价值为 v,过程中以 i (前 i 种物品为状态标记量))

1.初始化 p(0)<0 , 0 >   // 边界

2.由 p(i-1)[ v ( i -1 , j ) 的状态图 ] 得到 q(i-1)[ v ( i - 1 , j - w[i] ) + v[i] 的状态图 ] . 只需要在 p(i-1)的基础上 + (wi,vi)得到新的跳跃点即可。

3. p(i-1)并上 q(i-1)在减去必然不合法的点(同 w 下 v 非最大的点)即为 p(i)

上述即为求解过程,迭代实现即可。

代码在实际实现的过程中,把一维表 table 用作了 二维表,通过 head[ ] 数组的划分,达到了二维表的效果,head[ i ] 表示 p(i)在表中的首元素位置(这样来区分不同的i对应的数据,相当于行标记)。同时借助 p(i-1)导 p(i)的时候,用 l ,r 作为指针,卡在了p(i-1)的左右作为边界,next是p(i)要填写的位置,最初时 next 为 p(i)的head 位置。

图示:

 

先在 p(i-1)的元素 j 上得到一个新状态,然后 w 小于它的不受影响,直接搬,w 等于它的,对 v 取大更,w 大于它的,根据 v 值直接pass 掉 不合法的点(w 大但 v 小于前边的),同时,出现了新状态往里写的时候,也要注意,w 大 v 也大时才合法,才可以往里写,反之直接扔掉。(在跳跃点函数图像中,保留的的是 max ,即p(i-1),q(i-1)俩函数图取 max 的合图)

记录好表之后,从终态开始往回倒找解路径即可。

代码如下:

 

  1 // knapSack.cpp: 定义控制台应用程序的入口点。
  2 //
  3 
  4 #include "stdafx.h"
  5 // 动态规划 背包问题 跳跃点优化
  6 #include<stack>
  7 #include<minmax.h>
  8 #include<iostream>
  9 using namespace std;
 10 
 11 const int N = 1e4, M = 1e6;
 12 
 13 struct Data
 14 {
 15 	int w, v;
 16 	Data(int nw = 0, int nv = 0) :w(nw), v(nv) {}//构造函数,默认w,v都为0
 17 	Data operator +(const Data& r)const//设计data结构体的+操作
 18 	{
 19 		return Data(w + r.w, v + r.v);
 20 	}
 21 
 22 	bool operator ==(const Data& r)const//设计data结构体的==操作
 23 	{
 24 		return (w == r.w) && (v == r.v);
 25 	}
 26 };
 27 
 28 int head[N + 5];//标记每一个i的表的首元素,相当于将一维数组转换为二维数组
 29 Data goods[N], table[M];//goods[]存储物品信息,table[i]存储对应装前i个物品对应的(w,v)
 30 stack<int> numlist;//存放最终放入物品序号的栈
 31 int n, c;//物品数量和背包总容量
 32 
 33 // trace back to find the solution vector x[1……n]
 34 void traceBack(Data eState)
 35 {//eState是最后一个i的表的最后一个数据信息
 36 	int i, j;
 37 	bool x[N + 2];
 38 	for (i = n; i >= 1; --i)
 39 	{
 40 		x[i] = false;
 41 		for (j = head[i] - 1; j >= head[i - 1]; --j)//从p[i-1]的末尾遍历到p[i-1]的开头
 42 		{
 43 			if (table[j] + goods[i] == eState && (table[j].w != 0 || !j))
 44 			{//这里判断有没有加入第i个物品,若加入,则将i序号进栈,并且将对应i-1的表的最好的数据赋值给estate进行回溯
 45 				numlist.push(i);
 46 				//cout << i << ",";//测试进栈情况
 47 				eState = table[j];
 48 				break;
 49 			}
 50 		}
 51 	}
 52 }
 53 
 54 // jump points' method to slove the 0-1 bag's problem
 55 int GKnapSack()
 56 {
 57 	int i, k, j, boundaryL, boundaryR, next;//boundaryL, boundaryR作为指针,卡在p[i-1]的左右为边界,next是p[i]要填写的位置,初始时next为
 58 	//p[i]的首位置,k依次往后移动,直到到达p[i-1]的右边界
 59 	Data temp;
 60 
 61 	head[0] = 0;//p[0]的首位置为索引0
 62 	table[0] = Data(0, 0);//没有装物品时各项数据都为0
 63 	boundaryL = boundaryR = 0;
 64 	next = 1;//p[1]要填写的位置即索引1
 65 	head[1] = 1;//p[i]的首位置为1
 66 	for (i = 1; i <= n; ++i)
 67 	{
 68 		k = boundaryL;//数字k在处理中从p[i-1]的左边界移动到右边界
 69 		for (j = boundaryL; j <= boundaryR; ++j)
 70 		{
 71 			if (table[j].w + goods[i].w > c)
 72 				break;
 73 			/*先在 p(i-1)的元素 j 上得到一个新状态,然后 w 小于它的不受影响,直接搬*/
 74 			temp = table[j] + goods[i];//将p[i-1]对应的列表中的移动到的数据的容量和价值分别加上第i件物品的容量和价值
 75 			while (k <= boundaryR && table[k].w)
 76 			{//next是p[i]要填写的位置,初始时next为p[i]的首位置,依次往后移动,
 77 	         //直到到达p[i-1]的右边界
 78 				table[next] = table[k];
 79 			++next;
 80 			++k;
 81 			}
 82 			/*w 等于它的,对 v 取更大的值*/
 83 				if (k <= boundaryR && table[k].w == temp.w)
 84 				{
 85 					temp.v = max(temp.v, table[k].v);//如果容量相同,则取价值最大的
 86 					++k;
 87 				}
 88 			/*w 大于它的,根据 v 值直接pass 掉 不合法的点(w 大但 v 小于前边的)*/
 89 			if (temp.v > table[next - 1].v)
 90 			{
 91 				table[next] = temp;
 92 				++next;
 93 			}
 94 			while (k <= boundaryR && table[k].v <= table[next - 1].v)
 95 				++k;
 96 		}
 97 		while (k <= boundaryR)
 98 		{
 99 			table[next] = table[k];
100 			++next;
101 			++k;
102 		}
103 
104 		boundaryL = boundaryR + 1;//boundaryL指向p[i]的左边界,即下一个i对应的表的左边界,因为此时p[i]的表已经填好,要利用i标记来借助p[i]导出p[i+1]
105 		boundaryR = next - 1;//因为next指向p[i+1]的首位置的索引,所以boundaryR指向next-1,即得到p[i]的右边界
106 		head[i + 1] = next;
107 	}
108 
109 	traceBack(table[next - 1]);
110 
111 	return table[next - 1].v;
112 }
113 
114 int main()
115 {
116 	int i;
117 	cout << "请输入背包的总数量和总容量" << endl;
118 	cin >> n;
119 	cin >> c;
120 	cout << "请分别输入每个背包的重量和价值" << endl;
121 	for (i = 1; i <= n; ++i)
122 	{
123 		cin >> goods[i].w;
124 		cin >> goods[i].v;
125 	}
126 	cout << "结果最大价值是:" << GKnapSack() << endl;
127 	cout << "该实例的重量价值表:" << endl;
128 	for (i = 0; i <= n; ++i)
129 	{
130 		cout << i << ":";
131 		for (int j = head[i]; j < head[i + 1]; ++j)
132 		{
133 			cout << table[j].w << "," << table[j].v << ";";
134 		}
135 		cout << endl;
136 	}
137 
138 	cout << endl;
139 	cout << "装入的物品序号是:" << endl;
140 	while (!numlist.empty())
141 	{
142 		cout << numlist.top() << ",";
143 		numlist.pop();
144 	}
145 	cout << endl;
146 	return 0;
147 }

结果截图:

 参考文章:

https://zhuanlan.zhihu.com/p/30959069

  • 5
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值