0%

算法-贪心

贪心相关算法

给定一个数组arr,代表每个人的能力值。再给定一个非负数k,如果两个人能力差值正好为k,那么可以凑在一起比赛,一局比赛只有两个人,返回最多可以同时有多少场比赛

微信图片_20220427021521
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
public int combinePair(int[] arr, int k) {
if (k < 0 || arr == null || arr.length < 2) {
return 0;
}
Arrays.sort(arr);
int pairNum = 0;
int L = 0;
int R = 0;
int N = arr.length;
Set<Integer> usedIndex = new HashSet<>();
while (L < N && R < N) {
if (usedIndex.contains(L)) {
L ++;
continue;
}
if (arr[R] - arr[L] > k) {
L ++;
} else if (arr[R] - arr[L] < k) {
R ++;
} else {
usedIndex.add(R);
pairNum ++;
L ++;
R ++;
}
}
return pairNum;
}

给定一个正数数组arr,代表若干人的体重,再给定一个正数limit,表示所有船共同拥有的载重量,每艘船最多坐两人,且不能超过载重。让所有的人同时过河,并且用最好的分配方法让船尽量少,返回最少的船

https://leetcode-cn.com/problems/boats-to-save-people/

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
33
34
35
36
37
38
39
40
public int numRescueBoats(int[] people, int limit) {
if (people == null || people.length == 0) {
return 0;
}
Arrays.sort(people);
// 二分查找 limit/2
int halfLimit = limit >> 1;
int N = people.length;
int l = 0;
int r = 0;
int mid = 0;
for (int i = 0; i < N; i++) {
if (people[i] > halfLimit) {
l = i - 1;
r = i;
mid = i;
break;
}
}
// 全都比 limit/2 大
if (l < 0) {
return N;
}
// 全都比 Limit/2 小
if (r == 0) {
return ((N + 1) >> 1);
}
int noUseNum = 0;
// 从 limit/2 两边出发,向左右移动
while (l >= 0 && r < N) {
if (people[l] + people[r] <= limit) {
l --;
r ++;
} else if (people[l] + people[r] > limit) {
l --;
noUseNum ++;
}
}
return ((l + 1 + noUseNum + 1) >> 1) + r - mid + N - r;
}

给定一个非负数组成的数组,长度一定大于1,想知道数组中哪两个数&的结果最大,返回这个最大结果。要求时间复杂度O(N),额外空间复杂度O(1)

思路:贪心选择高位与运算结果为1的数字,如果正好有2个数字的高位为1,直接返回;如果小于2个,则该高位无法与出1,看下一位;如果大于2个,则看这些数的下一位哪些是1,直到第0位

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 int maxAndValue(int[] arr) {
int N = arr.length;
for (int i = 30; i >= 0 ; i--) {
// 0...N-1为有效区,N-arr.lenth-1为淘汰区
int tmp = N;
for (int j = 0; j < N; ) {
if (((arr[j] >> i) & 1) == 0) {
swap(arr, j, --N);
} else {
j++;
}
}
// 从符号位下一位开始,找最高位为1的数字,如果只有一个,则找下一个高位
if (N < 2) {
N = tmp;
}
// 如果有两个,则返回这两个数字与的结果
if (N == 2) {
return arr[0] & arr[1];
}
// 如果有大于两个,在向下一个高位寻找
// next loop
}
// 循环结束,没有退出,说明循环中没有发现N==2的情况,总之arr[0]与arr[1]是有效区的数字
return arr[0] & arr[1];
}

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

376. 摆动序列

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

1
2
3
4
输入: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)
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];
//如果当前差值和上一个差值为一正一负
//等于0的情况表示初始时的preDiff
if ((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) {
count++;
preDiff = curDiff;
}
}
return count;
}
}

122. 买卖股票的最佳时机 II【记】

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润

1
2
3
4
5
输入: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
1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public int maxProfit(int[] prices) {
int maxProfit = 0;
for (int i = 1; i < prices.length; i++) {
int diff = prices[i] - prices[i-1];
if (diff > 0) {
maxProfit += diff;
}
}
return maxProfit;
}
}

55. 跳跃游戏

给定一个非负整数数组 nums ,你最初位于数组的 第一个下标

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标。

