web-dev-qa-db-ja.com

正確にk回の反転を伴うn要素順列の数

私は効率的に解決しようとしています SPOJ問題64:順列

A = [a1、a2、...、an]を整数1,2、...、nの順列とします。インデックスのペア(i、j)、1 <= i <= j <= nは、ai> ajの場合、順列Aの反転です。整数n> 0およびk> = 0が与えられます。正確にk個の反転を含むn要素の順列の数はいくつですか?

たとえば、正確に1つの反転を持つ4要素の順列の数は3に等しくなります。

与えられた例を見やすくするために、正確に1つの反転を持つ3つの4要素順列を次に示します。

(1, 2, 4, 3)
(1, 3, 2, 4)
(2, 1, 3, 4)

最初の順列では、4> 3であり、4のインデックスは3のインデックスよりも小さくなります。これは単一の反転です。順列には正確に1つの反転があるため、これは私たちが数えようとしている順列の1つです。

N個の要素の任意のシーケンスに対して、順列の数は階乗(n)です。したがって、ブルートフォースnを使用すると2 各順列の反転数をカウントし、それらがkに等しいかどうかを確認する方法では、この問題の解決策は時間計算量O(n!* n2)。


これまでの研究

この問題のサブ問題は、StackOverflowで以前に尋ねられました ここsingle順列の反転数をカウントするマージソートを使用したO(n log n)ソリューションが提供されました。ただし、そのソリューションを使用して各順列の反転数をカウントすると、O(n!* n log n)の時間計算量が得られますが、これは私の意見では依然として非常に高いです。

これ 正確な質問は以前にStack Overflowでも尋ねられましたが、回答はありませんでした。


これを解決するための数式がない場合(私は少し疑わしいですが)、効率的な動的計画法の解決策が可能であるというヒントを与える人々も見ました。 DPまたは別のアプローチを使用して、O(n!* n log n)よりも効率的なソリューションを作成したいのですが、どこから始めればよいのかわかりません。

ヒント、コメント、提案は大歓迎です。

編集:私は計算へのDPアプローチで以下の問題に答えました マホニアン数

16
Shashank

解決策にはいくつかの説明が必要です。 I(n、k)によって正確にk個の反転を持つn個のアイテムの順列の数を示しましょう。

ここで、I(n、0)は常に1です。任意のnに対して、反転が0の順列が1つだけ存在します。つまり、シーケンスがますますソートされる場合です。

シーケンス自体がないため、I(0、k)は常に0になります。

ここで、I(n、k)を見つけるために、4つの要素{1,2,3,4}を含むシーケンスの例を見てみましょう。

以下のn = 4の場合、転倒の数によって列挙およびグループ化された順列です。

_|___k=0___|___k=1___|___k=2___|___k=3___|___k=4___|___k=5___|___k=6___|
| 1234    | 1243    | 1342    | 1432    | 2431    | 3421    | 4321    |
|         | 1324    | 1423    | 2341    | 3241    | 4231    |         |
|         | 2134    | 2143    | 2413    | 3412    | 4312    |         |
|         |         | 2314    | 3142    | 4132    |         |         |
|         |         | 3124    | 3214    | 4213    |         |         |
|         |         |         | 4123    |         |         |         |
|         |         |         |         |         |         |         |
|I(4,0)=1 |I(4,1)=3 |I(4,2)=5 |I(4,3)=6 |I(4,4)=5 |I(4,5)=3 |I(4,6)=1 |
|         |         |         |         |         |         |         |
_

ここで、n = 5の順列の数を見つけ、可能なすべてのkについて、n番目(最大)の要素(5)を各順列のどこかに挿入することにより、I(4、k)から繰り返しI(5、k)を導出できます。前の順列。結果として生じる反転の数はkです。

たとえば、I(5,4)は、それぞれ正確に4つの反転を持つシーケンス{1,2,3,4,5}の順列の数に他なりません。列k = 4まで反転数が<= 4になるまで、上記のI(4、k)を観察してみましょう。次に、以下に示すように要素5を配置します。

