링크

https://leetcode.com/problems/k-inverse-pairs-array/?envType=daily-question&envId=2024-01-27

문제

문제

문제 설명 및 요점

  • Inverse Pair는 인덱스는 증가하지만 값은 감소하는 형태의 숫자 쌍을 뜻한다.
  • 1~ nn 까지의 숫자가 있을 때, kkInverse Pair 를 갖는 숫자 배치 경우의 수를 찾아야 한다.
  • 정답이 클 수 있으니 109+710^9 + 7 로 나눈 나머지를 반환한다.

풀이

  • 조합론에서의 오일러 수 개념을 확장한 문제. 오일러 수는 연속되는 인덱스에서 감소하는 쌍에 해당하는 순열이고, 문제에서는 인덱스의 연속 조건이 빠져있다.

  • 그래도 오일러 수 보다는 점화식 찾기가 쉬운 편이지만, 그럼에도 너무 어렵다…

  • nn 에 대해, kknC2_nC_2 까지 존재할 수 있다.

    • Inverse Pair가 가장 많이 존재할 수 있는 경우를 생각해보자.
    • 완벽하게 내림차순으로 정렬 되었을 때: [n, n-1, ..., 2, 1]
    • 위 경우에서 Inverse Pairnn 개의 숫자에서 2개의 숫자를 순서 고려 없이 뽑은 경우의 수와 같다.
  • 또한, 각 nn 에 대해 모든 kk 를 합치면 n!n!이 나온다. ( nn개의 숫자를 나열하는 경우의 수)

  • n=2n=2 에서 출발해보자. [1,2] ( k=0k=0 )와 [2,1]( k=1k=1 ) 두 경우가 존재한다.

  • n=3n=3 일 때, 0k30 \le k \le 3 이다. k=0k=0 일 때는 항상 1이므로, 나머지 경우에 대해 생각해보자.

    • k=1k=1 인 경우, [1,2] (n=2,k=0)(n=2,k=0) 인 경우에서 3을 중간에 끼워 넣거나, [2,1] (n=2,k=1)(n=2,k=1) 인 경우에서 3을 맨 뒤에 끼워 넣는 경우 밖에 없다.
    • k=2k=2인 경우, [1,2] (n=2,k=0)(n=2,k=0) 인 경우에서 3을 맨 앞에 끼워 넣거나, [2,1] (n=2,k=1)(n=2,k=1) 인 경우에서 3을 중간에 끼워 넣는 경우 밖에 없다.
    • k=3k=3인 경우, [2,1] (n=2,k=1)(n=2,k=1) 인 경우에서 3을 맨 앞에 끼워 넣는 경우 밖에 없다.
  • n=2n=2 -> n=3n=3 을 만드는 원리가 보이는가? n=2n=2 인 경우의 수에서 3을 어떻게 끼우느냐에 달려있다.

    • 표기를 간소화하기 위해 (n,k)(n,k) 형태로 표기하겠다.
    • (2,0)(2,0)(3,0),(3,1),(3,2)(3, 0), (3, 1), (3, 2)에 관여한다.
    • (2,1)(2,1)(3,1),(3,2),(3,3)(3, 1), (3, 2), (3, 3)에 관여한다.
    • 시각화 하면 이와 같다. image
  • n=3n=3 -> n=4n=4 도 만들어보자. n=3n=3 인 경우의 수에서 4를 어떻게 끼울지 생각해보면 아래와 같은 결과를 얻을 수 있다.

    • 표기를 간소화하기 위해 (n,k)(n,k) 형태로 표기하겠다.
    • (3,0)(3, 0)(4,0),(4,1),(4,2),(4,3)(4, 0), (4, 1), (4, 2), (4, 3)에 관여한다.
    • (3,1)(3, 1)(4,1),(4,2),(4,3),(4,4)(4, 1), (4, 2), (4, 3), (4, 4)에 관여한다.
    • (3,2)(3, 2)(4,2),(4,3),(4,4),(4,5)(4, 2), (4, 3), (4, 4), (4, 5)에 관여한다.
    • (3,3)(3, 3)(4,3),(4,4),(4,5),(4,6)(4, 3), (4, 4), (4, 5), (4, 6)에 관여한다.
    • 시각화 하면 이와 같다. image
  • 수열이 만들어지는 원리는 이게 다인데, 이걸 그대로 구현하면 시간초과가 발생한다. nn 개의 숫자에 대해 kk 개의 숫자를 다음 수열에 n+1n+1 번 누적시켜줘야 하므로, O(N2K)O(N^2K) 라서 해결할 수 없다.

  • 따라서 계산을 최적화해야 한다. 다시 위 시각화를 끌고 와서 생각해보자.

    • 표기를 간소화하기 위해 (n,k)(n,k) 형태로 표기하겠다.
    • (4,2)(4,2) 가 어떻게 만들어지는지 자세히 살펴보자. image
    • (4,2)(4,2) 는 빨간 직선 하나, 초록 직선 하나, 파란 직선 하나로 구성되어 있다.
    • (4,1)(4,1) 은 빨간 직선 하나, 초록 직선 하나로 구성되어 있다.
    • 따라서 (4,2)=(3,0)+(3,1)+(3,2)=(4,1)+(3,2)(4,2) = (3, 0) + (3, 1) + (3, 2) = (4, 1) + (3, 2) 이다.
    • 마치 조합의 성질 처럼, (n,k)=(n,k1)+(n1,k)(n, k) = (n, k -1) + (n - 1, k) 를 통해 계산을 최적화 할 수 있다.
    • 다만, 이는 k<nk < n 인 경우에만 성립한다. 나머지는 대칭성을 이용해 계산해야 한다.
    • (4,3),(4,4)(4,3), (4,4) 에 집중해보자. image
    • (4,3)(4,3) 은 빨강, 초록, 파랑, 노랑으로 구성되어 있다.
    • (4,4)(4,4) 는 초록, 파랑, 노랑으로 구성되어 있다.
    • 따라서 (4,4)=(4,3)+(3,4)(3,0)(4, 4) = (4,3) + (3, 4) - (3, 0) 이다.
    • 나머지 경우도 확장해보면, knk \ge n 인 경우에 대해 (n,k)=(n,k1)+(n1,k)(n1,kn)(n, k) = (n , k - 1) + (n - 1, k) - (n - 1, k-n) 이 성립함을 알 수 있다.
  • 정리하면, 최종 점화식은 다음과 같다.

    • 표기를 간소화하기 위해 (n,k)(n,k) 형태로 표기하겠다. n2n \ge 2 에서
    • k<nk < n 인 경우에 대해, (n,k)=(n,k1)+(n1,k)(n, k) = (n, k -1) + (n - 1, k)
    • knk \ge n 인 경우에 대해, (n,k)=(n,k1)+(n1,k)(n1,kn)(n, k) = (n , k - 1) + (n - 1, k) - (n - 1, k-n)
    • 점화식을 그대로 재귀로 구현해도 정답을 받을 수 있다.
    • 생 재귀로는 당연히 불가능하고, 이전 값을 저장해두어야 한다. 파이썬의 경우 @cache 데코레이터를 사용하면 쉽게 구현할 수 있다.
  • 좀 더 최적화를 시켜보자.

    • n=1n=1 일 때 부터 시작한다. (1,0)(1, 0) 은 1이다.
    • k+1k + 1 칸 짜리 배열을 하나 준비하고, 빈 배열과 이전 값을 누적할 변수를 하나 둔다.
    • 누적값을 빈 배열에 계속 추가해 나가다가, kn+1k \ge n + 1 이 되는 시점부터 누적값에서 dp[k - n - 1] 을 뺀다.
    • nC2_nC_2 를 전부 계산할 필요 없이, kk까지만 계산한 후 dp 배열과 교체해준다.
    • 코드로 옮기면 아래와 같다. 109+710^9 + 7 과의 나머지 연산을 잊지 말자.

코드

class Solution:
    def kInversePairs(self, n: int, k: int) -> int:
        MOD = 10 ** 9 + 7
        dp = [0] * (k + 1)
        dp[0] = 1
        
        for i in range(n):
            temp, number = [], 0
            for j in range(k + 1):
                number += dp[j]
                if j-i >= 1: number -= dp[j-i-1]
                number %= MOD
                temp.append(number)
            dp = temp
        return dp[k]