Medium
题目描述
国际象棋中的骑士有独特的移动方式,它可以垂直移动两个方格并水平移动一个方格,或者水平移动两个方格并垂直移动一个方格(两种移动都形成L形)。
我们有一个国际象棋骑士和一个电话拨号盘,如下所示,骑士只能站在数字单元格上(即蓝色单元格)。
1 2 3
4 5 6
7 8 9
0
给定一个整数 n,返回我们可以拨打的长度为 n 的不同电话号码的数量。
你可以将骑士放在任何数字单元格上作为起始位置,然后你需要执行 n - 1 次跳跃来拨打长度为 n 的号码。所有跳跃都必须是有效的骑士跳跃。
由于答案可能非常大,请返回答案模 10^9 + 7 的结果。
示例 1:
输入: n = 1
输出: 10
解释: 我们需要拨打长度为1的号码,所以将骑士放在10个单元格中的任何一个都足够了。
示例 2:
输入: n = 2
输出: 20
解释: 所有有效的号码是 [04, 06, 16, 18, 27, 29, 34, 38, 40, 43, 49, 60, 61, 67, 72, 76, 81, 83, 92, 94]
示例 3:
输入: n = 3131
输出: 136006598
解释: 请注意取模。
约束条件:
- 1 <= n <= 5000
解题思路
这是一道经典的动态规划问题。我们需要分析骑士在电话拨号盘上的移动规律。
首先,我们需要建立数字键盘的布局映射关系。观察电话拨号盘:
1 2 3
4 5 6
7 8 9
0
根据骑士的移动规则(L形移动),我们可以构建每个数字能够到达的相邻数字列表:
- 0 → [4, 6]
- 1 → [6, 8]
- 2 → [7, 9]
- 3 → [4, 8]
- 4 → [0, 3, 9]
- 5 → [] (无法到达任何位置)
- 6 → [0, 1, 7]
- 7 → [2, 6]
- 8 → [1, 3]
- 9 → [2, 4]
动态规划思路:
定义 dp[i][j] 表示经过 i 步跳跃后,骑士位于数字 j 上的方案数。
状态转移方程:dp[i][j] = sum(dp[i-1][k]), 其中 k 是能够一步到达 j 的所有数字。
初始状态:dp[0][j] = 1 (对所有数字 j),表示第一步可以选择任意数字。
最终答案是 sum(dp[n-1][j]) 对所有 j 的总和。
为了优化空间复杂度,我们可以使用滚动数组,只保存当前步和前一步的状态。
代码实现
class Solution {
public:
int knightDialer(int n) {
const int MOD = 1000000007;
// 每个数字能够到达的下一个数字
vector<vector<int>> moves = {
{4, 6}, // 0
{6, 8}, // 1
{7, 9}, // 2
{4, 8}, // 3
{0, 3, 9}, // 4
{}, // 5
{0, 1, 7}, // 6
{2, 6}, // 7
{1, 3}, // 8
{2, 4} // 9
};
// 当前状态:每个数字位置的方案数
vector<long long> prev(10, 1);
for (int step = 1; step < n; step++) {
vector<long long> curr(10, 0);
for (int digit = 0; digit <= 9; digit++) {
for (int next : moves[digit]) {
curr[next] = (curr[next] + prev[digit]) % MOD;
}
}
prev = curr;
}
long long result = 0;
for (int i = 0; i <= 9; i++) {
result = (result + prev[i]) % MOD;
}
return result;
}
};
class Solution:
def knightDialer(self, n: int) -> int:
MOD = 10**9 + 7
# 每个数字能够到达的下一个数字
moves = {
0: [4, 6],
1: [6, 8],
2: [7, 9],
3: [4, 8],
4: [0, 3, 9],
5: [],
6: [0, 1, 7],
7: [2, 6],
8: [1, 3],
9: [2, 4]
}
# 当前状态:每个数字位置的方案数
prev = [1] * 10
for step in range(1, n):
curr = [0] * 10
for digit in range(10):
for next_digit in moves[digit]:
curr[next_digit] = (curr[next_digit] + prev[digit]) % MOD
prev = curr
return sum(prev) % MOD
public class Solution {
public int KnightDialer(int n) {
const int MOD = 1000000007;
// 每个数字能够到达的下一个数字
int[][] moves = new int[][] {
new int[] {4, 6}, // 0
new int[] {6, 8}, // 1
new int[] {7, 9}, // 2
new int[] {4, 8}, // 3
new int[] {0, 3, 9}, // 4
new int[] {}, // 5
new int[] {0, 1, 7}, // 6
new int[] {2, 6}, // 7
new int[] {1, 3}, // 8
new int[] {2, 4} // 9
};
// 当前状态:每个数字位置的方案数
long[] prev = new long[10];
Array.Fill(prev, 1);
for (int step = 1; step < n; step++) {
long[] curr = new long[10];
for (int digit = 0; digit <= 9; digit++) {
foreach (int next in moves[digit]) {
curr[next] = (curr[next] + prev[digit]) % MOD;
}
}
prev = curr;
}
long result = 0;
for (int i = 0; i <= 9; i++) {
result = (result + prev[i]) % MOD;
}
return (int)result;
}
}
var knightDialer = function(n) {
const MOD = 1000000007;
// 每个数字能够到达的下一个数字
const moves = {
0: [4, 6],
1: [6, 8],
2: [7, 9],
3: [4, 8],
4: [0, 3, 9],
5: [],
6: [0, 1, 7],
7: [2, 6],
8: [1, 3],
9: [2, 4]
};
// 当前状态:每个数字位置的方案数
let prev = new Array(10).fill(1);
for (let step = 1; step < n; step++) {
const curr = new Array(10).fill(0);
for (let digit = 0; digit <= 9; digit++) {
for (const next of moves[digit]) {
curr[next] = (curr[next] + prev[digit]) % MOD;
}
}
prev = curr;
}
return prev.reduce((sum, count) => (sum + count) % MOD, 0);
};
复杂度分析
| 复杂度类型 | 分析 |
|---|---|
| 时间复杂度 | O(n) - 需要进行 n-1 轮状态转移,每轮遍历10个数字及其邻接关系 |
| 空间复杂度 | O(1) - 只使用了固定大小的数组存储状态,与输入规模无关 |