Knapsack DP

Prerequisite knowledge: Introduction to Dynamic Programming.

Introduction

Before explaining what “knapsack DP” is, consider the following example:

「USACO07 DEC」Charm Bracelet

In summary: there are nn items and a knapsack of capacity WW. Each item has a weight wiw_{i} and a value viv_{i}. Select some items to put into the knapsack so that the total value is maximized while the total weight does not exceed the capacity.

In the example above, each object has only two possible states (selected or not selected), corresponding to 00 and 11 in binary. Such problems are called “0-1 knapsack problems”.

0-1 Knapsack

Explanation

The known conditions in the example are the weight wiw_{i} and value viv_{i} of item ii, as well as the total capacity WW of the knapsack.

Let the DP state fi,jf_{i,j} be the maximum total value attainable by a knapsack of capacity jj when only the first ii items may be placed in it.

Consider the transition. Suppose all states for the first i−1i-1 items have already been processed. For item ii, if it is not put into the knapsack, the remaining capacity and total value remain unchanged, so the maximum value in this case is fi−1,jf_{i-1,j}. If it is put into the knapsack, the remaining capacity decreases by wiw_{i} and the total value increases by viv_{i}, so the maximum value is fi−1,j−wi+vif_{i-1,j-w_{i}}+v_{i}.

Thus the state transition is

fi,j=max⁡(fi−1,j,fi−1,j−wi+vi)f_{i,j}=\max(f_{i-1,j},f_{i-1,j-w_{i}}+v_{i})

Recording the states directly in a two-dimensional array causes MLE. A rolling array can be used for optimization.

Only fif_i is affected by fi−1f_{i-1}, so the first dimension can be removed. Let fif_{i} directly denote the maximum value for capacity ii after processing the current item. We obtain

fj=max⁡(fj,fj−wi+vi)f_j=\max \left(f_j,f_{j-w_i}+v_i\right)

Be sure to remember and understand this transition, because the transitions for most knapsack problems are derived from it.

Implementation

One thing to note is that it is easy to write the following incorrect core code:

C++

for (int i = 1; i <= n; i++)
  for (int l = 0; l <= W - w[i]; l++)
    f[l + w[i]] = max(f[l] + v[i], f[l + w[i]]);
// Simplified from f[i][l + w[i]] = max(max(f[i - 1][l + w[i]], f[i - 1][l] + v[i]),
// f[i][l + w[i]])

Python

for i in range(1, n + 1):
    for l in range(0, W - w[i] + 1):
        f[l + w[i]] = max(f[l] + v[i], f[l + w[i]])
# Simplified from f[i][l + w[i]] = max(max(f[i - 1][l + w[i]], f[i - 1][l] + v[i]),
# f[i][l + w[i]])

What is wrong with this code? The enumeration order is wrong.

Observe carefully that for the currently processed item ii and current state fi,jf_{i,j}, when j⩾wij\geqslant w_{i}, fi,jf_{i,j} is affected by fi,j−wif_{i,j-w_{i}}. This is equivalent to putting item ii into the knapsack multiple times, which does not match the problem statement. (In fact, this is the solution to the unbounded knapsack problem.)

To avoid this, we can change the enumeration order to go from WW down to wiw_{i}. Then the above error does not occur, because fi,jf_{i,j} is always updated before fi,j−wif_{i,j-w_{i}}.

Thus the actual core code is

C++

for (int i = 1; i <= n; i++)
  for (int l = W; l >= w[i]; l--) f[l] = max(f[l], f[l - w[i]] + v[i]);

Python

for i in range(1, n + 1):
    for l in range(W, w[i] - 1, -1):
        f[l] = max(f[l], f[l - w[i]] + v[i])
Example code
#include <iostream>
using namespace std;
constexpr int MAXN = 13010;
int n, W, w[MAXN], v[MAXN], f[MAXN];

int main() {
  cin >> n >> W;
  for (int i = 1; i <= n; i++) cin >> w[i] >> v[i];  // Read the data
  for (int i = 1; i <= n; i++)
    for (int l = W; l >= w[i]; l--)
      if (f[l - w[i]] + v[i] > f[l]) f[l] = f[l - w[i]] + v[i];  // State transition
  cout << f[W];
  return 0;
}

Unbounded Knapsack

Explanation

The unbounded knapsack model is similar to 0-1 knapsack. The only difference is that an item may be selected infinitely many times rather than only once.

