动态规划小结


一. 动态规划简介

动态规划是将一个大问题分解成连续的小问题,通过一步一步在上一个小问题的基础上解决下一个小问题,并将小问题的解连续存储,最终得到最终结果的方法。
一个大问题能够采用动态规划解决的前提是:动态规划智能应用于最优子结构的问题。最优子结构的意思是局部最优解能决定全局最优解。


二. 动态规划步骤

1.将问题拆解成各个小问题
2.建立小问题的状态转移方程
3.考虑状态转移方程不能cover的初始情况
4.用递归按顺序求解各个子问题
5.输出最终结果

三. 常见力扣习题分析

1.爬楼梯

1.1 Leetcode 70 爬楼梯

思路:
1.子问题是爬到第i阶有几种方法;
2.爬到第i阶的方法是【从第i-1阶往上爬1阶】或【从第i-2阶往上爬2阶】;
3.一般来说,这个问题得从第3阶开始,要初始化第1阶和第2阶。但是我们可以补充一个第0阶,从第0阶爬到第0阶只有一种办法 ,即不动,那么这个问题可以从第2阶开始递归,初始化第1阶。
4.从第2阶开始;
5.输出第n阶的结果
代码如下:

int climbStairs(int n) {
        if (n == 1) return 1; //初始化第1阶
        int p1 = 1, p2 = 1, temp = 0;
        for (int i = 2; i <= n; ++i) { // 从第2阶开始
            temp = p1 + p2; // 状态转移方程
            p1 = p2;
            p2 = temp;
        }
        return p2; // 输出第n阶的结果
    }

也可以用vector来存储每一步的结果。

int climbStairs(int n) {
	if (n <= 3) return n;
  vector<int> dp(n, 0);
  dp[0] = 1, dp[1] = 2;
  for (int i = 2; i < n; ++i) {
    dp[i] = dp[i - 1] + dp[i - 2];
  }
  return dp[n - 1];
}

1.2 剑指 Offer II 088. 爬楼梯的最少成本

int minCostClimbingStairs(vector<int>& cost) {
	 int n = cost.size();
	 if (n == 1) return cost[0];
	 if (n == 2) return min(cost[0], cost[1]);
	 vector<int> dp(n + 1, INT_MAX - 1);
	 for (int i = 2; i <= n; ++i) {
	 	dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]); // 站到本级台阶的花费不包括本级的cost,本级的cost是爬到下一层的cost。
	 }
	 return dp[n];
}

2.Leetcode 198 打家劫舍

思路:
1.子问题是偷到第i家有几种方法;
2.偷到第i家的方法是【偷到第i-1家】或【偷到第i-2家+第i家】的最大值;
3.一般来说,这个问题得从第3家开始,要初始化第1家和第2家。但是我们可以补充一个第0家,偷到第0家即偷了0个单位的现金,即不偷,那么这个问题可以从第2家开始递归,初始化第1家。
4.从第2家开始;
5.输出第n家的结果
代码如下:

int rob(vector<int>& nums) {
        int n = nums.size();
        if (n == 1) return nums[0];
        int p1 = 0, p2 = nums[0], temp = 0;
        for (int i = 1; i < n; ++i) {
            temp = max(p1 + nums[i], p2);
            p1 = p2;
            p2 = temp;
        }
        return p2;
    }

3.找子数组或子序列

3.1 或Leetcode 413 等差数列划分

思路:
0.让我们重新陈述一下这个问题:以其中【各个数】为结尾的等差数列一共有多少个;
1.子问题是【以第i个数为结尾的等差数列一共有多少个】;
2.当i 和i-1的差等于i-1和i-2的差时,【以第i个数为结尾的等差数列总和】是【以第i个数为结尾的等差数列总和】+1,否则为0;
3.这个问题得从第3个开始,要初始化第1家和第2家。
4.从第3个开始;
5.计算到第n个
6.计算这个数列之和
代码如下:

int numberOfArithmeticSlices(vector<int>& nums) {
        int n = nums.size();
        vector<int> dp(n, 0); // 初始化第1家和第2家为0
        for (int i = 2; i < n; ++i) {
            if (nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2]){
                dp[i] = dp[i - 1] + 1; // 当i 和i-1的差等于i-1和i-2的差时,【以第i个数为结尾的等差数列总和】是【以第i个数为结尾的等差数列总和】+1
            }
        }
        return accumulate(dp.begin(), dp.end(), 0); // 计算这个数列之和
    }

