1. 程式人生 > 其它 >三道動態規劃題

三道動態規劃題

技術標籤:演算法演算法動態規劃

三道簡單的動態規劃題

第一題

有 m x n 個格子,機器人在最左上角的格子,星星在最右下角的格子。機器人只能向左和向下走。
問:機器人拿到星星,總共有多少種拿法?

如下圖:
在這裡插入圖片描述

思路:
問題其實就是從左上角走到右下角,有多少種走法。
如果倒過來看,站在右下角的角度,走到右下角,只有2種可能:
一是從右下角格子的左邊走來;二是從右下角格子的上方走來。
所以,假設走到右下角格子的左邊的格子有 x 種走法,走到右下角格子的上面的格子有 y 種走法,
那麼,走到星星的位置總共就是有 x + y 種走法。
想到這裡,自然會去想,走到左邊格子和走到上面格子分別有多少種走法呢,即 x 和 y 應怎麼計算呢?

當然仍然是由目標格子的左邊和上面的格子的數字加起來。
於是,這道題很自然的就變成了一道填表題:
假設目標格子的下邊是 u 和 v,那麼公式為:

array[u][v] = array[u-1][v] + array[u][v-1]

現在來考慮邊界條件:
u-1 和 v-1 都必須大於等於0,即,u和v都大於1;
那麼,當 u 或 v 為 0 的時候,array[u][v]是多少呢?
閱讀題目,自然而然地知道,當u為0或v為0,即第1排和第1列中的所有格子都應該填1,即到達這樣的格子只有一種走法。
於是,公式更新為:

當 u == 0 或 v == 0,
    array[u][v] = 1
當其他時候,
    array[u][v] = array[u-1][v] + array[u][v-1]

第一排和第一列先都填上1,然後其他格子就順著填就填出來了。右下角的格子自然最後也就填出來了。

程式如下:

class Solution {
public:
    int uniquePaths(int m, int n)
    {
        if (m == 1 || n == 1) return 1;

        int **array = new int * [m];
        for (int i=0; i<m; i++) {
            array[i] = new int [n];
        }

        for (int i=0;
i<n; i++) { array[0][i] = 1; } for (int i=0; i<m; i++) { array[i][0] = 1; } for (int i=1; i<m; i++) { for (int j=1; j<n; j++) { array[i][j] = array[i-1][j] + array[i][j-1]; } } int result = array[m-1][n-1]; for (int i=0; i<m; i++) { delete [] array[i]; } delete [] array; return result; } };

第二題

第2題和第1題很像。仍然是 m x n 個格子,機器人從左上角到右下角拿星星。
不同之處是, m x n 個格子中會放置若干個障礙物,遇到障礙物自然被阻擋不能前進。
問:機器人拿到星星有多少種方法?

如下圖:
在這裡插入圖片描述

思路:
首先,要想一下,障礙物如何表示。這很簡單,令有障礙物的格子的數字為 -1 即可。
然後,我們要想一下 array[u][v] 的公式該怎麼改寫了。
假設目標格子的上面是障礙物,那麼 array[u][v] = array[u-1][v] + 0;
假設目標格子的左邊是障礙物,那麼 array[u][v] = array[u][v-1] + 0;
假設上面和左邊都是障礙物,那麼自然 array[u][v] = 0

現在,要開始考慮令 u-1 < 0 和 v-1 < 0 的情況,即第一排和第一列,該如何初始化?
上一題中,第一排和第一列都是初始化為 1 的;但是現在有障礙物之後,就不能都初始化為 1 了。

  • 在第一排和第一列中,如果沒有障礙物的時候,就初始化為1;
  • 一旦出現了一個障礙物,這個障礙物之後的所有的位置都是到達不了的,這時候就初始化為0即可。
    最後,要注意一下邊界條件。即起點和終點也可能放障礙物,這樣的話,則相當於永遠無法拿到星星。

初始化完第一排和第一列之後,剩下的所有格子就依然用填表法即可解決。程式碼如下:

class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid)
{
    if (obstacleGrid[0][0] == 1) return 0;
    
    int m = obstacleGrid.size();
    int n = obstacleGrid[0].size();
    
    if (obstacleGrid[m-1][n-1] == 1) return 0;
    if (m==1 && n==1) return 1;
    
