【问题标题】:Random Permutations随机排列
【发布时间】:2012-06-24 07:53:20
【问题描述】:

我无法找到一种体面的方法来随机打乱std::vector 中的元素,并在进行一些操作后恢复原始顺序。我知道这应该是一个相当琐碎的算法,但我想我太累了……

由于我被限制使用自定义随机数生成器类,我想我不能使用std::random_shuffle,这无论如何也无济于事,因为我还需要保留原始顺序。所以,我的方法是创建一个std::map,作为原始位置和随机位置之间的映射,如下所示:

std::map<unsigned int, unsigned int> getRandomPermutation (const unsigned int &numberOfElements)
{
    std::map<unsigned int, unsigned int> permutation;

    //populate the map
    for (unsigned int i = 0; i < numberOfElements; i++)
    {
        permutation[i] = i;
    }

    //randomize it
    for (unsigned int i = 0; i < numberOfElements; i++)
    {
        //generate a random number in the interval [0, numberOfElements)
        unsigned long randomValue = GetRandomInteger(numberOfElements - 1U);

        //broken swap implementation
        //permutation[i] = randomValue;
        //permutation[randomValue] = i;

        //use this instead:
        std::swap(permutation[i], permutation[randomValue]);
    }

    return permutation;
}

我不确定上述算法是否是随机排列的正确实现,因此欢迎任何改进。

现在,这就是我如何设法利用这个排列图:

std::vector<BigInteger> doStuff (const std::vector<BigInteger> &input)
{
    /// Permute the values in a random order
    std::map<unsigned int, unsigned int> permutation = getRandomPermutation(static_cast<unsigned int>(input.size()));

    std::vector<BigInteger> temp;

    //permute values
    for (unsigned int i = 0; i < static_cast<unsigned int>(input.size()); ++i)
    {
        temp.push_back(input[permutation[i]]);
    }

    //do all sorts of stuff with temp

    /// Reverse the permutation
    std::vector<BigInteger> output;
    for (unsigned int i = 0; i < static_cast<unsigned int>(input.size()); ++i)
    {
        output.push_back(temp[permutation[i]]);
    }

    return output;
}

有些东西告诉我,我应该只能使用一个 std::vector&lt;BigInteger&gt; 来进行该算法,但是,现在,我无法找出最佳解决方案。老实说,我并不关心input 中的数据,所以我什至可以将其设为非常量,覆盖它,然后跳过创建它的副本,但问题是如何实现算法?

如果我做这样的事情,我最终会射中自己的脚,对吗? :)

for (unsigned int i = 0; i < static_cast<unsigned int>(input.size()); ++i)
{
    BigInteger aux = input[i];
    input[i] = input[permutation[i]];
    input[permutation[i]] = aux;
}

编辑:在史蒂夫关于使用“Fisher-Yates”洗牌的评论之后,我相应地更改了我的getRandomPermutation 函数:

std::map<unsigned int, unsigned int> getRandomPermutation (const unsigned int &numberOfElements)
{
    std::map<unsigned int, unsigned int> permutation;

    //populate the map
    for (unsigned int i = 0; i < numberOfElements; i++)
    {
        permutation[i] = i;
    }

    //randomize it
    for (unsigned int i = numberOfElements - 1; i > 0; --i)
    {
        //generate a random number in the interval [0, numberOfElements)
        unsigned long randomValue = GetRandomInteger(i);

        std::swap(permutation[i], permutation[randomValue]);
    }

    return permutation;
}

【问题讨论】:

  • 我可以推荐 bogosort 它将解决您的两个问题。 en.wikipedia.org/wiki/Bogosort
  • 为什么不保存原始列表的状态;完成洗牌后,只需将您保存的列表重新分配给洗牌的列表?
  • @Brendan 我只需要保留订单,而不是列表的内容。这是安全交互协议的一部分,它要求列表中的项目在进行交互之前随机打乱,并且在协议完成后,我需要恢复原始顺序。
  • @RTS 你能详细说明你的想法吗?

标签: c++ algorithm random mapping permutation


【解决方案1】:

