Chương 1·Bài 1.3.3

Knapsack

Có n≤100n \le 100 đồ vật; vật ii nặng wiw_i và có giá trị viv_i. Chọn một số vật có tổng khối lượng không quá sức chứa W≤105W \le 10^5 của chiếc ba lô sao cho tổng giá trị lớn nhất. Đây là bài ba lô (knapsack), bài toán gốc của cả một họ bài quy hoạch động.

Thử mọi tập con của nn vật mất 2n2^n lần, quá nhiều khi n=100n = 100. Nhưng với bài này, một tình huống con chỉ cần hai số: ta đã xét ii vật đầu và đang dùng hết nhiều nhất cc đơn vị sức chứa. Chỉ có n⋅W=107n \cdot W = 10^7 tình huống như vậy, mỗi tình huống tính bằng một phép so sánh.

IBa lô 0/1

Khi mỗi vật chỉ được lấy tối đa một lần, ta có bài ba lô 0/1: chọn một tập vật có tổng khối lượng không quá WW, tổng giá trị lớn nhất. Mỗi vật có hai khả năng, lấy hoặc không lấy, và có lấy được hay không còn tùy sức chứa còn lại. Vậy trạng thái cần hai tham số: dpi,cdp_{i,c} là tổng giá trị lớn nhất khi chỉ xét ii vật đầu và tổng khối lượng không quá cc.

Xét vật ii. Không lấy nó thì còn lại dpi−1,cdp_{i-1,c}. Lấy nó (nếu wi≤cw_i \le c) thì được thêm viv_i và phần còn lại là i−1i - 1 vật đầu với sức chứa c−wic - w_i:

dpi,c=max⁡(dpi−1,c, dpi−1,c−wi+vi),dp0,c=0.dp_{i,c} = \max\bigl(dp_{i-1,c},\ dp_{i-1,c-w_i} + v_i\bigr), \qquad dp_{0,c} = 0.

Đáp án là dpn,Wdp_{n,W}. Bảng có (n+1)(W+1)(n + 1)(W + 1) ô, mỗi ô tính trong O(1)O(1), tổng cộng O(nW)O(nW).

Mỗi ô lấy từ ô ngay trên (bỏ vật) hoặc ô lùi w cột ở hàng trên (lấy vật); bước cuối là truy ngược.

Hình dùng bốn vật (w,v)=(3,4),(2,3),(4,6),(1,1)(w, v) = (3, 4), (2, 3), (4, 6), (1, 1) và W=6W = 6. Ô cuối dp4,6=dp3,6dp_{4,6} = dp_{3,6} vì lấy thêm vật 4 chỉ được dp3,5+1=8dp_{3,5} + 1 = 8, kém 99. Ô dp3,6=dp2,2+6=9dp_{3,6} = dp_{2,2} + 6 = 9 (lấy vật 3) và dp2,2=dp1,0+3dp_{2,2} = dp_{1,0} + 3 (lấy vật 2), nên tập tốt nhất là vật 2 và vật 3, nặng 66 và có giá trị 99.

Điều kiện “không quá cc” ứng với khởi tạo dp0,c=0dp_{0,c} = 0 cho mọi cc. Nếu đề đòi khối lượng đúng bằng cc, chỉ dp0,0=0dp_{0,0} = 0 còn các ô khác khởi tạo −∞-\infty (hoặc +∞+\infty khi tìm giá trị nhỏ nhất).

Hàng ii chỉ dùng hàng i−1i - 1, nên ta giữ một mảng dp[c] và sửa tại chỗ: dp[c] = max(dp[c], dp[c - w] + v). Công thức cần dp[c - w] của hàng cũ, tức là khi chưa xét vật ii. Vì c−w<cc - w < c, nếu duyệt cc giảm dần thì ô c−wc - w chưa bị sửa trong lượt này. Nếu duyệt tăng dần, dp[c - w] đã chứa vật ii và vật sẽ bị lấy lại nhiều lần, tức là ta đã giải một bài khác (ba lô không giới hạn, ở mục sau). Với một vật w=2w = 2, v=3v = 3 và W=4W = 4: duyệt giảm cho dp=(0,0,3,3,3)dp = (0, 0, 3, 3, 3), duyệt tăng cho (0,0,3,3,6)(0, 0, 3, 3, 6) vì vật được lấy hai lần.

Hỏi nhanh

Ta sửa vòng lặp ba lô thành duyệt cc tăng dần, rồi chạy trên đúng một vật có w=3w = 3, v=5v = 5 và sức chứa W=7W = 7. Bấm Chạy để xem chương trình in gì:

C++
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll w, v, W, dp[105];

signed main() {
    cin >> w >> v >> W;
    for (ll c = w; c <= W; c++) dp[c] = max(dp[c], dp[c - w] + v);
    cout << dp[W] << '\n';
}
3 5 7

Với ba lô 0/1 thì đáp án đúng là 55. Vì sao chương trình in 1010?

Ví dụTủ Sách Trong Ngân Sách

Đề. Cửa hàng có n≤1000n \le 1000 quyển sách khác nhau; quyển ii giá hih_i và có sis_i trang (hi,si≤1000h_i, s_i \le 1000). Mỗi quyển mua tối đa một lần. Chọn các quyển sao cho tổng giá không quá x≤105x \le 10^5 và tổng số trang lớn nhất.

Lời giải. Đây đúng là ba lô 0/1 với khối lượng là giá sách và giá trị là số trang. Dùng một mảng, duyệt ngân sách từ xx xuống hih_i.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1e5 + 5;
ll n, x, h[1005], s[1005], dp[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> x;
    for (ll i = 1; i <= n; i++) cin >> h[i];
    for (ll i = 1; i <= n; i++) cin >> s[i];
    for (ll i = 1; i <= n; i++)
        for (ll c = x; c >= h[i]; c--) dp[c] = max(dp[c], dp[c - h[i]] + s[i]);
    cout << dp[x] << '\n';
}
5 13
6 4 7 3 9
8 5 10 4 13

Độ phức tạp: O(nx)O(nx), với n=1000n = 1000 và x=105x = 10^5 cần khoảng 10810^8 phép tính.

IIBa lô không giới hạn: đổi tiền

Khi mỗi vật được lấy bao nhiêu lần cũng được, ta có ba lô không giới hạn. Đổi tiền là ví dụ quen thuộc nhất: có các mệnh giá c1,…,ckc_1, \dots, c_k, mỗi mệnh giá dùng bao nhiêu đồng cũng được. Tạo tổng đúng bằng xx bằng ít đồng nhất.

Cách nghĩ đầu tiên là tham lam (bài 0.5): luôn lấy đồng lớn nhất không vượt phần còn lại. Với các mệnh giá {1,3,4}\{1, 3, 4\} và x=6x = 6, tham lam lấy 4+1+14 + 1 + 1 (33 đồng), trong khi 3+33 + 3 chỉ cần 22 đồng. Lấy đồng lớn trước có thể phá hỏng phần còn lại, nên ta xét mọi khả năng một cách có hệ thống.

Gọi dpsdp_s là số đồng ít nhất để tạo tổng đúng ss, bằng +∞+\infty nếu không tạo được. Trong một phương án tối ưu cho ss, lấy ra một đồng bất kỳ, mệnh giá cc; phần còn lại tạo tổng s−cs - c và phải dùng ít đồng nhất có thể. Ta chưa biết cc là mệnh giá nào nên thử tất cả:

dp0=0,dps=1+min⁡ci≤sdps−ci.dp_0 = 0, \qquad dp_s = 1 + \min_{c_i \le s} dp_{s - c_i}.

Số hạng bằng +∞+\infty thì bỏ qua. Duyệt ss tăng dần vì dpsdp_s phụ thuộc các ô nhỏ hơn. Với {1,3,4}\{1, 3, 4\} ta có dp0..6=(0,1,2,1,1,2,2)dp_{0..6} = (0, 1, 2, 1, 1, 2, 2); riêng dp6=1+min⁡(dp5,dp3,dp2)=2dp_6 = 1 + \min(dp_5, dp_3, dp_2) = 2.

Ví dụÍt Đồng Xu Nhất

Đề. Có n≤100n \le 100 mệnh giá đồng xu đôi một khác nhau, mỗi mệnh giá dùng tùy ý. Tạo tổng đúng bằng x≤106x \le 10^6 với ít đồng nhất; in −1-1 nếu không tạo được.

Lời giải. Dùng công thức trên với dpsdp_s khởi tạo 101810^{18} cho s>0s > 0; tổng không tạo được vẫn giữ giá trị rất lớn, nên cộng thêm 11 không làm nó nhỏ đi hay tràn số. Cuối cùng, nếu dpxdp_x còn là giá trị rất lớn thì in −1-1.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1e6 + 5;
ll n, x, c[105], dp[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> x;
    for (ll i = 1; i <= n; i++) cin >> c[i];
    for (ll s = 1; s <= x; s++) {
        dp[s] = 1e18;
        for (ll i = 1; i <= n; i++)
            if (c[i] <= s) dp[s] = min(dp[s], dp[s - c[i]] + 1);
    }
    if (dp[x] >= 1e18) cout << -1 << '\n';
    else cout << dp[x] << '\n';
}
3 10
4 6 7

