线性动态规划:K个逆序对数组
字数 1691 2025-10-31 08:19:17

线性动态规划:K个逆序对数组

题目描述
给定两个整数 n 和 k,找出所有包含从 1 到 n 的数字,且恰好拥有 k 个逆序对的不同的数组的数量。逆序对的定义是:对于数组中的两个不同位置 i 和 j,如果满足 i < j 且 a[i] > a[j],则其为一个逆序对。由于答案可能很大,只需返回答案对 10^9 + 7 取模的结果。

示例:
输入: n = 3, k = 1
输出: 2
解释: 数组 [1, 3, 2] 和 [2, 1, 3] 都有恰好一个逆序对。

解题过程

1. 问题分析
我们需要计算长度为 n 的排列(由 1 到 n 组成)中,恰好包含 k 个逆序对的排列数量。这是一个典型的计数类动态规划问题,关键在于找到状态定义和状态转移方程。

2. 动态规划状态定义
定义 dp[i][j] 表示使用数字 1 到 i 组成的排列中,恰好包含 j 个逆序对的排列数量。我们的目标是求 dp[n][k]。

3. 状态转移思路
考虑如何从 dp[i-1][] 推导出 dp[i][]。当我们已经有一个由 1 到 i-1 组成的排列时,现在要插入数字 i(当前最大的数字)。这个数字 i 可以插入到排列的多个位置:

  • 插入到最末尾:不会新增任何逆序对
  • 插入到倒数第二个位置:新增 1 个逆序对(与原来的最后一个数字)
  • 插入到倒数第三个位置:新增 2 个逆序对
  • ...
  • 插入到最前面:新增 i-1 个逆序对

因此,当我们插入数字 i 时,根据插入位置的不同,可以新增 0 到 i-1 个逆序对。

4. 状态转移方程
根据上述分析,我们可以得到:
dp[i][j] = dp[i-1][j] + dp[i-1][j-1] + dp[i-1][j-2] + ... + dp[i-1][j-(i-1)]

其中,要求 j ≥ 0 且 j-(i-1) ≥ 0。对于超出范围的索引,其值视为 0。

5. 优化状态转移
直接计算上述求和会导致时间复杂度为 O(n²k),当 n 和 k 较大时可能超时。我们可以使用前缀和优化:

定义前缀和数组 prefix[i][j] = dp[i][0] + dp[i][1] + ... + dp[i][j]

那么状态转移可以简化为:
dp[i][j] = prefix[i-1][j] - prefix[i-1][j-i] (当 j ≥ i 时)
dp[i][j] = prefix[i-1][j] (当 j < i 时)

这样每次状态转移可以在 O(1) 时间内完成。

6. 边界条件

  • dp[0][0] = 1(空排列有 0 个逆序对)
  • 对于 i > 0,dp[i][0] = 1(完全升序排列有 0 个逆序对)
  • 对于 j < 0,dp[i][j] = 0

7. 算法实现步骤

  1. 初始化 dp[0][0] = 1
  2. 对于 i 从 1 到 n:
    • 计算前缀和数组 prefix[i-1]
    • 对于 j 从 0 到 k:
      • 如果 j < i:dp[i][j] = prefix[i-1][j]
      • 否则:dp[i][j] = prefix[i-1][j] - prefix[i-1][j-i]
  3. 返回 dp[n][k] mod (10^9+7)

8. 复杂度分析

  • 时间复杂度:O(nk)
  • 空间复杂度:O(k)(可以使用滚动数组优化)

9. 示例验证
以 n=3, k=1 为例:

  • i=1: dp[1][0]=1, dp[1][其他]=0
  • i=2:
    • dp[2][0] = dp[1][0] = 1
    • dp[2][1] = dp[1][1] + dp[1][0] = 0 + 1 = 1
  • i=3:
    • dp[3][0] = dp[2][0] = 1
    • dp[3][1] = dp[2][1] + dp[2][0] = 1 + 1 = 2

结果与题目示例一致。

