分组的集合的词典顺序

Lexicographic Rank of a Set Partitioned Into Groups

给定一组 8 个序号 {0..7},分成 4 个大小为 2 的组,每组中的数字按升序排列,如何为该组生成排名?排名应该是字典顺序,最好算法复杂度是线性的。

分区示例:

{{0 1} {2 3} {4 5} {6 7}} // Rank 0
...
{{6 7} {4 5} {2 3} {0 1}} // Rank 2519

因为每组中的数字是按升序排列的,所以这些组实际上被视为组合,而不是排列,所以一个包含例如{5 4}永远不会发生。

这组数字如何在[0, 2520)8C2 * 6C2 * 4C2)范围内按顺序排列?

目前我将每个组的排名计算为一个 8C2 组合,然后将每个排名组合在一起,将其视为 base-28 数字。这显然会在排名中留下差距,这对我来说是不可取的。但是,就其价值而言,这是我目前的排名。

#include <array>
using std::array;
#include <cstdint>
#include <cstddef>
#include <iostream>
using std::cout;
using std::endl;

// Calculates n!.
uint32_t factorial(uint32_t n)
{
  return n <= 1 ? 1 : n * factorial(n - 1);
}

// Calculate nCk: n!/((n-k)!*k!).
uint32_t choose(uint32_t n, uint32_t k)
{
  return (n < k)
    ? 0
    : factorial(n) / (factorial(n - k) * factorial(k));
}

template<size_t N, size_t K>
class CombinationRanker
{
  array<array<uint32_t, K+1>, N+1> choices;

public:
  /**
   * Initialize a precomputed array of nCk (N and K inclusive).
   */
  CombinationRanker()
  {
    for (unsigned n = 0; n <= N; ++n)
      for (unsigned k = 0; k <= K; ++k)
        this->choices[n][k] = choose(n, k);
  }

  /**
   * Get the rank of a combination.
   * @param comb A combination array of size K in ascending order.
   */
  uint32_t rank(const array<uint8_t, K> comb) const
  {
    // Formula: (nCk) - ((n-c_1)Ck) - ((n-c_2)C(k-1)) - ... - ((n-c_k)C1)
    // That assumes 1-based combinations with ranks starting at 1, so each
    // element in the combination has 1 added to it, and the end result has 1
    // subtracted from it to make the rank 0-based.
    uint32_t rank = this->choices[N][K];

    for (unsigned i = 0; i < K; ++i)
      rank -= this->choices[N - (comb[i] + 1)][K - i];

    return rank - 1;
  }
};

int main(int argc, char* argv[])
{
  CombinationRanker<8, 2> ranker;

  array<array<uint8_t, 2>, 4> nums =
  {{
    {0, 1}, {2, 3}, {4, 5}, {6, 7}
  }};

  // Horribly sparse rank.
  unsigned rank =
    ranker.rank(nums[0]) * 28 * 28 * 28 +
    ranker.rank(nums[1]) * 28 * 28 +
    ranker.rank(nums[2]) * 28 +
    ranker.rank(nums[3]);

  cout << rank << endl; // 10835, but I want 0.

  return 0;
}

我已将 post 标记为 C++,因为那是我正在使用的语言;但是,用另一种语言回答也没问题。这更像是一个数学问题,但我正在寻找一个我作为程序员而不是数学家可以理解的答案,代码片段在这方面会有所帮助。

这是我想出的。它的复杂度是二次方的,虽然不是最复杂的,但它可以解决问题。基本算法如下

给定一组来自​​ [0..7] 的序列号,并将其划分为无序对,遍历每一对并找到其在满足以下条件的对中的排名 排除它前面的数字。然后将每个等级乘以其变量 根据。每个等级的可变基数为 6C2*4C2*2C24C2*2C22C2.

例如,{{2,3}, {6,7}, {4,5}, {0,1}}:

  • {2, 3} 排名 13。
  • {6, 7} 在不包括 2 和 3 的对中排名第 14。
  • {4, 5} 在不包括 2、3、6 和 7 的对中排名第 5。
  • {0, 1} 被忽略。

一共13*6C2*4C2*2C2 + 14*4C2*2C2 + 5*2C2 = 1259

