[动态规划,回溯有何区别] https://www.cnblogs.com/genialx/p/10191366.html
[什么是动态规划,动态规划的意义?]https://www.zhihu.com/question/23995189
选或不选这类问题:
例1:给定一组数据,相邻两个数不能同时选择,求如何选择使得所选数之和最大?
a r r = [ 1 , 2 , 4 , 1 , 7 , 8 , 3 ] arr=[1,2,4,1,7,8,3] arr=[1,2,4,1,7,8,3] 直接看最后一个数,它有两种方案,选或不选。
d p [ 6 ] = m a x ( d p [ 4 ] + a r r [ 6 ] , d p [ 5 ] ) dp[6]=max( dp[4]+arr[6] , dp[5] ) dp[6]=max(dp[4]+arr[6],dp[5])
递推公式: d p [ i ] = m a x ( d p [ i − 2 ] + a r r [ i ] , d p [ i − 1 ] ) dp[i]= max( dp[i-2]+arr[i] ,dp[i-1] ) dp[i]=max(dp[i−2]+arr[i],dp[i−1])
递推出口: d p [ 0 ] = a r r [ 0 ] , d p [ 1 ] = m a x ( a r r [ 0 ] , a r r [ 1 ] ) dp[0]=arr[0] ,dp[1]=max(arr[0],arr[1]) dp[0]=arr[0],dp[1]=max(arr[0],arr[1])
例2:给定一数组,和一个目标数tar,问有没有几个数相加之和等于tar?
a r r = [ 3 , 34 , 4 , 12 , 5 , 2 ] arr=[3,34,4,12,5,2] arr=[3,34,4,12,5,2] 每个数有两种方案,选或者不选
s u b s e t ( a r r , i , s ) = s u b s e t ( a r r , i − 1 , s ) ∣ ∣ s u b s e t ( a r r , i − 1 , s − a r r [ i ] ) subset(arr,i,s)= subset(arr,i-1,s) || subset(arr, i-1, s-arr[i]) subset(arr,i,s)=subset(arr,i−1,s)∣∣subset(arr,i−1,s−arr[i])
其中s为目标数,当i=5时,s=tar 。 看递推公式||左侧,为不选该数的情况,右侧为选择该数的情况,若选择了,则目标数发生变化
递推出口: if s = = 0 s==0 s==0: return t r u e true true
if i = = 0 i==0 i==0: return a r r [ 0 ] = = s arr[0]==s arr[0]==s
if a r r [ i ] > s arr[i]>s arr[i]>s: return s u b s e t ( a r r , i − 1 , s ) subset(arr,i-1,s) subset(arr,i−1,s) 即只有不选这种情况
非递归需要用二维数组来保存,此题类似0-1背包问题
int backpack(vector<int>&w, vector<int>&v, int capacity){
vector<int>temp(capacity + 1, 0);
vector<vector<int>>dp(w.size(), temp);
for (int j = 0; j < capacity; ++j){ //初始化第一行
if (j >= w[0])
{
dp[0][j] = v[0];
}
}
for (int i = 1; i < w.size(); ++i){
for (int j = 1; j <= capacity; ++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]] + v[i]);
}
}
}
return dp[w.size() - 1][capacity - 1];
}
int backpack_better(vector<int>&w, vector<int>&v, int capacity){
vector<int>dp(capacity + 1);
for (int i = 0; i < w.size(); i++) //里面的循环包含了初始化第一行的步骤
{
for (int j = capacity; j >= 0; j--)
{
if (j - w[i] >= 0)
dp[j] = max(dp[j - w[i]] + v[i], dp[j]);
}
}
return dp[capacity];
}
int minPathSum(vector<vector<int>>& grid) {
int row = grid.size(), col = grid[0].size();
if (row == 0 || col == 0) return -1;
vector<int>temp(col, 0);
vector<vector<int>>dp(row, temp);
dp[0][0] = grid[0][0];
for (int j = 1; j < col; ++j) //初始化第一行
dp[0][j] = dp[0][j - 1] + grid[0][j];
for (int i = 1; i < row; ++i) //初始化第一列
dp[i][0] += dp[i - 1][0] + grid[i][0];
for (int i = 1; i < row; ++i)
for (int j = 1; j < col; ++j){
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
return dp[row - 1][col - 1];
}
从递推公式可以看出,当前行只会依赖于上方和左侧的值,只开一个一维数组也可解决。问题是开多大?
所以要么开辟row大小的数组从左向右滚,要么开col大小的数组从上向下滚。row跟col哪个小用哪个的值开辟数组会省空间
int minPathSum_better(vector<vector<int>>& grid) {
int row = grid.size(), col = grid[0].size();
if (row == 0 || col == 0) return -1;
int more = max(row, col);
int less = min(row, col);
bool flag = row > col; //flag为true,从上向下滚
vector<int>dp(less, 0);
dp[0] = grid[0][0];
for (int i = 1; i < less; ++i)
dp[i] = dp[i - 1] + (flag ? grid[0][i] : grid[i][0]);
for (int j = 1; j < more; ++j)
{
dp[0] += flag ? grid[j][0] : grid[0][j];
for (int i = 1; i < less; ++i)
dp[i] = min(dp[i], dp[i - 1]) + (flag ? grid[j][i] : grid[i][j]);
}
return dp[less - 1];
}
int coinChange(vector<int>& coins, int amount)
{
if (coins.size() == 0 || amount <= 0) return 0;
vector<int>temp(amount + 1, 0);
vector<vector<int>>dp(coins.size(), temp);
for (int j = 1; j <= amount; ++j){
if (j%coins[0] == 0)
{
dp[0][j] = j / coins[0]; //初始化第一行,注意该dp矩阵建立大小跟01背包一样
}
else dp[0][j] = INT_MAX;
}
for (int i = 1; i < coins.size(); ++i)
for (int j = 1; j <= amount; ++j)
{
int help = INT_MAX;
for (int k = 0; j - coins[i] * k >= 0; ++k)
{
if (dp[i - 1][j - k*coins[i]] != INT_MAX)
{
help = min(help, dp[i - 1][j - k*coins[i]] + k);
}
}
dp[i][j] = help;
}
return dp[coins.size() - 1][amount] == INT_MAX ? -1 : dp[coins.size() - 1][amount];
}
注意到,以例1为例,dp[1][6]用到上排的元素有:dp[0][6],dp[0][4],dp[0][2],dp[0][0] 考虑到在计算dp[1][4]时已经用到了dp[0][4],dp[0][2],dp[0][0]
递推公式可改为: dp[i][j]=min( dp[i-1][j], dp[i][j-coins[i]]+1)
int coinChange_better(vector<int>& coins, int amount)
{
if (coins.size() == 0 || amount <= 0) return 0;
vector<int>temp(amount + 1, 0);
vector<vector<int>>dp(coins.size(), temp);
for (int j = 1; j <= amount; ++j){
if (j%coins[0] == 0)
{
dp[0][j] = j / coins[0];
}
else dp[0][j] = INT_MAX;
}
for (int i = 1; i < coins.size(); ++i)
for (int j = 1; j <= amount; ++j)
{
/*******************改动*********************/
int help = INT_MAX;
if (j - coins[i] >= 0 && dp[i][j - coins[i]] != INT_MAX)
help = dp[i][j - coins[i]] + 1;
dp[i][j] = min(help, dp[i - 1][j]);
/*******************************************/
}
return dp[coins.size() - 1][amount] == INT_MAX ? -1 : dp[coins.size() - 1][amount];
}
从coinChange_better可以看出,只用到了本行元素和正上方元素, 类似LeetCode-64 : Minimum Path Sum ,使用一维数组进行优化
int coinChange_best(vector<int>& coins, int amount)
{
if (coins.size() == 0 || amount < 0) {
return 0;
}
vector<int>dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int j = 1; j <= amount; ++j){
if (j%coins[0] == 0)
{
dp[j] = j / coins[0];
}
}
for (int i = 0; i < coins.size(); ++i)
for (int j = 0; j <= amount; ++j)
{
int help = INT_MAX;
if (j - coins[i] >= 0 && dp[j - coins[i]] != INT_MAX)
help = dp[j - coins[i]] + 1;
dp[j] = min(help, dp[j]);
}
return dp[amount] == INT_MAX ? -1 : dp[amount];
}
给定未排序的整数数组,找到最长的递增子序列的长度。
Input:[10, 9, 2, 5, 3, 7, 101, 18] Output : 4 Explanation : The longest increasing subsequence is[2, 3, 7, 101], therefore the length is 4.
dp[i]:遍历到第i个数时的最长递增子序列长度 d p [ i ] = m a x ( 1 , ( d p [ i − k ] + 1 ) ∣ n u m s [ i ] > n u m s [ i − k ] ) dp[i]=max(1, ( dp[i-k]+1 )|nums[i]>nums[i-k] ) dp[i]=max(1,(dp[i−k]+1)∣nums[i]>nums[i−k])
int lengthOfLIS(vector<int>& nums) {
if (nums.empty()) return 0;
vector<int>dp(nums.size(), 1);
for (int i = 1; i < nums.size(); ++i)
for (int j = i - 1; j >= 0; --j)
{
if (nums[i]>nums[j])
dp[i] = max(dp[i], dp[j] + 1);
}
// return dp[nums.size()-1]; 这里不正确,例如nums = {1,3,6,7,9,4,10,5,6}; dp[nums.size()-1]=5; 应返回dp数组中最大那个值
int res = 0;
for (auto c : dp){
res = max(res, c);
}
return res;
}
如果我们另开一个数组,记录连续的子序列中的最大值,如长度为1的递增子序列最大值为 MaxV[1] ,长度为2的递增子序列最大值为MaxV[2]
这样只需比较当前数nums[i]与MaxV[nMaxLIS],MaxV[nMaxLIS-1]…只要大于其中一个,则找到当前数应该加入的位置
int lengthOfLIS_better(vector<int>& nums) {
if (nums.empty()) return 0;
vector<int>dp(nums.size(), 1);
vector<int>MaxV(nums.size() + 1, -1);
MaxV[1] = nums[0];
MaxV[0] = INT_MIN; //这项是为了更新长度为1的子序列
//如序列为10 2 3,进入内层for循环后小于当前MaxV[1]=10,如果到此就break肯定是不对的,我们要替换MaxV[1]为2
int nMaxLIS = 1; //当前数组最长递增子序列的长度
for (int i = 1; i < nums.size(); ++i)
{
int j;
for (j = nMaxLIS; j >= 0; --j){
if (nums[i]>MaxV[j])
{
dp[i] = j + 1; break;
}
}
//更新信息
if (dp[i] > nMaxLIS)
{
nMaxLIS = dp[i]; MaxV[nMaxLIS] = nums[i];
}
else if (MaxV[j] < nums[i] && nums[i] < MaxV[j + 1])
{
MaxV[j + 1] = nums[i]; //例如 1 2 3 4 和 1 2 3 5都是长度为4的递增序列,但我们肯定选择1 2 3 4因为这样有助长度的增长
}
}
return nMaxLIS;
}
上述时间复杂度依然为O(N^2),利用二分查找优化dp数组生成过程,将时间复杂度降为O(NlogN)
int lengthOfLIS_best(vector<int>& nums) {
if (nums.empty()) return 0;
vector<int>dp(nums.size(), 1);
vector<int>ends(nums.size(), 0);
int right = 0;
ends[0] = nums[0];
int l = 0, r = 0, m = 0;
for (int i = 1; i < nums.size();++i)
{
l = 0; r = right;
while (l<=r)
{
m = l + (r - l) / 2;
if (nums[i]>ends[m])
l = m + 1;
else r = m - 1;
}
right = max(right, l);
ends[l] = nums[i];
dp[i] = l + 1;
}
int res = 0;
for (auto c : dp){
res = max(res, c);
}
return res;
}
string LCSE(string str1, string str2){
if (str1.empty() || str2.empty()) return "";
vector<vector<int> > dp(str1.size(), vector<int>(str2.size(), 0));
dp[0][0] = (str1[0] == str2[0]) ? 1 : 0;
for (int i=1; i < str1.size();++i)
{
dp[i][0] = max(dp[i - 1][0], (str1[i]==str2[0])?1:0); //初始化第一列
}
for (int j = 1; j < str2.size();++j)
{
dp[0][j] = max(dp[0][j - 1], (str1[0]==str2[j] ) ? 1 : 0); //初始化第一行
}
for (int i = 1; i < str1.size();++i)
for (int j = 1; j < str2.size();++j)
{
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
if (str1[i] == str2[j])
dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + 1);
}
//dp数组右下角的点就是最长公共子序列的长度,如果只需要知道长度,可用一维数组来优化
//但是我们要得到这个子序列,就需要从右下角开始移动至左上角 ,判断这个dp是怎么来的
string res ="";
int m = str1.size() - 1, n = str2.size() - 1;
int index = dp[m][n] - 1;
while (index>=0)
{
if (n > 0 && dp[m][n] == dp[m][n - 1]) //如果dp[i][j]==dp[i-1][j],说明dp[i - 1][j - 1] + 1不是必须的选择,向上移动
--n;
else if (m > 0 && dp[m][n] == dp[m - 1][n])
--m;
else{
res= str1[m]+res; //如果dp[i][j]>dp[i-1][j] 且dp[i][j]>dp[i][j-1],说明在计算dp[i][j]一定是选择了dp[i - 1][j - 1] + 1,此时str1[i]==str2[j]
--index;
--m;
--n;
}
}
return res;
}