Độ phức tạp: O(nx)O(nx), với n=100n = 100 và x=106x = 10^6 cần khoảng 10810^8 phép tính.

Hỏi nhanh

Một bạn quên gán +∞+\infty cho các ô dpsdp_s (s>0s > 0) trong chương trình đổi tiền, nên chúng giữ giá trị 00. Bấm Chạy để xem với một mệnh giá duy nhất bằng 22 và x=3x = 3:

C++
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll n, x, c[10], dp[100];

signed main() {
    cin >> n >> x;
    for (ll i = 1; i <= n; i++) cin >> c[i];
    for (ll s = 1; s <= x; s++)
        for (ll i = 1; i <= n; i++)
            if (c[i] <= s) dp[s] = min(dp[s], dp[s - c[i]] + 1);
    cout << dp[x] << '\n';
}
1 3
2

Tổng 33 không thể tạo bằng đồng 22, đáp án phải là −1-1, nhưng chương trình in 00. Vì sao?

Đổi câu hỏi thành đếm số cách thì trước hết phải nói rõ hai cách khác nhau khi nào. Hai cách hiểu cho hai bài toán, và hai đoạn code chỉ khác nhau ở thứ tự của hai vòng for.

Nếu 1+21 + 2 và 2+12 + 1 là hai cách (dãy có thứ tự), mỗi dãy có đúng một phần tử cuối cc, bỏ nó đi còn một dãy tạo tổng s−cs - c. Vậy dps=∑ci≤sdps−cidp_s = \sum_{c_i \le s} dp_{s - c_i} (tổng các dps−cidp_{s-c_i} qua mọi mệnh giá ci≤sc_i \le s), với dp0=1dp_0 = 1 (dãy rỗng). Tổng ss nằm ở vòng ngoài, mệnh giá ở vòng trong.

Nếu chỉ quan tâm mỗi mệnh giá dùng bao nhiêu đồng (tập mệnh giá), ta xử lý lần lượt từng mệnh giá để mỗi tập chỉ được sinh một lần. Gọi dpk,sdp_{k,s} là số cách tạo ss chỉ dùng kk mệnh giá đầu. Hoặc không dùng mệnh giá ckc_k, còn dpk−1,sdp_{k-1,s} cách; hoặc dùng ít nhất một đồng ckc_k, bỏ một đồng ấy đi thì còn dpk,s−ckdp_{k,s-c_k} cách:

dpk,s=dpk−1,s+dpk,s−ck.dp_{k,s} = dp_{k-1,s} + dp_{k,s-c_k}.

Nén thành một mảng, mệnh giá ở vòng ngoài và ss tăng dần ở vòng trong: dp[s] += dp[s - c]. Duyệt tăng chính là để dp[s - c] đã chứa các cách dùng cc. Chương trình dưới in số dãy có thứ tự (f) rồi số tập mệnh giá (g).

C++
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll MOD = 1e9 + 7, N = 1e6 + 5;
ll n, x, c[105], f[N], g[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> x;
    for (ll i = 0; i < n; i++) cin >> c[i];
    f[0] = g[0] = 1;
    for (ll s = 1; s <= x; s++)
        for (ll i = 0; i < n; i++)
            if (c[i] <= s) f[s] = (f[s] + f[s - c[i]]) % MOD;
    for (ll i = 0; i < n; i++)
        for (ll s = c[i]; s <= x; s++) g[s] = (g[s] + g[s - c[i]]) % MOD;
    cout << f[x] << '\n' << g[x] << '\n';
}
2 5
1 2

Độ phức tạp: O(nx)O(nx) cho mỗi cách đếm, với n=100n = 100 và x=106x = 10^6 cần khoảng 2⋅1082 \cdot 10^8 phép tính.

Với x=5x = 5 và hai mệnh giá 1,21, 2: có 33 tập (2+2+12+2+1, 2+1+1+12+1+1+1, năm đồng 11) nhưng 88 dãy có thứ tự, vì ba tập đó cho lần lượt 33, 44 và 11 cách xếp. Đổi chỗ hai vòng for là đổi sang bài toán khác, nên phải đọc kỹ đề xem “cách” có phân biệt thứ tự không.

