ひとりで勝手にはじめた蟻本読書会 本当は難しい貪欲法 選択肢を多く残す 蟻本読書会の続きです。今回から動的計画法ゾーンに入ります。

01ナップサック問題とは?

ナップサック問題、価値 viと重量 wi をもつ n 種類の品物が与えられたとき、重量の合計が W を超えない範囲で品物のいくつかをナップサックに入れて、その価値の合計を最大化するにはどうすればいいかという問題です。ナップサック問題には同じ種類の品物を1つまでしか入れられないものと、同じ品物をいくつでも入れてよいものがあります。前者が01ナップサック問題といわれているものです。

ズバリこんな問題です。この問題、1 ≦ W ≦ 10^9 という制約があって一筋縄ではいかないのですが、W が極端に大きな値でなければ比較的簡単にできます。

D – ナップサック問題

N 個の荷物があり、i(1 ≦ i ≦ N) 番目の荷物には価値 v_i と重さ w_i が割り当てられている。許容重量 W のナップサックが1つある。重さの和が W 以下となるように荷物の集合を選びナップサックに詰め込むとき、価値の和の最大値を求めよ。ただし、同じ荷物は一度しか選ぶことができない。

「N ≦ 30」、「全てのi(1 ≦ i ≦ N) について 1 ≦ w_i ≦1000」、「全てのi(1 ≦ i ≦ N) について 1 ≦ v_i ≦ 1000」という 3 つの条件のうち少なくともひとつの条件が成り立つ。

入力されるデータ
N W
v_1 w_1
v_2 w_2

v_W w_W
(ただし 1 ≦ N ≦ 200, 1 ≦ W ≦ 10^9)

2^n 全探索で解く方法と問題点

ではどのようにして解けばよいのでしょうか?

すべての品物について入れる入れないを考えるという全探索を行えば、その計算量は O(2^n)となります。n が小さいときは問題ありませんが、少し大きな値になると制限時間内には終わりません。

とりあえず計算量が O(2^n)になるコードは以下のようになります。

すでに N には品物の総数が、W はナップサックにいれられる品物の重さが、配列 Weights には各品物の重さが、配列 Values には各品物の価値が格納されているものとします。

再起呼び出しで i 番目以降の品物をそれらの重さの総和が u 以下になるように選んだときの価値を返すSearchメソッドを以下のように定義します。するとSearch(0, W)で解を求めることができます。

メモ化再帰で速度を改善する

このコードの欠陥は計算量が O(2^n)であり、nの値が大きくなると時間がかかることです。n = 10程度であれば問題ありませんが、n = 30 を超えるとなかなか結果を返してくれなくなります。

Searchメソッドは同じ引数であれば常に同じ値を返します。ならば一度呼び出されたら計算結果をどこかに保存して同じ引数で再び呼び出された場合は計算をしないで保存しておいた値をそのまま返せば処理速度を改善できるのではないかと考えることができます。「メモ化再帰」です。

以下のコードでは2次元配列を定義して、一度呼び出されたら計算結果を保存しています。これで処理速度を改善することができます。

漸化式を使った解法

漸化式を使った解法もあります。

2次元配列を宣言し、dp[i + 1][w] を i 番目までの品物の中から重さが w を超えないように選んだときの、価値の総和の最大値とします。

dp[i + 1][w] は dp[i][w] と dp[i][w – Weights[i]] + Values[i] を比較して大きいほうを採用します。dp[i][w] は i 番目の品物を入れない場合、dp[i][w – Weights[i]] + Values[i] は i 番目の品物を入れることで重量が w – Weights[i] から w へと増え、価値も Values[i] だけ増えた状態です。両者を比較すれば入れたほうがいいか入れないほうがいいかわかります。これによってi 番目までの品物の中から重さが w を超えないように選んだときの価値の総和の最大値も確定させることができます。

部分和問題

A – コンテスト

N 問の問題があるコンテストがあり、i 問目の問題の配点は p_i 点である。コンテスタントは、この問題の中から何問か解き、解いた問題の配点の合計が得点となる。このコンテストの得点は何通り考えられるか。

入力されるデータ
N
p_1 p_2 p_3 … p_N

(ただし 1 ≦ N ≦ 100, 1 ≦ p_i ≦ 100)

入力されるデータの制約から満点をとっても10000点を超えることはありません。そこで new bool[N + 1, 10010] を宣言します。DP[i][p]には i 番目まで見て p 点を取ることが可能かを格納します。

dp[i – 1, p] が true なら当然 dp[i, p] も true です。また i 問目の問題の配点は values[i] である以上、あらたに dp[i – 1, p + values[i – 1] ] も true になります。最後に dp[N, i] の true の数を数えます。

これは全探索っぽいやり方です。最初は1問も解いていないので0点です。そのあと i問目が終わったときに得点が加算された場合とされなかった場合についてリストに得点を格納しています。得点が同じだった場合に内側のループで二重に格納しないように check 配列で確認しています。一応、処理は制限時間以内に終わります。

重さ以外の制約がついたナップサック問題

D – 高橋くんの苦悩

高橋くんは、画面のスクリーンショットを表計算ソフトに貼り付ける作業を命じられました。 画面のスクリーンショットは N 枚あり、高さは全て等しいのですが、幅が異なります。 また、表計算ソフトに貼りつけ可能なスクリーンショットには 2 つの制約が存在します。

表計算ソフトの幅は W しかない。そのため貼りつけるスクリーンショットの幅の合計値は W 以下でなければなりません。また枚数制限もあります。表計算ソフトは K 枚より多くのスクリーンショットを貼りつけることができません。よって貼りつけ可能なスクリーンショットは K 枚までです。

さらに、スクリーンショットには「重要度」が存在するため、高橋くんは 2 つの制約を満たしながら、貼り付けるスクリーンショットが持つ重要度の合計値を最大化したいです。 しかし、彼にとってこの仕事は難しいので、あなたが彼の代わりに表計算ソフトに貼り付け可能なスクリーンショットが持つ重要度の合計の最大値を求めてください。

入力されるデータ
W
N K
A_1 B_1
A_2 B_2

A_N B_N

(ただし 1 ≦ W ≦ 10000, 1 ≦ N ≦ 50, 1 ≦ K ≦ N,
1 ≦ A_i ≦ 1000, 1 ≦ B_i ≦ 100)

ナップサック問題に重さだけでなく選べる数の制約がついた感じの問題です。2次元配列ではなく3次元配列をつかった漸化式を考えます。

dp[i][k][w]:(A[i] ,B[i])の組までで k 組選んだとき、A の総和が w の時の B の総和の最大値

3次元配列なので三重ループを回すことになります。最後に dp[N][j][w] のなかから最大値を探して結果を返します。計算量は O(NKW)です。

これは全探索っぽいやり方です。

スクリーンショットを追加しないのであればそのまま、statusesに格納されているオブジェクトをループのなかで新しいリストに引き渡し、追加する場合は個数と幅の制限をこえないことを確認して、個数と価値を増やしたオブジェクトを新しく生成して追加しています。このとき同じ個数で同じ幅のオブジェクトがすでに存在するならリストには追加しないで値の更新だけをおこなっています。

同じ個数で同じ幅のオブジェクトが存在するかどうかは2次元配列 olds で確認しています。これを忘れると制限時間内に終わらないうえにメモリーをどか食いして散々な目に遭います。一応、処理は制限時間以内に終わります。