线性动态规划:K个逆序对数组 题目描述 给定两个整数 n 和 k,找出所有包含从 1 到 n 的数字,且恰好拥有 k 个逆序对的不同的数组的数量。逆序对的定义是:对于数组中的两个不同位置 i 和 j,如果满足 i < j 且 a[ i] > a[ j ],则其为一个逆序对。由于答案可能很大,只需返回答案对 10^9 + 7 取模的结果。 示例: 输入: n = 3, k = 1 输出: 2 解释: 数组 [ 1, 3, 2] 和 [ 2, 1, 3 ] 都有恰好一个逆序对。 解题过程 1. 问题分析 我们需要计算长度为 n 的排列(由 1 到 n 组成)中,恰好包含 k 个逆序对的排列数量。这是一个典型的计数类动态规划问题,关键在于找到状态定义和状态转移方程。 2. 动态规划状态定义 定义 dp[ i][ j] 表示使用数字 1 到 i 组成的排列中,恰好包含 j 个逆序对的排列数量。我们的目标是求 dp[ n][ k ]。 3. 状态转移思路 考虑如何从 dp[ i-1][ ] 推导出 dp[ i][ ]。当我们已经有一个由 1 到 i-1 组成的排列时,现在要插入数字 i(当前最大的数字)。这个数字 i 可以插入到排列的多个位置: 插入到最末尾:不会新增任何逆序对 插入到倒数第二个位置:新增 1 个逆序对(与原来的最后一个数字) 插入到倒数第三个位置:新增 2 个逆序对 ... 插入到最前面:新增 i-1 个逆序对 因此,当我们插入数字 i 时,根据插入位置的不同,可以新增 0 到 i-1 个逆序对。 4. 状态转移方程 根据上述分析,我们可以得到: dp[ i][ j] = dp[ i-1][ j] + dp[ i-1][ j-1] + dp[ i-1][ j-2] + ... + dp[ i-1][ j-(i-1) ] 其中,要求 j ≥ 0 且 j-(i-1) ≥ 0。对于超出范围的索引,其值视为 0。 5. 优化状态转移 直接计算上述求和会导致时间复杂度为 O(n²k),当 n 和 k 较大时可能超时。我们可以使用前缀和优化: 定义前缀和数组 prefix[ i][ j] = dp[ i][ 0] + dp[ i][ 1] + ... + dp[ i][ j ] 那么状态转移可以简化为: dp[ i][ j] = prefix[ i-1][ j] - prefix[ i-1][ j-i ] (当 j ≥ i 时) dp[ i][ j] = prefix[ i-1][ j] (当 j < i 时) 这样每次状态转移可以在 O(1) 时间内完成。 6. 边界条件 dp[ 0][ 0 ] = 1(空排列有 0 个逆序对) 对于 i > 0,dp[ i][ 0 ] = 1(完全升序排列有 0 个逆序对) 对于 j < 0,dp[ i][ j ] = 0 7. 算法实现步骤 初始化 dp[ 0][ 0 ] = 1 对于 i 从 1 到 n: 计算前缀和数组 prefix[ i-1 ] 对于 j 从 0 到 k: 如果 j < i:dp[ i][ j] = prefix[ i-1][ j ] 否则:dp[ i][ j] = prefix[ i-1][ j] - prefix[ i-1][ j-i ] 返回 dp[ n][ k ] mod (10^9+7) 8. 复杂度分析 时间复杂度:O(nk) 空间复杂度:O(k)(可以使用滚动数组优化) 9. 示例验证 以 n=3, k=1 为例: i=1: dp[ 1][ 0]=1, dp[ 1][ 其他 ]=0 i=2: dp[ 2][ 0] = dp[ 1][ 0 ] = 1 dp[ 2][ 1] = dp[ 1][ 1] + dp[ 1][ 0 ] = 0 + 1 = 1 i=3: dp[ 3][ 0] = dp[ 2][ 0 ] = 1 dp[ 3][ 1] = dp[ 2][ 1] + dp[ 2][ 0 ] = 1 + 1 = 2 结果与题目示例一致。