Hỏi nhanh

Mệnh giá {1,2}\{1, 2\}, x=3x = 3, dp[0] = 1. Chương trình có vòng ngoài duyệt ss từ 11 tới xx, vòng trong duyệt từng mệnh giá c≤sc \le s và cộng dp[s] += dp[s - c]. Cuối cùng dp[3] bằng bao nhiêu và nó đếm gì?

IIITổng con: những tổng nào tạo được

Có nn thẻ, thẻ ii mang số nguyên dương xix_i, mỗi thẻ chọn tối đa một lần. Tổng nào của một tập thẻ có thể đạt được? Đây là ba lô 0/1 nhưng không cần giá trị: chỉ hỏi đúng hay sai. Gọi oksok_s là “có tập thẻ có tổng đúng ss hay không”, ok0ok_0 đúng. Khi thêm thẻ xx, tổng ss tạo được nếu trước đó đã tạo được ss, hoặc đã tạo được s−xs - x:

oks←oks hoặc oks−x.ok_s \leftarrow ok_s \ \text{hoặc}\ ok_{s-x}.

Giống ba lô 0/1, ta duyệt ss từ lớn xuống nhỏ để mỗi thẻ chỉ được dùng một lần.

Ví dụDanh Sách Tổng Khả Dụng

Đề. Có n≤100n \le 100 thẻ, thẻ ii mang số nguyên dương xi≤1000x_i \le 1000. In tất cả các tổng dương khác nhau của các tập con không rỗng, theo thứ tự tăng dần: dòng đầu là số lượng tổng, dòng sau là các tổng.

Lời giải. Tổng lớn nhất là tổng mọi thẻ, không quá 10510^5. Khi thêm thẻ xx, ta cộng xx vào tổng hiện có SS rồi duyệt ss từ SS xuống xx.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1e5 + 5;
ll n, S, ok[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n;
    ok[0] = 1;
    for (ll i = 1; i <= n; i++) {
        ll x;
        cin >> x;
        S += x;
        for (ll s = S; s >= x; s--)
            if (ok[s - x]) ok[s] = 1;
    }
    ll cnt = 0;
    for (ll s = 1; s <= S; s++) cnt += ok[s];
    cout << cnt << '\n';
    for (ll s = 1; s <= S; s++)
        if (ok[s]) cout << s << ' ';
    cout << '\n';
}
4
3 5 6 8

Độ phức tạp: O(nS)O(n S), với n=100n = 100 và S=105S = 10^5 cần khoảng 10710^7 phép tính.

Cũng theo cách này, ta đếm được số cách chia một nhóm số thành hai nhóm có tổng bằng nhau: thay phép “hoặc” bằng phép cộng số cách, và đích là nửa tổng.

IVBa lô khi khối lượng quá lớn

Nếu sức chứa WW tới 10910^9 thì bảng n×Wn \times W không thể lập. Khi đó, nếu tổng giá trị S=v1+⋯+vnS = v_1 + \cdots + v_n nhỏ, ta đổi vai: trạng thái là giá trị, và đại lượng cần tối ưu là khối lượng. Gọi dpxdp_x là tổng khối lượng nhỏ nhất của một tập vật có tổng giá trị đúng xx, với dp0=0dp_0 = 0 và các ô khác +∞+\infty. Mỗi vật (w,v)(w, v) sửa mảng với xx giảm dần: dpx←min⁡(dpx,dpx−v+w)dp_x \leftarrow \min(dp_x, dp_{x-v} + w). Đáp án là xx lớn nhất có dpx≤Wdp_x \le W.

Ví dụBa Lô Theo Tổng Giá Trị

Đề. Có N≤100N \le 100 vật, vật ii nặng WiW_i và có giá trị Vi≤1000V_i \le 1000; ba lô chịu tổng khối lượng C≤109C \le 10^9. Chọn một tập vật, mỗi vật tối đa một lần, có tổng khối lượng không quá CC để tổng giá trị lớn nhất.

