いかにも ABC-E にありあそうな DP!!
これとか似てる
問題概要
個の店がある。 日間にわたって、いずれか 1 つの店で買い物をする。
- 日目に 番目の店で買い物をしたときの値段は で与えられる (10 の倍数)
- ただし 2 日連続で同一の店で買い物をしたときは、1 割引きとなる
- 3 日以上連続で同一の店で買い物をしたときは、3 割引きとなる
日間の買い物の支払い金額の総和の最小値を求めよ。
制約
考えたこと
いかにも DP という感じ。次のような DP を考えたくなる
- dp[ i ][ j ][ k ] := i 日間の買い物を行う。前日に買った店は j 番目であって、前日までに j 番目の店で k 回連続で買い物をしていたとする。この場合の総額の最小値
ただし、 の値は の 3 つで十分である。2 日連続以上となった場合はずっと のままとすれば OK。
DP の遷移
前日にお店 j で買い物して、その次の日にお店 j2 で買い物をする場合を単純に探索 (以下のコードのように) すると、 の計算量となってしまう。
for (int j = 0; j < N; ++j) { for (int k = 0; k < 3; ++k) { for (int j2 = 0; j2 < N; ++j2) { if (j != j2) chmin(dp[i+1][j2][1], dp[i][j][k] + (新しい価格)); else chmin(dp[i+1][j2][min(k+1, 2)], dp[i][j][k] + (新しい価格)); } } }
もっと上手にできる。お店が変わる場合は、そもそも前日までのあらゆる場合の最小値を としたとき
for (int j = 0; j < N; ++j) chmin(dp[i+1][j][1], vmin + (新しい価格));
というふうに遷移すれば OK。お店が変わらない場合はそもそも j2 == j なので、新たな変数 j2 を持ち出す必要はない。
こうして計算量は となった。
コード
以下のコードでは、配列を使い回す実装をしている。
#include <bits/stdc++.h> using namespace std; #define REP(i, n) for (long long i = 0; i < (long long)(n); ++i) template<class T> inline bool chmin(T& a, T b) { if (a > b) { a = b; return 1; } return 0; } const long long INF = 1LL<<60; int main() { int N, D; cin >> N >> D; vector<vector<long long>> P(D, vector<long long>(N)); REP(d, D) REP(i, N) cin >> P[d][i]; vector<vector<long long>> dp(N, vector<long long>(3, INF)); REP(j, N) dp[j][0] = 0; REP(d, D) { // DP の遷移先 vector<vector<long long>> nex(N, vector<long long>(3, INF)); // different item long long vmin = INF; REP(j, N) REP(k, 3) chmin(vmin, dp[j][k]); REP(j, N) chmin(nex[j][1], vmin + P[d][j]); // same item REP(j, N) REP(k, 3) { long long add = P[d][j]; if (k == 1) add = add * 9 / 10; else if (k == 2) add = add * 7 / 10; chmin(nex[j][min(k+1, 2LL)], dp[j][k] + add); } // DP を遷移 swap(dp, nex); } long long res = INF; REP(j, N) REP(k, 3) chmin(res, dp[j][k]); cout << res << endl; }