0%

Greedy

Greedy Algorithm选编

代码随想录

贪心法原理

贪心的本质是选择每一阶段的局部最优,从而达到全局最优

这么说有点抽象,来举一个例子:

例如,有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿?

指定每次拿最大的,最终结果就是拿走最大数额的钱。

每次拿最大的就是局部最优,最后拿走最大数额的钱就是推出全局最优。

再举一个例子如果是 有一堆盒子,你有一个背包体积为n,如何把背包尽可能装满,如果还每次选最大的盒子,就不行了。这时候就需要动态规划。

55. Jump Game

法一:DP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public boolean canJump(int[] nums) {
int n=nums.length;
boolean[] dp=new boolean[n];
dp[0]=true;
for (int i = 0; i < n-1; i++) {
if(dp[i]){
for (int j = 1; j <= nums[i]; j++) {
if(i+j>=n){
break;
}
dp[i+j]=true;
}
}
}
return dp[n-1];
}

法二:Greedy

贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点

1
2
3
4
5
6
7
8
9
10
public boolean canJump(int[] nums) {
int cover=0;
for (int i = 0; i <= cover; i++) {
cover=Math.max(cover,i+nums[i]);
if(cover>=nums.length-1){
return true;
}
}
return false;
}

45. Jump Game II

You can assume that you can always reach the last index.

法一:DP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class LeetCode45 {
public int jump(int[] nums) {
int n=nums.length;
int[] dp=new int[n];
for (int i = 1; i <= nums[0]; i++) {
if(i>=n){
break;
}
dp[i]=1;
}
for (int i = 1; i < n-1; i++) {
if(dp[i]>0){
for (int j = 1; j <= nums[i]; j++) {
if(i+j>=n){
break;
}
if(dp[i+j]==0){//第一次抵达则赋初值
dp[i+j]=dp[i]+1;
}else{//之前已抵达过则保留最小值(次数)
dp[i+j]=Math.min(dp[i+j],dp[i]+1);
}
}
}
}
return dp[n-1];
}
}

法二:Greedy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public int jump(int[] nums) {
int result = 0;
// 当前覆盖的最远距离下标
int end = 0;
// 下一步覆盖的最远距离下标
int temp = 0;
for (int i = 0; i <= end && end < nums.length - 1; ++i) {
temp = Math.max(temp, i + nums[i]); //更新再跳一步的最远距离
// 可达位置的改变次数就是跳跃次数
if (i == end) { //i已经到达当前范围极限,且当前范围还没覆盖到终点,必须再跳一步
end = temp;
result++;
}
}
return result;
}
}

135. Candy

  • Each child must have at least one candy.
  • Children with a higher rating get more candies than their neighbors.

好题!

先从左向右,再从右向左

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LeetCode135 {
public int candy(int[] ratings) {
int n=ratings.length;
int[] arr=new int[n];
Arrays.fill(arr,1);
//先满足大于左孩子,正序
for (int i = 1; i < n; i++) {
if(ratings[i]>ratings[i-1]){
arr[i]=arr[i-1]+1;
}
}
//在此基础上,再满足大于右孩子,逆序
for (int i = n-2; i >= 0; i--) {
if(ratings[i]>ratings[i+1]){
arr[i]=Math.max(arr[i+1]+1,arr[i]);
}
}
return Arrays.stream(arr).sum();
}
}

860. Lemonade Change

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class LeetCode860 {
public boolean lemonadeChange(int[] bills) {
int five=0;
int ten=0;
for (int bill : bills) {
if(bill==5){
five++;
}else if(bill==10){
if(five==0){
return false;
}
five--;
ten++;
}else{
//Greedy: 有ten优先找ten,因为five更万能
if(ten>=1){
ten--;
}else if(five>=2){
five-=2;
}else{
return false;
}
if(five>0){
five--;
}else{
return false;
}
}
}
return true;
}
}

1005. Maximize Sum Of Array After K Negations

