剑指offer:动态规划

文章探讨了一系列基于动态规划和贪心算法的数组问题,包括寻找最长不含重复字符的子字符串、最长回文子串、跳台阶问题、买卖股票的最佳时机等。这些问题都涉及到了动态规划的子序列和状态转移,以及贪心算法的局部最优解组合成全局最优解的策略。文章还提到了空间复杂度和时间复杂度的要求,以及如何通过优化算法实现这些要求。
摘要由CSDN通过智能技术生成

JZ42 连续子数组的最大和(一)
简单 通过率:40.77% 时间限制:1秒 空间限制:64M
知识点动态规划贪心
描述
输入一个长度为n的整型数组array,数组中的一个或连续多个整数组成一个子数组,子数组最小长度为1。求所有子数组的和的最大值。
数据范围:
1<=n<=2×105
−100<=a[i]<=100

要求:时间复杂度为 O(n),空间复杂度为 O(n)
进阶:时间复杂度为 O(n),空间复杂度为 O(1)
示例1
输入:
[1,-2,3,10,-4,7,2,-5]
返回值:
18
说明:
经分析可知,输入数组的子数组[3,10,-4,7,2]可以求得最大和为18
示例2
输入:
[2]
返回值:
2
示例3
输入:
[-10]
返回值:
-10

方法一:贪心法

class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {
        int max = array[0];
        int sum = 0;
        for(int index = 0; index < array.size(); index++ ) {
            sum += array[index];
            //前面子数组的和加上array[index]后,sum变得更大了,更新最大值
            if ( max < sum ) {
                max = sum;
            }
            //前面子数组的和小于等于0,对后面结果是没有贡献的
            //子数组从下一个元素开始
            if (sum <= 0) {
                sum = 0;
            }
        }
        return max;
    }
};

方法二:动态规划

class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {
        //记录到下标i为止的最大连续子数组的和
        vector<int> dp(array.size(), 0);
        dp[0] = array[0];
        int max_sum = array[0];
        for(int i = 1; i < array.size(); i++){
            //状态转移公式:连续子数组的和的最大值
            dp[i] = max(dp[i - 1] + array[i], array[i]);
            //维护最大值
            if (max_sum < dp[i]) {
                max_sum = dp[i];
            }
        }
        return max_sum;
    }
};

JZ85 连续子数组的最大和(二)
中等 通过率:36.73% 时间限制:1秒 空间限制:256M
知识点贪心动态规划数组双指针
描述
输入一个长度为n的整型数组array,数组中的一个或连续多个整数组成一个子数组,找到一个具有最大和的连续子数组。
1.子数组是连续的,比如[1,3,5,7,9]的子数组有[1,3],[3,5,7]等等,但是[1,3,7]不是子数组
2.如果存在多个最大和的连续子数组,那么返回其中长度最长的,该题数据保证这个最长的只存在一个
3.该题定义的子数组的最小长度为1,不存在为空的子数组,即不存在[]是某个数组的子数组
4.返回的数组不计入空间复杂度计算

数据范围:
1<=n<=105
−100<=a[i]<=100

要求:时间复杂度O(n),空间复杂度O(n)
进阶:时间复杂度O(n),空间复杂度O(1)

示例1
输入:
[1,-2,3,10,-4,7,2,-5]
返回值:
[3,10,-4,7,2]

说明:
经分析可知,输入数组的子数组[3,10,-4,7,2]可以求得最大和为18,故返回[3,10,-4,7,2]
示例2
输入:
[1]
返回值:
[1]

示例3
输入:
[1,2,-3,4,-1,1,-3,2]
返回值:
[1,2,-3,4,-1,1]

说明:
经分析可知,最大子数组的和为4,有[4],[4,-1,1],[1,2,-3,4],[1,2,-3,4,-1,1],故返回其中长度最长的[1,2,-3,4,-1,1]
示例4
输入:
[-2,-1]
返回值:
[-1]

说明:
子数组最小长度为1,故返回[-1]

知识点:动态规划
动态规划算法的基本思想是:将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果。
思路:
既然是连续子数组,如果我们拿到了当前的和,对于后面一个即将加入的元素,如果加上他这一串会变得更大,我们肯定会加上它,如果它自己会比加上前面这一串更大,说明从它自己开始连续子数组的和可能会更大。
那我们可以用dp数组表示以下标i为终点的最大连续子数组和,则每次遇到一个新的数组元素,连续的子数组要么加上变得更大,要么它本身就更大,因此状态转移为dp[i]=max(dp[i−1]+array[i],array[i]),这是最基本的求连续子数组的最大和。
但是题目要求需要返回长度最长的一个,我们则每次用left、right记录该子数组的起始,需要更新最大值的时候(要么子数组和更大,要么子数组和相等的情况下区间要更长)顺便更新最终的区间首尾,这样我们的区间长度就是最长的。
具体做法:
step 1:创建动态规划辅助数组,记录到下标i为止的最大连续子数组和,下标为0的时候,肯定等于原数组下标为0的元素。
step 2:准备左右区间双指针记录每次连续子数组的首尾,再准备两个双指针记录最大和且区间最长的连续子数组的首尾。
step 3:遍历数组,对于每个元素用上述状态转移公式记录其dp值,更新区间首尾(如果需要)。
step 4:出现一个最大值。且区间长度更大的时候,更新记录最长区间的双指针。
step 5:根据记录的最长子数组的位置取数组。

