本篇文章为LeetCode 贪心算法模块的刷题笔记,仅供参考。
Leetcode31. 下一个排列
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
例如,arr = [1,2,3],以下这些都可以视作arr的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。
给你一个整数数组 nums ,找出 nums 的下一个排列。
必须 原地 修改,只允许使用额外常数空间。
示例 1:
输入:nums = [1,2,3]
输出:[1,3,2]
示例 2:
输入:nums = [3,2,1]
输出:[1,2,3]
示例 3:
输入:nums = [1,1,5]
输出:[1,5,1]
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 100
其实很像二进制编码,本质就是找nums[i]i=n
满足 nums[n]
nums[i]>=nums[i+1]
,因此将 nums[n]
后第一个比 n 大的数与 nums[n]
交换,然后 nums[n]
后的数升序排列即可:
class Solution {
public:
void nextPermutation(vector<int>& nums) {
bool flag=0;
int i;
for(i=nums.size()-2;i>=0;i--){
if(nums[i]<nums[i+1]){
flag=1;
break;
}
}
if(!flag){
sort(nums.begin(),nums.end());
return;
}
for(int j=nums.size()-1;j>=i+1;j--){
if(nums[j]>nums[i]){
int tmp=nums[i];
nums[i]=nums[j];
nums[j]=tmp;
break;
}
}
sort(nums.begin()+i+1,nums.end());
return;
}
};
Leetcode55.跳跃游戏
给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
示例 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 <= 3 * 104
0 <= nums[i] <= 105
法一:本题可以直接 dfs 暴力求解,用时惨淡:
class Solution {
public:
void dfs(vector<int>& nums,vector<bool>& visit,int start){
int n=nums.size();
int next=start+nums[start];
for(int i=start+1;i<min(n,next+1);i++){
if(!visit[i]){
visit[i]=true;
dfs(nums,visit,i);
}
}
return;
}
bool canJump(vector<int>& nums) {
int n=nums.size();
vector<bool> visit(n); //是否访问过
for(int i=0;i<n;i++) visit[i]=false;
visit[0]=true;
dfs(nums,visit,0);
return visit[n-1];
}
};
法二:利用贪心法则,遍历数组维护数组能够到达的最远的点 furthest。对于数组的某一个位置 x,它所能到达的点的范围为 [x,x+nums[x]]
。因此遍历数组,持续更新 furthest:
class Solution {
public:
bool canJump(vector<int>& nums) {
int furthest=0;
int n=nums.size();
for(int i=0;i<n;i++){
if(i<=furthest){
int tmp=i+nums[i];
if(tmp>=n-1) return true;
furthest=max(furthest,tmp);
}else{
return false;
}
}
return true;
}
};
Leetcode45.跳跃游戏 II
给你一个非负整数数组 nums ,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
假设你总是可以到达数组的最后一个位置。
示例 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
法一:本题也可以 bfs 求解,记数组 step[i] 表示从起点到达 i 的最少步数,遍历 nums 数组,将 step[i+1] 到 step[i+nums[i]] 标记为 min(step[i],step[i]+1) 即可:
class Solution {
public:
int jump(vector<int>& nums) {
int n=nums.size();
vector<int> step(n);
for(int i=0;i<n;i++){
int tmp=nums[n];
for(int j=i+1;j<=i+nums[i] && j<n;j++){
if(step[j]==0) step[j]=step[i]+1;
}
}
return step[n-1];
}
};
法二:法一属于暴力算法,求解了所有点的最短距离,下采用贪心思想:遍历数组 nums ,同时维护3个变量:下一步内可达的最远距离 furthest、当前 step 的边界 bound、当前步数 step。遍历数组的过程中,实际上用 bound 进行划分每一步,如果当前位置还在 bound 范围内,则可以:
class Solution {
public:
int jump(vector<int>& nums) {
int n=nums.size();
int furthest=0;
int step=0;
int bound=0;
for(int i=0;i<n;i++){
if(bound>=n-1) return step;
if(i<=bound){
int tmp=i+nums[i];
furthest=max(furthest,tmp);
}else{
bound=furthest;
step++;
int tmp=i+nums[i];
furthest=max(furthest,tmp);
}
}
return step;
}
};
Leetcode134.加油站
在一条环路上有 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
最直接但是错误的想法是:如果可以走完一周,开始节点一定是 (gas[i]-cost[i]) 最大的节点。 反例:gas=[5,8,2,8],cost=[6,5,6,6]。
其实只要 Σgas ≥ Σ cost,总存在开始节点使得汽车成功环绕一周,这可以作为判断前提。不妨记 remain[i] = gas[i] - cost[i],表示走完每段路的剩余油量(负数表示要求走这段路之前就有库存)。如果某节点剩余油量为负,那么肯定不能作为开始节点;如果某节点开始的连续累计和一直非负,则可以作为开始节点。因此遍历 remain 数组,对 remain 计算累计和并记录开始节点,如果某节点的累计和为负,则重置初始节点和累计和。
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int n=gas.size();
int n_gas=0;
int n_cost=0;
vector<int> remain(n);
for(int i=0;i<n;i++){
n_gas+=gas[i];
n_cost+=cost[i];
remain[i]=gas[i]-cost[i];
}
if(n_gas<n_cost) return -1;
int start=0;
int sum=0;
for(int i=0;i<n;i++){
sum+=remain[i];
if(sum<0){
start=(i+1)%n;
sum=0;
}
}
return start;
}
};
Leetcode179.最大数
给定一组非负整数 nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。
注意:输出结果可能非常大,所以你需要返回一个字符串而不是整数。
示例 1:
输入:nums = [10,2]
输出:“210”
示例 2:
输入:nums = [3,30,34,5,9]
输出:“9534330”
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 109
一开始考虑的是使用自定义优先级的优先队列存储字符串,每次弹出栈顶元素加入字符串。但当遇到 “31”、“3” 这种情况时,虽然下一字符串开头不会大于 3,但无法判断是 0,1 还是 2,因此无法判断使用哪个字符串进行拼接。
因此考虑比较 s1+s2 和 s2+s1,将大的设置为更高的优先级。提交时发现卡在测试点 [0, 0],“0” 上,提前判断即可:
class Solution {
public:
struct cmp{
bool operator() (string s1, string s2){
return (s1+s2)<(s2+s1);
}
};
string largestNumber(vector<int>& nums) {
priority_queue<string,vector<string>,cmp> p;
int n=nums.size();
for(int i=0;i<n;i++){
p.push(to_string(nums[i]));
}
string ans="";
for(int i=0;i<n;i++){
if(!(ans=="0" && p.top()=="0")){ // 防止 “00”
ans+=p.top();
}
p.pop();
}
return ans;
}
};
Leetcode213.打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:
输入:nums = [1,2,3]
输出:3
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 1000
本题是在 Leetcode198.打家劫舍 的基础上加上 房屋环形相连 的限制,原本首尾不相连的题目很容易采用动态规划解决,但加上首尾相连的限制后,子问题的不相关性被打破。例如 {1,2,3},计算到 3 时其最优解受 1 影响,不再独立。后来在评论区看到一句评论茅塞顿开:“其实就是 把环拆成两个队列,一个是从 0 到 n-1,另一个是从 1 到 n,然后返回两个结果中最大的。”
class Solution {
public:
int help(vector<int>& nums) {
int n=nums.size();
if(n==1) return nums[0];
vector<int> dp(n); //dp[i]表示盗取前i家能够获得的最高金额
vector<bool> flag(n); //是否盗取第i家
dp[0]=nums[0];
flag[0]=true;
if(nums[0]>nums[1]){
dp[1]=nums[0];
flag[1]=false;
}else{
dp[1]=nums[1];
flag[1]=true;
}
for(int i=2;i<n;i++){
if(!flag[i-1]){
dp[i]=dp[i-1]+nums[i];
flag[i]=true;
}else{
if(dp[i-1]>dp[i-2]+nums[i]){
dp[i]=dp[i-1];
flag[i]=false;
}else{
dp[i]=dp[i-2]+nums[i];
flag[i]=true;
}
}
}
return dp[n-1];
}
int rob(vector<int>& nums) {
int n=nums.size();
if(n==1) return nums[0];
vector<int> v1(nums.begin(),nums.end()-1);
vector<int> v2(nums.begin()+1,nums.end());
return max(help(v1),help(v2));
}
};
Leetcode334.递增的三元子序列
给你一个整数数组 nums ,判断这个数组中是否存在长度为 3 的递增子序列。
如果存在这样的三元组下标 (i, j, k) 且满足 i < j < k ,使得 nums[i] < nums[j] < nums[k] ,返回 true ;否则,返回 false 。
示例 1:
输入:nums = [1,2,3,4,5]
输出:true
解释:任何 i < j < k 的三元组都满足题意
示例 2:
输入:nums = [5,4,3,2,1]
输出:false
解释:不存在满足题意的三元组
示例 3:
输入:nums = [2,1,5,0,4,6]
输出:true
解释:三元组 (3, 4, 5) 满足题意,因为 nums[3] = 0 < nums[4] = 4 < nums[5] = 6
提示:
1 <= nums.length <= 5 * 105
-231 <= nums[i] <= 231 - 1
三元子序列可以不连续
很容易想到维护两个变量来记录 min_num
和 mid_num
,然后遍历数组寻找合适的 max_num
。在遍历数组时,如果:
(1)min_num
< mid_num
< nums[i]
,则已经找到长度为 3 的递增子序列。这是函数返回的唯一标准;
(2)min_num
< nums[i]
< mid_num
,则更新 mid_num
。此时相当于降低了 mid_num
的标准,后面如果有满足要求的递增子序列更容易满足;
(3)nums[i]
< min_num
< mid_num
,则更新 min_num
。此处不细看会认为有问题,因为 nums[i]
在 min_num
和 mid_num
后面出现,不满足序列的顺序。但 min_num
更新为 nums[i]
后隐藏着一个真相:min_num
和 mid_num
之间还存在某个数(即原 min_num
),当后面出现大于 mid_num
的数,可以满足(1)直接返回,并且该序列是顺序的;
如果还是对情况(3)存疑,可以再看下一个元素,下面举例证明,无非以下 3 种情况:
再来看 min_num
和 mid_num
的初值:显然 mid_num
初值必须为 INT_MAX,否则直接进入(1)返回 true。若 min_num
初值为 INT_MIN,则第一次一定会进入(2),一旦 nums[1] > nums[0],就会进入(1)返回 true,不符合要求;若 min_num
初值为 INT_MAX,则第一次一定会进入(3),在 mid_num
被赋实值之前不会结束。
class Solution {
public:
bool increasingTriplet(vector<int>& nums) {
int n=nums.size();
if(n<=2) return false;
int min_num=INT_MAX;
int mid_num=INT_MAX;
for(int i=0;i<n;i++){
if(nums[i]<=min_num){
min_num=nums[i];
}else if(nums[i]>mid_num){
return true;
}else{
mid_num=nums[i];
}
}
return false;
}
};
Leetcode376.摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [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
法一:动态规划
这题显然可以用动态规划,开两个二维数组 swing_len 和 sub:swing_len 存储 nums[ i…j ] 的最长摆动子序列的长度;sub 存储 nums[ i…j ] 中的最长摆动子序列的末尾两个元素的增 / 减情况,1 表示子序列末尾是上升的,-1 表示下降的。当数组 nums 持续上升或下降时,可以将其视为同一个点,只保持最高点或最低点作为当前摆动子序列的最后节点。
在动态规划过程中,nums[i…j] 由 nums[i…j-1] 得到:如果 nums[i…j-1] 中的最长递增子序列的末尾是递增或递减的,那么结合 nums[j-1] 和 nums[j] 的增减关系可以很容易判断出 swing_len[i][j] 和 sub[i][j] 的取值;如果 nums[i…j-1] 中的最长递增子序列的末尾是平的(即 nums[j-1] = nums[j-2]),则取 nums[i…j-2] 中的最长摆动子序列的末尾两个元素的增 / 减情况即可。如果开头出现 nums 持平不变的情况,可以单独赋值(本题中赋为 0)单独处理,表明可增可减。
nums 数组的最长摆动子序列长度为 swing_len[0][n-1]:
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int n=nums.size();
vector<vector<int> > swing_len; //存储最长子序列长度
vector<int> tmp1(n);
swing_len.resize(n,tmp1);
vector<vector<int> > sub; //存储最长子序列的符号
vector<int> tmp2(n);
sub.resize(n,tmp2);
for(int i=0;i<n;i++){
for(int j=i;j<n;j++){
if(i==j){ //初始情况
swing_len[i][j]=1;
sub[i][j]=0;
}
else{ //dp
//nums[i][j-1]的最长递增子序列末尾上升或下降
if(sub[i][j-1]==0){
if(nums[j]==nums[j-1]){
swing_len[i][j]=1;
sub[i][j]=0;
}else if(nums[j-1]<nums[j]){
swing_len[i][j]=2;
sub[i][j]=1;
}else{
swing_len[i][j]=2;
sub[i][j]=-1;
}
}
//nums[i][j-1]的最长递增子序列末尾上升
else if(sub[i][j-1]==1){
if(nums[j-1]<=nums[j]){
swing_len[i][j]=swing_len[i][j-1];
sub[i][j]=1;
}else{
swing_len[i][j]=swing_len[i][j-1]+1;
sub[i][j]=-1;
}
}
//nums[i][j-1]的最长递增子序列末尾下降
else if(sub[i][j-1]==-1){
if(nums[j-1]>=nums[j]){
swing_len[i][j]=swing_len[i][j-1];
sub[i][j]=-1;
}else{
swing_len[i][j]=swing_len[i][j-1]+1;
sub[i][j]=1;
}
}
}
}
}
return swing_len[0][n-1];
}
};
法二:贪心下的动态规划
向本题这种需要划分为两种情况(结尾上升 / 下降)讨论的问题可以维护两个数组:一个用来标记结尾上升,另一个用来标记结尾下降。具有相似思路的还有 Leetcode213.打家劫舍 II。设 up[i] 表示前 i 个元素中的 结尾上升的最长摆动子序列 的长度,down[i] 表示前 i 个元素中的 结尾下降的最长摆动子序列 的长度。
对于 up[i],其结果由 up[i-1] 和 down[i-1] 决定,因为是 up[i] 表示结尾上升,因此只有 nums[i-1] < nums[i] 才有效。如果 nums[i-1] < nums[i],则 up[i] 赋值为 up[i-1](连续上升不增加摆动子序列长度) 和 down[i-1]+1(结尾下降的摆动子序列再上升) 中较大的元素;如果 nums[i-1] >= nums[i],则 up[i] 保持为 up[i-1]。
同理,对于 down[i],只有 nums[i-1] > nums[i] 才有效。如果 nums[i-1] > nums[i],则 down[i] 赋值为 down[i-1](连续下降不增加摆动子序列长度) 和 up[i-1]+1(结尾上升的摆动子序列再下降) 中较大的元素;如果 nums[i-1] <= nums[i],则 down[i] 保持为 down[i-1]。
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int n=nums.size();
vector<int> up(n);
vector<int> down(n);
up[0]=1;
down[0]=1;
for(int i=1;i<n;i++){
if(nums[i-1]<nums[i]){
up[i]=max(up[i-1],down[i-1]+1);
down[i]=down[i-1];
}else if(nums[i-1]==nums[i]){
up[i]=up[i-1];
down[i]=down[i-1];
}else{
up[i]=up[i-1];
down[i]=max(down[i-1],up[i-1]+1);
}
}
return max(up[n-1],down[n-1]);
}
};