1
2
3
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public boolean canJump(int[] nums) {
if (nums.length == 1) {
return true;
}
int right = 0;
for (int i = 0; i <= right; i++) {
if (nums[i] + i > right) {
right = nums[i] + i;
if (right >= nums.length - 1) {
return true;
}
}
}
return false;
}
}

45. 跳跃游戏 II

给你一个非负整数数组 nums ,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

假设你总是可以到达数组的最后一个位置

1
2
3
4
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
  从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置
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 right = 0;
int step = 0;
int maxStep = Integer.MIN_VALUE;
for (int i = 0; i <= right; i++) {
if (right >= nums.length - 1) {
return step;
}
maxStep = Math.max(maxStep, nums[i] + i);
if (i == right) {
right = maxStep;
step ++;
}
}
return step;
}
}

134. 加油站

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

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

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

1
2
输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
输出: 3

思路:

局部贪心:任意两个加油站能行驶一周,要求油总量减去总消耗大于等于0;每个加油站的剩余量rest[i]为gas[i] - cost[i]。i 从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,起始位置从i+1算起,再从0计算curSum。

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
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int totalGas = 0;
int totalCost = 0;
int[] gasLeft = new int[gas.length];
for (int i = 0; i < gas.length; i++) {
totalGas += gas[i];
totalCost += cost[i];
gasLeft[i] = gas[i] - cost[i];
}
if (totalCost > totalGas) {
return -1;
}
int totalGasLeft = 0;
int start = 0;
for (int i = 0; i < gasLeft.length; i++) {
totalGasLeft += gasLeft[i];
if (totalGasLeft < 0) {
totalGasLeft = 0;
start = i + 1;
}
}
return start;
}
}

135. 分发糖果【记】

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。

你需要按照以下要求,给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
  • 相邻两个孩子评分更高的孩子会获得更多的糖果。

请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目

1
2
3
输入:ratings = [1,0,2]
输出:5
解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。

思路:先从左向右满足条件,再从右向左满足条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public int candy(int[] ratings) {
int[] candyNum = new int[ratings.length];
Arrays.fill(candyNum, 1);
for (int i = 1; i < ratings.length; i++) {
if (ratings[i] > ratings[i-1]) {
candyNum[i] = candyNum[i-1] + 1;
}
}
for (int i = ratings.length - 2; i >= 0 ; i--) {
if (ratings[i] > ratings[i+1] && candyNum[i] <= candyNum[i+1]) {
candyNum[i] = candyNum[i+1] + 1;
}
}
int totalCandyNum = 0;
for (int i = 0; i < candyNum.length; i++) {
totalCandyNum += candyNum[i];
}
return totalCandyNum;
}
}

860. 柠檬水找零

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false

1
2
输入:bills = [5,5,5,10,20]
输出:true
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
class Solution {
public boolean lemonadeChange(int[] bills) {
int five = 0;
int ten = 0;
for (int i = 0; i < bills.length; i++) {
if (bills[i] == 5) {
five ++;
}
else if (bills[i] == 10) {
five--;
ten ++;
}
else if (bills[i] == 20) {
if (ten > 0) {
five--;
ten--;
} else {
five -= 3;
}
}
if (five < 0 || ten < 0) {
return false;
}
}
return true;
}
}

406. 根据身高重建队列【技】

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好ki 个身高大于或等于 hi 的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)

1
2
输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int[][] reconstructQueue(int[][] people) {
// 先按照身高降序排序,如果身高相同,按照k升序排序
Arrays.sort(people, (o1, o2) -> {
if (o1[0] == o2[0]) {
return o1[1] - o2[1];
}
return o2[0] - o1[0];
});
// 身高从高到低,依次维护队列关系
LinkedList<int[]> que = new LinkedList<>();
for(int[] p : people) {
que.add(p[1], p);
}
return que.toArray(new int[people.length][]);
}

注:技巧性题目,考的可能性不大

452. 用最少数量的箭引爆气球

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstartxend之间的气球。你不知道气球的确切 y 坐标。

一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 x``startx``end, 且满足 xstart ≤ x ≤ x``end,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。

给你一个数组 points返回引爆所有气球所必须射出的 最小 弓箭数