class Solution {
public:
    vector<int> FindGreatestSumOfSubArray(vector<int>& array) {
        vector<int> res;
        //记录到下标i为止的最大连续子数组的和
        vector<int> dp(array.size(), 0);
        dp[0] = array[0];
        int max_sum = array[0];
        //子数组区间
        int left = 0;
        int right = 0;
        //最长的子数组区间
        int max_left = 0;
        int max_right = 0;
        for(int i = 1; i < array.size(); i++){
            //右边界
            right++;
            //左边界
            if(dp[i - 1]  <  0) {
                //子数组从当前元素开始
                left = right;
             }
            
             //状态转移公式:连续子数组和最大值
            dp[i] = max(dp[i - 1] + array[i], array[i]);


            //当有新的子数组的和的最大值出现时,更新最大值和最大区间
            if(dp[i] > max_sum) {
                max_sum = dp[i];
                max_left = left;
                max_right = right;
            }

            //当多个子数组的和的最大值相同时,更新最大区间
            if(dp[i] == max_sum && (right - left + 1) > (max_right - max_left + 1)) {
                max_left = left;
                max_right = right;
            }
        }
        //获取元素
        for(int i = max_left; i <= max_right; i++) {
            res.push_back(array[i]);
        }
        return res;
    }
};

JZ69 跳台阶
简单 通过率:40.89% 时间限制:1秒 空间限制:256M
知识点递归动态规划记忆化搜索
描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

数据范围:1≤n≤40
要求:时间复杂度:O(n) ,空间复杂度: O(1)JZ48 最长不含重复字符的子字符串.note
示例1
输入:
2
返回值:
2
说明:
青蛙要跳上两级台阶有两种跳法,分别是:先跳一级,再跳一级或者直接跳两级。因此答案为2
示例2
输入:
7
返回值:
21

// 1、 2、 3、5、8、13、21、34 …

class Solution {
public:
    int jumpFloor(int number){
        if (number == 1) {
            return 1;
        }
        if (number == 2) {
            return 2;
        }
        int one = 1;
        int two = 2;
        int three;
        for (int i = 3; i <= number; i++) {  
            three = two + one;  
            one = two;  
            two = three;  
        }  
        return three;  
    } 
};

JZ10 斐波那契数列
入门 通过率:36.65% 时间限制:1秒 空间限制:256M
知识点数组动态规划记忆化搜索快速幂递归
描述
大家都知道斐波那契数列,现在要求输入一个正整数 n ,请你输出斐波那契数列的第 n 项。
斐波那契数列是一个满足 fib(x)={1fib(x−1)+fib(x−2)​x=1,2x>2​ 的数列
数据范围:1≤n≤40
要求:空间复杂度 O(1),时间复杂度 O(n) ,本题也有时间复杂度 O(logn) 的解法

输入描述:
一个正整数n
返回值描述:
输出一个正整数。
示例1
输入:
4
返回值:
3
说明:
根据斐波那契数列的定义可知,fib(1)=1,fib(2)=1,fib(3)=fib(3-1)+fib(3-2)=2,fib(4)=fib(4-1)+fib(4-2)=3,所以答案为3。
示例2
输入:
1
返回值:
1
示例3
输入:
2
返回值:
1

F(0)=1,F(1)=1,F(2)=2,F(3)=3,F(4)=5 …
F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)

//a3 = a2 + a1 (1)
//a4 = a3 + a2 (2)
//a5 = a4 + a3 (3)
//(1)中的a3和a2在(2)中用到
//(2)中的a4和a3在(3)中用到
//为了便于循环
//第一步:被加数 赋值给 加数
//第二步:和 赋值给 被加数
//第一步和第二步的顺序不能颠倒,也就是 和、被加数 向后平移,相加;然后 和、倍加数向后平移,相加;以此循环
//1、1、2、3、5、8、13、21、34 …

class Solution {  
public:  
    int Fibonacci(int n){
        if (n == 1){
            return 1;
        }
        if (n == 2) {
            return 1;
        }
        int one = 1;
        int two = 1;
        int three;
        for ( int i = 3; i <= n; i++ ){
            three = two + one;
            one = two;
            two = three;
        }
        return three;
    }
}; 

JZ71 跳台阶扩展问题
简单 通过率:42.66% 时间限制:1秒 空间限制:64M
知识点动态规划递归记忆化搜索
描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶(n为正整数)总共有多少种跳法。

