贪心法,又称贪心算法,贪婪算法,在对问题求解时,总是做出在当前看来最好的选择,期望通过每个阶段的局部最优选择达到全局最优,但结果不一定最优
适用场景:简单的说,问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解,就能用贪心算法的到最后的最优解,这种子问题最优解称为最优子结构
贪心算法与动态规划的不同点在于它对每个子问题的解决方案都做出当前的最优选择,不能回退,而动态规划会保留之前的运算结果,并根据之前的结果进行选择,有回退的功能,贪心是动态规划的理想化的情况。
给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间,CPU 可以完成一个任务,或者处于待命状态。然而,两个 相同种类 的任务之间必须有长度为整数 n 的冷却时间,因此至少有连续 n 个单位时间内 CPU 在执行不同的任务,或者在待命状态。你需要计算完成所有任务所需要的 最短时间 。示例 1:输入:tasks = "A","A","A","B","B","B", n = 2 输出:8 解释:A -> B -> (待命) -> A -> B -> (待命) -> A -> B 在本示例中,两个相同类型任务之间必须间隔长度为 n = 2 的冷却时间,而执行一个任务只需要一个单位时间,所以中间出现了(待命)状态。 示例 2:输入:tasks = "A","A","A","B","B","B", n = 0 输出:6 解释:在这种情况下,任何大小为 6 的排列都可以满足要求,因为 n = 0 "A","A","A","B","B","B" "B","B","B","A","A","A" ... 诸如此类 示例 3:输入:tasks = "A","A","A","A","A","A","B","C","D","E","F","G", n = 2 输出:16 解释:一种可能的解决方案是: A -> B -> C -> A -> D -> E -> A -> F -> G -> A -> (待命) -> (待命) -> A -> (待命) -> (待命) -> A提示:1 <= task.length <= 104 tasksi 是大写英文字母 n 的取值范围为 0, 100
O(n)
,空间复杂度O(1)
js:
function leastInterval(tasks, n) {
let arr = Array(26).fill(0);
for (let c of tasks) {
//统计各个字母出现的次数
arr[c.charCodeAt() - "A".charCodeAt()]++;
}
let max = 0;
for (let i = 0; i < 26; i++) {
//找到最大次数
max = Math.max(max, arr[i]);
}
let ret = (max - 1) * (n + 1); //计算前n-1行n的间隔的时间大小
for (let i = 0; i < 26; i++) {
//计算和最大次数相同的字母个数,然后累加进ret
if (arr[i] == max) {
ret++;
}
}
return Math.max(ret, tasks.length); //在tasks的长度和ret中取较大的一个
}
给定数组 people 。peoplei表示第 i 个人的体重 ,船的数量不限,每艘船可以承载的最大重量为 limit。每艘船最多可同时载两人,但条件是这些人的重量之和最多为 limit。返回 承载所有人所需的最小船数 。示例 1:输入:people = 1,2, limit = 3 输出:1 解释:1 艘船载 (1, 2) 示例 2:输入:people = 3,2,2,1, limit = 3 输出:3 解释:3 艘船分别载 (1, 2), (2) 和 (3) 示例 3:输入:people = 3,5,3,4, limit = 5 输出:4 解释:4 艘船分别载 (3), (3), (4), (5)提示:1 <= people.length <= 5 104 1 <= peoplei <= limit <= 3 104
O(nlogn)
,排序的复杂度。空间复杂度O(logn)
,排序的栈空间js:
var numRescueBoats = function (people, limit) {
people.sort((a, b) => (a - b));
let ans = 0,
left = 0,//左指针初始化在0的位置
right = people.length - 1 //右指针初始化在people.length - 1的位置
while (left <= right) {//两指针向中间靠拢 遍历
//当people[left] + people[right--]) <= limit 表示左右两边的人可以一起坐船 然后让left++ right--
//如果两人坐不下,那只能让重的人先坐一条船 也就是让right--
if ((people[left] + people[right--]) <= limit) {
left++
}
ans++
}
return ans
};
给定一个非负整数数组 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 <= numsi <= 105
dp[i]
表示能否到达位置i,对每个位置i判断能否通过前面的位置跳跃过来,当前位置j能达到,并且当前位置j加上能到达的位置如果超过了i,那dp[i]
更新为ture,便是i位置也可以到达。O(n^2)
,空间复杂度O(n)
js:
function canJump(nums) {
let dp = new Array(nums.length).fill(false); //初始化dp
dp[0] = true; //第一项能到达
for (let i = 1; i < nums.length; i++) {
for (let j = 0; j < i; j++) {
//当前位置j能达到,并且当前位置j加上能到达的位置如果超过了i,那dp[i]更新为ture,便是i位置也可以到达
if (dp[j] && nums[j] + j >= i) {
dp[i] = true;
break;
}
}
}
return dp[nums.length - 1];
}
O(n)
,遍历一边。空间复杂度O(1)
js:
var canJump = function (nums) {
if (nums.length === 1) return true; //长度为1 直接就是终点
let cover = nums[0]; //能覆盖的最远距离
for (let i = 0; i <= cover; i++) {
cover = Math.max(cover, i + nums[i]); //当前覆盖距离cover和当前位置加能跳跃的距离中取一个较大者
if (cover >= nums.length - 1) {
//覆盖距离超过或等于nums.length - 1 说明能到达终点
return true;
}
}
return false; //循环完成之后 还没返回true 就是不能达到终点
};
在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gasi 升。你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 costi 升。你从其中的一个加油站出发,开始时油箱为空。给定两个整数数组 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 <= gasi, costi <= 104
O(n)
,空间复杂度O(1)
js:
var canCompleteCircuit = function (gas, cost) {
let totalGas = 0;
let totalCost = 0;
for (let i = 0; i < gas.length; i++) {
totalGas += gas[i];
totalCost += cost[i];
}
if (totalGas < totalCost) {//总油量小于总油耗 肯定不能走一圈
return -1;
}
let currentGas = 0;
let start = 0;
for (let i = 0; i < gas.length; i++) {
currentGas = currentGas - cost[i] + gas[i];
if (currentGas < 0) {//如果到达下一站的时候油量为负数 就以这个站为起点 从新计算
currentGas = 0;
start = i + 1;
}
}
return start;
};
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。注意,一开始你手头没有任何零钱。给你一个整数数组 bills ,其中 billsi 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。示例 1:输入:bills = 5,5,5,10,20 输出:true 解释: 前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。 第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。 第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。 由于所有客户都得到了正确的找零,所以我们输出 true。 示例 2:输入:bills = 5,5,10,10,20 输出:false 解释: 前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。 对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。 对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。 由于不是每位顾客都得到了正确的找零,所以答案是 false。提示:1 <= bills.length <= 105 billsi 不是 5 就是 10 或是 20
O(n)
,空间复杂度O(1)
js:
var lemonadeChange = function (bills) {
let five = 0, ten = 0;
for (const bill of bills) {
if (bill === 5) {//面值为5 直接可以兑换柠檬水
five += 1;
} else if (bill === 10) {//面值为10 兑换柠檬水 还需要找5元
if (five === 0) {
return false;
}
five -= 1;
ten += 1;
} else {//面值为20 兑换柠檬水 需要找3个5元或一个10元一个5元
if (five > 0 && ten > 0) {
five -= 1;
ten -= 1;
} else if (five >= 3) {
five -= 3;
} else {
return false;
}
}
}
return true;
};
给定一个区间的集合 intervals ,其中 intervalsi = 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 intervalsi.length == 2 -5 104 <= starti < endi <= 5 104
dp[i]
表示前i个区间中最大不重合区间的个数,首先将区间数组按左边界排序,找出intervals中最多有多少个不重复的区间,动态规划方程dp[i] = Math.max(dp[i], dp[j] + 1)
。intervals的长度减去最多的不重复的区间 就是最少删除区间的个数O(n^2)
,两层嵌套循环leetcode执行超时 复杂度过高。空间复杂度O(n)
,dp数组的空间js:
//leetcode执行超时 复杂度过高
var eraseOverlapIntervals = function (intervals) {
if (!intervals.length) {
return 0;
}
intervals.sort((a, b) => a[0] - b[0]); //按左边界排序
const n = intervals.length;
const dp = new Array(n).fill(1); //初始化dp数组
for (let i = 1; i < n; i++) {
for (let j = 0; j < i; j++) {
//循环i,j找出intervals中最多有多少个不重复的区间
//j的右边界小于i的左边界 相当于多出了一个不重合区间
if (intervals[j][1] <= intervals[i][0]) {
dp[i] = Math.max(dp[i], dp[j] + 1); //更新dp[i]
}
}
}
return n - Math.max(...dp); //n减去最多的不重复的区间 就是最少删除区间的个数
};
O(nlogn)
,数组排序O(nlogn)
,循环一次数组O(n)
。空间复杂度O(logn)
,排序需要的栈空间js:
var eraseOverlapIntervals = function (intervals) {
if (!intervals.length) {
return 0;
}
//按右边界排序,然后从左往右遍历,右边界结束的越早,留给后面的区间的空间就越大,不重合的区间个数就越多
intervals.sort((a, b) => a[1] - b[1]);
const n = intervals.length;
let right = intervals[0][1]; //right初始化为第一个区间的右边界
let ans = 1; //最多的不重合区间的个数
for (let i = 1; i < n; ++i) {
//循环区间数组
if (intervals[i][0] >= right) {
//当区间的左边界大于上一个区间的右边界的时候 说明是一对不重合区间
++ans; //ans加1
right = intervals[i][1]; //更新right
}
}
return n - ans; //intervals的长度减去最多的不重复的区间 就是最少删除区间的个数
};
能不能用贪心算法需要满足贪心选择性,贪心算法正确的的证明可以用反证法
以这一题为例:
[a, b]
,我们称为区间A。[c, d]
,我们称为区间C,使得它是最优解中的一个区间,其中d>b
,因为算法a选择的是结尾最先结束且不重合的区间,如果算法a不正确,又因为区间数组中的区间是固定的,则其他算法c肯定存在d>b
的情况。b<d
,所以不影响区间C后面区间的结果。所以我们选择了区间A也构成了一个最优解。而我们假设的是选择区间A不是最优解,所以和之前的假设矛盾,所以算法a是正确的贪心算法有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中pointsi = xstart, xend 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。示例 1:输入:points = [10,16,2,8,1,6,7,12] 输出:2 解释:气球可以用2支箭来爆破: -在x = 6处射出箭,击破气球2,8和1,6。 -在x = 11处发射箭,击破气球10,16和7,12。 示例 2:输入:points = [1,2,3,4,5,6,7,8] 输出:4 解释:每个气球需要射出一支箭,总共需要4支箭。 示例 3:输入:points = [1,2,2,3,3,4,4,5] 输出:2 解释:气球可以用2支箭来爆破:在x = 2处发射箭,击破气球1,2和2,3。 在x = 4处射出箭,击破气球3,4和4,5。 提示:1 <= points.length <= 105 pointsi.length == 2 -231 <= xstart < xend <= 231 - 1
O(nlogn)
,排序的复杂度O(nlogn)
,循环数组的复杂度O(n)
。空间复杂度O(logn)
,排序栈空间js:
var findMinArrowShots = function (points) {
if (!points.length) {
return 0;
}
points.sort((a, b) => a[1] - b[1]); //按照区间结尾排序
let pos = points[0][1];
let ans = 1;
for (let balloon of points) {
if (balloon[0] > pos) {
//如果后面一个区间的开始大于前一个区间的结尾 就需要新增一支箭
pos = balloon[1]; //更新pos为新的区间的结尾
ans++;
}
}
return ans;
};
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i,都有一个胃口值 gi,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 sj 。如果 sj >= gi,我们可以将这个饼干 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.
O(mlogm + nlogn)
。空间复杂度O(logm + logn)
js:
var findContentChildren = function (g, s) {
g = g.sort((a, b) => a - b);
s = s.sort((a, b) => a - b); //排序数组
let result = 0;
let index = s.length - 1;
for (let i = g.length - 1; i >= 0; i--) {
//从胃口大的小孩开始满足
if (index >= 0 && s[index] >= g[i]) {
result++; //结果加1
index--;
}
}
return result;
};
给你一个整数数组 prices ,其中 pricesi 表示某支股票第 i 天的价格。在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。返回 你能获得的 最大 利润 。示例 1:输入:prices = 7,1,5,3,6,4 输出:7 解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。 总利润为 4 + 3 = 7 。 示例 2:输入:prices = 1,2,3,4,5 输出:4 解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。 总利润为 4 。 示例 3:输入:prices = 7,6,4,3,1 输出:0 解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。提示:1 <= prices.length <= 3 * 104 0 <= pricesi <= 104
dp[i][0]
表示第 i天交易完后手里没有股票的最大利润,dp[i][1]
表示第 i天交易完后手里持有一支股票的最大利润,接下来就是定义状态转移方程:dp[i][0]
,表示手中没股票,则可由前一天的两种情况转移过来,第一种是dp[i-1][0]
,表示前一天手里没股票,而且今天没做任何操作。第二种是dp[i-1][1]
,表示前一天持有股票,但是今天卖了,所以收益是dp[i-1][1]+prices[i]
,我们需要求出这两种情况下的最大值就是最大利润,状态转移方程就是: `dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);`
dp[i][1]
,表示手中有股票,则可由前一天的两种情况转移过来,第一种是dp[i−1][1]
,表示前一天手中有股票,即是今天没做任何操作。第二种是dp[i−1][0]
,表示前一天没有股票,但是今天买进了,所以收益是dp[i-1][1]-prices[i]
,我们需要求出这两种情况下的最大值就是最大利润,状态转移方程就是: `dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);`
由上面的状态转移方程我们知道,当前天的最大收益,只与前一天的状态相关,所以我们可以不用定义二维数组来存放状态,只需要将dp[i - 1][0]
,dp[i - 1][1]
存放在变量中。
O(n)
,n是数组长度,每天有持有股票或者没持有两种状态,一共2n的状态转移次数,时间复杂度就是O(2n)
,时间复杂度和常系数无关,所以时间复杂度就是O(n)
。空间复杂度O(n)
,因为要开辟n的空间存放状态,虽然是二维数组,但是第二维是常数。如果进行了状态压缩,空间复杂度可以优化到O(1)js:
var maxProfit = function (prices) {
const n = prices.length;
const dp = new Array(n).fill(0).map((v) => new Array(2).fill(0)); //初始化状态数组
(dp[0][0] = 0), (dp[0][1] = -prices[0]); //3.定义初始值
for (let i = 1; i < n; ++i) {
//1.确定状态
//2.推导状态转移方程
//当前没持有股票,可由前一天的两种状态转移过了,
//1是前一天没持有,今天不动,2是前一天持有,今天卖掉,求这两种情况的较大值
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
//当前持有股票,可由前一天的两种状态转移过了,
//1是前一天持有,今天不动,2是前一天没持有,今天买入,求这两种情况的较大值
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
}
//4.确定输出值
return dp[n - 1][0]; //返回第n-1天的最大值
};
//空间压缩
var maxProfit = function (prices) {
const n = prices.length;
let dp0 = 0,
dp1 = -prices[0];
for (let i = 1; i < n; ++i) {
let newDp0 = Math.max(dp0, dp1 + prices[i]);
let newDp1 = Math.max(dp1, dp0 - prices[i]);
dp0 = newDp0;
dp1 = newDp1;
}
return dp0;
};
O(n)
,n是数组的长度。空间复杂度是O(1)
js:
var maxProfit = function (prices) {
let ans = 0;
let n = prices.length;
for (let i = 1; i < n; ++i) {
//今天价格和昨天的差值是否为正,如果为正累加进去,为负则加0
ans += Math.max(0, prices[i] - prices[i - 1]);
}
return ans;
};
视频讲解:传送门
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。