贪心算法专题

45. 跳跃游戏 II

题目描述

给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。

每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

0 <= j <= nums[i] 
i + j < n

返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。

  • 示例 1:

    输入: nums = [2,3,1,1,4]
    输出: 2
    解释: 跳到最后一个位置的最小跳跃数是 2。从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。

    示例 2:

    输入: nums = [2,3,0,1,4]
    输出: 2

提示:

1 <= nums.length <= 104
0 <= nums[i] <= 1000
题目保证可以到达 nums[n-1]

解析

我们的目标是到达数组的最后一个位置,因此我们可以考虑最后一步跳跃前所在的位置,该位置通过跳跃能够到达最后一个位置。

如果有多个位置通过跳跃都能够到达最后一个位置,那么我们应该如何进行选择呢?直观上来看,我们可以「贪心」地选择距离最后一个位置最远的那个位置,也就是对应下标最小的那个位置。因此,我们可以从左到右遍历数组,选择第一个满足要求的位置。

找到最后一步跳跃前所在的位置之后,我们继续贪心地寻找倒数第二步跳跃前所在的位置,以此类推,直到找到数组的开始位置。


代码

class Solution {
    public int jump(int[] nums) {
        int position = nums.length - 1;
        int steps = 0;
        while (position > 0) {
            for (int i = 0; i < position; i++) {
                if (i + nums[i] >= position) {
                    position = i;
                    steps++;
                    break;
                }
            }
        }
        return steps;
    }
}

55. 跳跃游戏

题目描述

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。

  • 示例 1:

    输入:nums = [2,3,1,1,4]
    输出:true
    解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。

    示例 2:

    输入:nums = [3,2,1,0,4]
    输出:false
    解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。

提示:

1 <= nums.length <= 104
0 <= nums[i] <= 105

解析

设想一下,对于数组中的任意一个位置 y,我们如何判断它是否可以到达?根据题目的描述,只要存在一个位置 x,它本身可以到达,并且它跳跃的最大长度为 x+nums[x],这个值大于等于 y,即 x+nums[x]≥y,那么位置 y 也可以到达。

换句话说,对于每一个可以到达的位置 x,它使得 x+1,x+2,⋯ ,x+nums[x] 这些连续的位置都可以到达。

这样以来,我们依次遍历数组中的每一个位置,并实时维护 最远可以到达的位置。对于当前遍历到的位置 x,如果它在 最远可以到达的位置 的范围内,那么我们就可以从起点通过若干次跳跃到达该位置,因此我们可以用 x+nums[x] 更新 最远可以到达的位置。

在遍历的过程中,如果 最远可以到达的位置 大于等于数组中的最后一个位置,那就说明最后一个位置可达,我们就可以直接返回 True 作为答案。反之,如果在遍历结束后,最后一个位置仍然不可达,我们就返回 False 作为答案。


代码

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int n = nums.size();
        int rightmost = 0;
        for (int i = 0; i < n; ++i) {
            if (i <= rightmost) {
                rightmost = max(rightmost, i + nums[i]);
                if (rightmost >= n - 1) {
                    return true;
                }
            }
        }
        return false;
    }
};

134. 加油站

题目描述

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

  • 示例 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 可为起始索引。

    示例 2:

    输入: gas = [2,3,4], cost = [3,4,3]
    输出: -1
    解释: 你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站.我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油 开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油 开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油 你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。 因此,无论怎样,你都不可能绕环路行驶一周。

提示:

gas.length == n
cost.length == n
1 <= n <= 105
0 <= gas[i], cost[i] <= 104

解析

最容易想到的解法是:从头到尾遍历每个加油站,并检查以该加油站为起点,最终能否行驶一周。我们可以通过减小被检查的加油站数目,来降低总的时间复杂度。

假设我们此前发现,从加油站 x 出发,每经过一个加油站就加一次油(包括起始加油站),最后一个可以到达的加油站是 y(不妨设 x<y)。这就说明:

∑i=xygas[i]<∑i=xycost[i]

∑i=xjgas[i]≥∑i=xjcost[i] (For all j∈[x,y))

第一个式子表明无法到达加油站 y 的下一个加油站,第二个式子表明可以到达 y 以及 y 之前的所有加油站。

现在,考虑任意一个位于 x,y 之间的加油站 z(包括 x 和 y),我们现在考察从该加油站出发,能否到达加油站 y 的下一个加油站,也就是要判断 ∑i=zygas[i] 与 ∑i=zycost[i] 之间的大小关系。

根据上面的式子,我们得到:

∑i=zygas[i]=∑i=xygas[i]−∑i=xz−1gas[i]