We can borrow the idea of 0-1 knapsack and define the state as follows: let fi,jf_{i,j} be the maximum value attainable by a knapsack of capacity jj when only the first ii items may be selected.

Note that although this definition is similar to that of 0-1 knapsack, its state transition is different.

Process

A naive approach is to enumerate how many copies of item ii are selected for each transition. This has time complexity O(n3)O(n^3).

The transition is

fi,j=max⁡k=0+∞(fi−1,j−k×wi+vi×k)f_{i,j}=\max_{k=0}^{+\infty}(f_{i-1,j-k\times w_i}+v_i\times k)

Consider a simple optimization. For fi,jf_{i,j}, it is enough to transition through fi,j−wif_{i,j-w_i}. Thus the transition becomes

fi,j=max⁡(fi−1,j,fi,j−wi+vi)f_{i,j}=\max(f_{i-1,j},f_{i,j-w_i}+v_i)

The reason is that when making this transition, fi,j−wif_{i,j-w_i} has already been updated from fi,j−2×wif_{i,j-2\times w_i}, so fi,j−wif_{i,j-w_i} is the optimal result after fully considering how many copies of item ii are selected. In other words, we reuse the previous enumeration through the optimal-substructure property and reduce the enumeration complexity.

As with 0-1 knapsack, we can remove the first dimension to optimize space complexity. Once the 0-1 knapsack optimization is understood, it is not difficult to see that the compressed loop runs forward (that is, it uses the incorrect optimization mentioned above).

「Luogu P1616」Crazy Potion

In summary: there are nn types of items and a knapsack of capacity WW. Each type has weight wiw_{i} and value viv_{i}. Select items so that the total value is maximized while the total weight does not exceed the capacity.

Example code
#include <iostream>
using namespace std;
constexpr int MAXN = 1e4 + 5;
constexpr int MAXW = 1e7 + 5;
int n, W, w[MAXN], v[MAXN];
long long f[MAXW];

int main() {
  cin >> W >> n;
  for (int i = 1; i <= n; i++) cin >> w[i] >> v[i];
  for (int i = 1; i <= n; i++)
    for (int l = w[i]; l <= W; l++)
      if (f[l - w[i]] + v[i] > f[l]) f[l] = f[l - w[i]] + v[i];  // Core transition
  cout << f[W];
  return 0;
}

Bounded Knapsack

Bounded knapsack is another variant of 0-1 knapsack. The difference is that there are kik_i copies of each type of item rather than one.

A very naive idea is to convert “select each type of item kik_i times” into “there are kik_i identical items, each selected once.” This converts it into a 0-1 knapsack model, which can be solved using the method above. The transition is

fi,j=max⁡k=0ki(fi−1,j−k×wi+vi×k)f_{i,j}=\max_{k=0}^{k_i}(f_{i-1,j-k\times w_i}+v_i\times k)

The time complexity is O(W∑i=1nki)O(W\sum_{i=1}^nk_i).

Core code
for (int i = 1; i <= n; i++) {
  for (int weight = W; weight >= w[i]; weight--) {
    // Iterate one more layer over the item count
    for (int k = 1; k * w[i] <= weight && k <= cnt[i]; k++) {
      dp[weight] = max(dp[weight], dp[weight - k * w[i]] + k * v[i]);
    }
  }
}

Binary Grouping Optimization

Consider an optimization. We still solve bounded knapsack by converting it into a 0-1 knapsack model.

Explanation

The O(nW)O(nW) part of the complexity clearly cannot be optimized further, so we can only work on the O(∑ki)O(\sum k_i) part. For convenience, let Ai,jA_{i,j} denote the jjth item obtained by splitting type ii.

In the naive approach, for every ∀j≤ki\forall j\le k_i, Ai,jA_{i,j} represents the same item. The main reason for the low efficiency is the large amount of repeated work. For example, we consider both “select Ai,1,Ai,2A_{i,1},A_{i,2} together” and “select Ai,2,Ai,3A_{i,2},A_{i,3} together,” which are completely equivalent. This repetition occurs many times. Thus, improving the splitting method is the key to solving the problem.

Process

We can make the splitting method more elegant through “binary grouping”.

Specifically, let Ai,j(j∈[0,⌊log⁡2(ki+1)⌋−1])A_{i,j}\left(j\in\left[0,\lfloor \log_2(k_i+1)\rfloor-1\right]\right) represent a large item formed by “bundling” 2j2^{j} individual items. If ki+1k_i+1 is not an integer power of 22, add one final large item formed by “bundling” ki−2⌊log⁡2(ki+1)⌋−1k_i-2^{\lfloor \log_2(k_i+1)\rfloor-1} individual items to make up the remainder.

