部分和問題の応用問題!
問題概要
個の非負整数 が与えられれる。
これらの整数から 個選んで、総和が の倍数となるようにする。その値として考えられる最大値を答えよ。 の倍数にするのが不可能な場合は -1
を答えよ。
制約
前提知識
この問題を解く前に、次の問題を解けるかどうかを確認しておこう!!
個の非負整数 が与えられれる。
これらの整数からいくつか選んだ総和を にすることが可能かどうかを判定してください。
この問題は部分和問題と呼ばれる有名問題で、DP で の計算量で解けます。この問題に馴染みのない方は、まずは部分和問題を解く DP を勉強しましょう!!
次のような配列 dp
を考えます。
dp[i][v]
← 数列 の最初の 個を使って を作れるかどうか
そして次の遷移式で解けます。計算量は です。
dp[i+1][v] |= dp[i][v]
( を選ばない場合)dp[i+1][v+A[i]] |= dp[i][v]
( を選ぶ場合)
詳細については、たとえば次の記事で解説しています。
制約条件 (1): の倍数
部分和問題に「総和が の倍数」という制約をつけて、さらに最大値を求める問題にすると、次のようになります。
個の非負整数 が与えられれる。
これらの整数からいくつか選んだ総和が の倍数となるようにする。その値として考えられる最大値を答えよ。 の倍数にするのが不可能な場合は -1
を答えよ。
さて、「 の倍数である」という制約は「 で割ったあまりが 0」と言い換えられます。これに基づいて、次の配列 dp
を考えましょう。このように、DP 配列に "あまり" という情報を付与する方法は典型的です。
dp[i][d]
← 数列 の最初の 個を使って作れる「 で割って あまる数」の最大値 (不可能な場合は -1)
答えは dp[N][0]
です。最初はこの配列全体を -1 で初期化しておきます。さらに、初期条件は dp[0][0] = 0
と表せます。
遷移式は次のように表せます。ただし、dp[i][d]
の値が -1 の場合はこの遷移を実行しないようにします。
dp[i+1][d] = max(dp[i+1][d], dp[i][d])
( を選ばない場合)dp[i+1][(d+A[i])%D] = max(dp[i+1][(d+A[i])%D], dp[i][d])
( を選ぶ場合)
計算量は となります。
制約条件 (2): 個
最後に「選ぶ数の個数が 個」という制約条件を加味します。そうすると元の問題と同一となります。配列 dp
に添字をさらに付加して、次のように定義してみましょう。
dp[i][k][d]
← 数列 の最初の 個の中から、 個を選んで作れる「 で割って あまる数」の最大値 (不可能な場合は -1)
遷移式は次のように表せます。ただし、dp[i][j][d]
の値が -1 の場合はこの遷移を実行しないようにします。
dp[i+1][j][d] = max(dp[i+1][j][d], dp[i][j][d])
( を選ばない場合)dp[i+1][j+1][(d+A[i])%D] = max(dp[i+1][j+1][(d+A[i])%D], dp[i][j][d])
( を選ぶ場合)
計算量は となります。
コード
#include <bits/stdc++.h> using namespace std; void chmax(long long &a, long long b) { a = max(a, b); } int main() { // 入力 long long N, K, D; cin >> N >> K >> D; vector<long long> A(N); for (int i = 0; i < N; ++i) cin >> A[i]; // DP long long dp[110][110][110]; memset(dp, -1, sizeof(dp)); // 配列全体を -1 で初期化 dp[0][0][0] = 0; for (int i = 0; i < N; ++i) { for (int j = 0; j <= N; ++j) { for (int d = 0; d < D; ++d) { if (dp[i][j][d] == -1) continue; // 選ばない場合 chmax(dp[i+1][j][d], dp[i][j][d]); // 選ぶ場合 chmax(dp[i+1][j+1][(d+A[i])%D], dp[i][j][d] + A[i]); } } } cout << dp[N][K][0] << endl; }