Leetcode练习题:贪心思想

Leetcode练习题:贪心思想

  • 122:买卖股票的最佳时机Ⅱ
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获
  • 316:去除重复字母
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获
  • 321:拼接最大数
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获
  • 330:按要求补齐数组
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获
  • 435:无重叠区间
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获
  • 659:分割数组为连续子序列
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获
  • 757:设置交集大小至少为2
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获
  • 861:翻转矩阵后的得分
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获
  • 881:救生艇
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获
  • 1029:两地调度
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获

贪心思想是非常普遍并且运用较多的算法,但是有些题目难度会很大,个人觉得难点在于,要想清楚贪的是什么,怎么贪,以及是否要贪的判断标准。

122:买卖股票的最佳时机Ⅱ

问题描述

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入: [7,1,5,3,6,4]

输出: 7

解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4。随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。

示例 2:

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

输出: 4

解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:

输入: [7,6,4,3,1]

输出: 0

解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

说明:

1 <= prices.length <= 3 * 10 ^ 4

0 <= prices[i] <= 10 ^ 4

解题思路

如果想象成折线图,那就是每一段上升的曲线都算进去。因为不限制买买次数,所以每一次上涨我都买。

代码实现

int maxProfit(vector<int>& prices) {
        //贪心 不限制买卖次数 所以每次上升的时候我都买
        int sum=0;
        for(int i=0;i<prices.size()-1;i++)
        {
            if(prices[i]<prices[i+1])
            {
                sum+=prices[i+1]-prices[i];
            }
        }
        return sum;
    }

反思与收获

不要想的太复杂,什么点买进,什么点卖出。完全诠释贪心的思想,只要上涨的我都买,可以稍微考虑的简单简洁一点。

316:去除重复字母

问题描述

给你一个仅包含小写字母的字符串,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证返回结果的字典序最小(要求不能打乱其他字符的相对位置)。

示例 1:

输入: “bcabc”

输出: “abc”

示例 2:

输入: “cbacdcbc”

输出: “acdb”

解题思路

要求是字典序最小,所以会想到使用栈来解决,栈顶元素与当前元素的比较,能保证字典序最小,但问题是怎么让每个字母都只出现一次,而且不打断相对顺序。因此我们可以添加一个判断。最后栈里的元素即为答案。

如果当前元素栈里已经存在,直接跳过
如果当前元素小于栈顶元素并且栈顶元素在后面的字符串中还存在,这意味着我们可以把字典序较大的元素在后面的时候在放入,因此将栈顶元素pop

将当前元素放入栈里

代码实现

  string removeDuplicateLetters(string s) {
        string stackk;
        for(int i=0;i<s.length();i++)
        {
            if(stackk.find(s[i])!=string::npos) {continue;}
            while(!stackk.empty()&&stackk.back()>s[i]&&s.find(stackk.back(),i)!=string::npos)
            {
                stackk.pop_back();
            }
            stackk.push_back(s[i]);
        }
        return stackk;
    }

反思与收获

这边利用string来代替stack,能实现本题所需的stack功能,并且最后也不需要倒置。
string.find()函数返回该字符的下标,没有找到也返回npos
string.find(string target,int index)表示在s中从下标index开始寻找target
有时还会用find_first_of()和find_last_of()

321:拼接最大数

问题描述

给定长度分别为 m 和 n 的两个数组,其元素由 0-9 构成,表示两个自然数各位上的数字。现在从这两个数组中选出 k (k <= m + n) 个数字拼接成一个新的数,要求从同一个数组中取出的数字保持其在原数组中的相对顺序。

求满足该条件的最大数。结果返回一个表示该最大数的长度为 k 的数组。

说明: 请尽可能地优化你算法的时间和空间复杂度。

示例 1:

输入:

nums1 = [3, 4, 6, 5]

nums2 = [9, 1, 2, 5, 8, 3]

k = 5

输出:

[9, 8, 6, 5, 3]

示例 2:

输入:

nums1 = [6, 7]

nums2 = [6, 0, 4]

k = 5

输出:

[6, 7, 6, 0, 4]

示例 3:

输入:

nums1 = [3, 9]

nums2 = [8, 9]

k = 3

输出:

[9, 8, 9]

解题思路

这题的难度在于,在数组中的相对顺序还不能改变。参考题解
将问题分成两个小问题,一个总长为k的序列,意味在nums1中选s个,在nums2中选k-s个,返回长度为s,k-s的最大子序列,然后考虑将两者合并起来,将合并后的序列与res比较,是否进行更新

