動態規劃解題算法2

題目1:不同的二叉搜索樹

給定一個整數 n,求以 1 … n 爲節點組成的二叉搜索樹有多少種?

示例:
輸入: 3
輸出: 5
解釋:
給定 n = 3, 一共有 5 種不同結構的二叉搜索樹:

   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 \
   2     1         2                 3

解題思路:動態規劃,當 n = 1的時候有 1 中,等於 2 的時候有兩種,分別是以 1 爲根節點和以 2 爲根節點;

dp[i] 表示節點數爲 i 的搜索二叉樹有 dp[i] 種

dp[i] = dp[j-1] * dp[i-j] (j爲當前的根節點)

dp[j-1] 爲左子樹的種類,dp[i-j] 爲右子樹的種類

C++ 參考代碼

class Solution {
public:
    int numTrees(int n) {
        vector<int>dp(n+1,0);
        dp[0] = 1,dp[1] = 1;
        // 當前節點總數
        for(int i = 2;i<n+1;++i){
            // 以 j 爲根節點
            for(int j =1;j<i+1;++j){
                // 左節點 * 右節點數
                dp[i] += dp[j-1]*dp[i-j];
            }
        }
        return dp[n];
    }
};

題目2:不同的子序列

給定一個字符串 S 和一個字符串 T,計算在 S 的子序列中 T 出現的個數。

一個字符串的一個子序列是指,通過刪除一些(也可以不刪除)字符且不干擾剩餘字符相對位置所組成的新字符串。(例如,“ACE” 是 “ABCDE” 的一個子序列,而 “AEC” 不是)

示例 1:
輸入: S = “rabbbit”, T = “rabbit”
輸出: 3
解釋:

如下圖所示, 有 3 種可以從 S 中得到 “rabbit” 的方案
(上箭頭符號 ^ 表示選取的字母)

rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^

解題思路:動態規劃

步驟1:初始化,當字符串 T 字符串爲空的時候,因爲空串是任何字符串的一個子集,所以在 S 字符串出現的次數是 1

dp[i][j] 代表 S [0-i] 字符串可以由 T[0-j] 字符串組成最多個數

步驟2:動態轉義方程

在這裏插入圖片描述
如果 T[j-1] == S[i-1] 的時候,需要檢測 T 的前一個匹配數量 + T 當前的匹配數量

在這裏插入圖片描述
如果 j > i 代表 T 的長度大於 S ,則也沒有必要尋找了,因爲肯定找不到,給 dp[i][j] 賦值爲 0 或者直接跳過
在這裏插入圖片描述
C++ 參考代碼

class Solution {
public:
    int numDistinct(string s, string t) {
        int m = s.size();
        int n = t.size();
        vector<vector<long>>dp(m+1,vector<long>(n+1));
        for(int i = 0;i<=m;++i){
            dp[i][0] = 1;
        }
        // 如果 S 爲空字符串的話, 讓 T 字符串在 S 中尋找,
        // 肯定是找不到的,所以應該初始化爲0,我們在定義的時候
        // 已經初始化爲0了,所以這一步我們不操作

        // dp[i][j] 代表 S [0-i] 字符串可以由 T[0-j] 字符串
        for(int i = 1;i<=m;++i){
            for(int j = 1;j<=n;++j){
                // 沒必要在找了,因爲 T 的長度大於 S ,肯定找不到
                if(j>i) continue;
                
                // 下面是最難找的動態方程,用圖來說明一下
                if(s[i-1] == t[j-1]){
                    dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
                }else{
                    dp[i][j] = dp[i-1][j]; 
                }
            }
        }
        return dp[m][n];
    }
};

題目3:交錯字符串

給定三個字符串 s1, s2, s3, 驗證 s3 是否是由 s1 和 s2 交錯組成的

示例 1:
輸入: s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbcbcac”
輸出: true

示例 2:
輸入: s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbbaccc”
輸出: false

解題思路:動態規劃,s1 和 s2 的某個前綴是否組成 s3 的前綴

dp[i][j] 代表 s1 的前 i 個字符和 s2 的前 j 個字符是否組成 s3 的前 k 個字符(k = i + j + 1)

初始化,如果 s2 爲空串,則要滿足題必須 s1[i] == s3[i],同理若 s1 爲空串,則 s2[j] == s3[j]

在這裏插入圖片描述

class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        if(s1.size() + s2.size() != s3.size()){
            return false;
        }
        int n1 = s1.size();
        int n2 = s2.size();
        vector<vector<bool>>dp(n1+1,vector<bool>(n2+1));
        // dp[i][j] 代表 s1[0~i-1] 和 s2[0~j-1]可以交錯爲s3[0~i+j-1]
        dp[0][0] = true;
        // 如果 s2 爲空串,則要想可以交錯成功
        // 下標對應的字符必須相等,也就是 s1[i] == s2[i]
        // 只要有一個字符不相等,就無法成功
        for(int i = 1;i<=n1;++i){
            dp[i][0] = dp[i-1][0] && (s1[i-1] == s3[i-1]);
        }
        // s1 同上
        for(int i = 1;i<=n2;++i){
            dp[0][i] = dp[0][i-1] && (s2[i-1] == s3[i-1]);
        }
        for(int i = 1;i<=n1;++i){
            for(int j = 1;j<=n2;++j){
                dp[i][j] = (dp[i-1][j] && s1[i-1] ==s3[i-1+j])                  || (dp[i][j-1] && s3[i-1+j] ==s2[j-1]);
            }
        }  
        return dp[n1][n2];
    }
};