数据范围:1≤n≤20
进阶:空间复杂度 O(1) , 时间复杂度 O(1)
示例1
输入:
3
返回值:
4
示例2
输入:
1
返回值:
1
/*
解答:

也可以用逆推的思路去想,跳n级台阶,
可以从n-1级跳上来,从n-2级跳上来,从n-3级跳上来,…,从第1级跳上来;或直接跳上去,即从第0级跳上来

跳n级台阶的方法数相当于其它所有台阶数的方法的总和,加上从第0级跳上来

表达式为 f(n) = f(n-1) + f(n-2) +…+ f(2) + f(1) + 1。

例如:

当跳1级台阶时,f(1) = 1;

当跳2级台阶时,f(2) = f(1) + 1 = 2;

当跳3级台阶时,f(3) = f(2) + f(1) + 1 = 4;

当跳4级台阶时,f(4) = f(3) + f(2) + f(1) + 1 = 8;

f(n) = f(n-1) + f(n-2) +…+ f(2) + f(1) + 1 (1)

f(n-1) = f(n-2) +…+ f(2) + f(1) + 1 (2)

由(1) - (2)得到

f(n) - f(n-1) = f(n-1) ===》 f(n) = 2 * f(n-1)
*/

class Solution {
public:
    int jumpFloorII(int number){
        if (number == 1){
            return 1;
        }
        int res = 1;
        for (int i = 2; i <= number; i++){
            res *= 2;
        }
        return res;
    }
};

JZ70 矩形覆盖
中等 通过率:36.72% 时间限制:1秒 空间限制:64M
知识点递归动态规划
描述
我们可以用 21 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 21 的小矩形无重叠地覆盖一个 2*n 的大矩形,从同一个方向看总共有多少种不同的方法?

数据范围:0≤n≤38
进阶:空间复杂度 O(1) ,时间复杂度 O(n)

注意:约定 n == 0 时,输出 0

比如n=3时,2*3的矩形块有3种不同的覆盖方法(从同一个方向看):

输入描述:
21的小矩形的总个数n
返回值描述:
覆盖一个2
n的大矩形总共有多少种不同的方法(从同一个方向看)
示例1
输入:
0
返回值:
0
示例2
输入:
1
返回值:
1
示例3
输入:
4
返回值:
5

class Solution {
public:
    int rectCover(int number){
        if (number == 0) {
            return 0;
        }
        if (number == 1){
            return 1; 
        }
        if (number == 2){
            return 2;
        }
        int one = 1;
        int two = 2;
        int three;
        for ( int i = 3; i <= number; i++ ){  
            three = two + one;
            one = two;
            two = three;
        }
        return three;
    }
};

JZ63 买卖股票的最好时机(一)
简单 通过率:54.27% 时间限制:1秒 空间限制:256M
知识点动态规划贪心
描述
假设你有一个数组prices,长度为n,其中prices[i]是股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益
1.你可以买入一次股票和卖出一次股票,并非每天都可以买入或卖出一次,总共只能买入和卖出一次,且买入必须在卖出的前面的某一天
2.如果不能获取到任何利润,请返回0
3.假设买入卖出均无手续费

数据范围: 0≤n≤105,0≤val≤104
要求:空间复杂度 O(1),时间复杂度 O(n)
示例1
输入:
[8,9,2,5,4,7,1]
返回值:
5

说明:
在第3天(股票价格 = 2)的时候买入,在第6天(股票价格 = 7)的时候卖出,最大利润 = 7-2 = 5 ,不能选择在第2天买入,第3天卖出,这样就亏损7了;同时,你也不能在买入前卖出股票。
示例2
输入:
[2,4,1]
返回值:
2
示例3
输入:
[3,2,1]
返回值:
0

/*
知识点:贪心思想
贪心思想属于动态规划思想中的一种,其基本原理是找出整体当中给的每个局部子结构的最优解,并且最终将所有的这些局部最优解结合起来形成整体上的一个最优解。
思路:
如果我们在某一天卖出了股票,那么要想收益最高,一定是它前面价格最低的那天买入的股票才可以。

因此我们可以利用贪心思想解决,每次都将每日收入与最低价格相减维护最大值。

具体做法:
step 1:首先排除数组为空的特殊情况。
step 2:将第一天看成价格最低,后续遍历的时候遇到价格更低则更新价格最低。
step 3:每次都比较最大收益与当日价格减去价格最低的值,选取最大值作为最大收益。
*/

class Solution {  
public:  
    int maxProfit(vector<int> &prices) {  
        if(prices.empty()) {
            return 0;
        }
        int min_price = prices[0];
        int max_profit = 0;
        for (int i = 1; i < prices.size(); i++) {
            if (min_price > prices[i]) {
                // 当前价格比最低价格小,更新最低价格,按照最低价格买入
                min_price = prices[i];
            } else {
                // 当前价格比最低价格大,判断是否卖出
                if (max_profit < prices[i] - min_price) {
                    // 如果利润更大,更新利润,按照当前价格卖出
                    max_profit = prices[i] - min_price;
                }
            }
        }
        return max_profit;
    }  
};  

BM81 买卖股票的最好时机(二):多次买卖股票
中等 通过率:63.60% 时间限制:1秒 空间限制:256M
知识点贪心动态规划
描述
假设你有一个数组prices,长度为n,其中prices[i]是某只股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益

  1. 你可以多次买卖该只股票,但是再次购买前必须卖出之前的股票
  2. 如果不能获取收益,请返回0
  3. 假设买入卖出均无手续费

