2次元の動的計画法まとめ ナップザック問題編
動的計画法を用いて解くことができる問題のうち、ナップザック問題とその亜種問題の解法を自分用にまとめる。
基本のナップザック問題
問題
重さと価値がそれぞれ
DPテーブル
DPテーブルの定義
初期条件
重さが0ならば、
また、
漸化式
-
番目の商品を選ばないi -
番目の商品を(選べるならば)選ぶi
の2通りが考えられる。
漸化式は以下のようになる。1段目が
出力
例題と解答例
例題
解答例
AtCoder Educational DP Contest D - Knapsack1のC++解答例のソースコードを以下に示す。
#include <iostream>
#include <vector>
int main() {
int64_t N, W;
std::cin >> N >> W;
// DPテーブルに合わせて、入力を受け取る配列は1-indexedにする
// もちろん0-indexedでも全く問題なく解けるけど、漸化式がややこしく
// なるので1-indexedにする
std::vector<int64_t> w(N + 1);
std::vector<int64_t> v(N + 1);
for (int64_t i = 1; i <= N; i++) {
std::cin >> w.at(i) >> v.at(i);
}
// DPテーブル
std::vector<std::vector<int64_t>> dp(N + 1, std::vector<int64_t>(W + 1, 0));
for (int64_t i = 1; i <= N; i++) {
for (int64_t j = 1; j <= W; j++) {
// i番目の要素を選ばない場合
dp.at(i).at(j) = dp.at(i - 1).at(j);
// i番目の要素を選ぶ場合(選べるときだけ)
if (j - w.at(i) >= 0) {
dp.at(i).at(j) = std::max(dp.at(i - 1).at(j),
dp.at(i - 1).at(j - w.at(i)) + v.at(i));
}
}
}
std::cout << dp.at(N).at(W) << std::endl;
return 0;
}
実際の提出結果はこちら。
個数制限無しナップザック問題
問題
重さと価値がそれぞれ
ただし、同じ種類の商品はいくつでも選ぶことができる。
DPテーブル
DPテーブルの定義
初期条件
重さが0ならば、
また、
漸化式
ナイーブな実装
同じ種類の商品をいくつ選んでもよいという条件をナイーブに実装すると、漸化式は以下のようになる。
この漸化式をプログラムに実装すると、以下のようなソースコードになる。
std::vector<std::vector<int64_t>> dp(N + 1, std::vector<int64_t>(W + 1, 0));
for (int64_t i = 1; i <= N; i++) {
for (int64_t j = 1; j <= W; j++) {
for (int64_t k = 0; k * w.at(i) <= j; k++) {
dp.at(i).at(j) = std::max(dp.at(i).at(j),
dp.at(i - 1).at(j - k * w.at(i)) + k * v.at(i));
}
}
}
この実装では計算量は
この計算量を減らしてみる。
従って、漸化式の右辺を以下のように式変形することが可能となる。
基本のナップザック問題における漸化式とほぼ同じ形だが、2段目の式の
出力
例題と解答例
#include <iostream>
#include <vector>
int main() {
int64_t N, W;
std::cin >> N >> W;
std::vector<int64_t> w(N + 1);
std::vector<int64_t> v(N + 1);
for (int64_t i = 1; i <= N; i++) {
std::cin >> v.at(i) >> w.at(i);
}
std::vector<std::vector<int64_t>> dp(N + 1, std::vector<int64_t>(W + 1, 0));
for (int64_t i = 1; i <= N; i++) {
for (int64_t j = 1; j <= W; j++) {
if (j - w.at(i) >= 0) {
dp.at(i).at(j) =
std::max(dp.at(i - 1).at(j), dp.at(i).at(j - w.at(i)) + v.at(i));
} else {
dp.at(i).at(j) = dp.at(i - 1).at(j);
}
}
}
std::cout << dp.at(N).at(W) << std::endl;
return 0;
}
実際の提出結果はこちら。
ナップザック問題その2
問題
重さと価値がそれぞれ
ただし、
DPテーブル
DPテーブルの定義
そこで、価値の総和について計算を行うDPテーブルを定義する。
初期条件
漸化式
漸化式の形は、最小値を取ることを除けば基本のナップザック問題とそれほど変わらない。
出力
例題と解答例
解答例のコードを以下に示す。
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
int main() {
int64_t N, W;
std::cin >> N >> W;
std::vector<int64_t> w(N + 1);
std::vector<int64_t> v(N + 1);
for (int64_t i = 1; i <= N; i++) {
std::cin >> w.at(i) >> v.at(i);
}
int64_t V = std::accumulate(v.begin(), v.end(), static_cast<int64_t>(0));
// 無限大の代わりとして、十分大きな数(2^60)を初期条件にセットしている
std::vector<std::vector<int64_t>> dp(N + 1, std::vector<int64_t>(V + 1, 1LL << 60));
dp.at(0).at(0) = 0;
for (int64_t i = 1; i <= N; i++) {
for (int64_t j = 0; j <= V; j++) {
dp.at(i).at(j) = dp.at(i - 1).at(j);
if (j - v.at(i) >= 0) {
dp.at(i).at(j) = std::min(dp.at(i - 1).at(j), dp.at(i - 1).at(j - v.at(i)) + w.at(i));
}
}
}
int64_t answer = 0;
for (int64_t i = 1; i <= N; i++) {
for (int64_t j = 1; j <= V; j++) {
if (dp.at(i).at(j) <= W) {
answer = std::max(answer, j);
}
}
}
std::cout << answer << std::endl;
}
実際の提出結果はこちら。
参考文献
- 秋葉拓哉、岩田陽一、北川 宜稔、問題解決のアルゴリズム活用力とコーディングテクニックを鍛える プログラミングコンテストチャレンジブック 第2版、株式会社マイナビ出版、2021年4月12日
Discussion