題目4:分割回文串

給定一個字符串 s,將 s 分割成一些子串,使每個子串都是迴文串。

返回符合要求的最少分割次數。

示例:
輸入: “aab”
輸出: 1
解釋: 進行一次分割就可將 s 分割成 [“aa”,“b”] 這樣兩個迴文子串

解題思路:動態規劃

dp[i] 代表到第 i 個字符爲止,最少切割的次數
如果我們可以保證第 j 到 i-1 的字符是迴文串,那麼只要再切割一次就可以保證 j 到 i 個字符是迴文串

初始化最多的切割次數 dp[i] = i - 1 ,例如有 5 個字符,最多需要 4 次切割

方程爲:dp[i] = min(dp[i],d[j]+1)

判斷第 j 到 i-1 是否是迴文串,也可以使用動態規劃的方式去判斷,從後往前遍歷

情況1:如果 j == i ,則代表的是一個字符,那麼它一定是迴文串

情況2:如果 j == i+1 ,表示 j 到 i +1 的字符,如果字符 s[i+1] == s[j] 相等,那麼就是一個迴文串,否則不是

情況3:如果 s[i] == s[j],如果去掉首位字符它依然是迴文串,那麼第 j 到 i 的字符串就是迴文串

C++ 參考代碼

class Solution {
public:
    int minCut(string s) {
        if(s.empty()) return 0;
		int n = s.size();
        vector<int>dp(n+1,0);
        // dp[i] 表示到第 i 個字符需要的最小分割次數
        for(int i = 0;i<=n;++i){
            dp[i] = i-1;
        } 
        // 這裏已經判斷好了第 j 到 i-1 個字符是否是迴文串
        vector<vector<bool>>mat = ispalindrome(s);
        for(int i = 1;i<=n;++i){
            for(int j = 0;j<i;++j){
                // dp(i) = min(dp(i),1+dp(j)) 
                if(mat[j][i-1]){
                    dp[i] = min(dp[i],1+dp[j]);
                }
            }
        }
		return dp[n];
    }
    // 判斷標記第 j 到 i-1 是否是迴文串
    vector<vector<bool>>ispalindrome(string s){
        int len = s.size();
        vector<vector<bool>>mat(len,vector<bool>(len,false));
        for(int i = len-1;i>=0;--i){
            for(int j = i;j<len;++j){
                if(j == i){
                    mat[i][j] = true;
                }
                else if(j == i+1){
                    mat[i][j] = (s[i]==s[j]);
                }else{
                    mat[i][j] = ((s[i]==s[j]) && mat[i+1][j-1]);
                }
            }
        }
        return mat;
    }
};

寫法二:

class Solution {
public:
    int minCut(string s) {
		int n = s.size();
		vector<vector<bool>>boolean(n,vector<bool>(n));
        vector<int>dp(n); 
        // dp[i]表示s中第i個字符到第(n-1)個字符
        // 所構成的子串的最小分割次數
		for (int i = n - 1; i >= 0; i--) {
			dp[i] = INT_MAX;
			for (int j = i; j < n; j++) {
				/// 滿足這個條件,說明 i 到 j 爲迴文串
				if (s[i] == s[j] && (j - i <= 1 || boolean[i + 1][j - 1])) {
					boolean[i][j] = true;
					// 尋找最小的切割點 j 對於的次數
					if (j + 1 < n) {
						dp[i] = min(dp[i], 1 + dp[j + 1]);
					}else{
						// j == n-1 的時候,說明整個字符串是一個迴文串
						dp[i] = 0;
					}
				}
			}
		}
		return dp[0];
    }
};

題目:地下城遊戲

一些惡魔抓住了公主(P)並將她關在了地下城的右下角。地下城是由 M x N 個房間組成的二維網格。我們英勇的騎士(K)最初被安置在左上角的房間裏,他必須穿過地下城並通過對抗惡魔來拯救公主

騎士的初始健康點數爲一個正整數。如果他的健康點數在某一時刻降至 0 或以下,他會立即死亡

在這裏插入圖片描述有些房間由惡魔守衛,因此騎士在進入這些房間時會失去健康點數(若房間裏的值爲負整數,則表示騎士將損失健康點數);其他房間要麼是空的(房間裏的值爲 0),要麼包含增加騎士健康點數的魔法球(若房間裏的值爲正整數,則表示騎士將增加健康點數)

爲了儘快到達公主,騎士決定每次只向右或向下移動一步

解題思路:動態規劃,從右到左,從下當上,定義 dp[i][j] 爲勇士到達點(i,j) 的最小生命值

class Solution {
public:
    int calculateMinimumHP(vector<vector<int>>& dungeon) {
        int n = dungeon.size();
        int m = dungeon[0].size();
        // dp[i][j] 表示到達(i,j) 位置最小生命值
        vector<vector<int>> dp(n, vector<int>(m));         
        for (int i = n-1; i >= 0; --i) {
            for (int j = m-1; j >= 0; --j) {
                if(i == n-1 && j==m-1){
                    dp[i][j] = max(1,1-dungeon[i][j]);
                }
                else if(i== n-1){
                    dp[i][j] = max(1,dp[i][j+1]-dungeon[i][j]);
                }
                else if(j == m-1){
                    dp[i][j] = max(1,dp[i+1][j]-dungeon[i][j]);
                }else{
                    dp[i][j] = max(1,min(dp[i+1][j],dp[i][j+1])-dungeon[i][j]);
                }
            }
        }
        return dp[0][0];
    }
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章