数据范围: 1≤n≤1×105 , 1≤prices[i]≤104
要求:空间复杂度 O(n),时间复杂度 O(n)
进阶:空间复杂度 O(1),时间复杂度 O(n)
示例1
输入:
[8,9,2,5,4,7,1]
返回值:
7
说明:
在第1天(股票价格=8)买入,第2天(股票价格=9)卖出,获利9-8=1
在第3天(股票价格=2)买入,第4天(股票价格=5)卖出,获利5-2=3
在第5天(股票价格=4)买入,第6天(股票价格=7)卖出,获利7-4=3
总获利1+3+3=7,返回7
示例2
输入:
[5,4,3,2,1]
返回值:
0

说明:
由于每天股票都在跌,因此不进行任何交易最优。最大收益为0。
示例3
输入:
[1,2,3,4,5]
返回值:
4

说明:
第一天买进,最后一天卖出最优。中间的当天买进当天卖出不影响最终结果。最大收益为4。
备注:
总天数不大于200000。保证股票每一天的价格在[1,100]范围内。

知识点:贪心思想
贪心思想属于动态规划思想中的一种,其基本原理是找出整体当中给的每个局部子结构的最优解,并且最终将所有的这些局部最优解结合起来形成整体上的一个最优解。
思路:
其实我们要想获取最大收益,只需要在低价买入高价卖出就可以了,因为可以买卖多次。利用贪心思想:只要一段区间内价格是递增的,那么这段区间的差值就是我们可以有的收益。
具体做法:
step 1:遍历数组,只要数组后一个比前一个更大,就可以有收益。
step 2:将收益累加,得到最终结果。

/*
这题可以用贪心法求解。
策略:
只要后一天的股票价格比前一天高,就在前一天买入,后一天卖出;
反之什么也不做。
输入:
[1,2,3,4,5]
返回值:
4
说明:
第一天买进,最后一天卖出最优。
中间的当天买进当天卖出不影响最终结果。最大收益为4。 
*/
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if (prices.empty()) {
            return 0;
        }
        int max_profit = 0;
        for (int index = 1; index < prices.size(); index++) {
            if (prices[index - 1] < prices[index]) {
                max_profit += prices[index] - prices[index - 1];
            }
        }
        return max_profit;
    }
};

JZ48 最长不含重复字符的子字符串
中等 通过率:40.11% 时间限制:1秒 空间限制:256M
知识点字符串哈希双指针
描述
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
数据范围:
s.length≤40000
示例1
输入:
“abcabcbb”
返回值:
3

说明:
因为无重复字符的最长子串是"abc",所以其长度为 3。
示例2
输入:
“bbbbb”
返回值:
1

说明:
因为无重复字符的最长子串是"b",所以其长度为 1。
示例3
输入:
“pwwkew”
返回值:
3

说明:
因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是子串的长度,“pwke” 是一个子序列,不是子串。

方法:滑动窗口+哈希表(推荐使用)

知识点1:滑动窗口
滑动窗口是指在数组、字符串、链表等线性结构上的一段,类似一个窗口,而这个窗口可以依次在上述线性结构上从头到尾滑动,且窗口的首尾可以收缩。
我们在处理滑动窗口的时候,常用双指针来解决,左指针维护窗口左界,右指针维护窗口右界,二者同方向不同速率移动维持窗口。

知识点2:哈希表
哈希表是一种根据关键码(key)直接访问值(value)的一种数据结构。
而这种直接访问意味着只要知道key就能在O(1)时间内得到value,因此哈希表常用来统计频率、快速检验某个元素是否出现过等。

思路:
既然要找一段连续子串的内不重复的长度,我们可以使用滑动窗口,保证窗口内都是不重复的,然后窗口右界不断向右滑,如果窗口内出现了重复字符,
说明新加入的元素与之前的重复了,只需要窗口左界也向右收缩就可以保证窗口内都是不重复的。
而保证窗口内的元素不重复,我们可以使用根据key值快速访问的哈希表,key值为窗口内的元素,value为其出现次数,只要新加入窗口的元素出现次数不为1,就是重复。

123 while(mp.get(s.charAt(right)) > 1) //窗口左移,同时减去该数字的出现次数 mp.put(s.charAt(left), mp.get(s.charAt(left++)) - 1);
具体做法:
step 1:构建一个哈希表,用于统计字符元素出现的次数。
step 2:窗口左右界都从字符串首部开始,每次窗口优先右移右界,并统计进入窗口的元素的出现频率。
step 3:一旦右界元素出现频率大于1,就需要右移左界直到窗口内不再重复,将左边的元素移除窗口的时候同时需要将它在哈希表中的频率减1,保证哈希表中的频率都是窗口内的频率。
step 4:每轮循环,维护窗口长度最大值。

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param s string字符串 
     * @return int整型
     */
    int lengthOfLongestSubstring(string s) {
        //哈希表记录窗口内非重复的字符及其数量
        unordered_map<char, int> m;
        int max = 0;
        for (int left = 0, right = 0; right < s.length(); ++right) {
            m[s[right]]++;
            while (m[s[right]] > 1) {
                m[s[left]]--;
                left++;
            }
            if (max < right - left + 1) {
                max = right - left + 1;
            }
        }
        return max;
    }
};