3.2 Leetcode 300 最长递增子序列

思路:
1.子问题是【到i的最长递增子序列的长度】;
2.【到i的最长递增子序列的长度】是各个【比i小的位置的最长递增子序列的长度】+1;
3.这个问题得从第2个开始,因为1就是1。
4.计算到最后1个。
代码如下:

int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        if (n == 1) return 1;
        vector<int> dp(n, 1);
        int ans = 1;
        for (int i = 1; i < n; ++i) { // 从第2个开始
            for (int j = 0; j < i; ++j) {
                if (nums[i] > nums[j]) {
                    dp[i] = max(dp[i], dp[j] + 1); // 【到i的最长递增子序列的长度】是各个【比i小的位置的最长递增子序列的长度】+1;
                    ans = max(ans, dp[i]);
                }
            }
        }
        return ans;
    }

4.Leetcode 64 最小路径和

思路:
1.子问题是【到grid[i][j]的最小路径和是多少】;
2.【到grid[i][j]的最小路径和】是【到grid[i-1][j]的最小路径和】与【到grid[i][j-1]的最小路径和】的最小值+当前位置的值;
3.一般来说,这个问题得从第1行第1列开始,要初始化第0行和第0列。但是这样代码复杂了,直接从第0行第0列开始。
4.计算到最后1个
代码如下:

int minPathSum(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        vector<int> dp(n, 0);
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (i + j == 0) {
                    dp[0] = grid[0][0];
                }
                else if (i == 0) dp[j] = dp[j - 1] + grid[i][j]; //第0行
                else if (j == 0) dp[j] = dp[j] + grid[i][j]; //第0列
                else dp[j] = min(dp[j], dp[j - 1]) + grid[i][j]; 从第1行第1列开始
            }
        }
        return dp[n - 1];
    }

5.Leetcode 221 最大正方形

思路:
1.子问题是【到grid[i][j]的最大正方形是多少】;
2.【到grid[i][j]的最大正方形】是【到grid[i-1][j]的最大正方形】与【到grid[i][j-1]的最大正方形】与【到grid[i][j-1]的最大正方形】的最小值+1;
3.一般来说,这个问题得从第1行第1列开始,要初始化第0行和第0列。但是这样代码复杂了,我们补充为0的一行一列,放在这个matrix的左上角,这样状态转移关系可以普遍适用。
4.计算到最后1个
代码如下:

int maximalSquare(vector<vector<char>>& matrix) {
        int m = matrix.size(), n = matrix[0].size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        int ans = 0;
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (matrix[i -1][j - 1] == '0') dp[i][j] = 0;
                else {
                    dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1;
                    ans = max(ans, dp[i][j]);
                }
            }
        }
        return ans*ans;
    }

6.Leetcode 279 完全平方数

思路:
1.子问题是【到i可以有几个完全平方数组成】;
2.【到i可以有几个完全平方数组成】是【到i-1可以有几个完全平方数组成】与【到i-4可以有几个完全平方数组成】与【到i-9可以有几个完全平方数组成】······的最小值+1;
3.这个问题得从第2个开始,因为1就是1。为了方便,我们多补充一个0,0不需要完全平方数组成。
4.计算到最后1个。
代码如下:

int numSquares(int n) {
        if (n == 1) return 1;
        vector<int> dp(n + 1, 100);
        dp[0] = 0, dp[1] = 1;
        for (int i = 2; i <= n; ++i) {
            for (int j = 1; j*j <= i; ++j) {
                dp[i] = min(dp[i], dp[i - j*j] + 1);
            }
        }
        return dp[n];
    }

9.Leetcode 416 分割等和子集

思路:
0.令sum为vector所有元素之和;
0.5.问题重述:【到最后1个元素时,是否能让部分和为sum/2】
1.子问题是【到第i个元素时,是否能让部分和为j】;
2.当第i个数大于j时,【到第i个元素时,是否能让部分和为j】=【到第i-1个元素时,是否能让部分和为j】,因为此时第i个数由于太大,不能放进去;当第i个数小于等于j时,【到第i个元素时,是否能让部分和为j】=【到第i-1个元素时,是否能让部分和为j】||【到第i-1个元素时,是否能让部分和为j-nums[i]】;
3.一般来说这个问题得从第1个开始。但是我们多补充一个0行0列,0行表示没有元素放进去,0列表示不需要放入元素,因此0列所有值先默认为true;
4.计算到最后1个。
代码如下:

bool canPartition(vector<int>& nums) {
        int n = nums.size(), sum = accumulate(nums.begin(), nums.end(), 0), me = *max_element(nums.begin(), nums.end());
        if (sum % 2) return false; // 若数组之和为奇数,肯定不行
        if (2 * me > sum) return false; // 若数组最大数的2倍超过总和,那么肯定也不行
        vector<vector<bool>> dp(n + 1, vector<bool>(sum / 2 + 1, false));
        dp[0][0] = true;
        for (int i = 1; i <= n; ++i) {
            for (int j = 0; j <= sum / 2; ++j) {
                if (j < nums[i - 1]) {
                    dp[i][j] = dp[i - 1][j];  // 当第i个数大于j时,【到第i个元素时,是否能让部分和为j】=【到第i-1个元素时,是否能让部分和为j】
                }
                else {
                    dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]]; // 当第i个数小于等于j时,【到第i个元素时,是否能让部分和为j】=【到第i-1个元素时,是否能让部分和为j】||【到第i-1个元素时,是否能让部分和为j-nums[i]】
                }
            }
        }
        return dp[n][sum / 2];
    }

9.Leetcode 338 比特位计数

思路:
1.子问题是【到i的为1的比特位有多少个】;
2.若i的二进制最后一位为1,则【到i的为1的比特位】是【到i的为1的比特位】+1;若i的二进制最后一位为0,则【到i的为1的比特位】是【到i>>1的为1的比特位】;
3.这个问题得从1个开始。
4.计算到最后1个。
代码如下:

if (n == 0) return vector<int>{0};
vector<int> ans(n + 1, 0);
for (int i = 0; i <= n; ++i) {
	if (i & 1 == 1) {
		ans[i] = 1 + ans[i - 1];
	}
	else {
		ans[i] = ans[i>>1];
	}
}
return ans;	

10.HJ 32 密码截取

思路:
1.子问题是【从i到j最长的对称字符串的长度】;
2.若第i个字符==第j个字符,且从第i+1个字符到第j-1个字符构成的字符串是对称的,那么【从i到j最长的对称字符串的长度】=【从i+1到j-1最长的对称字符串的长度】+2,否则【从i到j最长的对称字符串的长度】=max(【从i到j-1最长的对称字符串的长度】,【从i+1到j最长的对称字符串的长度】);
3.这个问题得从(0,0)个开始。
4.将整个二维数组遍历完成。
代码如下:

int main() {
    string s;
    cin >> s;
    int n = s.size();
    vector<vector<int>> dp(n, vector<int>(n, 0));
    for (int i = 0; i < n; ++i) {
        int r = 0, c = i;
        if (i == 0) {
            for (int j = 0; j < n; ++j) {
                dp[j][j] = 1;
            }
        } else {
            while (c < n) {
                if (s[r] == s[c] && dp[r + 1][c - 1] == c - r - 1) dp[r][c] = dp[r + 1][c - 1] + 2;
                else dp[r][c] = max(dp[r][c - 1], dp[r + 1][c]);
                r++; c++;
            }
        }
    }
    cout << dp[0][n - 1];
    return 0;
}

11. 回文子串

11.1 Leetcode 647 回文子串

子问题是【从i到j是不是回文子串】。

int n = s.size();
if (n == 1) return 1;
vector<vector<bool>> dp(n, vector<bool>(n, false));
int cnt = 0;
for (int i = 0; i < n; ++i) {
	dp[i][i] = true;
	cnt++;
}
for (int i = 1; i < n; ++i) {
	int l = 0. r = i;
	while (r < n) {
		if (s[l] == s[r] && (r - l == 1 || dp[l + 1][r - 1])) {
			dp[l][r] = true;
			cnt++
		}
	}
	l++;r++;
}
return cnt;

