贪心算法相关面试算法题总结

前言

贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。

贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

算法题

1. LeetCode 1403 : 非递增顺序的最小子序列

LeetCode 1403

给你一个数组 nums,请你从中抽取一个子序列,满足该子序列的元素之和 严格 大于未包含在该子序列中的各元素之和。
如果存在多个解决方案,只需返回 长度最小 的子序列。如果仍然有多个解决方案,则返回 元素之和最大 的子序列。
与子数组不同的地方在于,「数组的子序列」不强调元素在原数组中的连续性,也就是说,它可以通过从数组中分离一些(也可能不分离)元素得到。
注意,题目数据保证满足所有约束条件的解决方案是 唯一 的。同时,返回的答案应当按 非递增顺序 排列。
示例 1:
输入:nums = [4,3,10,9,8]
输出:[10,9]
解释:子序列 [10,9] 和 [10,8] 是最小的、满足元素之和大于其他各元素之和的子序列。但是 [10,9] 的元素之和最大。
示例 2:
输入:nums = [4,4,7,6,7]
输出:[7,7,6]
解释:子序列 [7,7] 的和为 14 ,不严格大于剩下的其他元素之和(14 = 4 + 4 + 6)。因此,[7,6,7] 是满足题意的最小子序列。注意,元素按非递增顺序返回。
示例 3:
输入:nums = [6]
输出:[6]
提示:
1 <= nums.length <= 500
1 <= nums[i] <= 100

/*
 * 1
 * LeetCode 1403 : 非递增顺序的最小子序列
 * https://leetcode-cn.com/problems/minimum-subsequence-in-non-increasing-order/
 */
vector<int> minSubsequence(vector<int>& nums)
{
    sort(nums.begin(), nums.end(), greater<int>());
    int sum = 0;
    for(auto num : nums)
    {
        sum += num;
    }
    int cur = 0;
    for(int i = 0; i < nums.size(); i++)
    {
        cur += nums[i];
        if(cur > sum-cur)
        {
            return vector<int>(nums.begin(), nums.begin()+i+1);
        }

    }
    return nums;
}


2. LeetCode 455 : 分发饼干

