公式

G - Stonks 解説 by physics0523


\(dp[\) \(i\) 日目までの取引を終えた状態 \(][\) 現状 \(j\) 株持っている \(]=\{\) 所持金の増加額の最大値 \(\}\) というDPを行えばこの問題を \(O(N^2)\) で解くことができます。取引終了時に株を残しておくメリットがないことから、答えは \(dp[N][0]\) となります。

サンプル1 を使い、このDPを観察してみましょう。

サンプル1 の入力:

8
2 5 4 3 7 1 8 6

上から \(i\) 行目、左から \((j-1)\) 列目に \(dp[i][j]\) を出力した数表:

 0  -2
 3  -2  -7
 3  -1  -6 -11
 3   0  -4  -9 -14
 7   3  -2  -7 -14 -21
 7   6   2  -3  -8 -15 -22
14  10   5   0  -7 -14 -22 -30
16  11   6   0  -6 -13 -20 -28 -36

横方向に等差数列がいくつか見えるので、隣接項の公差をとってみましょう。
上から \(i\) 行目、左から \(j\) 列目に \(dp[i][j-1]-dp[i][j]\) を出力した数表:

2
5 5
4 5 5
3 4 5 5
4 5 5 7 7
1 4 5 5 7 7
4 5 5 7 7 8 8
5 5 6 6 7 7 8 8

\(dp[N][0]\) を求めるためにこの数表たちを観察すると、以下の解法が成立しそうに見えます。

  • まず、 多重集合 \(M=\{A_1\}\) を用意し、答え \(R\)\(0\) で初期化する。
  • \(i=2,3,\dots,N\) に対して、順に以下を行う。
    • もし、 \(M\) の最小値 \(m\) より \(A_i\) の方が大きければ、 \(R\)\(A_i-m\) を加算し、 \(M\) から \(m\) を (\(1\) つ) 削除する。その後、 \(M\)\(A_i\)\(2\) つ追加する。
    • そうでない場合、 \(M\)\(A_i\)\(1\) つ追加する。

実際、この解法は正しいです。ここからは、解法の正当性を説明します。

横軸に \(j\) 、縦軸に \(dp[i][j]\) の値をとりプロットした折れ線が上に凸になることを仮定します(\(dp[1]\) の時点では、これは明らかに正しいです。)
\(dp[i]\) から \(dp[i+1]\) へと、どのような遷移が行われるか考察します。
\(dp[i+1][j]\) は、以下の \(3\) つの \(\max\) をとったものとなります。

  • \(dp[i][j]\) ( \(i+1\) 日目に何もしなかったケース)
  • \(dp[i][j-1]-A_{i+1}\) ( \(i+1\) に株を買ったケース)
  • \(dp[i][j+1]+A_{i+1}\) ( \(i+1\) に株を売ったケース)

これを折れ線の平行移動に対応させると、それぞれ

  • \(dp[i]\) (この折れ線を \(dp\) とする)
  • \(dp[i]\) を右に \(1\) 、下に \(A_{i+1}\) 移動させた折れ線 (この折れ線を \(dp_-\) とする)
  • \(dp[i]\) を左に \(1\) 、上に \(A_{i+1}\) 移動させた折れ線 (この折れ線を \(dp_+\) とする)

に対応します。

以下の図は、 \(A=(4,2,2,1,1,1,3)\) であったとき、 \(dp[6]\) から \(dp[7]\) を求める際の各折れ線です。

まず、 \(dp\)\(dp_-\) とを比較し、それらの \(\max\) を取った折れ線 \(L_-\) を考えてみましょう。

  • 両折れ線の傾きが \(-A_{i+1}\) 以上の部分については、 \(dp\)\(dp_-\) 以上の位置にあるので、 \(L_-\)\(dp\) が一致します。
  • 両折れ線の傾きが \(-A_{i+1}\) 未満の部分については、 \(dp_-\)\(dp\) より上の位置にあるので、 \(L_-\)\(dp_-\) が一致します。
  • この \(2\) つの部分の間に、折れ線 \(L_-\) 上に、傾きが \(-A_{i+1}\) の (横幅 \(1\) の) 線分が入ります。ここで、 \(dp_-\)\(dp\) より上側に来ます。

次に、 \(dp\)\(dp_+\) とを比較して同様の考察を行った折れ線 \(L_+\) について検討します。

  • 両折れ線の傾きが \(-A_{i+1}\) 以上の部分については、 \(dp_+\)\(dp\) 以上の位置にあるので、 \(L_+\)\(dp_+\) が一致します。
  • 両折れ線の傾きが \(-A_{i+1}\) 未満の部分については、 \(dp\)\(dp_+\) より上の位置にあるので、 \(L_+\)\(dp\) が一致します。
  • この \(2\) つの部分の間に、折れ線 \(L_+\) 上に、傾きが \(-A_{i+1}\) の (横幅 \(1\) の) 線分が入ります。ここで、 \(dp\)\(dp_+\) より上側に来ます。

最終的な遷移後の \(dp\)\(L_-\)\(L_+\)\(\max\) を取ったものであるため、出来上がる \(dp\) は遷移前の \(dp\) に傾き \(-A_{i+1}\) の横幅が \(1\) または \(2\) の線分を挿入して適切に平行移動したような形になります。

この議論を整理すると、先程示した解法の正当性を示すことができます。

実装例(C++):

#include<bits/stdc++.h>

using namespace std;

int main(){
  int n;
  cin >> n;
  vector<long long> a(n);
  for(auto &nx : a){cin >> nx;}
  long long res=0;
  priority_queue<long long,vector<long long>,greater<long long>> pq;
  pq.push(a[0]);
  for(int i=1;i<n;i++){
    if(pq.top()<a[i]){
      res+=(a[i]-pq.top());
      pq.pop();
      pq.push(a[i]);
    }
    pq.push(a[i]);
  }
  cout << res << '\n';
  return 0;
}

投稿日時:
最終更新: