揹包問題(01揹包與完全揹包)

hsy2093發表於2024-05-31

dp考慮兩個方面,包括如何表示狀態(維度,屬性(min、max、cnt)),如何計算當前狀態(狀態轉移方程)。dp問題的最佳化一般是對狀態轉移方程進行等價變形。

01揹包問題

有n個物品和一個容量為V的揹包。

每個物品有兩個屬性,包括所佔用的體積v以及擁有的價值w,每件物品只能用一次。

求揹包能裝得下的情況下所能擁有的最大價值為多少。

f[i, j] = max(f[i-1, j], f[i, j-v] + w

滾動陣列最佳化

  • 能發現,f[i][j] 的狀態轉移僅使用到了 f[i-1][...],故可以採用滾動陣列來做。即當前層的狀態轉移僅與上一層有關
  • 當前層是 i & 1,上一層是 i-1 & 1

完全揹包問題

與01揹包的區別在於,每件物品可以拿無數次。

最佳化過程

f[i, j] = max(f[i-1, j], f[i-1, j-v]+w, f[i-1, j-2v]+2w, f[i-1, j-3v]+3w ...)
對比:
f[i, j-v] = max(f[i-1, j-v], f[i-1, j-2v]+w, f[i-1, j-3v]+2w ...)

也就是

f[i][j]    =max(f[i-1][j], f[i-1][j-v]+w, f[i-1][j-2v]+2w, f[i-1][j-3v]+3w,...)
f[i][j-v]  =max(           f[i-1][j-v], f[i-1][j-2v]+w, f[i-1][j-3v]+2w, f[i-1][j-4v]+3w,...)
f[i][j-v]+w=max(           f[i-1][j-v]+w, f[i-1][j-2v]+2w, f[i-1][j-3v]+3w, f[i-1][j-4v]+4w,...)

1)透過兩項對比,可以得出結論,可省掉一重迴圈

f[i, j] = max(f[i][j], f[i][j-v]+w)

所以實際上多重揹包的本質也還是01揹包

但是01揹包問題為從後往前面推導,完全揹包問題為從前往後推導

2)同時,對狀態儲存進行最佳化,還可以省略掉一維陣列

f[j] = max(f[j], f[j-v]+ w)

3)完全揹包問題實際上求得的是某一個陣列區間內的最大值,也可以視作滑動視窗最大值,對於該問題即可利用單調佇列進行最佳化

習題講解

1. 最長上升子序列

題目描述

這是一個簡單的動規板子題。

給出一個由 \(n(n\le 5000)\) 個不超過 \(10^6\) 的正整陣列成的序列。請輸出這個序列的最長上升子序列的長度。

最長上升子序列是指,從原序列中按順序取出一些數字排在一起,這些數字是逐漸增大的。

輸入格式

第一行,一個整數 \(n\),表示序列長度。

第二行有 \(n\) 個整數,表示這個序列。

輸出格式

一個整數表示答案。

樣例輸入 #1

6
1 2 4 1 3 4

樣例輸出 #1

4

提示

分別取出 \(1\)\(2\)\(3\)\(4\) 即可。

程式碼參考

//B3637
#include<stdio.h>
#include<iostream>
using namespace std;
int a[5005], f[5005];

int main(){
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++){
        cin >> a[i];
        f[i] = 1;
    }
    for(int i = 2; i <= n; i++){
        for(int j = 1; j <= i-1; j++){
            if(a[i] > a[j])     f[i] = max(f[i], f[j] + 1);
        }
    }
    int ans = 0;
    for(int i = 1; i <= n; i++){
        ans = max(ans, f[i]);
    }
    cout << ans << endl;
    return 0;
}

2. NASA的食物計劃

題目背景

NASA(美國航空航天局)因為太空梭的隔熱瓦等其他安全技術問題一直大傷腦筋,因此在各方壓力下終止了太空梭的歷史,但是此類事情會不會在以後發生,誰也無法保證。所以,在遇到這類航天問題時,也許只能讓航天員出倉維修。但是過多的維修會消耗航天員大量的能量,因此 NASA 便想設計一種食品方案,使體積和承重有限的條件下多裝載一些高卡路里的食物。

題目描述

太空梭的體積有限,當然如果載過重的物品,燃料會浪費很多錢,每件食品都有各自的體積、質量以及所含卡路里。在告訴你體積和質量的最大值的情況下,請輸出能達到的食品方案所含卡路里的最大值,當然每個食品只能使用一次。

輸入格式

第一行 \(2\) 個整數,分別代表體積最大值 \(h\) 和質量最大值 \(t\)

第二行 \(1\) 個整數代表食品總數 \(n\)

接下來 \(n\) 行每行 \(3\) 個數 體積 \(h_i\),質量 \(t_i\),所含卡路里 \(k_i\)