1
2
3
4
5
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
-在x = 6处射出箭,击破气球[2,8]和[1,6]。
-在x = 11处发射箭,击破气球[10,16]和[7,12]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
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 0;
}
return o1[1] > o2[1] ? 1 : -1;
}
});
int res = 1;
for (int i = 1; i < points.length; i++) {
if (points[i][0] > points[i-1][1]) {
res ++;
} else {
points[i][1] = points[i-1][1];
}
}
return res;
}
}

注:排序函数如下写法,会导致整型越界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void integerTest() {
int[][] points = new int[][]{{1, 5}, {3, 2}, {4, Integer.MAX_VALUE}, {2, Integer.MIN_VALUE}};
Arrays.sort(points, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[1] - o2[1];
}
});
System.out.println(Arrays.deepToString(points));
// [[3, 2], [1, 5], [4, 2147483647], [2, -2147483648]]
System.out.println(Integer.MAX_VALUE - Integer.MIN_VALUE);
// -1
}

435. 无重叠区间

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

1
2
3
输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort(intervals, (o1, o2) -> {
if (o1[1] == o2[1]) {
return 0;
}
return o1[1] - o2[1] > 0 ? 1 : -1;
});
int noLapNum = 1;
int noLapIndex = 0;
// 保留右端点小的,给右侧尽可能留空间
for (int i = 1; i < intervals.length; i++) {
if (intervals[i][0] >= intervals[noLapIndex][1]) {
noLapNum ++;
noLapIndex = i;
}
}
return intervals.length - noLapNum;
}
}

763. 划分字母区间

字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表

1
2
3
4
输入:S = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca", "defegde", "hijhklij"
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
class Solution {
public List<Integer> partitionLabels(String s) {
List<Integer> res = new ArrayList<>();
if (s == null || s.isEmpty()) {
return res;
}
char[] charArr = s.toCharArray();
int[][] charIndex = new int[26][2];
// 计算每个字符出现的第一个位置和最后一个位置
for (int i = 0; i < charArr.length; i++) {
if (charIndex[charArr[i] - 'a'][0] == 0) {
charIndex[charArr[i] - 'a'][0] = i;
}
charIndex[charArr[i] - 'a'][1] = i;
}
int start = 0;
int end = charIndex[charArr[0] - 'a'][1];
for (int i = 0; i < charArr.length; i++) {
int t = charIndex[charArr[i] - 'a'][1];
if (i == end) {
res.add(end - start + 1);
start = end + 1;
if (start < charArr.length) {
end = charIndex[charArr[start] - 'a'][1];
}
} else if (t > end) {
end = t;
}
}
return res;
}
}

56. 合并区间【记】

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间

1
2
3
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int[][] merge(int[][] intervals) {
List<int[]> res = new LinkedList<>();
Arrays.sort(intervals, (o1, o2) -> Integer.compare(o1[0], o2[0]));
int start = intervals[0][0];
for (int i = 1; i < intervals.length; i++) {
if (intervals[i][0] > intervals[i-1][1]) {
res.add(new int[]{start, intervals[i-1][1]});
start = intervals[i][0];
} else {
intervals[i][1] = Math.max(intervals[i][1], intervals[i-1][1]);
}
}
res.add(new int[]{start, intervals[intervals.length - 1][1]});
return res.toArray(new int[res.size()][]);
}
}

注:先按左端点升序排序,再从左向右判断是否重合

738. 单调递增的数字【技】

当且仅当每个相邻位数上的数字 xy 满足 x <= y 时,我们称这个整数是单调递增的。

给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增

1
2
输入: n = 332
输出: 299
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
class Solution {
public int monotoneIncreasingDigits(int n) {
String s = n + "";
int[] arr = new int[s.length()];
for (int i = 0; i < s.length(); i++) {
arr[i] = s.charAt(i) - '0';
}
for (int i = arr.length - 1; i >= 1; i--) {
if (arr[i] >= arr[i-1]) {
continue;
}
arr[i] = 9;
arr[i-1] --;
}
int res = arr[0];
for (int i = 1; i < arr.length; i++) {
// 如果最后存在不符合递增的,一律置为9
if (arr[i] < arr[i-1]) {
arr[i] = 9;
}
res = res * 10 + arr[i];
}
return res;
}
}

对于 3332,先对最后两位判断是否单调递增,32改为29,整体变为3329,向左移,变为3299,继续左移,2999;对于100,先变为100,再变为090,最后将最后的0变为9