在第二个列表中混合两个列表而不重复

时间:2013-04-03 05:29:27

标签: c++ algorithm c++11

我有2个字符串向量(一个大约是另一个的1/3)。我试图实现一个算法,将两个向量随机混合在一起。在结果向量中,先前在向量A中的项目可以相互跟随,但是向量B中的项目不能。

例如,如果向量A中的每个元素都是“FOO”并且向量B中的每个元素都是“BAR”,那么得到的向量可能是{“FOO”,“FOO”,“BAR”,“FOO”,“ BAR”, “FOO”, “FOO”, “BAR”}

正如您所看到的,“FOO”可能会重复,但“BAR”不能

这大致是我到目前为止:

#include <string>
#include <chrono>
#include <algorithm>
#include <random>
#include <vector>

std::vector<std::string> a(1000, "FOO");
std::vector<std::string> b(300, "BAR");
std::vector<std::string> result;

bool resultIsValid();

void mergeVectors()
{
    unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();
    std::mt19937 generator(seed);

    result = a;
    result.insert(result.end(), b.begin(), b.end());
    while (!resultIsValid())
    {
        std::shuffle(a.begin(), a.end(), generator);
    }
}

bool resultIsValid()
{
    for(int i=0; i<result.size()-2; ++i)
        if (result[i] == "BAR" && result[i+1] == "BAR")
            return false;
    return true;
}

这不是实际的代码,但这应该给出一个想法。当我运行它时,程序进入无限循环,因为字符串的实际数量要高得多(在10000范围内),它永远不会得到有效的向量。总是存在至少一个“BAR”顺序重复。有人能够建议一个更好的选择,然后只是继续重新检查创建的矢量为重复的“BAR”?我是否比这更复杂?

5 个答案:

答案 0 :(得分:6)

结果列表包含"BAR","FOO""FOO"个元素。例如

{"FOO","FOO","BAR","FOO","BAR","FOO","FOO","BAR","FOO"}

可以拆分为

"FOO" | "FOO" | "BAR","FOO" | "BAR","FOO" | "FOO" | "BAR","FOO"

可以压缩为

{0, 0, 1, 1, 0, 1}

其中0表示单个元素,1表示从"BAR""FOO"的转换。

01的数量是不变的,因此可以生成包含这些内容的向量并将其随机播放。

唯一的问题是在最后,单个"BAR"也是有效的(如果你将"BAR","FOO"视为原始元素,那么在开始时会出现同样的问题。)

如果包含"FOO"的向量增加1个虚拟元素(sentinel),则可以解决此问题。结果列表始终以"FOO"元素结尾,但在其他方面确实是随机的。但是我们可以安全地删除最后一个元素,因为这是我们的虚拟元素。

实现算法的简单代码(没有在Iterators和Allocators上进行模板化)可能如下所示:

std::vector<std::string> mergeVectors(std::vector<std::string> const& canvas,
                                      std::vector<std::string> const& sprinkle)
{
  assert (canvas.size() + 1>= sprinkle.size()); // otherwise impossible

  std::vector<int> transitions; // 1 for [sprinkle, canvas]
                                // 0 for single [canvas]

  // sprinkle.size() times [canvas, sprinkle]
  transitions.insert(transitions.end(), sprinkle.size(), 1);
  // rest is [canvas].
  transitions.insert(transitions.end(), canvas.size() - sprinkle.size() + 1, 0);

  // There is a problem with the last element since this always is from canvas
  // as well.  So we set the last canvas to a sentinel element which is always removed.
  // This way, we can ensure that the result is truly randomly distributed.

  std::mt19937 generator(std::chrono::system_clock::now().time_since_epoch().count());
  std::shuffle(transitions.begin(), transitions.end(), generator);

  bool last_is_sprinkle = transitions.back(); transitions.pop_back();

  std::vector<std::string> result;
  auto canvas_it   = canvas.begin();
  auto sprinkle_it = sprinkle.begin();

  for (auto t : transitions) {
    if (t) result.push_back(*sprinkle_it++);
    result.push_back(*canvas_it++);
  }
  if (last_is_sprinkle)
    result.push_back(*sprinkle_it);
  return result;
}

答案 1 :(得分:2)

我们可以根据剩余要从A和B中分配的元素数量计算结果向量中下一个元素应该来自A或B的概率,并从A或B中随机选择下一个元素就那个概率而言。

如果选择的最后一个元素来自列表B(这会阻止来自B的连续元素),从列表A中选择下一个元素的概率总是100%。

如果A中剩余的元素数等于B中剩余的元素数,那么如果添加到结果列表中的最后一个元素来自A,那么从B中获取下一个元素的概率是100%。 / p>

否则,从A中选择元素的概率应该等于(A.length() - B.length())/A.length(),假设我们将元素从A和B中取出,因为我们将它们放在结果向量中,所以它们的长度在整个过程中减少到零。我们可以通过测试0到1之间随机生成的值来确定该元素是否应来自列表A或列表B.

这应该保证As和Bs被均匀地洗牌,可以通过多次运行程序并比较结果向量的每一半中的B数来测试。

(编辑)

我认为在Python中测试这个算法更快,所以这是我的Python实现:

from random import random

