动态规划五部曲
确定dp数组(dp table)以及下标的含义
确定递推公式
dp数组如何初始化
确定遍历顺序
举例推导dp数组
先确定递推公式再确定如何对dp数组初始化
一维动态规划 斐波那契数列
1 2 3 4 5 6 7 8 9 10 11 class Solution {public : int fib (int n) { int res[3 ]; res[0 ] = 0 ;res[1 ] = 1 ;res[2 ] = 0 ; for (int i = 2 ;i <= n;i++){ res[i%3 ] = (res[(i-1 )%3 ] + res[(i-2 )%3 ])%1000000007 ; } return res[n%3 ]; } };
上台阶问题本质也是斐波那契数列只不过更改了初始值而已。
1 2 3 4 5 6 7 8 9 10 11 class Solution {public : int numWays (int n) { int res[3 ]; res[0 ] = 1 ;res[1 ] = 1 ; for (int i = 2 ;i <= n;i++){ res[i%3 ] = (res[(i-1 )%3 ] + res[(i-2 )%3 ])%1000000007 ; } return res[n%3 ]; } };
打家劫舍
定义一个数组 dp, dp[i] 表示抢劫到第 i 个房子时,可以抢劫的最大数量。我们考虑 dp[i],此时可以抢劫的最大数量有两种可能,一种是我们选择不抢劫这个房子,此时累计的金额即为dp[i-1];另一种是我们选择抢劫这个房子,那么此前累计的最大金额只能是 dp[i-2],因为我们不能够抢劫第 i-1 个房子,否则会触发警报机关。因此本题的状态转移方程为 dp[i] = max(dp[i-1],nums[i-1] + dp[i-2])。
另外注意值得初始化,dp[0]=nums[0]; dp[1]=max(nums[0],nums[1]);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution {public : int rob (vector<int >& nums) { int size = nums.size (); vector<int >dp (size,0 ); dp[0 ] = nums[0 ]; if (size == 1 )return dp[0 ]; else dp[1 ] = max (nums[0 ],nums[1 ]); for (int i =2 ;i < size;i++){ dp[i] = max (dp[i - 1 ],dp[i - 2 ]+nums[i]); } return dp[size - 1 ]; } };
等差数列划分
这道题略微特殊,因为要求是等差数列,可以很自然的想到子数组必定满足 num[i] - num[i-1]= num[i-1] - num[i-2]。然而由于我们对于 dp 数组的定义通常为以 i 结尾的,满足某些条件的子数组数量,而等差子数组可以在任意一个位置终结,因此此题在最后需要对 dp 数组求和。
开始我没想到dp数组求和,结果写了一个比较复杂的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Solution {public : int lenArith = 1 ; int numberOfArithmeticSlices (vector<int >& nums) { int len = nums.size (); if (len <= 2 )return 0 ; vector<int >dp (len,0 ); for (int i = 2 ;i < len;i++){ if (isArithmetic (nums, i)){ dp[i] = dp[i - 1 ] + lenArith++; } else { dp[i] = dp[i - 1 ]; lenArith = 1 ; } } int max = -1 ; for (int i = 0 ;i < len;i++){ if (max < dp[i])max =dp[i]; } return max; } bool isArithmetic (vector<int >&nums,int i) { if (nums[i]+nums[i-2 ]==2 *nums[i-1 ]){ return true ; } else return false ; } };
其实还可以这样,简单易懂
1 2 3 4 5 6 7 8 9 10 11 int numberOfArithmeticSlices (vector<int >& nums) { int n = nums.size (); if (n < 3 ) return 0 ; vector<int > dp (n, 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 ; } } return accumulate (dp.begin (), dp.end (), 0 ); }
连续子数组的最大和
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution {public : int maxSubArray (vector<int >& nums) { int size = nums.size (); vector<int >dp (size + 1 ,0 ); int maxn = -100 ; for (int i = 0 ;i < size;i++){ dp[i+1 ] = max (dp[i] + nums[i],0 + nums[i]); maxn = max (maxn,dp[i+1 ]); } return maxn; } };
n个骰子的点数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution {public : vector<double > dicesProbability (int n) { vector<double >dp (6 ,1.0 /6.0 ); for (int i = 2 ;i <= n;i++){ vector<double >tmp (6 * i - i + 1 ); for (int k = 0 ;k < 6 ;k++){ for (int j = 0 ;j < dp.size ();j++){ tmp[k+j] += dp[j] * 1.0 /6.0 ; } } dp = tmp; } return dp; } };
给你一个整数 n
,对于 0 <= i <= n
中的每个 i
,计算其二进制表示中 1
的个数 ,返回一个长度为 n + 1
的数组 ans
作为答案。
示例 1:
1 2 3 4 5 6 输入:n = 2 输出:[0,1,1] 解释: 0 --> 0 1 --> 1 2 --> 10
示例 2:
1 2 3 4 5 6 7 8 9 输入:n = 5 输出:[0,1,1,2,1,2] 解释: 0 --> 0 1 --> 1 2 --> 10 3 --> 11 4 --> 100 5 --> 101
二维动态规划 最小路径和
我们可以定义一个同样是二维的 dp 数组,其中 dp[i][j] 表示从左上角开始到 (i, j) 位置的最优路径的数字和。因为每次只能向下或者向右移动,我们可以很容易得到状态转移方程 dp[i][j] =min(dp[i-1][j], dp[i][j-1]) + grid[i][j],其中 grid 表示原数组。
这道题稍微注意一下初始化,不是很难。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution {public : int minPathSum (vector<vector<int >>& grid) { int m = mat.size (),n = mat[0 ].size (); vector<vector<int >>dp (m,vector<int >(n,0 )); for (int i = 1 ;i < n;i++)dp[0 ][i] = dp[0 ][i-1 ] + grid[0 ][i]; for (int i = 1 ;i < m;i++)dp[i][0 ] = dp[i-1 ][0 ] + grid[i][0 ]; for (int i = 1 ;i < m;i++) for (int j = 1 ;j < n;j++){ dp[i][j] = min (dp[i-1 ][j]+grid[i][j],dp[i][j-1 ]+grid[i][j]); } return dp[m-1 ][n-1 ]; } };
时间复杂度:O(mn),其中 m 和 n 分别是网格的行数和列数。需要对整个网格遍历一次,计算 \textit{dp}dp 的每个元素的值。
空间复杂度:O(mn),其中 m和 n分别是网格的行数和列数。创建一个二维数组dp,和网格大小相同。 空间复杂度可以优化,例如每次只存储上一行的 dp 值,则可以将空间复杂度优化到 O(n)。
因为 dp 矩阵的每一个值只和左边和上面的值相关,我们可以使用空间压缩将 dp 数组压缩为一维。对于第 i 行,在遍历到第 j 列的时候,因为第 j-1 列已经更新过了,所以 dp[j-1] 代表 dp[i][j-1]的值(即为图中dp[j-1]);而 dp[j] 待更新,当前存储的值是在第 i-1 行的时候计算的,所以代表 dp[i-1][j] 的值(即为图中dp[j]old )。
这里写代码的时候把dp初始化为n+1个空间的数组,主要为了防止计算过程中数组越界,dp[j-1]越界。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution {public : int minPathSum (vector<vector<int >>& grid) { int m = grid.size (); int n = grid[0 ].size (); vector<int > dp (n+1 ,INT_MAX) ; dp[1 ] = 0 ; for (int i=1 ;i<=m;i++) for (int j=1 ;j<=n;j++){ dp[j] = min (dp[j-1 ],dp[j])+grid[i-1 ][j-1 ]; } return dp[n]; } };
01矩阵
一般来说,因为这道题涉及到四个方向上的最近搜索,所以很多人的第一反应可能会是广度优先搜索。但是对于一个大小 O(mn)的二维数组,对每个位置进行四向搜索,最坏情况的时间复杂度(即全是 1)会达到恐怖的 O(m^2*n^2)。一种办法是使用一个 dp 数组做 memoization,使得广度优先搜索不会重复遍历相同位置;另一种更简单的方法是,我们从左上到右下进行一次动态搜索,再从右下到左上进行一次动态搜索。两次动态搜索即可完成四个方向上的查找。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution {public : vector<vector<int >> updateMatrix (vector<vector<int >>& mat) { int m = mat.size (),n = mat[0 ].size (); vector<vector<int >>dp (m,vector<int >(n,INT_MAX-1 )); for (int i = 0 ;i < m;i++) for (int j = 0 ;j < n;j++){ if (mat[i][j] == 0 ){ dp[i][j] = 0 ; } else { if (i>0 )dp[i][j] = min (dp[i-1 ][j]+1 ,dp[i][j]); if (j>0 )dp[i][j] = min (dp[i][j-1 ]+1 ,dp[i][j]); } } for (int i = m - 1 ;i >= 0 ;i--) for (int j = n - 1 ;j >= 0 ;j--){ if (mat[i][j]!=0 ) { if (i<m-1 )dp[i][j] = min (dp[i+1 ][j]+1 ,dp[i][j]); if (j<n-1 )dp[i][j] = min (dp[i][j+1 ]+1 ,dp[i][j]); } } return dp; } };
复杂度分析
时间复杂度:O(mn)O (r**c ),其中 m 为矩阵行数,n为矩阵列数。计算 dp 数组的过程中我们需要遍历2次矩阵,因此时间复杂度为 O(2rc)=O (rc )。
空间复杂度:O(1),这里我们只计算额外的空间复杂度。除了答案数组以外,我们只需要常数空间存放若干变量。
最大正方形
对于在矩阵内搜索正方形或长方形的题型,一种常见的做法是定义一个二维 dp 数组,其中dp[i][j] 表示满足题目条件的、以 (i, j) 为右下角的正方形或者长方形的属性。对于本题,则表示以 (i, j) 为右下角的全由 1 构成的最大正方形面积。如果当前位置是 0,那么 dp[i][j] 即为 0;如果当前位置是 1,我们假设 dp[i][j] = k2,其充分条件为 dp[i-1][j-1]、 dp[i][j-1] 和 dp[i-1][j] 的值必须都不小于 (k - 1)^2,否则 (i, j) 位置不可以构成一个边长为 k 的正方形。同理,如果这三个值中的的最小值为 k - 1,则 (i, j) 位置一定且最大可以构成一个边长为 k 的正方形。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution {public : int maximalSquare (vector<vector<char >>& matrix) { int m = matrix.size (),n = matrix[0 ].size (); vector<vector<int >>dp (m,vector<int >(n,0 )); int maxm = 0 ; for (int i = 0 ;i < m;i++){ for (int j = 0 ;j < n;j++){ if (matrix[i][j] == '1' ){ if (i == 0 )dp[i][j] = 1 ; else if (j == 0 )dp[i][j] =1 ; else { dp[i][j] = min (min (dp[i-1 ][j],dp[i][j-1 ]),dp[i-1 ][j-1 ])+1 ; } maxm = max (maxm,dp[i][j]); } } } return maxm*maxm; };
编辑距离
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution {public : int minDistance (string word1, string word2) { int m = word1.size (),n = word2.size (); vector<vector<int >>dp (m+1 ,vector<int >(n+1 ,0 )); for (int i = 1 ;i < m + 1 ;i++){ dp[i][0 ] = dp[i-1 ][0 ] + 1 ; } for (int i = 1 ;i < n + 1 ;i++){ dp[0 ][i] = dp[0 ][i-1 ] + 1 ; } for (int i = 1 ;i < m + 1 ;i++){ for (int j = 1 ;j < n + 1 ;j++){ if (word1[i - 1 ] == word2[j - 1 ])dp[i][j] = dp[i-1 ][j-1 ]; else dp[i][j] = min (dp[i][j-1 ],min (dp[i-1 ][j],dp[i-1 ][j-1 ])) + 1 ; } } return dp[m][n]; } };
分割类问题 该数的完全平方数个数
对于分割类型题,动态规划的状态转移方程通常并不依赖相邻的位置,而是依赖于满足分割条件的位置。我们定义一个一维矩阵 dp,其中 dp[i] 表示数字 i 最少可以由几个完全平方数相加构成。在本题中,位置 i 只依赖 i - k^2 的位置,如 i - 1、 i - 4、 i - 9 等等,才能满足完全平方分割的条件。因此 dp[i] 可以取的最小值即为 1 + min(dp[i-1], dp[i-4], dp[i-9] · · · )。
1 2 3 4 5 6 7 8 9 10 11 12 class Solution {public : int numSquares (int n) { vector<int >dp (n+1 ,INT_MAX); dp[0 ] = 0 ; for (int i = 1 ;i < n+1 ;i++) for (int j = 1 ;j*j <= i;j++) dp[i] = min (dp[i - j*j] + 1 ,dp[i]); return dp[n]; } };
解码方法总数
这个题目难在对情况的各个分类,想清楚了还是好做的。
首先按道理来说译码只应有一种,dp[i] = dp[i-1],但是由于0,1,2存在多了各种可能,就逐条分情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution {public : int numDecodings (string s) { if (s[0 ] == '0' ) return 0 ; vector<int > dp (s.size()+1 ) ; dp[0 ]=1 ;dp[1 ]=1 ; for (int i =1 ; i < s.size (); i++) { if (s[i] == '0' ) if (s[i - 1 ] == '1' || s[i - 1 ] == '2' ) dp[i+1 ] = dp[i-1 ]; else return 0 ; else if (s[i - 1 ] == '1' || (s[i - 1 ] == '2' && s[i] <= '6' )) dp[i+1 ] = dp[i]+dp[i-1 ]; else dp[i+1 ] = dp[i]; } return dp[s.size ()]; } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int numDecodings (string s) { if (s[0 ] == '0' ) return 0 ; int pre = 1 , curr = 1 ; for (int i = 1 ; i < s.size (); i++) { int tmp = curr; if (s[i] == '0' ) if (s[i - 1 ] == '1' || s[i - 1 ] == '2' ) curr = pre; else return 0 ; else if (s[i - 1 ] == '1' || (s[i - 1 ] == '2' && s[i] >= '1' && s[i] <= '6' )) curr = curr + pre; pre = tmp; } return curr; }
单词拆分
类似于完全平方数分割问题,这道题的分割条件由集合内的字符串决定,因此在考虑每个分割位置时,需要遍历字符串集合,以确定当前位置是否可以成功分割。注意对于位置 0,需要初始化值为真。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution {public : bool wordBreak (string s, vector<string>& wordDict) { int len = s.length (); vector<bool >dp (len+1 ,false ); dp[0 ] = true ; for (int i = 1 ;i < len+1 ;i++){ for (auto word : wordDict){ int l = word.length (); if (i >= l&&s.substr (i - l, l)==word){ dp[i] = dp[i - l] || dp[i]; } } } return dp[len]; } };
子序列问题 最长递增子序列
在本题中, dp[i] 可以表示以 i 结尾的、最长子序列长度。对于每一个位置 i,如果其之前的某个位置 j 所对应的数字小于位置 i 所对应的数字,则我们可以获得一个以 i 结尾的、长度为 dp[j]+ 1 的子序列。为了遍历所有情况,我们需要 i 和 j 进行两层循环,其时间复杂度为 O(n^2)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution {public : int lengthOfLIS (vector<int >& nums) { int n = nums.size (); vector<int >dp (n,1 ); int maxm = 0 ; for (int i = 0 ;i < n;i++){ for (int j = 0 ;j < i;j++){ if (nums[j] < nums[i]){ dp[i] = max (dp[i],dp[j]+1 ); } } maxm = max (dp[i],maxm); } return maxm; } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include <iostream> #include <vector> #include <algorithm> #include <string> using namespace std;struct Node { int x; int y; }; bool cmp (Node& a, Node& b) { if (a.x == b.x)return a.y > b.y; return a.x < b.x; } void func () { int n; cin >> n; vector<Node>nodes (n+1 , Node{ 0 ,0 }); for (int i = 1 ; i <= n; i++) { cin >> nodes[i].x; } for (int i = 1 ; i <= n; i++) { cin >> nodes[i].y; } sort (nodes.begin ()+1 , nodes.end (), cmp); int sum = 0 ; vector<int >tails (n + 1 , -1 ); int end = 0 ; for (int i = 1 ; i <= n; i++) { if (tails[end] < nodes[i].y) { end++; tails[end] = nodes[i].y; } else { int l = 0 ; int r = end; while (l + 1 != r) { int mid = l + ((r - l) >> 1 ); if (tails[mid] >= nodes[i].y) { r = mid; } else l = mid; } tails[r] = nodes[i].y; } } cout << end << endl; } int main () { int T; cin >> T; for (int j = 0 ; j < T; j++) { func (); } return 0 ; }
最长公共子序列
对于子序列问题,第二种动态规划方法是,定义一个 dp 数组,其中 dp[i] 表示到位置 i 为止的子序列的性质,并不必须以 i 结尾。这样 dp 数组的最后一位结果即为题目所求,不需要再对每个位置进行统计。 在本题中,我们可以建立一个二维数组 dp,其中 dp[i][j]
表示到第一个字符串位置 i 为止、到第二个字符串位置 j 为止、最长的公共子序列长度。这样一来我们就可以很方便地分情况讨论这两个位置对应的字母相同与不同的情况了
从初值开始考虑dp[i][0]
这一列设为0,dp[0][j]
这一列设为0,因为一个字符串前0个字符和另一个字符串公共长度一定是0 而后考虑dp[i][j]
与dp[i-1][j],dp[i][j-1],dp[i-1][j-1]
这三个的关系。 首先最容易确定的是dp[i][j] = dp[i-1][j-1] + 1;
这代表着text1[i]与text2[j]两个字符串新加上的末尾字符相等,那么一定是等于dp[i-1][j-1]
+1。 对于text1[i]与text2[j]两个字符串的末尾字符不相等的情况,那么从根来推: 当前两个字符不等,text1[i]!=text2[j]的话,那么长度最少也是dp[i-1][j-1]
,但这还不够,因为我们希望拿到之前的比较中尽可能大的长度。那么当前字符已经不相等的情况下,就应该把当前的字符也放入到之前的比较中,那么一定有dp[i][j-1]
和dp[i-1][j]
>=dp[i][j]
。简单来说,dp[i][j]的值应该从dp[i-1][j-1]
,dp[i][j-1]
,dp[i-1][j]
三者中取,但dp[i][j]
≤另外两个,故比较另外两个,取较大的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution {public : int longestCommonSubsequence (string text1, string text2) { int m = text1.length (), n = text2.length (); vector<vector<int >> dp (m + 1 , vector<int >(n + 1 , 0 )); aacx aaxc for (int i = 1 ;i < m+1 ;i++) { for (int j = 1 ;j < n+1 ;j++){ if (text1[i-1 ] == text2[j-1 ]){ dp[i][j] = dp[i-1 ][j-1 ] + 1 ; } else { dp[i][j] = max (dp[i][j-1 ],dp[i-1 ][j]); } } } return dp[m][n]; } };
n个物品递增阿里笔试 背包问题 0/1背包 我们可以用动态规划来解决背包问题。以 0-1 背包问题为例。我们可以定义一个二维数组 dp存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。在我们遍历到第 i 件物品时,在当前背包总容量为 j 的情况下,如果我们不将物品 i 放入背包,那么 dp[i][j]= dp[i-1][j]
,即前 i 个物品的最大价值等于只取前 i-1 个物品时的最大价值;如果我们将物品 i 放入背包,假设第 i 件物品体积为 w,价值为 v,那么我们得到 dp[i][j] = dp[i-1][j-w] + v
。我们只需在遍历过程中对这两种情况取最大值即可,总时间复杂度和空间复杂度都为 O(NW)。
核心代码
1 2 3 4 5 6 7 8 for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= m; j++) { if (j < w[i]) dp[i][j] = dp[i - 1 ][j]; else dp[i][j] = max (dp[i - 1 ][j], dp[i - 1 ][j - w[i]] + c[i]); } }
我们可以进一步对 0-1 背包进行空间优化,将空间复杂度降低为 O(W)。如图所示,假设我们目前考虑物品 i = 2,且其体积为 w = 2,价值为 v = 3;对于背包容量 j,我们可以得到 dp[2][j]= max(dp[1][j], dp[1][j-2] + 3)
。这里可以发现我们永远只依赖于上一排 i = 1 的信息,之前算过的其他物品都不需要再使用。因此我们可以去掉 dp 矩阵的第一个维度,在考虑物品 i 时变成 dp[j]= max(dp[j], dp[j-w] + v)
。这里要注意我们在遍历每一行的时候必须逆向遍历 ,这样才能够调用上一行物品 i-1 时 dp[j-w] 的值;若按照从左往右的顺序进行正向遍历,则 dp[j-w] 的值在遍历到j 之前就已经被更新成物品 i 的值了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 for (int i = 1 ; i <= n; i++) { for (int j = m; j >= 1 ; j--) { if (j < w[i]) dp[j] = dp[j]; else dp[j] = max (dp[j], dp[j - w[i]] + c[i]); } } for (int i = 1 ; i <= n; i++) { for (int j = m; j >= 1 ; j--) { if (j >= w[i]) dp[j] = max (dp[j], dp[j - w[i]] + c[i]); } }
完全背包
一个朴素 的想法,在完全背包问题中,一个物品可以拿多次。我们就选择一直拿到放不下为止,如图上半部分所示,假设我们遍历到物品 i = 2,且其体积为 w = 2,价值为 v = 3;对于背包容量 j = 5,最多只能装下 2 个该物品。那么我们的状态转移方程就变成了 dp[2][5] = max(dp[1][5], dp[1][3] + 3, dp[1][1] + 6)
。
只需在0/1背包基础上稍作修改
1 2 3 4 5 6 7 8 for (int i = 1 ; i <= n; i++) { for (int j = m; j >= 1 ; j--) { for (int k = 0 ;k <= j/w[i];k++){ if (j >= w[i]) dp[j] = max (dp[j], dp[j - k * w[i]] + k * c[i]); } } }
但是如果采用这种方法,假设背包容量无穷大而物体的体积无穷小,我们这里的比较次数也会趋近于无穷大,远超 O(NW)的时间复杂度
1 2 3 4 5 6 7 8 9 10 11 12 for (int i = 1 ; i <= n; i++) { for (int j = 0 ; j <= m; j++) { if (j >= w[i]) dp[j] = max (dp[j], dp[j - w[i]] + c[i]); } } for (int i = 1 ; i <= n; i++) { for (int j = w[i]; j <= m; j++) { dp[j] = max (dp[j], dp[j - w[i]] + c[i]); } }
分割等和子集
思路分析:
0/1背包问题,区别在于 原先的0/1背包大小是给你一个值m,总体积不超过m即可。 这一题稍作变换,体积要求,给你一个值m,值m应该为数组和的一半,总体积要恰好等于m。物品数量即数组大小。 初始化分析dp[0][0]
意为0件物品总和体积为0,这个值必然为true。 我们最终目标是求得dp[num.size][sum/2]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution {public : bool canPartition (vector<int >& nums) { int n = nums.size (); int sum = accumulate (nums.begin (),nums.end (),0 ); if (sum%2 )return false ; else sum /= 2 ; vector<bool >dp (sum+1 ,false ); for (int i = 0 ; i <= n;i++)dp[0 ] = true ; for (int i = 1 ;i <= n;i++){ for (int j = sum;j >= nums[i-1 ];j--){ dp[j] = dp[j] || dp[j-nums[i-1 ]]; } } return dp[sum]; } };
一和零
思路分析:
0/1背包问题,区别在于 原先的0/1背包大小是给你一个值m,总体积不超过m即可。 而这一题稍作变换,有两个体积要求,字符’0’的总体积(数量)不超过m,字符’1’的总体积(数量)不超过n。而每件物品的价值均是字符’0’的数量和字符’1’的数量。物品数量即总共的字符串数量。
1 2 3 4 5 6 7 8 for (int i = 1 ;i <= size;i++) for (int j = m;j >= w[i];j--) dp[j] = max (dp[j],dp[j - w[i]]); for (int i = 1 ;i <= size;i++) for (int j = m;j >= nums0 (物品i);j--) for (int k = n;k >= nums1 (物品i);k--) dp[j] = max (dp[j][k],dp[j - nums0 (物品i)][k - nums1 (物品i)]);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class Solution {public : int findMaxForm (vector<string>& strs, int m, int n) { int size = strs.size (); vector<vector<int >>dp (m + 1 ,vector<int >(n + 1 ,0 )); for (int i = 1 ; i <= size;i++){ int nums0 = nums_0 (strs[i-1 ]); int nums1 = nums_1 (strs[i-1 ]); for (int j = m;j >= nums0;j--){ for (int k = n;k >= nums1;k--){ if (j >= nums0 && k >= nums1) dp[j][k] = max (dp[j][k],dp[j-nums0][k-nums1] + 1 ); } } } return dp[m][n]; } int nums_1 (string str) { int a = 0 ; for (int i = 0 ;i < str.length ();i++){ if (str[i] == '1' )a++; } return a; } int nums_0 (string str) { int a = 0 ; for (int i = 0 ;i < str.length ();i++){ if (str[i] == '0' )a++; } return a; } };
方法一:回溯
时间复杂度过高
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution { int targetSum; int results = 0 ; public : int findTargetSumWays (vector<int >& nums, int target) { targetSum = target; dfs (nums,0 ,0 ); return results; } void dfs (vector<int >&nums,int cur,int sum) { if (cur == nums.size () && sum == targetSum){ results++; } else if (cur == nums.size ()){ return ; } else { dfs (nums,cur + 1 ,sum + nums[cur]); dfs (nums,cur + 1 ,sum - nums[cur]); } } };
方法二:动态规划
确定dp数组(dp table)以及下标的含义
确定递推公式
dp数组如何初始化
确定遍历顺序
举例推导dp数组
1.确定dp数组(dp table)以及下标的含义 搞清楚需要输出的结果后,就可以来想办法画一个表格,也就是定义dp数组的含义。根据背包问题的经验,可以将dp[ i ][ j ]
定义为从数组nums中 0 - i 的元素进行加减可以得到 j 的方法数量。
2.确定递推公式 搞清楚状态以后,我们就可以根据状态去考虑如何根据子问题的转移从而得到整体的解。这道题的关键不是nums[i]的选与不选,而是nums[i]是加还是减,那么我们就可以将方程定义为:
dp[ i ][ j ] = dp[ i - 1 ][ j - nums[ i ] ] + dp[ i - 1 ][ j + nums[ i ] ]
可以理解为nums[i]这个元素我可以执行加,还可以执行减,那么我dp[i][j]的结果值就是加/减之后对应位置的和。
3.dp数组如何初始化 这个数组第一行应该要有初始化状态,初始化应该就是dp[0][sum + nums[0]]++; 与 dp[0][sum - nums[0]]++;
4.确定遍历顺序 二维数组简单点,从左到右,从上到下
5.举例推导dp数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution {public : int findTargetSumWays (vector<int >& nums, int target) { int len = nums.size (); int sum = accumulate (nums.begin (),nums.end (),0 ); if (abs (sum) < abs (target) || (sum + target)%2 == 1 )return 0 ; vector<vector<int >>dp (len,vector<int >(2 * sum + 1 ,0 )); dp[0 ][sum + nums[0 ]]++; dp[0 ][sum - nums[0 ]]++; for (int i = 1 ;i < len;i++){ for (int j = 0 ;j <= sum * 2 ;j++){ int l1 = 0 ,l2 = 0 ; if (j - nums[i] >= 0 && j - nums[i] <= 2 * sum)l1 = dp[i-1 ][j-nums[i]]; if (j + nums[i] >= 0 && j + nums[i] <= 2 * sum)l2 = dp[i-1 ][j+nums[i]]; dp[i][j] = l1 + l2; } } return dp[len-1 ][sum + target]; } };
股票交易问题 股票最大利润
许久没做了竟然忘了???开始思路还是还是暴力求解
1 2 3 4 5 6 7 8 9 10 int maxProfit (vector<int >& prices) { int size = prices.size (); int maxp = 0 ; for (int i = 0 ;i < size;i++){ for (int j = i + 1 ;j < size;j++){ maxp = max (prices[j] - prices[i],maxp); } } return maxp; }
这样时间复杂度O(n^2),但是明显可以压缩,此题可以定义一个状态转移方程dp[i] = max(dp[i-1] , prices[i] - min(prince[0 : i])); 而求取min值的时候也可以简便一下,minp = min(minp,prices[i]),而不是重新再遍历一遍。同样的dp数组可以继续缩减为O(1),只需两个变量存储无需dp数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution {public : int maxProfit (vector<int >& prices) { int size = prices.size (); if (size == 0 )return 0 ; int dpi = 0 ,dpi_1 = 0 ; int minp = prices[0 ]; for (int i = 1 ;i < size;i++){ dpi = max (dpi_1,prices[i] - minp); minp = min (minp,prices[i]); dpi_1 = dpi; } return dpi; } };
股票问题系列通解
因此对T[i][k]
的定义需要分成两项:
T[i][k][0]
表示在第 i 天结束时,最多进行 k 次交易且在进行操作后持有 0 份股票的情况下可以获得的最大收益;T[i][k][1]
表示在第 i 天结束时,最多进行 k 次交易且在进行操作后持有 1 份股票的情况下可以获得的最大收益。 使用新的状态表示之后,可以得到基准情况和状态转移方程。
基准情况:
1 2 T[-1 ][k][0 ] = 0 , T[-1 ][k][1 ] = -Infinity T[i][0 ][0 ] = 0 , T[i][0 ][1 ] = -Infinity
状态转移方程:
1 2 T[i][k][0 ] = max (T[i - 1 ][k][0 ], T[i - 1 ][k][1 ] + prices[i]) T[i][k][1 ] = max (T[i - 1 ][k][1 ], T[i - 1 ][k - 1 ][0 ] - prices[i])
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <iostream> #include <vector> using namespace std;int main () { int N = 0 ; cin >> N; vector<vector<int >>arr (N,vector<int >(N,0 )); int des = 0 ; int i = 0 ,j = 0 ; for (int k = 0 ;k < N*N;){ if (des%4 == 0 &&j < N&&arr[i][j] == 0 )arr[i][j++] = ++k; else if (des%4 == 1 &&i < N&&arr[i][j] == 0 )arr[i++][j] = ++k; else if (des%4 == 2 &&j >= 0 &&arr[i][j] == 0 )arr[i][j--] = ++k; else if (des%4 == 3 &&i >= 0 &&arr[i][j] == 0 )arr[i--][j] = ++k; else { if (des%4 == 0 )j--; else if (des%4 == 1 )i--; else if (des%4 == 2 )j++; else if (des%4 == 3 )i++; des++;continue ; } if (i == N/2 && j==N/2 )break ; } for (int i = 0 ;i < N;i++){ for (int j = 0 ;j < N;j++){ cout << arr[i][j] <<" " ; } cout << endl; } return 0 ; }
字符串问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution {public : int translateNum (int num) { string s = to_string (num); vector<unsigned int >dp (s.size ()+1 ,0 ); dp[0 ] = 1 ; dp[1 ] = 1 ; for (int i = 2 ;i <= s.size ();i++){ if (s[i-2 ]=='1' || (s[i-2 ]=='2' &&s[i-1 ]<='5' ))dp[i] = dp[i-1 ]+dp[i-2 ]; else dp[i] = dp[i-1 ]; } return dp[s.size ()]; } };
回文字符串
确定dp数组以及下标的含义
dp[i][j]:表示区间范围[i,j]的子串是否是回文子串,如果是dp[i][j]
为true,否则为false。
状态转移方程 回文串的定义是正反读都是一样说明,回文串的首尾字符肯定一样所以秉持这个定义进行分类讨论:
dp数组初始化 dp[i][j]
初始化为false
确定遍历顺序 由情况3可知dp[i][j]
的状态是由dp[i + 1][j - 1]
所以i是有i+1推得,j由j-1推得所以综上分析i应该从大到小倒序,j应该从i开始正序遍历 ,保证了dp[i + 1][j - 1]
都是经过计算的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 string longestPalindrome (string s) { int len = s.length (); int maxl = 0 ; int l = 0 ,r = 0 ; vector<vector<bool >>dp (len,vector<bool >(len,false )); for (int i = len-1 ;i >= 0 ;i--){ for (int j = i;j < len;j++){ if (s[i] == s[j] && j - i <= 1 ){ dp[i][j] = true ; } else if (s[i] == s[j] && dp[i+1 ][j-1 ] == true ){ dp[i][j] = true ; } if (dp[i][j] && j - i > maxl){ maxl = j - i; l = i; r = j; } } } return s.substr (l,r-l+1 ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int countSubstrings (string s) { int len = s.length (); vector<vector<bool >>dp (len,vector<bool >(len,false )); int result = 0 ; for (int i = len-1 ;i >= 0 ;i--){ for (int j = i;j <= len;j++){ if (s[i] == s[j] && j - i <= 1 ){ dp[i][j] = true ; result++; } else if (s[i] == s[j] && dp[i + 1 ][j - 1 ] == true ){ dp[i][j] = true ; result++; } } } return result; }