【算法竞赛】动态规划·必考(附leetcode100经典练习)

目录

一、动态规划知识点

二、题目一

思路

代码

三、题目二

思路

代码

四、题目三

思路

代码


一、动态规划知识点

动态规划几乎是所有算法认证和竞赛中必考的一个知识点,将大问题分解成更简单的子问题,对整体问题的最优解决方案取决于子问题的最优解决方案。本文将详细介绍动态规划的知识点和经典例题。

1、动态规划一般用来解决什么问题?

常用于求解计数问题(求方案数)和最值问题(最大价值、最小花费)

如何识题?很简单的一个办法就是判断当前的结果是不是取决于当前的状态,并且当前的状态是不是和之前的状态有关(记忆化),如果是,基本就是用动态规划解题了。一个小tips就是dp[][]..[]=ans;dp的维度坐标存储的是状态,可能一个时刻是由多个角度的状态构成的,所以dp的维度可能是多维的,ans即当前状态对应的结果。

2、用动态规划求解的问题需要满足哪些条件

(1)重叠子问题:记录子问题的结果,每个子问题只计算一次

(2)最优子结构:大问题的最优解包含小问题的最优解,可以通过小问题的最优解推导出大问题的最优解

(3)无后效性:“未来与过去无关”(这个性质需要好好体会一下),这是dp的必要条件,只有这样才能降低算法的复杂度,应用dp才有意义。如果不满足无后效性,比如在计算斐波那契数列的fib(n)时,还要重新计算fib(n-1)和fib(n-2),时间并没有优化。换句话说,只关心前面的结果,不关心前面的过程,并且可以把前面的结果直接拿来用

3、动态规划的2种编程方法

(1)自顶向下与记忆化【递归】

先考虑大问题,再缩小到小问题。为了避免递归时重复计算子问题,可以在子问题得到解决时就保存结果(存入数组),再次需要这个结果时,直接返回保存的结果就可以了

(2)自底向上与制表递推【迭代】

先解决子问题,再递推到大问题,通常通过填写多维表格完成,编码时采用若干for循环语句填表,根据表中的结果,逐步计算出大问题的解决方案

4、动态规划有哪些优化方式?

如滚动数组缩小空间复杂度,将会在后面详细介绍

二、题目一

139. 单词拆分 - 力扣(LeetCode)

思路

1、这里dp主要的目的就是记忆化,记忆已经被识别的字符串,比不记忆每次遍历都从头比较一遍要快很多。枚举分割点是dp中常见的一种做法,因为当前状态不仅取决于上一个状态,而取决于之前的所有状态

代码

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        //枚举分割点
        //dp:已经被识别的字符串,记忆化
        int n=wordDict.size();
        unordered_map<string,int> have;//记录存在的单词数
        for(int i=0;i<n;i++) have.insert({wordDict[i],1});
        bool dp[310]={false};//dp[i]:截至到索引i的字符串是否已被识别
        dp[0]=true;//一个单词也没有肯定为true,向前垫一位
        for(int i=1;i<=s.size();i++)
        {
            //分割点
            for(int j=0;j<i;j++)
            {
                //i本身就像前推了一位
                if(dp[j]&&have.find(s.substr(j,i-j))!=have.end())
                {
                    dp[i]=true;
                    break;
                }
            }
        }
        return dp[s.size()];
    }
};

三、题目二

300. 最长递增子序列 - 力扣(LeetCode)

思路

1、注意一:这和枚举分割点一样,要枚举之前所有小于当前元素的位置,而不是仅仅和上一个更小的位置有关【一定要仔细想当前状态可能和前面的哪些状态有关】。枚举完以后,比较要不要把当前元素加在dp[j]这个序列后面,如果加就是dp[j]+1,如果不加就不管前面的,只等于自己自身的【初始化dp数组都为1】

2、注意二:最长长度不一定是最后一个位置的,要枚举所有位置的最长序列,然后找到最大值

3、注意三:不要一上来就对数组操作,一定要判断数组的特殊情况,比如这里的空数组,不然很可能会有些测试点通过不了

代码

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        if (n == 0) return 0; // 处理空数组的情况

        vector<int> dp(n, 1); // 初始化 dp 数组,每个元素初始值为 1

        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);
                }
            }
        }

        int max_length = 0;
        for (int len : dp) {
            max_length = max(max_length, len);
        }

        return max_length;
    }
};

四、题目三

152. 乘积最大子数组 - 力扣(LeetCode)

思路

1、当前乘积的值同样是和当前的状态有关,所以考虑动态规划,这题相对简单的一个点是规定了要求子序列连续,那么就不用枚举前面的所有分割点,只用考虑要不要追加到上一个状态后面。如果追加,就dp[i-1]*nums[i],如果不追加,当前i就没有上一个邻接点,就等于dp[i]

2、易错点,正负交替乘,负负得正,可能后面的最优解是建立在前面的最差解基础上的

3、如何解决正负交替相乘的问题?

ans=正数*num,num越大,ans越大

ans=负数*num,num越小,ans越大

所以分别记录两个状态数组,一个记录最大乘积,一个记录最小乘积

如果是正数,则和最大乘积相乘后进行比较

如果是负数,则和最小乘积相乘后进行比较

4、注意:const的定义放在类的外面,不能放在里面

const int N = 2e4 + 10; 在类内部作为数组大小时会出错,因为 C++ 中不允许在类中用 const 常量来定义数组大小【 C++ 中数组的大小通常需要是一个编译时常量 】

代码

const int N=2e4+10;
class Solution {
public:
    //易错点,正负交替乘,负负得正,可能后面的最优解是建立在前面的最差解基础上的
    //注意是连续子数组,所以不用枚举分割点
    int dp1[N];//最大乘积
    int dp2[N];//最小乘积
    //dp[i]:截止到索引i的最大乘积
    //对于每个dp都求最优解
    int maxProduct(vector<int>& nums) {
        //int dp1[N];//最大乘积
        //int dp2[N];//最小乘积
        int n=nums.size();
        if(n==1) return nums[0];
        //初始化
        for(int i=0;i<n;i++){dp1[i]=dp2[i]=nums[i];}
        for(int i=1;i<n;i++)
        {
            //1.当前数为正数
            //最大乘积去比较末尾的最大乘积
            //最小乘积去比较末尾的最小乘积
            if(nums[i]>=0)
            {
                dp1[i]=max(dp1[i],dp1[i-1]*nums[i]);
                dp2[i]=min(dp2[i],dp2[i-1]*nums[i]);
            }
            else
            {
                dp1[i]=max(dp1[i],dp2[i-1]*nums[i]);
                dp2[i]=min(dp2[i],dp1[i-1]*nums[i]);
            }
        }
        int ans=dp1[0];
        //找子数组的最大值
        for(int i=1;i<n;i++) 
        {
            ans=max(ans,dp1[i]);
        }
        return ans;
    }
};

参考:

【1.】罗勇军、郭卫斌《算法竞赛·上册》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值