    for (int i=0; i<m; i++) {
        for (int j=0; j<n; j++) {
            if (obstacleGrid[i][j] == 1) {
                obstacleGrid[i][j] = -1;
            }
        }
    }
    
    for (int j=1; j<n; j++) {
        if (obstacleGrid[0][j] == -1) {
            for (int k=j; k<n; k++) {
                obstacleGrid[0][k] = 0;
            }
            break;
        }
        else {
            obstacleGrid[0][j] = 1;
        }
    }
    
    for (int i=1; i<m; i++) {
        if(obstacleGrid[i][0] == -1) {
            for (int k=i; k<m; k++) {
                obstacleGrid[k][0] = 0;
            }
            break;
        }
        else {
            obstacleGrid[i][0] = 1;
        }
    }
    
    for (int i=1; i<m; i++) {
        for (int j=1; j<n; j++) {
            if (obstacleGrid[i][j] == -1) continue;
            
            int up = obstacleGrid[i-1][j] < 0 ? 0 : obstacleGrid[i-1][j];
            int left = obstacleGrid[i][j-1] < 0 ? 0 : obstacleGrid[i][j-1];
            obstacleGrid[i][j] = up + left;
        }
    }
    
    return obstacleGrid[m-1][n-1];
}
};

第三題

有一個 m x n 的格子矩陣。裡面填滿了非負整數。要求:從左上角到右下角找一條路徑,使得這條路徑上所有的數字相加之和最小。

如下圖:
在這裡插入圖片描述

思考:
這道題最直觀的思考,很可能是這樣的:
左上角的格子就像是一棵二叉樹的root,它的左兒子是下面的格子,右兒子是右邊的格子。
然後左兒子和右兒子又分別有自己的左右子樹。這就構成了一棵龐大的二叉樹。
再然後就是遍歷這棵二叉樹中能到達右下角葉子節點的所有路徑,找出其中的最小值。

讀者可以畫一畫,這棵二叉樹中有太多冗餘的部分。所以生成這顆二叉樹再計算肯定不是正解。
為了避免生成冗餘的部分,就要生成一種有向無環圖。當這個有向無環圖生成完畢之後,還要遍歷以找到最小值。這2個過程都是比較複雜的,而且也比較耗時。

有了之前的2題做啟發,這道題也倒過來想呢?

  • 右下角格子上面的這個格子,它到右下角格子的路徑和是什麼呢?就是這2個格子的數字之和。
  • 右下角格子的左邊的格子,它到右下角格子的路徑和是什麼呢?也就是這2個格子的數字之和。
  • 那麼, 座標為 (m-2, n-2) 這個位置的格子,即右下角格子的左上方格子到右下角格子的最小路徑和是什麼呢?
    答案就是,它本身的數字,加上 min(它右邊格子的路徑和,它下面格子的路徑和)

好了,想到這裡,我們忽然發現,這道題也是可以用填表法來填的啊!只不過要倒著推。

  • 最後一排和最後一列上的每個格子到終點格子(即右下角格子)的路徑是唯一的,
    所以最後一排和最後一列的每個格子到終點格子的最小路徑值可以直接計算出來;
  • 依託於最後一排和最後一列的最小路徑值,由右下角向左上角出發,
    即由下向上,由右往左,依次計算出每個格子的最小路徑值;最後就自然地計算出了左上角位置的最小路徑值了。

程式碼如下:

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid)
    {
        int m = grid.size();
        if (m == 0) return 0;
        int n = grid[0].size();
        
        if (m == 1 && n == 1) return grid[0][0];
        
        vector<vector<int>> result;
        result.reserve(m);
        for (int i=0; i<m; i++) {
            vector<int> vecRow;
            vecRow.reserve(n);
            for (int j=0; j<n; j++) {
                vecRow.push_back(-1);
            }
            result.push_back(vecRow);
        }
        
        result[m-1][n-1] = grid[m-1][n-1];
        
        for (int col=n-2; col>=0; col--) {
            result[m-1][col] = grid[m-1][col] + result[m-1][col+1];
        }
        for (int row=m-2; row>=0; row--) {
            result[row][n-1] = grid[row][n-1] + result[row+1][n-1];
        }
       
        for (int i = m-2; i >= 0; i--) {
            for (int j = n-2; j >= 0; j--) {
                result[i][j] = grid[i][j] + min(result[i][j+1], result[i+1][j]);
            }
        }
        
        return result[0][0];
    }
};

(完)