恰有 K 根木棍可以看到的排列数目

标签: 数学 动态规划 组合数学

难度: Hard

n 根长度互不相同的木棍,长度为从 1n 的整数。请你将这些木棍排成一排,并满足从左侧 可以看到 恰好 k 根木棍。从左侧 可以看到 木棍的前提是这个木棍的 左侧 不存在比它 更长的 木棍。

  • 例如,如果木棍排列为 [1,3,2,5,4] ,那么从左侧可以看到的就是长度分别为 135 的木棍。

给你 nk ,返回符合题目要求的排列 数目 。由于答案可能很大,请返回对 109 + 7 取余 的结果。

 

示例 1:

输入:n = 3, k = 2
输出:3
解释:[1,3,2], [2,3,1] 和 [2,1,3] 是仅有的能满足恰好 2 根木棍可以看到的排列。
可以看到的木棍已经用粗体+斜体标识。

示例 2:

输入:n = 5, k = 5
输出:1
解释:[1,2,3,4,5] 是唯一一种能满足全部 5 根木棍可以看到的排列。
可以看到的木棍已经用粗体+斜体标识。

示例 3:

输入:n = 20, k = 11
输出:647427950
解释:总共有 647427950 (mod 109 + 7) 种能满足恰好有 11 根木棍可以看到的排列。

 

提示:

  • 1 <= n <= 1000
  • 1 <= k <= n

Submission

运行时间: 207 ms

内存: 69.5 MB

INT = 10 ** 9 + 7
@cache
def factorial(k):
    if k == 1: return 1
    return k * factorial(k-1) % INT
@cache
def getRes(n, k):
    if n == k: return 1
    if k == 1: return factorial(n-1)
    return (getRes(n-1, k) * (n-1) + getRes(n-1, k-1)) % INT
class Solution:
    def rearrangeSticks(self, n: int, k: int) -> int:
        return getRes(n, k)

Explain

此题解采用了递归+记忆化的方法来解决问题。首先,定义一个递归函数 `getRes(n, k)` 来计算有n根木棍时,恰好有k根木棍可见的排列数。对于基本情况,如果要求的可见木棍数k等于总棍数n,则只有一种排列满足要求。如果k为1,则任何排列中最长的木棍都必须放在最前面,此后的n-1根木棍可以任意排列,因此是(n-1)!种排列。对于一般情况,将问题分为两部分:1) 第n根木棍不增加可见木棍数的情况,此时它必须被前面的某根木棍遮挡,因此这种情况下的排列数为 `getRes(n-1, k) * (n-1)`;2) 第n根木棍增加可见木棍数的情况,即它是可见的,这种情况下的排列数为 `getRes(n-1, k-1)`。最终结果是这两种情况之和,模10^9+7。

时间复杂度: O(n * k)

空间复杂度: O(n * k)

python
from functools import lru_cache

INT = 10**9 + 7

@lru_cache(None)
def factorial(k):
    if k == 1: return 1  # 只有1的阶乘是1
    return k * factorial(k-1) % INT  # 递归计算k的阶乘,并取模

@lru_cache(None)
def getRes(n, k):
    if n == k: return 1  # 全部木棍都可见,只有一种排列
    if k == 1: return factorial(n-1)  # 只有一根木棍可见,计算(n-1)!
    # 两种情况:n不是可见的,n是新增加的可见木棍
    return (getRes(n-1, k) * (n-1) + getRes(n-1, k-1)) % INT

class Solution:
    def rearrangeSticks(self, n: int, k: int) -> int:
        return getRes(n, k)  # 获取结果

Explore

当`n < k`时,不可能有超过n根木棍可见,因为可见的木棍数不可能超过木棍的总数。因此,在这种情况下,应返回0,表示不存在这样的排列。这是一个边界情况,应在函数`getRes`中明确处理以避免错误的递归调用。

在`getRes(n-1, k) * (n-1)`这种情况下,考虑第n根木棍不是新的可见木棍(即它被之前的某一根木棍遮挡)。`getRes(n-1, k)`计算了n-1根木棍中有k根可见的排列数。乘以`(n-1)`的原因是,第n根木棍可以插入到前n-1根木棍的任何一个位置(除了最前面),而不改变原有的可见木棍数量,共有n-1种选择。

`lru_cache`是Python标准库`functools`中的一个装饰器,用于实现记忆化功能。它缓存装饰函数的调用结果,当函数用相同的参数再次被调用时,直接从缓存中返回结果,而不需要重新执行函数。这极大地提高了递归函数的性能,特别是在处理有大量重复子问题的递归调用时,如本题中的`getRes`和`factorial`。通过避免重复计算,减少了时间复杂度,使得算法能够在可接受的时间内解决问题。

在数学中,0的阶乘定义为1 (`0! = 1`)。题解中的`factorial`函数没有显式处理k为0的情况,这可能导致在特定情况下调用`factorial(0)`时出现错误。为了完善函数并避免潜在问题,应该在函数开始添加一个条件,检查k是否为0,如果是,则返回1。这样可以确保函数在所有情况下都正确运行。