以随机顺序迭代具有相同数量的1(或0)的二进制数

时间:2017-06-15 13:18:39

标签: algorithm math

我需要以随机顺序生成具有相同数量的1(或0)的二进制数 有谁知道固定长度二进制数的任何有效算法? 2个1位和4位数的示例(只是为了更清楚):

1100
1010
1001
0110
0101
0011

更新 无重复的随机顺序很重要。需要二进制数的序列,而不是单个排列。

5 个答案:

答案 0 :(得分:6)

如果你有足够的内存来存储所有可能的位序列,并且你不介意在得到第一个结果之前生成它们,那么解决方案就是使用一些有效的生成器来生成所有可能的序列。矢量然后使用Fisher-Yates shuffle对矢量进行洗牌。这很容易且没有偏见(只要你使用一个好的随机数生成器进行洗牌)但如果n很大,它会占用大量内存,特别是如果你不确定你需要完成迭代。

但是有一些解决方案不需要在内存中保留所有可能的单词。 (两种解决方案的C实现遵循文本。)

1。 Bit shuffle enumeration

最快的一个(我认为)是首先生成一个比特值的随机混乱,然后一次一个地迭代可能的单词,将shuffle应用于每个值的位。为了避免混淆实际比特的复杂化,可以以格雷码顺序生成字,其中只有两个比特位置从一个字改变到下一个字。 (这也称为"旋转门"迭代,因为添加了每个新1时,必须删除其他一些1。)这样可以更新位掩码快速,但这意味着连续的条目高度相关,这可能不适合某些目的。此外,对于n的小值,可能的位改组的数量非常有限,因此不会产生许多不同的序列。 (例如,对于n为4且k为2的情况,有6个可能的单词可以按6!(720)个不同方式排序,但只有4个!(24 )bit-shuffles。通过在序列中的随机位置开始迭代,可以稍微改善这种情况。)

始终可以找到格雷码。以下是n = 6,k = 3的示例:(每一步都交换粗体位。我想强调它们但是由于某些莫名其妙的原因,SO允许删除但不加下划线。)

111000   010110   100011   010101
101100   001110   010011   001101
011100   101010   001011   101001
110100   011010   000111   011001
100110   110010   100101   110001

此序列可以通过类似于that suggested by @JasonBoubin的递归算法生成 - 唯一的区别是每个递归的后半部分需要以相反的顺序生成 - 但它使用起来很方便算法的非递归版本。下面的示例代码中的一个来自Frank Ruskey's unpublished manuscript on Combinatorial Generation(第130页的算法5.7)。我将其修改为使用基于0的索引,以及添加代码以跟踪二进制表示。

2。随机生成整数序列并将其转换为组合

"更多"随机但有点慢的解决方案是生成一个混合的枚举索引列表(它是[0, n choose k)中的顺序整数),然后找到对应于每个索引的单词。

在连续范围内生成混合的整数列表的最简单的伪随机方法是使用随机选择的线性同余生成器(LCG)。 LCG是递归序列xi = (a * xi-1 + c) mod m。如果m的幂为2,a mod 4为1且c mod 2为1,则该递归将循环遍历所有2个 m 可能的值。要在[0, n choose k)范围内循环,我们只需选择m作为2的下一个较大幂,然后跳过任何不在所需范围内的值。 (由于显而易见的原因,这个数值不到一半。)

要将枚举索引转换为实际的单词,我们会根据n choose k个单词组由{0}开头的n-1 choose k个单词组成的事实对索引进行二项式分解。 1}}以1开头的单词。所以产生i th 字:

  • 如果n-1 choose k-1我们输出一个0,然后在设置了k位的n-1位字组中输出i th ;
  • 否则,我们输出1,然后从i中减去i < n-1 choose k作为设置k-1位的n-1位字的索引。

预先计算所有有用的二项式系数很方便。