<∑i=xycost[i]−∑i=xz−1gas[i]

<∑i=xycost[i]−∑i=xz−1cost[i]

=∑i=zycost[i]

其中不等式的第二步、第三步分别利用了上面的第一个、第二个不等式。

从上面的推导中,能够得出结论:从 x,y 之间的任何一个加油站出发,都无法到达加油站 y 的下一个加油站。

在发现了这一个性质后,算法就很清楚了:我们首先检查第 0 个加油站,并试图判断能否环绕一周;如果不能,就从第一个无法到达的加油站开始继续检查。


代码

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int n = gas.size();
        int i = 0;
        while (i < n) {
            int sumOfGas = 0, sumOfCost = 0;
            int cnt = 0;
            while (cnt < n) {
                int j = (i + cnt) % n;
                sumOfGas += gas[j];
                sumOfCost += cost[j];
                if (sumOfCost > sumOfGas) {
                    break;
                }
                cnt++;
            }
            if (cnt == n) {
                return i;
            } else {
                i = i + cnt + 1;
            }
        }
        return -1;
    }
};

179. 最大数

题目描述

给定一组非负整数 nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。

注意:输出结果可能非常大,所以你需要返回一个字符串而不是整数。

  • 示例 1:

    输入:nums = [10,2]
    输出:“210”

    示例 2:

    输入:nums = [3,30,34,5,9]
    输出:“9534330”

提示:

1 <= nums.length <= 100
0 <= nums[i] <= 109

解析

要想组成最大的整数,一种直观的想法是把数值大的数放在高位。于是我们可以比较输入数组的每个元素的最高位,最高位相同的时候比较次高位,以此类推,完成排序,然后把它们拼接起来。这种排序方式对于输入数组 没有相同数字开头 的时候是有效的,例如 [45,56,81,76,123]。

下面考虑输入数组 有相同数字开头 的情况,例如 [4,42] 和 [4,45]。

对于 [4,42],比较 442>424,需要把 4 放在前面;
对于 [4,45],比较 445<454,需要把 45 放在前面。

因此我们需要比较两个数不同的拼接顺序的结果,进而决定它们在结果中的排列顺序。

由于需要拼接以后才能决定两个数在结果中的先后顺序,N 个数就有 N! 种拼接的可能,我们是不是需要先得到 N 个数的全排列以后,再选出最大的呢?答案是没有必要。上述排序规则满足传递性,两个元素比较就可以确定它们在排序以后的相对位置关系。下面证明这种排序规则的必要性和充分性。


代码

class Solution {
public:
    string largestNumber(vector<int> &nums) {
        sort(nums.begin(), nums.end(), [](const int &x, const int &y) {
            long sx = 10, sy = 10;
            while (sx <= x) {
                sx *= 10;
            }
            while (sy <= y) {
                sy *= 10;
            }
            return sy * x + y > sx * y + x;
        });
        if (nums[0] == 0) {
            return "0";
        }
        string ret;
        for (int &x : nums) {
            ret += to_string(x);
        }
        return ret;
    }
};

316. 去除重复字母

题目描述

给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。

  • 示例 1:

    输入:s = “bcabc”
    输出:“abc”

    示例 2:

    输入:s = “cbacdcbc”
    输出:“acdb”

提示:

1 <= s.length <= 104
s 由小写英文字母组成

解析

首先考虑一个简单的问题:给定一个字符串 sss,如何去掉其中的一个字符 ch,使得得到的字符串字典序最小呢?答案是:找出最小的满足 s[i]>s[i+1] 的下标 i,并去除字符 s[i]。为了叙述方便,下文中称这样的字符为「关键字符」。

在理解这一点之后,就可以着手本题了。一个直观的思路是:我们在字符串 sss 中找到「关键字符」,去除它,然后不断进行这样的循环。但是这种朴素的解法会创建大量的中间字符串,我们有必要寻找一种更优的方法。

我们从前向后扫描原字符串。每扫描到一个位置,我们就尽可能地处理所有的「关键字符」。假定在扫描位置 s[i−1] 之前的所有「关键字符」都已经被去除完毕,在扫描字符 s[i] 时,新出现的「关键字符」只可能出现在 s[i] 或者其后面的位置。

于是,我们使用单调栈来维护去除「关键字符」后得到的字符串,单调栈满足栈底到栈顶的字符递增。如果栈顶字符大于当前字符 s[i],说明栈顶字符为「关键字符」,故应当被去除。去除后,新的栈顶字符就与 s[i] 相邻了,我们继续比较新的栈顶字符与 s[i] 的大小。重复上述操作,直到栈为空或者栈顶字符不大于 s[i]。