JZ47 礼物的最大价值
中等 通过率:57.85% 时间限制:1秒 空间限制:256M
知识点动态规划数组
描述
在一个m×n的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
如输入这样的一个二维数组,
[
[1,3,1],
[1,5,1],
[4,2,1]
]
那么路径 1→3→5→2→1 可以拿到最多价值的礼物,价值为12
示例1
输入:
[[1,3,1],[1,5,1],[4,2,1]]
返回值:
12

备注:
∙ 0<grid.length≤200
∙ 0<grid[0].length≤200

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param grid int整型vector<vector<>> 
     * @return int整型
     */
    int maxValue(vector<vector<int> >& grid) {
        // 状态转移公式:dp[i][j] = grid[i][j] + max(dp[i-1][j], dp[i][j-1])
        int row = grid.size();
        int col = grid[0].size();
        // 第一列只能来自上边
        for (int i = 1; i < row; ++i) {
            grid[i][0] += grid[i-1][0];
        }
        // 第一行只能来自左边
        for (int i = 1; i < col; ++i) {
            grid[0][i] += grid[0][i-1];
        }
        // 遍历每一个位置
        for (int i = 1; i < row; ++i) {
            for (int j = 1; j < col; ++j) {
                grid[i][j] += max(grid[i-1][j], grid[i][j-1]);
            }
        }
        return grid[row-1][col-1];
    }
};

BM78 打家劫舍(一)
中等 通过率:41.93% 时间限制:1秒 空间限制:256M
知识点动态规划
描述
你是一个经验丰富的小偷,准备偷沿街的一排房间,每个房间都存有一定的现金,为了防止被发现,你不能偷相邻的两家,即,如果偷了第一家,就不能再偷第二家;如果偷了第二家,那么就不能偷第一家和第三家。
给定一个整数数组nums,数组中的元素表示每个房间存有的现金数额,请你计算在不被发现的前提下最多的偷窃金额。

数据范围:数组长度满足 1≤n≤2×105 ,数组中每个值满足 1≤num[i]≤5000
示例1
输入:
[1,2,3,4]
返回值:
6

说明:
最优方案是偷第 2,4 个房间
示例2
输入:
[1,3,6]
返回值:
7

说明:
最优方案是偷第 1,3个房间
示例3
输入:
[2,10,5]
返回值:
10

说明:
最优方案是偷第 2 个房间

思路:
或许有人认为利用贪心思想,偷取最多人家的钱就可以了,要么偶数家要么奇数家全部的钱,但是有时候会为了偷取更多的钱,或许可能会连续放弃两家不偷,因此这种方案行不通,我们依旧考虑动态规划。
具体做法:

数值:2 2 2 2 2

下标:0 1 2 3 4

长度:1 2 3 4 5
i=4
dp[1] dp[2] dp[3] dp[4] dp[5]

如果偷nums[3], nums[2]不能偷,最大收益是 nums[3] + dp[2]
如果不偷nums[3],nums[2]可以偷,可以不偷,最大收益是 dp[3]

状态转移公式:
dp[4] = max(dp[3], nums[3] + dp[2]);

step 1:用dp[i]表示长度为i的数组,最多能偷取到多少钱,
只要每次转移状态逐渐累加就可以得到整个数组能偷取的钱。

step 2:(初始状态) 如果数组长度为1,只有一家人,肯定是把这家人偷了,收益最大,
因此dp[1]=nums[0]。

step 3:(状态转移) 每次对于一个人家,我们选择偷他或者不偷他,

如果选择偷,那么他的前一家必定不能偷,因此累加上上级的最多收益:nums[i−1]+dp[i−2]

如果选择不偷,那我们最多可以累加上一级的收益:dp[i−1]

因此转移方程为dp[i]=max(dp[i−1], nums[i−1]+dp[i−2])。

这里的i在dp中为数组长度,在nums中为下标。

class Solution {
public:
    int rob(vector<int>& nums) {
        //dp[i]表示长度为i的数组,最多能偷取多少钱
        vector<int> dp(nums.size() + 1, 0); 
        //长度为1只能偷第一家
        dp[1] = nums[0];
        for(int i = 2; i <= nums.size(); i++) {
            //对于每家可以选择偷或者不偷
            dp[i] = max(dp[i - 1], nums[i - 1] + dp[i - 2]); 
        }
        return dp[nums.size()];
    }
};

