力扣方法总结:贪心算法

本文通过四个具体的编程题目,展示了贪心算法在解决信息技术相关问题中的应用。第一题是重构排列,通过贪心策略确定数字的顺序;第二题是构建连续整数,通过排序和贪心扩展范围;第三题是补充数组,贪心地增加缺失数字;第四题是在加油站问题中,利用贪心确定最远可达站点。这些例子揭示了贪心算法在处理局部最优解问题时的有效性。
摘要由CSDN通过智能技术生成

942. 增减字符串匹配 Easy 贪心 2022/5/12

由范围 [0,n] 内所有整数组成的 n + 1 个整数的排列序列可以表示为长度为 n 的字符串 s ,其中:
如果 perm[i] < perm[i + 1] ,那么 s[i] == ‘I’
如果 perm[i] > perm[i + 1] ,那么 s[i] == ‘D’
给定一个字符串 s ,重构排列 perm 并返回它。如果有多个有效排列perm,则返回其中 任何一个 。
示例:
输入:s = “IDID”
输出:[0,4,1,3,2]

方法一:贪心算法:遍历原字符串,如果为I,就找一个最小的放进v,如果为D则找一个最大的放进v。

class Solution {
public:
    vector<int> diStringMatch(string s) {
        vector<int> v;
        // 最小值和最大值
        int min = 0;
        int max = s.size();
        for(int i = 0; i <= s.size(); i++)
        {
            // 找到一个最小的放进v
            if (s[i] == 'I') v.push_back(min++);
            // 找到一个最大的放进v
            else v.push_back(max--);
        }
        return v;
    }
};

方法二:先把0放进去,如果为I则放个1进去,为D就放个-1进去,同时最大最小值依次改变,也是一种贪心的变种算法,最后求得最小值(负数),所有数减去该数,变为非负数列。

class Solution {
public:
    vector<int> diStringMatch(string s) {
        vector<int> v;
        v.push_back(0);
        // 最小值和最大值
        int min = 0;
        int max = 0;
        for (int i = 0; i < s.size(); i++)
        {
            if (s[i] == 'I') v.push_back(++max);
            else v.push_back(--min);
        }
        int minus = 0;
        for (auto i : v)
            if (minus > i)
                minus = i;
        if (minus != 0)
            for(int i = 0; i < v.size(); i++)
                v[i] -= minus;
        return v;
    }
};

1798. 你能构造出连续值的最大数目 Medium 贪心 2023/2/4

给你一个长度为 n 的整数数组 coins ,它代表你拥有的 n 个硬币。第 i 个硬币的值为 coins[i] 。如果你从这些硬币中选出一部分硬币,它们的和为 x ,那么称,你可以 构造 出 x 。
请返回从 0 开始(包括 0 ),你最多能 构造 出多少个连续整数。
你可能有多个相同值的硬币。
示例:
输入:coins = [1,3]
输出:2
解释:你可以得到以下这些值:
0:什么都不取 []
1:取 [1]
从 0 开始,你可以构造出 2 个连续整数。

由于受到剑指 Offer II 103. 最少的硬币数目的启发,首先想到的是能否分解为子问题,遍历整数,并编写能否构成该数字的检测函数(只能用递归,因为硬币数量有限,需要增加和删除硬币)。

class Solution {
public:
    int getMaximumConsecutive(vector<int>& coins) {
        int i = 0;
        while (1) {
        	// 连续遍历每个硬币数
            if (traverse(coins, i) == false)
                break;
            i++;
        }
        return i;
    }
    bool traverse(vector<int> coins, int x) {
        if (x == 0) return true;
        if (x < 0) return false;
        for (auto iter = coins.begin(); iter < coins.end(); iter++) {
            int coin = *iter;
            // 删除该硬币
            coins.erase(iter);
            if (traverse(coins, x - coin) == true) return true; // 看子问题能否求解
            // 还原该硬币
            coins.insert(iter, coin);
        }
        return false;
    }
};

但由于硬币是用一个少一个,无法和上题一样对于每种硬币数建立一个统一的dp数组,每次对于一个硬币数都需要经过时间复杂度较高的递归过程,超时。
如果本题是检查某个数是否能被这些硬币所组合,可以考虑用上述方法。