我们还遗漏了一个要求:原字符串 s 中的每个字符都需要出现在新字符串中,且只能出现一次。为了让新字符串满足该要求,之前讨论的算法需要进行以下两点的更改。

在考虑字符 s[i] 时,如果它已经存在于栈中,则不能加入字符 s[i]。为此,需要记录每个字符是否出现在栈中。

在弹出栈顶字符时,如果字符串在后面的位置上再也没有这一字符,则不能弹出栈顶字符。为此,需要记录每个字符的剩余数量,当这个值为 0 时,就不能弹出栈顶字符了。

代码

class Solution {
public:
    string removeDuplicateLetters(string s) {
        vector<int> vis(26), num(26);
        for (char ch : s) {
            num[ch - 'a']++;
        }

        string stk;
        for (char ch : s) {
            if (!vis[ch - 'a']) {
                while (!stk.empty() && stk.back() > ch) {
                    if (num[stk.back() - 'a'] > 0) {
                        vis[stk.back() - 'a'] = 0;
                        stk.pop_back();
                    } else {
                        break;
                    }
                }
                vis[ch - 'a'] = 1;
                stk.push_back(ch);
            }
            num[ch - 'a'] -= 1;
        }
        return stk;
    }
};

376. 摆动序列

题目描述

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。

  • 示例 1:

    输入:nums = [1,7,4,9,2,5]
    输出:6
    解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。

    示例 2:

    输入:nums = [1,17,5,10,13,15,10,5,16,8]
    输出:7
    解释:这个序列包含几个长度为 7 摆动序列。其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。

    示例 3:

    输入:nums = [1,2,3,4,5,6,7,8,9]
    输出:2

提示:

1 <= nums.length <= 1000
0 <= nums[i] <= 1000

解析

观察这个序列可以发现,我们不断地交错选择「峰」与「谷」,可以使得该序列尽可能长。证明非常简单:如果我们选择了一个「过渡元素」,那么在原序列中,这个「过渡元素」的两侧有一个「峰」和一个「谷」。不失一般性,我们假设在原序列中的出现顺序为「峰」「过渡元素」「谷」。如果「过渡元素」在选择的序列中小于其两侧的元素,那么「谷」一定没有在选择的序列中出现,我们可以将「过渡元素」替换成「谷」;同理,如果「过渡元素」在选择的序列中大于其两侧的元素,那么「峰」一定没有在选择的序列中出现,我们可以将「过渡元素」替换成「峰」。这样一来,我们总可以将任意满足要求的序列中的所有「过渡元素」替换成「峰」或「谷」。并且由于我们不断地交错选择「峰」与「谷」的方法就可以满足要求,因此这种选择方法就一定可以达到可选元素数量的最大值。

这样,我们只需要统计该序列中「峰」与「谷」的数量即可(注意序列两端的数也是「峰」或「谷」),但需要注意处理相邻的相同元素。

在实际代码中,我们记录当前序列的上升下降趋势。每次加入一个新元素时,用新的上升下降趋势与之前对比,如果出现了「峰」或「谷」,答案加一,并更新当前序列的上升下降趋势。


代码

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int n = nums.size();
        if (n < 2) {
            return n;
        }
        int prevdiff = nums[1] - nums[0];
        int ret = prevdiff != 0 ? 2 : 1;
        for (int i = 2; i < n; i++) {
            int diff = nums[i] - nums[i - 1];
            if ((diff > 0 && prevdiff <= 0) || (diff < 0 && prevdiff >= 0)) {
                ret++;
                prevdiff = diff;
            }
        }
        return ret;
    }
};

397. 整数替换

题目描述

给定一个正整数 n ,你可以做如下操作:

如果 n 是偶数,则用 n / 2替换 n 。
如果 n 是奇数,则可以用 n + 1或n - 1替换 n 。

返回 n 变为 1 所需的 最小替换次数 。

  • 示例 1:

    输入:n = 8
    输出:3
    解释:8 -> 4 -> 2 -> 1

    示例 2:

    输入:n = 7
    输出:4
    解释:7 -> 8 -> 4 -> 2 -> 1 或 7 -> 6 -> 3 -> 2 -> 1

    示例 3:

    输入:n = 4
    输出:2

提示:

1 <= n <= 231 - 1

解析

我们可以使用递归的方法枚举所有将 n 变为 1 的替换序列:

当 n 为偶数时,我们只有唯一的方法将 n 替换为 n/2​。

当 n 为奇数时,我们可以选择将 n 增加 1 或减少 1。由于这两种方法都会将 n 变为偶数,那么下一步一定是除以 2,因此这里我们可以看成使用两次操作,将 n 变为 (n+1)/2​ 或 (n−1​)/2。

