このシリーズは、このアルゴリズムの教科書の拡張です:「Introduction to Advanced Algorithm Contest」(Jingdong Dangdang)Tsinghua University Press
ご意見がございましたら、お問い合わせください:(1)QQ Group、567554289;(2)Author QQ、15512356
DPの状態方程式のクラスがあります。たとえば、
\(dp [i] = min \ {dp [j] -a [i] * d [j] \} \) \(0≤j<i、d [j]≤d [J + 1]、[
I]≤A [I + 1] \) の両方のAが存在することを特徴とする\(I \)が持っている\(J \)用語\([I] * Dは [ j] \)。
プログラミング時に、単に\(i \)と\(j \)をループする場合、複雑さは\(O(n ^ 2)\)です。
勾配最適化(凸包トリック、凸包最適化)により、時間の複雑さは\(O(n)\)に最適化されます。
スロープ最適化のコアテクノロジーは、スロープ(凸包)モデルと単調キューです。
1.状態方程式を平面傾斜問題に変換する
固定\(i \)に対して\(j \)が変更されると、方程式は\(dp [i] \)の最適値を見つけます。したがって、\(i \)に関する部分は固定値と見なすことができます。\(j \)の部分は変数とみなされます。\(分\)除去され、式に:
\(DP [J] = A [I] * D [J] + DP [I] \)
観察の便宜のために、その結果:\(Y = DP [J] \ )、\(X = d [j] \)、\(k = a [i] \)、\(b = dp [i] \)、方程式は次のようになります:
\(y = kx + b \)
勾配の最適化の数学モデルは、状態伝達方程式を平面座標系の直線に変換することです:\(y = kx + b \)。その中で:
(1)変数\(x \)、\(y \)および\(j \)は関連しており、\(y \)だけが\(dp [j] \)を含みます。点\((x、y)\)は、問題の中で可能な決定です。
(2)勾配\(k \)と切片\(b \)は\(i \)に関連しており、\(b \)のみに\(dp [i] \)が含まれています。最小の\(b \)には、最小の\(dp [i] \)が含まれます。これは、状態方程式の解です。
勾配最適化を適用するための2つの条件に注意してください。\(x \)および\(k \)は単調に増加します。つまり、\(x \)は\(j \)とともに増加し、\(k \)は\に従います(i \)増分および増分。
2. dp [i]を見つける
固定\(i \)のとき、最初に\(dp [i] \)を見つけることを検討してください。以下のため\(I \)定数であり、その後の傾き\(K = [I] \)は一定とみなすことができます。場合\(J \)で\(0≤j<I \)ときの変化\(j_r \) 、点を生成する(V_R =(X_R、Y_r)\)\直線のこの時点で、\ (y = kx + b_r \)、\(b_r \)は切片です。図1に示すように。
ため(0≤j<I \)\全部で\(J \) 、それらの対応する点が平面上に描かれていると、点に対応する線の傾き\(K = [I] \)は同じであり、切片\(b \)のみが異なります。すべてのこれらの点において、点\(V '\)切片線形最小場合(\ \ b)は'、算出した'(\ B)\ので、(\ B)\'含有(DP [\ i] \)、次に最適な\(dp [i] \)が計算されます。図2に示すように。
最も有利な\(v '\)を見つける方法は?「下凸シェル」を使用。
前述のように、\(x \)は単調に増加します。つまり、\(x \)は\(j \)とともに増加します。図3(1)では4つの点が示され、それらの\(x \)座標は増加しています。
図3(1)の1,2,3は「下凸包」を構成しており、「下凸包」の特徴は、線分12の傾きが線分23の傾きよりも小さいことである。2、3、4は「上部凸シェル」を構成します。上部の凸包の中間点3を通る直線の切片bは、同じ勾配で2または4を通る直線の切片よりも小さくなければならないため、点3は最善の利点ではないので、削除してください。
「上凸包」を削除すると、図3(2)が得られ、残りのすべての点が「下凸包」の関係を満たします。一番の利点は、「下凸シェル」にあります。たとえば、図3(3)では、これらのポイントを\(k \)の傾きを持つ直線でカットします。ラインセグメント12の傾きが\(k \)より小さく、24の傾きが\(k \)より大きい場合、ポイント2は「下凸シェル」の最大のメリット。
上記の操作は、単調なキュープログラミングを使用するのに非常に便利です。
(1)キューに「下に凸の船体」を維持した状態でチームに入る。つまり、2つの連続するポイントごとに構成される直線であり、傾きは単調に増加している。新しいポイントがキューに入力されるとき、キュー内のポイントと一緒に「下部凸包」を形成できることを確認してください。たとえば、キューの最後の2つのポイントは\(v_1 \)、\(v_2 \)であり、キューに追加される新しいポイントは\(v_3 \)です。比較\(V_1 \)、\ (V_2 \)、\ (V_3 \) 、ラインを見て\(v_1v_2 \)と\(v_2v_3 \)の傾きがインクリメントされているかどうか、もしそうであれば、\(V_1 \)、\ (V_2 \)、\(v_3 \)は「下部凸包」を形成します。勾配が増加しない場合は、\(v_2 \)が正しくないことを意味し、チームの末尾から跳ね返ります。次に、キューの末尾と\(v_3 \)の2つのポイントを比較し続けます。繰り返します。上記の\(v_3 \)までの操作でチームに入ることができます。上記の操作の後、キュー内のポイントは大きな「下部凸包」を形成し、各2ポイントによって形成される直線は増加する勾配を持ち、キューは単調なキューのままです。
(2)待ち行列から抜け出し、最良の利点を見つけます。チームの先頭にある2つのポイントは\(v_1 \)と\(v_2 \)です。ラインセグメント\(v_1v_2 \)の勾配が\(k \)よりも小さい場合、\(v_1 \)が最善の利点ではないことを意味します。 、傾斜が\(k \)を超えるまでチームの先頭にある2つの新しいポイントを比較し続け、チームの先頭にあるポイントが最も有利な\(v '\)です。
3.すべてのdp [i]を見つける
上記は\(dp [i] \)、複雑さ\(O(n)\)を取得しました。すべての場合には\(I \) 、各そのような要求\(DP [I] \)は、全体的な複雑性はまだある(O(N ^ 2)\ \) 、および計算の複雑さは変わりません。最適化された方法はありますか?\(i_1 \)が
小さいほど、対応する点は{ \(v_0、v_1、...、v_ {i1} \) }になり、\(i_2 \)が大きいほど多くの点に対応します{ \(v_0、v_1、...、v_ {i1}、...、v_ {i2} \) }、これは\(i_1 \)のすべてのポイントを含みます。\(i_1 \)の最大の利点を探す場合は、{ \(v_0、v_1、...、v_ {i1} \) }を確認する必要があります。i2の最大の利点を探す場合は、{ \(v_0、v_1 ,.を確認する必要があります。 ..、v_ {i1}、...、v_ {i2} \) }。ここでは重複チェックが行われ、これらの重複を回避できます。これは、最適化できる場所であり、最適化のために「下凸シェル」を使用します。 (1)各\(i \)\(k_i = a [i] \)に対応する勾配は、制約に従って異なります
\(a [i]≤a [i + 1] \)、\(i \)が増加すると、勾配が増加します。
(2)前述のように、\(i_1 \)の最良の点を見つけたら、いくつかの点、つまり\(k_ {i1} \)より小さい勾配を持つ点を削除できます。これらの削除されたポイントは、背面の\(i_2 \)が大きい場合、傾き\(k_ {i2} \)も大きいため、削除する必要があります。
(1)と(2)の説明によると、最適化方法は次のとおりです:すべての\(i \)に対して、単調なキューを使用してすべてのポイントを統一された方法で処理します;小さい\(i_1 \)によって削除されたポイントは単調です跳ね返ると、後ろの大きな\(i_2 \)はそれらを処理しなくなります。
各ポイントは単調なキューに1回だけ入るため、全体の複雑度は\(O(n)\)です。
次のコードは、上記の操作を示しています。
//q[]是单调队列,head指向队首,tail指向队尾,slope()计算2个点组成的直线的斜率
for(int i=1;i<=n;i++){
while(head<tail && slope(q[head],q[head+1])<k) //队头的2个点斜率小于k
head++; //不合格,从队头弹出
int j = q[head]; //队头是最优点
dp[i] = ...; //计算dp[i]
while(head<tail && slope(i,q[tail-1])<slope(q[tail-1],q[tail])) //进队操作
tail--; //弹走队尾不合格的点
q[++tail] = i; //新的点进队列
}
上記のコードの理解を深めるために、特別なケースを考慮してください:キューに入るポイントはすべて「下部凸包」機能に準拠し、これらのポイントによって形成される直線の勾配はすべての勾配\(k_i \)よりも大きい場合、結果は次のようになります:チームヘッドはそれは排出され、チームに入るポイントは排出されず、チームヘッドは\(n \)回再利用されます。
4.例
典型的な例を以下にサンプル質問とともに示します。
HDU 3507 記事の印刷http://acm.hdu.edu.cn/showproblem.php?pid=3507
トピックの説明:Nワードを含む記事を印刷します。i番目のワードの印刷コストはCiです。1行にkワードを印刷するコストは、Mが定数であることです。コストを最小限に抑えるために記事を整理する方法は?
入力:多くのテストケースがあります。各テストケースでは、最初の行に2つの数値NとMがあります(0≤n≤500000、0≤M≤1000)。次に、次の2からN + 1行にN個の番号があります。入力はEOFで終了します。
出力:記事を印刷するための最小コストを示す数値。
入力例:
5 5
5
9
5
7
5
出力例:
230
タイトルの意味は次のとおりです。\(N \)数と定数\(M \)があり、\(N \)数はいくつかの部分に分割され、各部分の計算値はこれらの部分の合計の二乗と\(M \)、合計計算値はすべての部品の計算値の合計であり、最小の合計計算値を見つけます。ための(N \)\大きい、\(O(N ^ 2)\)アルゴリズムタイムアウト。
セット\(DP [I] \)の前に出力を表す\(I \) :単語の最小コスト、DP移動式
\(DP [I] =分\ {DP [J] +(SUM [I] -sum [ j])2 + M \} \) \(0 <j <i \)
ここで、\(sum [i] \)は最初の\(i \)桁の合計を表します。
次は、DP方程式を\(y = kx + b \)に書き換えます。最初に方程式を展開します:
\(dp [i] = dp [j] + sum [i] * sum [i] + sum [j] * sum [j] -2 * sum [i] * sum [j] + M \ )
アイテムの移動:
\(dp [j] + sum [j] * sum [j] = 2 * sum [i] * sum [j] + dp [i] -sum [i] * sum [i] -M \)
コントロール\(y = kx + b \)があります。
\(y = dp [j] + sum [j] * sum [j] \)、\(y \)は\(j \)にのみ関連しています。
\(X = 2×SUM [J] \)、\ (X \)のみと\(J \)について、とを有する\(J \)がインクリメントインクリメントされます。
\(k = sum [i] \)、\(k \)は\(j \)にのみ関連し、\(i \)が増加するにつれて増加します。
\(b = dp [i] -sum [i] * sum [i] -M \)、\(b \)は iにのみ関連し、\(dp [i] \)を含みます。
コードを以下に示します。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 500010;
int dp[MAXN];
int q[MAXN]; //单调队列
int sum[MAXN];
int X(int x){ return 2*sum[x]; }
int Y(int x){ return dp[x]+sum[x]*sum[x]; }
//double slope(int a,int b){return (Y(a)-Y(b))/(X(a)-X(b));} //除法不好,改成下面的乘法
int slope_up (int a,int b) { return Y(a)-Y(b);} //斜率的分子部分
int slope_down(int a,int b) { return X(a)-X(b);} //斜率的分母部分
int main(){
int n,m;
while(~scanf("%d%d",&n,&m)){
for(int i=1;i<=n;i++) scanf("%d",&sum[i]);
sum[0] = dp[0] = 0;
for(int i=1;i<=n;i++) sum[i]+=sum[i-1];
int head=1,tail=1; //队头队尾
q[tail]=0;
for(int i=1;i<=n;i++){
while(head<tail &&
slope_up(q[head+1],q[head])<=sum[i]*slope_down(q[head+1],q[head]))
head++; //斜率小于k,从队头弹走
int j = q[head]; //队头是最优点
dp[i] = dp[j]+m+(sum[i]-sum[j])*(sum[i]-sum[j]); //计算dp[i]
while(head<tail &&
slope_up(i,q[tail])*slope_down(q[tail],q[tail-1])
<= slope_up(q[tail],q[tail-1])*slope_down(i,q[tail]))
tail--; //弹走队尾不合格的点
q[++tail] = i; //新的点进队尾
}
printf("%d\n",dp[n]);
}
return 0;
}
5.演習
(1)Luogu P3195おもちゃの梱包https://www.luogu.com.cn/problem/P3195
DP式:\(dp [i] = min \ {dp [j] +(sum [i] + i− sum [j] −j−L−1)^ 2 \} \)
(2)Luogu 4072 SDOI2016ジャーニーhttps://www.luogu.com.cn/problem/P4072
2次元勾配最適化、DP方程式:\( dp [i] [p] = min \ {dp [j] [p-1] +(s [i] −s [j])^ 2 \} \)