LeetCode 剑指 Offer II 动态规划(一) 专题总结

  • 博客主页:⭐️这是一只小逸白的博客鸭~⭐️
  • 欢迎 关注❤️点赞收藏⭐️评论
  • 小逸白正在备战实习,经常更新面试题LeetCode题解,欢迎志同道合的朋友互相交流~
  • 若有问题请指正,记得关注哦,感谢~

往期文章 :

  • LeetCode 剑指 Offer II 前缀树(下) 专题总结
  • LeetCode 剑指 Offer II 二分查找 专题总结
  • LeetCode 剑指 Offer II 排序 专题总结
  • LeetCode 剑指 Offer II 回溯(上) 专题总结
  • LeetCode 剑指 Offer II 回溯(下) 专题总结

目录

  • 088. 爬楼梯的最少成本
  • 089. 房屋偷盗
  • 090. 环形房屋偷盗
  • 091. 粉刷房子
  • 092. 翻转字符
  • 093. 最长斐波那契数列

088. 爬楼梯的最少成本

题目:

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。
每当爬上一个阶梯都要花费对应的体力值,一旦支付了相应的体力值,就可以选择向上爬一个阶梯或者爬两个阶梯。
请找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

示例:

输入:cost = [10, 15, 20]
输出:15
解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。

提示:

  • 2 <= cost.length <= 1000
  • 0 <= cost[i] <= 999

思路:

要到达i阶楼梯,就要至少使用i-1阶 or i-2阶的总体力值
可以推出动态规划方程:dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
因为要到达楼层顶层,所以我们需要算到 n+1层楼梯的体力消耗
本题因为i 只需要 i-1,i-2两个条件即可得出,可以将空间复杂度压缩至o(1),后面很多题都会这样我这题就不压缩了

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        int n = cost.size();
        vector<int> dp(n + 1);
        // 初始化,到达0,1台阶不用体力值
        dp[0] = dp[1] = 0;
        for(int i = 2; i <= n; i++) {
            dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
        }
        return dp[n];
    }
};

089. 房屋偷盗

题目:

一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响小偷偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组 nums ,请计算 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例:

输入:nums = [1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 400

思路:

i时可以选择进去偷东西,dp[i] = dp[i-2] + nums[i];
i时不进去,dp[i] = dp[i-1];
动态规划方程:dp[i] = max(dp[i-2] + nums[i], dp[i-1]);
可以看到只需要两个变量就能得到dp[i],所以我们dp大小压缩为2,上题也可以压缩

class Solution {
public:
    int rob(vector<int>& nums) {
        if(nums.size() == 1) return nums[0];
        int n = nums.size();
        vector<int> dp(2);
        dp[0] = nums[0];
        dp[1] = max(0 + nums[1], dp[0]);
        for(int i = 2; i < n; i++) {
            dp[i % 2] = max(dp[(i-2) % 2] + nums[i], dp[(i-1) % 2]);
        }
        return max(dp[0], dp[1]);
    }
};

090. 环形房屋偷盗

题目:

一个专业的小偷,计划偷窃一个环形街道上沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组 nums ,请计算 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

示例:

输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 1000

思路:

跟上一题的区别就是在最后一间房子的时候需要考虑与第一间房子相邻,这两间房子互斥
我们可以将环形房子拆成0~n-2,1~n-2两部分,继续运用上题的思路

class Solution {
public:
    int rob(vector<int>& nums) {
        // 转换成:0 - n-2的盗窃值 跟 1 - n-1的盗窃值的最大值
        if(nums.size() == 1) return nums[0];
        int n = nums.size();
        return max(DP(nums, 0, n - 2), DP(nums, 1, n - 1));
    }
    // 把上一题的0, 跟n-1替换成start,end
    int DP(vector<int>& nums, int start, int end) {
    	// 如果只有一间房子直接返回
        if(start == end) return nums[start];
        vector<int> dp(2);
        dp[start % 2] = nums[start];
        dp[(start + 1) % 2] = max(nums[start + 1],nums[start]);
        for(int i = start + 2; i <= end; i++) {
            dp[i % 2] = max(dp[(i - 2) % 2] + nums[i], dp[(i - 1) % 2]);
        }
        return max(dp[0], dp[1]);
    }
};

091. 粉刷房子

题目:

假如有一排房子,共 n 个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。
当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个 n x 3 的正整数矩阵 costs 来表示的。
例如,costs[0][0] 表示第 0 号房子粉刷成红色的成本花费;costs[1][2] 表示第 1 号房子粉刷成绿色的花费,以此类推。
请计算出粉刷完所有房子最少的花费成本。

示例:

输入: costs = [[17,2,17],[16,16,5],[14,3,19]]
输出: 10
解释: 将 0 号房子粉刷成蓝色,1 号房子粉刷成绿色,2 号房子粉刷成蓝色。
最少花费: 2 + 5 + 3 = 10。

提示:

  • costs.length == n
  • costs[i].length == 3
  • 1 <= n <= 100
  • 1 <= costs[i][j] <= 20

思路:

状态转移方程:dp[i][0] = mid(dp[i-1][1],dp[i-1][2]) + costs[i][0];其他两个都是相似的
本层的最小值由上一层决定,本列最小值 = 本行本列值 + 上一行其他两列最小值,所以复杂度可以从O(N)降为0(1)

class Solution {
public:
    int minCost(vector<vector<int>>& costs) {
        
        vector<vector<int>> dp(2, vector<int>(3, 0));
        dp[0] = costs[0];
        for(int i = 1; i < costs.size(); i++) {
            for(int j = 0; j < 3; j++) {
                dp[i % 2][j] = min(dp[(i - 1) % 2][(j + 1) % 3], dp[(i - 1)% 2][(j + 2) % 3]) 
                				+ costs[i][j];
            }
        }
        // 判断最后一行是0还是1
        int last = (costs.size() - 1) % 2;
        return min(min(dp[last][0], dp[last][1]), dp[last][2]);
    }
};

092. 翻转字符

题目:

如果一个由 ‘0’ 和 ‘1’ 组成的字符串,是以一些 ‘0’(可能没有 ‘0’)后面跟着一些 ‘1’(也可能没有 ‘1’)的形式组成的,那么该字符串是 单调递增 的。
我们给出一个由字符 ‘0’ 和 ‘1’ 组成的字符串 s,我们可以将任何 ‘0’ 翻转为 ‘1’ 或者将 ‘1’ 翻转为 ‘0’。
返回使 s 单调递增 的最小翻转次数。

示例:

输入:s = “010110”
输出:2
解释:我们翻转得到 011111,或者是 000111。

提示:

  • 1 <= s.length <= 20000
  • s 中只包含字符 '0' 和 '1'

思路:

答案要求变成左边为0右边为1的格式,我们可以翻转0或者1
我们从左到右遍历,遇到1不知道后面是否还有0,不知道是否要翻转
而遇到0之后我们可以选择 翻转0或者翻转前面的1来达到左0右1
两者取最小值,前面翻转的是0还是1对后序翻转都没有影响,我们只需要记录翻转次数
动态规划方程dp = min(one, dp + 1); dp表示i要达到左0右1翻转的次数,one表示翻转i之前1的数, dp+1表示翻转当前数

class Solution {
public:
    int minFlipsMonoIncr(string s) {
        int dp = 0; 
        int one = 0; // 记录1的个数
        for(int i = 0; i < s.length(); i++) {
            if(s[i] == '0') {
                dp = min(one, dp + 1); // 选择 翻转前面为1的数 or 翻转当前数
            }else {
                one++;
            }
        }
        return dp;
    }
};

093. 最长斐波那契数列

题目:

如果序列 X_1, X_2, …, X_n 满足下列条件,就说它是 斐波那契式 的:
n >= 3
对于所有 i + 2 <= n,都有 X_i + X_{i+1} = X_{i+2}
给定一个严格递增的正整数数组形成序列 arr ,找到 arr 中最长的斐波那契式的子序列的长度。如果一个不存在,返回 0 。
(回想一下,子序列是从原序列 arr 中派生出来的,它从 arr 中删掉任意数量的元素(也可以不删),而不改变其余元素的顺序。例如, [3, 5, 8] 是 [3, 4, 5, 6, 7, 8] 的一个子序列)

示例1:

输入: arr = [1,2,3,4,5,6,7,8]
输出: 5
解释: 最长的斐波那契式子序列为 [1,2,3,5,8] 。
示例1:

输入: arr = [1,3,7,11,12,14,18]
输出: 3
解释: 最长的斐波那契式子序列有 [1,11,12]、[3,11,14] 以及 [7,11,18] 。

提示:

  • 3 <= arr.length <= 1000
  • 1 <= arr[i] < arr[i + 1] <= 10^9

思路:

动态规划 + 哈希表
下标i j k成立,我们将j k存入dp里,值为i j 为结尾的序列前面的个数 + 1
总结:如果 (i, j)(j, k) 是连通的, dp[j, k] = dp[i, j] + 1
arr = [1,2,3,4,5,6,7,8],子序列为 [1,2,3,5,8]
此时j k指向5 8的话,dp[5,8] = dp[3, 5] + 1 = d[2,3] + 1 + 1 = dp[1,2] + 1 + 1 + 1 = 3

class Solution {
public:
    int lenLongestFibSubseq(vector<int>& arr) {
        int n = arr.size();
        unordered_map<int, int> index;
        for(int i = 0; i < n; i++) {
            index[arr[i]] = i;
        }
        // 保存dp[j*n+k],值表示j k 序列前面的个数
        unordered_map<int, int> dp;
        int ans = 0;
        for(int k = 0; k < n; k++) {
            for(int j = 0; j < k; j++) {
                // i存在并且i < j,防止i j取两边,保证 i < j
                if(index.count(arr[k] - arr[j]) && index[arr[k] - arr[j]] < j) {
                    int i = index[arr[k] - arr[j]];
                    dp[j * n + k] = dp[i * n + j] + 1;
                    // j k前面个数加上 j k两个数
                    ans = max(ans, dp[j * n + k] + 2);
                }
            }
        }
        return ans;
    }
};

你可能感兴趣的:(LeetCode,leetcode,动态规划,算法,哈希表,c++)