代码

class Solution {
public:
    int integerReplacement(int n) {
        if (n == 1) {
            return 0;
        }
        if (n % 2 == 0) {
            return 1 + integerReplacement(n / 2);
        }
        return 2 + min(integerReplacement(n / 2), integerReplacement(n / 2 + 1));
    }
};

435. 无重叠区间

题目描述

给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。

  • 示例 1:

    输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
    输出: 1
    解释: 移除 [1,3] 后,剩下的区间没有重叠。

    示例 2:

    输入: intervals = [ [1,2], [1,2], [1,2] ]
    输出: 2
    解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。

    示例 3:

    输入: intervals = [ [1,2], [2,3] ]
    输出: 0
    解释: 你不需要移除任何区间,因为它们已经是无重叠的了。

提示:

1 <= intervals.length <= 105
intervals[i].length == 2
-5 * 104 <= starti < endi <= 5 * 104

解析

假设在某一种最优的选择方法中,[lk​,rk​] 是首个(即最左侧的)区间,那么它的左侧没有其它区间,右侧有若干个不重叠的区间。设想一下,如果此时存在一个区间 [lj​,rj​],使得 rj​<rk​,即区间 j 的右端点在区间 k 的左侧,那么我们将区间 k 替换为区间 jjj,其与剩余右侧被选择的区间仍然是不重叠的。而当我们将区间 k 替换为区间 j 后,就得到了另一种最优的选择方法。

我们可以不断地寻找右端点在首个区间右端点左侧的新区间,将首个区间替换成该区间。那么当我们无法替换时,首个区间就是所有可以选择的区间中右端点最小的那个区间。因此我们将所有区间按照右端点从小到大进行排序,那么排完序之后的首个区间,就是我们选择的首个区间。

如果有多个区间的右端点都同样最小怎么办?由于我们选择的是首个区间,因此在左侧不会有其它的区间,那么左端点在何处是不重要的,我们只要任意选择一个右端点最小的区间即可。

当确定了首个区间之后,所有与首个区间不重合的区间就组成了一个规模更小的子问题。由于我们已经在初始时将所有区间按照右端点排好序了,因此对于这个子问题,我们无需再次进行排序,只要找出其中与首个区间不重合并且右端点最小的区间即可。用相同的方法,我们可以依次确定后续的所有区间。

在实际的代码编写中,我们对按照右端点排好序的区间进行遍历,并且实时维护上一个选择区间的右端点 right。如果当前遍历到的区间 [li​,ri​] 与上一个区间不重合,即 li​≥right,那么我们就可以贪心地选择这个区间,并将 right 更新为 ri​。


代码

class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if (intervals.empty()) {
            return 0;
        }
        
        sort(intervals.begin(), intervals.end(), [](const auto& u, const auto& v) {
            return u[1] < v[1];
        });

        int n = intervals.size();
        int right = intervals[0][1];
        int ans = 1;
        for (int i = 1; i < n; ++i) {
            if (intervals[i][0] >= right) {
                ++ans;
                right = intervals[i][1];
            }
        }
        return n - ans;
    }
};

455. 分发饼干

题目描述

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

  • 示例 1:

    输入: g = [1,2,3], s = [1,1]
    输出: 1
    解释: 你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
    虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。 所以你应该输出1。

    示例 2:

    输入: g = [1,2], s = [1,2,3]
    输出: 2
    解释: 你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
    你拥有的饼干数量和尺寸都足以让所有孩子满足。 所以你应该输出2.

提示:

1 <= g.length <= 3 * 104
0 <= s.length <= 3 * 104
1 <= g[i], s[j] <= 231 - 1

解析

为了尽可能满足最多数量的孩子,从贪心的角度考虑,应该按照孩子的胃口从小到大的顺序依次满足每个孩子,且对于每个孩子,应该选择可以满足这个孩子的胃口且尺寸最小的饼干。证明如下。

假设有 m 个孩子,胃口值分别是 g1​ 到 gm​,有 n 块饼干,尺寸分别是 s1​ 到 sn​,满足 gi≤gi+1​ 和 sj​≤sj+1​,其中 1≤i<m,1≤j<n。

假设在对前 i−1 个孩子分配饼干之后,可以满足第 i 个孩子的胃口的最小的饼干是第 j 块饼干,即 sj​ 是剩下的饼干中满足 gi​≤sj​ 的最小值,最优解是将第 j 块饼干分配给第 i 个孩子。如果不这样分配,考虑如下两种情形:

如果 i<m 且 gi+1​≤sj​ 也成立,则如果将第 j 块饼干分配给第 i+1 个孩子,且还有剩余的饼干,则可以将第 j+1 块饼干分配给第 i 个孩子,分配的结果不会让更多的孩子被满足;

如果 j<n,则如果将第 j+1 块饼干分配给第 i 个孩子,当 gi+1​≤sj​ 时,可以将第 j 块饼干分配给第 i+1 个孩子,分配的结果不会让更多的孩子被满足;当 gi+1​>sj​ 时,第 j 块饼干无法分配给任何孩子,因此剩下的可用的饼干少了一块,因此分配的结果不会让更多的孩子被满足,甚至可能因为少了一块可用的饼干而导致更少的孩子被满足。

基于上述分析,可以使用贪心的方法尽可能满足最多数量的孩子。

首先对数组 g 和 s 排序,然后从小到大遍历 g 中的每个元素,对于每个元素找到能满足该元素的 s 中的最小的元素。具体而言,令 i 是 g 的下标,j 是 s 的下标,初始时 i 和 j 都为 0,进行如下操作。

对于每个元素 g[i],找到未被使用的最小的 j 使得 g[i]≤s[j],则 s[j] 可以满足 g[i]。由于 g 和 s 已经排好序,因此整个过程只需要对数组 g 和 s 各遍历一次。当两个数组之一遍历结束时,说明所有的孩子都被分配到了饼干,或者所有的饼干都已经被分配或被尝试分配(可能有些饼干无法分配给任何孩子),此时被分配到饼干的孩子数量即为可以满足的最多数量。


代码

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        int m = g.size(), n = s.size();
        int count = 0;
        for (int i = 0, j = 0; i < m && j < n; i++, j++) {
            while (j < n && g[i] > s[j]) {
                j++;
            }
            if (j < n) {
                count++;
            }
        }
        return count;
    }
};

581. 最短无序连续子数组

题目描述

给你一个整数数组 nums ,你需要找出一个 连续子数组 ,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。

请你找出符合题意的 最短 子数组,并输出它的长度。

  • 示例 1:

    输入:nums = [2,6,4,8,10,9,15]
    输出:5
    解释:你只需要对 [6, 4, 8, 10, 9] 进行升序排序,那么整个表都会变为升序排序。

    示例 2:

    输入:nums = [1,2,3,4]
    输出:0

    示例 3:

    输入:nums = [1]
    输出:0

提示:

1 <= nums.length <= 104
-105 <= nums[i] <= 105

解析

我们将给定的数组 nums 表示为三段子数组拼接的形式,分别记作 numsA​,numsB​,numsC​。当我们对 numsB​ 进行排序,整个数组将变为有序。换而言之,当我们对整个序列进行排序,numsA​ 和 numsC​ 都不会改变。

本题要求我们找到最短的 numsB,即找到最大的 numsA​ 和 numsC​ 的长度之和。因此我们将原数组 nums 排序与原数组进行比较,取最长的相同的前缀为 numsA​,取最长的相同的后缀为 numsC​,这样我们就可以取到最短的 numsB​。

具体地,我们创建数组 nums 的拷贝,记作数组 numsSorted,并对该数组进行排序,然后我们从左向右找到第一个两数组不同的位置,即为 numsB​ 右边界。最后我们输出 numsB​ 的长度即可。

特别地,当原数组有序时,numsB​ 的长度为 0,我们可以直接返回结果。


代码

class Solution {
public:
    int findUnsortedSubarray(vector<int>& nums) {
        if (is_sorted(nums.begin(), nums.end())) {
            return 0;
        }
        vector<int> numsSorted(nums);
        sort(numsSorted.begin(), numsSorted.end());
        int left = 0;
        while (nums[left] == numsSorted[left]) {
            left++;
        }
        int right = nums.size() - 1;
        while (nums[right] == numsSorted[right]) {
            right--;
        }
        return right - left + 1;
    }
};

646. 最长数对链

题目描述

给你一个由 n 个数对组成的数对数组 pairs ,其中 pairs[i] = [lefti, righti] 且 lefti < righti 。

现在,我们定义一种 跟随 关系,当且仅当 b < c 时,数对 p2 = [c, d] 才可以跟在 p1 = [a, b] 后面。我们用这种形式来构造 数对链 。

找出并返回能够形成的 最长数对链的长度 。

你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。

  • 示例 1:

    输入:pairs = [[1,2], [2,3], [3,4]]
    输出:2
    解释:最长的数对链是 [1,2] -> [3,4] 。

    示例 2:

    输入:pairs = [[1,2],[7,8],[4,5]]
    输出:3
    解释:最长的数对链是 [1,2] -> [4,5] -> [7,8] 。

