打赏

LeetCode动态规划集锦

前言

之前刷的题让我感觉很分散,这次就用集中突破的方式,我觉得这种方式,应该比较容易掌握这方面的知识,再不济,这种思想也能让我在对待难题时,有一定的思路。

技巧

答案
第一步骤:定义数组元素的含义,上面说了,我们会用一个数组,来保存历史数组,假设用一维数组 dp[] 吧。这个时候有一个非常非常重要的点,就是规定你这个数组元素的含义,例如你的 dp[i] 是代表什么意思?

第二步骤:找出数组元素之间的关系式,我觉得动态规划,还是有一点类似于我们高中学习时的归纳法的,当我们要计算 dp[n] 时,是可以利用 dp[n-1],dp[n-2].....dp[1],来推出 dp[n] 的,也就是可以利用历史数据来推出新的元素值,所以我们要找出数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],这个就是他们的关系式了。而这一步,也是最难的一步,后面我会讲几种类型的题来说。

学过动态规划的可能都经常听到最优子结构,把大的问题拆分成小的问题,说时候,最开始的时候,我是对最优子结构一梦懵逼的。估计你们也听多了,所以这一次,我将换一种形式来讲,不再是各种子问题,各种最优子结构。所以大佬可别喷我再乱讲,因为我说了,这是我自己平时做题的套路。
第三步骤:找出初始值。学过数学归纳法的都知道,虽然我们知道了数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],我们可以通过 dp[n-1] 和 dp[n-2] 来计算 dp[n],但是,我们得知道初始值啊,例如一直推下去的话,会由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,所以我们必须要能够直接获得 dp[2] 和 dp[1] 的值,而这,就是所谓的初始值。

由了初始值,并且有了数组元素之间的关系式,那么我们就可以得到 dp[n] 的值了,而 dp[n] 的含义是由你来定义的,你想求什么,就定义它是什么,这样,这道题也就解出来了。

正文

关于最终的结果为何要取余1000000007?

  1. 1000000007是一个质数(素数),对质数取余能最大程度避免结果冲突/重复
  2. int32位的最大值为2147483647,所以对于int32位来说1000000007足够大。
  3. int64位的最大值为2^63-1,用最大值模1000000007的结果求平方,不会在int64中溢出。
  4. 所以在大数相乘问题中,因为(a∗b)%c=((a%c)∗(b%c))%c,所以相乘时两边都对1000000007取模,再保存在int64里面不会溢出。
  5. 取模之后能够计算更多的情况

一、青蛙跳台阶

  1. 题目
    在这里插入图片描述
  2. 解题步骤

a. 决定dp[i]代表什么含义:这里代表的是->跳上一个i级台阶总共有多少种跳法。
b. 找出数组元素之间的关系式:dp[n] = dp[n-1]+dp[n-2]
c. 找出初始条件:dp[1] =1,dp[0] = 1; 为了让算式正确,只能这样做

  1. 代码
    code
答案
class Solution {
public:
    int jumpFloor(int n) {
        //整个的逆向思维就是下台阶
        if(n<=1)
            return n;
        int dp[n+1];
        dp[0] = 1;
        dp[1] = 1;
        for(int i = 2;i<=n;i++)
            dp[i] = dp[i-1]+dp[i-2];//dp[2] = dp[1]+0=1 dp[3] = dp[2]+dp[1]
        return dp[n];
    }
};

二、机器人走到网格右下角

  1. 题目
    在这里插入图片描述
  2. 解题步骤

