動的プログラミングのパターン


免責事項: 以下に説明する方法は普遍的なものではありませんが、多くの場合、問題を解決したり、適切な解決策を見つけるのに役立ちます.

問題が、最適な方法で交差しないサブセグメント (一連の連続した要素) に配列を分割する (または適切な分割数をカウントする) 必要があるという事実に要約される場合は、解決を試みる価値があります。動的計画法を使用します。

ソリューション スキームの例は次のとおりです。
dp[i] - 最初の i 要素に対する応答

dp[i] のカウント: 最初の i 要素のみを考慮しているため、i 番目の要素が最後の要素になります。つまり、この要素は最後のサブセグメントにあり、同時にそこの右端の要素になります。したがって、最後のサブセグメント j の左側の境界を反復処理できます。列挙の過程で、このサブセグメントの値を計算し、それが正しい場合は、dp[i] から dp[j - 1] とサブセグメントの値 [j;i] を再計算します。

次の簡単な問題を考えてみましょう: 整数の配列が与えられた場合、それを最小数の交差しないサブセグメントに分割して、各数値が何らかのサブセグメントに含まれ、各サブセグメントに同じ数値が含まれるようにする必要があります。たとえば、配列 1 2 2 3 3 3 2 1 1 の場合、最適なパーティションは [1] [2 2] [3 3 3] [2] [1 1] のようになります。このタスクは、配列を通過するだけで簡単に解決できますが (同じ連続する要素をすべて 1 つのサブセグメントに入れます)、例として動的計画法を使用して解決します。
  intn; シン>> n; // 配列を 1 インデックスで埋めます vector arr(n + 1); for (int i = 1; i <= n; i++) シン>> arr[i]; // 最初に +oo を dp 値に設定します vector dp(n + 1, 1000000000); // 長さゼロの配列は分割する必要がないので、答えは 0 です dp[0] = 0; // i の昇順で dp[i] の答えを数えます for (int i = 1; i <= n; i++) { // 現在 arr[i] は最後の要素であるため、最後のサブセグメントの右端の番号になります // この最後のサブセグメントが開始された場所のすべてのオプションをループします for (int j = i; j > 0; j--) { if (arr[j] != arr[i]) { // 最後の要素と等しくない要素に遭遇した場合、サブセグメントには異なる数値が含まれ、これは条件に適合しません // 続行しても意味がないため、左の境界線を左に移動しても、この要素は消えないため、ブレークします 壊す; } // 最後のサブセグメントが [j;i] だったとします // したがって、最初の j-1 要素の最適な分割を取得し、1 を追加する必要があります (サブセグメント [j; i] 自体) dp[i] = min(dp[i], dp[j - 1] + 1); } } cout << dp[n];
要素がどのサブセグメントにも属さない可能性がある場合は、dp[i] = dp[i - 1] のように適切なオプションを検討する必要があります。

配列を正確に k 個のサブセグメントに分割する必要がある場合は、動的計画法で 2 番目のパラメーター (分割するセグメントの数) を追加するだけです。
つまり、次の dp を検討します。
dp[i][j] は、最初の i 個の要素を正確に j 個のセグメントに分割した場合の答えです。
無効な状態に注意してください。

ダイナミクスの再計算は同じですが、2 番目のパラメーターが考慮されます。つまり、dp[i][k] をカウントし、最後のサブセグメント j の左側の境界をソートして、dp[j - 1][k - 1] とセグメントの値を通じて dp[i][k] を再計算します。 [j;i]。

免責事項: 以下に説明する方法は普遍的なものではありませんが、多くの場合、問題を解決したり、適切な解決策を見つけるのに役立ちます.

ある軸 (通常は時間軸または配列のインデックス) 上に一連のギャップがあり、選択したギャップが交差しないように最適な方法でそれらの一部を選択する必要がある場合は、動的プログラミングを使用してみる必要があります。 。

おおよその解決スキーム:

最初に、利用可能なギャップを右の境界線で並べ替えます。次のダイナミクスを開始しましょう: dp[i] - 最初の i 間隔の答え。
次のように再計算します。まず、この間隔が使用されない状況を考慮し、次に dp[i] = dp[i-1] だけを計算します。これにより、i が増加しても dp[i] の値が減少しないことが保証されることに注意してください。そして、これは論理的だからです。新しいギャップを追加しても、全体的な答えを悪化させることはできません。単に新しいギャップを無視するか、それを使用してより収益性の高いバリアントを構築するかのどちらかです。ここで、i 番目のギャップを使用したい場合は、重複しないギャップのセットを選択する必要があるため、現在のギャップの右境界が左境界より小さいギャップを使用できます。これを行うために、最初に右側の境界線でギャップを並べ替えたので、必要な位置を効率的に見つけることができるようになりました。これは可能であれば分析的に行うことができますが、一般的な場合は、ビンサーチを使用してギャップを見つけることが可能です。そのギャップの右境界は現在の境界の左境界より小さく、同時に可能な限り最大の境界になります。一。貪欲な理由から、適切な境界線を最大化したいと考えています。私が成長するにつれて、答えは増えるだけです。したがって、必要な位置 p を見つけて、dp[i] から dp[p] および i 番目の間隔を再計算します。