LeetCode 629 - K Inverse Pairs Array

題目

題目連結:https://leetcode.com/problems/k-inverse-pairs-array/

給定 NN 以及 KK,求出包含 1N1\sim N 的序列中,有多少種恰好有 KK 個逆序數對。

答案要對 109+710^9 + 7 取餘。

範例說明

Example 1:

1
2
3
Input: n = 3, k = 0
Output: 1
Explanation: Only the array [1,2,3] which consists of numbers from 1 to 3 has exactly 0 inverse pairs.

Example 2:

1
2
3
Input: n = 3, k = 1
Output: 2
Explanation: The array [1,3,2] and [2,1,3] have exactly 1 inverse pair.

想法

假設我們要產生 1N1\sim N 的任意序列,我們可以從第一個位置開始擺起,假設 N=3N=3K=2K=2,則:

  1. 若第一個數字是 1,則我們可以知道不管剩下的數字怎麼擺,1 與剩下數字皆不會產生逆序數對。因此若剩下的兩個數字中能產生 k0=20=2k-0=2-0=2 組逆序數對,就滿足我們 N=3N=3K=2K=2 的要求。
  2. 若第一個數字是 2,則我們可以知道不管剩下的數字怎麼擺,2 與剩下的數字一定會產生一組逆序數對 (2,1)(2,1),因此若剩下的兩個數字中能產生 k1=21=1k-1=2-1=1 組逆序數對,就能滿足我們 N=3N=3K=2K=2 的需求。
  3. 若第一個數字是 3,則我們可以知道不管剩下的數字怎麼擺,3 與剩下的數字一定會產生兩組逆序數對 (3,1)(3,1)(3,2)(3,2),因此若剩下的兩個數字中能產生 k2=22=0k-2=2-2=0 組逆序數對,就能滿足我們 N=3N=3K=2K=2 的需求。

因此我們可以定義 dp(i,j)dp(i,j) 為長度為 ii 的序列中恰好有 jj 個逆序數對的序列數量。則我們知道長度為 ii 的序列中,第一個數字有 ii 種選擇:

  • 若第一個數字選擇第 1 小的數字,第一個數字不會與後面的任何數字產生逆序數對,因此 dp(i,j)=dp(i1,j)dp(i,j)=dp(i-1,j)
  • 若第一個數字選擇第 2 小的數字,第一個數字恰好與後面的所有數字產生 1 組逆序數對,因此 dp(i,j)=dp(i1,j1)dp(i,j)=dp(i-1,j-1)
  • 若第一個數字選擇第 ii 小的數字(也就是最大的),第一個數字恰好與後面的 i1i-1 個數字都產生逆序數對,因此 dp(i,j)=dp(i1,ji+1)dp(i,j)=dp(i-1,j-i+1)

因此總結來說轉移即為:

dp(i,j)=k=1idp(i1,jk+1)\red{dp(i,j)=\sum_{k=1}^{i}dp(i-1,j-k+1)}

舉例來說:

  • dp(3,0)=dp(2,0)+dp(2,1)+dp(2,2)dp(3,0)=dp(2,0)+dp(2,-1)+dp(2,-2)
  • dp(3,1)=dp(2,1)+dp(2,0)+dp(2,1)dp(3,1)=dp(2,1)+dp(2,0)+dp(2,-1)
  • dp(3,2)=dp(2,2)+dp(2,1)+dp(2,0)dp(3,2)=dp(2,2)+dp(2,1)+dp(2,0)
  • dp(3,3)=dp(2,3)+dp(2,2)+dp(2,1)dp(3,3)=dp(2,3)+dp(2,2)+dp(2,1)
  • dp(3,4)=dp(2,4)+dp(2,3)+dp(2,2)dp(3,4)=dp(2,4)+dp(2,3)+dp(2,2)

因此我們即可開始實作。

實作細節

時間複雜度優化