11.2 Leetcode 5 最长回文子串

	string longestPalindrome(string s) {
        int n = s.size(), maxl = 0, maxlen = 1;
        vector<vector<bool>> dp(n, vector<bool>(n, false));
        for (int i = 0; i < n; ++i) {
            for (int j = 0; i + j < n; ++j) {
                if (i == 0) dp[j][j] = true;
                else {
                    if (s[j] == s[i + j]) {
                        if (i == 1) {
                            dp[j][i + j] = true;
                        }
                        else dp[j][i + j] = dp[j + 1][i + j - 1];
                        if (dp[j][i + j] && maxlen < i + 1) {
                            maxl = j;
                            maxlen = i + 1;
                        }
                    }
                }
            }
        }
        return s.substr(maxl, maxlen);
    }

12. JZ46 把数字翻译成字符串

设字符串长度为n,子问题是【从0到n-1或n-2有几种翻译方法】。

int solve(string nums) {
    // write code here
    int n = nums.size();
    if (n == 0) return 1;
    if (n == 1) {
        if (nums[0] == '0') return 0;
        else return 1;
    }
    if (nums[n - 1] == '0') { // 必须要要和前面的结合,没有别的可能
        if (nums[n - 2] >= '3') return 0; // 结合的话前面是3-9,则说明超出范围了,返回0
        return solve(nums.substr(0, n - 2));
    } else {
        if (nums[n - 2] == '1' || nums[n - 2] == '2' && nums[n - 1] <= '6') { // 可以和前面一个数组组成新的可能
            return solve(nums.substr(0, n - 1)) + solve(nums.substr(0, n - 2));
        } else { // 不能和前面的组合,只能作为一个新的字母
            return solve(nums.substr(0, n - 1));
        }
    }
}

13.Leetcode 650 只有两个键的键盘

这一题的状态空间方程是dp[i] = dp[j] + dp[i/j],但是其实不需要用循环去求解每一个dp的值,只需要求需要的就可以了,所以可以用递归来做,如下:

int minSteps(int n) {
    if (n == 1) return 0;
    if (n <= 4) return n;
    for (int i = 2; i <n; ++i) {
        if (n % i == 0) return minSteps(n / i) + minSteps(i);
    }
    return n;
}

14.股票问题

14.1 Leetcode 121 买卖股票的最佳时机

int maxProfit(vector<int>& prices) {
   int n = prices.size(), minp = prices[0], ans = 0;
   vector<int> dp(n, 0);
   for (int i = 1; i < n; ++i) {
   	dp[i] = prices[i] - minp;
   	ans = max(dp[i], ans);
   	minp = min(minp, prices[i]);
   }
   return ans;
}

14.2 Leetcode 122 买卖股票的最佳时机II

这个变种是可以多次买卖

int maxProfit(vector<int>& prices) {
        int ans = 0;
        for (int i = 1; i < prices.size(); ++i) {
            if (prices[i] > prices[i - 1]) ans += prices[i] - prices[i - 1];
        }
        return ans;

    }

14.2 Leetcode 309 含冷却时间的最佳买卖股票时机

这一题要计算每天处于卖出状态时至当天的收益 和 每天处于持有状态时至当天的收益。
今天处于卖出状态时至今天的收益 = max(昨天处于卖出状态时至昨天的收益, 昨天处于持有状态时至昨天的收益+今天卖出的收益)
今天处于持有状态时至今天的收益 = max(昨天处于持有状态时至昨天的收益, 前天处于卖出状态时至前天的收益+今天买入的收益)

int maxProfit(vector<int>& prices) {
	int n = prices.size();
	vector<vector<int>> dp(n, vector<int>(2, 0)); 
	// 第一列是处于卖出状态时至当天的收益
	// 第二列是处于持有状态时至当天的收益
	dp[0][1] = -prices[0];
	for (int i = 1; i < n; ++i) {
		dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
		// 今天处于卖出状态时至今天的收益 = max(昨天处于卖出状态时至昨天的收益, 昨天处于持有状态时至昨天的收益+今天卖出的收益)
		dp[i][1] = max(dp[i - 1][1], dp[max(0, i - 2)][0] - prices[i]);
		// 今天处于持有状态时至今天的收益 = max(昨天处于持有状态时至昨天的收益, 前天处于卖出状态时至前天的收益+今天买入的收益)
	}
	return dp[n - 1][0]; // 题目条件是一定卖出了,所以返回第一列的值
	
}