Lời giải. Bảng theo khối lượng cần 10910^9 ô, không khả thi, nhưng tổng mọi giá trị S≤105S \le 10^5 nhỏ. Dùng dpxdp_x như trên. Giữ khối lượng nhỏ nhất cho mỗi giá trị là đủ, vì ràng buộc chỉ là tổng khối lượng không quá CC: tập nhẹ hơn cùng giá trị luôn thay được tập nặng hơn. Khối lượng cộng lại tới 101110^{11} nên dùng ll, vô cực đặt 101810^{18} để cộng thêm một khối lượng vẫn không tràn. Với ví dụ của đề, vật 1 và 2 cho giá trị 1313, nặng đúng 1010; mọi giá trị lớn hơn đều cần từ 1111 trở lên.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll n, C, w[105], v[105], dp[100005];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> C;
    ll S = 0;
    for (ll i = 0; i < n; i++) {
        cin >> w[i] >> v[i];
        S += v[i];
    }
    for (ll x = 1; x <= S; x++) dp[x] = 1e18;
    for (ll i = 0; i < n; i++)
        for (ll x = S; x >= v[i]; x--) dp[x] = min(dp[x], dp[x - v[i]] + w[i]);
    ll ans = 0;
    for (ll x = 0; x <= S; x++)
        if (dp[x] <= C) ans = x;
    cout << ans << '\n';
}
4 10
6 8
4 5
5 7
3 4

Độ phức tạp: O(N⋅S)O(N \cdot S) với S≤105S \le 10^5, cần khoảng 10710^7 phép tính; bộ nhớ O(S)O(S).

VTruy vết phương án

Cho đến giờ ta chỉ in giá trị tốt nhất. Muốn in cả tập vật được chọn, ta phải giữ cả bảng hai chiều dpi,cdp_{i,c}, vì mảng một chiều đã ghi đè mất thông tin của các hàng trước. Từ ô (n,W)(n, W) đi ngược: nếu dpi,c=dpi−1,cdp_{i,c} = dp_{i-1,c} thì vật ii không cần lấy; ngược lại, vật ii được lấy, ta ghi lại nó rồi lùi sức chứa cc đi wiw_i. Danh sách ghi được theo thứ tự ngược nên in từ cuối về đầu.

C++
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll n, W, w[105], v[105], dp[105][1005], pick[105];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> W;
    for (ll i = 1; i <= n; i++) cin >> w[i] >> v[i];
    for (ll i = 1; i <= n; i++)
        for (ll c = 0; c <= W; c++) {
            dp[i][c] = dp[i - 1][c];
            if (c >= w[i]) dp[i][c] = max(dp[i][c], dp[i - 1][c - w[i]] + v[i]);
        }
    ll cnt = 0, c = W;
    for (ll i = n; i >= 1; i--)
        if (dp[i][c] != dp[i - 1][c]) {
            pick[cnt++] = i;
            c -= w[i];
        }
    cout << dp[n][W] << '\n';
    for (ll i = cnt - 1; i >= 0; i--) cout << pick[i] << ' ';
    cout << '\n';
}
4 6
3 4
2 3
4 6
1 1

Độ phức tạp: O(nW)O(nW) thời gian và bộ nhớ, với n=100n = 100 và W=1000W = 1000 cần khoảng 10510^5 phép tính.

Trên ví dụ này chương trình in vật 2 và vật 3, đúng như đường truy ngược trong hình ở đầu trang. Nếu bảng quá lớn để giữ, ta chỉ lưu thêm một bit “có lấy vật ii ở sức chứa cc hay không”, nhưng vẫn là O(nW)O(nW) bit.

Bài nâng cao

Xem 1 bài nâng cao

Ví dụTháp Khối Chịu Lực

Đề. Có N≤1000N \le 1000 khối; khối ii có trọng lượng WiW_i, sức chịu tải SiS_i và giá trị ViV_i (Wi,Si≤104W_i, S_i \le 10^4, Vi≤109V_i \le 10^9). Chọn một số khối rồi xếp thành tháp theo thứ tự tùy ý, sao cho với mỗi khối, tổng trọng lượng của các khối nằm trên nó không vượt SiS_i. Tối đa hóa tổng giá trị.

Lời giải. Nếu đã biết thứ tự xếp, bài trở thành ba lô: duyệt các khối từ trên xuống dưới, nhớ tổng trọng lượng jj của các khối đã xếp ở trên, và khối mới chỉ đặt xuống dưới được khi j≤Sij \le S_i; khi đó tổng trọng lượng thành j+Wij + W_i. Còn phải chọn thứ tự. Ta sẽ xếp các khối theo Si+WiS_i + W_i tăng dần từ trên xuống dưới (cách sắp theo một khóa như bài 0.5).

Chứng minh

