1. 前言
贪心算法(Greedy Algorithm)是一种在每一步选择中都采取当前状态下最优决策的算法。贪心算法通常用来解决最优化问题,其核心思想是通过局部最优解逐步推导出全局最优解。
在贪心算法中,我们并不总是考虑到未来可能发生的情况,而是只关注当前的最优选择。这种贪心选择性质使得贪心算法特别适合解决那些具有最优子结构性质的问题,即局部最优解能够推导出全局最优解的问题。
贪心算法的基本思路可以总结为以下几步:
- 确定问题的最优子结构:问题的最优解可以通过子问题的最优解逐步推导得到。
- 构造贪心选择:在每一步都做出当前状态下的最优选择,即局部最优解。
- 证明贪心选择性质:证明每一步的贪心选择都是最优的,能够推导出全局最优解。
需要注意的是,贪心算法并不适用于所有的问题,因为并非所有问题都具有最优子结构性质。在某些情况下,贪心算法得到的结果可能并不是全局最优解,而只是一个较好的解。因此,在应用贪心算法时,需要仔细分析问题的特性,以确定贪心算法是否适用于该问题。
2. 算法题
2.1_最长回文串
题意分析
- 题目要求找到字符串的 字符可以组成的最长回文串的长长度
- 对于回文串,只要出现次数是偶数,就可以加上,最多只能出现一个单个字符(中间位)
思路
- 由于只需要长度而不需要实际的串,我们可以利用哈希表和一个结果变量ret:
- hash统计各字符的出现次数:
- ret直接加上所有字符的最高偶数次,最后如果s仍有元素,返回ret+1,否则ret就是最终结果
代码
class Solution {
public:
int longestPalindrome(string s) {
// 数组代替哈希
int hash[127] = { 0 };
for(char ch : s) hash[ch]++;
// 统计结果
int ret = 0;
for(int x : hash)
ret += x / 2 * 2; // 先加上所有偶数位(比如7个a,就加上6个)
// 如果原始字符串的长度比计算出的偶数部分长度还要长,说明存在至少一个字符出现奇数次,因此需要额外再加上一个奇数长度。
return ret < s.size() ? ret + 1 : ret;
}
};
2.2_增减字符串匹配
题意分析
- 根据题目,重点在于如何分配元素,使序列在满足当前比较关系的同时,不影响后面的分配
思路
- 遍历字符串,当前字符为’I’:插入最小的元素(保证当前元素一定小于后面)
- 当前字符为’D’:插入最大的元素(保证当前元素一定大于后面)
代码
class Solution {
public:
vector<int> diStringMatch(string s) {
int left = 0, right = s.size();
vector<int> ret;
for(char ch : s)
{
if(ch == 'I')
ret.push_back(left++);
else
ret.push_back(right--);
}
ret.push_back(left); // 加上最后一位
return ret;
}
};
2.3_分发饼干
题意分析
- 要求尽可能使更多的孩子满足,注意每个孩子只能吃一块饼干,有了这点我们就可以利用贪心,给每个孩子吃能满足自己的最小的饼干,如何做到?——排序
思路
- 排序两数组,利用两指针:如果孩子的胃口小于饼干大小,就可以吃,记录结果,指针右移,反之找更大的饼干
- 由于进行了排序,只要当前饼干不能满足当前孩子,自然更不能满足后面的孩子
代码
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
// 排序数组
sort(g.begin(), g.end());
sort(s.begin(), s.end());
int ret = 0, i = 0, j = 0; // 两数组的指针
while(i < g.size() && j < s.size())
{
if(g[i] <= s[j])
i++, j++, ret++;
else
j++;
}
return ret;
}
};
2.4_跳跃游戏 II
题意分析
- 本题目像是 跳台阶 的变体:重点是 可以跳跃的距离根据当前数组元素的值决定
- 题目要求找到最后一个位置的最小跳跃次数,即我们跳跃要尽可能的远。
- 这里我们使用贪心,要尽可能的跳远:(当然这道题也可以使用动态规划解题)
思路
- 我们可以利用两个指针,分别标记当前可以跳跃的左右区间范围
- 当
left <= right
,进行循环:- 每次遍历left到right 找最大的跳跃距离
max(maxPos, nums[i] + i)
- 后根据
maxPos
更新下一次可以跳跃到的区间
- 每次遍历left到right 找最大的跳跃距离
代码
class Solution {
public:
int jump(vector<int>& nums) {
// 类似层序遍历
int left = 0, right = 0, ret = 0, n = nums.size(), maxPos = 0;
while(left <= right)
{
// 跳到了最后
if(maxPos >= n - 1) return ret;
// 本次跳跃可以到的位置
for(int i = left; i <= right; ++i)
maxPos = max(maxPos, nums[i] + i); // 更新 maxPos 为当前位置能够到达的最远位置
left = right + 1, right = maxPos; // 下一次跳跃的范围
ret++;
}
// 特殊情况,没到达最后
return -1;
}
};
顺便贴出动态规划解法(dp):
int jump(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n, INT_MAX); // 初始化动态规划数组,初始值为无穷大
dp[0] = 0; // 起始位置不需要跳跃
for (int i = 1; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (j + nums[j] >= i) { // 如果位置 j 能够跳到位置 i
dp[i] = min(dp[i], dp[j] + 1); // 更新跳到位置 i 的最小跳跃次数
}
}
}
return dp[n - 1]; // 返回跳到最后一个位置的最小跳跃次数
}
2.5_跳跃游戏
题意分析
- 对于跳跃游戏Ⅰ, 只需要判断是否可以跳到最后一个下标:
- 跟随上一题的思路使用贪心
思路
- 跟随上一题的思路,当max >= n - 1时,返回true即可
代码
class Solution {
public:
bool canJump(vector<int>& nums) {
int left = 0, right = 0, maxPos = 0, n = nums.size();
while(left <= right)
{
// 跳到了最后一个位置
if(maxPos >= n - 1) return true;
for(int i = left; i <= right; ++i)
maxPos = max(maxPos, nums[i] + i);
left = right+1, right = maxPos;
}
// 到不了最后一个下标
return false;
}
};
2.6_加油站
题意分析
- 题目要求从某个加油站出发,绕行一圈回到起点,使得途中不会因为油量不足而无法到达下一个加油站
- 我们可以对这一过程进行模拟:
思路
- 遍历gas数组,对每一个起点进行枚举:
- 从起点位置向后移动,每次计算剩余油量(利用循环),如果剩余<0,则该起点不合适,继续遍历
- 直到出现走过一轮后,剩余油量依然>=0,此时返回下标
代码
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int n = gas.size();
for(int i = 0; i < n; ++i)
{
int remain = 0, step = 0;
for( ; step < n; ++step)
{
int index = (i + step) % n; // index: 走step步后的下标
remain += gas[index] - cost[index];
if(remain < 0) break; // 无法到达下一站
}
if(remain >= 0) // 绕行了一圈
return i;
i += step; // 跳过已经遍历过的
}
return -1;
}
};