Calculate Nth multiset combination (with repetition) based only on index

后端 未结 2 1275
無奈伤痛
無奈伤痛 2020-12-18 14:38

How can i calculate the Nth combo based only on it\'s index. There should be (n+k-1)!/(k!(n-1)!) combinations with repetitions.

with n=2, k=5 you get:

0|{0         


        
2条回答
  •  有刺的猬
    2020-12-18 14:44

    I have done some preliminary analysis on the problem. Before I talk about the inefficient solution I found, let me give you a link to a paper I wrote on how to translate the k-indexes (or combination) to the rank or lexigraphic index to the combinations associated with the binomial coefficient:

    http://tablizingthebinomialcoeff.wordpress.com/

    I started out the same way in trying to solve this problem. I came up with the following code that uses one loop for each value of k in the formula (n+k-1)!/k!(n-1)! when k = 5. As written, this code will generate all combinations for the case of n choose 5:

    private static void GetCombos(int nElements)
    {
       // This code shows how to generate all the k-indexes or combinations for any number of elements when k = 5.
       int k1, k2, k3, k4, k5;
       int n = nElements;
       int i = 0;
       for (k5 = 0; k5 < n; k5++)
       {
          for (k4 = k5; k4 < n; k4++)
          {
             for (k3 = k4; k3 < n; k3++)
             {
                for (k2 = k3; k2 < n; k2++)
                {
                   for (k1 = k2; k1 < n; k1++)
                   {
                      Console.WriteLine("i = " + i.ToString() + ", " + k5.ToString() + " " + k4.ToString() + 
                         " " + k3.ToString() + " " + k2.ToString() + " " + k1.ToString() + " ");
                      i++;
                   }
                }
             }
          }
       }
    }
    

    The output from this method is:

    i = 0, 0 0 0 0 0
    i = 1, 0 0 0 0 1
    i = 2, 0 0 0 0 2
    i = 3, 0 0 0 1 1
    i = 4, 0 0 0 1 2
    i = 5, 0 0 0 2 2
    i = 6, 0 0 1 1 1
    i = 7, 0 0 1 1 2
    i = 8, 0 0 1 2 2
    i = 9, 0 0 2 2 2
    i = 10, 0 1 1 1 1
    i = 11, 0 1 1 1 2
    i = 12, 0 1 1 2 2
    i = 13, 0 1 2 2 2
    i = 14, 0 2 2 2 2
    i = 15, 1 1 1 1 1
    i = 16, 1 1 1 1 2
    i = 17, 1 1 1 2 2
    i = 18, 1 1 2 2 2
    i = 19, 1 2 2 2 2
    i = 20, 2 2 2 2 2
    

    This is the same values as you gave in your edited answer. I also have tried it with 4 choose 5 as well, and it looks like it generates the correct combinations as well.

    I wrote this in C#, but you should be able to use it with other languages like C/C++, Java, or Python without too many edits.

    One idea for a somewhat inefficient solution is to modify GetCombos to accept k as an input as well. Since k is limited to 6, it would then be possible to put in a test for k. So the code to generate all possible combinations for an n choose k case would then look like this:

    private static void GetCombos(int k, int nElements)
    {
       // This code shows how to generate all the k-indexes or combinations for any n choose k, where k <= 6.
       //
       int k1, k2, k3, k4, k5, k6;
       int n = nElements;
       int i = 0;
       if (k == 6)
       {
          for (k6 = 0; k6 < n; k6++)
          {
             for (k5 = 0; k5 < n; k5++)
             {
                for (k4 = k5; k4 < n; k4++)
                {
                   for (k3 = k4; k3 < n; k3++)
                   {
                      for (k2 = k3; k2 < n; k2++)
                      {
                         for (k1 = k2; k1 < n; k1++)
                         {
                            Console.WriteLine("i = " + i.ToString() + ", " + k5.ToString() + " " + k4.ToString() +
                               " " + k3.ToString() + " " + k2.ToString() + " " + k1.ToString() + " ");
                            i++;
                         }
                      }
                   }
                }
             }
          }
       }
       else if (k == 5)
       {
          for (k5 = 0; k5 < n; k5++)
          {
             for (k4 = k5; k4 < n; k4++)
             {
                for (k3 = k4; k3 < n; k3++)
                {
                   for (k2 = k3; k2 < n; k2++)
                   {
                      for (k1 = k2; k1 < n; k1++)
                      {
                         Console.WriteLine("i = " + i.ToString() + ", " + k5.ToString() + " " + k4.ToString() +
                            " " + k3.ToString() + " " + k2.ToString() + " " + k1.ToString() + " ");
                         i++;
                      }
                   }
                }
             }
          }
       }
       else if (k == 4)
       {
          // One less loop than k = 5.
       }
       else if (k == 3)
       {
          // One less loop than k = 4.
       }
       else if (k == 2)
       {
          // One less loop than k = 3.
       }
       else
       {
          // k = 1 - error?
       }
    }
    

    So, we now have a method that will generate all the combinations of interest. But, the problem is to obtain a specific combination from the lexigraphic order or rank of where that combination lies within the set. So, this can accomplished by a simple count and then returning the proper combination when it hits the specified value. So, to accommodate this an extra parameter that represents the rank needs to be added to the method. So, a new function to do this looks like this:

    private static int[] GetComboOfRank(int k, int nElements, int Rank)
    {
       // Gets the combination for the rank using the formula (n+k-1)!/k!(n-1)! where k <= 6.
       int k1, k2, k3, k4, k5, k6;
       int n = nElements;
       int i = 0;
       int[] ReturnArray = new int[k];
       if (k == 6)
       {
          for (k6 = 0; k6 < n; k6++)
          {
             for (k5 = 0; k5 < n; k5++)
             {
                for (k4 = k5; k4 < n; k4++)
                {
                   for (k3 = k4; k3 < n; k3++)
                   {
                      for (k2 = k3; k2 < n; k2++)
                      {
                         for (k1 = k2; k1 < n; k1++)
                         {
                            if (i == Rank)
                            {
                               ReturnArray[0] = k1;
                               ReturnArray[1] = k2;
                               ReturnArray[2] = k3;
                               ReturnArray[3] = k4;
                               ReturnArray[4] = k5;
                               ReturnArray[5] = k6;
                               return ReturnArray;
                            }
                            i++;
                         }
                      }
                   }
                }
             }
          }
       }
       else if (k == 5)
       {
          for (k5 = 0; k5 < n; k5++)
          {
             for (k4 = k5; k4 < n; k4++)
             {
                for (k3 = k4; k3 < n; k3++)
                {
                   for (k2 = k3; k2 < n; k2++)
                   {
                      for (k1 = k2; k1 < n; k1++)
                      {
                         if (i == Rank)
                         {
                            ReturnArray[0] = k1;
                            ReturnArray[1] = k2;
                            ReturnArray[2] = k3;
                            ReturnArray[3] = k4;
                            ReturnArray[4] = k5;
                            return ReturnArray;
                         }
                         i++;
                      }
                   }
                }
             }
          }
       }
       else if (k == 4)
       {
          // Same code as in the other cases, but with one less loop than k = 5.
       }
       else if (k == 3)
       {
          // Same code as in the other cases, but with one less loop than k = 4.
       }
       else if (k == 2)
       {
          // Same code as in the other cases, but with one less loop than k = 3.
       }
       else
       {
          // k = 1 - error?
       }
       // Should not ever get here.  If we do - it is some sort of error.
       throw ("GetComboOfRank - did not find rank");
    }
    

    ReturnArray returns the combination associated with the rank. So, this code should work for you. However, it will be much slower than what could be achieved if a table lookup was done. The problem with 300 choose 6 is that:

    300 choose 6 = 305! / (6!(299!) = 305*304*303*302*301*300 / 6! = 1,064,089,721,800
    

    That is probably way too much data to store in memory. So, if you could get n down to 20, through preprocessing then you would be looking at a total of:

    20 choose 6 = 25! / (6!(19!)) = 25*24*23*22*21*20 / 6! = 177,100
    20 choose 5 = 24! / (5!(19!)) = 24*23*22*21,20 / 5!    =  42,504
    20 choose 4 = 23! / (4!(19!)) = 23*22*21*20 / 4!       =   8,855
    20 choose 3 = 22! / (3!(19!)) = 22*21*20 / 3!          =   1,540
    20 choose 2 = 21! / (2!(19!)) = 22*21 / 2!             =     231
                                                             =======
                                                             230,230
    

    If one byte is used for each value of the combination, then the total number of bytes used to store a table (via a jagged array or perhaps 5 separate tables) in memory could be calculated as:

    177,100 * 6 = 1,062,600
     42,504 * 5 =   212,520
      8,855 * 4 =    35,420
      1,540 * 3 =     4,620
        231 * 2 =       462
                  =========
                  1,315,622
    

    It depends on the target machine and how much memory is available, but 1,315,622 bytes is not that much memory when many machines today have gigabytes of memory available.

提交回复
热议问题