提示:

n == pairs.length
1 <= n <= 1000
-1000 <= lefti < righti <= 1000

解析

用一个数组 arr 来记录当前最优情况,arr[i] 就表示长度为i+1 的数对链的末尾可以取得的最小值,遇到一个新数对时,先用二分查找得到这个数对可以放置的位置,再更新 arr。


代码

class Solution {
public:
    int findLongestChain(vector<vector<int>>& pairs) {
        sort(pairs.begin(), pairs.end());
        vector<int> arr;
        for (auto p : pairs) {
            int x = p[0], y = p[1];
            if (arr.size() == 0 || x > arr.back()) {
                arr.emplace_back(y);
            } else {
                int idx = lower_bound(arr.begin(), arr.end(), x) - arr.begin();
                arr[idx] = min(arr[idx], y);
            }
        }
        return arr.size();
    }
};

649. Dota2 参议院

题目描述

Dota2 的世界里有两个阵营:Radiant(天辉)和 Dire(夜魇)

Dota2 参议院由来自两派的参议员组成。现在参议院希望对一个 Dota2 游戏里的改变作出决定。他们以一个基于轮为过程的投票进行。在每一轮中,每一位参议员都可以行使两项权利中的 一 项:

禁止一名参议员的权利:参议员可以让另一位参议员在这一轮和随后的几轮中丧失 所有的权利 。
宣布胜利:如果参议员发现有权利投票的参议员都是 同一个阵营的 ,他可以宣布胜利并决定在游戏中的有关变化。

给你一个字符串 senate 代表每个参议员的阵营。字母 ‘R’ 和 'D’分别代表了 Radiant(天辉)和 Dire(夜魇)。然后,如果有 n 个参议员,给定字符串的大小将是 n。

以轮为基础的过程从给定顺序的第一个参议员开始到最后一个参议员结束。这一过程将持续到投票结束。所有失去权利的参议员将在过程中被跳过。

假设每一位参议员都足够聪明,会为自己的政党做出最好的策略,你需要预测哪一方最终会宣布胜利并在 Dota2 游戏中决定改变。输出应该是 “Radiant” 或 “Dire” 。

  • 示例 1:

    输入:senate = “RD”
    输出:“Radiant”
    解释: 第 1 轮时,第一个参议员来自 Radiant 阵营,他可以使用第一项权利让第二个参议员失去所有权利。 这一轮中,第二个参议员将会被跳过,因为他的权利被禁止了。 第 2 轮时,第一个参议员可以宣布胜利,因为他是唯一一个有投票权的人。

    示例 2:

    输入:senate = “RDD”
    输出:“Dire”
    解释: 第 1 轮时,第一个来自 Radiant 阵营的参议员可以使用第一项权利禁止第二个参议员的权利。 这一轮中,第二个来自 Dire 阵营的参议员会将被跳过,因为他的权利被禁止了。
    这一轮中,第三个来自 Dire 阵营的参议员可以使用他的第一项权利禁止第一个参议员的权利。 因此在第二轮只剩下第三个参议员拥有投票的权利,于是他可以宣布胜利。

提示:

n == senate.length
1 <= n <= 104
senate[i] 为 'R' 或 'D'

解析

我们以天辉方的议员为例。当一名天辉方的议员行使权利时:

如果目前所有的议员都为天辉方,那么该议员可以直接宣布天辉方取得胜利;

如果目前仍然有夜魇方的议员,那么这名天辉方的议员只能行使「禁止一名参议员的权利」这一项权利。显然,该议员不会令一名同为天辉方的议员丧失权利,所以他一定会挑选一名夜魇方的议员。那么应该挑选哪一名议员呢?容易想到的是,应该贪心地挑选按照投票顺序的下一名夜魇方的议员。这也是很容易形象化地证明的:既然只能挑选一名夜魇方的议员,那么就应该挑最早可以进行投票的那一名议员;如果挑选了其他较晚投票的议员,那么等到最早可以进行投票的那一名议员行使权利时,一名天辉方议员就会丧失权利,这样就得不偿失了。

由于我们总要挑选投票顺序最早的议员,因此我们可以使用两个队列 radiant 和 dire 分别按照投票顺序存储天辉方和夜魇方每一名议员的投票时间。随后我们就可以开始模拟整个投票的过程:

如果此时 radiant 或者 dire 为空,那么就可以宣布另一方获得胜利;