_|___k=0___|___k=1___|___k=2___|___k=3___|___k=4___|___k=5___|___k=6___|
| |5|1234 | 1|5|243 | 13|5|42 | 143|5|2 | 2431|5| | 3421    | 4321    |
|         | 1|5|324 | 14|5|23 | 234|5|1 | 3241|5| | 4231    |         |
|         | 2|5|134 | 21|5|43 | 241|5|3 | 3412|5| | 4312    |         |
|         |         | 23|5|14 | 314|5|4 | 4132|5| |         |         |
|         |         | 31|5|24 | 321|5|4 | 4213|5| |         |         |
|         |         |         | 412|5|3 |         |         |         |
|         |         |         |         |         |         |         |
|    1    |    3    |    5    |    6    |    5    |         |         |
|         |         |         |         |         |         |         |
_

5を含む上記の順列のそれぞれには、正確に4つの反転があります。したがって、4つの反転を伴う全順列I(5,4)= I(4,4)+ I(4,3)+ I(4,2)+ I(4,1)+ I(4,0)= 1 + 3 + 5 + 6 + 5 = 20

同様に、I(4、k)からのI(5,5)の場合

_|___k=0___|___k=1___|___k=2___|___k=3___|___k=4___|___k=5___|___k=6___|
|   1234  | |5|1243 | 1|5|342 | 14|5|32 | 243|5|1 | 3421|5| | 4321    |
|         | |5|1324 | 1|5|423 | 23|5|41 | 324|5|1 | 4231|5| |         |
|         | |5|2134 | 2|5|143 | 24|5|13 | 341|5|2 | 4312|5| |         |
|         |         | 2|5|314 | 31|5|44 | 413|5|2 |         |         |
|         |         | 3|5|124 | 32|5|14 | 421|5|3 |         |         |
|         |         |         | 41|5|23 |         |         |         |
|         |         |         |         |         |         |         |
|         |    3    |    5    |    6    |    5    |    3    |         |
|         |         |         |         |         |         |         |
_

したがって、5つの反転を伴う全順列I(5,5)= I(4,5)+ I(4,4)+ I(4,3)+ I(4,2)+ I(4,1)= 3 + 5 + 6 + 5 + 3 = 22

したがって、I(n, k) = sum of I(n-1, k-i) such that i < n && k-i >= 0

また、kはn *(n-1)/ 2まで上がる可能性があります。これは、シーケンスが降順で並べ替えられたときに発生します https://secweb.cs.odu.edu/~zeil/cs361/web/website /Lectures/insertion/pages/ar01s04s01.htmlhttp://www.algorithmist.com/index.php/SPOJ_PERMUT1

_#include <stdio.h>

int dp[100][100];

int inversions(int n, int k)
{
    if (dp[n][k] != -1) return dp[n][k];
    if (k == 0) return dp[n][k] = 1;
    if (n == 0) return dp[n][k] = 0;
    int j = 0, val = 0;
    for (j = 0; j < n && k-j >= 0; j++)
        val += inversions(n-1, k-j);
    return dp[n][k] = val;
}

int main()
{
    int t;
    scanf("%d", &t);
    while (t--) {
        int n, k, i, j;
        scanf("%d%d", &n, &k);
        for (i = 1; i <= n; i++)
            for (j = 0; j <= k; j++)
                dp[i][j] = -1;
        printf("%d\n", inversions(n, k));
    }
    return 0;
}
_
12
Vineel Kovvuri

それは1日後、動的計画法を使用して問題を解決することができました。私はそれを提出し、私のコードはSPOJに受け入れられたので、将来に興味のある人のためにここで私の知識を共有すると思います。

離散数学の反転について説明しているウィキペディアのページ を調べたところ、ページの下部に興味深い推奨事項が見つかりました。

K回の反転を伴うn個の要素の順列の数。マホニアン番号: A008302

OEISへのリンク をクリックすると、マホニアン数の三角形と呼ばれる整数の無限のシーケンスが表示されました。

1、1、1、1、2、2、1、1、3、5、6、5、3、1、1、4、9、15、20、22、20、15、9、4、1 1、5、14、29、49、71、90、101、101、90、71、49、29、14、5、1、1、6、20、49、98、169、259、359、455、 531、573、573、531、455、359、259、169、98、49、20、6、1。 。 。

