一、题目描述
对于一个整数数组 nums
,逆序对是一对满足 0 <= i < j < nums.length
且 nums[i] > nums[j]
的整数对 [i, j]
。
给你两个整数 n
和 k
,找出所有包含从 1
到 n
的数字,且恰好拥有 k
个 逆序对 的不同的数组的个数。由于答案可能很大,只需要返回对 10^9 + 7
取余的结果。
示例 1:
输入:n = 3, k = 0 输出:1 解释: 只有数组 [1,2,3] 包含了从1到3的整数并且正好拥有 0 个逆序对。
示例 2:
输入:n = 3, k = 1 输出:2 解释: 数组 [1,3,2] 和 [2,1,3] 都有 1 个逆序对。
提示:
1 <= n <= 1000
0 <= k <= 1000
二、解题思路
这个问题可以通过动态规划来解决。动态规划的核心思想是将问题分解为子问题,并保存子问题的解,以避免重复计算。
解题思路如下:
- 定义一个二维数组
dp
,其中dp[i][j]
表示从 1 到 i 的数字组成的数组恰好有 j 个逆序对的方案数。 - 初始化
dp
数组,dp[i][0]
表示从 1 到 i 的数字组成的数组逆序对为 0 的方案数,显然只有一种,即数组按升序排列。 - 对于每一个数字 i(从 1 到 n),我们可以将其插入到之前已经排好序的数组中,插入的位置决定了逆序对的数量。例如,将数字 i 插入到数组
[1, 2, ..., i-1]
中,插入到第 j 个位置会增加 j 个逆序对。 - 根据这个性质,我们可以更新
dp
数组:dp[i][j] = dp[i-1][j] + dp[i-1][j-1] + ... + dp[i-1[0]
,但是直接这样计算会导致时间复杂度过高。我们可以通过前缀和优化这个计算过程。 - 最终答案是
dp[n][k]
。
三、具体代码
class Solution {
public int kInversePairs(int n, int k) {
final int MOD = 1000000007;
int[][] dp = new int[n + 1][k + 1];
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= k; j++) {
dp[i][j] = dp[i - 1][j]; // 当前数字不增加逆序对的情况
if (j > 0) {
dp[i][j] = (dp[i][j] + dp[i][j - 1]) % MOD; // 加上增加一个逆序对的情况
}
if (j >= i) {
dp[i][j] = (dp[i][j] - dp[i - 1][j - i] + MOD) % MOD; // 减去超过 i 个逆序对的情况
}
}
}
return dp[n][k];
}
}
四、时间复杂度和空间复杂度
1. 时间复杂度
- 代码中包含两个嵌套循环,外层循环遍历数字 n(从 1 到 n),内层循环遍历逆序对的数量 k(从 0 到 k)。
- 在内层循环中,除了初始化
dp[i][j]
为dp[i-1][j]
之外,还可能执行两次模运算(一次加法,一次减法),这些操作的时间复杂度都是常数时间 O(1)。 - 因此,内层循环的每一次迭代的时间复杂度是 O(1),总共执行了 n*(k+1) 次。
- 综上所述,整个算法的时间复杂度是 O(n*k)。
2. 空间复杂度
- 代码中使用了一个二维数组
dp
来存储子问题的解,这个数组的大小是 (n+1) 行和 (k+1) 列。 - 因此,数组
dp
占用的空间是 (n+1)*(k+1)。 - 除此之外,算法中没有使用额外的空间,所有的临时变量都是常数空间。
- 综上所述,整个算法的空间复杂度是 O(n*k)。
五、总结知识点
-
动态规划(Dynamic Programming):
- 动态规划是一种算法设计技术,用于解决具有重叠子问题和最优子结构性质的问题。
- 在本代码中,
dp[i][j]
表示从 1 到 i 的数字组成的数组恰好有 j 个逆序对的方案数。
-
二维数组:
- 代码中使用了一个二维数组
dp
来存储中间结果,这是动态规划中常见的做法。
- 代码中使用了一个二维数组
-
模运算(Modulo Operation):
- 由于结果可能非常大,使用模运算来确保结果在 10^9+7 的范围内,这是编程竞赛中常见的技巧。
-
边界条件初始化:
dp[0][0] = 1
初始化为 1,表示没有数字时逆序对的数量为 0 的方案数为 1。
-
嵌套循环:
- 两个嵌套循环用于填充动态规划表,外层循环遍历数字 i,内层循环遍历逆序对的数量 j。
-
状态转移方程:
- 代码中包含了状态转移方程的实现,即如何从一个状态(
dp[i-1][...]
)转移到另一个状态(dp[i][...]
)。
- 代码中包含了状态转移方程的实现,即如何从一个状态(
-
取模运算中的负数处理:
- 在减法操作后立即加上模数,然后取模,是为了防止结果出现负数。
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。