如果均不为空,那么比较这两个队列的首元素,就可以确定当前行使权利的是哪一名议员。如果 radiant 的首元素较小,那说明轮到天辉方的议员行使权利,其会挑选 dire 的首元素对应的那一名议员。因此,我们会将 dire 的首元素永久地弹出,并将 radiant 的首元素弹出,增加 n 之后再重新放回队列,这里 n 是给定的字符串 senate 的长度,即表示该议员会参与下一轮的投票。

    为什么这里是固定地增加 n,而不是增加与当前剩余议员数量相关的一个数?读者可以思考一下这里的正确性。

同理,如果 dire 的首元素较小,那么会永久弹出 radiant 的首元素,剩余的处理方法也是类似的。

这样一来,我们就模拟了整个投票的过程,也就可以得到最终的答案了。


代码

class Solution {
public:
    string predictPartyVictory(string senate) {
        int n = senate.size();
        queue<int> radiant, dire;
        for (int i = 0; i < n; ++i) {
            if (senate[i] == 'R') {
                radiant.push(i);
            }
            else {
                dire.push(i);
            }
        }
        while (!radiant.empty() && !dire.empty()) {
            if (radiant.front() < dire.front()) {
                radiant.push(radiant.front() + n);
            }
            else {
                dire.push(dire.front() + n);
            }
            radiant.pop();
            dire.pop();
        }
        return !radiant.empty() ? "Radiant" : "Dire";
    }
};


659. 分割数组为连续子序列

题目描述

给你一个按 非递减顺序 排列的整数数组 nums 。

请你判断是否能在将 nums 分割成 一个或多个子序列 的同时满足下述 两个 条件:

每个子序列都是一个 连续递增序列(即,每个整数 恰好 比前一个整数大 1 )。
所有子序列的长度 至少 为 3 。

如果可以分割 nums 并满足上述条件,则返回 true ;否则,返回 false 。

  • 示例 1:

    输入:nums = [1,2,3,3,4,5]
    输出:true
    解释:nums 可以分割成以下子序列: [1,2,3,3,4,5] --> 1, 2, 3 [1,2,3,3,4,5] --> 3, 4, 5

    示例 2:

    输入:nums = [1,2,3,3,4,4,5,5]
    输出:true
    解释:nums 可以分割成以下子序列:[1,2,3,3,4,4,5,5] --> 1, 2, 3, 4, 5 [1,2,3,3,4,4,5,5] --> 3, 4, 5

    示例 3:

    输入:nums = [1,2,3,4,4,5]
    输出:false
    解释:无法将 nums 分割成长度至少为 3 的连续递增子序列。

提示:

1 <= nums.length <= 104
-1000 <= nums[i] <= 1000
nums 按非递减顺序排列

解析

使用两个哈希表,第一个哈希表存储数组中的每个数字的剩余次数,第二个哈希表存储数组中的每个数字作为结尾的子序列的数量。

初始时,每个数字的剩余次数即为每个数字在数组中出现的次数,因此遍历数组,初始化第一个哈希表。

在初始化第一个哈希表之后,遍历数组,更新两个哈希表。只有当一个数字的剩余次数大于 0 时,才需要考虑这个数字是否属于某个子序列。假设当前元素是 x,进行如下操作。

首先判断是否存在以 x−1 结尾的子序列,即根据第二个哈希表判断 x−1 作为结尾的子序列的数量是否大于 0,如果大于 0,则将元素 x 加入该子序列中。由于 x 被使用了一次,因此需要在第一个哈希表中将 x 的剩余次数减 1。又由于该子序列的最后一个数字从 x−1 变成了 x,因此需要在第二个哈希表中将 x−1 作为结尾的子序列的数量减 1,以及将 x 作为结尾的子序列的数量加 1。

否则,x 为一个子序列的第一个数,为了得到长度至少为 3 的子序列,x+1 和 x+2 必须在子序列中,因此需要判断在第一个哈希表中 x+1 和 x+2 的剩余次数是否都大于 0。

    当 x+1 和 x+2 的剩余次数都大于 0 时,可以新建一个长度为 3 的子序列 [x,x+1,x+2]。由于这三个数都被使用了一次,因此需要在第一个哈希表中将这三个数的剩余次数分别减 1。又由于该子序列的最后一个数字是 x+2,因此需要在第二个哈希表中将 x+2 作为结尾的子序列的数量加 1。

    否则,无法得到长度为 3 的子序列[x,x+1,x+2],因此无法完成分割,返回 false。

如果整个数组遍历结束时,没有遇到无法完成分割的情况,则可以完成分割,返回 true。


代码

