目录
什么是贪心算法
贪心策略:解决问题的策略,局部最优 ->全局最优
把解决问题的过程分为若干步;
解决每一步的时候,都选择当前看起来"最优的"解法;
3.可能得到全局最优解。
以下三个例子帮助理解贪心策略
例一:找零问题
在商店用 50 元买 4 元的物品,老板要找 46 元给我,要求纸币的张数最少
1、把解决问题的过程分为若干步:在面额为 20,10,5,1 的纸币选择一张。
2.、解决每一步的时候,都选择当前看起来"最优的"解法:每次选择当前可以选择的最大面额的纸币
例二:最小路径和
一个矩形方格内有许多小方格,每个小方格代表一个随机的数字,起始位置在左上角,你每次只能向右走一步或向下走一步,要求从该矩形方格的左上角走到右下角,所经过的小方格内的数字之和最小。比如:

1、把解决问题的过程分为若干步:每次向右走一步或向下走一步
2、解决每一步的时候,都选择当前看起来"最优的"解法:每次都走向较小的那个方格。
通过贪心策略的路径如上图绿色路径,明显错误。
例三:完全背包问题
有几种物品,每个物品都有体积和价值,你有一个背包,背包有最大体积,要求在不超过背包最大体积的前提下,背包能装物品的最大价值,比如:

这时就有几种贪心策略了:
1、只考虑体积:选择体积最小的物品,塞满背包(物品的数量越多,总价值可能越大)
2、只考虑价值:每次选择当前可以装得下的价值最大的物品。
3、只考虑单位体积的价值:算出每个物品的 w/v,每次选择当前可以装得下的单位体积价值最大的物品。
这三种贪心策略都是错误的,因为装两个 2 号物品才是正确的。
贪心算法的特点
通过上面三个例子,可以总结出贪心算法的特点:
1、不同问题的贪心策略不同,即贪心策略没有固定模板。同一道题可能有不同的贪心策略
2、贪心策略可能是一个错误的解决方法。正确的贪心策略是需要证明的。
贪心策略积累

解析:你只可能出现找 5 元或找 15 元两种情况, 找 5 元只有一种方法,而找 15 元有两种方法:1、找一张 10 元和 5 元 2、找 3 张 5 元。贪心:5 元的功能更多,不应该优先把 5 找出去,所以找 15 元这种情况应该优先使用第一种方法。
cpp
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int five = 0;
int ten = 0;
for(auto bill : bills)
{
if(bill == 5) five++;
else if(bill == 10) ten++;
int change = bill - 5;
if(change == 0) continue;
if(change == 5)
{
if(five >= 1) five--;
else return false;
}
else if(change == 15)
{
if(ten >= 1 && five >= 1) // 贪心
{
ten--;
five--;
}
else if(five >= 3) five -= 3;
else return false;
}
}
return true;
}
};

解析:每次都选择数组中最大的数减半,这样每次减的数都最大,操作次数应该最少。
cpp
class Solution {
public:
int halveArray(vector<int>& nums) {
double sum = 0;
for(auto i : nums) sum += i;
double target = sum / 2.0;
priority_queue<double> q;
for(auto i : nums)
{
q.push((double)i);
}
double reduced = 0;
int operations = 0;
while(reduced < target)
{
double half = q.top() / 2.0;
q.pop();
reduced += half;
operations++;
q.push(half);
}
return operations;
}
};

解析:先回顾这道题用动态规划是如何解决的:dpi 表示:以 i 位置为结尾的所有子序列中,最长递增子序列的长度,状态转移方程:dpi = max(dpj) + 1 (其中 j < i && numsj < numsi),我们发现,在只考虑长度时,并不关心序列具体长什么样子,我们只关心它的最后一个元素是谁。
贪心优化:以 7,3,8,4,7,2,14,13 为例:
当遍历到 7,获取一个信息:有一个长度为 1 的递增序列,末尾元素为 7。遍历到 3,获取信息:有一个长度为 1 的递增序列,末尾元素为 3,为什么不是有两个个长度为 1 的递增序列,末尾元素为分别为 3 和 7 呢,这就是贪心的地方,因为所有可以跟在 7 后面的数字,必定可以跟在 3 后面,并且 3 后面可以跟比 7 跟多的数字(4,6 之间的数字)。遍历到 8,获取信息:有一个长度为 2 的递增序列,末尾元素为 8(即 8 跟在了 3 后面)。遍历到 4,获取信息:有一个长度为 2 的递增序列,末尾元素为 4(即 4 跟在了 3 的后面)。遍历到 7 ,获取信息:有一个长度为 3 的递增序列,末尾元素为 7(即 7 跟在了 4 的后面)... 以此类推。

cpp
class Solution {
public:
/*
//int memo[2500];
//int n;
// 动态规划
int lengthOfLIS(vector<int>& nums) {
/*vector<int> memo(2500,-1);
//memset(memo,-1,sizeof(memo));
n = nums.size();
int ret = 0;
for(int i = 0; i < n; i++)
{
if(memo[i] == -1) ret = max(dfs(nums,i,memo),ret);
}
return ret;
int n = nums.size();
vector<int> dp(n,1);
for(int i = 1; i < n; i++)
{
int m = 0;
for(int j = i-1; j >= 0; j--)
{
if(nums[j] < nums[i]) m = max(m,dp[j]);
}
dp[i] += m;
}
int ret = 1;
for(auto i : dp) ret = max(i,ret);
return ret;
}
// 记忆化搜索
/*int dfs(vector<int>& nums,int pos,vector<int>& memo)
{
if(pos == n - 1) return 1;
if(memo[pos] != -1) return memo[pos];
memo[pos] = 1;
for(int i = pos + 1; i < n; i++)
{
if(nums[i] > nums[pos])
{
memo[pos] = max(memo[pos],dfs(nums,i,memo) + 1);
}
}
return memo[pos];
}*/
// 动态规划 + 贪心优化
int lengthOfLIS(vector<int>& nums)
{
vector<int> arr(1,INT_MIN);
int n = nums.size();
for(int i = 0; i < n; i++)
{
if(nums[i] > arr.back()) arr.push_back(nums[i]);
else
{
int left = 1;
int right = arr.size() - 1;
while(left < right)
{
int mid = left + (right - left) / 2;
if(arr[mid] < nums[i]) left = mid + 1;
else right = mid;
}
arr[left] = nums[i];
}
}
return arr.size() - 1;
}
};
键盘输入一个高精度的正整数 n(不超过 250 位),去掉其中任意 k 个数字后剩下的数字按原左右次序将组成一个新的非负整数。编程对给定的 n 和 k,寻找一种方案使得剩下的数字组成的新数最小。
贪心策略:每次从前往后遍历,如果 si > si+1 ,删除 si 。如果 si 一直 <= si+1 ,删除最后一个数字。还要注意处理前导 0 :10001,删除 1和 1 后变成了 000.

贪心策略:从第一堆纸牌开始,与它的左边的纸牌堆进行交换,直接将当前纸牌堆的纸牌数量交换成最终数量(平均数),即使左边的纸牌堆的数量变成负数,也进行交换。
cpp
#include <iostream>
using namespace std;
typedef long long LL;
const LL N = 110;
LL arr[N];
LL n;
int main()
{
cin >> n;
LL sum = 0;
for (LL i = 1; i <= n; i++)
{
cin >> arr[i];
sum += arr[i];
}
LL aim = sum / n;
LL count = 0;
for (LL i = 1; i <= n - 1; i++)
{
LL diff = aim - arr[i];
if (diff == 0) continue;
arr[i] = aim;
arr[i + 1] -= diff;
count++;
}
cout << count << endl;
return 0;
}
田忌赛马
问题:齐威王有 12.24.8.32 四匹马,田忌有 13,25,32,11 四匹马,齐威王的马的出场顺序不变,就是 12.24.8.32,田忌可以随意调整他的马的出场顺序,问田忌可以胜过齐威王的最优出场顺序。
贪心策略:在双方各自出马的情况下,田忌要用自己"最弱能胜"的马去对对方最强的马,实在赢不了,就用最弱的马去消耗对方最强的马。

cpp
class Solution {
public:
vector<int> advantageCount(vector<int>& nums1, vector<int>& nums2) {
// num1 : 田忌
// num2 : 齐威王
int n = nums1.size();
vector<int> ret(n,0);
sort(nums1.begin(),nums1.end());
int index2[n];
for(int i = 0; i < n; i++) index2[i] = i;
sort(index2,index2 + n,[&](int i,int j){
return nums2[i] < nums2[j];
});
int cur = 0;
int left = 0;
int right = n - 1;
while(cur < n)
{
if(nums1[cur] <= nums2[index2[left]])
ret[index2[right--]] = nums1[cur];
else
ret[index2[left++]] = nums1[cur];
cur++;
}
return ret;
}
};