剑指offer:动态规划

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

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 ∙ 0

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

你可能感兴趣的:(数据结构和算法,动态规划,算法)