For example:

  • 6=1+2+36=1+2+3
  • 8=1+2+4+18=1+2+4+1
  • 18=1+2+4+8+318=1+2+4+8+3
  • 31=1+2+4+8+1631=1+2+4+8+16

Clearly, this splitting method can represent every equivalent choice of at most ≤ki\le k_i items. After splitting each type of item in this way, apply the 0-1 knapsack method.

The time complexity is O(W∑i=1nlog⁡2ki)O(W\sum_{i=1}^n\log_2k_i).

Implementation

Binary grouping code

C++

index = 0;
for (int i = 1; i <= m; i++) {
  int c = 1, p, h, k;
  cin >> p >> h >> k;
  while (k > c) {
    k -= c;
    list[++index].w = c * p;
    list[index].v = c * h;
    c *= 2;
  }
  list[++index].w = p * k;
  list[index].v = h * k;
}

Python

index = 0
for i in range(1, m + 1):
    c = 1
    p, h, k = map(int, input().split())
    while k > c:
        k -= c
        index += 1
        list[index].w = c * p
        list[index].v = c * h
        c *= 2
    index += 1
    list[index].w = p * k
    list[index].v = h * k

Monotone Queue Optimization

See Monotone queue/monotone stack optimization.

Exercise: 「Luogu P1776」Treasure Selection_NOI Guide 2010 Advanced (02)

Mixed Knapsack

Mixed knapsack combines the three knapsack problems above: some items can be selected only once, some infinitely many times, and some at most kk times.

Such problems may look intimidating, but once the central ideas of the preceding knapsack types are understood and combined, they become manageable. The pseudocode is as follows:

for (each item type) {
  if (the item is 0-1 knapsack)
    apply the 0-1 knapsack transition;
  else if (the item is complete knapsack)
    apply the complete-knapsack transition;
  else if (the item is bounded knapsack)
    apply the bounded-knapsack transition;
}

Example Problem

「Luogu P1833」Cherry Blossom

There are nn types of cherry trees and TT units of time. Some cherry trees can be viewed only once, some at most AiA_{i} times, and some infinitely many times. Each tree has an aesthetic value CiC_{i}. Find which cherry trees to view within time TT to maximize the aesthetic value.

Core code
for (int i = 1; i <= n; i++) {
  if (cnt[i] == 0) {  // If the count is unlimited, use the core unbounded-knapsack code
    for (int weight = w[i]; weight <= W; weight++) {
      dp[weight] = max(dp[weight], dp[weight - w[i]] + v[i]);
    }
  } else {  // For limited items, use the core bounded-knapsack code; it also handles 0-1 knapsack
    for (int weight = W; weight >= w[i]; weight--) {
      for (int k = 1; k * w[i] <= weight && k <= cnt[i]; k++) {
        dp[weight] = max(dp[weight], dp[weight - k * w[i]] + k * v[i]);
      }
    }
  }
}

Exercise: HDU 5410 CRB and His Birthday

Knapsack with Two-Dimensional Costs

「Luogu P1855」Squeezing kkksc03

There are nn tasks to complete. Completing task ii takes tit_i minutes and costs cic_i yuan.

There are TT minutes and WW yuan available to handle these tasks. Find the maximum number of tasks that can be completed.

This is clearly a 0-1 knapsack problem, but selecting an item consumes two resources (money and time). We only need to add one dimension to the state to store the second resource.

At this point, adding another dimension for the item number is unsuitable because it can easily cause MLE.

Implementation

C++

for (int k = 1; k <= n; k++)
  for (int i = m; i >= mi; i--)    // Enumerate the budget layer
    for (int j = t; j >= ti; j--)  // Enumerate the time layer
      dp[i][j] = max(dp[i][j], dp[i - mi][j - ti] + 1);

Python

for k in range(1, n + 1):
    for i in range(m, mi - 1, -1):  # Enumerate the budget layer
        for j in range(t, ti - 1, -1):  # Enumerate the time layer
            dp[i][j] = max(dp[i][j], dp[i - mi][j - ti] + 1)

Group Knapsack

「Luogu P1757」Tongtian's Group Knapsack

There are nn items and a knapsack of size mm. The value of item ii is wiw_i and its volume is viv_i. Each item belongs to a group, and at most one item in each group may be selected. Find the maximum total value that can be loaded into the knapsack.

