这个问题有一个标准的动态规划解法:我们创建一个函数g: (当前重量,当前物品) -> 剩余最大价值来代替创建一个函数f: 当前重量 -> 剩余最大价值,然后遍历每个物品。通过这种方式,我们避免了g形成的隐式图中的循环,并且可以使用动态规划。C ++中此类函数的一个标准实现可以在这里看到:
#include<iostream>
#include<vector>
#include <chrono>
#define INF (int)1e8
using namespace std;
using namespace std::chrono;
const int MAX_WEIGHT = 10000;
const int N_ITEMS = 10;
int memo[N_ITEMS][MAX_WEIGHT + 1];
vector<int> weights;
vector<int> values;
void initializeMemo(){
for(int i = 0; i < N_ITEMS; i++)
for(int j = 0; j < MAX_WEIGHT + 1; j++)
memo[i][j] = -1;
}
// return max possible remaining value
int dpUnboundedKnapsack(int currentItem, int currentWeight){
if(currentItem == N_ITEMS)
return 0;
int& ans = memo[currentItem][currentWeight];
if(ans != -1)
return ans;
int n_taken = 0;
while(true){
int newWeight = currentWeight + n_taken * weights[currentItem];
if(newWeight > MAX_WEIGHT)
break;
int value = n_taken * values[currentItem];
ans = max(ans, dpUnboundedKnapsack(currentItem+1, newWeight) + value);
n_taken++;
}
return ans;
}
int main(){
initializeMemo();
// weights equal 1
weights = vector<int>(N_ITEMS, 1);
// values equal 1, 2, 3, ... N_ITEMS
values = vector<int>(N_ITEMS);
for(int i = 0; i < N_ITEMS; i++)
values[i] = i+1;
auto start = high_resolution_clock::now();
cout << dpUnboundedKnapsack(0, 0) << endl;
auto stop = high_resolution_clock::now();
auto duration = duration_cast<microseconds>(stop - start);
cout << duration.count() << endl;
return 0;
}
该解决方案遍历了一个DAG(我们从不返回任何项,因此这里没有循环,所有边的形式为(item,weight) ->(item + 1,new_weight))。 DAG的每个边缘最多只访问一次。因此,该算法的时间复杂度为O(E),其中E是图的边数。在最坏的情况下,每个权重都等于1,因此每个顶点(item,weigth)平均连接到其他W/2 个顶点。因此我们有O(E)= O(W·#顶点)= O(W·W·n)= O(W ^ 2·n)。问题在于,无论我在互联网上查找什么,都说该算法的运行时复杂度为O(W·n),因为每个顶点只计算一次[1] [2] [3]。这种解释似乎没有意义。而且,如果是这样的话,上面的算法不应该运行得那么慢。这是算法运行时间×MAX_WEIGHT值的表格(请自行尝试此操作,只需运行上面的代码):
MAX_WEIGHT time (microseconds)
100 1349
1000 45773
10000 5490555
20000 21249396
40000 80694646
我们可以清楚地看到,当W的值较大时,出现了一个O(W^2)的趋势,正如我们所猜测的。
最后,有人可能会问:这种最坏情况非常无聊,因为你只需要对每个重复的重量取最大值即可。确实,通过这种简单的预处理,最坏情况现在变成了重量为[1、2、3、4、......n]的情况。在这种情况下,大约有O(W^2·log(n) + W·n)条边(见下面的图片。我尽力了,希望你能理解)。因此,算法的运行时间复杂度应该是O(W^2·log(n) + W·n),而不是像几乎所有地方都建议的那样是O(W·n)?
顺便说一句,这里是我得到的weights = [1, 2, 3, 4, ..., n]的运行时间:
MAX_WEIGHT time (microseconds)
100 964
200 1340
400 2541
800 6878
1000 10407
10000 1202582
20000 5181070
40000 18761689
[1] 无界背包问题(允许重复选择)
[2] 背包问题-动态规划
[3] 为什么背包问题是伪多项式时间?
n
,W*n
占主导地位,而W^2*log(n)
则相对较小。 - ad absurdumn
大于W
的情况,因为我们最多只会取W
个不同的值。所以保留术语W^2*log(n)
是有益的。 - F. Bruno Dias