那么本题的解题步骤为:

  • 第一步:将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
  • 第二步:从前向后遍历,遇到负数将其变为正数,同时K–
  • 第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
  • 第四步:求和
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public int largestSumAfterKNegations(int[] nums, int k) {
// 将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
nums = IntStream.of(nums)
.boxed()
.sorted((o1, o2) -> Math.abs(o2) - Math.abs(o1))
.mapToInt(Integer::intValue).toArray();
int n=nums.length;
for (int i = 0; i < n; i++) {
if(nums[i]<0){
nums[i]=-nums[i];
k--;
if(k==0){
break;
}
}
}
if((k&1)==1){
nums[n-1]=-nums[n-1];
}
return Arrays.stream(nums).sum();
}

134. Gas Station

法一:暴力模拟

超时!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int canCompleteCircuit(int[] gas, int[] cost) {
int n=gas.length;
for (int i = 0; i < n; i++) {
int rest=gas[i]-cost[i];
int index=(i+1)%n;
while(rest>=0 && index!=i){
rest+=gas[index];
rest-=cost[index];
index=(index+1)%n;
}
if(rest>=0 && index==i){
return i;
}
}
return -1;
}

法二:Greedy

  • 局部最优:当前累加rest[j]的和curSum一旦小于0,起始位置至少要是j+1,因为从j开始一定不行。

  • 全局最优:找到可以跑一圈的起始位置

[^心路历程]: 有一个环形路上有n个站点; 每个站点都有一个好人或一个坏人; 好人会给你钱,坏人会收你一定的过路费,如果你带的钱不够付过路费,坏人会跳起来把你砍死; 问:从哪个站点出发,能绕一圈活着回到出发点?首先考虑一种情况:如果全部好人给你 的钱加起来 小于 坏人收的过路费之和,那么总有一次你的钱不够付过路费,你的结局注定会被砍死。假如你随机选一点 start 出发,那么你肯定会选一个有好人的站点开始,因为开始的时候你没有钱,遇到坏人只能被砍死;现在你在start出发,走到了某个站点end,被end站点的坏人砍死了,说明你在 [start, end) 存的钱不够付 end点坏人的过路费,因为start站点是个好人,所以在 (start, end) 里任何一点出发,你存的钱会比现在还少,还是会被end站点的坏人砍死;于是你重新读档,聪明的选择从 end+1点出发,继续你悲壮的征程; 终于有一天,你发现自己走到了尽头(下标是n-1)的站点而没有被砍死; 此时你犹豫了一下,那我继续往前走,身上的钱够不够你继续走到出发点Start?当然可以,因为开始已经判断过,好人给你的钱数是大于等于坏人要的过路费的,你现在攒的钱完全可以应付 [0, start) 这一段坏人向你收的过路费。(此处存疑) 这时候你的嘴角微微上扬,眼眶微微湿润,因为你已经知道这个世界的终极奥秘:Start就是这个问题的答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int canCompleteCircuit(int[] gas, int[] cost) {
int n=gas.length;
int cur=0;
int total=0;
int start=0;
for (int i = 0; i < n; i++) {
cur+=gas[i]-cost[i];
total+=gas[i]-cost[i];
if(cur<0){
start=i+1;
cur=0;
}
}
if(total<0){
return -1;
}
return start; //If there exists a solution, it is guaranteed to be unique
}

376. Wiggle Subsequence

Greedy解法

局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值

整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列

局部最优推出全局最优,并举不出反例,那么试试贪心!

(为方便表述,以下说的峰值都是指局部峰值)

实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)

这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public int wiggleMaxLength(int[] nums) {
if (nums.length <= 1) {
return nums.length;
}
//当前差值
int curDiff = 0;
//上一个差值
int preDiff = 0;
int count = 1;
for (int i = 1; i < nums.length; i++) {
//得到当前差值
curDiff = nums[i] - nums[i - 1];
//直到当前差值和上一个差值为一正一负,此时wiggle sequence长度增加,更新preDiff和currDiff,并且count++
//等于0的情况表示初始时的preDiff
if ((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) {
count++;
preDiff = curDiff;
}
}
return count;
}
}
  • 时间复杂度:$O(n)$
  • 空间复杂度:$O(1)$

Dynamic Programming基础版

考虑用动态规划的思想来解决这个问题。

很容易可以发现,对于我们当前考虑的这个数,要么是作为山峰(即nums[i] > nums[i-1]),要么是作为山谷(即nums[i] < nums[i - 1])。

  • 设dp状态dp[i][0],表示考虑前i个数,第i个数作为山峰的摆动子序列的最长长度
  • 设dp状态dp[i][1],表示考虑前i个数,第i个数作为山谷的摆动子序列的最长长度

