942. 增减字符串匹配 Easy 贪心 2022/5/12
由范围 [0,n] 内所有整数组成的 n + 1 个整数的排列序列可以表示为长度为 n 的字符串 s ,其中:
如果 perm[i] < perm[i + 1] ,那么 s[i] == ‘I’
如果 perm[i] > perm[i + 1] ,那么 s[i] == ‘D’
给定一个字符串 s ,重构排列 perm 并返回它。如果有多个有效排列perm,则返回其中 任何一个 。
示例:
输入:s = “IDID”
输出:[0,4,1,3,2]
方法一:贪心算法:遍历原字符串,如果为I,就找一个最小的放进v,如果为D则找一个最大的放进v。
class Solution {
public:
vector<int> diStringMatch(string s) {
vector<int> v;
// 最小值和最大值
int min = 0;
int max = s.size();
for(int i = 0; i <= s.size(); i++)
{
// 找到一个最小的放进v
if (s[i] == 'I') v.push_back(min++);
// 找到一个最大的放进v
else v.push_back(max--);
}
return v;
}
};
方法二:先把0放进去,如果为I则放个1进去,为D就放个-1进去,同时最大最小值依次改变,也是一种贪心的变种算法,最后求得最小值(负数),所有数减去该数,变为非负数列。
class Solution {
public:
vector<int> diStringMatch(string s) {
vector<int> v;
v.push_back(0);
// 最小值和最大值
int min = 0;
int max = 0;
for (int i = 0; i < s.size(); i++)
{
if (s[i] == 'I') v.push_back(++max);
else v.push_back(--min);
}
int minus = 0;
for (auto i : v)
if (minus > i)
minus = i;
if (minus != 0)
for(int i = 0; i < v.size(); i++)
v[i] -= minus;
return v;
}
};
1798. 你能构造出连续值的最大数目 Medium 贪心 2023/2/4
给你一个长度为 n 的整数数组 coins ,它代表你拥有的 n 个硬币。第 i 个硬币的值为 coins[i] 。如果你从这些硬币中选出一部分硬币,它们的和为 x ,那么称,你可以 构造 出 x 。
请返回从 0 开始(包括 0 ),你最多能 构造 出多少个连续整数。
你可能有多个相同值的硬币。
示例:
输入:coins = [1,3]
输出:2
解释:你可以得到以下这些值:
0:什么都不取 []
1:取 [1]
从 0 开始,你可以构造出 2 个连续整数。
由于受到剑指 Offer II 103. 最少的硬币数目的启发,首先想到的是能否分解为子问题,遍历整数,并编写能否构成该数字的检测函数(只能用递归,因为硬币数量有限,需要增加和删除硬币)。
class Solution {
public:
int getMaximumConsecutive(vector<int>& coins) {
int i = 0;
while (1) {
// 连续遍历每个硬币数
if (traverse(coins, i) == false)
break;
i++;
}
return i;
}
bool traverse(vector<int> coins, int x) {
if (x == 0) return true;
if (x < 0) return false;
for (auto iter = coins.begin(); iter < coins.end(); iter++) {
int coin = *iter;
// 删除该硬币
coins.erase(iter);
if (traverse(coins, x - coin) == true) return true; // 看子问题能否求解
// 还原该硬币
coins.insert(iter, coin);
}
return false;
}
};
但由于硬币是用一个少一个,无法和上题一样对于每种硬币数建立一个统一的dp数组,每次对于一个硬币数都需要经过时间复杂度较高的递归过程,超时。
如果本题是检查某个数是否能被这些硬币所组合,可以考虑用上述方法。
考虑到题目中连续整数,可以考虑先将数组排序,每次都插入最小的那个元素,如果该元素>原来可表示的最大元素+1
,则说明无法继续表示,否则加上该元素以扩展可表示的元素。
贪心算法的核心就在于在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。
class Solution {
public:
int getMaximumConsecutive(vector<int>& coins) {
sort(coins.begin(), coins.end());
int span = 0;
for (int coin: coins) {
// 前面这些小数能表示这个大数
if (coin <= span + 1)
span += coin; // 加入范围
else break;
}
return span + 1; // 连续整数的个数
}
};
330. 按要求补齐数组 Hard 贪心 2023/2/4
给定一个已排序的正整数数组 nums ,和一个正整数 n 。从 [1, n] 区间内选取任意个数字补充到 nums 中,使得 [1, n] 区间内的任何数字都可以用 nums 中某几个数字的和来表示。
请返回 满足上述要求的最少需要补充的数字个数 。
示例:输入: 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] 区间里所有的数。
所以我们最少需要添加一个数字。
本题为1798题的进阶版本,要求输出需要补充的数字数,很容易想到对于数组中的每个数,判断是否有漏的(span+1缺失无法表示),如果有漏,则扩张到2*span+1
并计数+1,直到能把数组中的数添加进来,如果数组元素都用完了还不能表示n,则再通过上述方式扩张。
class Solution {
public:
int minPatches(vector<int>& nums, int n) {
long span = 0;
int cnt = 0;
for (int num: nums) {
if (span >= n) break;
// 有漏的,需要加数(span + 1)
while (span + 1 < num) {
cnt++;
span += (span + 1);
if (span >= n) break;
}
// 添加数组元素扩大范围
span += num;
}
// 数组元素都用完了
while (span < n) {
span += (span + 1);
cnt++;
}
return cnt;
}
};
这样的思路可行,但编写代码时,需要时刻注意span>=n
时退出循环,有可能数组中的数还没添加完就已经够了,想到可以改进遍历数组的过程,改为一个while循环,在里面进行数组遍历。
只有在数组索引合法且index当前数在连续范围内时,用数组中的数进行范围扩容,否则直接进行2*span+1
的扩容,非常精简巧妙。
class Solution {
public:
int minPatches(vector<int>& nums, int n) {
int cnt = 0; // 初始化要补的次数为0次
long span = 0; // 初始化span,此时区间为[0,0]
int index = 0; // 从第0个位置开始
while (span < n) { // 退出条件
// 数组能遍历且index当前数在连续范围内
if (index < nums.size() && nums[index] <= span + 1) {
span += nums[index];
index++;
}
// 范围扩容(加上span)
else {
span += span + 1;
cnt++;
}
}
return cnt;
}
};
134. 加油站 Medium 贪心 2023/2/4
在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas 和 cost ,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -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 可为起始索引。
首先,本题可以进行以下简化:
- 如果gas总和小于cost总和,显然不存在解。
- 将gas每项与cost作差,并进行延拓,此时起始加油站只需遍历0~n即可。
- 此时gas的含义是经过该加油站得到的油数(可能为负数)。
如果对于每个起始加油站,向右进行遍历n个数,时间复杂度为O(n²),超时。
想到可以利用滑动窗口,左窗口不变,右窗口向右滑动直到总和<0,再将左窗口右移直到油数>=0。
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int n = gas.size();
// 逐项作差
for (int i = 0; i < n; i++)
gas[i] -= cost[i];
if (accumulate(gas.begin(), gas.end(), 0) < 0)
return -1;
gas.insert(gas.end(), gas.begin(), gas.end());
int left = 0;
int right = 0;
int ngas = 0;
while (left < n && right < 2*n) {
ngas += gas[right];
if (ngas < 0) {
while (ngas < 0) {
ngas -= gas[left];
left++;
}
}
if (right == left + n) return left;
right++;
}
return -1;
}
};
那么本题为什么会放在贪心算法中呢?这不是一道简单的滑动窗口的题吗?
答案在于,本题左窗口右移时,可以直接移动到右窗口向右的一个位置!因为前面这一段找不到比左窗口更好的起始加油站了。
假设从x加油站出发经过z加油站最远能到达y加油站,那么从z加油站直接出发,不可能到达y下一个加油站。因为从x出发到z加油站时肯定还有存储的油,这都到不了y的下一站,而直接从z出发刚开始是没有存储的油的,所以更不可能到达y的下一站。
因此可以优化左窗口右移的代码,一次遍历直接出结果。
while (left < n && right < 2*n) {
ngas += gas[right];
if (ngas < 0) {
left = right + 1; // 左窗口直接跳到右窗口处!
ngas = 0;
}
if (right == left + n) return left;
right++;
}
605.种花问题 Easy 贪心 2023/2/16
假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。
给你一个整数数组 flowerbed 表示花坛,由若干 0 和 1 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false。
示例:
输入:flowerbed = [1,0,0,0,1], n = 1
输出:true
本题很显然为贪心,但遍历时很容易出错,故使用预处理避免边界讨论:插入前导零和末尾零,非常巧妙。
class Solution {
public:
bool canPlaceFlowers(vector<int>& flowerbed, int n) {
if (n == 0) return true;
// 插入前导零和末尾零,避免边界讨论
flowerbed.insert(flowerbed.begin(), 0);
flowerbed.push_back(0);
for (int i = 0; i < flowerbed.size(); i++) {
// 直到找到一个为0的花坛
if (flowerbed[i] == 1) continue;
// 后面两个也必须为0
if (i + 1 >= flowerbed.size() || flowerbed[i + 1] == 1) continue;
if (i + 2 >= flowerbed.size() || flowerbed[i + 2] == 1) continue;
n--;
i++;
if (n == 0) return true;
}
return false;
}
};
135.分发糖果 Hard 贪心 2023/2/22
n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
每个孩子至少分配到 1 个糖果。
相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
给定一个字符串 s ,重构排列 perm 并返回它。如果有多个有效排列perm,则返回其中 任何一个 。
示例:
输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。
第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。
两次遍历贪心算法:我们可以将「相邻的孩子中,评分高的孩子必须获得更多的糖果」这句话拆分为两个规则,分别处理。
- 左规则:当 r a t i n g s [ i − 1 ] < r a t i n g s [ i ] ratings[i−1]<ratings[i] ratings[i−1]<ratings[i] 时, i i i 号学生的糖果数量将比 i − 1 i-1 i−1 号孩子的糖果数量多。
- 右规则:当 r a t i n g s [ i ] > r a t i n g s [ i + 1 ] ratings[i]>ratings[i+1] ratings[i]>ratings[i+1] 时, i i i 号学生的糖果数量将比 i + 1 i+1 i+1 号孩子的糖果数量多。
我们遍历该数组两次,处理出每一个学生分别满足左规则或右规则时,最少需要被分得的糖果数量。每个人最终分得的糖果数量即为这两个数量的最大值。
class Solution {
public:
int candy(vector<int>& ratings) {
int n = ratings.size();
// 左规则
vector<int> left(n);
for (int i = 0; i < n; i++) {
if (i > 0 && ratings[i] > ratings[i - 1]) {
left[i] = left[i - 1] + 1;
} else {
left[i] = 1;
}
}
// 右规则
vector<int> right(n);
for (int i = n - 1; i >= 0; i--) {
if (i < n - 1 && ratings[i] > ratings[i + 1]) {
right[i] = right[i + 1] + 1;
} else {
right[i] = 1;
}
}
int ret = 0;
for (int i = 0; i < n; i++) {
ret += max(left[i], right[i]);
}
return ret;
}
};