其他示例:

  • {{0, 1}, {2, 3}, {4, 5}, {6, 7}} -> 0
  • {{2, 3}, {6, 7}, {4, 5}, {0, 1}} -> 1259
  • {{2, 4}, {0, 1}, {3, 5}, {6, 7}} -> 1260
  • {{6, 7}, {4, 5}, {2, 3}, {0, 1}} -> 2519

这是代码中的算法。为了简洁起见,我硬编码了很多。

#include <iostream>
using std::cout;
using std::endl;
#include <array>
using std::array;
#include <cstdint>

typedef array<uint8_t, 2> pair_t;

/**
 * @param set A set of 8 sequential numbers, [0..7], partitioned into unordered
 * pairs.
 */
uint32_t rank(const array<pair_t, 4>& set) {
  // All 28 (8C2) possible unordered subsets of the set of 8 sequential
  // numbers, [0..7], in lexicographic order.  Hard-coded here for brevity.
  array<pair_t, 28> pairs = {{
    {0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7},
            {1, 2}, {1, 3}, {1, 4}, {1, 5}, {1, 6}, {1, 7},
                    {2, 3}, {2, 4}, {2, 5}, {2, 6}, {2, 7},
                            {3, 4}, {3, 5}, {3, 6}, {3, 7},
                                    {4, 5}, {4, 6}, {4, 7},
                                            {5, 6}, {5, 7},
                                                    {6, 7},
  }};

  // Variable base for each rank "digit" (the base corresponding to the rank of
  // each subset): 6C2*4C2*2C2, 4C2*2C2, 2C2.  Again, hard-coded for brevity.
  array<uint32_t, 3> bases = {{90, 6, 1}};

  // Now rank the set.
  uint32_t rank = 0;
  // Rank among this many pairs.  For N=8, 8C2->6C2->4C2->2C2 (28->15->6->1).
  unsigned numRemaining = 28; // N*(N-1)/2
  array<pair_t, 28> remaining = pairs;

  // Loop over the first three unordered subsets.  The last isn't needed for
  // ranking--n from [0...(N-2)/2).
  for (unsigned n = 0; n < 3; ++n)
  {
    unsigned remainingInd = 0;
    const pair_t& sPair = set[n];

    for (unsigned r = 0; r < numRemaining; ++r)
    {
      const pair_t& rPair = remaining[r];

      if (sPair == rPair)
      {
        // Found the pair: rank it relative to the ramining pairs, and multiply
        // it by the base for digit n.
        rank += r * bases[n];
      }
      else if (
        sPair[0] != rPair[0] && sPair[0] != rPair[1] &&
        sPair[1] != rPair[0] && sPair[1] != rPair[1]
      )
      {
        // The pair excludes the numbers in set[n], so keep it in the
        // list of remaining pairs for the next digit's rank.
        remaining[remainingInd++] = rPair;
      }
    }

    // Number of remaining pairs.
    numRemaining = remainingInd;
  }

  return rank;
}

int main(int argc, char* argv[])
{
  // Examples pairs.
  array<array<pair_t, 4>, 7> sets = {{
    {{{0, 1}, {2, 3}, {4, 5}, {6, 7}}},
    {{{0, 1}, {2, 3}, {4, 6}, {5, 7}}},
    {{{0, 1}, {2, 3}, {4, 7}, {5, 6}}},
    {{{0, 1}, {2, 3}, {5, 6}, {4, 7}}},
    // snip
    {{{2, 3}, {6, 7}, {4, 5}, {0, 1}}},
    // snip
    {{{6, 7}, {4, 5}, {1, 3}, {0, 2}}},
    {{{6, 7}, {4, 5}, {2, 3}, {0, 1}}},
  }};

  for (unsigned i = 0; i < 7; ++i)
  {
    const array<pair_t, 4>& set = sets[i];

    cout << rank(set) << ": ";

    for (unsigned j = 0; j < 4; ++j)
      cout << '{' << (unsigned)set[j][0] << ", " << (unsigned)set[j][1] << '}';

    cout << endl;
  }

  return 0;
}

输出:

0: {0, 1}{2, 3}{4, 5}{6, 7}
1: {0, 1}{2, 3}{4, 6}{5, 7}
2: {0, 1}{2, 3}{4, 7}{5, 6}
3: {0, 1}{2, 3}{5, 6}{4, 7}
1259: {2, 3}{6, 7}{4, 5}{0, 1}
2518: {6, 7}{4, 5}{1, 3}{0, 2}
2519: {6, 7}{4, 5}{2, 3}{0, 1}