How should this problem be approached? It changes “select one item from all items” into “select one item from the current group,” so we can perform one 0-1 knapsack for each group.

As for storage, let tk,it_{k,i} denote the item number of the iith item in group kk, and let cntk\mathit{cnt}_k denote the number of items in group kk.

Implementation

C++

for (int k = 1; k <= ts; k++)          // Loop over each group
  for (int i = m; i >= 0; i--)         // Loop over knapsack capacities
    for (int j = 1; j <= cnt[k]; j++)  // Loop over every item in the group
      if (i >= w[t[k][j]])             // The knapsack has enough capacity
        dp[i] = max(dp[i],
                    dp[i - w[t[k][j]]] + c[t[k][j]]);  // Transition as in 0-1 knapsack

Python

for k in range(1, ts + 1):  # Loop over each group
    for i in range(m, -1, -1):  # Loop over knapsack capacities
        for j in range(1, cnt[k] + 1):  # Loop over every item in the group
            if i >= w[t[k][j]]:  # The knapsack has enough capacity
                dp[i] = max(
                    dp[i], dp[i - w[t[k][j]]] + c[t[k][j]]
                )  # Transition as in 0-1 knapsack

Be careful: the loop order must not be wrong, otherwise correctness cannot be guaranteed.

Knapsack with Dependencies

「Luogu P1064」Jinming's Budget Plan

Jinming has nn yuan and wants to buy mm items. Item ii costs viv_i yuan and has importance pip_i. Some items are attachments belonging to a main item; to buy such an item, its main item must also be bought.

Maximize the sum of vi×piv_i \times p_i over all purchased items.

Consider the cases. For a main item and several attachments, the possibilities are to buy only the main item or the main item together with some attachments. Since only one of these possibilities can be selected, this can be viewed as group knapsack.

If the items form a collection of multiway trees, first calculate the collection for the child nodes, and finally the collection for the parent node.

Knapsack of Generalized Items

In this knapsack variant, the cost and value are not fixed; its value depends on the cost allocated to it. In a knapsack of capacity VV, when the cost allocated to it is viv_i, the value obtained is h(vi)h\left(v_i\right). Thus, replace the fixed value with a function reference.

Miscellaneous

Small Optimizations

By the greedy principle, when costs are equal, keep only the item with the highest value; when values are fixed, keep only the item with the lowest cost; if there are two items i,ji,j such that item ii has a higher value than item jj and item ii has a lower cost than item jj, keep only ii.

Variants of Knapsack Problems

Outputting a Solution

Outputting a solution means recording how a certain state in the knapsack was derived. Let gi,vg_{i,v} indicate whether item ii is selected when it occupies space vv. During the transition, record which strategy was used (selected or not selected). The pseudocode for output is:

int v = V;  // Record the current storage space

// Since the last item stores the final state, loop from the last item
for (items from last to first) {
  if (g[i][v]) {
    select item i;
    v -= weight[i];
  } else {
    do not select item i;
  }
}

Counting the Number of Solutions

Given a knapsack capacity, item costs, and other relations, count the total number of ways to fill the knapsack to a certain capacity.

This problem is solved by replacing maximization with summation.

For example, the transition for 0-1 knapsack becomes

dpj←dpj+dpj−ci(j≥ci)\mathit{dp}_j \leftarrow \mathit{dp}_j + \mathit{dp}_{j-c_i} \qquad (j \ge c_i)

Initial condition: dp0=1\mathit{dp}_0=1.

This is because when the capacity is 00, there is still one solution: selecting nothing.

Counting Optimal Solutions

To count optimal solutions, slightly modify the definition of the dp\mathit{dp} array in 0-1 knapsack. Let the DP state fi,jf_{i,j} be the maximum total value attainable when the knapsack contains exactly volume jj and only the first ii items may be used.

After this modification, each DP state can be associated with a gi,jg_{i,j} representing the number of solutions.

fi,jf_{i,j} is the maximum value when considering only the first ii items and the knapsack volume is exactly jj.

gi,jg_{i,j} is the number of solutions when considering only the first ii items and the knapsack volume is exactly jj.

The transition is as follows:

If fi,j=fi−1,jf_{i,j} = f_{i-1,j} and fi,j≠fi−1,j−v+wf_{i,j} \neq f_{i-1,j-v}+w, not selecting the item is better, so the number of solutions comes from gi−1,jg_{i-1,j}.

