c++ 问题的时间复杂度是O(n×W)吗?

xytpbqjk  于 2023-07-01  发布在  其他
关注(0)|答案(1)|浏览(171)

给定一个背包重量 W 和一组 n 个项目,每个项目都有一定的值 value_i 和重量 w_i,我们需要计算我们可以从总重量≤ W 的项目中获得的最大值。这与经典的背包问题不同,这里我们允许使用无限数量的项目示例(改编自[1])。
对于这个问题,有一个标准的解决方案,使用动态编程:而不是创建一个函数 f:CurrentWeight -> MaxRemainingValue,我们创建一个函数 g:(CurrentWeight,CurrentItem)-> MaxRemainingValue,迭代每个项目。这样我们就避免了由 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,因此每个顶点 (项,权重) 平均连接到其他 W/2 个顶点。所以我们有O(E)= O(W·#vertexes)= 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)
顺便说一句,这里是我为案例权重= [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][https://www.geeksforgeeks.org/unbounded-knapsack-repetition-items-allowed/](https://www.geeksforgeeks.org/unbounded-knapsack-repetition-items-allowed/)
[2] https://en.wikipedia.org/wiki/Knapsack_problem#Dynamic_programming_in-advance_algorithm
[3]为什么背包问题是伪多项式?

v8wbuo2f

v8wbuo2f1#

你是正确的,你的算法的运行时间确实是O(nW 2)。这里有一个简单的方法来看到这一点:有O(nW)个网格单元格需要填充,每个单元格需要O(W)的时间来处理,因为你最多可以复制任何一个项目的W个副本。
然而,有一种比你在这里提出的更快的方法来计算答案。具体地说,我们可以通过使用以下见解来消除代码中的内部while循环。假设你想只使用列表的前n个项目和W个可用容量来获得最大的可能值。有两种选择:

  • 如果第n件物品的重量超过W,您就不能拿它。最好的选择是从前n-1个项目中获得最大值,并具有最大权重W。
  • 否则,你有两个选择。你可以说“我对这个项目不感兴趣”,在这种情况下,你最大化前n-1个项目的价值,最大权重W。你也可以说“我至少想要其中的一个”,在这种情况下,如果物品的重量为w,那么你现在有W - w的剩余重量。但是,您还没有排除使用该项目的更多副本的可能性。所以你递归地计算你可以从最后n个项目中得到的最大值,使用权重W - w,加上项目的一个副本的值。

换句话说,而不是在每一点上问“我想要多少份?”,”你问“我把这个项目的副本拿完了吗,还是我还想要一个副本?”
这将表中每个元素所做的工作减少到O(1),这就是O(nW)运行时的来源。

相关问题