BM79 打家劫舍(二)
中等 通过率:43.94% 时间限制:2秒 空间限制:256M
知识点动态规划
描述
你是一个经验丰富的小偷,准备偷沿湖的一排房间,每个房间都存有一定的现金,为了防止被发现,你不能偷相邻的两家,即,如果偷了第一家,就不能再偷第二家,如果偷了第二家,那么就不能偷第一家和第三家。沿湖的房间组成一个闭合的圆形,即第一个房间和最后一个房间视为相邻。
给定一个长度为n的整数数组nums,数组中的元素表示每个房间存有的现金数额,请你计算在不被发现的前提下最多的偷窃金额。

数据范围:数组长度满足 1≤n≤2×105 ,数组中每个值满足 1≤nums[i]≤5000
示例1
输入:
[1,2,3,4]
返回值:
6

说明:
最优方案是偷第 2 4 个房间
示例2
输入:
[1,3,6]
返回值:
6

说明:
由于 1 和 3 是相邻的,因此最优方案是偷第 3 个房间

思路:
这道题与BM78.打家劫舍(一)比较类似,区别在于这道题是环形,第一家和最后一家是相邻的,既然如此,在原先的方案中第一家和最后一家不能同时取到。
具体做法:

step 1:使用原先的方案是:用dp[i]表示长度为i的数组,最多能偷取到多少钱,只要每次转移状态逐渐累加就可以得到整个数组能偷取的钱。

step 2:(初始状态) 如果数组长度为1,只有一家人,肯定是把这家人偷了,收益最大,因此dp[1]=nums[0]。

step 3:(状态转移) 每次对于一个人家,我们选择偷他或者不偷他,如果我们选择偷那么前一家必定不能偷,因此累加的上上级的最多收益,同理如果选择不偷他,那我们最多可以累加上一级的收益。因此转移方程为dp[i]=max(dp[i−1],nums[i−1]+dp[i−2])。这里的i在dp中为数组长度,在nums中为下标。

step 4:此时第一家与最后一家不能同时取到,那么我们可以分成两种情况讨论:
情况1:偷第一家的钱,不偷最后一家的钱。初始状态与状态转移不变,只是遍历的时候数组最后一位不去遍历。
情况2:偷最后一家的请,不偷第一家的钱。初始状态就设定了dp[1]=0,第一家就不要了,然后遍历的时候也会遍历到数组最后一位。

step 5:最后取两种情况的较大值即可。

class Solution {
public:
    int rob(vector<int>& nums) {
        //dp[i]表示长度为i的数组,最多能偷取多少钱
        vector<int> dp(nums.size() + 1, 0); 


        //选择偷第一家
        dp[1] = nums[0]; 
        //不能偷最后一家
        for(int i = 2; i < nums.size(); i++) {
            //对于每家可以选择偷或者不偷
            dp[i] = max(dp[i - 1], nums[i - 1] + dp[i - 2]); 
        }
        int first = dp[nums.size() - 1]; 


        //清除dp数组,第二次循环
        dp.clear(); 


        //选择不偷第一家
        dp[1] = 0; 
        //可以偷最后一家
        for(int i = 2; i <= nums.size(); i++) {
            //对于每家可以选择偷或者不偷
            dp[i] = max(dp[i - 1], nums[i - 1] + dp[i - 2]);
        }
        int second = dp[nums.size()];

        
        //选择最大值
        return max(first, second); 
    }
};

BM78 打家劫舍(一)
中等 通过率:41.93% 时间限制:1秒 空间限制:256M
知识点动态规划
描述
你是一个经验丰富的小偷,准备偷沿街的一排房间,每个房间都存有一定的现金,为了防止被发现,你不能偷相邻的两家,即,如果偷了第一家,就不能再偷第二家;如果偷了第二家,那么就不能偷第一家和第三家。
给定一个整数数组nums,数组中的元素表示每个房间存有的现金数额,请你计算在不被发现的前提下最多的偷窃金额。

数据范围:数组长度满足 1≤n≤2×105 ,数组中每个值满足 1≤num[i]≤5000
示例1
输入:
[1,2,3,4]
返回值:
6

说明:
最优方案是偷第 2,4 个房间
示例2
输入:
[1,3,6]
返回值:
7

说明:
最优方案是偷第 1,3个房间
示例3
输入:
[2,10,5]
返回值:
10

说明:
最优方案是偷第 2 个房间

思路:
或许有人认为利用贪心思想,偷取最多人家的钱就可以了,要么偶数家要么奇数家全部的钱,但是有时候会为了偷取更多的钱,或许可能会连续放弃两家不偷,因此这种方案行不通,我们依旧考虑动态规划。
具体做法:

数值:2 2 2 2 2

下标:0 1 2 3 4

长度:1 2 3 4 5
i=4
dp[1] dp[2] dp[3] dp[4] dp[5]

如果偷nums[3], nums[2]不能偷,最大收益是 nums[3] + dp[2]
如果不偷nums[3],nums[2]可以偷,可以不偷,最大收益是 dp[3]

状态转移公式:
dp[4] = max(dp[3], nums[3] + dp[2]);

step 1:用dp[i]表示长度为i的数组,最多能偷取到多少钱,
只要每次转移状态逐渐累加就可以得到整个数组能偷取的钱。

step 2:(初始状态) 如果数组长度为1,只有一家人,肯定是把这家人偷了,收益最大,
因此dp[1]=nums[0]。