a. 定义dp[i][j]的含义:dp[i][j]:当机器人走到(i,j)位置时,一共有dp[i][j]种路径。
b. dp[i][j] = dp[i-1][j]+dp[i][j-1]
c. dp[i][0] = 1;,dp[0][i] = 1; dp[ 都初始化一遍就没啥问题了。

  1. 代码
    code
答案
class Solution {
public:
    int uniquePaths(int m, int n) {
        int dp[m][n];
        for(int i = 0;i<m;i++)
            dp[i][0] = 1;
        for(int j = 0;j<n;j++)
            dp[0][j] = 1;
        for(int i = 1;i<m;++i)
        {
            for(int j = 1;j<n;++j)
            {
                dp[i][j] = dp[i-1][j]+dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
};
  1. 优化
    在这里插入图片描述
    其实由这个公式dp[i][j] = dp[i-1][j]+dp[i][j-1]这个公式得知,我们每次除了第0行和第0列,其实,我们每次获取一个值,只需要它的左边的值和上面的值。只要有这两个值就可以了。所以,就生出了一个公式:dp[i]=dp[i]+dp[i-1] (第一个dp[i]是新的这一行新的值,第二个dp[i]是上面的那一行在对应这个位置的值(上边的值),dp[i-1]就是它左边的值。所以,这样就可以优化成功了。
    code
答案
public static int uniquePaths(int m, int n) {
    if (m <= 0 || n <= 0) {
        return 0;
    }

    int[] dp = new int[n]; // 
    // 初始化
    for(int i = 0; i < n; i++){
      dp[i] = 1;
    }

        // 公式:dp[i] = dp[i-1] + dp[i]
    for (int i = 1; i < m; i++) {
        // 第 i 行第 0 列的初始值
        dp[0] = 1;
        for (int j = 1; j < n; j++) {
            dp[j] = dp[j-1] + dp[j];
        }
    }
    return dp[n-1];
}

三、最小路径和

  1. 题目
  2. 解题步骤

a. dp[i][j]:为到(i,j)位置的路径的数字总和。
b. dp[i] [j] = min(dp[i-1][j],dp[i][j-1]) + arr[i][j];// arr[i][j] 表示网格中的值
c. dp[0][0] = arr[0][0] ;dp[i][0] = 1;,dp[0][i] = 1;

  1. 代码
    code
答案
class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int rows = grid.size();
        int cols = grid[0].size();
        if(rows<=0||cols<=0)
            return 0;
        vector<vector<int>> dp(rows,vector<int>(cols));
        dp[0][0] = grid[0][0];
        for(int i = 1;i<rows;i++)
        {
            dp[i][0] = dp[i-1][0]+grid[i][0];
        }
        for(int j = 1;j<cols;j++)
        {
            dp[0][j] = dp[0][j-1]+grid[0][j];
        }
        for(int i = 1;i<rows;i++)
        {
            for(int j =1;j<cols;j++)
            {
                dp[i][j] = min(dp[i-1][j],dp[i][j-1])+grid[i][j];
            }
        }
        return dp[rows-1][cols-1];
    }
};

四、编辑距离

  1. 题目
    在这里插入图片描述
  2. 解题步骤

a. 当字符串 word1 的长度为 i,字符串 word2 的长度为 j 时,将 word1 转化为 word2 所使用的最少操作次数为 dp[i] [j]。
b. dp[i][j] = min(dp[i-1][j-1],min(dp[i-1][j],dp[i][j-1])
c. dp[0][0] = 0;dp[0][j] = j;dp[i][0] = i;
在这里插入图片描述

  1. 代码
    code
答案
class Solution {
public:
    int minDistance(string word1, string word2) {
        int rows = word1.size();
        int cols = word2.size();
        if(rows*cols==0)
            return rows+cols;
        int dp[rows+1][cols+1];
        dp[0][0] = 0;
        for(int j = 0;j<=cols;j++)
        {
            dp[0][j] = j;//这个基本上就是插入操作,有多少个字符,插入多少次,所以,直接赋值j就可以
        }
        for(int i = 0;i<=rows;i++)
        {
            dp[i][0] = i;//这个是同上面的
        }
        for(int i = 1;i<=rows;i++)
        {
            for(int j= 1;j<=cols;j++)
            {
                if(word1[i-1]==word2[j-1])//若i-1这个位置==j-1这个位置的字符,则dp[i][j]= dp[i-1][j-1]
                    dp[i][j] = dp[i-1][j-1];
                else
                {
                    dp[i][j] = min(min(dp[i-1][j-1],dp[i][j-1]),dp[i-1][j])+1;
                }
            }

        }
        return dp[rows][cols];
    }
};
  1. 优化方法
    code
答案
class Solution {
public:
    int minDistance(string word1, string word2) {
        int n1 = word1.length();
        int n2 = word2.length();
        if(n1*n2==0)
            return n1+n2;
        int dp[n2+1];
        for(int j = 0;j<=n2;j++)//初始化
            dp[j] = j;
        for(int i = 1;i<=n1;i++)
        {
            int temp = dp[0];
            dp[0] = i;//要记得随时更新dp[0]的这个值
            for(int j = 1;j<=n2;j++)
            {
                int pre = temp;//pre相当于之前的dp[i-1][j-1]
                temp = dp[j];//将之前的dp[j]赋值给temp
                if(word1[i-1]==word2[j-1])//如果word1的i个字符和word2的第j个字符是相等的,那么就无需任何操作,该dp[i] = dp[i-1]
                    dp[j] = pre;
                else
                {
                    dp[j] = min(min(dp[j-1],pre),dp[j])+1;//否则就在三种操作中取最小的一种进行+1
                }


            }
        }
        return dp[n2];
    }
};

五、 零钱兑换

  1. 题目
    在这里插入图片描述
  2. 解题步骤
  1. dp[r] 代表要凑成r块钱,至少要多少个硬币。
  2. dp[i] = min(dp[i],1+dp[i-coin])
  3. dp[r] 都初始化为r+1 这也就是他们最多需要的硬币数+1,其他方案一定小于等于该硬币数。所以如果该初始化有被修改,则一定有硬币可以匹配,否则,则没有返回-1.
    在这里插入图片描述
  1. 代码
    code
答案
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int dp[amount+1];
        for(int i = 1;i<=amount;i++)
            dp[i] = amount+1;//初始化就赋的比他最多的情况还要多1,如果有被修改,就是修改后的值,如果没有,就是没有适合的硬币可用
        dp[0] = 0;
        for(int j = 1;j<=amount;j++)
        {
            for(int coin:coins)
            {
                if(j-coin<0)
                    continue;
                dp[j] = min(dp[j],1+dp[j-coin]);
            }
        }
        return dp[amount]==amount+1?-1:dp[amount];//若dp[amount]
    }
};

六、 最长递增子序列

  1. **题目
    在这里插入图片描述
  2. 解题步骤
  1. dp[i] :到第i个字符结尾代表的最长上升子序列是多少?
  2. dp[i] = if(nums[j]<nums[i]) dp[i]= max(dp[i],dp[j]+1)
  3. 所以0到nums.size()的初始值都赋值为1.
  1. 代码
    解法一
    code
答案
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        if(nums.size()==0)
            return 0;
        int dp[nums.size()+1];//dp[i]代表的是到第i个字符结束的最长子序列是多长?
        for(int i = 0;i<nums.size();i++)
        {
            dp[i] = 1;//在每个位置的初始化都为1
            for(int j = 0;j<i;j++)//然后从第0个位置到第i-1个位置开始,如果有比当前的i元素要小的,就是属于递增子序列中的一部分。
            {
                if(nums[j]<nums[i])//开始判断该元素是不是属于该递增子序列中的一部分。
                {
                    dp[i] = max(dp[i],dp[j]+1);//判断dp[i]的值
                }
            }
        }
        int maxValue = 0;
        for(int i = 0;i<nums.size();i++)
        {
            maxValue = max(maxValue,dp[i]);
        }
        return maxValue;
        
    }
};