LCG的缺点是,在看到前几个术语后,它们很容易预测。此外,n-1 choose ka的一些随机选择的值将产生连续索引高度相关的索引序列。 (此外,低阶位总是非常随机。)通过对最终结果应用随机位混洗,可以稍微改善这些问题中的一些。这在下面的代码中没有说明,但它会减慢很少的速度,应该很明显如何做到这一点。 (它基本上包括用表查找替换c到洗牌位中。)

下面的C代码使用了一些优化,这使得阅读有点难度。二项式系数存储在较低对角线的数组中:

1UL<<n

可以看出, row index [ 0] 1 [ 1] 1 1 [ 3] 1 2 1 [ 6] 1 3 3 1 [10] 1 4 6 4 1 的数组索引为binom(n, k),如果我们有该索引,我们只需减去n(n+1)/2 + k即可找到binom(n-1, k),{ {1}}减去n。为了避免需要在数组中存储零,我们确保永远不会查找binom(n-1, k-1)为负或大于n+1的二项式系数。特别是,如果我们到达kn的递归中的某个点,我们肯定知道要查找的索引是0,因为只有一个可能的单词。此外,一组k == nk == 0的单词集中的索引0 将精确地包含n零后跟k个,这是2 k -1的n位二进制表示。通过在索引达到0时缩短算法,我们可以避免担心n-kk之一不是有效索引的情况。

两种解决方案的C代码

带有混洗位的格雷码

binom(n-1, k)

具有字数转换的枚举索引的LCG

binom(n-1, k-1)

