第 7 章 深入浅出动态规划

本文仅为个人刷题时笔记!
现在前面:
dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值
理解 这句话,差不多花了我一天时间,人不聪明,但要努力
比如322,不停的思考,为什么预处理是[0 , n],为什么二维循环是[1 ,n],为什么返回又是dp[n][amount]。具体看322

70. 爬楼梯
简单题 dp[i] = dp[i - 1] + dp[i - 2] 但是注意越界问题,这个公式只适用于n >= 3
不需要开数组的做法:

		int cur ,pre1 ,pre2;

        pre1 = pre2 = 1;
        for(int i = 2;i <= n;i++)
        {
            cur = pre1 + pre2;
            pre2 = pre1;
            pre1 = cur;
        }

198. 打家劫舍
二刷了,已经完全会了。但是代码太臃肿了。
我的想法是写出dp[ 0] = nums[0]和dp[1] = max(nums[0],nums[1])的值,循环从2开始。但官大想法是dp[0] = 0,dp[1] = nums[1],相当于用dp[0]这个过度值,就不用再分类讨论了。

413. 等差数列划分 爷做出来了啊啊啊啊啊啊啊啊啊啊
64. 最小路径和 爷又做出来了啊啊啊啊啊 就是代码太臃肿了,不需要预处理,直接在循环了进行 if else判断

542. 01 矩阵
思路很清晰但是不会写代码,通过这题,又学到一招。
很明显,0的位置dp还是0,1的位置要么接壤1要么接壤0,接壤0 就好办了,直接0位置的dp + 1,接壤1 尤其是多个1,就只需要看四个方向哪个1的dp值最小。但是这时我就懵了,min函数只能判断俩,四个值怎么办。
大佬的思路就是两遍动态搜索,先从左上到右下,遇到 0 就赋值 0 ,1的位置,只要不再左,上两个位置的边界(因为j - 1或 i - 1会越界)那么先将自身和上,左两个位置取最小值。

if(j > 0)   dp[i][j] = min(dp[i][j] , dp[i][j - 1] + 1);
if(i > 0)   dp[i][j] = min(dp[i][j],dp[i - 1][j] + 1);

再从右下到左上一次动态搜索,取自身,下,右两个位置最小值

if(j < m - 1)   dp[i][j] = min(dp[i][j] , dp[i][j + 1] + 1);
if(i < n - 1)   dp[i][j] = min(dp[i][j],dp[i + 1][j] + 1);

dp要先初始化为INT_MAX .

221. 最大正方形
有点乱,没写出来(写了一天了脑子快炸了)
首先要明确,dp[i][j] 是指,以第map[i][j]为右下角的正方形。那么好办了,直接循环判断每个1出现的地方,如果是i == 0 || j == 0这种边界,那dp[i][j] = 1,否则 dp[i][j] = min(min(dp[i - 1][j], dp[i][j - 1]) , dp[i-1][j-1] ) + 1;
然后就是取最大值了,注意最后求的是面积,dp是边长.

279. 完全平方数
dfs必超时,,但是dfs我写出来了啊。。。所以贴个代码纪念一下。

class Solution {
public:
    int t ;
    int minn = INT_MAX;
    int numSquares(int n) 
    {        
        t = sqrt(n);
        if(t * t == n)  return 1;        
        dfs(n ,0);
        return minn;
    }

    void dfs(int n ,int ans)
    {
        if(n == 0)
        {
            cout<<3<<" "<<n<<":"<<ans<<endl;
            if(ans < minn)
            {
                minn = ans;
            }
            return;
        }

        for(int i = t; i >= 1 ; i--)
        {
            if(n < 0)   continue;
            n = n - i * i;
            ans++;
            cout<<1<<" "<<n<<":"<<ans<<endl;
            dfs(n,ans);
            n = n + i * i;
            ans--;
            cout<<2<<" "<<n<<":"<<ans<<endl;
            
        }
    }
};