解法二
code

答案
class Solution {
    int lengthOfLIS(vector<int>& nums) {
        /**
        dp[i]: 所有长度为i+1的递增子序列中, 最小的那个序列尾数.
        由定义知dp数组必然是一个递增数组, 可以用 maxL 来表示最长递增子序列的长度. 
        对数组进行迭代, 依次判断每个数num将其插入dp数组相应的位置:
        1. num > dp[maxL], 表示num比所有已知递增序列的尾数都大, 将num添加入dp
           数组尾部, 并将最长递增序列长度maxL加1
        2. dp[i-1] < num <= dp[i], 只更新相应的dp[i]
        **/
        int maxL = 0;
        int dp[nums.length];
        for(int num : nums) {
            // 二分法查找, 也可以调用库函数如binary_search
            int lo = 0, hi = maxL;
            while(lo < hi) {//这是遍历所有的dp呀  将每个新的num安排在合适的位置,虽然,最终的结果有可能是错的,但是个数会是对的
                int mid = lo+(hi-lo)/2;
                if(dp[mid] < num)//只要有元素小于当前的这个元素,
                    lo = mid+1;
                else
                    hi = mid;
            }
            dp[lo] = num;
            if(lo == maxL)//这样求出来,最终的顺序会是错误的,但是元素的个数确实正确的。
                maxL++;
        }
        return maxL;
    }
}

