公式

D - 荷物の配達 / Package Delivery 解説 by MMNMM


解法を思いつく道筋について

前から荷物を見ていって、そこまでの荷物の選び方を管理することを考えてみます。 前から \(k\) 個の荷物を見たとき、そこまでの荷物の選び方は \(2 ^ k\) 通りあります。 \(50\) 個の荷物を見たときの選び方 \(2 ^ {50}\) は非常に多いため、選び方を絞る必要があります。

まず、荷物の重量はすべて正なので、最終的な重量の合計は今の重量の合計を下回りません。 よって、現在の合計の重量が \(S\) を越える選び方を覚えておく必要はありません。 また、\(2\) つの選び方 \(A,B\) について、

  • \(A\) で選んだ個数は \(B\) で選んだ個数以下
  • \(A\) で選んだ荷物の重量の合計は \(B\) で選んだ荷物の重量の合計以下
  • \(A\) で選んだ荷物の純利益の合計は \(B\) で選んだ荷物の純利益の合計以上

という関係が成り立っているとき、選び方 \(B\) を覚えておく必要はありません(最終的に、選び方 \(B\) にこのあと見る荷物をいくつか追加したものが条件を満たすなら、同じ荷物を選び方 \(A\) に追加したものも条件を満たすためです)。

よって、次のような条件を満たす選び方だけを覚えておけばよいです。

  • 選んだ個数が \(n\) 個以下、重量の合計が \(w\) 以下であるような選び方のうち、純利益の合計が最大であるもの \((1\le n\le N,1\le w\le S)\)

これの先頭から荷物を \(k\) 個見た時点での値を \(\operatorname{dp} _ k[n][w]\) と書くことにしましょう。

新しく \(k+1\) 番目の荷物を見たとき、\(\operatorname{dp} _ {k+1}\) を求める必要があります。 これは、 \(k+1\) 番目の荷物を選ぶか選ばないかを比較することに相当します。 具体的には、

  • \(\operatorname{dp} _ k[n+1][w+W _ {k+1}]\)
  • \(\operatorname{dp} _ k[n][w]\) に \(k+1\) 番目の荷物を追加したもの

の \(2\) つの選び方を比較し、より純利益の大きいほうを新しい \(\operatorname{dp} _ {k+1}[n+1][w+W _ i]\) とすればよいです。

これを繰り返してすべての荷物を見たあとの、\(\operatorname{dp} _ N[i][S]\) の純利益の合計が \(T\) 以上になるような最小の \(i\) が求める最小の個数です。

ここまで、\(\operatorname{dp} _ k[n][w]\) は「選び方」を管理していましたが、更新や最後の判定のときには「選び方の純利益の合計」さえわかっていれば十分です。

以上より、

  • \(\operatorname{dp} _ k[n][w]\coloneqq k\) 番目までの荷物を使った、選んだ個数が \(n\) 個以下、重量の合計が \(w\) 以下であるような選び方に対する純利益の合計の最大値 \((1\le n\le N,1\le w\le S)\)

のように定義すると、効率的に管理・更新ができ、この問題を解くことができます。

これは、一般的なナップサック問題の DP(動的計画法)に個数の情報を付け加えたものだと思うことができます。

\(N\) 個の荷物を前から見て、荷物を見るたびに \(O(NS)\) 個の値を(定数時間で)更新するため、時間計算量は \(O(N ^ 2S)\) となります。

実装例は以下のようになります。

#include <iostream>
#include <vector>
using namespace std;

int main(){
    int N, S, T;
    cin >> N >> S >> T;

    vector dp(N + 1, vector<int>(S + 1)); // [0 .. N] × [0 .. S] の DP テーブル

    for (int i = 0; i < N; ++i) {
        int P, C, W;
        cin >> P >> C >> W;

        for (int n = N; n--; ) { // n が大きいほうから(小さいほうから処理すると同じ荷物を重複して選んでしまう)
            for (int w = 0; w + W <= S; ++w) { // 更新する範囲がはみ出ないように
                dp[n + 1][w + W] = max(dp[n + 1][w + W], dp[n][w] + P - C); // 新しい荷物を選ぶか選ばないかを比較して更新する
            }
        }
    }

    for (int n = 0; n <= N; ++n) { // n が小さいほうから見ていって
        if (dp[n][S] >= T) { // 個数が n 個以下で重量が S 以下である選び方の中で最大の純利益が T 以上なら
            cout << n << endl; // 答えは n
            return 0;
        }
    }
    cout << -1 << endl; // なければ -1
    return 0;
}
N, S, T = map(int, input().split())

dp = [[0 for _ in range(S + 1)] for _ in range(N + 1)] # [0 .. N] × [0 .. S] の DP テーブル

for _ in range(N):
    P, C, W = map(int, input().split())

    for n in reversed(range(N)): # n が大きいほうから(小さいほうから処理すると同じ荷物を重複して選んでしまう)
        for w in range(S - W + 1): # 更新する範囲がはみ出ないように
            dp[n + 1][w + W] = max(dp[n + 1][w + W], dp[n][w] + P - C) # 新しい荷物を選ぶか選ばないかを比較して更新する

for n in range(N + 1): # n が小さいほうから見ていって
    if dp[n][S] >= T: # 個数が n 個以下で重量が S 以下である選び方の中で最大の純利益が T 以上なら
        print(n)
        break
else:
    print(-1)

投稿日時:
最終更新: