【算法】贪心

目录

什么是贪心算法

贪心算法的特点

贪心策略积累


什么是贪心算法

贪心策略:解决问题的策略,局部最优 ->全局最优

  1. 把解决问题的过程分为若干步;

  2. 解决每一步的时候,都选择当前看起来"最优的"解法;

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;
    }

};

P1106 删数问题 - 洛谷

键盘输入一个高精度的正整数 n(不超过 250 位),去掉其中任意 k 个数字后剩下的数字按原左右次序将组成一个新的非负整数。编程对给定的 n 和 k,寻找一种方案使得剩下的数字组成的新数最小。

贪心策略:每次从前往后遍历,如果 si > si+1 ,删除 si 。如果 si 一直 <= si+1 ,删除最后一个数字。还要注意处理前导 0 :10001,删除 1和 1 后变成了 000.

P1031 NOIP 2002 提高组 均分纸牌 - 洛谷

贪心策略:从第一堆纸牌开始,与它的左边的纸牌堆进行交换,直接将当前纸牌堆的纸牌数量交换成最终数量(平均数),即使左边的纸牌堆的数量变成负数,也进行交换。

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;
    }
};