輸出格式

一個數,表示所能達到的最大卡路里(int 範圍內)

樣例 #1

樣例輸入 #1

320 350
4
160 40 120
80 110 240
220 70 310
40 400 220

樣例輸出 #1

550

提示

對於 \(100\%\) 的資料,\(h,t,h_i,t_i \le 400\)\(n \le 50\)\(k_i \le 500\)

程式碼參考

#include <stdio.h>
#include <iostream>
using namespace std;
int v[55], m[55], kolo[55];
//dp陣列
int f[55][405][405];

int main(){
    int v_max, m_max;
    int N;
    int t1, t2;
    cin >> v_max >> m_max;
    cin >> N;
    for(int i = 1; i <= N; i++)
        cin >> v[i] >> m[i] >> kolo[i];
    for(int i = 1; i <= N; i++){
        for(int j = 0; j <= v_max; j++)
            for(int k = 0; k <= m_max; k++){
                t1 = v[i];
                t2 = m[i];
                f[i][j][k] = f[i-1][j][k];
                if(j >= t1 && k >= t2)
                f[i][j][k] = max(f[i-1][j][k], f[i-1][j-t1][k-t2]+kolo[i]);
            }
    }
    cout << f[N][v_max][m_max];
    return 0;
}

3. [NOIP2006 普及組] 開心的金明

題目描述

金明今天很開心,家裡購置的新房就要領鑰匙了,新房裡有一間他自己專用的很寬敞的房間。更讓他高興的是,媽媽昨天對他說:“你的房間需要購買哪些物品,怎麼佈置,你說了算,只要不超過 \(N\) 元錢就行”。今天一早金明就開始做預算,但是他想買的東西太多了,肯定會超過媽媽限定的 \(N\) 元。於是,他把每件物品規定了一個重要度,分為 \(5\) 等:用整數 \(1-5\) 表示,第 \(5\) 等最重要。他還從因特網上查到了每件物品的價格(都是整數元)。他希望在不超過 \(N\) 元(可以等於 \(N\) 元)的前提下,使每件物品的價格與重要度的乘積的總和最大。

設第\(j\)件物品的價格為 \(v_j\),重要度為 \(w_j\),共選中了 \(k\) 件物品,編號依次為 \(j_1,j_2,…,j_k\),則所求的總和為:

\(v_{j_1} \times w_{j_1}+v_{j_2} \times w_{j_2} …+v_{j_k} \times w_{j_k}\)

請你幫助金明設計一個滿足要求的購物單。

輸入格式

第一行,為 \(2\) 個正整數,用一個空格隔開:\(n,m\)\(n<30000,m<25\))其中 \(n\) 表示總錢數,\(m\) 為希望購買物品的個數。

從第 \(2\) 行到第 \(m+1\) 行,第 \(j\) 行給出了編號為 \(j-1\) 的物品的基本資料,每行有 \(2\) 個非負整數 \(v,p\)(其中 \(v\) 表示該物品的價格 \((v \le 10000)\)\(p\) 表示該物品的重要度(\(1\le p\le5\))。

輸出格式

\(1\) 個正整數,為不超過總錢數的物品的價格與重要度乘積的總和的最大值(\(<100000000\))。

樣例 #1

樣例輸入 #1

1000 5
800 2
400 5
300 5
400 3
200 2

樣例輸出 #1

3900

提示

NOIP 2006 普及組 第二題

程式碼參考

//P2725
#include<stdio.h>
#include<iostream>
#include<string.h>
using namespace std;
//f[i][j]陣列代表當選到第i個物品,花了j元時,所能得到的價格與權重的乘積和最大值
int f[30][30005];
struct wp{
    int v, p;
}w[30];

int main(){
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= m; i++)     cin >> w[i].v >> w[i].p;
    for(int i = 1; i <= m; i++){
        for(int j = 1; j <= n; j++){
            if(j >= w[i].v)     f[i][j] = max(f[i-1][j], f[i-1][j-w[i].v] + w[i].v * w[i].p);
            else    f[i][j] = f[i-1][j];
        }
    }
    int ans = 0;
    for(int i = 1; i <= n; i++)
        ans = max(ans, f[m][i]);
    cout << ans << endl;
    return 0;
}

4. [USACO3.1] 郵票 Stamps

題目描述

給一組 \(n\) 枚郵票的面值集合和一個上限 \(k\) —— 表示信封上能夠貼 \(k\) 張郵票。請求出最大的正整數 \(m\),滿足 \(1\)\(m\) 的面值都可以用不超過 \(k\) 張郵票表示出來。

輸入格式

輸入的第一行是兩個整數,分別代表郵票上限 \(k\) 和郵票面值數 \(n\)