考虑到题目中连续整数,可以考虑先将数组排序,每次都插入最小的那个元素,如果该元素>原来可表示的最大元素+1,则说明无法继续表示,否则加上该元素以扩展可表示的元素。

贪心算法的核心就在于在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。

class Solution {
public:
    int getMaximumConsecutive(vector<int>& coins) {
        sort(coins.begin(), coins.end());
        int span = 0;
        for (int coin: coins) {
            // 前面这些小数能表示这个大数
            if (coin <= span + 1)
                span += coin; // 加入范围
            else break;
        }
        return span + 1; // 连续整数的个数
    }
};

330. 按要求补齐数组 Hard 贪心 2023/2/4

给定一个已排序的正整数数组 nums ,和一个正整数 n 。从 [1, n] 区间内选取任意个数字补充到 nums 中,使得 [1, n] 区间内的任何数字都可以用 nums 中某几个数字的和来表示。
请返回 满足上述要求的最少需要补充的数字个数 。
示例:输入: nums = [1,3], n = 6
输出: 1
解释:
根据 nums 里现有的组合 [1], [3], [1,3],可以得出 1, 3, 4。
现在如果我们将 2 添加到 nums 中, 组合变为: [1], [2], [3], [1,3], [2,3], [1,2,3]。
其和可以表示数字 1, 2, 3, 4, 5, 6,能够覆盖 [1, 6] 区间里所有的数。
所以我们最少需要添加一个数字。

本题为1798题的进阶版本,要求输出需要补充的数字数,很容易想到对于数组中的每个数,判断是否有漏的(span+1缺失无法表示),如果有漏,则扩张到2*span+1并计数+1,直到能把数组中的数添加进来,如果数组元素都用完了还不能表示n,则再通过上述方式扩张。

class Solution {
public:
    int minPatches(vector<int>& nums, int n) {
        long span = 0;
        int cnt = 0;
        for (int num: nums) {
            if (span >= n) break;
            // 有漏的,需要加数(span + 1)
            while (span + 1 < num) {
                cnt++;
                span += (span + 1);
                if (span >= n) break;
            }
            // 添加数组元素扩大范围
            span += num;
        }
        // 数组元素都用完了
        while (span < n) {
            span += (span + 1);
            cnt++;
        }
        return cnt;
    }
};

这样的思路可行,但编写代码时,需要时刻注意span>=n时退出循环,有可能数组中的数还没添加完就已经够了,想到可以改进遍历数组的过程,改为一个while循环,在里面进行数组遍历。

只有在数组索引合法且index当前数在连续范围内时,用数组中的数进行范围扩容,否则直接进行2*span+1的扩容,非常精简巧妙。

class Solution {
public:
    int minPatches(vector<int>& nums, int n) {
        int cnt = 0;  // 初始化要补的次数为0次
        long span = 0;  // 初始化span,此时区间为[0,0]
        int index = 0;   // 从第0个位置开始     
        while (span < n) {  // 退出条件
            // 数组能遍历且index当前数在连续范围内
            if (index < nums.size() && nums[index] <= span + 1) {
                span += nums[index];
                index++;
            } 
            // 范围扩容(加上span)
            else {
                span += span + 1;
                cnt++;
            }
        } 
        return cnt;
    }
};

134. 加油站 Medium 贪心 2023/2/4

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas 和 cost ,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。
示例:
输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。

首先,本题可以进行以下简化:

  • 如果gas总和小于cost总和,显然不存在解。
  • 将gas每项与cost作差,并进行延拓,此时起始加油站只需遍历0~n即可。
  • 此时gas的含义是经过该加油站得到的油数(可能为负数)。

如果对于每个起始加油站,向右进行遍历n个数,时间复杂度为O(n²),超时。
想到可以利用滑动窗口,左窗口不变,右窗口向右滑动直到总和<0,再将左窗口右移直到油数>=0。

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int n = gas.size();
        // 逐项作差
        for (int i = 0; i < n; i++)
            gas[i] -= cost[i];
        if (accumulate(gas.begin(), gas.end(), 0) < 0)
            return -1;
        gas.insert(gas.end(), gas.begin(), gas.end());
        int left = 0;
        int right = 0;
        int ngas = 0;
        while (left < n && right < 2*n) {
            ngas += gas[right];
            if (ngas < 0) {
                while (ngas < 0) {
                    ngas -= gas[left];
                    left++;
                }
            }
            if (right == left + n) return left;
            right++;
        }
        return -1;
    }
};

