【アルゴリズムの問題】動的計画中間段階の異なるパス、最小パスと合計

序文

動的プログラミング (Dynamic Programming、略して DP) は、多段階の意思決定プロセスの最適化問題を解く手法です。これは、複雑な問題を重複する部分問題に分解し、各部分問題の最適解を維持することで問題の最適解を導き出す戦略です。

動的プログラミングの主なアイデアは、解決された部分問題の最適解を使用して、より大きな問題の最適解を導き出し、計算の繰り返しを回避することです。したがって、動的プログラミングでは、通常、ボトムアップ アプローチを使用して最初に小規模な問題を解決し、次に問題全体の最適な解決策が解決されるまで、徐々に規模の大きな問題を導き出します。

動的プログラミングには通常、次の基本的な手順が含まれます。

  1. 状態を定義する: 問題をいくつかのサブ問題に分割し、サブ問題の解決策を表す状態を定義します。
  2. 状態遷移方程式を定義する: 部分問題間の関係に従って、状態遷移方程式、つまり既知の状態から未知の状態の計算プロセスを推定する方法を設計します。
  3. 初期状態を決定します。最小の部分問題の解を定義します。
  4. ボトムアップ解: 状態遷移方程式に従ってすべての状態の最適解を計算します。
  5. 最適解から問題の解決策を構築します。

動的プログラミングは、最短経路問題、ナップザック問題、最長共通部分列問題、編集距離問題など、多くの実際的な問題を解決できます。同時に、動的計画法は、分割統治アルゴリズムや貪欲アルゴリズムなど、他の多くのアルゴリズムの中核となるアイデアでもあります。

動的計画法は、多段階の意思決定プロセスの最適化問題を解く手法であり、複雑な問題を重複する部分問題に分解し、各部分問題の最適解を維持することで問題の最適解を導き出します。動的プログラミングには、状態の定義、状態遷移方程式の設計、初期状態の決定、ボトムアップ解、問題解の構築などのステップが含まれます。動的プログラミングは多くの実際的な問題を解決でき、他のアルゴリズムの中核となるアイデアの 1 つでもあります。

1. 異なるパス

ロボットは、mxn グリッドの左上隅に配置されます (下図では開始点に「開始」とマークされています)。

ロボットは一度に 1 ステップ下または右へのみ移動できます。ロボットはグリッドの右下隅 (下の画像で「終了」とマークされている) に到達しようとします。

異なるパスは合計で何通りありますか?

ここに画像の説明を挿入

例 1:

入力: m = 3、n = 7
出力: 28

例 2:

入力: m = 3、n = 2
出力: 3
説明:
左上隅から開始して、右下隅に到達するまでに合計 3 つのパスがあります。

  1. 右→下→下
  2. 下 -> 下 -> 右
  3. 下→右→下

例 3:

入力: m = 7、n = 3
出力: 28

例 4:

入力: m = 3、n = 3
出力: 6

出典: LeetCode。

1.1. アイデア

dp[i][j] を i、j への最も多くのパスとする

動的方程式: dp[i][j] = dp[i-1][j] + dp[i][j-1]

最初の行 dp[0][j] または最初の列 dp[i][0] については、すべて境界上にあるため、1 のみになることに注意してください。

時間計算量: O(m∗n)

空間複雑さ: O(m∗n)

最適化: 毎回必要なのは dp[i-1][j]、dp[i][j-1] のみであるため、これら 2 つの数値のみを記録する必要があります。

1.2. コードの実装

class Solution {
    
    
public:
    int uniquePaths(int m, int n) {
    
    
        long long ans = 1;
        for (int x = n, y = 1; y < m; ++x, ++y) {
    
    
            ans = ans * x / y;
        }
        return ans;
    }
};

2. 異なるパス II

ロボットは、mxn グリッドの左上隅に配置されます (下図では開始点に「開始」とマークされています)。

ロボットは一度に 1 ステップ下または右へのみ移動できます。ロボットはグリッドの右下隅 (下の画像で「終了」とマークされている) に到達しようとします。