代码实现

 vector<int> maxNumber(vector<int>& nums1, vector<int>& nums2, int k) {
        vector<int> ans(k,0);

        int len1=nums1.size(),len2=nums2.size();
        //如果k大于nums2的长度,则s至少为k-nums2.length,min也是同理
        for(int s=max(0,k-len2);s<=min(k,len1);s++)
        {
            vector<int> temp;
            //找各自长度为要求的最大子序列
            vector<int> temp1=maxSubsequence(nums1,s);

            vector<int> temp2=maxSubsequence(nums2,k-s);

            auto i=temp1.begin(),j=temp2.begin();
            while(i!=temp1.end()||j!=temp2.end())
            {
                //注意 lexicographical_compare函数的用法
                temp.push_back(lexicographical_compare(i,temp1.end(),j,temp2.end())? *j++ : *i++);
            }

            ans=lexicographical_compare(ans.begin(),ans.end(),temp.begin(),temp.end())?temp:ans;
        }
         return ans;
    }

    vector<int> maxSubsequence(vector<int> v,int k)
    {
        //学习到一种新的求最大子序列的方法
        int n=v.size();
        if(n<=k)
        {
            return v;
        }
        vector<int> ans;
        int p=n-k;
        for(int i=0;i<n;i++)
        {
            while(!ans.empty()&&v[i]>ans.back()&&p-->0)
                ans.pop_back();
            ans.push_back(v[i]);
        }
        //数组v元素全部相同时会把所有数加入res,这时得删掉res数组多余的元素
        ans.resize(k);
        return ans;
    }

反思与收获

规定长度最大子序列求法:记录需要删除的个数,如果当前元素大于栈顶元素且需要删除个数仍大于0,则将此pop。因为91balabala一定小于95balabala
lexicographical_compare是C++ STL 泛型算法函数:用于按字典序比较两个序列。如果[first1, last1)按字典序列小于[first2, last2),返回true,否则返回false。
字典序列比较的意思是:
从左往右,只要某个位置的字符大了,则这个字符串就大,跟长度没有关系。
a大于bac,a小于aa
自己写函数可以如下

int cmp(string s1,string s2)	
{	
		for (int i = 0,j=0; i <s1.length()&&j<s2.length() ; i++,j++) {
            if(s1[i]>s2.[i]){
                return 1;
            }else if(s1.[i]<s2.[i]){
                return -1;
            }
        }
        if(s1.length()==s2.length()){
            return 0;
        }else if(s1.length()>s2.length()){
            return 1;
        }else {
            return -1;
        }
 }

比如67 和604两个序列进行合并,我之前选择使用双指针比较然后往后走,就会出现66704的结果,显然是错误,使用字典序列函数比较的好处就在于,67一定大于604,因此是上面的迭代器往后走。

330:按要求补齐数组

问题描述

给定一个已排序的正整数数组 nums,和一个正整数 n 。从 [1, n] 区间内选取任意个数字补充到 nums 中,使得 [1, n] 区间内的任何数字都可以用 nums 中某几个数字的和来表示。请输出满足上述要求的最少需要补充的数字个数。

示例 1:

输入: nums = [1,3], n = 6

输出: 1

解释:

根据 nums 里现有的组合 [1], [3], [1,3],可以得出 1, 3, 4。

现在如果我们将 2 添加到 nums 中, 组合变为: [1], [2], [3], [1,3], [2,3], [1,2,3]。

其和可以表示数字 1, 2, 3, 4, 5, 6,能够覆盖 [1, 6] 区间里所有的数。

所以我们最少需要添加一个数字。

示例 2:

输入: nums = [1,5,10], n = 20

输出: 2

解释: 我们需要添加 [2, 4]。

示例 3:

输入: nums = [1,2,2], n = 5

输出: 0

解题思路

参考题解
这题基本就是放弃…也没有找到什么规律,题解也看了才理解的,想也想不到的

miss是缺少的数字当中最小的那一个,就是说[1,miss)已经完全覆盖,因此我们需要一个小于等于miss的数字。
比如nums=[1,2,5],区间[1,3],[5,8]已经覆盖,因此我们肯定要选择4来填补空缺,否则没有办法覆盖4.

假设添加x,则[1,miss),[1+x, miss+x)将会被覆盖,x是<=miss的,所以实质上[1,miss+x)这样子的区间,希望越大越好,贪在这里,因此将x=miss是最优选择