注意:我没有包含void gray_combs(int n, int k) { /* bit[i] is the ith shuffled bit */ uint32_t bit[n+1]; { uint32_t mask = 1; for (int i = 0; i < n; ++i, mask <<= 1) bit[i] = mask; bit[n] = 0; shuffle(bit, n); } /* comb[i] for 0 <= i < k is the index of the ith bit * in the current combination. comb[k] is a sentinel. */ int comb[k + 1]; for (int i = 0; i < k; ++i) comb[i] = i; comb[k] = n; /* Initial word has the first k (shuffled) bits set */ uint32_t word = 0; for (int i = 0; i < k; ++i) word |= bit[i]; /* Now iterate over all combinations */ int j = k - 1; /* See Ruskey for meaning of j */ do { handle(word, n); if (j < 0) { word ^= bit[comb[0]] | bit[comb[0] - 1]; if (--comb[0] == 0) j += 2; } else if (comb[j + 1] == comb[j] + 1) { word ^= bit[comb[j + 1]] | bit[j]; comb[j + 1] = comb[j]; comb[j] = j; if (comb[j + 1] == comb[j] + 1) j += 2; } else if (j > 0) { word ^= bit[comb[j - 1]] | bit[comb[j] + 1]; comb[j - 1] = comb[j]; ++comb[j]; j -= 2; } else { word ^= bit[comb[j]] | bit[comb[j] + 1]; ++comb[j]; } } while (comb[k] == n); } static const uint32_t* binom(unsigned n, unsigned k) { static const uint32_t b[] = { 1, 1, 1, 1, 2, 1, 1, 3, 3, 1, 1, 4, 6, 4, 1, 1, 5, 10, 10, 5, 1, 1, 6, 15, 20, 15, 6, 1, // ... elided for space }; return &b[n * (n + 1) / 2 + k]; } static uint32_t enumerate(const uint32_t* b, uint32_t r, unsigned n, unsigned k) { uint32_t rv = 0; while (r) { do { b -= n; --n; } while (r < *b); r -= *b; --b; --k; rv |= 1UL << n; } return rv + (1UL << k) - 1; } static bool lcg_combs(unsigned n, unsigned k) { const uint32_t* b = binom(n, k); uint32_t count = *b; uint32_t m = 1; while (m < count) m <<= 1; uint32_t a = 4 * randrange(1, m / 4) + 1; uint32_t c = 2 * randrange(0, m / 2) + 1; uint32_t x = randrange(0, m); while (count--) { do x = (a * x + c) & (m - 1); while (x >= *b); handle(enumerate(b, x, n, k), n); } return true; } 的实施;代码随时可用。 randrange生成shuffle范围内的随机整数; randrange(low, lim)随机抽取长度为[low, lim)的整数向量shuffle(vec, n)

此外,循环为每个生成的单词调用vec。必须用每种组合要做的任何事情来取代。

n定义为无效的函数,handle(word, n)在我的笔记本电脑上花了150毫秒来查找所有40,116,600个28位字,并设置了14位。 handle花了5.5秒。

答案 1 :(得分:3)

设置了正好k位的整数很容易生成in order

你可以这样做,然后通过对结果应用位置换来改变顺序(见下文),例如这里是一个随机生成的16位(你应该选择一个具有正确位数的位,基于字大小不是设置位数)位置换(未测试):

uint permute(uint x) {
  x = bit_permute_step(x, 0x00005110, 1);  // Butterfly, stage 0
  x = bit_permute_step(x, 0x00000709, 4);  // Butterfly, stage 2
  x = bit_permute_step(x, 0x000000a1, 8);  // Butterfly, stage 3
  x = bit_permute_step(x, 0x00005404, 1);  // Butterfly, stage 0
  x = bit_permute_step(x, 0x00000231, 2);  // Butterfly, stage 1
  return x;
}

uint bit_permute_step(uint x, uint m, int shift) {
  uint t;
  t = ((x >> shift) ^ x) & m;
  x = (x ^ t) ^ (t << shift);
  return x;
}

生成重新排序的序列很简单:

uint i = (1u << k) - 1;
uint max = i << (wordsize - k);
do
{
    yield permute(i);
    i = nextPermutation(i);
} while (i != max);
yield permute(i); // for max

nextPermutation来自链接问题,

uint nextPermutation(uint v) {
  uint t = (v | (v - 1)) + 1;
  uint w = t | ((((t & -t) / (v & -v)) >> 1) - 1);
  return w;
}

应该选择比特置换作为随机置换(例如取0 ..(wordsize-1)和shuffle)然后转换为bfly掩码(我使用programming.sirrida.de/calcperm.php),而不是随机生成的bfly掩码

答案 2 :(得分:2)

您可以修改常规置换算法以使用二进制。这是C ++中的一个实现:

#include<iostream>
#include<string>
#include<iostream>
void binaryPermutation(int ones, int digits, std::string current){
        if(digits <= 0 && ones <= 0){
                std::cout<<current<<std::endl;
        }
        else if(digits > 0){
                if(ones > 0){
                        binaryPermutation(ones-1, digits-1, current+"1");
                }
                binaryPermutation(ones, digits-1, current+"0");
        }
}
int main()
{
        binaryPermutation(2, 4, "");
        return 0;
}

此代码输出以下内容: 1100 1010 1001 0110 0101 0011

您可以对其进行修改以将这些输出存储在集合中,或者执行除简单打印之外的其他操作。

答案 3 :(得分:2)

这个用于排列的Python解决方案怎么样?

from itertools import permutations

fixed_length = 4
perms = [''.join(p) for p in permutations('11' + '0' * (fixed_length - 2))]
unique_perms = set(perms)

这会将数字作为字符串返回,可以使用int(num, 2)轻松转换。

至于效率,在我的机器上运行这个花了0.021毫秒。

答案 4 :(得分:2)

我认为你可以使用Heap's algorithm。该算法生成n个对象的所有可能的排列。只需创建简单数组并使用算法生成所有可能的排列。

  

如果要使用BINARY操作迭代二进制数,则此算法无效。对于二进制操作,您可以使用LFSR

LFSR是一种迭代所有数字的简单方法。我认为您可以使用LFSR对固定大小的零代数进行一些简单的修改。