如果您要“随机化”包含 n 个元素的向量,您可以创建另一个 std::vector&lt;size_t&gt; index(n),将 index[x] = x 设置为 0 &lt;= x &lt; n,然后随机播放 index。然后您的查找采用以下形式:original_vector[index[i]]。原始向量的顺序从未改变,因此无需恢复顺序。

...限制使用自定义随机数生成器类,我想我不能使用std::random_shuffle...

你注意到这个超载了吗?

template <class RandomAccessIterator, class RandomNumberGenerator>
void random_shuffle ( RandomAccessIterator first, RandomAccessIterator last,
                    RandomNumberGenerator& rand );

有关如何使用兼容对象包装随机数生成器的详细信息,请参阅http://www.sgi.com/tech/stl/RandomNumberGenerator.html

【讨论】:

  • "set index[x] = x for 0 std::iota,如果可用 :-)
  • 哦,即使你因为某种原因必须改变原始向量,你也可以使用索引向量来做,然后再次使用它来逆转这个过程。这是通过遵循索引向量定义的排列中的循环来完成的。
  • 不幸的是,因为这将是一个交互式协议,所以我不能将索引向量与original_vector 一起发送。如果我要使用std::random_shuffle,我需要将它包装在一些自定义类(模板)中,因为我无法将我的随机数生成器插入开箱即用。
  • @Mihai: random_shuffle 有一个可选的第三个参数来指定随机数的来源。
  • @SteveJessop 是的,但它有这个签名:Pointer to unary function taking one argument and returning a value, both of the appropriate difference type (generally ptrdiff_t). The function shall return a value between zero and its argument (lower than this). 这意味着我需要将我的自定义生成器包装在某个函数中,老实说,我认为这不值得付出努力。我刚刚将我的代码转换为使用“Fisher-Yates”洗牌,我可以接受。剩下的就是看看我是否可以改进我使用排列图的方式。
【解决方案2】:

如果您要查找代码中的特定错误:

permutation[i] = randomValue;
permutation[randomValue] = i;

错了。请注意,完成后,每个值不一定会在地图的值中出现一次。所以这不是一个排列,更不用说一个均匀分布的随机排列了。

生成随机排列的正确方法是 Tony 所说的,在最初表示恒等排列的向量上使用 std::random_shuffle。或者,如果您想知道如何正确执行随机播放,请查看“Fisher-Yates”。一般来说,任何从0 .. N-1 统一随机选择N 的方法都注定要失败,因为这意味着它有N^N 可能的运行方式。但是N! 可能有N 项的排列,而N^N 通常不能被N! 整除。因此,每个排列不可能是相同数量的随机选择的结果,即分布不均匀。

问题是如何实现算法?

所以,您有您的排列,并且您想根据该排列重新排序 input 的元素。

要知道的关键是每个排列都是“循环”的组合。也就是说,如果你从一个给定的起点重复排列排列,你就会回到你开始的地方(这条路径就是那个起点所属的循环)。在给定的排列中可能有不止一个这样的循环,如果permutation[i] == i 对一些i,那么i 的循环长度为1。

循环都是不相交的,也就是说每个元素恰好出现在一个循环中。因为循环不会相互“干扰”,所以我们可以通过应用每个循环来应用排列,并且我们可以按任何顺序执行循环。因此,对于每个索引i,我们需要:

  • 检查我们是否已经完成i。如果是,请转到下一个索引。
  • 设置current = i
  • index[current]index[permutation[current]] 交换。所以index[current] 被设置为它的正确值(循环中的下一个元素),它的旧值沿着循环“推”向前。
  • current标记为“完成”
  • 如果permutuation[current]i,我们已经完成了循环。所以循环的第一个值最终出现在循环的最后一个元素之前占据的位置,这是正确的。转到下一个索引。
  • 设置current = permutation[current] 并返回交换步骤。

根据所涉及的类型,您可以围绕交换进行优化 - 最好复制/移动到临时变量和每个循环的开始,然后在每个步骤执行复制/移动而不是交换循环,最后复制/移动临时到循环结束。

反转过程是相同的,但使用排列的“逆”。排列perm 的逆inv 是排列使得inv[perm[i]] == i 对应每个i。您可以计算逆并使用上面的确切代码,也可以使用与上面类似的代码,除了沿每个循环沿相反方向移动元素。