class Solution {
public:
    bool isPossible(vector<int>& nums) {
        unordered_map<int, int> countMap;
        unordered_map<int, int> endMap;
        for (auto& x : nums) {
            int count = countMap[x] + 1;
            countMap[x] = count;
        }
        for (auto& x : nums) {
            int count = countMap[x];
            if (count > 0) {
                int prevEndCount = endMap[x - 1];
                if (prevEndCount > 0) {
                    countMap[x] = count - 1;
                    endMap[x - 1] = prevEndCount - 1;
                    endMap[x] = endMap[x] + 1;
                } else {
                    int count1 = countMap[x + 1];
                    int count2 = countMap[x + 2];
                    if (count1 > 0 && count2 > 0) {
                        countMap[x] = count - 1;
                        countMap[x + 1] = count1 - 1;
                        countMap[x + 2] = count2 - 1;
                        endMap[x + 2] = endMap[x + 2] + 1;
                    } else {
                        return false;
                    }
                }
            }
        }
        return true;
    }
};


670. 最大交换

题目描述

给定一个非负整数,你至多可以交换一次数字中的任意两位。返回你能得到的最大值。

  • 示例 1 :

    输入: 2736 输出: 7236 解释: 交换数字2和数字7。

    示例 2 :

    输入: 9973 输出: 9973 解释: 不需要交换。

注意:

给定数字的范围是 [0, 108]

解析

由于对于整数 num 的十进制数字位长最长为 8 位,任意两个数字交换一次最多有 28 种不同的交换方法,因此我们可以尝试遍历所有可能的数字交换方法即可,并找到交换后的最大数字即可。

我们将数字存储为长度为 n 的列表,其中 n 为整数 num 的十进制位数的长度。对于位置为(i, j) 的每个候选交换,我们交换数字并记录组成的新数字是否大于当前答案;
对于前导零的问题,我们也不需要特殊处理。
由于数字只有 8 位,所以我们不必考虑交换后溢出的风险;

代码

class Solution {
public:
    int maximumSwap(int num) {
        string charArray = to_string(num);
        int n = charArray.size();
        int maxNum = num;
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                swap(charArray[i], charArray[j]);
                maxNum = max(maxNum, stoi(charArray));
                swap(charArray[i], charArray[j]);
            }
        }
        return maxNum;
    }
};


680. 验证回文串 II

题目描述

给你一个字符串 s,最多 可以从中删除一个字符。

请你判断 s 是否能成为回文字符串:如果能,返回 true ;否则,返回 false 。

  • 示例 1:

    输入:s = “aba” 输出:true

    示例 2:

    输入:s = “abca” 输出:true 解释:你可以删除字符 ‘c’ 。

    示例 3:

    输入:s = “abc” 输出:false

提示:

1 <= s.length <= 105
s 由小写英文字母组成

解析

考虑最朴素的方法:首先判断原串是否是回文串,如果是,就返回 true;如果不是,则枚举每一个位置作为被删除的位置,再判断剩下的字符串是否是回文串。这种做法的渐进时间复杂度是 O(n2) 的,会超出时间限制。

我们换一种想法。首先考虑如果不允许删除字符,如何判断一个字符串是否是回文串。常见的做法是使用双指针。定义左右指针,初始时分别指向字符串的第一个字符和最后一个字符,每次判断左右指针指向的字符是否相同,如果不相同,则不是回文串;如果相同,则将左右指针都往中间移动一位,直到左右指针相遇,则字符串是回文串。

在允许最多删除一个字符的情况下,同样可以使用双指针,通过贪心实现。初始化两个指针 low 和 high 分别指向字符串的第一个字符和最后一个字符。每次判断两个指针指向的字符是否相同,如果相同,则更新指针,将 low 加 1,high 减 1,然后判断更新后的指针范围内的子串是否是回文字符串。如果两个指针指向的字符不同,则两个字符中必须有一个被删除,此时我们就分成两种情况:即删除左指针对应的字符,留下子串 s[low+1:high],或者删除右指针对应的字符,留下子串 s[low:high−1]。当这两个子串中至少有一个是回文串时,就说明原始字符串删除一个字符之后就以成为回文串。


代码

class Solution {
public:
    bool checkPalindrome(const string& s, int low, int high) {
        for (int i = low, j = high; i < j; ++i, --j) {
            if (s[i] != s[j]) {
                return false;
            }
        }
        return true;
    }

    bool validPalindrome(string s) {
        int low = 0, high = s.size() - 1;
        while (low < high) {
            char c1 = s[low], c2 = s[high];
            if (c1 == c2) {
                ++low;
                --high;
            } else {
                return checkPalindrome(s, low, high - 1) || checkPalindrome(s, low + 1, high);
            }
        }
        return true;
    }
};


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值