def intersperse(a,b):
    la = len(a)
    lb = len(b)
    ia = 0
    ib = 0
    bWasLast = False
    res = []
    while (ia < la) or (ib < lb):
        if bWasLast:
            res.append(a[ia])
            ia += 1
            bWasLast = False
        elif ((lb - ib) > (la - ia)) and not bWasLast:
            res.append(b[ib])
            ib += 1
            bWasLast = True
        else:
            laRemaining = la - ia
            lbRemaining = lb - ib
            probA = (laRemaining - lbRemaining)/laRemaining
            if random() < probA:
                res.append(a[ia])
                ia += 1
                bWasLast = False
            else:
                res.append(b[ib])
                ib += 1
                bWasLast = True
    return res

测试代码如下:

A = 'a'*10000
B = 'b'*3000

sumBLeft = 0
sumBRight = 0

for n in range(100):
    r = intersperse(A,B)
    sumBLeft += sum([1 for x in r[:len(r)//2] if x == 'b'])
    sumBRight += sum([1 for x in r[len(r)//2:] if x == 'b'])

print (sumBLeft/sumBRight)

该程序的输出结果显示,结果左侧的数量仅比右侧的数量大0.08%。我的测试结果的一面,确认通过结果向量的分布是均匀的。

答案 2 :(得分:0)

就我从你的代码中看到的那样,你得到了好运,直到你得到的混合是有效的。我的方法如下:

foo带有“FOO”项目的向量和bar带有“BAR”项目的向量,并且FB各自的大小。<登记/> 除非“BAR”项是结果序列中的第一项,否则任何条形项必须以“FOO”项开头。因此,如果我们将“BAR”及其前面的“FOO”组合在一起,我们会得到B“FOOBAR”序列(或B-1,如果我们以“BAR”开头)和{{1 (或F-B)“FOO”介于两者之间。 结果序列以“BAR”项开头的概率为F-(B-1)

我首先为“BAR”和“FOO”项目的混合制作一个模式向量,然后将实际结果向量混合在一起。该模式将由“FOOBAR”和“FOO”项组成,因为“BAR”项必须以“FOO”开头,除非结果序列以“BAR”开头

伪代码:

B/(F+1)

对于示例bool startsWithBar = B < (random * (F+1)); //if the sequence starts with a BAR, there are B-1 FOOBAR items int nFOOBAR = B - (startsWithBar ? 1 : 0); int nFOO = F - nFOOBAR; vector<char> pattern(nFOOBAR, 'm'); //m for mix - FOOBAR = FOO followed by a BAR pattern.insert(pattern.end(), nFOO, 'f'); shuffle(pattern); shuffle(foo); shuffle(bar); vector<string> result; result.reserve(F+B); auto itBar = begin(bar); auto itFoo = begin(foo); //fill the result according to the pattern if (startsWithBar) result.push_back(*itBar++); for (char patt : pattern) { switch (patt) { case 'f': //"FOO" result.push_back(*itFoo++); break; case 'm': //"FOOBAR" result.push_back(*itFoo++); result.push_back(*itBar++); break; } } ,模式为{"FOO","FOO","BAR","FOO","BAR","FOO","FOO","BAR"}

答案 3 :(得分:0)

如果您的第二个向量v2v1短,则必须将v2的每个元素复制到输出向量vout中,并追加v1的一个或多个元素1}}每一次。一种解决方案是,枚举v1的所有可能索引并随机擦除它们,直到只剩下多个索引,因为元素在v2中。因此,两个相邻索引之间的差异是v1中元素的数量,您在vout元素之后插入v2。例如:

    using size_type = std::vector<std::string>::size_type;

    std::vector<size_type> vid(v1.size());
    std::iota(begin(vid), end(vid), 0);
    std::random_shuffle(begin(vid), end(vid));
    vid.erase(std::next(begin(vid), v2.size()), end(vid));
    std::sort(begin(vid), end(vid));

    size_type id_last = 0;
    for(size_type i = 0; i < vid.size(); ++i) {
        vout.insert(end(vout), std::next(begin(v1), id_last),
                                                 std::next(begin(v1), vid[i]));
        vout.push_back(v2[i]);
        id_last = vid[i];
    }
    vout.insert(end(vout), std::next(begin(v1), vid.back()), end(v1));

这可能不是最快的方法,但它应该勾勒出背后的想法。我相信整个索引管理也可以使用boost中的一些迭代器适配器来重写。此外,如果合并后不需要原始字符串向量,则可以移动字符串而不是复制它们。

答案 4 :(得分:0)

一个想法是如下:

  • 随机播放A.
  • 将结果向量C初始化为混洗B.此时,C中的元素为|B|,您知道必须在它们之间放置至少|B|-1个元素。
  • 从混洗的A向量中弹出元素,将它们插入C中的适当位置,以避免C中重复的B元素。
  • 使用A中的其余元素,在随机位置完成对{C}向量的|A|-(|B|-1)插入操作。

假设你有:

A = {FOO1, FOO2, FOO3, FOO4, FOO5}
B = {BAR1, BAR2, BAR3}

第1步&amp; 2:

A = {FOO4, FOO3, FOO1, FOO5, FOO2}
C = {BAR3, BAR1, BAR2}

第3步:

A = {FOO1, FOO5, FOO2}
C = {BAR3, FOO4, BAR1, FOO3, BAR2}

第4步:

C = {FOO1, FOO5, BAR3, FOO4, BAR1, FOO3, FOO2, BAR2}