正片开始:
在这里插入图片描述
两层循环,第一次循环是遍历每个1~ n,第二次从1 ~ sqrt(n),得到每个dp值。

91. 解码方法
思路还是比较乱。理一理。
在i这个位置,他的结果取决于前面一个数。
(1)如果当前这个数s[i -1] != ‘0’,那么dp[i] = dp[i -1],
(2)如果当前这个数s[i- 1] 又可以和s[i -2]合并if(i > 1 && s[i - 2] != '0' && ((s[i - 2] - '0') * 10 + s[i - 1] - '0' ) <= 26) dp[i] = dp[i -2]
满足以上两种情况则把两种都加起来,dp[i]+=dp[i - 1] ; dp[i] += dp[i - 2];
最后return dp[n]

关于这个i ,循环中,dp[i]代表是字符s[i - 1]的dp值,意思是从s[0]到s[i - 1]这段字符串中的正确结果

139. 单词拆分
假设例子是 catsandog
(1)dp[i]意思看上面,所以i = = 4,判断的是cat及其子串的部分
(2)善用find函数 ,把字典放入unordered_set,unordered_set<string> wordDictSet(wordDict.begin(),wordDict.end());,使用find函数查找子串是否存在。
(3)对于每一次的判断条件,每次从j开始切割 j从0到 i这个位置不断遍历子串,但是必须保证dp[j] == true,这样才保证切割的位置前的串都是正确满足字典的串。

