1. 程式人生 > 實用技巧 >動態規劃_揹包問題

動態規劃_揹包問題

揹包問題:

  • 問題描述有\(n\)件物品, 每件物品的體積為\(V_i\),價值為\(W_i\), 有一個體積為\(V\)的揹包, 求總體積不大於\(V\)的所有物品總價值最大是多少

01揹包問題: 每件物品只能用一次

狀態表示: \(dp[i][j]\)

  • 集合:所有選法
  • 條件:僅從前\(i\)個物品中選擇,而且使得總體積不超過\(j\)
  • 屬性:\(dp[i][j]\), 最大價值

狀態計算: 集合的劃分

樸素做法:二維

void solve() {
    for(int i = 1; i <= N; i++) {
        for(int j = 0; j <= V; j++) {
            dp[i][j] = dp[i - 1][j];
            if(v[i] <= j) dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
        }
    }
    printf("%d\n", dp[N][V]);
}

優化版本:等價變形,每一層由於上一層有關

void solve() {
    for(int i = 1; i <= N; i++) 
        for(int j = V; j >= v[i]; j--) 
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
    printf("%d\n", dp[V]);
}

完全揹包:每件物品無使用次數的限制

樸素做法

void solve() {
    for(int i = 1; i <= n; i++) // 列舉所有用到物品
        for(int j = 0; j <= V; j++) // 列舉所有體積
            for(int k = 0; k * v[i] <= j; k++) // 列舉每件物品用到的次數
                dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v[i]] + w[i] * k);
    cout << dp[n][V];
}

優化-1

優化思路:

\(dp(i, j) = dp(i - 1, j - v_i \times k) + w_i \times k\)

\(dp(i, j)\)展開式:

\(dp(i, j) = max(dp(i - 1, j), dp(i - 1, j - v_i) + w_i, dp(i - 1,j - 2 \times v_i + 2 \times w_i,...)\)
\(dp(i,j - v_i) = max(dp(i - 1,j - v), dp(i - 1,j - 2 \times v_i + w_i, ...)\)

由以上兩式可得:

\(dp(i, j) = max(dp(i - 1, j), dp(i, j - v_i) + w_i)\)

  • \(dp(i, j) = dp(i - 1, j)\) 表示第i個物品不選
  • \(dp(i, j) = dp(i, j - v_i) + w_i)\): 表示第i個物品選若干個
void solve() {
    for(int i = 1; i <= n; i++) {
        for(int j = 0; j <= V; j++) {
            dp[i][j] = dp[i - 1][j];
            if(j >= v[i]) dp[i][j] = max(dp[i][j], dp[i][j - v[i]] + w[i]);
        }
    }
    cout << dp[n][V];
}

優化-2: 變成一維

void solve() {
    for(int i = 1; i <= n; i++) {
        for(int j = v[i]; j <= V; j++) 
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
    }
    cout << dp[V];
}

完全揹包與01揹包的區別:狀態方程的比較

  • 01揹包:\(dp(i,j) = max(dp(i - 1, j), dp(i - 1, j - v_i) + w_i)\)
  • 完全揹包: \(dp(i, j) = max(dp(i - 1, j), dp(i, j - v_i) + w_i)\)

多重揹包:每個物品的數量有限:僅有s[i]

樸素暴力做法: \(O(nms)\)

void solve() {
    for(int i = 1; i <= n; i++) 
        for(int j = 0; j <= V; j++) 
            for(int k = 0; k <= s[i] && k * v[i] <= j; k++)
                dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v[i]] + w[i] * k);
    cout << dp[n][V];
}

優化版本1:二進位制拆分優化: \(O(nmlogs)\)

  • 例如: 體積為\(v\)的物品有\(s\)個,將這些物品按照2的冪次方個物品打包成新物品,可將其轉化成01揹包問題。
  • 對每個物品的個數進行優化
  • 假設有1023個物品,用多少個數可以表示從0到1023之間任意一個數?
  • 將1023按照二進位制表示拆分成十個數(\(log1023 < 10\)),每個數表示其二進位制表示中的一位
void solve() {
    int cnt = 0;
    for(int i = 0; i < n; i++) {
        int a, b, c; scanf("%d%d%d", &a, &b, &c);
        int k = 1;
        while(k <= c) {
            cnt++;
            v[cnt] = k * a;
            w[cnt] = k * b;
            c -= k;
            k *= 2;
        }
        if(c) { // 2^k + c == v
            cnt++;
            v[cnt] = a * c;
            w[cnt] = b * c;
        }
    }
    
    for(int i = 1; i <= cnt; i++)
        for(int j = m; j >= v[i]; j--)
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
            
    printf("%d\n", dp[m]);
}

優化版本2:滑動視窗:\(O()\)

分組揹包

void solve() {
for(int i = 1; i <= n; i++) {
        scanf("%d", &s[i]);
        for(int j = 0; j < s[i]; j++)
            scanf("%d%d", &v[i][j], &w[i][j]);
}
for(int i = 1; i <= n; i++) 
    for(int j = m; j >= 0; j--)
        for(int k = 0; k < s[i]; k++)
            if(v[i][k] <= j)
                dp[j] = max(dp[j], dp[j - v[i][k]] + w[i][k]);
                
printf("%d\n", dp[m]);
}

1013. 機器分配

  • 每個公司當成一個物品組
  • 可以選擇用\(dfs\)
  • 可抽象成組合揹包問題

487. 金明的預算方案

二維費用的揹包問題+01揹包

  • 狀態表示:\(dp[i, j, k]\),從前\(i\)個物品中選,總體積不超過\(j\)、總重量不超過\(k\)的總價值最大值
  • 狀態計算:
    1.如果不選擇第\(i\)個物品,\(dp[i][j][k] = dp[i - 1][j][k]\)
    2.如果選擇第i個物品,\(dp[i][j][k] = max(dp[i][j][k], dp[i - 1][j - v[i]][k - w[i]] + b[i])\)
  • 解釋:當選擇了第\(i\)個物品,其總體積為\(j\),總重量為\(k\), 因此,去掉第\(i\)個物品,其總價值為\(dp[i - 1][j - v[i]][k - w[i]]\),再加上第\(i\)個物品的價值即為選擇第\(i\)個物品之後的總價值

1020. 潛水員

  • 題目中的要求氧氣和氮氣體積至少為多少,求所需要的氣缸重量最小值
  • 與常見揹包問題略有不同,通常的揹包問題要求體積不超過某一值
  • 狀態表示:氧氣體積至少為\(j\),氮氣體積至少為\(k\),氣缸重量的最小值
  • 初始化:\(dp[0][0] = 0\),其他狀態表示為正無窮
int x, y; scanf("%d%d", &x, &y);
int n; scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d%d%d", &o2[i], &n2[i], &v[i]);

memset(dp, 0x3f, sizeof dp);
dp[0][0] = 0;
for(int i = 1; i <= n; i++) {
    for(int j = x; j >= 0; j--) {
        for(int k = y; k >= 0; k--) {
            // 當j - o2[i] < 0 時,表示氧氣體積至少為j - o2[i],此時該狀態是合法的,但該狀態數為0,氮氣同理
            dp[j][k] = min(dp[j][k], dp[max(0, j - o2[i])][max(0, k - n2[i])] + v[i]);
        }
    }
}
printf("%d", dp[x][y]);

揹包求具體方案

  • 題目要求輸出字典序最小的方案,因此需要逆序求\(dp陣列\),正序求方案