01 揹包

Phantasia1116發表於2024-04-22

題目描述

辰辰是個很有潛能、天資聰穎的孩子,他的夢想是稱為世界上最偉大的醫師。

為此,他想拜附近最有威望的醫師為師。醫師為了判斷他的資質,給他出了一個難題。

醫師把他帶到個到處都是草藥的山洞裡對他說:

“孩子,這個山洞裡有一些不同的草藥,採每一株都需要一些時間,每一株也有它自身的價值。

我會給你一段時間,在這段時間裡,你可以採到一些草藥。如果你是一個聰明的孩子,你應該可以讓採到的草藥的總價值最大。”

如果你是辰辰,你能完成這個任務嗎?

輸入描述

輸入的第一行有兩個整數𝑇(1≤𝑇≤1000)和𝑀(1≤𝑀≤100),𝑇代表總共能夠用來採藥的時間,𝑀代表山洞裡的草藥的數目。

接下來的M行每行包括兩個在1到100之間(包括1和100)的的整數,分別表示採摘某株草藥的時間和這株草藥的價值。

輸出描述

可能有多組測試資料,對於每組資料:

輸出只包括一行,這一行只包含一個整數,表示在規定的時間內,可以採到的草藥的最大總價值。

輸入樣例1

42 6
1 35
25 70
59 79
65 63
46 6
28 82
962 6
43 96
37 28
5 92
54 3
83 93
17 22
0 0

輸出樣例1

117
334

知識點標籤:揹包,搜尋,記憶化,剪枝

題解

這應該是實際上手的第一道 01DP 吧,算是人生第一道 dp 了,對一個演算法苦守來說還是很困難的,其實很容易看出來,這就是一道十分簡單的 dp,如何劃分最優子問題,如何定義最優子問題,這些都是很重要的。

對一個通常的 01 揹包問題,一定會出現選或者不選的情況,如果是這樣就會出現一顆二叉選擇樹,每一個節點都在選擇或者不選擇,這會導致複雜度以指數級別升高,自然十分不明智。

對於一個重疊子問題(最優子問題),這道題而言,我們需要最終求得一個 m 個物品的情況下,價值為 n 的結果,如果定義 dp 陣列恰好能夠模擬這兩個引數的情況自然是最好的,那如果對於一個 u 個輸入的陣列而言,** \(t_i\) 是採集草藥所花費的時間,而 \(v_i\) 是第 i 個草藥的價值 **,dp 陣列的大小當然最好是大於 u 個輸入的,因為總會存在一個可能性會把所有的草藥都採摘,所以 dp 陣列的兩個引數顯而易見和輸入有關,那到底該如何定義 dp 陣列的含義呢?

我想,如果需要同時滿足我們上述的限制條件和最優子問題,那至少要求一個 dp[\(i\)][\(j\)] 在 \(i_{1}> i_2\) 並且 \(j_{1}> j_2\) 的情況下前者應當包括後者的結果,那麼 ij 至少是和選擇有關的,這麼說起來似乎可以定義成,前 i 個物品在 j 的時間內獲得的最大價值,那豈不是美哉?

在這個情況下似乎是可以實現某種可能性的轉移的。
image.png

那麼如何確定轉移又成了現在的難題,對當前的 dp[i][j]來說,i 是前 i 個物品,但並不能溯源前 i-1 個物品,所以也就是這第 i 個物品選或者不選,就成了從 i-1 個物品轉移到第 i 個物品的解決方案。

這個轉移是很好想的,對第 i 個物品,要麼選了,要麼沒選,

  • 如果沒選,那麼 dp[i][j]=dp[i-1][j]=dp[i-1][j-0]+0 (j 不需要減少,揹包的空沒有減少)
  • 如果選了,那麼 dp[i][j]=dp[i-1][j- \(t_i\)] + \(v_i\) ,也就是說原先的狀態是 dp[i-1][j- \(t_i\)] 因為啊因為啊到 i 的時候時間是 j,那麼 i-1 的時間其實是從 i 這裡回退的才對,只是這種回退可以寫成具體的表示式,又變成了正向的前進,後面加上的 \(v_i\) 自然是很好理解,加上了選擇 i 的價值而已。
    image.png

轉移的事情考慮的差不多了,應當考慮如果寫具體的實現程式碼應當如何初始化了,那自然 dp[0][0~j]都是應當初始化為 0 的。

M 是總數量,T 是所有事件的總時間。

最終要的答案是 dp[M][T],求解的過程中需要用到前面的子問題,所以應當是從 dp[i][j] 中開始,i=0 的時候用作初始化,但是 j=0 的時候是需要進行遍歷的,我們並不好排除有些情況下不消耗時間也能得到一定的價值,所以就這樣,本題完結。

如果這樣的話可以寫出一些程式碼了

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll T, M;
ll dp[110][1010];
ll t[110], v[110];

void solve() {
  for (int i = 0; i <= T; ++i) {
    dp[0][i] = 0;
  }
  for (int i = 1; i <= M; ++i) {
    cin >> t[i] >> v[i];
  }
  for (int i = 1; i <= M; ++i) {
    for (int j = 0; j <= T; ++j) {
    	
      // 加上其他判斷,首先得時間夠
      // 也就是說如果要選第i個物品,那前提是保證i-1的時候有 j-t[i]
      // 是正數,這樣可以確定時間是足夠的才會選擇第i個物品
      if (j >= t[i])
        dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - t[i]] + v[i]);
      else
        dp[i][j] = dp[i - 1][j];
    }
  }
  cout << dp[M][T] << '\n';
}

int main() {
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  while (cin >> T >> M) {
    if (T == 0 && M == 0) break;
    solve();
  }
  return 0;
}

相關文章