300. 最长递增子序列
经典中的经典题了。用代码来分析。

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) 
    {
        int n = nums.size();
        vector<int> dp(n + 1,1);
        if(n == 0)  return 0;
        //这题关键在于要套俩循环
        //第一次循环是指以nums[i]为最后一个数,求 i 之前最长递增子序列
        //第二次循环是遍历i之前所有数,把i之前每个小于nums[i]的值都得到
       //才不会落下
        for(int i = 0;i < n;i++)
        {
            for(int j = 0;j < i;j++)
            {
                if(nums[i] > nums[j])
                {
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
        }
        return *max_element(dp.begin(),dp.end());//stl好东西
    }
};

1143. 最长公共子序列
啊啊啊啊很接近了啊啊啊但还是不会写啊啊啊啊
老想着预处理预处理,啊不是我又不要把整个dp输出,dp开大一层最外层设为0就好了啊。
思路就是形成一个二维dp,text1.size()行text2.size()列,dp[i] [j]代表的是 text1[0] ~text1[i] 中与text2[0] ~text2[j] ,最大子序列的值
(1)如果当前c1 == c2,那只要在dp[i- 1][j - 1] + 1就行
(2)如果不相等,那就只能继承以前的最大值dp[i][j] = max(dp[i - 1][j] ,dp[i][j -1]);,相当于,反正不相等,两边少这个不相等的字符都不影响,只要继承到一个最大的值

416分割等和子集
开始了,背包问题。但是没想到怎么转换成背包问题
思路:在数组里找数装入背包,让背包的总数和 = 全数据 / 2。

有一些例外要单独列出
(1)总数和为单数
(2)数组中最大数 > sum/ 2,显然,数组中每个数都要用到,这个最大值无论放在哪里,都会导致一边不满足
(3)数组中只有一个数
以上全部返回false;

dp[i][j]意思是,从nums[0] ~ nums[i]中选取任意数,也可以是 0 个,使得总和为 j

首先预处理边界 i==0,很明显,dp[i][0] = true,只要i 不越界,无论 i 为多少,不去就行,总和为 0 一定满足,其次,就是数组中第一个值dp[0][nums[0]] = true,取第一个值一定可以为 真。
对于 i > 0 和 j > 0,就要对比每次选取的数据和j 的值

  • j > nums[i]
    可以选nums[i] dp[i][j] = dp[i−1][j−nums[i]]
    也可不选 nums[i] dp[i][j] = dp[i- 1][j]
    两者去或就行,只有一者为true,dp[i][j]就可以满足 为true
  • j < nums[j]
    不能选 nums[i] dp[i][j] = dp[i- 1][j]

474. 一和零
这是一个两个背包的问题,所以要开三维数组。当然也可以压缩,先看三维的

dp[i][j][k] 从 strs[0] ~strs[1]选取若干个字符串,所有字符串0个数和cnt0 == j,cnt1 == k

关于得到一个字符串 0 和 1 ,这里使用的辅函数,辅函数返回类型为pair<int,int>

 pair<int,int> count(const string& s)
  {
      int cnt0,cnt1;
      cnt1 = cnt0 = 0;
      
      for(int i =0;i < s.size();i++)
      {
          if(s[i] == '1')
          {
              cnt1++;
          }
          else  cnt0++;
      }
   //   cout<<cnt0<<" "<<cnt1<<endl;
      return make_pair(cnt0,cnt1);//最后要返回pair<int,int>
  }

所以数据的使用方法是auto [cnt0,cnt1] = count(strs[i - 1]),注意是 i -1啊
然后就是经典背包规划,但是这道题,取strs[ i -1]有两个条件, j >= cnt0 && k >= cnt1 。所以,dp[i][j][k]直接记作dp[i - 1][j][k],只有当满足了这个条件,才进行最大值判断。

322. 零钱兑换
几乎跟416一模一样,就是max换成min,但是自己在写二维dp的时候很吃力,很多细节不到位,记录一下不断debug的过程。几个小时啊啊啊啊啊
首先明确一点:dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值(这句话多看几遍**)

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) 
    {
        int n = coins.size();

        vector<vector<long>> dp(n + 1,vector<long>(amount + 1,INT_MAX ));
        //全部置为INT_MAX,我们求的是最小值
        for(int i = 0;i <= n;i++)
        {
            dp[i][0] = 0;
        }
第一次预处理,从前i个物品中,只要不取,j == 0一定可以满足且dp值为 0
       for(int j = 1;j <= amount;j++)1开始,因为前0个一个不取也是0,也就是说dp[0][0] = 0,这个在上面已处理
       {
            dp[0][j] = INT_MAX;
       }
第二次预处理,前0个物品,怎么选都没得啊,所以全部预处理为INT_MAX


到此,两个边界i 和 j都处理完了

        for(int i = 1;i <= n;i++)1开始循环,而且可以等于n
表示前1个物品到前n个物品,这里的循环的i代表的数量数目,选取硬币的个数
        {
            
            for(int j = 1;j <= amount;j++)
            {
                
                if(j >= coins[i - 1])  
这里的i-1代表的是第i - 1个啊 比如i == 1,1个硬币在coins中是coins[0]
                {
                  //  cout<<"*";
                    dp[i][j] = min(dp[i - 1][j] ,dp[i][j - coins[i - 1]] + 1 );
                }
                 
                else    dp[i][j] = dp[i - 1][j];
            }
        }
        
return dp[n][amount] == INT_MAX ? -1 : dp[n][amount];
    }
};

72. 编辑距离
没思路
在这里插入图片描述
对于边界:i == 0 dp[0][j] = j,等价于第一个串要插入j个字符才能成为前word2[j]
对于判断第i位与第j位是否相等(word1[i - 1] == word2[j - 1]),相等则不用加1,不相等要 + 1:

word1[i - 1] == word2[j - 1]) ? 0 : 1