step 3:(状态转移) 每次对于一个人家,我们选择偷他或者不偷他,

如果选择偷,那么他的前一家必定不能偷,因此累加上上级的最多收益:nums[i−1]+dp[i−2]

如果选择不偷,那我们最多可以累加上一级的收益:dp[i−1]

因此转移方程为dp[i]=max(dp[i−1], nums[i−1]+dp[i−2])。

这里的i在dp中为数组长度,在nums中为下标。

class Solution {
public:
    int rob(vector<int>& nums) {
        //dp[i]表示长度为i的数组,最多能偷取多少钱
        vector<int> dp(nums.size() + 1, 0); 
        //长度为1只能偷第一家
        dp[1] = nums[0];
        for(int i = 2; i <= nums.size(); i++) {
            //对于每家可以选择偷或者不偷
            dp[i] = max(dp[i - 1], nums[i - 1] + dp[i - 2]); 
        }
        return dp[nums.size()];
    }
};

BM79 打家劫舍(二)
中等 通过率:43.94% 时间限制:2秒 空间限制:256M
知识点动态规划
描述
你是一个经验丰富的小偷,准备偷沿湖的一排房间,每个房间都存有一定的现金,为了防止被发现,你不能偷相邻的两家,即,如果偷了第一家,就不能再偷第二家,如果偷了第二家,那么就不能偷第一家和第三家。沿湖的房间组成一个闭合的圆形,即第一个房间和最后一个房间视为相邻。
给定一个长度为n的整数数组nums,数组中的元素表示每个房间存有的现金数额,请你计算在不被发现的前提下最多的偷窃金额。

数据范围:数组长度满足 1≤n≤2×105 ,数组中每个值满足 1≤nums[i]≤5000
示例1
输入:
[1,2,3,4]
返回值:
6

说明:
最优方案是偷第 2 4 个房间
示例2
输入:
[1,3,6]
返回值:
6

说明:
由于 1 和 3 是相邻的,因此最优方案是偷第 3 个房间

思路:
这道题与BM78.打家劫舍(一)比较类似,区别在于这道题是环形,第一家和最后一家是相邻的,既然如此,在原先的方案中第一家和最后一家不能同时取到。
具体做法:

step 1:使用原先的方案是:用dp[i]表示长度为i的数组,最多能偷取到多少钱,只要每次转移状态逐渐累加就可以得到整个数组能偷取的钱。

step 2:(初始状态) 如果数组长度为1,只有一家人,肯定是把这家人偷了,收益最大,因此dp[1]=nums[0]。

step 3:(状态转移) 每次对于一个人家,我们选择偷他或者不偷他,如果我们选择偷那么前一家必定不能偷,因此累加的上上级的最多收益,同理如果选择不偷他,那我们最多可以累加上一级的收益。因此转移方程为dp[i]=max(dp[i−1],nums[i−1]+dp[i−2])。这里的i在dp中为数组长度,在nums中为下标。

step 4:此时第一家与最后一家不能同时取到,那么我们可以分成两种情况讨论:
情况1:偷第一家的钱,不偷最后一家的钱。初始状态与状态转移不变,只是遍历的时候数组最后一位不去遍历。
情况2:偷最后一家的请,不偷第一家的钱。初始状态就设定了dp[1]=0,第一家就不要了,然后遍历的时候也会遍历到数组最后一位。

step 5:最后取两种情况的较大值即可。

class Solution {
public:
    int rob(vector<int>& nums) {
        //dp[i]表示长度为i的数组,最多能偷取多少钱
        vector<int> dp(nums.size() + 1, 0); 


        //选择偷第一家
        dp[1] = nums[0]; 
        //不能偷最后一家
        for(int i = 2; i < nums.size(); i++) {
            //对于每家可以选择偷或者不偷
            dp[i] = max(dp[i - 1], nums[i - 1] + dp[i - 2]); 
        }
        int first = dp[nums.size() - 1]; 


        //清除dp数组,第二次循环
        dp.clear(); 


        //选择不偷第一家
        dp[1] = 0; 
        //可以偷最后一家
        for(int i = 2; i <= nums.size(); i++) {
            //对于每家可以选择偷或者不偷
            dp[i] = max(dp[i - 1], nums[i - 1] + dp[i - 2]);
        }
        int second = dp[nums.size()];

        
        //选择最大值
        return max(first, second); 
    }
};

BM74 数字字符串转化成IP地址
中等 通过率:36.40% 时间限制:1秒 空间限制:256M
知识点字符串回溯
描述
现在有一个只包含数字的字符串,将该字符串转化成IP地址的形式,返回所有可能的情况。
例如:
给出的字符串为"25525522135",
返回[“255.255.22.135”, “255.255.221.35”]. (顺序没有关系)

数据范围:字符串长度 0≤n≤12
要求:空间复杂度 O(n!),时间复杂度 O(n!)

注意:ip地址是由四段数字组成的数字序列,格式如 “x.x.x.x”,其中 x 的范围应当是 [0,255]。

