一、贪心算法简介
证明贪心策略正确性的常用方法:直接证明、交换论证法、反证法、分类讨论…
二、相关编程题
2.1 柠檬水找零
题目链接
860. 柠檬水找零 - 力扣(LeetCode)
题目描述
算法原理
提示:最优解和贪心解唯一可能不同的就是20块钱的找零方法,贪心解由优先选择10+5,最优解可能是5+5+5。最优解中可能没有用到10块找零,也可能在后面找零时用到了,但都可以和5+5交换也转换成10+5。这样在不破坏最优性质的前提下就可以转化为贪心解。
编写代码
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int f = 0, t = 0;
for(auto e : bills)
{
if(e == 5) ++f;
else if(e == 10)
{
if(f>0) ++t, --f;
else return false;
}
else
{
if(t>0 && f>0) --t, --f;
else if(f>2) f-=3;
else return false;
}
}
return true;
}
};
2.2 将数组和减半的最少操作次数
题目链接
2208. 将数组和减半的最少操作次数 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
int halveArray(vector<int>& nums) {
priority_queue<double> heap;
double sum = 0;
for(auto e : nums)
{
heap.push(e);
sum+=e;
}
int cnt = 0;
double aim = sum/2;
while(sum > aim)
{
double top = heap.top()/2;
heap.pop();
sum-=top;
heap.push(top);
++cnt;
}
return cnt;
}
};
2.3 最大数
题目链接
179. 最大数 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
string largestNumber(vector<int>& nums) {
vector<string> strs;
for(auto e : nums)
strs.push_back(to_string(e));
sort(strs.begin(), strs.end(), [](const string& a, const string& b)
{
return a+b > b+a;
});
string ret;
for(auto e : strs) ret += e;
if(ret[0] == '0') return "0";
return ret;
}
};
2.4 摆动序列
题目链接
376. 摆动序列 - 力扣(LeetCode)
题目描述
算法原理
提示:这里使用的是反证法证明的
编写代码
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int n = nums.size();
if(n < 2) return n;
int left = 0, right = 0;
int ret = 0;
for(int i = 0; i < n-1; ++i)
{
right = nums[i+1]-nums[i];
if(right == 0) continue; //right跳过中间连续相等的区间
if(left*right <= 0) ++ret; //等于0是为了选第1个位置
left = right;
}
return ret+1; //最后1个位置是一定要选的
}
};
2.5 最长递增子序列
题目链接
300. 最长递增子序列 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> arr;
arr.push_back(nums[0]);
for(auto e : nums)
{
if(e > arr.back())
arr.push_back(e);
else
{
int left = 0, right = arr.size();
while(left < right)
{
int mid = left+(right-left)/2;
if(arr[mid] < e) left = mid+1;
else if(arr[mid] >= e) right = mid;
}
arr[left] = e;
}
}
return arr.size();
}
};
2.6 递增的三元子序列
题目链接
334. 递增的三元子序列 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
bool increasingTriplet(vector<int>& nums) {
int a = nums[0], b = INT_MAX;
for(int i = 1; i < nums.size(); ++i)
{
if(nums[i] > b) return true;
else if(nums[i] <= a) a = nums[i];
else b = nums[i];
}
return false;
}
};
2.7 最长连续递增序列
题目链接
674. 最长连续递增序列 - 力扣(LeetCode)
题目描述
算法原理
略
编写代码
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int ret = 0;
int i = 0, j = 1;
for(; j < nums.size(); ++j)
{
if(nums[j] <= nums[j-1])
{
ret = max(ret, j-i);
i = j;
}
}
ret = max(ret, j-i); //记得算上最后一个子数组
return ret;
}
};
2.8 买卖股票的最佳时机
题目链接
121. 买卖股票的最佳时机 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
int maxProfit(vector<int>& prices) {
int ret = 0, prevmin = prices[0];
for(int i = 1; i < prices.size(); ++i)
{
ret = max(ret, prices[i]-prevmin);
prevmin = min(prevmin, prices[i]);
}
return ret;
}
};
2.9 买卖股票的最佳时机Ⅱ
题目链接
122. 买卖股票的最佳时机 II - 力扣(LeetCode)
题目描述
算法原理
编写代码
//解法一:双指针
class Solution {
public:
int maxProfit(vector<int>& prices) {
int ret = 0;
int i = 0, j = 1;
for(; j < prices.size(); ++j)
{
if(prices[j] <= prices[j-1]) //找到上升末端的下一个位置j
{
ret += prices[j-1]-prices[i];
i = j;
}
}
ret += prices[j-1]-prices[i];
return ret;
}
};
//解法二:将交易拆分成一天一天
class Solution {
public:
int maxProfit(vector<int>& prices) {
int ret = 0;
for(int i = 1; i < prices.size(); ++i)
{
int tmp = prices[i]-prices[i-1];
if(tmp > 0) ret+=tmp;
}
return ret;
}
};
2.10 k次取反后最大化的数组和
题目链接
1005. K 次取反后最大化的数组和 - 力扣(LeetCode)
题目描述
算法原理
每次将数组中最小的数变为相反数即可,也可以优化一下,如果数组中最小的数是正数且此时的变换次数是偶数可以直接忽略跳过。
编写代码
class Solution {
public:
int largestSumAfterKNegations(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> heap; //用小根堆选出最小的数
int sum = 0;
for(auto e : nums)
{
heap.push(e);
sum += e;
}
while(k--)
{
int top = heap.top();
if(top>=0 && (k+1)%2==0) break;
heap.pop();
heap.push(-top);
sum-=top*2;
}
return sum;
}
};
2.11 按身高排序
题目链接
2418. 按身高排序 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
vector<string> sortPeople(vector<string>& names, vector<int>& heights) {
int n = names.size();
vector<int> index(n);
for(int i = 0; i < n; ++i)
index[i] = i;
sort(index.begin(), index.end(), [&](int a, int b){
return heights[a] > heights[b];
});
vector<string> ret;
for(auto i : index)
{
ret.push_back(names[i]);
}
return ret;
}
};
2.12 优势洗牌
题目链接
870. 优势洗牌 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
vector<int> advantageCount(vector<int>& nums1, vector<int>& nums2) {
int n = nums1.size();
vector<int> index(n);
for(int i = 0; i < n; ++i)
index[i] = i;
sort(nums1.begin(), nums1.end());
sort(index.begin(), index.end(), [&](int a, int b){
return nums2[a] < nums2[b];
});
vector<int> ret(n);
int l = 0, r = n-1;
for(auto e : nums1)
{
if(e <= nums2[index[l]]) //比不过
ret[index[r--]] = e;
else //比得过
ret[index[l++]] = e;
}
return ret;
}
};
2.13 最长回文串
题目链接
409. 最长回文串 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
int longestPalindrome(string s) {
//计数
int hash[127] = {0};
for(auto ch : s)
hash[ch]++;
//统计结果
int ret = 0;
for(auto e : hash)
{
ret += e/2*2; //整数除法向下取整
}
return ret==s.size()? ret:ret+1;
}
};
2.14 增减字符串匹配
题目链接
942. 增减字符串匹配 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
vector<int> diStringMatch(string s) {
int l = 0, r = s.size();
vector<int> ret;
for(int i = 0; i < s.size(); ++i)
{
if(s[i] == 'I') ret.push_back(l++);
else ret.push_back(r--);
}
ret.push_back(l);
return ret;
}
};
2.15 分发饼干
题目链接
455. 分发饼干 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(), g.end());
sort(s.begin(), s.end());
int ret = 0;
for(int i = 0, j = 0; i<g.size() && j<s.size();)
{
if(s[j] >= g[i]) ++ret, ++i, ++j;
else ++j;
}
return ret;
}
};
2.16 最优除法
题目链接
553. 最优除法 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
string optimalDivision(vector<int>& nums) {
int n = nums.size();
if(n == 1) return to_string(nums[0]);
if(n == 2) return to_string(nums[0])+"/"+to_string(nums[1]);
string ret;
ret += to_string(nums[0])+"/("+to_string(nums[1]);
for(int i = 2; i < n; ++i)
{
ret+="/"+to_string(nums[i]);
}
ret+=")";
return ret;
}
};
2.17 跳跃游戏Ⅱ
题目链接
45. 跳跃游戏 II - 力扣(LeetCode)
题目描述
算法原理
编写代码
//解法一:动态规划
class Solution {
public:
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)
dp[i] = min(dp[i], dp[j]+1);
}
}
return dp[n-1];
}
};
//解法二:一层一层遍历
class Solution {
public:
int jump(vector<int>& nums) {
int n = nums.size();
int left = 0, right = 0; //每一次跳跃范围的左右端点
int maxpos = 0; //下一跳范围的右端点
int ret = 0;
while(left <= right) //保险写法,以防跳不到n-1的位置
{
if(right >= n-1) //判断是否已经能跳到最后位置
return ret;
//否则遍历当前层,求下一层的右端点
for(int i = left; i <= right; ++i)
maxpos = max(maxpos, i+nums[i]);
++ret;
left = right+1;
right = maxpos;
}
return -1; //跳不到的情况
}
};
//解法三:贪心
class Solution {
public:
int jump(vector<int>& nums) {
int n = nums.size();
if(n == 1) return 0;
int cnt = 0;
for(int i = 0; i < n;)
{
if(i+nums[i] >= n-1) //判断下一跳是否已经能跳到最后位置
{
++cnt;
break;
}
//贪心策略:在每一条的范围中选择 当前跳+下一跳 跳地最远地位置
int maxstep = 0, maxi;
int j = 1;
for(; j <= nums[i]; ++j)
{
if(j+nums[i+j] > maxstep)
{
maxstep = j+nums[i+j];
maxi = i+j;
}
}
i = maxi;
++cnt;
}
return cnt;
}
};
2.18 跳跃游戏
题目链接
55. 跳跃游戏 - 力扣(LeetCode)
题目描述
算法原理
同上一题
编写代码
class Solution {
public:
bool canJump(vector<int>& nums) {
int n = nums.size();
int left = 0, right = 0;
int maxpos = 0;
while(left <= right)
{
if(right >= n-1)
return true;
for(int i = left; i <= right; ++i)
maxpos = max(maxpos, i+nums[i]);
left = right+1;
right = maxpos;
}
return false;
}
};
2.19 加油站
题目链接
134. 加油站 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int n = gas.size();
for(int i = 0; i < n;)
{
int mass = 0; //油量
int step = 0;
for(; step < n; ++step)
{
int index = (i+step)%n;
mass += gas[index]-cost[index];
if(mass < 0) break; //从当前站能否到下一站
}
if(mass >= 0) return i;
i = i+step+1;
}
return -1;
}
};
2. 20 单调递增的数字
题目链接
738. 单调递增的数字 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
int monotoneIncreasingDigits(int n) {
string strn = to_string(n);
for(int i = 0; i < strn.size()-1; ++i)
{
if(strn[i] > strn[i+1])
{
while(i-1 >= 0 && strn[i-1] == strn[i])
--i;
--strn[i];
for(int j = i+1; j < strn.size(); ++j)
strn[j] = '9';
}
}
return stoi(strn);
}
};
2.21 坏了的计算器
题目链接
991. 坏了的计算器 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
int brokenCalc(int s, int t) {
int ret = 0;
while(t!= s)
{
if(t < s)
{
ret+=s-t;
break;
}
else
{
if(t%2==0) t/=2;
else ++t;
++ret;
}
}
return ret;
}
};
2.22 合并区间
题目链接
56. 合并区间 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
//先将所有区间按照左端点排序
sort(intervals.begin(), intervals.end());
vector<vector<int>> ret;
int left = intervals[0][0], right = intervals[0][1]; //合并区间的左右端点
//合并区间
for(int i = 1; i < intervals.size(); ++i)
{
//判断和合并区间是否有交集
if(intervals[i][0] <= right)
{
right = max(right, intervals[i][1]);
}
else
{
ret.push_back({left, right});
left = intervals[i][0];
right = intervals[i][1];
}
}
ret.push_back({left, right}); //将最后一个区间加入结果
return ret;
}
};
2.23 无重叠区间
题目链接
435. 无重叠区间 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
int ret = 0;
sort(intervals.begin(), intervals.end());
int right = intervals[0][1]; //用于记录最小的右区间端点
for(int i = 1; i < intervals.size(); ++i)
{
if(intervals[i][0] < right) //有重叠
{
right = min(right, intervals[i][1]);
++ret; //删掉右端点较大者
}
else
{
right = intervals[i][1];
}
}
return ret;
}
};
2.24 用最少数量的箭引爆气球
题目链接
452. 用最少数量的箭引爆气球 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
int findMinArrowShots(vector<vector<int>>& points) {
sort(points.begin(), points.end());
int ret = 0;
int right = points[0][1]; //交集区间的右端点
for(int i = 0; i < points.size(); i++)
{
if(points[i][0] <= right)
right = min(right, points[i][1]);
else
{
++ret; //一支箭射穿所有相交的气球
right = points[i][1];
}
}
++ret;
return ret;
}
};
2.25 整数替换
题目链接
397. 整数替换 - 力扣(LeetCode)
题目描述
算法原理
编写代码
//解法一:记忆化搜索
class Solution {
unordered_map<long, int> hash;
public:
int integerReplacement(long n) {
if(hash.count(n)) return hash[n];
if(n == 1) return 0;
if(n%2 == 0) hash[n] = integerReplacement(n/2)+1;
else hash[n] = min(integerReplacement(n+1), integerReplacement(n-1))+1;
return hash[n];
}
};
//解法二:贪心
class Solution {
public:
int integerReplacement(long n) {
int ret = 0;
while(n>1)
{
if(n%2==0)
n/=2;
else
{
if(n == 3 || n%4==1) --n;
else if(n%4==3) ++n;
}
++ret;
}
return ret;
}
};
2.26 俄罗斯套娃信封问题
题目链接
354. 俄罗斯套娃信封问题 - 力扣(LeetCode)
题目描述
算法原理
提示:为什么要重写排序?
- 按宽升序排序:最终目的是要将题目改造成最长递增子序列一样的单一限制条件(而本题要求宽高两个条件),因此要先按宽升序排序,这样在后续的贪心过程中就只需要比较高了。
- 宽相同时,按高降序排序:相同宽度的信封不能套娃,因此要将相同宽度的信封的高降序排序,这样在后续的贪心比较中,这些相同宽度的信封就不会增加套娃长度了。
编写代码
//解法一:动态规划(超时)
class Solution {
public:
int maxEnvelopes(vector<vector<int>>& env) {
sort(env.begin(), env.end());
int n = env.size();
vector<int> dp(n, 1);
int ret = 1;
for(int i = 1; i < n; ++i)
{
for(int j = 0; j < i; ++j)
{
if(env[i][0]>env[j][0] && env[i][1]>env[j][1])
{
dp[i] = max(dp[i], dp[j]+1);
}
}
ret = max(ret, dp[i]);
}
return ret;
}
};
//解法二:重写排序+贪心+二分
class Solution {
public:
int maxEnvelopes(vector<vector<int>>& env) {
//重写排序
sort(env.begin(), env.end(), [](const vector<int>& a, const vector<int>& b)
{
if(a[0] != b[0]) return a[0]<b[0];
else return a[1]>b[1];
});
//贪心+二分
vector<int> len;
int n = env.size();
len.push_back(env[0][1]);
for(int i = 1; i < n; ++i)
{
if(env[i][1] > len.back())
len.push_back(env[i][1]);
else
{
int left = 0, right = len.size()-1;
while(left < right)
{
int mid = (left+right)>>1;
if(len[mid] < env[i][1])
left = mid+1;
else
right = mid;
}
len[left] = env[i][1];
}
}
return len.size();
}
};
2.27 可被三整除的最大和
题目链接
1262. 可被三整除的最大和 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
int maxSumDivThree(vector<int>& nums) {
const int INF = 0x3f3f3f3f;
int x1 = INF, x2 = INF, y1 = INF, y2 = INF;
int sum = 0;
for(int e :nums)
{
sum+=e;
if(e%3 == 1)
{
if(e < x1) x2 = x1, x1 = e;
else if(e < x2) x2 = e;
}
else if(e%3 == 2)
{
if(e < y1) y2 = y1, y1 = e;
else if(e < y2) y2 = e;
}
}
if(sum%3==1) sum-=min(x1, y1+y2);
else if(sum%3==2) sum-=min(y1, x1+x2);
return sum;
}
};
2.28 距离相等的条形码
题目链接
1054. 距离相等的条形码 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
vector<int> rearrangeBarcodes(vector<int>& barcodes) {
//统计各数字的出现次数,并找出出现最多的数字
unordered_map<int, int> hash;
int maxn, maxc=0;
for(int e : barcodes)
{
hash[e]++;
if(hash[e] > maxc)
{
maxc = hash[e];
maxn = e;
}
}
int n = barcodes.size();
vector<int> ret(n);
int i = 0; //先放偶数下标
//先处理出现次数最多的数字
while(hash[maxn]>0)
{
--hash[maxn];
ret[i] = maxn;
i+=2;
}
//再处理其他数字
for(auto& [x, y] : hash)
{
while(y>0)
{
if(i > n-1) i = 1; //再放奇数下标
--y;
ret[i] = x;
i+=2;
}
}
return ret;
}
};
2.29 重构字符串
题目链接
767. 重构字符串 - 力扣(LeetCode)
题目描述
算法原理
同上一题
编写代码
class Solution {
public:
string reorganizeString(string s) {
unordered_map<char, int> hash;
char maxn;
int maxc=0;
for(char ch : s)
{
hash[ch]++;
if(hash[ch] > maxc)
{
maxc = hash[ch];
maxn = ch;
}
}
int n = s.size();
if(maxc > (n+1)/2) return ""; //判断以下是否可行
int i = 0;
string ret(n, '0');
while(hash[maxn]>0)
{
--hash[maxn];
ret[i] = maxn;
i+=2;
}
for(auto& [x,y] : hash)
{
while(y>0)
{
if(i>n-1) i = 1;
--y;
ret[i] = x;
i+=2;
}
}
return ret;
}
};