Giả sử một tháp hợp lệ có hai khối kề nhau, aa nằm trên bb, với Sa+Wa>Sb+WbS_a + W_a > S_b + W_b, và LL là tổng trọng lượng các khối phía trên aa. Hợp lệ nghĩa là L≤SaL \le S_a và L+Wa≤SbL + W_a \le S_b. Đổi chỗ để bb nằm trên aa. Khối bb chịu tải L≤L+Wa≤SbL \le L + W_a \le S_b. Khối aa chịu tải L+WbL + W_b, và ta có Sa>Sb+Wb−Wa≥L+Wa+Wb−Wa=L+WbS_a > S_b + W_b - W_a \ge L + W_a + W_b - W_a = L + W_b, nên L+Wb≤SaL + W_b \le S_a cũng đúng. Các khối nằm dưới cặp này chịu cùng tổng tải như trước. Vậy tháp sau khi đổi vẫn hợp lệ, với cùng tập khối. Lặp lại, mọi tháp hợp lệ đều có thể sắp lại theo Si+WiS_i + W_i tăng dần từ trên xuống dưới mà vẫn hợp lệ.

Sau khi sắp, ta chạy ba lô 0/1 theo thứ tự đó. Gọi dpjdp_j là tổng giá trị lớn nhất của các khối đã chọn có tổng trọng lượng đúng jj. Với khối ii (xét từ trên xuống), duyệt jj từ SiS_i xuống 00: nếu dpjdp_j đạt được thì dpj+Wi←max⁡(dpj+Wi, dpj+Vi)dp_{j + W_i} \leftarrow \max(dp_{j + W_i},\ dp_j + V_i). Tổng trọng lượng mới không quá Si+Wi≤2⋅104S_i + W_i \le 2 \cdot 10^4. Đáp án là dpjdp_j lớn nhất.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

struct Block {
    ll w, s, v;
};

const ll N = 1005;
ll n, dp[20005];
Block b[N];

bool cmp(Block x, Block y) {
    return x.s + x.w < y.s + y.w;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n;
    for (ll i = 1; i <= n; i++) cin >> b[i].w >> b[i].s >> b[i].v;
    sort(b + 1, b + n + 1, cmp);
    for (ll j = 1; j < 20005; j++) dp[j] = -1e18;
    for (ll i = 1; i <= n; i++)
        for (ll j = b[i].s; j >= 0; j--)
            if (dp[j] >= 0) dp[j + b[i].w] = max(dp[j + b[i].w], dp[j] + b[i].v);
    ll ans = 0;
    for (ll j = 0; j < 20005; j++) ans = max(ans, dp[j]);
    cout << ans << '\n';
}
3
3 6 3
1 3 1
5 1 6

Độ phức tạp: O(Nlog⁡N)O(N \log N) cho bước sắp xếp và O(N⋅Smax⁡)O(N \cdot S_{\max}) cho ba lô, với N=1000N = 1000 và Smax⁡=104S_{\max} = 10^4 cần khoảng 10710^7 phép tính.

Khi đi thi

  • Ba lô 0/1 duyệt sức chứa giảm dần, ba lô không giới hạn và đổi tiền duyệt tăng dần; đếm tập mệnh giá thì mệnh giá ở vòng ngoài, đếm dãy có thứ tự thì tổng ở vòng ngoài.
  • Sức chứa lớn mà tổng giá trị nhỏ thì đổi vai: trạng thái là giá trị, đại lượng tối ưu là khối lượng.
  • Trạng thái chưa tạo được phải mang giá trị vô cực (hoặc −∞-\infty khi tìm lớn nhất) chứ không phải 00; muốn truy vết thì giữ bảng hai chiều.

Tóm tắt

  • Ba lô 0/1: dpi,c=max⁡(dpi−1,c, dpi−1,c−wi+vi)dp_{i,c} = \max(dp_{i-1,c},\ dp_{i-1,c-w_i} + v_i), nén một mảng và duyệt cc giảm dần; thời gian O(nW)O(nW).
  • Ba lô không giới hạn và đổi tiền: duyệt cc tăng dần để một vật được lấy nhiều lần; tham lam có thể sai khi tìm số đồng ít nhất.
  • Hai vòng lặp đổi chỗ cho hai bài đếm khác nhau: mệnh giá ở vòng ngoài đếm tập mệnh giá, tổng ở vòng ngoài đếm dãy có thứ tự.
  • Bài tổng con dùng mảng đúng/sai; sức chứa quá lớn thì đổi vai khối lượng và giá trị.
  • Truy vết đi ngược bảng hai chiều từ ô đáp án: dpi,c≠dpi−1,cdp_{i,c} \ne dp_{i-1,c} nghĩa là vật ii được lấy.

Bài tập

Đọc thêm