650. 只有两个键的键盘
也是完全没有什么思路啊。。。
在这里插入图片描述
这份题解已经很清晰了。。然而还有个疑问,为什么遍历j只要第一次找到 一个j满足是i的因数就可以跳出循环了,不用比较最小值吗?
评论区数学证明:
在这里插入图片描述
10. 正则表达式匹配
看清题,* 的意思是,可以匹配零个或多个前面的那一个元素
那么将 *与前面那个元素组合起来匹配
动规一共要分三个大情况去考虑。 *,字符,.
(1) p[j] == ‘*’
分两种情况,一是j - 1前一个字符匹配上了,另一个是没有匹配上
先看第一种
s[i - 1] == p[j - 1] ,p[j - 1]p[j]这个组合还可以继续用,s[i - 1]可以直接当做删除不影响结果
比如
s : a a b
p : a * b
0 位置匹配上 ,来到位置1,1位置时,s[1] == p[0],s[1]可以直接删掉不影响
dp[i][j] = dp[i - 1][j]
如果没有匹配上,那就将整个p[j - 1]p[j]组合扔掉 dp[i][j] |= dp[i][j - 2];
(2)p[j] == ‘.’ 那么直接不用比较了,dp[i][j = dp[i - 1][j - 1]
(3)都是字符 比较两个是否相等 s[i] == p[j]

121. 买卖股票的最佳时机
思路:如何卖出赚的最多?那就是尽可能的在最低点买进,最高点卖出。我们假设每个i都是最佳卖出的时机,对此遍历。我们需要找一个不超过i这个点的最小值,maxprofit = max(maxprofit,price - minprice);

188. 买卖股票的最佳时机 IV
难的啊,真的很难。
这题需要维护两个二维数组
buy[i][j] :表示在第 i 天的,已进行 j 笔交易,且当前手上持有股票,这种情况下的最大利润。
sell[i][j] :表示在第 i 天的,已进行 j 笔交易,且当前手上不持有股票,这种情况下的最大利润。
买入不算交易,卖出才算一次交易。
(1)边界处理:buy[0][j] 和 sell[0][j]都是第0 天,没得交易 直接赋值 INT_MIN/2,这里要除以2,,因为buy[0][j] 和 sell[0][j]也是要进行±prices[i - 1]运算的,直接复制INT_MIN会溢出。
(2)关于遍历的交易次数。不一定是k次。加入每天都在交易,一天买一天卖,实际交易次数只有 n / 2(卖出才算交易,只有手上有股票才可以卖出)。所以 k = min(n / 2, k)
(3)对于每个buy[i][j] ,有两种情况。此时手上有股票,可以是是i - 1刚卖出,i天又买了股票,也可是是i-1就已经有股票一直没卖。代码表示为
buy[i][j] = max(sell[i - 1][j] - prices[i - 1], buy[i - 1][j]);(注意i是第i天,那么对应prices[i - 1],prices是从0 到 n-1)
对于,sell[i][j],可以是从i - 1天开始手中就没有股票,也可能是i -1持有,第i天卖出,sell[i][j] = max(buy[i - 1][j - 1] + prices[i - 1], sell[i - 1][j]);
看上面两个代码 ,j - 1,那么 j 参与循环只能从1开始遍历到k,所以要单独处理buy[i][0]这种buy[i][0] = max(sell[i - 1][0] - prices[i - 1] ,buy[i - 1][0]);
不存在sell[i][0]这种情况,卖出必须进行一次交易即j - 1的情况,不可能出现buy[i - 1][- 1] + prices[i - 1]
(4)最后返回手上第n天没有股票的sell[n]情况。手上没有多余的股票肯定比手上买入没卖出盈利的利润高。但是只需要return *max_element(sell[n].begin(), sell[n].end());,,题目只是说最多k次交易,具体几次不在乎,只需要求得最大值

309. 最佳买卖股票时机含冷冻期
(我要成为股神(bushi))
二维数组,记录三个状态
dp[i][0]:i天手上有股票的最大利益
dp[i][1]:i天手上无股票且处于冷冻期的最大利益
dp[i][2]:i天手上无股票且不处于冷冻期的最大利益

dp[i][0] :可能是i - 1天就有的股票,也可以是i天买的股票
dp[i][0] = max(dp[i - 1][0] ,dp[i - 1][2] - prices[i]);
dp[i][1] :只能是因为昨天把股票卖了
dp[i][1] = dp[i - 1][0] + prices[i];
dp[i][2] :可能是昨天冷冻期今天解冻,可能是昨天就没股票
dp[i][2] = max(dp[i - 1][1],dp[i - 1][2]);
返回return max(dp[n - 1][1],dp[n - 1][2]);,dp[n - 1][0]不必考虑,手上留着卖不出的股票一定比卖出得到收益低

213. 打家劫舍 II
这题的关键是分区间和处理边界。
(1)偷了第一家就不能偷最后一家。分开两个区间偷,求最大
max(robchange(nums, 0 , n - 2) ,robchange(nums,1 , n -1));
(2)dp数组循环的边界处理
dp[0] = nums[0],无疑问
关于dp[1],如果可以偷最后一家,那么nums[0]不可以偷,相当于从1开始,dp[1] = nums[1],,但是最后一家不可以偷,dp[1] = max(nums[0] ,nums[1])
其他问题处理就跟无环一样处理

class Solution {
public:
    
    int rob(vector<int>& nums) 
    {
        int n = nums.size();
        if(n == 0)  return 0;
        else if(n == 1) return nums[0];
        else if(n == 2) return max(nums[0] ,nums[1]);
        
        return max(robchange(nums, 0 , n - 2) ,robchange(nums,1 , n -1));

    }

    int robchange(vector<int>& nums,int start,int end)
    {
        vector<int> dp(nums.size() + 1);
                
        for(int i = start; i <= end ;i++ )
        {            
            if(i == 0)  
            {
                dp[0] = nums[0];        
            }
            else if(i == 1)
            {
                if(end == nums.size() - 1)
                {
                    dp[1] = nums[1];
                }
                else
                {
                    dp[1] = max(nums[0] ,nums[1]);
                }
            }
            else dp[i] = max(dp[i - 2] + nums[i] ,dp[i - 1]);
        }
        return dp[end];
    }
};

53. 最大子序和
奇耻大辱,,居然没写出来。直接上代码把

class Solution {
public:
    int maxSubArray(vector<int>& nums) 
    {
        int n = nums.size();
        vector<int> dp(n + 1);

        dp[0] = nums[0];
        int ans = dp[0];
        for(int i = 1; i < n;i++)
        {
            dp[i] = max(nums[i], dp[i - 1] + nums[i]);
            ans = max(ans, dp[i]);
        }
return ans;
    }
};

一直在考虑如果当前nums[i]不加,dp[i]该如何维护,不加就直接dp[i] = nums[i]就好了啊,,最大值需要新开一个变量,不一定是dp[n - 1],求的是最大子序列,不等于一定加到最后一个值.
343. 整数拆分
动规不出来啊–
遍历找最大值。对于任意一个i ,一半拆分成 j,另一半就是i - j,这个 i *(i - j)也许是正确答案,但如果不是,就意味着需要继续按同样的方法拆分 i - j。用代码表示就是curmax = max(curmax, max(j * (i - j) ,j * dp[i - j] ));

583. 两个字符串的删除操作
跟72几乎一样
动规部分稍微改改就行

if(word1[i - 1] == word2[j - 1])
{
	dp[i][j] = dp[i - 1][j - 1];
}
else
{
	dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1;
}

646. 最长数对链
看了题解才知道,其实这题本质上就是求最长递增子序列的变种。
先排序,按照数组对的第一个元素从小到大排序。这里记录一下Lambda表达式的写法

sort(pairs.begin(),pairs.end(),[](vector<int>& v1 ,vector<int>& v2)
{
	return v1[0] < v2[0];        
});

dp[i] 表示 以 pairs[i]数组对为结尾的最长数对链

两层循环,外层遍历每一个数组对,以该数组对为结尾的结果。内层j < i数组对中找能够在以pair[i]为结尾的数组对往前填充数组,尽可能的延长该数对链。

		for(int i = 1;i < n;i++)
        {
            for(int j = 0;j < i;j++)
            {
                if(pairs[i][0] > pairs[j][1])
                {
                    dp[i] = max(dp[i] , dp[j] + 1);
                }
            }
        }

376. 摆动序列
周赛写了1955,看懂1955后这题会写了,记录一下,泪目

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) 
    {
        int n = nums.size();

        if(n == 1)  return 1;
        else if (n == 2 && nums[0] != nums[1])  return 2;

        vector<vector<int>> dp(n + 1,vector<int>(2,1) );

        for(int i = 1;i < n;i++)
        {
            if(nums[i] - nums[i - 1] > 0)
            {
                for(int j = 0;j < i;j++)
                {
                    dp[i][0] = max(dp[i][0] ,dp[j][1] + 1);
                }
                
            }
            else if(nums[i] - nums[i - 1] < 0)
            {
                for(int j = 0;j < i;j++)
                {
                    dp[i][1] = max(dp[i][1] ,dp[j][0] + 1);
                }                
            }
            else
            {
                for(int j = 0;j < i;j++)
                {
                    dp[i][1] = max(dp[i][1] ,dp[j][1]);
                    dp[i][0] = max(dp[i][0] ,dp[j][0]);
                }
            }
        }
return *max_element(dp[n - 1].begin(),dp[n - 1].end());
    }
};