七、最长公共子序列

  1. 题目
    在这里插入图片描述
  2. 解题步骤
  1. dp[i][j] :到text1的第i个字符,到text2的第j个字符,这两个字符串最长的公共子序列。
  2. 在这里插入图片描述
  1. dp[i][0] = 0 dp[0][j] = 0
  1. code
答案
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int n1 = text1.size();
        int n2 = text2.size();
        if(n1*n2==0)
            return 0;
        int dp[n1+1][n2+1];
        for(int i = 0;i<=n1;i++)
        {
            dp[i][0] = 0;
        }
        for(int j = 0;j<=n2;j++)
        {
            dp[0][j] = 0;
        }
        for(int i = 1;i<=n1;i++)
        {
            for(int j = 1;j<=n2;j++)
            {
                if(text1[i-1]==text2[j-1])//判断i-1与j-1就是判断i,j的值,这个一定要切记
                {
                    dp[i][j] = dp[i-1][j-1]+1;
                }
                else
                {
                    dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
                }
            }
        }
        return dp[n1][n2];


    }
};

八、高楼扔鸡蛋问题

  1. 题目
    在这里插入图片描述

九. 最长回文字符串

题目
在这里插入图片描述
code

答案
class Solution {
public:
    string longestPalindrome(string s) {
        //使用动态规划的方式
        //1.dp[i][j]:代表从第i到第j个元素的字符串是否是回文字符串
        //2.dp[i][j]={
        //     a.   s[i]==s[j]  j-i<=2
        //     b.   s[i]==s[j]&&dp[i+1][j-1]?
        // }
         
        if(s.size()<=1)
            return s;
        int n = s.size();
        int dp[n][n];
        for(int i = 0;i<n;i++)
        {
            dp[i][i] = true;//只有一个元素的话,肯定是=true的
        }
        int maxLen = 1;
        int begin = 0;
        for(int L = 2;L<=n;L++)//这个L其实就是控制dp[i][j]有多少个元素的情况
        {
            for(int i = 0;i<n;i++)
            {
                int j = L+i-1;
                if(j>=n)
                    break;
                if(s[i]!=s[j])
                    dp[i][j] = false;
                else
                {
                    if(j-i<3)
                        dp[i][j] = true;
                    else
                    {
                        dp[i][j]= dp[i+1][j-1];
                    }

                }

                if(dp[i][j]&&(j-i+1)>maxLen)
                {
                    maxLen = j-i+1;
                    begin = i;
                }

            }
        }
        return s.substr(begin,maxLen);
    }
};

参考

  1. 告别动态规划,连刷 40 道题,我总结了这些套路,看不懂你打我(万字长文
posted @ 2022-02-28 23:03  idoe  阅读(81)  评论(0编辑  收藏  举报