程序员老鬼

被裁员被退婚,也是被我摊上了~

刚看到个贴子,姑娘年底被裁员不说,还赶上订婚告吹,理由竟然是——她婚前自己买了套房。

Image

网友回帖里有说男方小气的,也有说姑娘“多虑”的,但我看了看,核心矛盾很简单:她想给自己留点安全感,男方却觉得这是“不信任”。

这不是沟通问题,这是三观不匹配的问题。婚前买房在现代人眼里就像给手机贴个膜,图个踏实,又不是为了防谁。

很多网友替姑娘惋惜,但我反而觉得,这是上天帮她提前避个雷。房子是她自己的,装修钱她也愿意拿,本来已经很有诚意了,还能被否定,那以后遇到事儿更难谈。

不过话说回来,30岁被催婚的压力确实烦,但为了不被催而将就,更亏。与其急着把自己塞进不合适的关系,不如等一个愿意跟你并肩、而不是控制你的人。【备注:文末可领最新资料】

面试题:K 个逆序对数组

题目叫「K 个逆序对数组」,一般完整说法是:

给你两个整数 n 和 k,问把数字 1 ~ n 排成一个数组,有多少种排列,它们里面刚好有 k 个逆序对。结果通常要对 1e9+7 取模。

逆序对是啥? 很简单:下标 i < j,但 nums[i] > nums[j],这一对 (i, j) 就算一个。

举个特别小的例子:n = 3,所有排列:

  • [1,2,3]:0 个逆序对
  • [1,3,2]:1 个(3,2)
  • [2,1,3]:1 个(2,1)
  • [2,3,1]:2 个(2,1)(3,1)
  • [3,1,2]:2 个(3,1)(3,2)
  • [3,2,1]:3 个

所以如果 k=2,答案就是 2 种。

暴力枚举全排列再数逆序对肯定不行,n 一大就炸了,这题标准解是动态规划 + 一点小优化。

怎么想到用 DP?

一个很自然的想法是: 我们不是一次性把 1..n 都排好,而是一个一个往里插。

先只看数字 1:

  • 只有一种排法 [1],逆序对数只能是 0

再看数字 1,2:

  • 在 [1] 里插入 2,可以插在:

    • 最后:[1,2],新产生 0 个逆序对
    • 开头:[2,1],新产生 1 个逆序对(2 > 1)

再看 1,2,3:

  • 在每一个 2 个数的排列里,把 3 插到不同的位置
  • 插到最右边不会新增逆序对,越往左插,新产生的逆序对就越多

于是有个很关键的观察:

当我们把数字 i 放进前面已经排好的 1..i-1 里时, 最多会新产生 i-1 个逆序对(把它塞到最左边)。

这就非常适合 DP 去枚举「我今天多加了几个逆序对」。

状态怎么设计?

经典写法:

  • dp[i][j] 表示: 用数字 1..i 能组成的、刚好有 j 个逆序对 的排列个数

考虑把第 i 个数插进来:

  • 它可以插在最右边 → 新增 0 个逆序对
  • 往左移动一格 → 新增 1 个
  • ……
  • 最左边 → 新增 i-1 个

也就是说: 如果我最后的逆序对总数是 j,那:

  • 这一次我新增了 x 个(x 取值 0 ~ min(j, i-1))
  • 那之前 1..i-1 必须有 j - x 个

翻译成公式就是:

dp[i][j] = dp[i-1][j] 
         + dp[i-1][j-1] 
         + ... 
         + dp[i-1][j - (i-1)]

这样做有个问题: 如果你真的每次都把这一长串加一遍,时间复杂度会变成 O(n * k * n),也就是 O(n^2 * k),肯定会超时。

怎么办?用前缀和把这一坨加法变成 O(1)。

前缀和怎么优化?

注意上面的式子,其实就是「一段连续区间的和」。

我们可以这样写一个等价的形式(把区间滑动展开):

dp[i][j] = sum_{x=0..min(j, i-1)} dp[i-1][j-x]

这玩意儿其实是 dp[i-1] 的一个「滑动窗口和」。 于是用前缀和数组 pre:

pre[j] = dp[i-1][0] + dp[i-1][1] + ... + dp[i-1][j]

那:

  • 如果 j < i:dp[i][j] = pre[j]
  • 否则:dp[i][j] = pre[j] - pre[j-i]    (把多出来的那段减掉)

再记得整个过程都要对 MOD = 1e9+7 取模, 减法后如果是负数补回去就行。

初始条件:

  • 用 1 个数,只有一种数组 [1],逆序对只能是 0

    • dp[1][0] = 1
  • 一般也可以从 dp[0][0] = 1 开始推,更统一。

最终答案:dp[n][k]。

时间复杂度:

  • 外层 i = 1..n
  • 内层 j = 0..k
  • 每次 O(1) → 整体 O(n * k),这就是这题能过的大致极限。

按上面的思路写个比较干净的 Java 版本:

classSolution{
privatestaticfinalint MOD = 1_000_000_007;

publicintkInversePairs(int n, int k){
// dp[i][j] 只跟 i-1 行有关,可以做成滚动数组
int[] dp = newint[k + 1];
int[] pre = newint[k + 1];

        dp[0] = 1; // 用 1 个数(其实这里是 i=1 时的前一行 i=0):只有 0 个逆序对

for (int i = 1; i <= n; i++) {
// 先根据上一行 dp 构建前缀和
            pre[0] = dp[0];
for (int j = 1; j <= k; j++) {
                pre[j] = pre[j - 1] + dp[j];
if (pre[j] >= MOD) pre[j] -= MOD;
            }

// newDp 是当前这一行的 dp[i][*]
int[] newDp = newint[k + 1];

for (int j = 0; j <= k; j++) {
// 这一行的 j 不能比最大可能的逆序对还大
// 最大逆序对是 i * (i - 1) / 2
if (j > i * (i - 1) / 2) {
                    newDp[j] = 0;
continue;
                }

// 取 dp[i-1][j] + ... + dp[i-1][j - (i-1)]
int left = j - (i - 1);
if (left <= 0) {
                    newDp[j] = pre[j];
                } else {
                    newDp[j] = pre[j] - pre[left - 1];
if (newDp[j] < 0) newDp[j] += MOD;
                }
            }

            dp = newDp;
        }

return k < 0 ? 0 : dp[k];
    }
}

上面这版注意了几件小事:

  • 用滚动数组节省空间,只留一行 dp
  • 每层 i 再开一个 newDp,算完之后丢给 dp
  • 加减都要记得取模、处理负数
  • 顺便剪枝:j > i*(i-1)/2 的位置直接设为 0,省点计算

大概就这么回事儿,这题主要难在「想到插入第 i 个数会多出 0..i-1 个逆序对」,然后把那堆和用前缀和优化掉,一通顺下来就挺自然的。你要是愿意,还可以自己拿 n=3,4 小数据,把中间的 dp 表手算几行,对照一下更好理解。

-END-

我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html

最后给大家分享一份不错的副业资料,点击下方公众号,回复关键字: 副业 领,也可以链接我微信:hls404