LeetCode 455

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
示例 2:
输入: g = [1,2], s = [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.
提示:
1 <= g.length <= 3 * 104
0 <= s.length <= 3 * 104
1 <= g[i], s[j] <= 231 - 1

/*
 * 2
 * LeetCode 455 : 分发饼干
 * https://leetcode-cn.com/problems/assign-cookies/
 */
int findContentChildren(vector<int>& g, vector<int>& s)
{
/*
 * 利用贪心算法的思想:每次都先满足胃口最小的孩子,直到有效饼干分完,或者孩子都被满足则停止分发。
 */
    int res = 0;
    sort(g.begin(),g.end());
    sort(s.begin(),s.end());
    int gi = 0, si = 0;
    while(gi<g.size() && si<s.size())
    {
        if(s[si]>=g[gi])
        {
            gi++;
            si++;
            res++;
        }
        else
            si++;
    }
    return res;
}


3. LeetCode 1046 : 最后一块石头的重量

LeetCode 1046

有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出两块 最重的 石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块石头。返回此石头的重量。如果没有石头剩下,就返回 0。
示例:
输入:[2,7,4,1,8,1]
输出:1
解释:
先选出 7 和 8,得到 1,所以数组转换为 [2,4,1,1,1],
再选出 2 和 4,得到 2,所以数组转换为 [2,1,1,1],
接着是 2 和 1,得到 1,所以数组转换为 [1,1,1],
最后选出 1 和 1,得到 0,最终数组转换为 [1],这就是最后剩下那块石头的重量。
提示:
1 <= stones.length <= 30
1 <= stones[i] <= 1000

/*
 * 3
 * LeetCode 1046 : 最后一块石头的重量
 * https://leetcode-cn.com/problems/last-stone-weight/
 */
int lastStoneWeight(vector<int>& stones)
{
/*
 * 优先队列 : priority_queue
 * 对于基础类型 默认是大根堆(降序队列) : priority_queue<int> pq;
 * 小根堆(升序队列) : priority_queue<int, vector<int>, greater<int> > pq;
 */
    priority_queue<int> record;
    for(int weight : stones)
        record.push(weight);
    while(! record.empty())
    {
        if(record.size() == 1)
            return record.top();
        else
        {
            int max1 = record.top();
            record.pop();
            int max2 = record.top();
            record.pop();
            if(abs(max1 - max2) != 0)
                record.push(max1 - max2);
        }
    }
    return 0;
}


4. LeetCode 1578. 避免重复字母的最小删除成本

LeetCode 1578

给你一个字符串 s 和一个整数数组 cost ,其中 cost[i] 是从 s 中删除字符 i 的代价。
返回使字符串任意相邻两个字母不相同的最小删除成本。
请注意,删除一个字符后,删除其他字符的成本不会改变。
示例 1:
输入:s = “abaac”, cost = [1,2,3,4,5]
输出:3
解释:删除字母 “a” 的成本为 3,然后得到 “abac”(字符串中相邻两个字母不相同)。
示例 2:
输入:s = “abc”, cost = [1,2,3]
输出:0
解释:无需删除任何字母,因为字符串中不存在相邻两个字母相同的情况。
示例 3:
输入:s = “aabaa”, cost = [1,2,3,4,1]
输出:2
解释:删除第一个和最后一个字母,得到字符串 (“aba”) 。
提示:
s.length == cost.length
1 <= s.length, cost.length <= 10^5
1 <= cost[i] <= 10^4
s 中只含有小写英文字母

/*
 * 4
 * LeetCode 1578. 避免重复字母的最小删除成本
 * https://leetcode-cn.com/problems/minimum-deletion-cost-to-avoid-repeating-letters/
 */
int minCost(string s, vector<int>& cost)
{
    /*
     * 如果字符串中有若干相邻的重复字母,则这些字母中最多只能保留一个。
     * 因此我们可以采取贪心的策略:在这一系列重复字母中,我们保留删除成本最高的字母,
     * 并删除其他字母。这样得到的删除成本一定是最低的。
     *
     */
    int n = s.size();
    int sum = 0;
    for(int i = 0; i < n-1; i++)
    {
        if(s[i] == s[i+1])
        {
            sum += min(cost[i], cost[i+1]);
            if(cost[i] > cost[i+1])
                swap(cost[i], cost[i+1]);
        }
    }
    return sum;
}


5. LeetCode 435. 无重叠区间

LeetCode 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
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。

/*
 * 5
 * LeetCode 435. 无重叠区间
 * https://leetcode-cn.com/problems/non-overlapping-intervals/
 */
static bool compareLeft(const vector<int>& a, const vector<int>& b)
{
    return a[1] < b[1];
}
int eraseOverlapIntervals(vector<vector<int> >& intervals)
{
    //区间右端点的贪心算法
    if(intervals.size() == 0)
        return 0;
    sort(intervals.begin(),intervals.end(), compareLeft);
    int res = 1;
    int pre = 0;
    for(int i = 1; i < intervals.size(); i++)
        if(intervals[i][0] >= intervals[pre][1])
        {
            res++;
            pre = i;
        }
    return intervals.size()-res;
}

#if 0
static bool compareRight(const vector<int>& a, const vector<int>& b)
{
    return a[0] < b[0];
}
int eraseOverlapIntervals(vector<vector<int> >& intervals)
{
    //区间左端点的贪心算法
    if(intervals.size() == 0) {
        return 0;
    }
    sort(intervals.begin(), intervals.end(), compareRight);
    int end = intervals[0][1];
    int prev = 0;
    int count = 0;
    for(int i = 1; i < intervals.size(); i++)
    {
        if(intervals[prev][1] > intervals[i][0]) {
            if(intervals[prev][1] > intervals[i][1])
            {
                prev = i;
            }
            count++;
        } else {
            prev = i;
        }
    }
    return count;
}
#endif


6. LeetCode 1288. 删除被覆盖区间

LeetCode 1288

给你一个区间列表,请你删除列表中被其他区间所覆盖的区间。
只有当 c <= a 且 b <= d 时,我们才认为区间 [a,b) 被区间 [c,d) 覆盖。
在完成所有删除操作后,请你返回列表中剩余区间的数目。
示例:
输入:intervals = [[1,4],[3,6],[2,8]]
输出:2
解释:区间 [3,6] 被区间 [2,8] 覆盖,所以它被删除了。
提示:​​​​​​
1 <= intervals.length <= 1000
0 <= intervals[i][0] < intervals[i][1] <= 10^5
对于所有的 i != j:intervals[i] != intervals[j]

/*
 * 6
 * LeetCode 1288. 删除被覆盖区间
 * https://leetcode-cn.com/problems/remove-covered-intervals/
 */
/*
1. 对起点进行升序排序,如果起点相同,则对终点进行降序排序。
2. 初始化没有被覆盖的区间数:count=0。
3. 迭代排序后的区间并且比较终点大小。
   如果当前区间不被前一个区间覆盖 end > prev_end,则增加 count,指定当前区间为下一步的前一个区间。
   否则,当前区间被前一个区间覆盖,不做任何事情。
4. 返回 count。
*/
static bool compare(const vector<int>& a, const vector<int>& b)
{
    if(a[0] == b[0]) {
        return a[1] > b[1];
    } else {
        return a[0] < b[0];
    }
}
int removeCoveredIntervals(vector<vector<int> >& intervals)
{
    sort(intervals.begin(), intervals.end(), compare);
    int count = 0;
    int end, prev_end = 0;
    for(auto curr : intervals)
    {
        end = curr[1];
        // if current interval is not covered
        // by the previous one
        if(prev_end < end) {
            ++count;
            prev_end = end;
        }
    }
    return count;
}


7. LeetCode 134. 加油站

LeetCode 134

在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。
说明:
如果题目有解,该答案即为唯一答案。
输入数组均为非空数组,且长度相同。
输入数组中的元素均为非负数。
示例 1:
输入:
gas = [1,2,3,4,5]
cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。
示例 2:
输入:
gas = [2,3,4]
cost = [3,4,3]
输出: -1
解释:
你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油
开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。

/*
 * 7
 * LeetCode 134. 加油站
 * https://leetcode-cn.com/problems/gas-station/
 */
int canCompleteCircuit(vector<int>& gas, vector<int>& cost)
{
    int n = gas.size();
    int total_tank = 0;
    int curr_tank = 0;
    int starting_station = 0;
    for(int i = 0; i < n; ++i)
    {
        total_tank += gas[i] - cost[i];
        curr_tank += gas[i] - cost[i];
        // If one couldn't get here,
        if(curr_tank < 0)
        {
            // Pick up the next station as the starting one.
            starting_station = i + 1;
            // Start with an empty tank.
            curr_tank = 0;
        }
    }
    return total_tank >= 0 ? starting_station : -1;
}


8. LeetCode 763. 划分字母区间

LeetCode 763

字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一个字母只会出现在其中的一个片段。返回一个表示每个字符串片段的长度的列表。
示例 1:
输入:S = “ababcbacadefegdehijhklij”
输出:[9,7,8]
解释:
划分结果为 “ababcbaca”, “defegde”, “hijhklij”。
每个字母最多出现在一个片段中。
像 “ababcbacadefegde”, “hijhklij” 的划分是错误的,因为划分的片段数较少。
提示:
S的长度在[1, 500]之间。
S只包含小写字母 ‘a’ 到 ‘z’ 。

/*
 * 8
 * LeetCode 763. 划分字母区间
 * https://leetcode-cn.com/problems/partition-labels/
 */
/*
算法:对于遇到的每一个字母,去找这个字母最后一次出现的位置,用来更新当前的最小区间。
1. 定义数组 last[char] 来表示字符 char 最后一次出现的下标。
2. 定义 anchor 和 j 来表示当前区间的首尾。
   如果遇到的字符最后一次出现的位置下标大于 j, 就让 j=last[c] 来拓展当前的区间。
   当遍历到了当前区间的末尾时(即 i==j ),把当前区间加入答案,同时将 start 设为 i+1 去找下一个区间。
*/
vector<int> partitionLabels(string S)
{
    vector<int> last(26, 0);
    for(int i = 0; i < S.size(); ++i)
        last[S[i] - 'a'] = i;

    int j = 0, anchor = 0;
    vector<int> res;
    for(int i = 0; i < S.size(); ++i)
    {
        j = max(j, last[S[i] - 'a']);
        if(i == j)
        {
            res.push_back(i - anchor + 1);
            anchor = i + 1;
        }
    }
    return res;
}


总结

以上是贪心算法的相关面试算法题汇总,个别题目也给出了解题思路和注释。其中关于重叠区间问题特别经典,类似题目也很多,遇到相关题可以在纸上画画,分类归纳总结。
所有代码都可以去我的GitHub网站查看,后续也将继续补充其他算法方面的相关题目。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值