自第二行起,除最後一行外,每行有 \(15\) 個整數 \(a_i\) ,最後一行的整數個數不超過 \(15\),共有 \(n\) 個整數,第 \(i\) 個整數代表第 \(i\) 種郵票的面值 \(a_i\)

輸出格式

輸出一行一個整數代表 \(m\)。若 \(m\) 不存在請輸出 \(0\)

樣例 #1

樣例輸入 #1

5 2
1 3

樣例輸出 #1

13

提示

樣例輸入輸出 1 解釋

\(1\) 分和 \(3\) 分的郵票;你最多可以貼 \(5\) 張郵票。很容易貼出 \(1\)\(5\) 分的郵資(用 \(1\) 分郵票貼就行了),接下來的郵資也不難:

  • \(6 = 3 + 3\)
  • \(7 = 3 + 3 + 1\)
  • $8 = 3 + 3 + 1 + 1 $。
  • $9 = 3 + 3 + 3 $。
  • $10 = 3 + 3 + 3 + 1 $。
  • $11 = 3 + 3 + 3 + 1 + 1 $。
  • $12 = 3 + 3 + 3 + 3 $。
  • \(13 = 3 + 3 + 3 + 3 + 1\)

然而,使用 \(5\)\(1\) 分或者 \(3\) 分的郵票根本不可能貼出 \(14\) 分的郵資。因此,答案為 \(13\)

資料規模與約定

對於 \(100\%\) 的資料,保證 \(1 \leq k \leq 200\)\(1 \leq n \leq 50\)\(1 \leq a_i \leq 10^4\)

說明

題目翻譯來自 NOCOW。

程式碼參考

//P2725
//完全揹包
#include<stdio.h>
#include<iostream>
#include<string.h>
using namespace std;
int a[55];
//f[i]記錄面值為i時最少需要的郵票數
int f[2000005];

int main(){
    int n, k;
    cin >> k >> n;
    memset(f, 0x3f3f3f,sizeof(f));
    f[0] = 0;
    for(int i = 1; i <= n; i++)     cin >> a[i];
    for(int i = 1; i <= 2000000; i++){
        for(int j = 1; j <= n; j++){
            if(i >= a[j])     f[i] = min(f[i-a[j]]+1, f[i]);
        }
    }
    for(int i = 1; i <= 2000000; i++){
        if(f[i] > k){
            cout << i-1 << endl;
            return 0;
        }
    }
    return 0;
}

5.機器分配

題目描述

總公司擁有高效裝置 \(M\) 臺,準備分給下屬的 \(N\) 個分公司。各分公司若獲得這些裝置,可以為國家提供一定的盈利。問:如何分配這 \(M\) 臺裝置才能使國家得到的盈利最大?求出最大盈利值。其中 \(M \le 15\)\(N \le 10\)。分配原則:每個公司有權獲得任意數目的裝置,但總檯數不超過裝置數 \(M\)

輸入格式

第一行有兩個數,第一個數是分公司數 \(N\),第二個數是裝置臺數 \(M\)

接下來是一個 \(N \times M\) 的矩陣,表明了第 \(i\) 個公司分配 \(j\) 臺機器的盈利。

輸出格式

第一行為最大盈利值。

接下來 \(N\) 行為第 \(i\) 分公司分 \(x\) 臺。

P.S. 要求答案的字典序最小。

樣例 #1

樣例輸入 #1

3 3
30 40 50
20 30 50
20 25 30

樣例輸出 #1

70
1 1
2 1
3 1

程式碼參考

//P2066
#include<stdio.h>
#include<iostream>
#include<string.h>
using namespace std;
typedef long long ll;
ll a[15][20];
//f[i][j]代表取到第i家公司,用掉了j臺裝置時,能夠獲得的利益最大值
ll f[15][20];
//存答案
ll cnt[15][20][2];
ll cnt2[15];

int main(){
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            cin >> a[i][j];
        }
    }
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            for(int k = 0; k <= j; k++){
                if(f[i][j] <= f[i-1][j-k] + a[i][k]){
                    f[i][j] = f[i-1][j-k] + a[i][k];
                    cnt[i][j][0] = k;
                    cnt[i][j][1] = j-k;
                }
            }
        }
    }
    //輸出最大值
    ll ans = 0, t = 0;
    for(int i = 1; i <= m; i++){
        if(ans <= f[n][i]){
            t = i;
            ans = f[n][i];
        }
    }
    cout << ans << endl;
    //輸出每一項數量
    for(int i = n; i >= 1; i--){
        cnt2[i] = cnt[i][t][0];
        t = cnt[i][t][1];
    }
    for(int i = 1; i <= n; i++)     cout << i << " " << cnt2[i] << endl;
    return 0;
}

相關文章