文章目录
前言
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
算法题
1. 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 : 分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 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 : 最后一块石头的重量
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出两块 最重的 石头,然后将它们一起粉碎。假设石头的重量分别为 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. 避免重复字母的最小删除成本
给你一个字符串 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. 无重叠区间
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意:
可以认为区间的终点总是大于它的起点。
区间 [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. 删除被覆盖区间
给你一个区间列表,请你删除列表中被其他区间所覆盖的区间。
只有当 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. 加油站
在一条环路上有 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. 划分字母区间
字符串 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网站查看,后续也将继续补充其他算法方面的相关题目。