所有这一切的替代方案,因为您自己实现了 Fisher-Yates - 当您运行 Fisher-Yates 时,对于每次执行的交换,您都会记录在 vector&lt;pair&lt;size_t,size_t&gt;&gt; 中交换的两个索引。然后你不必担心周期。您可以通过应用相同的交换序列将排列应用于向量。您可以通过应用相反的交换序列来反转排列。

【讨论】:

  • 关于更正,实际上是可以的。无论我执行多少次迭代,我都不知道我怎么会得到任何重复。感谢“Fisher-Yates”(Knuth shuffle)的建议。我记得现在在某个地方看到过,但是,昨晚真的很晚:)
  • @MihaiTodor:对不起,我可能看错了代码,昨晚有点晚了。我的想法是,“假设randomValue 恰好每次都出现0”。然后你的排列图将全零作为值,除了permutation[0] 将等于numberOfElements - 1。对不起,如果那是错误的。
  • 有趣的是,如果randomValue 每次都为0,那么我的排列最终将成为恒等排列。请记住,permutationstd::map,而不是 std::vector。现在,我正在尝试理解您关于迭代次数的评论,但这让我很难过。你能提供一个具体的例子吗?我认为这只是关于我执行的迭代次数的问题。如果我只进行 N 次迭代,那么数学表明我最终不会得到一个正确打乱的排列,对吧?
  • “记住排列是一个 std::map,而不是一个 std::vector”——我不明白这有什么不同。在您的原始代码中,如果您执行for (unsigned int i = 0; i &lt; numberOfElements; i++) { permutation[i] = 0; permutation[0] = i; },那么无论permutation 是哪种容器,您最终都会得到很多零。
  • @Mihai:顺便说一句,如果你的 RNG 从种子开始工作,那么在置换时,你可以在 input 上执行交换而不用 permutation,这正是 random_shuffle 所做的。反转排列时,您可以使用相同的种子重新播种 RNG,然后将交换记录到向量(或std::stack)中,然后在input 上向后回放它们。所以如果你的RNG是可重现的,你实际上只需要简单地存储交换。
【解决方案3】:

请注意,根据您的应用程序,如果您有一个真正均匀分布的排列很重要,您不能使用任何算法多次调用典型的伪随机数生成器。

原因是大多数伪随机数生成器,例如 clib 中的那个,都是线性同余的。那些有一个弱点,他们会生成在平面上聚集的数字 - 所以你的排列不会完全均匀分布。使用更高质量的生成器应该可以解决这个问题。

http://en.wikipedia.org/wiki/Linear_congruential_generator

或者,您可以在 0..(n!-1) 范围内生成一个随机数,并将其传递给 unrank 函数进行排列。对于足够小的 n,您可以存储它们并获得恒定时间算法,但如果 n 太大,则最好的 unrank 函数是 O(n)。无论如何,应用得到的排列将是 O(n)。

【讨论】:

  • 我正在使用 GMP 库生成随机数,它实现了 Mersenne Twister 算法。尽管这很慢,但无论如何我都应该预先生成随机数的缓存,所以我认为现在就足够了。无论如何,该代码并不打算最终投入生产。只是为了做一些加密协议的模拟。
【解决方案4】:

给定元素的有序序列a,b,c,d,e,您首先创建一个新的索引序列:X=(0,a),(1,b),(2,c),(3,d),(4,e)。然后,您随机打乱该序列并获取每对的第二个元素以获得随机序列。要恢复原始序列,您可以使用每对的第一个元素对 X 集进行递增排序。

【讨论】:

  • 嗯,是的,但这将涉及将我的向量实现从 std::vector 更改为 std::map,这不是我想要的,因为在应用排列之后,我必须将其内容克隆到一个临时的std::vector
  • 答案很好,但是如果你把它设为(0,&amp;a),(1,&amp;b),(2,&amp;c),(3,&amp;d),(4,&amp;e),存储指向元素而不是元素本身的指针,假设元素的向量在中间没有改变(这会使指针无效)。
猜你喜欢
  • 2021-07-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-08-26
  • 1970-01-01
  • 1970-01-01
  • 2022-06-15
相关资源
最近更新 更多