题目描述
题目链接 :恰好移动 K 步到达某一位置的方法数
难度:中等
题目描述 :
给你两个正整数 startPos 和 endPos 以及一个正整数 k。最初,你站在无限数轴上位置 startPos 处。在一步移动中,你可以向左或者向右移动一个位置。
请你返回恰好用 k 步从 startPos 走到 endPos 的 不同 方法数目。由于答案可能会很大,请你将它对 10^9 + 7 取余后返回。
注意:数轴包含负整数。
示例 1:
输入:startPos = 1, endPos = 2, k = 3
输出:3
解释:存在 3 种从 1 到 2 且恰好移动 3 步的方法:
- 1 -> 2 -> 3 -> 2
- 1 -> 2 -> 1 -> 2
- 1 -> 0 -> 1 -> 2
可以证明不存在其他方法,所以答案是 3。
示例 2:
输入:startPos = 2, endPos = 5, k = 10
输出:0
解释:不存在从 2 到 5 且恰好移动 10 步的方法。
提示:
1 <= startPos, endPos, k <= 1000
自行学习
排列组合公式
排列和组合是从 n n n 个不同元素里取 k k k 个的两种基本计数方式,区别只在"是否计较顺序"。
排列(Permutation)
记作 A n k A_n^k Ank 或 P ( n , k ) P(n,k) P(n,k),讲究顺序,相当于"挑出来再排成一队":
A n k = n ! ( n − k ) ! = n ( n − 1 ) ( n − 2 ) ⋯ ( n − k + 1 ) A_n^k = \frac{n!}{(n-k)!} = n(n-1)(n-2)\cdots(n-k+1) Ank=(n−k)!n!=n(n−1)(n−2)⋯(n−k+1)
直观理解:第一个位置 n n n 种选法,第二个位置剩 n − 1 n-1 n−1 种,第 k k k 个位置剩 n − k + 1 n-k+1 n−k+1 种,乘起来就是连乘 k k k 项。
组合(Combination)
记作 C n k C_n^k Cnk 或 ( n k ) \binom{n}{k} (kn),不讲顺序,相当于"只挑出来不排队":
( n k ) = n ! k ! ( n − k ) ! = A n k k ! \binom{n}{k} = \frac{n!}{k!\,(n-k)!} = \frac{A_n^k}{k!} (kn)=k!(n−k)!n!=k!Ank
直观理解:先按排列算出 A n k A_n^k Ank,但每一组 k k k 个元素被它自身的 k ! k! k! 种内部顺序重复计算了,所以除掉 k ! k! k!。
两者的关系
A n k = ( n k ) ⋅ k ! A_n^k = \binom{n}{k} \cdot k! Ank=(kn)⋅k!
常用恒等式
| 名称 | 公式 |
|---|---|
| 对称性 | ( n k ) = ( n n − k ) \binom{n}{k} = \binom{n}{n-k} (kn)=(n−kn) |
| 帕斯卡公式(杨辉三角递推) | ( n k ) = ( n − 1 k − 1 ) + ( n − 1 k ) \binom{n}{k} = \binom{n-1}{k-1} + \binom{n-1}{k} (kn)=(k−1n−1)+(kn−1) |
| 行和 | ∑ k = 0 n ( n k ) = 2 n \sum_{k=0}^{n}\binom{n}{k} = 2^n ∑k=0n(kn)=2n |
| 二项式定理 | ( x + y ) n = ∑ k = 0 n ( n k ) x k y n − k (x+y)^n = \sum_{k=0}^{n}\binom{n}{k} x^k y^{n-k} (x+y)n=∑k=0n(kn)xkyn−k |
| 范德蒙德卷积 | ( m + n k ) = ∑ i = 0 k ( m i ) ( n k − i ) \binom{m+n}{k} = \sum_{i=0}^{k}\binom{m}{i}\binom{n}{k-i} (km+n)=∑i=0k(im)(k−in) |
| 吸收/提取恒等式 | k ( n k ) = n ( n − 1 k − 1 ) k\binom{n}{k} = n\binom{n-1}{k-1} k(kn)=n(k−1n−1) |
| 上指标求和(hockey stick) | ∑ i = k n ( i k ) = ( n + 1 k + 1 ) \sum_{i=k}^{n}\binom{i}{k} = \binom{n+1}{k+1} ∑i=kn(ki)=(k+1n+1) |
约定
- ( n 0 ) = 1 \binom{n}{0} = 1 (0n)=1
- k < 0 k < 0 k<0 或 k > n k > n k>n 时 ( n k ) = 0 \binom{n}{k} = 0 (kn)=0
- 0 ! = 1 0! = 1 0!=1
模 10 9 + 7 10^9+7 109+7 下计算 ( n k ) \binom{n}{k} (kn) 的标准模板
python
MOD = 10**9 + 7
N = 1000 # 预处理上界,取题目里 k 的最大值即可
fact = [1] * (N + 1)
for i in range(1, N + 1):
fact[i] = fact[i-1] * i % MOD
inv_fact = [1] * (N + 1)
inv_fact[N] = pow(fact[N], MOD - 2, MOD) # 费马小定理求逆元
for i in range(N - 1, -1, -1):
inv_fact[i] = inv_fact[i+1] * (i+1) % MOD
def C(n, k):
if k < 0 or k > n:
return 0
return fact[n] * inv_fact[k] % MOD * inv_fact[n-k] % MOD
只要逆元打表打好,单次查询 O ( 1 ) O(1) O(1),整个组合数题基本都靠这一套模板。
回到 LeetCode 那题
Number of Ways to Reach a Position After Exactly k Steps:
- d = ∣ e n d P o s − s t a r t P o s ∣ d = |endPos - startPos| d=∣endPos−startPos∣
- 设向终点方向走 r r r 步,反方向走 l l l 步
- r + l = k r + l = k r+l=k, r − l = d r - l = d r−l=d ⇒ r = ( k + d ) / 2 r = (k+d)/2 r=(k+d)/2
- 需满足 k ≥ d k \ge d k≥d 且 k + d k + d k+d 为偶数,否则答案 0 0 0
- 答案 = ( k r ) m o d ( 10 9 + 7 ) = \binom{k}{r} \bmod (10^9 + 7) =(rk)mod(109+7)
CSDN问题分析
1. 核心观察
这是一个典型的组合计数问题,需要从 startPos 出发,经过恰好 k 步到达 endPos。每一步只能向左或向右移动一个单位。
设 d = abs(endPos - startPos) 为起点和终点之间的绝对距离。
关键观察:
- 如果
k < d,则不可能到达终点(因为最短路径也需要d步) - 如果
(k - d)是奇数,也不可能到达终点(因为多余的步数必须是偶数才能来回抵消) - 如果
(k - d)是偶数,则存在可行方案
2. 数学推导
设需要向右移动 r 步,向左移动 l 步,则:
r + l = k(总步数)r - l = d(净位移)
解这个方程组:
r = (k + d) / 2l = (k - d) / 2
由于 r 和 l 必须是非负整数,所以 (k + d) 和 (k - d) 都必须是偶数,且 k ≥ d。
如果满足条件,那么问题就转化为:在 k 步中,选择 r 步向右移动,其余 l 步向左移动。这是一个组合问题:
方法数 = C(k, r) = C(k, (k + d) / 2)
其中 C(n, m) 表示组合数。
解决方案
方法一:组合数学(直接计算)
python
class Solution:
def numberOfWays(self, startPos: int, endPos: int, k: int) -> int:
MOD = 10**9 + 7
d = abs(endPos - startPos)
# 不可能到达的情况
if k < d or (k - d) % 2 != 0:
return 0
r = (k + d) // 2
l = (k - d) // 2
# 计算组合数 C(k, r) mod MOD
# 使用预计算阶乘和逆元的方法
def comb(n, m, mod):
if m < 0 or m > n:
return 0
# 预计算阶乘和逆元
fact = [1] * (n + 1)
inv_fact = [1] * (n + 1)
for i in range(1, n + 1):
fact[i] = fact[i-1] * i % mod
# 费马小定理求逆元
inv_fact[n] = pow(fact[n], mod-2, mod)
for i in range(n-1, -1, -1):
inv_fact[i] = inv_fact[i+1] * (i+1) % mod
return fact[n] * inv_fact[m] % mod * inv_fact[n-m] % mod
return comb(k, r, MOD)
时间复杂度 :O(k),需要预计算阶乘
空间复杂度:O(k),存储阶乘数组
方法二:动态规划
python
class Solution:
def numberOfWays(self, startPos: int, endPos: int, k: int) -> int:
MOD = 10**9 + 7
d = abs(endPos - startPos)
# 不可能到达的情况
if k < d or (k - d) % 2 != 0:
return 0
# 动态规划:dp[step][offset] 表示走了 step 步,距离起点 offset 的方法数
# 由于数轴无限,我们只需要考虑相对位置
offset = d + k # 确保索引非负
dp = [[0] * (2 * offset + 1) for _ in range(k + 1)]
# 初始状态:0 步时在起点
dp[0][offset] = 1
for step in range(1, k + 1):
for pos in range(-offset, offset + 1):
idx = pos + offset
# 可以从左边或右边走过来
if idx - 1 >= 0:
dp[step][idx] = (dp[step][idx] + dp[step-1][idx-1]) % MOD
if idx + 1 < len(dp[0]):
dp[step][idx] = (dp[step][idx] + dp[step-1][idx+1]) % MOD
# 最终位置:距离起点 d
return dp[k][d + offset]
时间复杂度 :O(k²)
空间复杂度:O(k²)
方法三:优化空间复杂度的动态规划
python
class Solution:
def numberOfWays(self, startPos: int, endPos: int, k: int) -> int:
MOD = 10**9 + 7
d = abs(endPos - startPos)
if k < d or (k - d) % 2 != 0:
return 0
# 使用滚动数组优化空间
offset = k
dp_prev = [0] * (2 * offset + 1)
dp_curr = [0] * (2 * offset + 1)
dp_prev[offset] = 1 # 起点在中间
for step in range(1, k + 1):
for pos in range(-offset, offset + 1):
idx = pos + offset
dp_curr[idx] = 0
if idx - 1 >= 0:
dp_curr[idx] = (dp_curr[idx] + dp_prev[idx-1]) % MOD
if idx + 1 < len(dp_curr):
dp_curr[idx] = (dp_curr[idx] + dp_prev[idx+1]) % MOD
dp_prev, dp_curr = dp_curr, dp_prev
return dp_prev[d + offset]
时间复杂度 :O(k²)
空间复杂度:O(k)
算法比较
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 组合数学 | O(k) | O(k) | 推荐,效率最高 |
| 动态规划 | O(k²) | O(k²) | 理解问题本质 |
| 优化DP | O(k²) | O(k) | 空间优化版 |
测试用例
python
def test():
solution = Solution()
# 示例 1
assert solution.numberOfWays(1, 2, 3) == 3
# 示例 2
assert solution.numberOfWays(2, 5, 10) == 0
# 边界情况
assert solution.numberOfWays(1, 1, 0) == 1 # 起点终点相同,0步
assert solution.numberOfWays(1, 2, 1) == 1 # 直接一步到达
assert solution.numberOfWays(1, 3, 2) == 1 # 必须向右走两步
print("所有测试用例通过!")
# 运行测试
test()
总结
本题的关键在于理解:
- 可行性判断 :
k ≥ d且(k - d)为偶数 - 组合计数 :在
k步中选择(k + d)/2步向右走 - 模运算:使用费马小定理计算组合数的模逆元
推荐使用组合数学方法,时间复杂度最优。动态规划方法虽然效率较低,但有助于理解问题的本质。