LeetCode 2400. 恰好移动 K 步到达某一位置的方法数 - 动态规划解法详解

题目描述

题目链接恰好移动 K 步到达某一位置的方法数

难度:中等

题目描述

给你两个正整数 startPosendPos 以及一个正整数 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) 为起点和终点之间的绝对距离。

关键观察

  1. 如果 k < d,则不可能到达终点(因为最短路径也需要 d 步)
  2. 如果 (k - d) 是奇数,也不可能到达终点(因为多余的步数必须是偶数才能来回抵消)
  3. 如果 (k - d) 是偶数,则存在可行方案

2. 数学推导

设需要向右移动 r 步,向左移动 l 步,则:

  • r + l = k(总步数)
  • r - l = d(净位移)

解这个方程组:

  • r = (k + d) / 2
  • l = (k - d) / 2

由于 rl 必须是非负整数,所以 (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()

总结

本题的关键在于理解:

  1. 可行性判断k ≥ d(k - d) 为偶数
  2. 组合计数 :在 k 步中选择 (k + d)/2 步向右走
  3. 模运算:使用费马小定理计算组合数的模逆元

推荐使用组合数学方法,时间复杂度最优。动态规划方法虽然效率较低,但有助于理解问题的本质。