对于 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 }
结果截图:
参考文章: