LeetCode 629. K 个逆序对数组
题目描述

题意分析
用 $1$ 到 $n$ 的每个整数各一次组成排列,统计其中恰好有 $k$ 个逆序对的排列数量。逆序对是位置靠前、数值却更大的一对元素,答案对 $10^9+7$ 取模。
解法:滚动数组 + 前缀窗口优化
核心思路
[!blue]
定义
dp[i][j]为用 $1$ 到 $i$ 组成、恰好有 $j$ 个逆序对的排列数量。把最大元素 $i$ 插入一个由前 $i-1$ 个数组成的排列时,原有元素的相对顺序不变;$i$ 与右侧每个元素都形成逆序对,因此新增数量 $t$ 可以是 $0$ 到 $i-1$。每个插入位置对应唯一的 $t$。反过来,从一个最终排列中删除 $i$,也会唯一确定原排列和新增逆序对数,所以不会重复或遗漏计数。转移为 $dp[i][j]=\sum_{t=0}^{\min(j,i-1)}dp[i-1][j-t]$。
直接对每个状态求和会反复累加重叠区间。用
prev保存上一行时,dp[i][j]就是下标从max(0, j-i+1)到j的连续区间和。让j从小到大增加,每次加入右端prev[j];当j >= i时,再移除刚越过左边界的prev[j-i],便能用一次加减维护新窗口。空排列只有一种且没有逆序对,所以初始化
prev[0] = 1,其他位置为 $0$。每行重新把窗口和设为 $0$,算完后交换前后两行;所有加减都按模数计算,取余为负时再加一次模数,恢复非负结果。
解题步骤
- 创建长度为
k + 1的prev、cur,令prev[0] = 1。- 从 $i=1$ 到 $n$ 枚举排列规模,每行令
window = 0。- 从 $j=0$ 到 $k$ 枚举逆序对数,加入
prev[j],必要时减去prev[j-i],取模后写入cur[j]。- 算完整行后交换数组,让
prev始终指向最新结果,最后返回prev[k]。$k=0$ 时,每一行的零逆序对方案数都是 $1$,对应升序排列。若 $k>n(n-1)/2$,超过排列最多能有的逆序对数,转移得到的相应状态自然为 $0$。
代码实现
class Solution {
// dp[i][j] 到 dp[i-1][j-x] 的转移可由滑动窗口优化成 O(1) 更新。
private static final int MOD = 1_000_000_007;
public int kInversePairs(int n, int k) {
long[] prev = new long[k + 1];
long[] cur = new long[k + 1];
// 空排列只有一种,这是全部计数的起点
prev[0] = 1;
for (int i = 1; i <= n; i++) {
long window = 0;
for (int j = 0; j <= k; j++) {
// 当前右端进入窗口,随后才移出超过新增逆序范围的左端
window += prev[j];
// 移出对应新增 i 个逆序对的项,只允许新增零到 i 减一
if (j >= i) {
window -= prev[j - i];
}
window %= MOD;
// 模意义下的减法可能为负,恢复到非负代表值
if (window < 0) {
window += MOD;
}
cur[j] = window;
}
long[] previous = prev;
// 最新一行交换到 prev,下一轮继续读取它
prev = cur;
cur = previous;
for (int j = 0; j <= k; j++) {
cur[j] = 0;
}
}
return (int) prev[k];
}
}
const mod629 = 1_000_000_007
func kInversePairs(n int, k int) int {
// dp[i][j] 到 dp[i-1][j-x] 的转移可由滑动窗口优化成 O(1) 更新。
prev := make([]int64, k+1)
cur := make([]int64, k+1)
// 空排列只有一种,这是全部计数的起点
prev[0] = 1
for i := 1; i <= n; i++ {
var window int64
for j := 0; j <= k; j++ {
// 当前右端进入窗口,随后才移出超过新增逆序范围的左端
window += prev[j]
// 移出对应新增 i 个逆序对的项,只允许新增零到 i 减一
if j >= i {
window -= prev[j-i]
}
window %= mod629
// 模意义下的减法可能为负,恢复到非负代表值
if window < 0 {
window += mod629
}
cur[j] = window
}
// 最新一行交换到 prev,下一轮继续读取它
prev, cur = cur, prev
for j := 0; j <= k; j++ {
cur[j] = 0
}
}
return int(prev[k])
}
复杂度分析
- 时间复杂度:$O(n(k+1))$,每行 k+1 个状态。
- 空间复杂度:$O(k+1)$,只保留相邻两行。
关键点总结
[!green]
- 窗口最多 i 项,对应新增零到 i−1 个逆序对。
- 模运算中的减法可能为负,需要恢复到非负代表值。
易错点总结
[!yellow]
- 移除
j-i+1而不是j-i,会改变允许的新增逆序数。- 每行不重置窗口,会混入上一行残留。
- 交换后返回旧缓冲,会取错最后一行。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 903. DI 序列的有效排列 | 困难 | 同样按排列的名次关系做计数DP,可用前缀和加速一整段前驱状态的求和。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!