则转移方程为:

  • dp[i][0] = max(dp[i][0], dp[j][1] + 1),其中0 < j < inums[j] < nums[i],表示将nums[i]接到前面某个山谷后面,作为山峰。
  • dp[i][1] = max(dp[i][1], dp[j][0] + 1),其中0 < j < inums[j] > nums[i],表示将nums[i]接到前面某个山峰后面,作为山谷。

初始状态:

由于一个数可以接到前面的某个数后面,也可以以自身为子序列的起点,所以初始状态为:dp[0][0] = dp[0][1] = 1

  • 时间复杂度:$O(n^2)$
  • 空间复杂度:$O(n)$

Dynamic Programming进阶版

可以用两棵线段树来维护区间的最大值

  • 每次更新dp[i][0],则在tree1nums[i]位置值更新为dp[i][0]
  • 每次更新dp[i][1],则在tree2nums[i]位置值更新为dp[i][1]
  • 则dp转移方程中就没有必要j从0遍历到i-1,可以直接在线段树中查询指定区间的值即可。

时间复杂度:$O(n\log n)$

空间复杂度:$O(n)$

53. Maximum Subarray

Greedy解法

局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。

全局最优:选取最大“连续和”

局部最优的情况下,并记录最大的“连续和”,可以推出全局最优

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int maxSubArray(int[] nums) {
int n=nums.length;
int max=Integer.MIN_VALUE;
int count=0;
for (int i = 0; i < n; i++) {
count+=nums[i];
if(count>max){
max=count;
}
if(count<0){
count=0;
}
}
return max;
}

DP解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public int maxSubArray(int[] nums) {
int n=nums.length;
int[] dp=new int[n];
dp[0]=nums[0];
int max=dp[0];
for(int i=1; i<n; i++){
if(dp[i-1]>0){
dp[i]=dp[i-1]+nums[i];
}else{
dp[i]=nums[i];
}
max=Math.max(max,dp[i]);
}

return max;
}
}

labuladong

什么是贪心算法呢?贪心算法可以认为是动态规划算法的一个特例,相比动态规划,使用贪心算法需要满足更多的条件(贪心选择性质),但是效率比动态规划要高。

比如说一个算法问题使用暴力解法需要指数级时间,如果能使用动态规划消除重叠子问题,就可以降到多项式级别的时间,如果满足贪心选择性质,那么可以进一步降低时间复杂度,达到线性级别的。

什么是贪心选择性质呢,简单说就是:每一步都做出一个局部最优的选择,最终的结果就是全局最优。注意哦,这是一种特殊性质,其实只有一部分问题拥有这个性质。

比如你面前放着 100 张人民币,你只能拿十张,怎么才能拿最多的面额?显然每次选择剩下钞票中面值最大的一张,最后你的选择一定是最优的。

然而,大部分问题明显不具有贪心选择性质。比如打斗地主,对手出对儿三,按照贪心策略,你应该出尽可能小的牌刚好压制住对方,但现实情况我们甚至可能会出王炸。这种情况就不能用贪心算法,而得使用动态规划解决。

435. Non-overlapping Intervals

区间调度问题 Interval Scheduling

最多有几个不冲突的区间?

正确的思路其实很简单,可以分为以下三步:

  1. 从区间集合 intvs 中选择一个区间 x,这个 x 是在当前所有区间中结束最早的end 最小)。

  2. 把所有与 x 区间相交的区间从区间集合 intvs 中删除。

  3. 重复步骤 1 和 2,直到 intvs 为空为止。之前选出的那些 x 就是最大不相交子集。

make the rest of the intervals non-overlapping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LeetCode435 {
public int eraseOverlapIntervals(int[][] intervals) {
int n=intervals.length;
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[1]-o2[1];
}
});
int count=1;
int end=intervals[0][1];
for (int[] interval : intervals) {
int start=interval[0];
if(start>=end){//已由end升序;若start<end,则重叠,不选取(从集合中删去);否则选为新的区间x
count++;
end=interval[1];
}
}
return n-count;
}
}

注意:

1
2
3
4
5
6
7
8
9
10
11
12
Arrays.sort(intvs, new Comparator<int[]>() {
public int compare(int[] a, int[] b) {
return a[1] - b[1];
}
});
//以上写法可能出现溢出
//以下写法更保险
Arrays.sort(points, (o1,o2)->{
if(o1[1]>o2[1]) return 1;
if(o1[1]<o2[1]) return -1;
return 0;
});

452. Minimum Number of Arrows to Burst Balloons

依然是求最大不重叠区间数

每个不重叠区间都需要一个箭,每多一个不重叠区间,就多需要一根箭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class LeetCode452 {
public int findMinArrowShots(int[][] points) {
Arrays.sort(points, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
if(o1[1]>o2[1]){
return 1;
}
if(o1[1]<o2[1]){
return -1;
}
return 0;
}
});
int count=1;
int end=points[0][1];
for (int[] point : points) {
int start=point[0];
if(start>end){
count++;
end=point[1];
}
}
return count;
}
}

自选

605. Can Place Flowers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public boolean canPlaceFlowers(int[] flowerbed, int n) {
if(n==0){
return true;
}
int len=flowerbed.length;
int count=0;
for (int i = 0; i < len; i++) {
if(flowerbed[i]==1){
continue;
}
boolean leftPlanted=i>0 && flowerbed[i-1]==1;
boolean rightPlanted=i<len-1 && flowerbed[i+1]==1;
if(!leftPlanted && !rightPlanted){
flowerbed[i]=1;
count++;
if(count>=n){
return true;
}
}
}
return false;
}

2340. Minimum Adjacent Swaps to Make a Valid Array

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int minimumSwaps(int[] nums) {
int n=nums.length;
int min=0;
int max=n-1;
for (int i = 1; i < n; i++) {
if(nums[i]<nums[min]){
min=i;
}
if(nums[n-1-i]>nums[max]){
max=n-1-i;
}
}
return min-0+n-1-max-(min>max ? 1 : 0);
}

280. Wiggle Sort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void wiggleSort(int[] nums) {
int n=nums.length;
if(n==1){
return;
}
for (int i = 0; i < n-1; i++) {
//even<=odd>=even
if(i%2==0 && nums[i]>nums[i+1]
|| i%2==1 && nums[i]<nums[i+1]){
swap(nums,i,i+1);
}
}
}

void swap(int[] arr, int i, int j){
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}

1564. Put Boxes Into the Warehouse I

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int maxBoxesInWarehouse(int[] boxes, int[] warehouse) {
Arrays.sort(boxes);
Deque<Integer> stack=new LinkedList<>();
stack.push(warehouse[0]);
for (int i = 1; i < warehouse.length; i++) {
stack.push(Math.min(warehouse[i],stack.peek()));
}
int res=0;
for (int box : boxes) {
if(stack.isEmpty()){
break;
}
while(!stack.isEmpty() && stack.peek()<box){
stack.pop();
}
if(!stack.isEmpty()){
res++;
stack.pop();
}
}
return res;
}

自己想的还不够贪!

思路:

  • the edge is the largest space we can offer
  • if this box can’t fit at the edge, it can fit nowhere
  • try to fit from the largest box
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int maxBoxesInWarehouse(int[] boxes, int[] warehouse) {
int res=0;
int idx=0;
Arrays.sort(boxes);
for(int i=boxes.length-1; i>=0; i--){
if(idx==warehouse.length){
break;
}
if(boxes[i]<=warehouse[idx]){
res++;
idx++;
}
}
return res;
}

1580. Put Boxes Into the Warehouse II

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int maxBoxesInWarehouse(int[] boxes, int[] warehouse) {
int res=0;
Arrays.sort(boxes);
int p1=0;
int p2=warehouse.length-1;
for (int i = boxes.length - 1; i >= 0 && p1<=p2; i--) {
if(boxes[i]<=warehouse[p1]){
res++;
p1++;
}else if(boxes[i]<=warehouse[p2]){
res++;
p2--;
}
}
return res;
}

2439. Minimize Maximum of Array

1
2
3
4
5
6
7
8
9
10
public int minimizeArrayValue(int[] nums) {
int n=nums.length;
long ans=0;
long sum=0;
for (int i = 0; i < n; i++) {
sum+=nums[i];
ans=Math.max(ans,(sum+i)/(i+1));
}
return (int)ans;
}