[LeetCode] 1269. Number of Ways to Stay in the Same Place After Some Steps 停在原地的方案数
You have a pointer at index 0
in an array of size arrLen
. At each step, you can move 1 position to the left, 1 position to the right in the array, or stay in the same place (The pointer should not be placed outside the array at any time).
Given two integers steps
and arrLen
, return the number of ways such that your pointer still at index 0
after exactly steps
steps. Since the answer may be too large, return it modulo 109 + 7
.
Example 1:
Input: steps = 3, arrLen = 2
Output: 4
Explanation: There are 4 differents ways to stay at index 0 after 3 steps.
Right, Left, Stay
Stay, Right, Left
Right, Stay, Left
Stay, Stay, Stay
Example 2:
Input: steps = 2, arrLen = 4
Output: 2
Explanation: There are 2 differents ways to stay at index 0 after 2 steps
Right, Left
Stay, Stay
Example 3:
Input: steps = 4, arrLen = 2
Output: 8
Constraints:
1 <= steps <= 500
1 <= arrLen <= 10^6
这道题说是给了一个长度为 arrLen 的数组,起始位置在坐标0,现在有三种操作,向左移动一个位置,向右移动一个位置,或者是待在原地不动(也算一步),现在需要走 steps 步,问有多少种不同的走法使得最后仍然在坐标0的位置,结果需要对一个超大数字取余。一旦看到了说结果要对一个超大数字取余,则基本就是暗示了要用动态规划 Dynmaic Programming 来做,因为递归所有情况肯定会爆栈。那么首先就来定义 dp 数组吧,既然有步数和坐标两个信息,大家基本都会定义一个二维数组,其中 dp[i][j] 表示走了i步,到达坐标j位置的不同走法,而且状态转移方程也非常的直接,因为当前位置只能由三个位置过来,左边,右边,和其本身,而且 i-1 步的 dp 值又都是知道的,所以直接把三个位置的 dp 值加起来就是当前位置的值了,快速写好后满心欢喜的去提交,结果被 OJ 打了回来,超时了 Time Limit Exceeded。这道题的 OJ 非常严苛,基本上不是超时就是超内存,所以要同时进行时间上和空间上的优化,这道题的 trick 主要是在于空间上的优化,需要更新的位置少了,自然时间也就用的少了。
题目中给了 steps 和 arrLen 的范围,可以发现 arrLen 的范围要远大于 steps,但是仔细想一想,起始位置是在0,而最多走 500 步,就算每一步都往右走,最多就只能走 500 个位置,不管之后数组还有多少位子,都是无法到达的,为无法到达的位置申请空间是没有意义的,所以能到达的范围是 steps 和 arrLen 中的较小值,这样就省下了不少的空间。同时,可以发现第i步的值只跟第 i-1 步的值有关系,所以没有必要计算每一步的 dp 值,用一个一维的数组就行了,不过还需要记录上一步的每一个位置的值,以便更新当前值。分析到这,基本上代码就不难写了,用一个一维数组 last 来记录上一步每个位置的 dp 值,i从1遍历到 steps,新建一个一维数组 cur 来记录当前步的值,然后用j从0遍历到 steps 和 arrLen 中的较小值,若 last[j] 大于0(表示曾经到达过位置j),则将其加到 cur[j] 中并对超大数取余;若 j+1 小于 arrLen(表示没有越数组右边界),且 last[j+1] 大于0(表示曾经到达过位置 j+1),则将其加到 cur[j] 中并对超大数取余;若j大于0(表示没有越数组左边界),且 last[j-1] 大于0(表示曾经到达过位置 j-1),则将其加到 cur[j] 中并对超大数取余。遍历完所有位置之后,将 cur 赋值给 last,并进行下一步的循环,最终的结果保存在了 last[0] 中,参见代码如下:
解法一:
class Solution {
public:
int numWays(int steps, int arrLen) {
int M = 1e9 + 7, n = min(steps, arrLen);
vector<long> last(n + 1);
last[0] = 1;
for (int i = 1; i <= steps; ++i) {
vector<long> cur(n + 1);
for (int j = 0; j < n; ++j) {
if (last[j] > 0) {
cur[j] = (cur[j] + last[j]) % M;
}
if (j + 1 < arrLen && last[j + 1] > 0) {
cur[j] = (cur[j] + last[j + 1]) % M;
}
if (j > 0 && last[j - 1] > 0) {
cur[j] = (cur[j] + last[j - 1]) % M;
}
}
last = cur;
}
return last[0];
}
};
我们也可以使用带记忆数组的递归解法,可以用一个二维数组,大小为 steps+1 by steps/2+1,这里为啥要除以2呢?因为最终的目的是回到位置0,所以最多只能向右走 steps/2 步。递归函数的参数有当前剩余步数 steps,当前位置i,数组总长度 arrLen,和记忆数组 memo,首先判断若 steps 和 i 均为0,初始化为1,相当于 dp 数组初始化的种子值。否则继续判断,若i小于0,或者i大于等于 arrLen,表示越界了,应该返回0,或者 steps 等于0了,但由于此时不在位置0,也应该返回0,或者i大于 steps,表示剩余的 steps 步数无法回到位置0,同样应该返回0。否则看当前的状态的 memo 值是否大于0,是的话表示该状态之前计算过了,直接返回 memo[steps][i],否则就要来就计算当前状态的值,通过对 steps-1 步的右边位置,左边位置,和当前位置分别调用递归函数的值相加,然后对超大数取余即可,参见代码如下:
解法二:
class Solution {
public:
int numWays(int steps, int arrLen) {
vector<vector<int>> memo(steps + 1, vector<int>(steps / 2 + 1));
return dfs(steps, arrLen, 0, memo);
}
int dfs(int steps, int arrLen, int i, vector<vector<int>>& memo) {
if (steps == 0 && i == 0) return 1;
if (i < 0 || i >= arrLen || steps == 0 || i > steps) return 0;
if (memo[steps][i] > 0) return memo[steps][i];
int M = 1e9 + 7;
return memo[steps][i] = ((dfs(steps - 1, arrLen, i + 1, memo) % M + dfs(steps - 1, arrLen, i - 1, memo)) % M + dfs(steps - 1, arrLen, i, memo)) % M;
}
};
Github 同步地址:
https://github.com/grandyang/leetcode/issues/1269
参考资料:
https://leetcode.com/problems/number-of-ways-to-stay-in-the-same-place-after-some-steps/