那么本题为什么会放在贪心算法中呢?这不是一道简单的滑动窗口的题吗?

答案在于,本题左窗口右移时,可以直接移动到右窗口向右的一个位置!因为前面这一段找不到比左窗口更好的起始加油站了。

假设从x加油站出发经过z加油站最远能到达y加油站,那么从z加油站直接出发,不可能到达y下一个加油站。因为从x出发到z加油站时肯定还有存储的油,这都到不了y的下一站,而直接从z出发刚开始是没有存储的油的,所以更不可能到达y的下一站。

因此可以优化左窗口右移的代码,一次遍历直接出结果。

        while (left < n && right < 2*n) {
            ngas += gas[right];
            if (ngas < 0) {
                left = right + 1; // 左窗口直接跳到右窗口处!
                ngas = 0;
            }
            if (right == left + n) return left;
            right++;
        }

605.种花问题 Easy 贪心 2023/2/16

假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。
给你一个整数数组 flowerbed 表示花坛,由若干 0 和 1 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false。
示例:
输入:flowerbed = [1,0,0,0,1], n = 1
输出:true

本题很显然为贪心,但遍历时很容易出错,故使用预处理避免边界讨论:插入前导零和末尾零,非常巧妙。

class Solution {
public:
    bool canPlaceFlowers(vector<int>& flowerbed, int n) {
        if (n == 0) return true;
        // 插入前导零和末尾零,避免边界讨论
        flowerbed.insert(flowerbed.begin(), 0);
        flowerbed.push_back(0);
        for (int i = 0; i < flowerbed.size(); i++) {
        	// 直到找到一个为0的花坛 
            if (flowerbed[i] == 1) continue;
            // 后面两个也必须为0
            if (i + 1 >= flowerbed.size() || flowerbed[i + 1] == 1) continue;
            if (i + 2 >= flowerbed.size() || flowerbed[i + 2] == 1) continue;
            n--;
            i++;
            if (n == 0) return true;
        }
        return false;
    }
};

135.分发糖果 Hard 贪心 2023/2/22

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
每个孩子至少分配到 1 个糖果。
相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
给定一个字符串 s ,重构排列 perm 并返回它。如果有多个有效排列perm,则返回其中 任何一个 。
示例:
输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。
第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。

两次遍历贪心算法:我们可以将「相邻的孩子中,评分高的孩子必须获得更多的糖果」这句话拆分为两个规则,分别处理。

  • 左规则:当 r a t i n g s [ i − 1 ] < r a t i n g s [ i ] ratings[i−1]<ratings[i] ratings[i1]<ratings[i] 时, i i i 号学生的糖果数量将比 i − 1 i-1 i1 号孩子的糖果数量多。
  • 右规则:当 r a t i n g s [ i ] > r a t i n g s [ i + 1 ] ratings[i]>ratings[i+1] ratings[i]>ratings[i+1] 时, i i i 号学生的糖果数量将比 i + 1 i+1 i+1 号孩子的糖果数量多。

我们遍历该数组两次,处理出每一个学生分别满足左规则或右规则时,最少需要被分得的糖果数量。每个人最终分得的糖果数量即为这两个数量的最大值。

class Solution {
public:
    int candy(vector<int>& ratings) {
        int n = ratings.size();
        // 左规则
        vector<int> left(n);
        for (int i = 0; i < n; i++) {
            if (i > 0 && ratings[i] > ratings[i - 1]) {
                left[i] = left[i - 1] + 1;
            } else {
                left[i] = 1;
            }
        }
        // 右规则
        vector<int> right(n);
        for (int i = n - 1; i >= 0; i--) {
            if (i < n - 1 && ratings[i] > ratings[i + 1]) {
                right[i] = right[i + 1] + 1;
            } else {
                right[i] = 1;
            }
        }
        int ret = 0;
        for (int i = 0; i < n; i++) {
            ret += max(left[i], right[i]);
        }
        return ret;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值