初始化区间为[1,1)实际为空
当n没有被覆盖时

  • 如果nums[i]小于等于miss,在里面将范围扩展为[1,miss+nums[i]);i+1;
  • 如果nums[i]大于miss了,说明需要添加数字来覆盖miss,根据上述要求增加的就是miss,范围拓展为[1,miss+miss),count++;

代码实现

没想到代码这么简短,主要是解题思路
因为miss是开区间,所以要等于号

  int minPatches(vector<int>& nums, int n) {

        int count=0,i=0;
        long long miss=1;

        while(miss<=n)
        {
            if(i<nums.size()&&nums[i]<=miss)
            {
                miss+=nums[i];
                i++;
            }else{
                miss+=miss;
                count++;
            }
        }

        return count;
    }

反思与收获

一看题目就不知道怎么做,看题解才能理解,怎么证明贪心的正确性,证明得看一下。其实题目都还不是很理解,从 [1, n] 区间内选取任意个数字补充到 nums 中,使得 [1, n] 区间内的任何数字都可以用 nums 中某几个数字的和来表示好好体会。如果疑惑为什么 范围扩展为[1,miss+nums[i]) 可以直接这么扩展并保证这些数都能被覆盖的话,额这是显然的事情,不要自己绕进去了

435:无重叠区间

问题描述

给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。注意: 可以认为区间的终点总是大于它的起点。

区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。

示例 1:

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

输出: 1

解释: 移除 [1,3] 后,剩下的区间没有重叠。

示例 2:

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

输出: 2

解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。

示例 3:

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

输出: 0

解释: 你不需要移除任何区间,因为它们已经是无重叠的了

解题思路

关于区间重复的问题,通常需要做的第一个操作就是将这些区间进行排序,首先我们将区间按照左端点进行排序,然后我们考虑整个范围的最右端点right,如果当前区间的右端点在right的左边,则区间重复,通过保留最左侧右端点来简化去除区间的操作,贪心点在于尽量保留靠左的右端点,否则更新最右端点right

代码实现

  int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if(intervals.empty())
        {
            return 0;
        }
        sort(intervals.begin(),intervals.end());
        int right=intervals[0][1];
        int count=0;
        for(int i=1;i<intervals.size();i++)
        {
            if(intervals[i][0]<right)
            {
                count++;
                right=min(right,intervals[i][1]);
            }else{
                right=intervals[i][1];
            }
        }
        return count;
    }

反思与收获

关于区间的问题,通常考虑先将区间进行排序。

659:分割数组为连续子序列

问题描述

给你一个按升序排序的整数数组 num(可能包含重复数字),请你将它们分割成一个或多个子序列,其中每个子序列都由连续整数组成且长度至少为 3 。

一个子序列是从原始数组挑选一部分(也可以全部)元素而不改变相对位置形成的新数组

如果可以完成上述分割,则返回 true ;否则,返回 false 。

示例 1:

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

输出: True

解释:

你可以分割出这样两个连续子序列 :

1, 2, 3

3, 4, 5

示例 2:

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

输出: True

解释:

你可以分割出这样两个连续子序列 :

1, 2, 3, 4, 5

3, 4, 5

示例 3:

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

输出: False

说明:

输入的数组长度范围为 [1, 10000]

解题思路

对于任何一个元素而言,他只有两种情况,要么呢它自己跟后面的两个元素组成一个新序列,要么加入到已经存在的序列最后,至于哪个优先其实是不考虑的,因为只是问是否能完成这样的分割。
使用两个map来实现,一个记录该数元素目前还剩下的出现次数,一个记录以该元素为结尾的序列数。参考了leetcode的题解。

代码实现

 bool isPossible(vector<int>& nums) {
        unordered_map<int,int> count,tail;
        for(auto n:nums)
        {
            count[n]++;
        }
        for(auto n:nums)
        {
            if(count[n]==0)
            {
                continue;
            }else if(count[n]>0&&tail[n-1]>0)
            {
                count[n]--;
                tail[n-1]--;
                tail[n]++;
            }else if(count[n]>0&&count[n+1]>0&&count[n+2]>0)
            {
                count[n]--;
                count[n+1]--;
                count[n+2]--;
                tail[n+2]++;
            }else{
                return false;
            }
        }
        return true;

    }

反思与收获

总是遇到子序列问题就不知所措,只会往最笨最复杂的方向去想,有时题目所求的答案不一定要求很高,多学习几种解决常见子序列的方法。