14.Leetcode 322 找零钱

二维动态规划。

    int coinChange(vector<int>& coins, int amount) {
        if (amount == 0) return 0;
        int n = coins.size();
        vector<vector<int>> dp(amount + 1, vector<int>(n + 1, amount + 1));
        for (int i = 0; i <= amount; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (i == coins[j - 1]) dp[i][j] = 1; // 如果coins[j - 1]正好放在包里,一定是最小的方法,那么就放这1个
                else if (i < coins[j - 1]) dp[i][j] = dp[i][j - 1]; // 如果这个coin太大,就不放它
                else dp[i][j] = min(1 + dp[i- coins[j - 1]][j], dp[i][j - 1]); // 如果这个coin比价小,可以试试放进去,也可以不放进去
            }
        }
        if (dp[amount][n] == amount + 1) return -1;
        return dp[amount][n];
    }

15. Leetcode 22 括号生成

它的通项是如下的式子:
“(” + 【i=p时所有括号的排列组合】 + “)” + 【i=q时所有括号的排列组合】

    vector<string> generateParenthesis(int n) {
        if (n == 0) return vector<string>{""};
        if (n == 1) return vector<string>{"()"};
        if (n == 2) return vector<string>{"()()", "(())"};
        vector<string> ans;
        for (int i = 0; i <= n - 1; ++i) {
            auto vs1 = generateParenthesis(i), vs2 = generateParenthesis(n - 1 - i);
            int n1 = vs1.size(), n2 = vs2.size();
            for (int j = 0; j < n1; ++j) {
                for (int k = 0; k < n2; ++k) {
                    ans.push_back("(" + vs1[j] + ")" + vs2[k]);
                }
            }
        }
        return ans;

16. 不同路径

16.1 Leetcode 62 不同路径

    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (i + j == 2) dp[i][j] = 1;
                else dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        return dp[m][n];

    }

16.2 Leetcode 63 不同路径II

    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size(), n = obstacleGrid[0].size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (obstacleGrid[i - 1][j - 1]) continue;
                if (i + j == 2) dp[i][j] = 1;
                else {
                    if (i >= 2 && !obstacleGrid[i - 2][j - 1]) dp[i][j] += dp[i - 1][j]; // 要考虑障碍物
                    if (j >= 2 && !obstacleGrid[i - 1][j - 2]) dp[i][j] += dp[i][j - 1]; // 要考虑障碍物
                }
            }
        }
        return dp[m][n];
    }

17. Leetcode 96 不同的二叉搜索树

    int numTrees(int n) {
        if (n == 0) return 1;
        if (n <= 2) return n;
        int ans = 0;
        vector<int> dp(n + 1, 0);
        dp[0] = 1;
        dp[1] = 1;
        for (int i = 2; i <= n; ++i) {
            for (int j = 1; j <= i; ++j) dp[i] += dp[j - 1] * dp[i - j];
        }
        return dp[n];
    }

18. Leetcode 97 交错字符串

bool isInterleave(string s1, string s2, string s3) {
  int n1 = s1.size(), n2 = s2.size(), n3 = s3.size();
  if (n1 + n2 != n3) return false;
  vector<vector<bool>> dp(n1 + 1, vector<bool>(n2 + 1, false));
  for (int i = 0; i <= n1; ++i) {
    for (int j = 0; j <= n2; ++j) {
    	if (i == 0 && j == 0) {
    		dp[0][0] = true;
    		continue;
    	}
    	if (i > 0) dp[i][j] = dp[i][j] || (s3[i + j - 1] == s1[i - 1]) && dp[i - 1][j]; // 试试s1[i-1]和s3[i+j-1]相不相等
    	if (j > 0) dp[i][j] = dp[i][j] || (s3[i + j - 1] == s2[j - 1]) && dp[i][j - 1];
    }
  }
  return dp[n1][n2];
}

总结

刷完这些题之后,回过头来看看动态规划和分治法的区别:分治法将问题也拆分成了子问题,但是子问题之间互不影响,而动态规划的子问题具有连续性,若前面的不解决,后面的子问题也无法解决,子问题之前通过状态转移关系联系起来。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值