n位集的有效随机置换

时间:2013-06-09 14:54:05

标签: optimization random combinations bit-manipulation

对于产生具有正确n设置位的位模式的问题,我知道两种实用方法,但它们都有我不满意的限制。

首先,您可以枚举在预先计算的表中设置了多个位的所有可能的单词值,然后在该表中生成随机索引以选择可能的结果。这就产生了一个问题,即随着输出大小的增加,候选输出列表最终变得不切实际。

或者,您可以随机选择n非重叠位位置(例如,通过使用部分Fisher-Yates shuffle)并仅设置这些位。然而,这种方法在比可能结果的数量大得多的空间中计算随机状态。例如,它可以选择三个中的第一个和第二个位,或者它可以单独选择第二个和第一个位。

第二种方法必须消耗来自随机数源的更多位,而不是严格要求的位。由于当它们的顺序不重要时,它按特定顺序选择n位,这意味着它在n!生成相同结果的不同方式之间进行任意区分,并至少消耗{{1多于必要的比特。

这可以避免吗?

显然有第三种方法可以迭代计算和计算合法排列,直到达到随机索引,但这只是第一种方法的时空权衡,并没有直接帮助,除非那里是计算floor(log_2(n!))排列的有效方法。


澄清

第一种方法要求在零和<code>w! / (n!*(w-n)!)</code>之间选择一个随机数(其中n是输出大小),因为这是可能解决方案的数量。

第二种方法要求在零和w,零和n等之间选择w-1个随机值,这些值的乘积为<code>w! / (w-n)!</code>,即{比第一种方法大{0}}倍。

这意味着随机数源已被强制产生位以区分w-2不同的结果,这些结果都是等价的。我想知道是否有一种有效的方法可以避免依赖这种多余的随机性。也许通过使用生成无序位位列表的算法,或者通过直接计算第n位的唯一排列。

5 个答案:

答案 0 :(得分:2)

好像你想要一个Floyd算法的变体:

Algorithm to select a single, random combination of values?

在您的情况下应该特别有用,因为包含测试是一个简单的位掩码操作。这只需要对RNG进行 k 次调用。在下面的代码中,我假设您randint(limit)生成从0limit-1的统一随机数,并且您希望在32-中设置 k 位bit int:

mask = 0;
for (j = 32 - k; j < 32; ++j) {
    r = randint(j+1);
    b = 1 << r;
    if (mask & b) mask |= (1 << j);
    else mask |= b;
}

这里需要多少熵取决于randint()的实现方式。如果 k &gt; 16,将其设置为32 - k 并取消结果。

如果您使用colex顺序而不是词典排名,则生成表示集合中的一个组合的单个随机数的替代建议(数学家将其称为组合的 rank )更简单。这段代码,例如:

for (i = k; i >= 1; --i) {
    while ((b = binomial(n, i)) > r) --n;
    buf[i-1] = n;
    r -= b;
}

将填充数组 buf [] ,索引从0到 n-1 ,用于 k -cination at colex rank [R 。在您的情况下,您将buf[i-1] = n替换为mask |= (1 << n)。二项式()函数是二项式系数,我使用查找表(参见this)。这样可以最有效地利用熵,但我仍然认为Floyd的算法是更好的折衷方案。

答案 1 :(得分:1)

这是理论问题还是实际问题?

你仍然可以进行部分随机播放,但要跟踪其中的顺序并忘记零。在最终订单中有未使用的熵的log(k!)位,供您将来消费。

您也可以直接使用递归(n选择k)=(n-1选择k-1)+(n-1选择k)。生成0到(n选择k)-1之间的随机数。叫它r。迭代从第n个到第一个的所有位。如果我们必须设置i剩余的比特的j,则设置第i个,如果r < (i-1选择j-1)并清除它,减去(i-1选择j-1),否则。

实际上,我不会担心从部分洗牌中浪费熵的几个词;生成一个16位的随机32位字,设置成本在64到80位熵之间,这是完全可以接受的。所需熵的增长率渐渐比理论界限差,所以我会为真正的大词做些不同的事情。

对于非常大的单词,您可能会生成n个独立位,其概率为k / n,为1。这会立即影响你的熵预算(然后是一些),但它只使用线性多位。但是,设置位的数量紧密集中在k周围。对于进一步预期的线性熵成本,我可以修复它。这种方法比部分shuffle方法具有更好的内存局部性,所以我可能更喜欢它在实践中。

答案 2 :(得分:1)

[扩大我的评论:]如果您只有一点原始熵,那么使用PRNG进一步拉伸它。你只需要足够的原始熵来为PRNG播种。使用PRNG进行实际的随机播放,而不是原始熵。对于下一次洗牌,用一些更原始的熵重新种植PRNG。这会扩展原始熵并减少对熵源的需求。

如果您确切知道PRNG需要的数字范围,那么您可以小心地设置自己的LCG PRNG以覆盖适当的范围,同时需要最小的熵来播种它。

ETA:在C ++中有一种next_permutation()方法。尝试使用它。有关详情,请参阅std::next_permutation Implementation Explanation

答案 3 :(得分:0)

我会使用3号解决方案,生成第i个排列 但是你需要生成第一个i-1吗?

你可以用这里提出的分治方法快一点:Returning i-th combination of a bit array也许你可以稍微改进一下解决方案

答案 4 :(得分:0)

<强>背景

从你给出的公式 - w! /((w-n)!* n!)看起来你的问题集与二项式系数有关,二项式系数用于计算唯一组合的数量,而不是处理不同位置重复项的排列。

你说:

&#34;显然有第三种方法可以迭代计算和计算合法排列,直到达到随机索引,但这只是第一种方法的时空权衡,除非有一种有效的方法来计算这些n个排列,否则它并没有直接帮助。

...

这意味着随机数源已被强制产生位以区分n!不同的结果都是等价的。我想知道是否有一种有效的方法可以避免依赖这种多余的随机性。也许通过使用生成无序位位列表的算法,或者直接计算第n位的唯一排列。&#34;

因此,有一种方法可以有效地计算k个索引的第n个唯一组合或排名。 k指数是指独特的组合。例如,假设n选择4个选择3的k情况。这意味着总共可以选择4个数字(0,1,2,3),它们由n表示,它们以3个为一组,由k表示。唯一组合的总数可以计算为n! /((k!*(nk)!)。零等级对应于(2,1,0)的k指数。等级1由(3,1,0)的k指数组表示,等等。

<强>解决方案

有一个公式可用于在没有迭代的情况下在k-index组和相应的秩之间进行非常有效的转换。同样,有一个用于在等级和相应的k-index组之间进行转换的公式。

我写了一篇关于这个公式的论文,以及如何从帕斯卡的三角形中看到它。该论文称为Tablizing The Binomial Coeffieicent

我编写了一个C#类,它位于公共领域,实现了本文中描述的公式。它使用非常少的内存,可以从网站下载。它执行以下任务:

  1. 以任意N选择K到文件的格式输出所有k索引。 K-index可以用更具描述性的字符串或字母代替。

  2. 将k-index转换为正确的词典索引或排序二项系数表中条目的等级。这种技术比依赖迭代的旧发布技术快得多。它通过使用Pascal三角形中固有的数学属性来实现这一点,并且与迭代整个集合相比非常有效。

  3. 将已排序的二项系数表中的索引转换为相应的k-index。使用的技术也比旧的迭代解决方案快得多。

  4. 使用Mark Dominus方法计算二项式系数,它更不容易溢出并使用更大的数字。此版本返回一个long值。至少有一个其他方法返回一个int。确保使用返回long值的方法。

  5. 该类是用.NET C#编写的,它提供了一种通过使用通用列表来管理与问题相关的对象(如果有)的方法。此类的构造函数采用名为InitTable的bool值,当为true时,将创建一个通用列表来保存要管理的对象。如果此值为false,则不会创建表。为了使用上述4种方法,不需要创建该表。提供访问者方法来访问该表。

  6. 有一个关联的测试类,它显示了如何使用该类及其方法。它已经过至少2个案例的广泛测试,并且没有已知的错误。

  7. 以下测试的示例代码演示了如何使用该类并将遍历每个唯一组合:

    public void Test10Choose5()
    {
       String S;
       int Loop;
       int N = 10;  // Total number of elements in the set.
       int K = 5;  // Total number of elements in each group.
       // Create the bin coeff object required to get all
       // the combos for this N choose K combination.
       BinCoeff<int> BC = new BinCoeff<int>(N, K, false);
       int NumCombos = BinCoeff<int>.GetBinCoeff(N, K);
       // The Kindexes array specifies the indexes for a lexigraphic element.
       int[] KIndexes = new int[K];
       StringBuilder SB = new StringBuilder();
       // Loop thru all the combinations for this N choose K case.
       for (int Combo = 0; Combo < NumCombos; Combo++)
       {
          // Get the k-indexes for this combination.  
          BC.GetKIndexes(Combo, KIndexes);
          // Verify that the Kindexes returned can be used to retrive the
          // rank or lexigraphic order of the KIndexes in the table.
          int Val = BC.GetIndex(true, KIndexes);
          if (Val != Combo)
          {
             S = "Val of " + Val.ToString() + " != Combo Value of " + Combo.ToString();
             Console.WriteLine(S);
          }
          SB.Remove(0, SB.Length);
          for (Loop = 0; Loop < K; Loop++)
          {
             SB.Append(KIndexes[Loop].ToString());
             if (Loop < K - 1)
                SB.Append(" ");
          }
          S = "KIndexes = " + SB.ToString();
          Console.WriteLine(S);
       }
    }
    

    因此,将类应用于您的问题的方法是将字大小中的每个位视为项的总数。这将是n!/((k!(n - k)!)公式中的n。要获得k或组大小,只需计算设置为1的位数。您必须创建一个列表或数组每个可能的k的类对象,在这种情况下为32.请注意,类不处理N选择N,N选择0,或N选择1,因此代码必须检查这些情况并返回1 32选择0情况和32选择32情况。对于32选择1,它将需要返回32。

    如果你需要使用不大于32的值选择16(32项的最坏情况 - 产生601,080,390个唯一组合),那么你可以使用32位整数,这是当前类的实现方式。如果需要使用64位整数,则必须将类转换为使用64位长。长期可以容纳的最大值是18,446,744,073,709,551,616,即2 ^ 64.当n为64时,n选择k的最坏情况是64选择32. 64选择32是1,832,624,140,​​942,590,534 - 所以长值将适用于所有64个选择k个案例。如果你需要更大的数字,那么你可能想要研究使用某种大整数类。在C#中,.NET框架有BigInteger class。如果您使用其他语言,则应该不难移植。

    如果您正在寻找一款非常好的PRNG,那么最快,轻便,高质量的产品之一就是Tiny Mersenne Twister或TinyMT。我将代码移植到C ++和C#。它可以找到here,以及原作者C代码的链接。

    您可以考虑采用类似以下示例的方式来代替使用像Fisher-Yates这样的改组算法:

    // Get 7 random cards.
    ulong Card;
    ulong SevenCardHand = 0;
    for (int CardLoop = 0; CardLoop < 7; CardLoop++)
    {
      do
      {
        // The card has a value of between 0 and 51.  So, get a random value and
        // left shift it into the proper bit position.  
        Card = (1UL << RandObj.Next(CardsInDeck));
      } while ((SevenCardHand & Card) != 0);
      SevenCardHand |= Card;
    }
    

    上述代码比任何改组算法(至少用于获得随机卡的子集)更快,因为它仅适用于7个卡而不是52个。它还将卡封装在单个64位字内的各个位中。它还可以更有效地评估扑克手牌。

    作为一方,请注意,我发现最好的二项式系数计算器可以找到非常大的数字(它准确计算出结果中产生超过15,000位数的情况)here