根據上面的轉移式,我們可以很簡單的做出 O(NK2)O(N\cdot K^2) 的 DP,pseudo code 如下(這裡先不考慮邊界問題):

1
2
3
4
5
6
7
for (int i = 1; i <= N; ++i) {
for (int j = 1; j <= K; ++j) {
for (int k = 1; k <= i; ++k) {
dp[i][j] += dp[i - 1][j - k + 1];
}
}
}

但很顯然觀察上面列出的轉移,我們可以觀察到每次計算 dp(i,j)dp(i,j) 時,並不需要都獨立的用一個 k=1ik=1\sim i 的迴圈計算,而可以從 dp(i,j1)dp(i,j-1) 來轉移:

例如計算 dp(3,3)=dp(2,3)+dp(2,2)+dp(2,1)dp(3,3)=dp(2,3)+dp(2,2)+dp(2,1) 時,我們可以透過 dp(3,2)=dp(2,2)+dp(2,1)+dp(2,0)dp(3,2)=dp(2,2)+dp(2,1)+dp(2,0),得到 dp(3,3)=dp(3,2)+dp(2,3)dp(2,0)dp(3,3)=dp(3,2)+dp(2,3)-dp(2,0)

也就是說我們可以將轉移式簡化為:

dp(i,j)=dp(i,j1)+dp(i1,j)dp(i1,ji)\red{dp(i,j)=dp(i,j-1)+dp(i-1,j)-dp(i-1,j-i)}

因此中間的 kk 迴圈即可被省去,時間複雜度可以降到 O(NK)O(N \cdot K)

實作時,要注意:

  • 邊界 jij-i 可能會小於 0,由於我們知道不可能會有負數個逆序數對的情況,因此對於所有 j<0j \lt 0dp(i,j)=0dp(i,j)=0
  • 所有的 dp(i,0)=1dp(i,0)=1(長度為 ii 且沒有逆序數對的序列只有一種)。

最後的答案即為 dp(N,K)dp(N,K)

空間複雜度優化

我們可以開一個 (N+1)×(K+1)(N + 1)\times (K+1) 大小的陣列來儲存所有的 DP 狀態,但是透過上述的轉移式可以發現,在計算 dp(i,j)dp(i,j) 時,只會用到 dp(i1,j)dp(i-1,j)dp(i,j1)dp(i,j-1) 以及 dp(i1,ji)dp(i-1,j-i) 這三個值,因此我們只需要保留兩個 rows 的 DP 狀態就足夠了。

因此筆者在實作時使用 pre 陣列代表 dp(i1)dp(i-1)cur 陣列代表 dp(i)dp(i),每次 ii 迴圈後交換兩個陣列的指標即可。

如此一來可以把空間複雜度從 O(NK)O(N \cdot K) 降到 O(K)O(K)

程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* Author: justin0u0<[email protected]>
* Problem: https://leetcode.com/problems/k-inverse-pairs-array/
* Runtime: 33ms
* Time Complexity: O(NK)
* Space Complexity: O(K)
*/

class Solution {
private:
const int mod = 1e9 + 7;
public:
int kInversePairs(int n, int k) {
vector<int>* pre = new vector<int>(k + 1, 0);
vector<int>* cur = new vector<int>(k + 1, 0);
(*pre)[0] = 1;
(*cur)[0] = 1;

for (int i = 1; i <= n; ++i) {
swap(pre, cur); // cur->dp[i], pre->dp[i-1]
for (int j = 1; j <= k; ++j) {
if (j < i) {
// j - i < 0, so dp(i-1,j-i) = 0
(*cur)[j] = ((*cur)[j - 1] + (*pre)[j]) % mod;
} else {
(*cur)[j] = (((*cur)[j - 1] + (*pre)[j]) % mod - (*pre)[j - i] + mod) % mod;
}
}
}

int answer = (*cur)[k];
delete pre;
delete cur;

return answer;
}
};