494. 目标和
回溯做的,顺便写一下回溯的debug过程。
回溯法1.0版

	void dfs(vector<int>& nums, int target ,int step)
    {
        if(step == nums.size() && target == 0)
        {
                ans++;
                return;
        }
            dfs(nums,target - nums[step],step+1);            		
            dfs(nums,target + nums[step],step+1);        
    }

错解,这里 && 导致调用dfs的时候可能会出现没有return的情况,递归就失败了

回溯法2.0版

	void dfs(vector<int>& nums, int target ,int step)
    {
        if(step == nums.size())
        {
            if( target == 0)
            {
            	ans++;
                return;
            }                
        }
            dfs(nums,target - nums[step],step+1);            		
            dfs(nums,target + nums[step],step+1);        
    }

错解,跟上面一样,满足条件下return到上一层,外层if没有return无法回溯。
回溯法最终版

void dfs(vector<int>& nums, int target ,int step)
    {
        if(step == nums.size())
        {
            if(target == 0)
            {
                ans++;
                return;
            }
            return;
        }
            dfs(nums,target - nums[step],step+1);
            dfs(nums,target + nums[step],step+1);
        
    }

正解,都可以返回到上一层了。
但是这里记录一下官方的方法,不需要return,通过If - else 让语句全部执行完毕自动返回。
在这里插入图片描述
在这里插入图片描述
接下来是重点,动规方法。
所有的数据会被分为两个部分,一个前面带 + ,一个带 - 。假设带 - 好的数据之和为neg,neg与带正好部分sum - neg的关系就是
(sum−neg)−neg=sum−2⋅neg=target
也就是
neg= (sum−target)/ 2
这样就转换成了背包问题,其中dp[i][j] 表示在数组 nums 的前 i 个数中选取元素,使得这些元素之和等于 j 的方案数。假设数组nums 的长度为 n,则最终答案为 dp[n][neg]。接下来就是套路,不赘述了。

714. 买卖股票的最佳时机含手续费
股票会写了,我是股神(bushi
把关于边界和循环边界的思考记录一下。
dp[0][0]是指第一天无股票状态下最大收益,那就是0
dp[0][1]是指第一天有股票状态下最大收益,那就是刚买的,为 -prices[0]
这里是prices[0]开始,i循环应该是[1,n),每天的买入卖出操作是±prices[i],最后返回的是dp[n - 1][0];

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值