LeetCode-打卡题引申出来的dp练习

写在前面

今天的LC打卡虽然是easy,但是用到了dp的思路来解,对于dp思维的练习我觉得很有帮助,再加上很多大佬在题解里面列了不少类似的题目,所以就一并写了并且整理一下。(打家劫舍系列+丑数系列)

面试题 17.16. 按摩师

一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。

示例 1:

输入: [1,2,3,1]

输出: 4

解释: 选择 1 号预约和 3 号预约,总时长 = 1 + 3 = 4。

解法:前一位的状态只有选和不选,然后当前位置可以根据上一位的状态来找到当前位选和不选的最大值。

递推方程:dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])

这题不需要用dp数组,两个dp变量就行了。

代码:

class Solution {
public:
    int massage(vector<int>& nums) {
        if(nums.empty()) return 0;
        int dp0 = 0;
        int dp1 = nums[0];
        int i=1;
        while(i<nums.size()){
            int t0 = max(dp0,dp1);
            int t1 = max(dp0+nums[i],dp1);
            dp0 = t0;
            dp1 = t1;
            ++i;
        }
        return max(dp0,dp1);
    }
};

198. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

上一题的原型其实就是这道经典的小偷问题,完全一模一样,所以这里就不写了。

213. 打家劫舍 II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

解法:其实就是变成了环形的,头尾需要特判,所以分成不选头和不选尾,范围两个答案的较大值。

代码:

class Solution {
public:
    int rob(vector<int>& nums) {
        if(nums.empty()) return 0;
        if(nums.size()==1) return nums[0];
        int dp0 = 0;
        int dp1 = nums[0];
        int i=1;
        while(i<nums.size()-1){
            int t0 = max(dp0,dp1);
            int t1 = max(dp0+nums[i],dp1);
            dp0 = t0;
            dp1 = t1;
            ++i;
        }
        int res1 = max(dp0,dp1);
        dp0 = 0;
        dp1 = nums[1];
        i=2;
        while(i<nums.size()){
            int t0 = max(dp0,dp1);
            int t1 = max(dp0+nums[i],dp1);
            dp0 = t0;
            dp1 = t1;
            ++i;
        }
        int res2 = max(dp0,dp1);
        return max(res1,res2);
    }
};

337. 打家劫舍 III

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

解法:思路不变,但是因为变成了二叉树存放,所以变成了一道树形dp。

对于一个子树来说,有两种情况:

  1. 包含当前根节点
  2. 不包含当前根节点

情况1:包含根节点

由于包含了根节点,所以不能选择左右儿子节点,这种情况的最大值为:
当前节点 + 左儿子情况2 + 右二子情况2

情况2:不包含根节点

这种情况,可以选择左右儿子节点,所以有四种可能:

  1. 左儿子情况1 + 右儿子情况1
  2. 左儿子情况1 + 右儿子情况2
  3. 左儿子情况2 + 右儿子情况1
  4. 左儿子情况2 + 右儿子情况2
    综合来说就是,max(左儿子情况1, 左儿子情况2) + max(右儿子情况1, 右儿子情况2)。

结合这两种情况,dfs遍历二叉树即可。

来源:https://leetcode-cn.com/problems/house-robber-iii/solution/cdong-tai-gui-hua-si-xiang-shi-xian-xiang-xi-shuo-/

代码:

/**
* Definition for a binary tree node.
* struct TreeNode {
*     int val;
*     TreeNode *left;
*     TreeNode *right;
*     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
    pair<int, int> dfs(TreeNode *root) {
        if (root == nullptr) {
            return { 0, 0 };
        }
        auto left_pair = dfs(root->left);
        auto right_pair = dfs(root->right);
        return {root->val + left_pair.second + right_pair.second, 
                max(left_pair.first, left_pair.second) + max(right_pair.first, right_pair.second)};
    }
    int rob(TreeNode* root) {
        auto p = dfs(root);
        return max(p.first, p.second);
    }
};

做完打家劫舍系列,不如一并练习一下类似的简单dp题目。

1262. 可被三整除的最大和

给你一个整数数组 nums,请你找出并返回能被三整除的元素最大和。

示例 1:

输入:nums = [3,6,5,1,8]

输出:18

解释:选出数字 3, 6, 1 和 8,它们的和是 18(可被 3 整除的最大和)。

解法:

不妨设dp[i] 代表 选取的数字累加和 模3 = i 的数字和

假定nums[i] % 3 = 1 ,那么,和 前面选取的数字和模 3 = 2 的数相加,就可以模3为 0 ,表达起来就是 dp[0] = max(dp[0], nums[i] + dp[2])

依次类推,只要不断更新 dp 数组即可,注意一点,更新的时候要保存上一个状态的值,避免后续更新的时候重复影响。

代码:

class Solution {
public:
    int maxSumDivThree(vector<int>& nums) {
        vector<int> dp={0,0,0};
        for(int i=0; i<nums.size(); i++){
            int mod = nums[i] % 3;

            int a = dp[(3+0-mod)%3];
            int b = dp[(3+1-mod)%3];
            int c = dp[(3+2-mod)%3];

            if( a || mod==0) dp[0] = max(dp[0],a+nums[i]);
            if( b || mod==1) dp[1] = max(dp[1],b+nums[i]);
            if( c || mod==2) dp[2] = max(dp[2],c+nums[i]);
        }
        return dp[0];
    }
};

801. 使序列递增的最小交换次数

我们有两个长度相等且不为空的整型数组 A 和 B 。

我们可以交换 A[i] 和 B[i] 的元素。注意这两个元素在各自的序列中应该处于相同的位置。

在交换过一些元素之后,数组 A 和 B 都应该是严格递增的(数组严格递增的条件仅为A[0] < A[1] < A[2] < … < A[A.length - 1])。

给定数组 A 和 B ,请返回使得两个数组均保持严格递增状态的最小交换次数。假设给定的输入总是有效的。

示例:

输入: A = [1,3,5,4], B = [1,2,3,7]

输出: 1

解释:

交换 A[3] 和 B[3] 后,两个数组如下:

A = [1, 3, 5, 7] , B = [1, 2, 3, 4]

两个数组均为严格递增的。

注意:

  1. A, B 两个数组的长度总是相等的,且长度的范围为 [1, 1000]。
  2. A[i], B[i] 均为 [0, 2000]区间内的整数。

解法:

对于每一列的数,都有两种状态,换或者不换,所以我们定义两个dp变量来储存状态。

初始化:如果只有一列:那么dp = [0, 1]

dp[0] = 0, 不做交换,总交换数为0.

dp[1] = 1, 做交换,总交换数为1,同时也满足最后的要求,是一个递增数列,因为数组只有一个元素。


如果有两列, 假设 a1 = A[i - 1], b1 = B[i - 1] 以及 a2 = A[i], b2 = B[i], i = 1
相对的有4中情况

(1)a1 < a2 并且 b1 < b2

表明,当前列相对前一列,可以不做交换,因为条件已经满足了。

(2)a1 < b2 并且 b1 < a2

表明,当前列相对前一列,可以做交换,因为交换了条件可以满足。

(3) not a1 < a2 或者 not b1 < b2

是(1)取反,当前列相对前一列必须交换,这里的交换是相对的,如果前一列已经交换了,当前列可以不动。

(4)not a1 < b2 或者 not b1 < a2

是(2)取反,当前列相对前一列必须不能换。


对于(3)

new_dp[0] = dp[1],要想当前列不动,那么前一列必须是换过的。
new_dp[1] = dp[0] + 1,要想当前列换,前一列必须没动过。总之,对于情况(3),与前一列操作相反就对了。

对于(4)

new_dp[0] = dp[0],要想当前列不动,那么前一列必须也不动。
new_dp[1] = dp[1] + 1,要想当前列换,前一列必须也换过。总之,对于情况(4),与前一列操作相同就对了。

但是(3)(4)并没有包括全部的情况。对于(1)(2),可换可不换,按照贪心原则,那就是能不换就不换。

new_dp[0] = min(dp[0], dp[1]),管你前面换没换过,我是取小的那个数,然后就不换了。

new_dp[1] = min(dp[0], dp[1]) + 1,管你前面换没换过,我是取小的那个数,然后换一下,因为dp[1]对应的就是当前列交换的情况,所以不管怎么样,这个数值必须对应当前列交换这个操作。

代码:

class Solution {
public:
    int minSwap(vector<int>& a,vector<int>& b) {
        vector<int> dp = {0,1};
        for(int i=1; i<a.size(); i++){
            int a1 = a[i-1];
            int a2 = a[i];
            int b1 = b[i-1];
            int b2 = b[i];

            if(a1>=a2 || b1>=b2){
                int t = dp[0];
                dp[0] = dp[1];
                dp[1] = t + 1;
            }
            else if(a1>=b2 || b1>=a2){
                ++dp[1];
            }
            else{
                int m = min(dp[0],dp[1]);
                dp[0] = m;
                dp[1] = m+1;
            }
        }
        return min(dp[0], dp[1]);
    }
};

练完以上这些题目,我们再做一个经典系列:丑数

263. 丑数

编写一个程序判断给定的数是否为丑数。

丑数就是只包含质因数 2, 3, 5 的正整数。

解法:第一道没什么好说的,普通的暴力题目。

代码:

class Solution {
public:
    bool isUgly(int num) {
        if(num==0) return false;
        while(num%2==0) num /= 2;
        while(num%3==0) num /= 3;
        while(num%5==0) num /= 5;
        return num==1;
    }
};

264. 丑数 II

编写一个程序,找出第 n 个丑数。

丑数就是只包含质因数 2, 3, 5 的正整数。

示例:

输入: n = 10

输出: 12

解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。

解法:使用三个指针,分别代表2,3,5,然后记录指针位置对应的数乘对应的倍数的最小值,选中的指针+1,一直算到第n个就行了。

代码:

class Solution {
public:
    int nthUglyNumber(int k) {
        if(k==1) return 1;
        int a(0),b(0),c(0);//记录2,3,5对应的位置
        vector<int> dp;
        dp.push_back(1);
        int s = 1;
        while(dp.size()<k){
            int next = min(2*dp[a],min(3*dp[b],5*dp[c]));
            if(next==2*dp[a]) a++;//选中的指针要+1
            if(next==3*dp[b]) b++;
            if(next==5*dp[c]) c++;
            dp.push_back(next);
        }
        return dp.back();
    }
};

面试题 17.09. 第 k 个数

有些数的素因子只有 3,5,7,请设计一个算法找出第 k 个数。注意,不是必须有这些素因子,而是必须不包含其他的素因子。例如,前几个数按顺序应该是 1,3,5,7,9,15,21。

和上一题完全一样,除了三个因子的变化,只要是互质的,上一个解法就是可行的。

313. 超级丑数

编写一段程序来查找第 n 个超级丑数。

超级丑数是指其所有质因数都是长度为 k 的质数列表 primes 中的正整数。

示例:

输入: n = 12, primes = [2,7,13,19]

输出: 32

解释: 给定长度为 4 的质数列表 primes = [2,7,13,19],前 12 个超级丑数序列为:[1,2,4,7,8,13,14,16,19,26,28,32] 。

说明:

  1. 1 是任何给定 primes 的超级丑数。
  2. 给定 primes 中的数字以升序排列。
  3. 0 < k ≤ 100, 0 < n ≤ 106, 0 < primes[i] < 1000 。
  4. 第 n 个超级丑数确保在 32 位有符整数范围内。

解法:和上一道丑数的dp思路是一样的,只不过这里的因数变成了一个数组,所以我们也需要一个数组才存放对应的指针位置,值得一提的是,我本来是用数字来记录位置的,但是由于会出现重复的问题(由于primes个数变多,会出现多个因数可以乘出同一个next值,此时这些指针都要+1),如果用vector判重会超时,所以就想到了set,但是set的下标不能用整数来记录,所以就要用到iterator,这里也算是巩固了一下指针的概念。

代码:

class Solution {
public:
    int nthSuperUglyNumber(int n, vector<int>& primes) {
        if(primes.empty()) return 0;
        int l = primes.size();
        set<int> dp;
        dp.insert(1);
        vector<set<int>::iterator> points;//point数组记录dp集合的迭代位置
        for(int i=0; i<l; i++)
            points.push_back(dp.begin());
        while(dp.size()<n){
            int minv = INT_MAX;
            int mini = 0;
            for(int i=0; i<l; i++) 
                minv = min(minv,*points[i]*primes[i]);//算出next值
            dp.insert(minv);
            for(int i=0; i<l; i++)
                if(*points[i]*primes[i]==minv) points[i]++;//迭代位置+1
        }
        return *dp.rbegin();//set的最后一位索引,取值。
    }
};

set用的不是特别多,这题算是让我彻底搞明白了set的遍历以及迭代器的用法,受益匪浅。

1201. 丑数 III

请你帮忙设计一个程序,用来找出第 n 个丑数。

丑数是可以被 a 或 b 或 c 整除的 正整数。

示例:

输入:n = 3, a = 2, b = 3, c = 5

输出:4

解释:丑数序列为 2, 3, 4, 5, 6, 8, 9, 10… 其中第 3 个是 4。

提示:

  1. 1 <= n, a, b, c <= 10^9
  2. 1 <= a * b * c <= 10^18
  3. 本题结果在 [1, 2 * 10^9] 的范围内

解法:数据范围10^9,已经和dp没关系了,虽然名字还是丑数=.=

这里直接参考了一位大神的题解,其实就是一道数学题,用到了经典的容斥原理。

链接:https://leetcode-cn.com/problems/ugly-number-iii/solution/er-fen-fa-si-lu-pou-xi-by-alfeim/

代码:

class Solution {
public:
    using LL = long long;
    int nthUglyNumber(int n, int a, int b, int c) {
        //看到n的范围应该马上联想到是,典型的二分思路
        LL low = min(min(a,b),c);                           //下边界显然是a、b、c中最小者
        LL high = static_cast<LL>(low) * n;                 //上边界是这个最小者的n倍    
        LL res = Binary_Search(low,high,a,b,c,n);
        LL left_a = res%a;
        LL left_b = res%b;
        LL left_c = res%c;
        return res - min(left_a,min(left_b,left_c));
    }
    //二分搜索
    LL Binary_Search(LL low,LL high,int a,int b,int c,LL n){
        if(low >= high) return low;
        LL mid = (low + high)>>1;
        LL MCM_a_b = MCM(a,b);
        LL MCM_a_c = MCM(a,c);
        LL MCM_b_c = MCM(b,c);
        LL MCM_a_b_c = MCM(MCM_a_b,c);
        //独立的丑数个数为,当前数分别除以a、b、c的和,减去当前数除以a、b、c两两间最小公倍数的和,再加上当前数除以 a、b、c三者的最小公倍数
        LL count_n = mid/a + mid/b + mid/c - mid/MCM_a_b - mid/MCM_b_c - mid/MCM_a_c +  mid/MCM_a_b_c;
        if(count_n == n) return mid;
        if(count_n < n) return Binary_Search(mid + 1,high,a,b,c,n);
        return Binary_Search(low,mid-1,a,b,c,n);
    }
    //求最小公倍数:两数乘积除以最大公约数
    LL MCM(LL a,LL b){
        LL Multi = a * b;
        while(b > 0){
            LL tmp = a % b;
            a = b;
            b = tmp;
        }
        return Multi/a;
    }
};
发布了13 篇原创文章 · 获赞 27 · 访问量 2649

猜你喜欢

转载自blog.csdn.net/u011708337/article/details/105105850