K Inverse Pairs Array
HardPrompt
For an integer array nums, an inverse pair is a pair of integers [i, j] where 0 <= i < j < nums.length and nums[i] > nums[j].
Given two integers n and k, return the number of different arrays consisting of numbers from 1 to n such that there are exactly k inverse pairs. Since the answer can be huge, return it modulo 109 + 7.
Example 1:
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:
Input: n = 3, k = 1
Output: 2
Explanation: The array [1,3,2] and [2,1,3] have exactly 1 inverse pair.
Constraints:
1 <= n <= 10000 <= k <= 1000
Approaches
4 approaches with complexity analysis and trade-offs.
This approach involves generating every possible permutation of numbers from 1 to n. For each generated permutation, we count the number of inverse pairs. If the count matches k, we increment our result. This method is conceptually simple but computationally very expensive.
Algorithm
- Define a recursive function, say
generatePermutations(current_permutation, used_numbers). - The base case for the recursion is when the
current_permutationhasnelements. - In the base case, count the inverse pairs in the
current_permutation. An inverse pair(i, j)exists ifi < jandpermutation[i] > permutation[j]. - If the number of inverse pairs is exactly
k, increment a global counter. - In the recursive step, iterate through numbers from 1 to
n. If a number has not been used, add it to thecurrent_permutationand make a recursive call. - Backtrack by removing the number and marking it as unused to explore other possibilities.
Walkthrough
The brute-force method explores all possible arrangements of the numbers from 1 to n. It uses a backtracking algorithm to generate each permutation. Once a full permutation of n numbers is formed, it iterates through the permutation to count the number of inverse pairs. If this count equals the target k, a solution is found.
Algorithm:
- Define a recursive function, say
generate(current_permutation, used_numbers). - The base case for the recursion is when the
current_permutationhasnelements. - In the base case, count the inverse pairs in the
current_permutation. An inverse pair(i, j)exists ifi < jandpermutation[i] > permutation[j]. - If the number of inverse pairs is exactly
k, increment a counter. - In the recursive step, iterate through numbers from 1 to
n. If a number has not been used, add it to thecurrent_permutationand make a recursive call. - Backtrack by removing the number and marking it as unused to explore other possibilities.
// This is a conceptual implementation and is too slow to pass.class Solution { int count = 0; int N, K; public int kInversePairs(int n, int k) { this.N = n; this.K = k; // The maximum number of inversions for n elements is n*(n-1)/2. if (k > n * (n - 1) / 2) { return 0; } List<Integer> permutation = new ArrayList<>(); boolean[] used = new boolean[n + 1]; generate(permutation, used); return count; } private void generate(List<Integer> permutation, boolean[] used) { if (permutation.size() == N) { if (countInversions(permutation) == K) { count++; } return; } for (int i = 1; i <= N; i++) { if (!used[i]) { used[i] = true; permutation.add(i); generate(permutation, used); permutation.remove(permutation.size() - 1); used[i] = false; } } } private int countInversions(List<Integer> arr) { int inversions = 0; for (int i = 0; i < arr.size(); i++) { for (int j = i + 1; j < arr.size(); j++) { if (arr.get(i) > arr.get(j)) { inversions++; } } } return inversions; }}Complexity
Time
O(n! * n^2) - There are `n!` permutations. For each permutation, we spend `O(n^2)` to count the inversions.
Space
O(n) - For the recursion stack depth and to store the current permutation.
Trade-offs
Pros
Simple to understand and implement.
Cons
Extremely inefficient and will time out for all but the smallest values of
n(e.g., n > 10).
This approach uses dynamic programming. We define dp[i][j] as the number of permutations of numbers from 1 to i that have exactly j inverse pairs. We build the solution for n and k based on solutions for smaller subproblems by considering the placement of the i-th number.
Algorithm
- Create a 2D DP table
dp[n+1][k+1]. - Initialize the base case:
dp[0][0] = 1(an empty permutation has 0 inversions). - Iterate
ifrom 1 ton(for permutations of sizei). - Iterate
jfrom 0 tok(for the number of inversions). - Inside, iterate
pfrom 0 tomin(j, i-1)to calculate the sumdp[i][j] = (dp[i][j] + dp[i-1][j-p]) % MOD. - The final answer is
dp[n][k].
Walkthrough
The core idea is to construct a permutation of i elements by inserting the number i into a permutation of i-1 elements. When we insert the number i into a permutation of [1, ..., i-1], it is larger than all existing elements. If we place i at the p-th position from the right (0-indexed), it creates p new inverse pairs. This leads to the recurrence relation: dp[i][j] = sum_{p=0}^{i-1} dp[i-1][j-p]. This means the number of ways to get j inversions with i elements is the sum of ways to get j-p inversions with i-1 elements, for all possible new inversions p we can create (from 0 to i-1).
Algorithm:
- Create a 2D DP table
dp[n+1][k+1]. - Initialize the base case:
dp[0][0] = 1. - Iterate
ifrom 1 ton. - Iterate
jfrom 0 tok. - Inside, iterate
pfrom 0 tomin(j, i-1)to calculate the sumdp[i][j] = (dp[i][j] + dp[i-1][j-p]) % MOD. - The final answer is
dp[n][k].
class Solution { public int kInversePairs(int n, int k) { int MOD = 1_000_000_007; 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++) { for (int p = 0; p < i && j - p >= 0; p++) { dp[i][j] = (dp[i][j] + dp[i - 1][j - p]) % MOD; } } } return dp[n][k]; }}Complexity
Time
O(n * k * n) - Three nested loops for `i`, `j`, and `p`.
Space
O(n * k) - For the 2D DP table.
Trade-offs
Pros
Correctly models the problem using a DP state.
Significantly more efficient than the brute-force approach.
Cons
The time complexity is too high for the given constraints due to the third nested loop.
We can optimize the naive DP approach. The recurrence dp[i][j] = sum_{p=0}^{i-1} dp[i-1][j-p] involves a summation that can be calculated more efficiently. By observing the relationship between dp[i][j] and dp[i][j-1], or by using a sliding window sum (prefix sums), we can derive a new recurrence that avoids the inner loop.
Algorithm
- Create a 2D DP table
dp[n+1][k+1]. - Initialize
dp[0][0] = 1. - Iterate
ifrom 1 ton. - Maintain a
prefixSumfor the previous rowdp[i-1]. - Iterate
jfrom 0 tok. - Update the
prefixSumby addingdp[i-1][j]and subtractingdp[i-1][j-i]ifj >= i(sliding window sum). - Set
dp[i][j]to the currentprefixSum. - The final answer is
dp[n][k].
Walkthrough
The original recurrence is: dp[i][j] = dp[i-1][j] + dp[i-1][j-1] + ... + dp[i-1][j-i+1]. This is a sum over a window of size i on the previous row dp[i-1]. We can compute this sum efficiently in O(1) time for each j by maintaining a running sum.
The optimized recurrence relation is dp[i][j] = dp[i][j-1] + dp[i-1][j] - dp[i-1][j-i]. This removes the innermost loop, reducing the time complexity.
Algorithm:
- Create a 2D DP table
dp[n+1][k+1]. - Initialize
dp[0][0] = 1. - Iterate
ifrom 1 ton. - Maintain a
prefixSumfor the previous rowdp[i-1]. - Iterate
jfrom 0 tok. - Update the
prefixSumby addingdp[i-1][j]and subtractingdp[i-1][j-i]ifj >= i(this is a sliding window sum). - Set
dp[i][j]to the currentprefixSum(modulo10^9 + 7). - The final answer is
dp[n][k].
class Solution { public int kInversePairs(int n, int k) { int MOD = 1_000_000_007; int[][] dp = new int[n + 1][k + 1]; dp[0][0] = 1; for (int i = 1; i <= n; i++) { long prefixSum = 0; for (int j = 0; j <= k; j++) { prefixSum += dp[i - 1][j]; if (j >= i) { prefixSum -= dp[i - 1][j - i]; } // Handle negative results from modulo dp[i][j] = (int)((prefixSum % MOD + MOD) % MOD); } } return dp[n][k]; }}Complexity
Time
O(n * k) - We have two nested loops, and each transition takes constant time.
Space
O(n * k) - For the 2D DP table.
Trade-offs
Pros
Efficient enough to pass the given constraints.
Logically follows from the naive DP approach.
Cons
Uses a significant amount of memory for large
nandk.
This is the most efficient approach. It builds upon the optimized DP solution. By observing the recurrence, we can see that to compute the values for the current row i, we only need the values from the previous row i-1. This allows us to reduce the space complexity from O(n*k) to O(k) by using only a 1D array.
Algorithm
- Create a 1D array
dpof sizek+1. Initializedp[0] = 1. - Iterate
ifrom 1 ton. - Inside this loop, create a temporary array
tempof sizek+1to store the new values for rowi. - Use a variable
valto maintain the sliding window sum. Iteratejfrom 0 tok. - Update
valby addingdp[j](from the previous rowi-1) and subtractingdp[j-i]ifj >= i. - Set
temp[j]to the newval(modulo10^9 + 7). - After the inner loop, update
dp = tempfor the next iteration ofi. - The final answer is
dp[k].
Walkthrough
Instead of a 2D dp table, we use a 1D array to store the results for the previous row (i-1) and compute the results for the current row (i) into a temporary array. After each outer loop iteration, the temporary array becomes the main DP array for the next iteration. This reduces the space requirement significantly while keeping the time complexity the same.
Algorithm:
- Create a 1D array
dpof sizek+1. Initializedp[0] = 1. This represents the base case. - Iterate
ifrom 1 ton. - Inside this loop, create a temporary array
tempof sizek+1. - Use a variable
valto maintain the sliding window sum. Iteratejfrom 0 tok. - Update
valby addingdp[j](from the previous rowi-1) and subtractingdp[j-i]ifj >= i. - Set
temp[j]to the newval(modulo10^9 + 7). - After the inner loop, update
dp = tempfor the next iteration ofi. - The final answer is
dp[k].
class Solution { public int kInversePairs(int n, int k) { int MOD = 1_000_000_007; int[] dp = new int[k + 1]; dp[0] = 1; for (int i = 1; i <= n; i++) { int[] temp = new int[k + 1]; int val = 0; for (int j = 0; j <= k; j++) { val = (val + dp[j]) % MOD; if (j >= i) { val = (val - dp[j - i] + MOD) % MOD; } temp[j] = val; } dp = temp; } return dp[k]; }}Complexity
Time
O(n * k) - The time complexity remains the same as the `O(n*k)` space approach.
Space
O(k) - We use a 1D array of size `k+1` (and a temporary one of the same size) to store the DP states for one row.
Trade-offs
Pros
Most efficient solution in terms of both time and space.
Passes all constraints with optimal memory usage.
Cons
The logic can be slightly harder to grasp compared to the 2D DP table version.
Solutions
Solution
class Solution {public int kInversePairs(int n, int k) { final int mod = (int)1 e9 + 7; int[] f = new int[k + 1]; int[] s = new int[k + 2]; f[0] = 1; Arrays.fill(s, 1); s[0] = 0; for (int i = 1; i <= n; ++i) { for (int j = 1; j <= k; ++j) { f[j] = (s[j + 1] - s[Math.max(0, j - (i - 1))] + mod) % mod; } for (int j = 1; j <= k + 1; ++j) { s[j] = (s[j - 1] + f[j - 1]) % mod; } } return f[k]; }}class Solution {public: int kInversePairs(int n, int k) { int f[k + 1]; int s[k + 2]; memset(f, 0, sizeof(f)); f[0] = 1; fill(s, s + k + 2, 1); s[0] = 0; const int mod = 1e9 + 7; for (int i = 1; i <= n; ++i) { for (int j = 1; j <= k; ++j) { f[j] = (s[j + 1] - s[max(0, j - (i - 1))] + mod) % mod; } for (int j = 1; j <= k + 1; ++j) { s[j] = (s[j - 1] + f[j - 1]) % mod; } } return f[k]; }};class Solution: def kInversePairs(self, n: int, k: int) -> int: mod = 10 ** 9 + 7 f = [1] + [0] * k s = [0] * (k + 2) for i in range(1, n + 1): for j in range(1, k + 1): f[j] = (s[j + 1] - s[max(0, j - (i - 1))]) % mod for j in range(1, k + 2): s[j] = (s[j - 1] + f[j - 1]) % mod return f[k]Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.