1.柠檬水找零
link:860. 柠檬水找零 - 力扣(LeetCode)
code
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
// 贪心算法, 优先花出大面额bill, 尽可能保护小面额bill
int five = 0, ten = 0;// 不同bill数量
for(int bill : bills)
{
if(bill == 5) five++;
else if(bill == 10)
{
ten++;
if(five <= 0) return false;
else five--;
}
else // bill == 20
{
if(five >= 1 && ten >= 1) five--, ten--;// 贪心
else if(five >= 3) five -= 3;
else return false;
}
}
return true;
}
};
交换论证法 证明:
2.将数组和减半的最少操作次数
link:2208. 将数组和减半的最少操作次数 - 力扣(LeetCode)
code
class Solution {
public:
int halveArray(vector<int>& nums) {
int ans = 0;
double sum = 0.0;
priority_queue<double> q;
for(int& e : nums)
{
sum += e;
q.push(e);
}
double target = sum/2.0;
while(target > 0)
{
ans++;
double top = q.top(); q.pop();
target -= top / 2;
q.push(top / 2);
}
return ans;
}
};
交换论证法 证明:
3.最大数
link: 179. 最大数 - 力扣(LeetCode)
key:
此问题中比较两个字符串大小:return a + b > b + a, 而不是直接return a > b;
code
class Solution {
public:
string largestNumber(vector<int>& nums) {
vector<string> strs;
for(int num:nums)
{
strs.push_back(to_string(num));
}
sort(strs.begin(), strs.end(), [](string a, string b){
return a + b > b + a;// key:不要直接使用内置的字典排序(return a > b)
});
string ans;
for(string str:strs) ans += str;
if(ans[0] == '0') return "0";
return ans;
}
};
4.摆动序列
tips:
本题也可以使用动态规划解决, 时间复杂度O(n^2)
若使用贪心算法解决此问题,时间复杂度为O(n)
本题中如何实现贪心?
画出折线图, 选出所有极值即可。极值个数即为最长摆动序列长度
证明贪心正确性:反证法或交换论证法
code1:
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
auto it = unique(nums.begin(), nums.end());
nums.erase(it, nums.end());
if(nums.size() <= 2) return nums.size();
int flag;// flag == 0表示摆动序列第一个为大值,1为小值
flag = nums[0] > nums[1] ? 0 : 1;
int ans = 1;// 第一个一定参与
for(int i = 1; i < nums.size(); i++)
{
if(flag == 0)// 找极小值
{
if(nums[i] < nums[i-1]) continue;
else
{
ans++;
flag = !flag;
}
}
else// 找极大值
{
if(nums[i] > nums[i-1]) continue;
else// nums[i-1]就是极大值
{
ans++;
flag = !flag;
}
}
}
ans++; // 最后一个一定也为最长子序列
return ans;
}
};
code2:
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
// 求 波峰个数 + 波谷个数即可
auto it = unique(nums.begin(), nums.end());
nums.erase(it, nums.end());
if(nums.size() <= 2) return nums.size();
int ans = 2;// nums首尾都是波峰/波谷
for(int i = 1; i < nums.size() - 1; i++)// 判断除首尾的中间部分是否为波峰/波谷
{
if((nums[i] - nums[i-1]) * (nums[i] - nums[i+1]) > 0)// nums[i]是波峰/波谷
{
ans++;
}
}
return ans;
}
};
5.最长递增子序列
link:300. 最长递增子序列 - 力扣(LeetCode)
贪心思路:
只关心长度为x的递增子序列的 最后元素的 最小值
code
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
// arr[i]:长度为i+1的严格递增子序列的最小末尾值
vector<int> arr;
arr.push_back(nums[0]);
for(int i = 1; i < nums.size(); i++)
{
if (nums[i] > arr.back())
{
arr.push_back(nums[i]);
continue;
}
// 找arr中第一个 >= nums[i] 的元素,替换为nums[i]
int left = 0, right = arr.size()-1;
for(; left < right; )
{
int mid = (left + right) / 2;
if(arr[mid] >= nums[i]) right = mid;
else left = mid + 1;
}
arr[left] = nums[i];
}
return arr.size();
}
};
6.递增的三元子序列
link:334. 递增的三元子序列 - 力扣(LeetCode)
code
class Solution {
public:
bool increasingTriplet(vector<int>& nums) {
// 和300.最长递增子序列 类似, 不过此题更简单
// arr[i]表示i+1长度的递增子序列中最小的结尾数
vector<int> arr;
arr.push_back(nums[0]);
for(int i = 1; i < nums.size(); i++)
{
if(nums[i] > arr.back())
{
arr.push_back(nums[i]);
if(arr.size() >= 3) return true;
continue;
}
else
{
// 在arr中二分查找第一个>=nums[i]的数, 替换为nums[i]
int left = 0, right = arr.size() - 1;
while(left < right)
{
int mid = (left + right) >> 1;
if(arr[mid] >= nums[i]) right = mid;
else left = mid + 1;
}
arr[left] = nums[i];
}
}
return false;
}
};
7.最长连续递增序列
link:674. 最长连续递增序列 - 力扣(LeetCode)
这是道简单题, 没什么好说的
code
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
// 贪心 + 双指针
int ans = 1;
for(int i = 0; i < nums.size(); i++)
{
int j = i + 1;
while(j < nums.size() && nums[j] > nums[j-1]) j++;
ans = max(ans, j - i);
}
return ans;
}
};
8.买卖股票的最佳时机
link:121. 买卖股票的最佳时机 - 力扣(LeetCode)
由暴力枚举做优化, 用minn标记之前股票最低价位作为买入点
贪心正确性:
一定正确, 因为其由暴力枚举优化而来, 暴力枚举一定正确, 则贪心也一定正确
code
class Solution {
public:
int maxProfit(vector<int>& prices) {
// 暴力枚举 -> 贪心
int minn = prices[0];
int ans = 0;
for(int i = 1; i < prices.size(); i++)
{
int profit = prices[i] - minn > 0 ? prices[i] - minn : 0;
ans = max(ans, profit);
minn = min(minn, prices[i]);
}
return ans;
}
};
9.买卖股票的最佳时机II
link:122. 买卖股票的最佳时机 II - 力扣(LeetCode)
与I不同的是, 此问题可以进行无数次交易
任意相邻两天, 只要能获得正收益, 就进行交易
code1:拆分交易
将交易都拆分为一天的交易,任意相邻两天, 只要能获得正收益, 就进行交易(一次买卖)
class Solution {
public:
int maxProfit(vector<int>& prices) {
// 拆分交易
int ans = 0;
int preVal = prices[0];
for(int i = 1; i < prices.size(); i++)
{
ans += max(prices[i] - preVal, 0);
preVal = prices[i];
}
return ans;
}
};
code2 :双指针
低点买入, 高点卖出
class Solution {
public:
int maxProfit(vector<int>& prices) {
// 双指针,低点买入,高点卖出
// 双指针适合用来寻找连续的,具有某种性质的区间
int n = prices.size();
int ans = 0;
for(int i = 0; i < n; i++)
{
int j = i;
while(j + 1 < n && prices[j] < prices[j + 1]) j++;
ans += prices[j] - prices[i];
i = j;// 不用手动给i置为最低点
}
return ans;
}
};
9.k次取反后最大化的数组和
link:1005. K 次取反后最大化的数组和 - 力扣(LeetCode)
tip:
注意STL中最小堆的定义方法, 使用模板方法指明大堆/小堆:
priority_queue<int, vector<int>, greater<int>> pq(nums.begin(), nums.end());// 小堆
code
class Solution {
public:
int largestSumAfterKNegations(vector<int>& nums, int k) {
// 贪心:每次都取最小值进行取反
priority_queue<int, vector<int>, greater<int>> pq(nums.begin(), nums.end());// 小堆
while(k--)
{
int top = pq.top();
pq.pop();
pq.push(-top);
}
int ans = 0;
while(pq.size())
{
ans += pq.top(); pq.pop();
}
return ans;
}
};
10.按身高排序
link:2418. 按身高排序 - 力扣(LeetCode)
tip:
本题并不是贪心算法题, 只是一道普通排序题,为下一道题做铺垫
code1:绑定排序
将heights与names绑定在一起, 按照heights排序
class Solution {
public:
vector<string> sortPeople(vector<string>& names, vector<int>& heights) {
vector<tuple<int, string>> hn;
for(int i = 0; i < names.size(); i++)
{
hn.push_back({heights[i], names[i]});
}
sort(hn.begin(), hn.end(), [](tuple<int, string>& hn1, tuple<int, string>& hn2)
{return get<0>(hn1) > get<0>(hn2);}// 手动实现greater
);
vector<string> ans;
for(tuple<int, string> e:hn)
{
ans.push_back(get<1>(e));
}
return ans;
}
};
code2(非常实用):对下标排序
新建数组indexs从0到heights.size()-1,对应heights下标,再根据heights对indexs排序
class Solution {
public:
vector<string> sortPeople(vector<string>& names, vector<int>& heights) {
// 下标排序
vector<int> indexs;
for(int i = 0; i < names.size(); i++)
{
indexs.push_back(i);
}
sort(indexs.begin(), indexs.end(), [&heights](int i, int j){
return heights[i] > heights[j];
});
vector<string> ans;
for(int i:indexs)
{
ans.push_back(names[i]);
}
return ans;
}
};
11.优势洗牌
key:
用最小的 大于nums2[i]的nums1元素来匹配nums[i]
剩下的nums1元素用来匹配剩下的nums2元素,每次用最小的nums1匹配最大的nums2
可以使用交换论证法来证明贪心策略正确性
code
class Solution {
public:
vector<int> advantageCount(vector<int>& nums1, vector<int>& nums2) {
// 田忌赛马
// 用最小的 大于nums2[i]的nums1元素来匹配nums[i]
// 剩下的nums1元素用来匹配剩下的nums2元素,每次用最小的nums1匹配最大的nums2
int n = nums1.size();
vector<int> indexs2;// nums2的下标映射
for(int i = 0; i < nums2.size(); i++)
{
indexs2.push_back(i);
}
sort(nums1.begin(), nums1.end());
sort(indexs2.begin(), indexs2.end(), [&nums2](int i, int j){
return nums2[i] < nums2[j];
});// 用index2代替nums2排序
// 双指针
vector<int> tmp(n, 0);
for(int left = 0, right = n - 1, p1 = 0; left <= right; p1++)
{
if(nums1[p1] > nums2[indexs2[left]])
{
tmp[left++] = nums1[p1];
}
else
{
tmp[right--] = nums1[p1];
}
}
// 恢复排序
vector<int> ans(n, 0);
for(int i = 0; i < n; i++)
{
ans[indexs2[i]] = tmp[i];
}
return ans;
}
};
12.最长回文串
link:409. 最长回文串 - 力扣(LeetCode)
这是道简答题,不多说明
code
class Solution {
public:
int longestPalindrome(string s) {
int hash[256];
memset(hash, 0, sizeof hash);
for(char ch:s)
{
hash[ch]++;
}
int arr[2] = {0};
for(int e : hash)
{
arr[0] += e/2 * 2;
arr[1] += e%2;
}
int ans = arr[0] + (arr[1] ? 1 : 0);
return ans;
}
};
13.增减字符串匹配
link:942. 增减字符串匹配 - 力扣(LeetCode)
贪心策略:每次I选最小, 每次D选最大
证明贪心策略正确性:归纳法
当s[i] = ‘I'时, ans[i]选择当前最小值,则之后所有ans都比ans[i]大,这种情况一定满足题意
同理可证s[i] = ‘D',ans[i]选当前最大值, 则之后所有ans都比ans[i]小, 这种情况一定满足题意
code
class Solution {
public:
vector<int> diStringMatch(string s) {
// 贪心, 每次I选最小, 每次D选最大
int n = s.size();
int minn = 0, maxx = n;
vector<int> ans(n + 1, 0);
for(int i = 0; i < n; i++)
{
ans[i] = s[i] == 'I' ? minn++ : maxx--;
}
ans[n] = minn;//此时minn = maxx
return ans;
}
};
14.分发饼干
其实就是田忌赛马, 相当于询问田忌赛马最多胜利的场数
贪心策略:优先先满足最小胃口孩子的需求(用最小的满足其胃口的饼干)
贪心策略正确性证明见本文第11题“优势洗牌”(交换论证法)
code
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
// 贪心策略:优先先满足最小胃口孩子的需求(用最小的满足其胃口的饼干),类似田忌赛马
int ans = 0;
sort(g.begin(), g.end());
sort(s.begin(), s.end());
for(int ps = 0, pg = 0; ps < s.size() && pg < g.size(); ps++)
{
printf("s[ps] = %d, g[pg] = %d\n", s[ps], g[pg]);
if(s[ps] >= g[pg])
{
ans++;
pg++;
}
}
return ans;
}
};
15.最优除法
贪心策略:让nums[0]为分子,其他均为分母即可
key:nums[0]比为分子,nums[1]必为分母
由于nums[i] >2,易得贪心策略一定为最优解
code
class Solution {
public:
string optimalDivision(vector<int>& nums) {
// 贪心策略:让nums[0]为分子,其他均为分母即可
// key:nums[0]比为分子,nums[1]必为分母
if(nums.size() == 1) return to_string(nums[0]);
if(nums.size() == 2) return to_string(nums[0]) + '/' + to_string(nums[1]);
string ans;
ans += to_string(nums[0]) + "/(" + to_string(nums[1]);
for(int i = 2; i < nums.size(); i++)
{
ans += '/' + to_string(nums[i]);
}
ans += ')';
return ans;
}
};
16.跳跃游戏II
link:45. 跳跃游戏 II - 力扣(LeetCode)
贪心策略:选择能跳的最远的点作为下一个点(选择i + nums[i]最大的i点)
code
class Solution {
public:
int jump(vector<int>& nums) {
// 贪心策略:选择能跳的最远的点作为下一个点(选择i + nums[i]最大的i点)
int ans = 0;
int cur = 0;
int n = nums.size();
while(cur < nums.size()-1)
{// 题目保证可以到达终点则nums[cur] != 0
if(nums[cur] == 0)
{
printf("error, nums[cur] == 0\n");
return -1;
}
int nextStep = cur + 1, farest = cur + nums[cur];
if(farest >= n - 1) return ans + 1;
for(int i = cur + 1; i <= cur + nums[cur]; i++)
{
if(i + nums.at(i) > farest)
{
nextStep = i;
farest = i + nums.at(i);
}
}
cur = nextStep;
ans++;
}
return ans;
}
};
17.跳跃游戏
与跳跃游戏II类似,不多解释
code
class Solution {
public:
bool canJump(vector<int>& nums) {
// 贪心策略:每次选择能跳的最远的点为下一个点(选令i+nums[i]最大的i最为下一个点
// 下一个点的nums[i]为0则return false
int left = 0, right = 0;// 下一个点的选取区间
int nextStep = 0, farest = 0 + nums[0];
int cur = 0;
while(cur < nums.size() - 1)
{
if(nums[cur] == 0) return false;
left = cur + 1;
right = cur + nums[cur];
if(right >= nums.size()-1) return true;
nextStep = cur + 1;
for(int i = left; i <= right; i++)
{
if(i + nums[i] > farest)
{
farest = i + nums[i];
nextStep = i;
}
}
cur = nextStep;
}
return true;
}
};
18.加油站
本题贪心方案不是很明显,值得仔细分析
贪心策略:
每次只从diff[i] >= 0的位置开始,若遇到不能递达的站点j,则更新i为j([i, j]内所有站点都不满足条件)
diff[i] = gas[i] - cost[i]
code
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
// 暴力枚举 -> 贪心(改变i更新逻辑)
vector<int> diff;
for(int i = 0; i < gas.size(); i++)
{
diff.push_back(gas[i]-cost[i]);
}
for(int i = 0; i < diff.size(); i++)
{
if(diff[i] < 0) continue;
int sum = 0;
for(int cur = i; cur < i + diff.size(); cur++)
{
sum += diff[cur % diff.size()];
if(sum < 0)
{
i = cur;// key:改变i的更新逻辑
break;
}
}
if(sum >= 0) return i;
}
return -1;
}
};
19.单调递增的数字
link:738. 单调递增的数字 - 力扣(LeetCode)
贪心策略:若n合法,直接返回n; 若n不合法,返回最大位-1,后接多个9(此策略有暴力枚举优化而来)
贪心原理正确性:如果n本身不满足条件,为了保持递增性, 最后一个递增元素一定要减1, 在此情况下将其后所有元素置为9,既满足递增条件,又保证是最大值。即为最优解。
也可以使用反证法证明贪心策略正确性,自行证明比答案大的数不满足递增条件即可
code
class Solution {
public:
int monotoneIncreasingDigits(int n) {
// 贪心策略:若n合法,直接返回n; 若n不合法,返回最大位-1,后接多个9(此策略有暴力枚举优化而来)
// 如果n本身不满足条件,为了保持递增性, 最后一个递增元素一定要减1, 在此情况下将其后所有元素置为9,即为最优解
if(n <= 9) return n;
string sn = to_string(n);
bool valied = true;
int end = 0;// 最后一个递增元素的下标
for(int i = 0; i < sn.size() - 1; i++)
{
if(sn[i] > sn[i + 1])
{
valied = false;
end = i;
break;
}
}
if(valied) return n;
string ans = sn.substr(0, end) + func(sn.substr(end, string::npos));
return monotoneIncreasingDigits(stoi(ans));// 防止s[i-1] > s[i]
}
string func(string str)
{
string ret = to_string(str[0] - '0' - 1);
for(int i = 1; i < str.size(); i++)
{
ret += "9";
}
return ret;
}
};
20.坏了的计算器
link:991. 坏了的计算器 - 力扣(LeetCode)
key:正难则反,交换startValue与target ,/ - 改为 * +
贪心策略:能除就除
code
class Solution {
public:
int brokenCalc(int startValue, int target) {
// 正难则反,转化为target->startValue, 只能/2 或 +1
swap(startValue, target);
int ans = 0;
while(startValue != target)
{
if(startValue % 2 == 1)
{
startValue += 1;
}
else
{
if(startValue > target) startValue /= 2;
else startValue += 1;
}
ans++;
}
return ans;
}
};
21.合并区间
贪心策略:每次选择最小start最小的区间
code
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
// 贪心策略:每次选择最小start最小的区间
sort(intervals.begin(), intervals.end(), [](vector<int>& vc1, vector<int>& vc2){
return vc1[0] < vc2[0];
});
vector<vector<int>> ans;
int left = intervals[0][0], right = intervals[0][1];
for(int i = 1; i < intervals.size(); i++)
{
int start = intervals[i][0], end = intervals[i][1];
if(start <= right)
{
right = max(right, end);
}
else
{
ans.push_back({left, right});
left = start, right = end;
}
}
ans.push_back({left, right});
return ans;
}
};
22.无重叠区间
link:435. 无重叠区间 - 力扣(LeetCode)
正难则反:求使得区间互不重叠的区间的最大数量
贪心策略:优先选择终点最小的区间
tips:
一般来讲,区间问题中,既可以左端点排序,也可右端点排序。本题也是如此,只不过本题中使用右端点排序更加直观,容易理解
若本题使用左端点排序,则在每次重叠时选择end最小的区间即可
区间问题常用贪心算法
code(右端点排序)
class Solution {
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
// 正难则反:求使得区间互不重叠的区间的最大数量
// 贪心策略:优先选择终点最小的区间
sort(intervals.begin(), intervals.end(), [](vector<int>& vc1, vector<int>& vc2){
return vc1[1] < vc2[1];
});
int left = intervals[0][0], right = intervals[0][1];
int maxx = 1;
for(int i = 1; i < intervals.size(); i++)
{
if(intervals[i][0] >= right)
{
maxx++;
right = intervals[i][1];
}
else continue;
}
return intervals.size() - maxx;
}
};
code2-左端点排序
每次重叠时选择end最小的区间
本解法直接求需要删去的区间的个数
class Solution {
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
// 左端点排序
sort(intervals.begin(), intervals.end(), [](vector<int>& vc1, vector<int>& vc2){
return vc1[0] < vc2[0];
});
// 删除区间
int right = intervals[0][1];
int ans = 0;
for(int i = 1; i < intervals.size(); i++)
{
if(intervals[i][0] < right)// 重叠
{
ans++;
right = min(right, intervals[i][1]);
}
else right = intervals[i][1];
}
return ans;
}
};
23.用最少数量的箭引爆气球
link:452. 用最少数量的箭引爆气球 - 力扣(LeetCode)
与上题类似,与重叠区间有关
贪心策略:左端点排序,顺序遍历
与前组有重叠则更新交集,不重叠就射箭全部戳破,并更新交集
code
class Solution {
public:
int findMinArrowShots(vector<vector<int>>& points) {
// 贪心策略:左端点排序,顺序遍历
// 与前组有重叠则更新交集,不重叠就射箭全部戳破,并更新交集
sort(points.begin(), points.end(), [](vector<int>& vc1, vector<int>& vc2){
return vc1[0] < vc2[0];
});
int right = points[0][1];
int ans = 0;
for(int i = 1; i < points.size(); i++)
{
if(points[i][0] > right)// 不重叠
{
ans++;
right = points[i][1];
}
else right = min(right, points[i][1]);
}
return ans + 1;
}
};
24.整数替换
code1--dfs模拟
class Solution {
public:
unordered_map<int, int> hash;
int integerReplacement(int n) {
// 直接模拟(dfs 记忆化搜索)
return dfs(n);
}
int dfs(long long n)
{
if(hash.count(n)) return hash[n];
int ret;
if(n == 1)
{
hash[1] = 1;
ret = 0;
}
else if(n % 2 == 0)
{
ret = (1 + dfs(n / 2));
}
else
{
ret = 1 + min(dfs(n + 1), dfs(n - 1));
}
hash[n] = ret;
return ret;
}
};
code2--贪心
以二进制视角看待n
class Solution {
public:
int integerReplacement(int n) {
int ans = 0;
long long cur = n;
while(cur != 1)
{
if(cur % 2 == 0) cur /= 2;
else
{
if(cur == 3) cur-=1;
else if(cur % 4 == 1) cur -= 1;
else cur += 1;
}
ans++;
}
return ans;
}
};
25.俄罗斯套娃信封问题
link:354. 俄罗斯套娃信封问题 - 力扣(LeetCode)
左端点排序+贪心+二分
排序后求右端点的最长递增子序列即可
tips:
排序时,若左端点相同, 则按照右端点降序排序,这样可以保证最长递增子序列不出现同左端点的元素
本题也可以用动态规划代替贪心+二分部分,虽然这样会超市(时间复杂度O(N), 但动态规划是一种应用更加广泛,也更简单易懂的算法。但是为了降低时间复杂度,本题解使用贪心算法解决本问题
code
class Solution {
public:
int maxEnvelopes(vector<vector<int>>& evs) {
// 转化为最长递增子序列即可(左端点排序 + 重写排序)
sort(evs.begin(), evs.end(), [](vector<int>& vc1, vector<int>& vc2){
if(vc1[0] != vc2[0]) return vc1[0] < vc2[0];
else return vc1[1] > vc2[1];
}); // 重写排序,令左端点相同的元素们按照右端点降序,保证最长递增子序列中,同左端点的元素只出现一次
// 寻找右端点最长递增子序列(贪心方法)
vector<int> vc(1, -1);// vc[i]表示长度为i的最长递增子序列的最小结尾值
for(vector<int> ev :evs)
{
int val = ev[1];
if(val > vc.back())
{
vc.push_back(val);
continue;
}
// vc 是升序的, 二分查找第一个大于等于val的元素下标
int left = 1, right = vc.size() - 1;
for(; left < right; )
{
int mid = (left + right) >> 1;
if(vc[mid] >= val) right = mid;
else left = mid + 1;
}
vc[left] = val;
}
// chech
for(int e:vc)
{
printf("%d ", e);
}
return vc.size() - 1;
}
};
26.可被三整除的最大和
link:1262. 可被三整除的最大和 - 力扣(LeetCode)
tip:
因为本题mod3,3比较小,所以可以使用贪心+分情况讨论。
但是但是如果将3换为更大的数,贪心时的分类讨论会特别麻烦,此时我们就应该使用多状态动态规划
本题code可以稍微优化:可以先将两个mod3==1与两个mod3==2的最小的数求出,避免不同情况重复求共同值
code
class Solution {
public:
int maxSumDivThree(vector<int>& nums) {
// 正难则反:先求nums的sum,再分类讨论如何舍去较小元素使能sum被三整除
// 因为本题是mod3,3比较小,所以可以使用分类讨论+贪心,但是如果将3换为更大的数,贪心时的分类讨论会特别麻烦,此时我们就应该使用多状态动态规划
sort(nums.begin(), nums.end());
const int INF = 0x3f3f3f3f;
int ans;
int sum = 0;
for(int num:nums) sum += num;
if(sum % 3 == 0) return sum;
else if(sum % 3 == 1)
{
// 情况一:去除最小的mod3 == 1的元素
int a = INF;
for(int num:nums)
{
if(num % 3 == 1)
{
a = num;
break;
}
}
// 情况二:去除最小的两个mod3 == 2的元素
vector<int> b;
for(int num:nums)
{
if(num % 3 == 2)
{
b.push_back(num);
if(b.size() >= 2) break;
}
}
int sub = a; if(b.size() >= 2) sub = min(sub, b[0] + b[1]);
ans = sum - sub;
}
else // sum % 3 == 2
{
// 情况一:减去两个mod3 == 1的最小元素
vector<int> a;
for(int num:nums)
{
if(num % 3 == 1)
{
a.push_back(num);
if(a.size() >= 2) break;
}
}
// 情况二:减去最小的mod3 == 2的元素
int b = INF;
for(int num:nums)
{
if(num % 3 == 2)
{
b = num;
break;
}
}
int sub = b; if(a.size() >= 2) sub = min(sub, a[0] + a[1]);
ans = sum - sub;
}
return ans;
}
};
27.距离相等的条形码
link:1054. 距离相等的条形码 - 力扣(LeetCode)
贪心+模拟
code1
key:只要保证所有元素都不和其前一个元素重复即可
每次选择剩余的相同数量最大的且不与前一个元素重复的num
class Solution {
public:
static bool cmp(pair<int, int>& pr1, pair<int, int>& pr2)
{
return pr1.first < pr2.first;
}
vector<int> rearrangeBarcodes(vector<int>& bs) {
// 贪心+模拟
// key:只要保证所有元素都不和其前一个元素重复即可
unordered_map<int, int> num_cnt;
vector<int> ans;
for(int b:bs)
{
num_cnt[b]++;
}
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(&cmp)> pq(cmp);
for(auto [num, cnt]:num_cnt)
{
pq.push({cnt, num});
}
// 开始模拟
pair<int, int> prePair = pq.top(); pq.pop();
ans.push_back(prePair.second);
prePair.first--;
while(pq.size())
{
pair<int, int> pr = pq.top(); pq.pop();
ans.push_back(pr.second);
pr.first--;
if(prePair.first > 0)pq.push(prePair);
prePair = pr;
}
return ans;
}
};
code2
先摆放在偶数下标位置, 后摆放再奇数下标位置。
只要保证cnt最多的num先摆放即可,剩下的数的摆放顺序无所谓(但是相同的数要连续顺序摆放)
class Solution {
public:
vector<int> rearrangeBarcodes(vector<int>& bs) {
// 贪心+模拟 (code2分割法
unordered_map<int, int> num_cnt;
int maxCnt = 0;
int mostNum = 0;
for(int b:bs)
{
if(++num_cnt[b] > maxCnt)
{
maxCnt = num_cnt[b];
mostNum = b;
}
}
printf("maxCnt = %d, mostNum = %d\n", maxCnt, mostNum);
// 模拟
vector<int> ans(bs.size(), 0);
for(int i = 0; i < maxCnt; i++)
{
ans[i*2] = mostNum;
}
num_cnt.erase(mostNum);
int idx = maxCnt * 2;
for(auto& [num, cnt]:num_cnt)
{
for(int i = 0; i < cnt; i++)
{
if(idx >= ans.size()) idx = 1;
ans[idx] = num;
idx += 2;
}
}
return ans;
}
};
28.重构字符串
link:767. 重构字符串 - 力扣(LeetCode)
与上题“距离相等的条形码"相同,只不过参数从vector<int>改为了string
判断特殊情况, 之后直接复用上题代码即可
code
class Solution {
public:
string reorganizeString(string s) {
// 同1054.距离相等的条形码
int sz = s.size();
unordered_map<char, int> ch_cnt;
int maxCnt = 0;
char mostChar = 0;
for(char ch:s)
{
if(++ch_cnt[ch] > maxCnt)
{
maxCnt = ch_cnt[ch];
mostChar = ch;
}
}
if(maxCnt > (sz + 1) / 2) return "";
vector<int> vc(s.begin(), s.end());
vector<int> ret = rearrangeBarcodes(vc);
string ans;
for(char ch:ret)
{
ans += ch;
}
return ans;
}
static bool cmp(pair<int, int>& pr1, pair<int, int>& pr2)
{
return pr1.first < pr2.first;
}
vector<int> rearrangeBarcodes(vector<int>& bs) {
// 贪心+模拟
// key:只要保证所有元素都不和其前一个元素重复即可
unordered_map<int, int> num_cnt;
vector<int> ans;
for(int b:bs)
{
num_cnt[b]++;
}
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(&cmp)> pq(cmp);
for(auto [num, cnt]:num_cnt)
{
pq.push({cnt, num});
}
// 开始模拟
pair<int, int> prePair = pq.top(); pq.pop();
ans.push_back(prePair.second);
prePair.first--;
while(pq.size())
{
pair<int, int> pr = pq.top(); pq.pop();
ans.push_back(pr.second);
pr.first--;
if(prePair.first > 0)pq.push(prePair);
prePair = pr;
}
return ans;
}
};