示例1
输入:
“25525522135”
返回值:
[“255.255.22.135”,“255.255.221.35”]
示例2
输入:
“1111”
返回值:
[“1.1.1.1”]
示例3
输入:
“000256”
返回值:
[]

class Solution {
public:
    vector<string> restoreIpAddresses(string s) {
        vector<string> res;
        int n = s.length();
        //遍历IP的点
        //第一个点的位置
        for(int i = 1; i < 4 && i < n - 2; i++){ 
            //第二个点的位置
            for(int j = i + 1; j < i + 4 && j < n - 1; j++){ 
                //第三个点的位置
                for(int k = j + 1; k < j + 4 && k < n; k++){
                    //最后一段剩余数字不能超过3
                    if(n - k > 3) {
                        continue; 
                    }

                    //从点的位置分段截取
                    string a = s.substr(0, i);
                    string b = s.substr(i, j - i);
                    string c = s.substr(j, k - j);
                    string d = s.substr(k);

                    //IP每个数字不大于255
                    if(stoi(a) > 255 || stoi(b) > 255 || stoi(c) > 255 || stoi(d) > 255) {
                        continue;
                    }
                    //排除前导0的情况
                    if((a.length() != 1 && a[0] == '0') || (b.length() != 1 && b[0] == '0') ||  (c.length() != 1 && c[0] == '0') || (d.length() != 1 && d[0] == '0')) {
                        continue;
                    }

                    
                    //组装IP地址
                    string temp = a + "." + b + "." + c + "." + d; 
                    res.push_back(temp);
                }
            }
        }
        return res;
    }
};

BM73 最长回文子串
中等 通过率:38.00% 时间限制:1秒 空间限制:256M
知识点字符串动态规划
描述
对于长度为n的一个字符串A(仅包含数字,大小写英文字母),请设计一个高效算法,计算其中最长回文子串的长度。

数据范围: 1≤n≤1000
要求:空间复杂度 O(1),时间复杂度 O(n2)
进阶: 空间复杂度 O(n),时间复杂度 O(n)
示例1
输入:
“ababc”
返回值:
3

说明:
最长的回文子串为"aba"与"bab",长度都为3
示例2
输入:
“abbba”
返回值:
5

示例3
输入:
“b”
返回值:
1

知识点:贪心思想
贪心思想属于动态规划思想中的一种,其基本原理是找出整体当中给的每个局部子结构的最优解,并且最终将所有的这些局部最优解结合起来形成整体上的一个最优解。
思路:
回文串,有着左右对称的特征,从首尾一起访问,遇到的元素都是相同的。但是我们这里正是要找最长的回文串,并不事先知道长度,怎么办?判断回文的过程是从首尾到中间,那我们找最长回文串可以逆着来,从中间延伸到首尾,这就是中心扩展法。
具体做法:
step 1:遍历字符串每个字符。
step 2:以每次遍历到的字符为中心(分奇数长度和偶数长度两种情况),不断向两边扩展。
step 3:如果两边都是相同的就是回文,不断扩大到最大长度即是以这个字符(或偶数两个)为中心的最长回文子串。
step 4:我们比较完每个字符为中心的最长回文子串,取最大值即可。

class Solution {
public:
    int fun(string& s, int begin, int end){
        //每个中心点开始扩展
        while(begin >= 0 && end < s.length() && s[begin] == s[end]){ 
            begin--;
            end++;
        }
        //返回长度
        return end - begin - 1; 
    } 
    int getLongestPalindrome(string A) {
        int maxlen = 1;
        //以每个点为中心
        for(int i = 0; i < A.length() - 1; i++) {
            //分为奇数长度和偶数长度,向两边扩展
            maxlen = max(maxlen, max(fun(A, i, i), fun(A, i, i + 1))); 
        }
        return maxlen;
    }
};

BM66 最长公共子串
中等 通过率:34.49% 时间限制:1秒 空间限制:256M
知识点动态规划
描述
给定两个字符串str1和str2,输出两个字符串的最长公共子串
题目保证str1和str2的最长公共子串存在且唯一。

数据范围: 1≤∣str1∣,∣str2∣≤5000
要求: 空间复杂度 O(n2),时间复杂度 O(n2)
示例1
输入:
“1AB2345CD”,“12345EF”
返回值:
“2345”

备注:
1≤∣str1​∣,∣str2​∣≤5000

class Solution {
public:
    string LCS(string str1, string str2) {
        int max_length = 0;
        string max_str; 
        //遍历str1每个起始点
        for(int i = 0; i < str1.length(); i++){ 
            //遍历str2每个起点
            for(int j = 0; j < str2.length(); j++){ 
                int length = 0;
                string str;
                int x = i;
                int y = j;
                //比较每个起点为始的子串
                while(x < str1.length() && y < str2.length() && str1[x] == str2[y]) {
                    length++;
                    str += str1[x];
                    x++;
                    y++;
                }
                //更新更大的长度子串
                if(max_length < length){ 
                    max_length = length;
                    max_str = str;
                }
            }
        }
        return max_str;
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值