ここで、グリッド内に障害物があると考えてみましょう。では、左上から右下までの異なるパスは何通りあるでしょうか?

グリッド内の障害物と空き位置は、それぞれ 1 と 0 で表されます。

例 1:

入力:obstructionGrid = [[0,0,0],[0,1,0],[0,0,0]]
出力:2
説明:3x3 グリッドの中央に障害物があります。
左上隅から右下隅までの 2 つの異なるパスがあります。

  1. 右 -> 右 -> 下 -> 下
  2. 下 -> 下 -> 右 -> 右
    ここに画像の説明を挿入

例 2:

入力:obstructionGrid = [[0,1],[0,0]]
出力:1
ここに画像の説明を挿入

2.1. アイデア

再帰的なアイデア:
右下隅に到達する方法の数を f(m,n) として定義するとします。右下隅は上または左のグリッドによってのみ通過できるため、再帰的なコードを書くのは簡単です。解、つまり f (m,n)=f(m−1,n)+f(m,n−1) を計算し、最後に再帰終了条件を追加すれば完了です。

しかし、これで終わりではありません~ このボトムアップ再帰では多くの繰り返し計算が行われるため、2次元配列、つまり dp[i,j] のトップダウン再帰として書き直すことができます。 =dp[i−1,j]+dp[i,j−1]。

1. 状態定義: dp[i][j] はグリッド (i, j) に行く方法の数を表します。

2. 状態遷移: (i, j) に障害物がある場合、dp[i][j] の値は 0 で、このグリッドに行く方法の数が 0 であることを示します。そうでない場合は、グリッド (i , j) はグリッド (i−1,j) から移動することも、グリッド (i,j−1) に来ることもできるので、グリッドに行く方法はグリッド (i−1,j) に行くことと、 Grid (i,j−1) メソッドの数の合計、つまり、dp[i,j]=dp[i−1,j]+dp[i,j−1]。
状態遷移方程式は次のとおりです。
ここに画像の説明を挿入
3. 初期条件:

  • 最初の列のグリッドはその上のグリッドのみを通過できるため、dp[i][0] の初期値は 1 で、障害物がある場合は 0 になります。
  • 1 行目のグリッドは左側のグリッドからのみ移動できるため、dp[0][j] の初期値は 1 で、障害物がある場合は 0 になります。

2.2. コードの実装

class Solution {
    
    
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
    
    
        int n = obstacleGrid.size(), m = obstacleGrid.at(0).size();
        vector <int> f(m);

        f[0] = (obstacleGrid[0][0] == 0);
        for (int i = 0; i < n; ++i) {
    
    
            for (int j = 0; j < m; ++j) {
    
    
                if (obstacleGrid[i][j] == 1) {
    
    
                    f[j] = 0;
                    continue;
                }
                if (j - 1 >= 0 && obstacleGrid[i][j - 1] == 0) {
    
    
                    f[j] += f[j - 1];
                }
            }
        }

        return f.back();
    }
};

時間計算量: O(nm)、n はグリッドの行数、m はグリッドの列数です。すべてのグリッドを 1 回繰り返すだけで済みます。

空間の複雑さ: O(m)。ローリング配列最適化を使用すると、現在の行の f の値を記録するために O(m) スペースのみを使用できます。

3. 最小パスと

非負の整数の mxn グリッドが与えられた場合、パス上の数値の合計を最小にする左上隅から右下隅までのパスを見つけます。

注: 一度に下または右に 1 ステップずつのみ移動してください。

例 1:

入力: Grid = [[1,3,1],[1,5,1],[4,2,1]]
出力: 7
説明: パス 1→3→1→1→1 の合計は一番小さい。
ここに画像の説明を挿入

例 2:

入力: グリッド = [[1,2,3],[4,5,6]]
出力: 12

出典: LeetCode。

3.1. アイデア

状態定義: dp をサイズ m×n の行列とします。ここで、dp[i][j] の値は (i,j) までの最小パス合計を表します。