757:设置交集大小至少为2

问题描述

一个整数区间 [a, b] ( a < b ) 代表着从 a 到 b 的所有连续整数,包括 a 和 b。

给你一组整数区间intervals,请找到一个最小的集合 S,使得 S 里的元素与区间intervals中的每一个整数区间都至少有2个元素相交。

输出这个最小集合S的大小。

示例 1:

输入: intervals = [[1, 3], [1, 4], [2, 5], [3, 5]]

输出: 3

解释:

考虑集合 S = {2, 3, 4}. S与intervals中的四个区间都有至少2个相交的元素。

且这是S最小的情况,故我们输出3。

示例 2:

输入: intervals = [[1, 2], [2, 3], [2, 4], [4, 5]]

输出: 5

解释:

最小的集合S = {1, 2, 3, 4, 5}.

解题思路

参考leetcode题解,如果题目是每个区间交集大小为1,那很好做,可以选排序后的第一个区间的最右元素或者最后一个区间的最左元素来进行比较。这里题目为交集大小为2,因此我们可以设置每一个区间都有一个todo来记录还需要的交集元素个数。

将区间按左端点升序,右端点降序。注意右端点降序的意义是我们是从最后一个区间开始往前考虑的,这样保证在左端点相同时,拥有最小的右端点,这样子包含了更多的多重性。
例1:(0占位)
1 2 3 4
0 1 2 3
0 2 3 4 5
0 0 3 4 5 6
之后
1 2 3 4
0 1 2 3
0 2 3 4 5
0 0 3 4 5 6

代码实现

 static bool cmp(vector<int>& a,vector<int>& b)
    {
        return a[0]==b[0]?a[1]>b[1]:a[0]<b[0];
    }

    int intersectionSizeTwo(vector<vector<int>>& intervals) {

        sort(intervals.begin(),intervals.end(),cmp);

        int size=intervals.size();

        vector<int> todo(size,2);

        int count=0,i=size;

        while(--i>=0)
        {
            for(int val=intervals[i][0];val<intervals[i][0]+todo[i];val++)
            {
                for(int j=i-1;j>=0;j--)
                {
                    if(todo[j]&&val<=intervals[j][1])
                    {
                        todo[j]--;
                    }
                }
            }
            count+=todo[i];
        }
        return count;
    }

反思与收获

贪心的点在于往越多可能重复的地方靠,因为是从最后一个区间开始考虑,因此贪心选择最左边的元素。

861:翻转矩阵后的得分

问题描述

有一个二维矩阵 A ,其中每个元素的值为 0 或 1 。

翻转是指选择任一行或列,并转换该行或列中的每一个值:将所有 0 都更改为 1,将所有 1 都更改为 0。

在做出任意次数的翻转后,将该矩阵的每一行都按照二进制数来解释,矩阵的得分就是这些数字的总和。

返回尽可能高的分数。

示例:

输入:[[0,0,1,1],[1,0,1,0],[1,1,0,0]]

输出:39

解释:

转换为 [[1,1,1,1],[1,0,0,1],[1,1,1,1]]

0b1111 + 0b1001 + 0b1111 = 15 + 9 + 15 = 39

解题思路

首先要明确的是二进制,如果最高位为1,则该数大于其他位都是1的情况,1000=8,0111=7,所以如果每一行第一位不为1则翻转。
行翻转已经确定了,因此剩下位数是否翻转是通过列来实现的,判断标准为1的个数是否大于0。
问题就在于怎么将代码写的简洁,少用几个循环和判断。

代码实现

  int matrixScore(vector<vector<int>>& A)
    {
        if(A.empty())
        {
            return 0;
        }

        int m=A.size();
        int n=A[0].size();
        //对每行进行是否翻转操作
        for(int i=0;i<m;i++)
        {
            if(A[i][0]==0)
            {
                for(int j=0;j<n;j++)
                {
                    A[i][j]=A[i][j]^1;
                }
            }
        }

        int sum=m*pow(2,n-1);
        int count;

        //对每列进行是否翻转操作,这里i为列,j为行了
        for(int i=1;i<n;i++)
        {
            count=0;
            for(int j=0;j<m;j++)
            {
                if(A[j][i]==1)
                {
                    count++;
                }
            }
            if(count<=m/2)
            {
                count=m-count;
            }
            sum+=count*pow(2,n-1-i);
        }

        return sum;
    }
    }

