打地鼠
欢迎各位勇者来到力扣城,本次试炼主题为「打地鼠」。
勇者面前有一个大小为 3*3 的打地鼠游戏机,地鼠将随机出现在各个位置,moles[i] = [t,x,y] 表示在第 t
秒会有地鼠出现在 (x,y) 位置上,并于第 t+1 秒该地鼠消失。
勇者有一把可敲打地鼠的锤子,初始时刻(即第 0 秒)锤子位于正中间的格子 (1,1),锤子的使用规则如下:
- 锤子每经过 1 秒可以往上、下、左、右中的一个方向移动一格,也可以不移动
- 锤子只可敲击所在格子的地鼠,敲击不耗时
请返回勇者最多能够敲击多少只地鼠。
注意:
- 输入用例保证在相同时间相同位置最多仅有一只地鼠
示例 1:
输入: moles = [[1,1,0],[2,0,1],[4,2,2]] 输出: 2 解释: 第 0 秒,锤子位于 (1,1) 第 1 秒,锤子移动至 (1,0) 并敲击地鼠 第 2 秒,锤子移动至 (2,0) 第 3 秒,锤子移动至 (2,1) 第 4 秒,锤子移动至 (2,2) 并敲击地鼠 因此勇者最多可敲击 2 只地鼠
示例 2:
输入:moles = [[2,0,2],[5,2,0],[4,1,0],[1,2,1],[3,0,2]] 输出:3 解释: 第 0 秒,锤子位于 (1,1) 第 1 秒,锤子移动至 (2,1) 并敲击地鼠 第 2 秒,锤子移动至 (1,1) 第 3 秒,锤子移动至 (1,0) 第 4 秒,锤子在 (1,0) 不移动并敲击地鼠 第 5 秒,锤子移动至 (2,0) 并敲击地鼠 因此勇者最多可敲击 3 只地鼠
示例 3:
输入:moles = [[0,1,0],[0,0,1]] 输出:0 解释: 第 0 秒,锤子初始位于 (1,1),此时并不能敲击 (1,0)、(0,1) 位置处的地鼠
提示:
- 1 <= moles.length <= 10^5
- moles[i].length == 3
- 0 <= moles[i][0] <= 10^9
- 0 <= moles[i][1], moles[i][2] < 3
解题思路
leetcode的垃圾评测器一直卡常,单个样例一秒内可以跑出来结果全部样例一起评测就超时了,莫名其妙的,都不知道是怎么评测的。最搞的是那边显示全部样例已通过,结果最后莫名其妙给你来个TLE[黄豆流汗.jpg]:
先给我一开始的解法。首先很容易想到动态规划,定义状态$f(i,x,y)$表示从第$0$秒到第$i$秒结束,且最后锤子在$(x,y)$这个格子上的所有敲地鼠的方案,属性是敲打地鼠的数量的最大值。根据前一秒即$i-1$锤子最后停留的位置,即$(x, y)$与其上下左右四个方向来划分状态,状态转移方程为:$$f(i, x, y) = \max\{ f(i-1,x,y), f(i-1,x-1,y), f(i-1,x,y+1), f(i-1,x+1,y), f(i-1,x,y-1) \} + g(i, x, y)$$其中$g(i, x, y)$表示第$i$秒$(x, y)$上地鼠的数量。
需要注意的是前后两个时刻不一定恰好相差$1$秒,但在这段时间内的状态还是要转移的,虽然在这段时间内没有地鼠,但锤子可以移动到其他的格子。这就有两个问题,一个是时间可能会相差很大,那么我们需要把每一秒的状态都转移一次吗?显然不是,可以发现从一个格子到另外一个格子最多需要$4$秒:
又因为在状态转移的过程中不会出现地鼠,因此两个时刻之间最多只需要转移$4$次,第$4$次之后即使进行状态转移,状态还是保持不变。
另外一个问题是时间可能会很大,数组开不了这么多,这个进行离散化就好了。
TLE代码如下,时间复杂度为$O(n \log{n} + 225 \times n)$:
1 class Solution { 2 public: 3 int getMaximumNumber(vector<vector<int>>& moles) { 4 int n = moles.size(); 5 sort(moles.begin(), moles.end()); // 根据时间从小到大排序 6 vector<vector<vector<int>>> f(5 * n, vector<vector<int>>(3, vector<int>(3, -0x3f3f3f3f))); 7 f[0][1][1] = 0; // 一开始第0秒锤子在(1, 1) 8 int i = 0, last = 0; 9 while (i < n && !moles[i][0]) { // 先把第0秒的情况遍历完 10 if (moles[i][1] == 1 && moles[i][2] == 1) f[0][1][1]++; // 在第0秒只能打到(1, 1)上的地鼠 11 i++; 12 } 13 int sz = 0; // 对时间离散化 14 int dx[5] = {-1, 0, 1, 0, 0}, dy[5] = {0, 1, 0, -1, 0}; 15 while (i < n) { 16 for (int j = 0; j < min(4, moles[i][0] - last - 1); j++) { // 两个时刻之间状态最多转移4次 17 sz++; 18 for (int u = 0; u < 3; u++) { 19 for (int v = 0; v < 3; v++) { 20 for (int k = 0; k < 5; k++) { 21 int x = u + dx[k], y = v + dy[k]; 22 if (x < 0 || x >= 3 || y < 0 || y >= 3) continue; 23 f[sz][u][v] = max(f[sz][u][v], f[sz - 1][x][y]); 24 } 25 } 26 } 27 } 28 sz++; 29 int g[3][3] = {0}; 30 int j = i; 31 while (j < n && moles[i][0] == moles[j][0]) { // 把第moles[i][0]秒的地鼠找出来 32 g[moles[j][1]][moles[j][2]]++; 33 j++; 34 } 35 for (int u = 0; u < 3; u++) { 36 for (int v = 0; v < 3; v++) { 37 for (int k = 0; k < 5; k++) { 38 int x = u + dx[k], y = v + dy[k]; 39 if (x < 0 || x >= 3 || y < 0 || y >= 3) continue; 40 f[sz][u][v] = max(f[sz][u][v], f[sz - 1][x][y] + g[u][v]); 41 } 42 } 43 } 44 last = moles[i][0]; 45 i = j; 46 } 47 int ret = 0; 48 for (int i = 0; i < 3; i++) { 49 for (int j = 0; j < 3; j++) { 50 ret = max(ret, f[sz][i][j]); 51 } 52 } 53 return ret; 54 } 55 };
可以发现常数非常大,但是计算量是$10^{7}$级别的,按理来说是可以过的,但在leetcode就是过不了。
看了题解发现在状态转移的部分可以进行优化。上面是保证两个时刻只相差$1$秒然后从四个方向来进行状态转移的,其实对于两个时刻$t_{i}$和$t_{i-1}$,如果两个格子$(x, y)$和$(u, v)$满足$|x - u| + |y - v| \leq t_{i} - t_{i-1}$,那么就可以从$(u, v)$转移到$(x, y)$,因此在进行状态转移时就不需要单独考虑转移两个时刻间的状态,直接枚举$(x, y)$和$(u, v)$,只要满足两个格子间的距离不超过两个时刻的差就可以进行转移,状态转移方程为:$$f(i, x, y) = \max_{\begin{array}{c} 0 \leq {u, v} \leq 3 \\ |x-u|+|y-v| \leq t_i - t_{i-1} \end{array}}\left\{ f(i-1, u, v) \right\} + g(i, x, y)$$
这个时候时间复杂度就降到$O(n \log{n} + 81 \times n)$,当如果不优化空间复杂度还是会超时(垃圾leetcode)。根据状态转移方程可以发现第一维的状态每次转移都只依赖于前一次的状态,因此可以开个滚动数组,这样才能勉强AC。
AC代码如下:
1 class Solution { 2 public: 3 int getMaximumNumber(vector<vector<int>>& moles) { 4 int n = moles.size(); 5 sort(moles.begin(), moles.end()); 6 vector<vector<vector<int>>> f(2, vector<vector<int>>(3, vector<int>(3, -0x3f3f3f3f))); 7 f[0][1][1] = 0; 8 int i = 0, last = 0; 9 while (i < n && !moles[i][0]) { 10 if (moles[i][1] == 1 && moles[i][2] == 1) f[0][1][1]++; 11 i++; 12 } 13 int sz = 0; 14 while (i < n) { 15 sz ^= 1; 16 for (int x = 0; x < 3; x++) { 17 for (int y = 0; y < 3; y++) { 18 for (int u = 0; u < 3; u++) { 19 for (int v = 0; v < 3; v++) { 20 if (abs(x - u) + abs(y - v) <= moles[i][0] - last) f[sz][x][y] = max(f[sz][x][y], f[sz ^ 1][u][v]); // 这里转移先不考虑第moles[i][0]秒的地鼠 21 } 22 } 23 } 24 } 25 last = moles[i][0]; 26 while (i < n && moles[i][0] == last) { // 最后把第moles[i][0]秒的地鼠加上 27 f[sz][moles[i][1]][moles[i][2]]++; 28 i++; 29 } 30 } 31 int ret = 0; 32 for (int i = 0; i < 3; i++) { 33 for (int j = 0; j < 3; j++) { 34 ret = max(ret, f[sz][i][j]); 35 } 36 } 37 return ret; 38 } 39 };
最后给出另外一种状态的定义。定义$f(i)$表示所有前$i$只地鼠(按照时间先后排序),刚打完了第$i$只地鼠的方案,属性是敲打地鼠的最大值。容易想到通过枚举前一只地鼠$j$来进行状态转移。如果$|x_i - x_j| + |y_i - y_j| \leq t_i - t_j$,就可以从第$j$只地鼠转移到第$i$只地鼠,但这样做的时间复杂度为$O(n^2)$。由于$|x_i - x_j| + |y_i - y_j| \leq 4$,因此如果$t_i - t_j \geq 4$那么一定能从第$j$只地鼠转移到第$i$只地鼠,不需要重复枚举。又由于地鼠是按照时间先后排好序的,因此可以开个变量来维护前缀最大值,当枚举到第$i$只地鼠,用双指针来维护满足$t_i - t_j < 4$的最左边的位置$j$。
AC代码如下,由于题目保证每个时刻同一个位置最多只有一只地鼠,因此$i - j \leq 4 \times 9$,因此时间复杂度为$O(n \log{n} + 36 \times n)$:
1 class Solution { 2 public: 3 int getMaximumNumber(vector<vector<int>>& moles) { 4 int n = moles.size(); 5 sort(moles.begin(), moles.end()); 6 moles.insert(moles.begin(), {0, 1, 1}); 7 vector<int> f(n + 1); 8 for (int i = 1, j = 0, maxv = -2e9; i <= n; i++) { 9 while (moles[i][0] - moles[j][0] >= 4) { 10 maxv = max(maxv, f[j]); 11 j++; 12 } 13 f[i] = maxv; 14 for (int k = j; k < i; k++) { 15 if (abs(moles[i][1] - moles[k][1]) + abs(moles[i][2] - moles[k][2]) <= moles[i][0] - moles[k][0]) f[i] = max(f[i], f[k]); 16 } 17 f[i]++; 18 } 19 return *max_element(f.begin(), f.end()); 20 } 21 };
参考资料
DP:https://leetcode.cn/problems/ZbAuEH/solution/by-tsreaper-ey5v/
标签:sz,int,++,地鼠,moles,锤子 From: https://www.cnblogs.com/onlyblues/p/16987107.html