If fi,j≠fi−1,jf_{i,j} \neq f_{i-1,j} and fi,j=fi−1,j−v+wf_{i,j} = f_{i-1,j-v}+w, selecting the item is better, so the number of solutions comes from gi−1,j−vg_{i-1,j-v}.

If fi,j=fi−1,jf_{i,j} = f_{i-1,j} and fi,j=fi−1,j−v+wf_{i,j} = f_{i-1,j-v}+w, both selecting and not selecting achieve an optimal solution, so the number of solutions comes from gi−1,jg_{i-1,j} and gi−1,j−vg_{i-1,j-v}.

Initial conditions:

memset(f, 0xcf, sizeof(f));
// Since this is a maximization, initialize to negative infinity to avoid transitions from unfilled states
// For minimization, initialize to positive infinity 0x3f
f[0] = 0;
g[0] = 1;  // Selecting nothing is one solution

Because the maximum knapsack volume may not be filled, the optimal solution is not necessarily fmf_{m}.

Finally, find the value of the optimal solution and add up all solution counts in the gjg_{j} array that attain this value.

Implementation
for (int i = 0; i < N; i++) {
  for (int j = V; j >= v[i]; j--) {
    int tmp = std::max(dp[j], dp[j - v[i]] + w[i]);
    int c = 0;
    if (tmp == dp[j]) c += cnt[j];                       // Transition from dp[j]
    if (tmp == dp[j - v[i]] + w[i]) c += cnt[j - v[i]];  // Transition from dp[j-v[i]]
    dp[j] = tmp;
    cnt[j] = c;
  }
}
int max = 0;  // Find the optimal solution
for (int i = 0; i <= V; i++) {
  max = std::max(max, dp[i]);
}
int res = 0;
for (int i = 0; i <= V; i++) {
  if (dp[i] == max) {
    res += cnt[i];  // Sum the number of optimal solutions
  }
}

The kth Best Knapsack Solution

Ordinary 0-1 knapsack asks for the optimal solution. By slightly modifying the ordinary knapsack DP and adding one dimension to record the first k solutions for the current state, we obtain an algorithm for the kkth best solution to 0-1 knapsack. Specifically, dpi,j,k\mathit{dp_{i,j,k}} records the kkth largest value sum obtainable from the first ii items when the selected items have total volume jj. This state can be understood as extending ordinary 0-1 knapsack's dpi,j\mathit{dp_{i,j}}, which records only one value, to an ordered sequence of optimal values. The ordinary transition is dpi,j=max⁡(dpi−1,j,dpi−1,j−vi+wi)\mathit{dp_{i,j}}=\max(\mathit{dp_{i-1,j}},\mathit{dp_{i-1,j-v_{i}}}+w_{i}). Now we merge the two decreasing sequences of length kk, dpi−1,j\mathit{dp_{i-1,j}} and dpi−1,j−vi+wi\mathit{dp_{i-1,j-v_{i}}}+w_{i}, retaining the first kk largest values in dpi,j\mathit{dp_{i,j}}. This uses a two-pointer method with complexity O(k)O(k), giving an overall time complexity of O(nmk)O(nmk). The first dimension can be compressed just as in ordinary knapsack, giving space complexity O(mk)O(mk).

Example Problem HDU 2639 Bone Collector II

Find the strict kkth best solution to 0-1 knapsack. n≤100,v≤1000,k≤30n \leq 100,v \leq 1000,k \leq 30

Implementation
memset(dp, 0, sizeof(dp));
int i, j, p, x, y, z;
scanf("%d%d%d", &n, &m, &K);
for (i = 0; i < n; i++) scanf("%d", &w[i]);
for (i = 0; i < n; i++) scanf("%d", &c[i]);
for (i = 0; i < n; i++) {
  for (j = m; j >= c[i]; j--) {
    for (p = 1; p <= K; p++) {
      a[p] = dp[j - c[i]][p] + w[i];
      b[p] = dp[j][p];
    }
    a[p] = b[p] = -1;
    x = y = z = 1;
    while (z <= K && (a[x] != -1 || b[y] != -1)) {
      if (a[x] > b[y])
        dp[j][z] = a[x++];
      else
        dp[j][z] = b[y++];
      if (dp[j][z] != dp[j][z - 1]) z++;
    }
  }
}
printf("%d\n", dp[m][K]);

References and Notes

This content comes from OI Wiki: Knapsack DP. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.