これらの数字は私にはなじみがあるように見えたので、私はこれらの数字が何であるかについて興味がありました。それから私は以前にサブシーケンス1、3、5、6、5、3、1を見たことがあることに気づきました。実際、これは(n、k)のいくつかのペア、つまり(4、0)、(4、1)、(4、2)、(4、3)、(4、4)の問題に対する答えでした。 、(4、5)、(4、6)。このサブシーケンスの両側にあるものを調べたところ、n <4およびn> 4に対してすべて有効な(つまり、0より大きい順列)回答であることがわかりました。

シーケンスの式は次のように与えられました。

product_ {i = 0..n-1}(1 + x + ... + x ^ i)の展開における係数

これは私が理解して検証するのに十分簡単でした。基本的に任意のnを取り、式にプラグインできます。次に、xの係数k 項は(n、k)の答えになります。

N = 3の例を示します。

(x0)(x0 + 1)(x0 + x1 + x2) = (1)(1 + x)(1 + x + x2) = (1 + x)(1 + x + x2) = 1 + x + x + x2 + x2 + x3 = 1 + 2x + 2x2 + x3

最終的な拡張は1 + 2x + 2x2 + x3とxの係数k 項は、k = 0、1、2、3に対してそれぞれ1、2、2、および1でした。これはたまたま、3要素の順列のすべての有効な反転数です。

1、2、2、1は、次のように表に配置された場合のマホニアン番号の3行目です。

1
1 1
1 2 2 1
1 3 5 6 5 3 1
etc.

したがって、基本的に私の答えを計算することは、単にn番目のマホニアン行を計算し、kが0から始まり、インデックスが範囲外の場合は0を出力するk番目の要素を取得することでした。これは、ボトムアップ動的計画法の単純なケースでした。これは、各i番目の行を使用してi +1番目の行を簡単に計算できるためです。

以下に示すのは、私が使用したPythonソリューションで、わずか0.02秒で実行されました。この問題の最大制限時間は特定のテストケースで3秒で、以前はタイムアウトエラーが発生していたので、この最適化はかなり良いです。

def mahonian_row(n):
    '''Generates coefficients in expansion of 
    Product_{i=0..n-1} (1+x+...+x^i)
    **Requires that n is a positive integer'''
    # Allocate space for resulting list of coefficients?
    # Initialize them all to zero?
    #max_zero_holder = [0] * int(1 + (n * 0.5) * (n - 1))

    # Current max power of x i.e. x^0, x^0 + x^1, x^0 + x^1 + x^2, etc.
    # i + 1 is current row number we are computing
    i = 1
    # Preallocate result
    # Initialize to answer for n = 1
    result = [1]
    while i < n:
        # Copy previous row of n into prev
        prev = result[:]
        # Get space to hold (i+1)st row
        result = [0] * int(1 + ((i + 1) * 0.5) * (i))
        # Initialize multiplier for this row
        m = [1] * (i + 1)
        # Multiply
        for j in range(len(m)):
            for k in range(len(prev)):
                result[k+j] += m[j] * prev[k]
        # Result now equals mahonian_row(i+1)
        # Possibly should be memoized?
        i = i + 1
    return result


def main():
    t = int(raw_input())
    for _ in xrange(t):
        n, k = (int(s) for s in raw_input().split())
        row = mahonian_row(n)
        if k < 0 or k > len(row) - 1:
            print 0
        else:
            print row[k]


if __name__ == '__main__':
    main()

時間の複雑さはわかりませんが、このコードは メモ化 によって改善できると確信しています。10個のテストケースがあり、以前のテストケースの計算を使用して、将来的に「チート」できるからです。テストケース。私は将来その最適化を行いますが、現在の状態でのこの回答が、すべての順列を生成して反復するという素朴な階乗の複雑さのアプローチを回避するため、将来この問題を試みる人に役立つことを願っています。

8
Shashank

動的計画法の解決策がある場合は、長さnの順列の結果を使用して、長さn + 1の順列の結果を支援することにより、段階的に実行する方法がおそらくあります。

長さn-値1-nの順列が与えられた場合、n + 1の可能な位置に値(n + 1)を追加することにより、長さn +1の順列を取得できます。 (n + 1)は1-nのいずれよりも大きいため、これを行うときに作成する反転の数は、追加する場所によって異なります-最後の位置に追加し、反転を作成せず、最後に1つだけ追加します位置を設定し、1つの反転を作成します。以下同様です。1つの反転でn = 4のケースを振り返って、これを確認します。

したがって、追加できるn + 1の場所の1つを検討する場合(n + 1)を右から数えてjの場所に追加すると、最後の位置が位置0になるため、これによって作成されるK反転の順列の数は次のようになります。 nか所でのKj反転を伴う順列。

したがって、各ステップで、考えられるすべてのKについてK反転の順列の数を数える場合、長さnのK反転の順列の数を使用して、長さn +1のK反転の順列の数を更新できます。

6
mcdowella

これらの係数を計算する際の主な問題は、結果の積の次数のサイズです。多項式積i = 1,2、..、n {(1 + x)。(1 + x + x ^ 2)....(1 + x + x ^ 2 + .. + x ^ i)+ ...(1 + x + x ^ 2 + ... + x ^ n)は、n *(n + 1)と同等の次数になります。その結果、これはプロセスに制限的な計算制限を課します。 n-1の製品の以前の結果がnの製品の計算プロセスで使用されるプロセスを使用する場合、(n-1)* n整数のストレージを調べています。再帰的プロセスを使用することは可能ですが、これははるかに遅くなります。また、整数の一般的なサイズの平方根よりも小さい整数に制限されます。以下は、この問題のラフアンドレディー再帰コードです。関数mahonian(r、c)は、r番目の積のc番目の係数を返します。しかし、100を超える大きな製品の場合も非常に遅くなります。これを実行すると、再帰が明らかに答えではないことがわかります。

unsigned int numbertheory::mahonian(unsigned int r, unsigned int c)
  {
      unsigned int result=0;
      unsigned int k;

     if(r==0 && c==0)
       return 1;
     if( r==0 && c!=0)
      return 0;

   for(k=0; k <= r; k++)
       if(r > 0 && c >=k)
           result = result + mahonian(r-1,c-k);

   return result;

}

興味深いことに、以下を含めました。これは、再帰の例よりもはるかに高速なSashankのc ++バージョンです。 armadilloライブラリを使用していることに注意してください。

uvec numbertheory::mahonian_row(uword n){
 uword i = 2;
 uvec current;
 current.ones(i);
 uword current_size;
 uvec prev;
 uword prev_size;

 if(n==0){
   current.ones(1);
   return current;
 }

 while (i <= n){                  // increment through the rows
   prev_size=current.size();     // reset prev size to current size
   prev.set_size(prev_size);     // set size of prev vector
   prev= current;                //copy contents of current to prev vector
   current_size =1+ (i*(i+1)/2);      // reset current_size
   current.zeros(current_size);    // reset current vector with zeros

   for(uword j=0;j<i+1; j++)       //increment through current vector
      for(uword k=0; k < prev_size;k++)
         current(k+j) += prev(k);
   i++;                                        //increment to next row
}
return current;                                //return current vector
 }

 uword numbertheory::mahonian_fast(uword n, uword c) {
**This function returns the coefficient of c order of row n of
**the Mahonian numbers
    // check for input errors
    if(c >= 1+ (n*(n+1)/2)) {
        cout << "Error. Invalid input parameters" << endl;
   }
   uvec mahonian;
   mahonian.zeros(1+ (n*(n+1)/2));
   mahonian = mahonian_row(n);
   return mahonian(c);
 }
0
Paul Mackenzie

この問題を解決するために動的計画法を利用することができます。 1からnまでの数字を入力する場所がn個あり、_ _ _ _ _ _ _ n = 7を取ると、最初の場所で最大n-1の反転、少なくとも0を達成できます。2番目の場所でも同様です。最大でn-2の反転を達成し、少なくとも0を達成します。一般に、前に配置した数の選択に関係なく、i番目のインデックスで最大でniの反転を達成できます。再帰式は次のようになります。

f(n、k)= f(n-1、k)+ f(n-1、k-1)+ f(n-1、k-2)...........。 f(n-1、max(0、k-(n-1))反転なし1反転2反転n-1反転セット(1、n)の残りの数の最小値を配置することで0反転を実現できます1反転2番目に小さいなどを配置することにより、

再帰式の基本条件は次のようになります。

if(i == 0 && k == 0)return 1(有効な順列)

if(i == 0 && k!= 0)は0(無効な順列)を返します。

再帰ツリーを描画すると、サブ問題が複数回繰り返されることがわかります。したがって、メモ化を使用して、複雑さをO(n * k)に減らします。

0