反思与收获

位运算:

符号 名称 规则 举例
& 都为1,则1 0&0=0,0&1=0,1&0=0,1&1=1
| 都为0,则0 0|0=0,0|1=1,1|0=1,1|1=1
^ 异或 两位不同为1,否则为0 0^ 0=0,0^ 1=1,1^ 0=1,1^1=0
~ 取反 0变1,1变0 ~ 1=0,~0=1

这里运用了与1异或来实现01翻转
这个count的使用十分巧妙,不用真的实现发展操作,即记录了1的数量又记录了0的数量。
学会多角度思考,行方向,列方向。

881:救生艇

问题描述

第 i 个人的体重为 people[i],每艘船可以承载的最大重量为 limit。

每艘船最多可同时载两人,但条件是这些人的重量之和最多为 limit。

返回载到每一个人所需的最小船数。(保证每个人都能被船载)。

示例 1:

输入:people = [1,2], limit = 3

输出:1

解释:1 艘船载 (1, 2)

示例 2:

输入:people = [3,2,2,1], limit = 3

输出:3

解释:3 艘船分别载 (1, 2), (2) 和 (3)

示例 3:

输入:people = [3,5,3,4], limit = 5

输出:4

解释:4 艘船分别载 (3), (3), (4), (5)

解题思路

为了每艘船尽可能的发挥它的用途载两个人,因此我们希望最重的或许能跟最轻的组成一队,这样更有可能实现更多的配对。首先进行排序,给最重的一艘船,如果最轻的人可以坐上最好,不能就考虑坐上第二重的人的船。

代码实现

   int numRescueBoats(vector<int>& people, int limit) {
        sort(people.begin(),people.end());
        int i=0,j=people.size()-1;
        int ans=0;
        while(i<=j)
        {
            ans++;
            if(people[i]+people[j]<=limit)
            {
                i++;
            }
            j--;
        }
        return ans;
    }

反思与收获

这里用到的贪心是,将最重的和最轻的一起乘船,但是在leetcode上看到有人提问说为什么不是相加之和越符合limit的两个人一起住,也没人讲明白,我也讲不出所以然。希望能有清晰有说服力的数学证明

1029:两地调度

问题描述

公司计划面试 2N 人。第 i 人飞往 A 市的费用为 costs[i][0],飞往 B 市的费用为 costs[i][1]。

返回将每个人都飞到某座城市的最低费用,要求每个城市都有 N 人抵达。

示例:

输入:[[10,20],[30,200],[400,50],[30,20]] 输出:110 解释: 第一个人去 A 市,费用为 10。
第二个人去 A 市,费用为 30。 第三个人去 B 市,费用为 50。 第四个人去 B 市,费用为 20。

最低总费用为 10 + 30 + 50 + 20 = 110,每个城市都有一半的人在面试。

提示:

1 <= costs.length <= 100
costs.length 为偶数
1 <= costs[i][0], costs[i][1] <= 1000

解题思路

尽可能发费用更低,或许一开始会想到在A地选择前N个少的,再在B地选择前N个少的,但是可能会有重复冲突,怎么来解决,可能这么想会向复杂了,但是这或许能走向正确的解法。怎么来解决呢,选择A还是B,这时候你就要考虑选哪个你才不会亏的多,因此需要考虑两地之间的差价,选择 选择A地更划算的前N个,剩下的自然选B地

代码实现

  static bool cmp(const vector<int> &a,const vector<int> &b)
    {
        return a[2]<b[2];
    }
    int CalcCost(vector<vector<int>>& costs) {

        for(int i=0;i<costs.size();i++)
        {
            costs[i].push_back(costs[i][0]-costs[i][1]);
        }
        sort(costs.begin(),costs.end(),cmp);

        int ans=0,n=costs.size()/2;

        for(int i=0;i<n;i++)
        {
            ans+=costs[i][0]+costs[i+n][1];
        }
        return ans;
    }

反思与收获

vector中的cmp函数必须是

`static bool cmp(const vector<int> &a,const vector<int> &b)`

——————————————————————————————————————————
贪心思想真的挺难的,简单题目的话就是很简单,按正常思路去贪就行了,但是稍微难一点的题目就可能完全没有了头绪,就怕在题解那里是显然得出+几行代码就能解决的问题,自己却怎么也想不出怎么解…这几道题目可以多次回顾
题目都来自leetcode官网。

你可能感兴趣的:(Leetcode练习题:贪心思想)