广度优先搜索
标签: medium
2391.收集垃圾的最少总时间
目标
给你一个下标从 0 开始的字符串数组 garbage ,其中 garbage[i] 表示第 i 个房子的垃圾集合。garbage[i] 只包含字符 'M' ,'P' 和 'G' ,但可能包含多个相同字符,每个字符分别表示一单位的金属、纸和玻璃。垃圾车收拾 一 单位的任何一种垃圾都需要花费 1 分钟。
同时给你一个下标从 0 开始的整数数组 travel ,其中 travel[i] 是垃圾车从房子 i 行驶到房子 i + 1 需要的分钟数。
城市里总共有三辆垃圾车,分别收拾三种垃圾。每辆垃圾车都从房子 0 出发,按顺序 到达每一栋房子。但它们 不是必须 到达所有的房子。
任何时刻只有 一辆 垃圾车处在使用状态。当一辆垃圾车在行驶或者收拾垃圾的时候,另外两辆车 不能 做任何事情。
请你返回收拾完所有垃圾需要花费的 最少 总分钟数。
示例 1:
输入:garbage = ["G","P","GP","GG"], travel = [2,4,3]
输出:21
解释:
收拾纸的垃圾车:
1. 从房子 0 行驶到房子 1
2. 收拾房子 1 的纸垃圾
3. 从房子 1 行驶到房子 2
4. 收拾房子 2 的纸垃圾
收拾纸的垃圾车总共花费 8 分钟收拾完所有的纸垃圾。
收拾玻璃的垃圾车:
1. 收拾房子 0 的玻璃垃圾
2. 从房子 0 行驶到房子 1
3. 从房子 1 行驶到房子 2
4. 收拾房子 2 的玻璃垃圾
5. 从房子 2 行驶到房子 3
6. 收拾房子 3 的玻璃垃圾
收拾玻璃的垃圾车总共花费 13 分钟收拾完所有的玻璃垃圾。
由于没有金属垃圾,收拾金属的垃圾车不需要花费任何时间。
所以总共花费 8 + 13 = 21 分钟收拾完所有垃圾。
示例 2:
输入:garbage = ["MMM","PGM","GP"], travel = [3,10]
输出:37
解释:
收拾金属的垃圾车花费 7 分钟收拾完所有的金属垃圾。
收拾纸的垃圾车花费 15 分钟收拾完所有的纸垃圾。
收拾玻璃的垃圾车花费 15 分钟收拾完所有的玻璃垃圾。
总共花费 7 + 15 + 15 = 37 分钟收拾完所有的垃圾。
说明:
- 2 <= garbage.length <= 10^5
- garbage[i] 只包含字母 'M' ,'P' 和 'G' 。
- 1 <= garbage[i].length <= 10
- travel.length == garbage.length - 1
- 1 <= travel[i] <= 100
思路
暴力解法,计算每个房子的垃圾数以及回收各种垃圾需要到达的最远距离。
官网题解给出了一次遍历的解法。没时间看了。// todo
代码
/**
* @date 2024-05-11 8:37
*/
public class GarbageCollection2391 {
public int garbageCollection(String[] garbage, int[] travel) {
int res = 0;
int n = garbage.length;
int[] m = new int[n];
int[] p = new int[n];
int[] g = new int[n];
int[] t = new int[n-1];
t[0] = travel[0];
for (int i = 1; i < travel.length; i++) {
t[i] = t[i - 1] + travel[i];
}
for (char c : garbage[0].toCharArray()) {
if (c == 'M') {
m[0]++;
} else if (c == 'P') {
p[0]++;
} else if (c == 'G') {
g[0]++;
}
}
int mEnd = 0;
int pEnd = 0;
int gEnd = 0;
for (int i = 1; i < n; i++) {
char[] chars = garbage[i].toCharArray();
for (char c : chars) {
if (c == 'M') {
m[i]++;
} else if (c == 'P') {
p[i]++;
} else if (c == 'G') {
g[i]++;
}
}
m[i] += m[i - 1];
p[i] += p[i - 1];
g[i] += g[i - 1];
if (m[i] != m[i - 1]) {
mEnd = i;
}
if (p[i] != p[i - 1]) {
pEnd = i;
}
if (g[i] != g[i - 1]) {
gEnd = i;
}
}
res += m[mEnd] + p[pEnd] + g[gEnd];
if (mEnd > 0) {
res += t[mEnd - 1];
}
if (pEnd > 0) {
res += t[pEnd - 1];
}
if (gEnd > 0) {
res += t[gEnd - 1];
}
return res;
}
}
性能
2105.给植物浇水II
目标
Alice 和 Bob 打算给花园里的 n 株植物浇水。植物排成一行,从左到右进行标记,编号从 0 到 n - 1 。其中,第 i 株植物的位置是 x = i 。
每一株植物都需要浇特定量的水。Alice 和 Bob 每人有一个水罐,最初是满的 。他们按下面描述的方式完成浇水:
- Alice 按 从左到右 的顺序给植物浇水,从植物 0 开始。Bob 按 从右到左 的顺序给植物浇水,从植物 n - 1 开始。他们 同时 给植物浇水。
- 如果没有足够的水 完全 浇灌下一株植物,他 / 她会立即重新灌满浇水罐。
- 不管植物需要多少水,浇水所耗费的时间都是一样的。
- 不能 提前重新灌满水罐。
- 每株植物都可以由 Alice 或者 Bob 来浇水。
- 如果 Alice 和 Bob 到达同一株植物,那么当前水罐中水更多的人会给这株植物浇水。如果他俩水量相同,那么 Alice 会给这株植物浇水。
给你一个下标从 0 开始的整数数组 plants ,数组由 n 个整数组成。其中,plants[i] 为第 i 株植物需要的水量。另有两个整数 capacityA 和 capacityB 分别表示 Alice 和 Bob 水罐的容量。返回两人浇灌所有植物过程中重新灌满水罐的 次数 。
示例 1:
输入:plants = [2,2,3,3], capacityA = 5, capacityB = 5
输出:1
解释:
- 最初,Alice 和 Bob 的水罐中各有 5 单元水。
- Alice 给植物 0 浇水,Bob 给植物 3 浇水。
- Alice 和 Bob 现在分别剩下 3 单元和 2 单元水。
- Alice 有足够的水给植物 1 ,所以她直接浇水。Bob 的水不够给植物 2 ,所以他先重新装满水,再浇水。
所以,两人浇灌所有植物过程中重新灌满水罐的次数 = 0 + 0 + 1 + 0 = 1 。
示例 2:
输入:plants = [2,2,3,3], capacityA = 3, capacityB = 4
输出:2
解释:
- 最初,Alice 的水罐中有 3 单元水,Bob 的水罐中有 4 单元水。
- Alice 给植物 0 浇水,Bob 给植物 3 浇水。
- Alice 和 Bob 现在都只有 1 单元水,并分别需要给植物 1 和植物 2 浇水。
- 由于他们的水量均不足以浇水,所以他们重新灌满水罐再进行浇水。
所以,两人浇灌所有植物过程中重新灌满水罐的次数 = 0 + 1 + 1 + 0 = 2 。
示例 3:
输入:plants = [5], capacityA = 10, capacityB = 8
输出:0
解释:
- 只有一株植物
- Alice 的水罐有 10 单元水,Bob 的水罐有 8 单元水。因此 Alice 的水罐中水更多,她会给这株植物浇水。
所以,两人浇灌所有植物过程中重新灌满水罐的次数 = 0 。
说明:
- n == plants.length
- 1 <= n <= 10^5
- 1 <= plants[i] <= 10^6
- max(plants[i]) <= capacityA, capacityB <= 10^9
思路
两个人同时从左右两边开始浇水,如果水不够可以立即装满,浇水耗时都相同,如果同时到达相同的植物则剩余水多的浇,问总重新灌水的次数。
由于不计算装水时间,浇水耗时又相同,两人同时从左右开始浇水,同时到达同一植物只有在总植物数量为奇数时才会发生。
先计算二人各自需要重新灌水的次数然后再考虑是否存在同时到达以及是否需要重新灌水即可。
需要注意,无论n的奇偶性, for (int i = 0; i < (n >> 1); i++) {} 与 for (int i = n - 1; i > (n - 1 >> 1); i--) {}
都不会访问到中间节点。
代码
/**
* @date 2024-05-09 8:40
*/
public class MinimumRefill2105 {
public int minimumRefill(int[] plants, int capacityA, int capacityB) {
int n = plants.length;
int remainderA = capacityA;
int remainderB = capacityB;
int res = 0;
for (int i = 0; i < (n >> 1); i++) {
if (remainderA < plants[i]) {
remainderA = capacityA - plants[i];
res++;
} else {
remainderA -= plants[i];
}
}
for (int i = n - 1; i > (n - 1 >> 1); i--) {
if (remainderB < plants[i]) {
remainderB = capacityB - plants[i];
res++;
} else {
remainderB -= plants[i];
}
}
if (n % 2 == 1) {
int remainder = Math.max(remainderA, remainderB);
if (remainder < plants[n >> 1]) {
res++;
}
}
return res;
}
}
性能
2079.给植物浇水
目标
你打算用一个水罐给花园里的 n 株植物浇水。植物排成一行,从左到右进行标记,编号从 0 到 n - 1 。其中,第 i 株植物的位置是 x = i 。x = -1 处有一条河,你可以在那里重新灌满你的水罐。
每一株植物都需要浇特定量的水。你将会按下面描述的方式完成浇水:
- 按从左到右的顺序给植物浇水。
- 在给当前植物浇完水之后,如果你没有足够的水 完全 浇灌下一株植物,那么你就需要返回河边重新装满水罐。
- 你 不能 提前重新灌满水罐。
最初,你在河边(也就是,x = -1),在 x 轴上每移动 一个单位 都需要 一步 。
给你一个下标从 0 开始的整数数组 plants ,数组由 n 个整数组成。其中,plants[i] 为第 i 株植物需要的水量。另有一个整数 capacity 表示水罐的容量,返回浇灌所有植物需要的 步数 。
示例 1:
输入:plants = [2,2,3,3], capacity = 5
输出:14
解释:从河边开始,此时水罐是装满的:
- 走到植物 0 (1 步) ,浇水。水罐中还有 3 单位的水。
- 走到植物 1 (1 步) ,浇水。水罐中还有 1 单位的水。
- 由于不能完全浇灌植物 2 ,回到河边取水 (2 步)。
- 走到植物 2 (3 步) ,浇水。水罐中还有 2 单位的水。
- 由于不能完全浇灌植物 3 ,回到河边取水 (3 步)。
- 走到植物 3 (4 步) ,浇水。
需要的步数是 = 1 + 1 + 2 + 3 + 3 + 4 = 14 。
示例 2:
输入:plants = [1,1,1,4,2,3], capacity = 4
输出:30
解释:从河边开始,此时水罐是装满的:
- 走到植物 0,1,2 (3 步) ,浇水。回到河边取水 (3 步)。
- 走到植物 3 (4 步) ,浇水。回到河边取水 (4 步)。
- 走到植物 4 (5 步) ,浇水。回到河边取水 (5 步)。
- 走到植物 5 (6 步) ,浇水。
需要的步数是 = 3 + 3 + 4 + 4 + 5 + 5 + 6 = 30 。
示例 3:
输入:plants = [7,7,7,7,7,7,7], capacity = 8
输出:49
解释:每次浇水都需要重新灌满水罐。
需要的步数是 = 1 + 1 + 2 + 2 + 3 + 3 + 4 + 4 + 5 + 5 + 6 + 6 + 7 = 49 。
说明:
- n == plants.length
- 1 <= n <= 1000
- 1 <= plants[i] <= 10^6
- max(plants[i]) <= capacity <= 10^9
思路
简单的动态规划。
代码
/**
* @date 2024-05-08 0:01
*/
public class WateringPlants2079 {
public int wateringPlants(int[] plants, int capacity) {
int n = plants.length;
int[] dp = new int[n];
int remainder = capacity - plants[0];
dp[0] = 1;
for (int i = 1; i < plants.length; i++) {
if (remainder >= plants[i]) {
remainder -= plants[i];
dp[i] = dp[i - 1] + 1;
} else {
remainder = capacity - plants[i];
dp[i] = dp[i - 1] + (i << 1) + 1;
}
}
return dp[n - 1];
}
}
性能
3.无重复字符的最长子串
略
2462.雇佣K位工人的总代价
目标
给你一个下标从 0 开始的整数数组 costs ,其中 costs[i] 是雇佣第 i 位工人的代价。
同时给你两个整数 k 和 candidates 。我们想根据以下规则恰好雇佣 k 位工人:
- 总共进行 k 轮雇佣,且每一轮恰好雇佣一位工人。
- 在每一轮雇佣中,从最前面 candidates 和最后面 candidates 人中选出代价最小的一位工人,如果有多位代价相同且最小的工人,选择下标更小的一位工人。
- 比方说,costs = [3,2,7,7,1,2] 且 candidates = 2 ,第一轮雇佣中,我们选择第 4 位工人,因为他的代价最小 [3,2,7,7,1,2] 。
- 第二轮雇佣,我们选择第 1 位工人,因为他们的代价与第 4 位工人一样都是最小代价,而且下标更小,[3,2,7,7,2] 。注意每一轮雇佣后,剩余工人的下标可能会发生变化。
- 如果剩余员工数目不足 candidates 人,那么下一轮雇佣他们中代价最小的一人,如果有多位代价相同且最小的工人,选择下标更小的一位工人。
- 一位工人只能被选择一次。
返回雇佣恰好 k 位工人的总代价。
示例 1:
输入:costs = [17,12,10,2,7,2,11,20,8], k = 3, candidates = 4
输出:11
解释:我们总共雇佣 3 位工人。总代价一开始为 0 。
- 第一轮雇佣,我们从 [17,12,10,2,7,2,11,20,8] 中选择。最小代价是 2 ,有两位工人,我们选择下标更小的一位工人,即第 3 位工人。总代价是 0 + 2 = 2 。
- 第二轮雇佣,我们从 [17,12,10,7,2,11,20,8] 中选择。最小代价是 2 ,下标为 4 ,总代价是 2 + 2 = 4 。
- 第三轮雇佣,我们从 [17,12,10,7,11,20,8] 中选择,最小代价是 7 ,下标为 3 ,总代价是 4 + 7 = 11 。注意下标为 3 的工人同时在最前面和最后面 4 位工人中。
总雇佣代价是 11 。
示例 2:
输入:costs = [1,2,4,1], k = 3, candidates = 3
输出:4
解释:我们总共雇佣 3 位工人。总代价一开始为 0 。
- 第一轮雇佣,我们从 [1,2,4,1] 中选择。最小代价为 1 ,有两位工人,我们选择下标更小的一位工人,即第 0 位工人,总代价是 0 + 1 = 1 。注意,下标为 1 和 2 的工人同时在最前面和最后面 3 位工人中。
- 第二轮雇佣,我们从 [2,4,1] 中选择。最小代价为 1 ,下标为 2 ,总代价是 1 + 1 = 2 。
- 第三轮雇佣,少于 3 位工人,我们从剩余工人 [2,4] 中选择。最小代价是 2 ,下标为 0 。总代价为 2 + 2 = 4 。
总雇佣代价是 4 。
说明:
- 1 <= costs.length <= 10^5
- 1 <= costs[i] <= 10^5
- 1 <= k, candidates <= costs.length
思路
给我们一个数组 costs 和一个整数 candidates,每次从数组的前candidates与后candidates中选取值最小的元素,如果最小值相等则取下标最小的,costs中的每个值只能被选一次,求选出的k个元素和。
很直接的想法是使用优先队列保存这些元素,考虑到相同值需要取下标最小的,于是还要保存下标信息。循环从队列取出头元素累加到res,每取出一个元素需要从前/后补一个元素到队列中。
注意题目的数据范围,需要返回long类型。还有就是放队列以及补元素的停止条件以免重复。
代码
/**
* @date 2024-05-01 17:03
*/
public class TotalCost2462 {
public long totalCost(int[] costs, int k, int candidates) {
PriorityQueue<Integer[]> q = new PriorityQueue<>((x, y) -> {
int compare = x[0] - y[0];
return compare == 0 ? x[1] - y[1] : compare;
});
int n = costs.length;
int leftPos;
int rightPos = n - 1;
for (leftPos = 0; leftPos < candidates; leftPos++) {
if (leftPos < rightPos) {
// rightPos 与 leftPos指向的都是需要添加的下一个位置,
q.offer(new Integer[]{costs[leftPos], leftPos});
q.offer(new Integer[]{costs[rightPos], rightPos});
rightPos--;
}
else if (leftPos == rightPos) {
// 这里已经覆盖了数组的所有元素,循环结束后leftPos > rightPos
q.offer(new Integer[]{costs[leftPos], leftPos});
}
else {
break;
}
}
// 不使用long会溢出
long res = 0;
// 选到最小之后,补够candidates
for (int i = 0; i < k && !q.isEmpty(); i++) {
Integer[] c = q.poll();
res += c[0];
// 如果已经覆盖了所有元素就无需再添加了
if (leftPos > rightPos) {
continue;
}
if (c[1] < leftPos) {
q.offer(new Integer[]{costs[leftPos], leftPos});
leftPos++;
} else if (c[1] > rightPos) {
q.offer(new Integer[]{costs[rightPos], rightPos});
rightPos--;
}
}
return res;
}
}
性能
勉强通过,官网也是这个思路,耗时82ms。有网友的解法是分别使用了两个优先队列,只需要22ms。
1329.将矩阵按对角线排序
目标
矩阵对角线 是一条从矩阵最上面行或者最左侧列中的某个元素开始的对角线,沿右下方向一直到矩阵末尾的元素。例如,矩阵 mat 有 6 行 3 列,从 mat[2][0]
开始的 矩阵对角线 将会经过 mat[2][0]
、mat[3][1]
和 mat[4][2]
。
给你一个 m * n 的整数矩阵 mat ,请你将同一条 矩阵对角线 上的元素按升序排序后,返回排好序的矩阵。
说明:
- m == mat.length
- n == mat[i].length
- 1 <= m, n <= 100
1 <= mat[i][j] <= 100
思路
排序是一大块内容,有机会统一总结一下。// todo
这里偷懒使用了优先队列,先放到队列里面排序,然后再写回去。
代码
/**
* @date 2024-04-29 0:26
*/
public class DiagonalSort1329 {
public int[][] diagonalSort(int[][] mat) {
int m = mat.length;
int n = mat[0].length;
int i;
int j;
PriorityQueue<Integer> q = new PriorityQueue();
for (int col = 0; col < n; col++) {
j = col;
i = 0;
while (i < m && j < n) {
q.offer(mat[i++][j++]);
}
j = col;
i = 0;
while (i < m && j < n) {
mat[i++][j++] = q.poll();
}
}
for (int row = 1; row < m; row++) {
j = 0;
i = row;
while (i < m && j < n) {
q.offer(mat[i++][j++]);
}
j = 0;
i = row;
while (i < m && j < n) {
mat[i++][j++] = q.poll();
}
}
return mat;
}
}
性能
1146.快照数组
这个题涉及到hash表与二分查找,发现之前2529.正整数和负整数的最大计数里面寻找上界写错了。
有时间了再整理一下吧。
2385.感染二叉树需要的总时间
目标
给你一棵二叉树的根节点 root ,二叉树中节点的值 互不相同 。另给你一个整数 start 。在第 0 分钟,感染 将会从值为 start 的节点开始爆发。
每分钟,如果节点满足以下全部条件,就会被感染:
- 节点此前还没有感染。
- 节点与一个已感染节点相邻。
返回感染整棵树需要的分钟数。
示例 1:
输入:root = [1,5,3,null,4,10,6,9,2], start = 3
输出:4
解释:节点按以下过程被感染:
- 第 0 分钟:节点 3
- 第 1 分钟:节点 1、10、6
- 第 2 分钟:节点5
- 第 3 分钟:节点 4
- 第 4 分钟:节点 9 和 2
感染整棵树需要 4 分钟,所以返回 4 。
示例 2:
输入:root = [1], start = 1
输出:0
解释:第 0 分钟,树中唯一一个节点处于感染状态,返回 0 。
说明:
- 树中节点的数目在范围 [1, 10^5] 内
- 1 <= Node.val <= 10^5
- 每个节点的值 互不相同
- 树中必定存在值为 start 的节点
思路
从树中任意节点开始,每过一分钟感染会向周围扩散,问感染整棵树需要多久。
首先我们要找到感染开始的节点。从这个节点出发,向左右子树点以及父节点扩散。可以将树转换为以感染节点为起点的有向无环连通图,这样问题被转换为求起点到图中任意节点的最长路径。
如果不想建图可以考虑扩散的具体路径,刚开始很难把各种情况都考虑到。我们需要计算以开始节点为根的子树高度 h(start)
,并依次比较开始节点到祖先节点路径长度加上祖先另一子树高度的最大值,即max(d(start) - d(ancestor) + h(anotherAncestorSubtree))
,再取二者的最大值即可。特别需要注意的是,不能使用子树高度之差来计算祖先与开始节点的路径长度。例如,E是开始节点,E到B的路径长度为d(E) - d(B) = 2 - 1 = 1
,而如果使用子树高度相减的话就得到了h(B) - h(E) = 3 - 0 = 3
。
A
/ \
B C
/ \
D E
|
F
|
G
在具体实现的时候如何判断祖先节点的哪个子树包含开始节点困扰了我半天。刚开始我选择了一个标志位,分别在左右子树递归结束的时候检测该标志,发现找到之后立即重置该标志,这样父节点就知道了是左子树还是右子树包含开始节点。但问题是再向上返回的时候就无法判断了。
可以考虑返回二维数组,也有网友的题解使用返回值的符号来标识是否找到开始节点。
代码
/**
* @date 2024-04-24 8:56
*/
public class AmountOfTime2385 {
int startToParentToLeaf = 0;
int startToLeaf = 0;
int cnt = 0;
public int amountOfTime(TreeNode root, int start) {
dfs(root, start);
return Math.max(startToLeaf, startToParentToLeaf);
}
/**
* 返回子树深度
*/
public int[] dfs(TreeNode root, int start) {
if (root == null) {
return new int[]{0, 0};
}
int[] l = dfs(root.left, start);
int[] r = dfs(root.right, start);
boolean lfind = l[1] == 1;
boolean rfind = r[1] == 1;
int max = Math.max(r[0], l[0]);
if (lfind || rfind) {
startToParentToLeaf = Math.max(startToParentToLeaf, l[0] + r[0]);
// 这里的返回值不是max,而是祖先节点到开始节点的路径长度
return new int[]{(lfind ? l[0] : r[0]) + 1, 1};
}
if (root.val == start) {
startToLeaf = max;
// 这里直接返回1,不加max
// 视为将开始节点的左右子树删掉,后面回溯时直接相加左右子树高度即可
return new int[]{1, 1};
}
return new int[]{max + 1, 0};
}
/**
* 返回深度
*/
public int[] dfs_v1(TreeNode root, int start, int depth) {
if (root == null) {
return new int[]{depth - 1, 0};
}
int[] l = dfs_v1(root.left, start, depth + 1);
int[] r = dfs_v1(root.right, start, depth + 1);
boolean lfind = l[1] == 1;
boolean rfind = r[1] == 1;
int max = Math.max(r[0], l[0]);
if (lfind) {
cnt++;
startToParentToLeaf = Math.max(r[0] - depth + cnt, startToParentToLeaf);
}
if (rfind) {
cnt++;
startToParentToLeaf = Math.max(l[0] - depth + cnt, startToParentToLeaf);
}
if (root.val == start) {
startToLeaf = max - depth;
return new int[]{max, 1};
}
return new int[]{max, l[1] + r[1]};
}
}
性能
1052.爱生气的书店老板
目标
有一个书店老板,他的书店开了 n 分钟。每分钟都有一些顾客进入这家商店。给定一个长度为 n 的整数数组 customers ,其中 customers[i] 是在第 i 分钟开始时进入商店的顾客数量,所有这些顾客在第 i 分钟结束后离开。
在某些时候,书店老板会生气。 如果书店老板在第 i 分钟生气,那么 grumpy[i] = 1,否则 grumpy[i] = 0。
当书店老板生气时,那一分钟的顾客就会不满意,若老板不生气则顾客是满意的。
书店老板知道一个秘密技巧,能抑制自己的情绪,可以让自己连续 minutes 分钟不生气,但却只能使用一次。
请你返回 这一天营业下来,最多有多少客户能够感到满意 。
示例 1:
输入:customers = [1,0,1,2,1,1,7,5], grumpy = [0,1,0,1,0,1,0,1], minutes = 3
输出:16
解释:书店老板在最后 3 分钟保持冷静。
感到满意的最大客户数量 = 1 + 1 + 1 + 1 + 7 + 5 = 16.
示例 2:
输入:customers = [1], grumpy = [0], minutes = 1
输出:1
说明:
- n == customers.length == grumpy.length
- 1 <= minutes <= n <= 2 * 10^4
- 0 <= customers[i] <= 1000
- grumpy[i] == 0 or 1
思路
这道题要我们求满意顾客的最大值,老板会在指定的分钟生气,生气时,当前进入的顾客就会不满意,老板可以有一次忍耐的机会在一段时间内不生气。
刚开始想复杂了,以为顾客不是马上离开,而是在i分钟后离开。其实是第i分钟结束的时候就离开了。
因此,我们只需要计算老板忍耐的时间内,原本生气影响的顾客最大值,然后再加上不生气时的顾客总数即可。
这就是一个滑动窗口问题,窗口大小为忍耐时间,记录窗口内的不满意顾客的最大值。
这里面可以优化的点是使用乘法来代替条件判断,这样可以避免分支预测失败导致额外的性能损失。
代码
/**
* @date 2024-04-23 8:40
*/
public class MaxSatisfied1052 {
public int maxSatisfied_v1(int[] customers, int[] grumpy, int minutes) {
int n = customers.length;
int total = 0;
for (int i = 0; i < n; i++) {
total += customers[i] * (1 - grumpy[i]);
}
int unsatisfiedInWindow = 0;
for (int i = 0; i < minutes; i++) {
unsatisfiedInWindow += customers[i] * grumpy[i];
}
int max = unsatisfiedInWindow;
for (int i = minutes; i < n; i++) {
unsatisfiedInWindow = unsatisfiedInWindow
- customers[i - minutes] * grumpy[i - minutes]
+ customers[i] * grumpy[i];
max = Math.max(max, unsatisfiedInWindow);
}
return total + max;
}
}