LeetCode2021春季编程大赛地址:https://leetcode-cn.com/contest/season/2021-spring/
第一题:采购方案
从N个零件里选2个,保证这两个零件花费和不超过预算,将N个零件从小到大进行排序,对于某个零件(下标为
i
i
i,花费
a
a
a),从它右端到结尾查找,找到最大花费
b
b
b的零件,使得
a
+
b
≤
t
a
r
g
e
t
a+b≤target
a+b≤target,如果
a
≥
t
a
r
g
e
t
a≥target
a≥target了,就不存在可能的组合了。
查找的过程使用二分查找提高效率,如果可以查找到返回满足条件的最大下标
j
j
j(所以这里用right_bound()方法查找满足条件的右边界),如果查找不到,返回-1。
对于某个a,假设存在一个b,使得
a
+
b
≤
t
a
r
g
e
t
a+b≤target
a+b≤target,那么可以选择a,再从
[
i
+
1
,
j
]
[i+1, j]
[i+1,j]里选择任意一个来组合,对于每个a,有
j
−
i
j-i
j−i种组合,累加,最后取余。
class Solution {
public int purchasePlans(int[] nums, int target) {
int length = nums.length, MOD = 1000000007;
Arrays.sort(nums);
long result = 0;
for (int i = 0; i < length; i++) {
if (nums[i] >= target) {
break;
}
int rightBound = right_bound(nums, target - nums[i], i + 1, length - 1);
if (rightBound != -1) {
result += rightBound - i;
}
}
return (int) (result % MOD);
}
private int right_bound(int[] array, int key, int left, int right) {
while (left <= right) {
int mid = left + (right - left) / 2;
if (array[mid] > key) {
right = mid - 1;
} else if (array[mid] < key) {
left = mid + 1;
} else {
left = mid + 1;
}
}
return right;
}
}
第二题:乐团站位
这个题目道是不难,就是有点儿麻烦,首先一看数据量肯定不能模拟,要统计从左上角到要求点走过了多少个元素,再对9取余,从而判断[xPos, yPos]的值。
格外注意的一点是num的范围是
[
1
,
1
0
9
]
[1, 10^{9}]
[1,109],在计算的时候,很容易就爆int了,保险起见,全程用long处理。
在求外圈走了多少个数字的时候,直接用等差数列求和公式,不要for循环计算,非常耗时。
class Solution {
// 注意爆int的问题,int*int的结果可能爆int
public int orchestraLayout(int num, int xPos, int yPos) {
long circle = Math.min(Math.min(xPos, num - xPos - 1), Math.min(yPos, num - yPos - 1));
long total = 0;
// 记录完整的外圈走了多少个数字(未优化:2782 ms)
// for (int i = 0; i < circle; i++) {
// total += (num - (2 * i + 1)) * 4L;
// }
// 记录完整的外圈走了多少个数字(优化:0ms,直接用等差数列求解:(首项+末项)*项数/2)
total = 4L * ((num - 1) + (num - (2 * (circle - 1) + 1))) * circle / 2L;
if (xPos == circle) {// [xPos, yPos]位于圈的上侧
total += yPos - circle + 1;
} else if (yPos == num - circle - 1) {// [xPos, yPos]位于圈的右侧
total += (num - (2 * circle + 1)) * 1L + (xPos - circle + 1);
} else if (xPos == num - circle - 1) {// [xPos, yPos]位于圈的下侧
total += (num - (2 * circle + 1)) * 2L + (num - circle - yPos);
} else {// [xPos, yPos]位于圈的左侧
total += (num - (2 * circle + 1)) * 3L + (num - circle - xPos);
}
total %= 9;
if (total == 0) {
total = 9;
}
return (int) total;
}
}
第三题:魔塔游戏
先考虑-1的情况,遍历一遍求和sum,如果
s
u
m
<
0
sum < 0
sum<0,那么就是-1。
用一个优先队列存储前面碰到的负值,当出现
b
l
o
o
d
≤
0
blood≤0
blood≤0的时候,就从前面碰到的负值里,选择最小的一个进行恢复,恢复的目的在于让血量恢复的最多,也就是把这个扣最多的血量移动到最后。
class Solution {
public int magicTower(int[] nums) {
long blood = 1L;
for (int num : nums) {
blood += num;
}
if (blood < 0) {
return -1;
}
blood = 1;
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
int operation = 0;
for (int num : nums) {
if (num < 0) {// 把负数放到优先队列里
priorityQueue.offer(num);
if (blood + num <= 0) {// 加上num后就死了
blood -= priorityQueue.poll();// 将前面碰到过的最小负数的扣血量恢复,也就是移动到最后
operation++;
}
}
blood += num;
}
return operation;
}
}
第四题:变换的迷宫
首先,将问题简化,考虑一个
m
×
n
m \times n
m×n的矩阵,其中有一些点是不可达的,问,从
[
0
,
0
]
[0,0]
[0,0]点走到
[
m
−
1
,
n
−
1
]
[m-1,n-1]
[m−1,n−1]点,是否存在一条路径可达。这时候还是比较简单的,可以用DFS暴搜。
加上一个时间维度,还是可以理解的,还可以暴力搜索,此时的时间复杂度是
O
(
T
M
N
)
O(TMN)
O(TMN),然后考虑卷轴。
卷轴1是临时卷轴,使用一次就失效了,可以用0-1来表示使用状态。
卷轴2是永久卷轴,一旦使用,此位置始终是可达的,使用永久卷轴的目的是:将来某时刻会回到这个位置,这个操作可以等价为在使用永久卷轴位置停留了一段时间。
因此,使用临时卷轴的时候,只需要对下一时刻的DFS要将临时卷轴置1。使用永久卷轴的时候,需要从下一时刻到最终时刻的DFS将永久卷轴置1。
再加上一些剪枝操作进行优化。
class Solution {
int[] dx = new int[]{0, 1, 0, -1, 0}, dy = new int[]{1, 0, -1, 0, 0};
boolean[][][][][] visit;
int tLimit, m, n;
public boolean escapeMaze(List<List<String>> maze) {
tLimit = maze.size() - 1;// t的取值范围是[0, maze.size() - 1]
m = maze.get(0).size();// 矩阵的行数
n = maze.get(0).get(0).length();// 矩阵的列数
visit = new boolean[tLimit + 1][m][n][2][2];// 标记某种情况情况是否已经DFS探测过,避免重复探测
return DFS(0, 0, 0, 0, 0, maze);
}
/**
* @param t 时间维度
* @param x 当前的为位置
* @param y 当前的为位置
* @param magic1 临时卷轴 0:未使用magic1;1:使用了magic1
* @param magic2 永久卷轴 0:未使用magic2;1:使用了magic2
* @param maze 迷宫
*/
private boolean DFS(int t, int x, int y, int magic1, int magic2, List<List<String>> maze) {
if ((m - 1 - x) + (n - 1 - y) > tLimit - t) {// [x, y]走到[m - 1, n - 1]最少需要的时间比剩余可用时间还多,说明走不到了
return false;
}
if (visit[t][x][y][magic1][magic2]) {// 剪枝:当前情况已经考虑过了,不用继续对这种情况DFS了
return false;
}
visit[t][x][y][magic1][magic2] = true;// 标记这种情况为考虑过
if (x == m - 1 && y == n - 1) {// 走到了[m - 1, n - 1]的位置
return true;
}
// 考虑下一个时刻的运动情况:4个方向和原地不动
for (int i = 0; i < 5; i++) {
int fx = x + dx[i], fy = y + dy[i];// 下一时刻新的位置
if ((0 <= fx && fx <= m - 1) && (0 <= fy && fy <= n - 1)) {// 还在maze范围内
char nextTimePosition = maze.get(t + 1).get(fx).charAt(fy);// 下一时刻的下一个位置的地形
if (nextTimePosition == '.') {// 可达
// 如果DFS返回true,表示可达;如果DFS返回false,表示不可达,继续判断,这里我们只关心DFS返回true的情况
if (DFS(t + 1, fx, fy, magic1, magic2, maze)) {
return true;
}
} else {// 不可达
if (magic1 == 0) {// 卷轴1还没使用
// 如果DFS返回true,表示可达;如果DFS返回false,表示不可达,继续判断,这里我们只关心DFS返回true的情况
if (DFS(t + 1, fx, fy, 1, magic2, maze)) {
return true;
}
}
if (magic2 == 0) {// 卷轴2还没使用
// 在[t + 1, tLimit]时刻里,用“卷轴2已使用”状态继续探测,有没有可能走到[m - 1, n - 1]位置
for (int temp = t + 1; temp <= tLimit; temp++) {
// 如果DFS返回true,表示可达;如果DFS返回false,表示不可达,继续判断,这里我们只关心DFS返回true的情况
if (DFS(temp, fx, fy, magic1, 1, maze)) {
return true;
}
}
}
}
}
}
return false;
}
}
还看到一种比较好理解的方式,不过时间复杂度高,上面的方法,只要有一个满足要求的,就返回了,下面这种方式,会将整个图都跑一边遍,最后查找有没有满足条件的。
class Solution {
int[] dx = new int[]{0, 1, 0, -1, 0}, dy = new int[]{1, 0, -1, 0, 0};
boolean[][][][][] visit;
int tLimit, m, n;
public boolean escapeMaze(List<List<String>> maze) {
tLimit = maze.size();// t的取值范围是[0, maze.size())
m = maze.get(0).size();// 矩阵的行数
n = maze.get(0).get(0).length();// 矩阵的列数
visit = new boolean[tLimit][m][n][2][2];// 标记某种情况情况是否已经DFS探测过,避免重复探测
visit[0][0][0][0][0] = true;// 初始位置访问过
for (int t = 0; t + 1 < tLimit; t++) {// 遍历时刻
for (int x = 0; x < m; x++) {// 遍历行
for (int y = 0; y < n; y++) {// 遍历列
for (int i = 0; i < 5; i++) {// 考虑4个方向和原地不动
int fx = x + dx[i], fy = y + dy[i];// 新位置的坐标
if ((0 <= fx && fx < m) && (0 <= fy && fy < n)) {// 新位置没有出界
for (int magic1 = 0; magic1 < 2; magic1++) {// 遍历卷轴1
for (int magic2 = 0; magic2 < 2; magic2++) {// 遍历卷轴2
// 剪枝:如果t时刻的[x, y]还没访问过,是不能访问t + 1时刻的[fx, fy]的
if (!visit[t][x][y][magic1][magic2]) {
continue;
}
// 探测t + 1时刻的[fx, fy]的情况
detect(maze, t + 1, fx, fy, magic1, magic2);
}
}
}
}
}
}
}
// 探测过一遍后,从[0, tLimit)遍历t,关注[n - 1][m - 1]位置,有没有true的,如果有一个true的,说明可达
for (int t = 0; t < tLimit; t++) {
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
if (visit[t][m - 1][n - 1][i][j]) {
return true;
}
}
}
}
return false;
}
private void detect(List<List<String>> maze, int t, int x, int y, int magic1, int magic2) {
char nextTimePosition = maze.get(t).get(x).charAt(y);// 下一时刻下一位置的地形
if (nextTimePosition == '.') {// 可达
visit[t][x][y][magic1][magic2] = true;
} else {// 不可达
if (magic1 == 0) {// 卷轴1还没用
visit[t][x][y][1][magic2] = true;
}
if (magic2 == 0) {// 卷轴2还没用
for (int i = t; i < tLimit; i++) {
visit[i][x][y][magic1][1] = true;
}
}
}
}
}