パスの方向は下または右のみであるため、グリッドの最初の行の各要素は左上の要素から右へのみ移動でき、グリッドの最初の列の各要素には到達できます。左上から 角の要素が下に移動し始めますが、このときのパスは一意であるため、各要素に対応するパスの合計の最小値は、対応するパス上の数値の合計となります。

1 行 1 列目にない要素については、その上に隣接する要素から 1 ステップ下に移動するか、左側の隣接要素から 1 ステップ右に移動できます。は、その上の隣接する要素と、左側の 2 つの隣接する要素に対応する最小パス合計の最小値に現在の要素の値を加えた要素に等しい。各要素に対応する最小パスは、隣接する要素に対応する最小パスの和に関係するため、動的計画法を使用して解くことができます。

元のグリッドと同じサイズの 2 次元配列 dp を作成します。 dpdp[i][j] は、左上隅から位置 (i,j) までの最小パス合計を表します。明らかに、dp[0][0]=grid[0][0]です。dp内の残りの要素については、以下の状態遷移方程式により要素値が計算されます。

i>0 かつ j=0 の場合、dp[i][0] = dp[i−1][0]+grid[i][0]。

i=0、j>0の場合、dp[0][j]=dp[0][j−1]+grid[0][j]となります。

i>0かつj>0の場合、dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i][j]。

最後に、dp[m-1][n-1] の値は、グリッドの左上隅からグリッドの右下隅までの最小パス合計です。

3.3. コードの実装

class Solution {
    
    
public:
    int minPathSum(vector<vector<int>>& grid) {
    
    
        if (grid.size() == 0 || grid[0].size() == 0) {
    
    
            return 0;
        }
        int rows = grid.size(), columns = grid[0].size();
        auto dp = vector < vector <int> > (rows, vector <int> (columns));
        dp[0][0] = grid[0][0];
        for (int i = 1; i < rows; i++) {
    
    
            dp[i][0] = dp[i - 1][0] + grid[i][0];
        }
        for (int j = 1; j < columns; j++) {
    
    
            dp[0][j] = dp[0][j - 1] + grid[0][j];
        }
        for (int i = 1; i < rows; i++) {
    
    
            for (int j = 1; j < columns; j++) {
    
    
                dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
            }
        }
        return dp[rows - 1][columns - 1];
    }
};

時間計算量: O(mn)。

空間の複雑さ: O(mn)。

要約する

動的計画法(Dynamic Programming)は、多段階の意思決定最適化問題を解く手法であり、複雑な問題を重複する部分問題に分解し、各部分問題の最適解を維持することで問題の最適解を導き出します。動的プログラミングは、最短経路問題、ナップザック問題、最長共通部分列問題、編集距離問題など、多くの実際的な問題を解決できます。

動的プログラミングの基本的な考え方は、解決された部分問題の最適解を使用して、より大きな問題の最適解を導き出し、計算の繰り返しを回避することです。通常、ボトムアップのアプローチを使用して、最初に小規模な問題を解決し、次に問題全体の最適な解決策が解決されるまで、徐々に大規模な問題を導き出します。

動的プログラミングには通常、次の基本的な手順が含まれます。

  1. 状態を定義する: 問題をいくつかのサブ問題に分割し、サブ問題の解決策を表す状態を定義します。
  2. 状態遷移方程式を定義する: 部分問題間の関係に従って、状態遷移方程式、つまり既知の状態から未知の状態の計算プロセスを推定する方法を設計します。
  3. 初期状態を決定します。最小の部分問題の解を定義します。
  4. ボトムアップ解: 状態遷移方程式に従ってすべての状態の最適解を計算します。
  5. 最適解から問題の解決策を構築します。

動的計画法の時間計算量は通常O ( n 2 ) O(n^2)です。O ( n2) O ( n 3 ) O(n^3) O ( n3 ) の場合、空間複雑度は O(n) です。ここで、n は問題の規模を表します。実際のアプリケーションでは、空間の複雑さを軽減するために、通常、ローリング アレイなどの手法を使用して動的プログラミング アルゴリズムを最適化できます。

ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/Long_xu/article/details/131464877