【问题标题】:Shuffle list, ensuring that no item remains in same position随机列表,确保没有项目保持在同一位置
【发布时间】:2011-11-08 22:37:52
【问题描述】:

我想洗牌一个独特项目的列表,但不做一个完全随机的洗牌。我需要确保洗牌列表中没有元素与原始列表中的位置相同。因此,如果原始列表是(A,B,C,D,E),这个结果会是好的:(C,D,B,E,A),但这个结果不会:(C,E,A, D, B) 因为“D”仍然是第四项。该列表最多有七个项目。不考虑极端效率。我认为对 Fisher/Yates 的这种修改可以解决问题,但我无法从数学上证明:

function shuffle(data) {
    for (var i = 0; i < data.length - 1; i++) {
        var j = i + 1 + Math.floor(Math.random() * (data.length - i - 1));

        var temp = data[j];
        data[j] = data[i];
        data[i] = temp;
    }
}

【问题讨论】:

  • 将每个项目随机放在另一个位置。很有可能您找不到最后一个职位的位置,然后重新开始。
  • 有限递归将在数学上证明您的算法有效:在迭代 i 结束时,位置 i 处的元素不再是原始元素。在迭代 n-2 时,data[n-2] 自动与 data[n-1] 混洗。因此,如果 data[n-1] 仍然保持其原始值,它会在最后一次迭代中被交换。 data[n-1] 也是如此。

标签: algorithm random permutation combinatorics shuffle


【解决方案1】:

正如@FelixCQ 所提到的,您正在寻找的洗牌被称为错位。构建均匀随机分布的紊乱不是一个简单的问题,但文献中已知一些结果。构造紊乱最明显的方法是拒绝方法:使用像 Fisher-Yates 这样的算法生成均匀随机分布的排列,然后拒绝具有固定点的排列。该过程的平均运行时间为 e*n + o(n),其中 e 是欧拉常数 2.71828...这可能适用于您的情况。

另一种产生紊乱的主要方法是使用递归算法。但是,与 Fisher-Yates 不同的是,我们的算法有两个分支:列表中的最后一项可以与另一项交换(即,双循环的一部分),或者可以是一个更大的循环。因此,在每一步,递归算法都必须分支以生成所有可能的混乱。此外,必须以正确的概率决定是选择一个分支还是另一个分支。

令 D(n) 为 n 项的紊乱数。在每个阶段,取最后一项到两个循环的分支数为(n-1)D(n-2),取最后一项到更大循环的分支数为(n-1)D(n -1)。这为我们提供了一种计算错位数的递归方式,即 D(n)=(n-1)(D(n-2)+D(n-1)),并给出了分支到两个-任意阶段循环,即(n-1)D(n-2)/D(n-1)。

现在我们可以通过决定最后一个元素属于哪种类型的循环,将最后一个元素交换到其他 n-1 个位置之一,然后重复来构造紊乱。然而,跟踪所有分支可能很复杂,因此在 2008 年,一些研究人员使用这些想法开发了一种简化的算法。您可以在 http://www.cs.upc.edu/~conrado/research/talks/analco08.pdf 查看演练。该算法的运行时间与 2n + O(log^2 n) 成正比,比拒绝方法的速度提高了 36%。

我已经用 Java 实现了他们的算法。使用 longs 适用于 n 最多 22 左右。使用 BigIntegers 将算法扩展到 n=170 左右。使用 BigIntegers 和 BigDecimals 将算法扩展到 n=40000 左右(限制取决于程序其余部分的内存使用情况)。


    package io.github.edoolittle.combinatorics;

    import java.math.BigInteger;
    import java.math.BigDecimal;
    import java.math.MathContext;
    import java.util.Random;
    import java.util.HashMap;
    import java.util.TreeMap;

    public final class Derangements {

      // cache calculated values to speed up recursive algorithm
      private static HashMap<Integer,BigInteger> numberOfDerangementsMap 
        = new HashMap<Integer,BigInteger>();
      private static int greatestNCached = -1;

      // load numberOfDerangementsMap with initial values D(0)=1 and D(1)=0
      static {
        numberOfDerangementsMap.put(0,BigInteger.valueOf(1));
        numberOfDerangementsMap.put(1,BigInteger.valueOf(0));
        greatestNCached = 1;
      }

      private static Random rand = new Random();

      // private default constructor so class isn't accidentally instantiated
      private Derangements() { }

      public static BigInteger numberOfDerangements(int n)
        throws IllegalArgumentException {
        if (numberOfDerangementsMap.containsKey(n)) {
          return numberOfDerangementsMap.get(n);
        } else if (n>=2) {
          // pre-load the cache to avoid stack overflow (occurs near n=5000)
          for (int i=greatestNCached+1; i<n; i++) numberOfDerangements(i);
          greatestNCached = n-1;
          // recursion for derangements: D(n) = (n-1)*(D(n-1) + D(n-2))
          BigInteger Dn_1 = numberOfDerangements(n-1);
          BigInteger Dn_2 = numberOfDerangements(n-2);
          BigInteger Dn = (Dn_1.add(Dn_2)).multiply(BigInteger.valueOf(n-1));
          numberOfDerangementsMap.put(n,Dn);
          greatestNCached = n;
          return Dn;
        } else {
          throw new IllegalArgumentException("argument must be >= 0 but was " + n);
        }
      }

      public static int[] randomDerangement(int n)
        throws IllegalArgumentException {

        if (n<2)
          throw new IllegalArgumentException("argument must be >= 2 but was " + n);

        int[] result = new int[n];
        boolean[] mark = new boolean[n];

        for (int i=0; i<n; i++) {
          result[i] = i;
          mark[i] = false;
        }
        int unmarked = n;

        for (int i=n-1; i>=0; i--) {
          if (unmarked<2) break; // can't move anything else
          if (mark[i]) continue; // can't move item at i if marked

          // use the rejection method to generate random unmarked index j &lt i;
          // this could be replaced by more straightforward technique
          int j;
          while (mark[j=rand.nextInt(i)]);

          // swap two elements of the array
          int temp = result[i];
          result[i] = result[j];
          result[j] = temp;

          // mark position j as end of cycle with probability (u-1)D(u-2)/D(u)
          double probability 
        = (new BigDecimal(numberOfDerangements(unmarked-2))).
        multiply(new BigDecimal(unmarked-1)).
        divide(new BigDecimal(numberOfDerangements(unmarked)),
               MathContext.DECIMAL64).doubleValue();
          if (rand.nextDouble() < probability) {
        mark[j] = true;
        unmarked--;
          }

          // position i now becomes out of play so we could mark it
          //mark[i] = true;
          // but we don't need to because loop won't touch it from now on
          // however we do have to decrement unmarked
          unmarked--;
        }

        return result;
      }

      // unit tests
      public static void main(String[] args) {
        // test derangement numbers D(i)
        for (int i=0; i<100; i++) {
          System.out.println("D(" + i + ") = " + numberOfDerangements(i));
        }
        System.out.println();

        // test quantity (u-1)D_(u-2)/D_u for overflow, inaccuracy
        for (int u=2; u<100; u++) {
          double d = numberOfDerangements(u-2).doubleValue() * (u-1) /
        numberOfDerangements(u).doubleValue();
          System.out.println((u-1) + " * D(" + (u-2) + ") / D(" + u + ") = " + d);
        }

        System.out.println();

        // test derangements for correctness, uniform distribution
        int size = 5;
        long reps = 10000000;
        TreeMap<String,Integer> countMap = new TreeMap&ltString,Integer>();
        System.out.println("Derangement\tCount");
        System.out.println("-----------\t-----");
        for (long rep = 0; rep < reps; rep++) {
          int[] d = randomDerangement(size);
          String s = "";
          String sep = "";
          if (size > 10) sep = " ";
          for (int i=0; i<d.length; i++) {
        s += d[i] + sep;
          }

          if (countMap.containsKey(s)) {
        countMap.put(s,countMap.get(s)+1);
          } else {
        countMap.put(s,1);
          }
        }

        for (String key : countMap.keySet()) {
          System.out.println(key + "\t\t" + countMap.get(key));
        }

        System.out.println();

        // large random derangement
        int size1 = 1000;
        System.out.println("Random derangement of " + size1 + " elements:");
        int[] d1 = randomDerangement(size1);
        for (int i=0; i<d1.length; i++) {
          System.out.print(d1[i] + " ");
        }

        System.out.println();
        System.out.println();

        System.out.println("We start to run into memory issues around u=40000:");
        {
          // increase this number from 40000 to around 50000 to trigger
          // out of memory-type exceptions
          int u = 40003;
          BigDecimal d = (new BigDecimal(numberOfDerangements(u-2))).
        multiply(new BigDecimal(u-1)).
        divide(new BigDecimal(numberOfDerangements(u)),MathContext.DECIMAL64);
          System.out.println((u-1) + " * D(" + (u-2) + ") / D(" + u + ") = " + d);
        }

      }

    }

【讨论】:

    【解决方案2】:

    在 C++ 中:

    template <class T> void shuffle(std::vector<T>&arr)
    {
        int size = arr.size();
    
        for (auto i = 1; i < size; i++)
        {
            int n = rand() % (size - i) + i;
            std::swap(arr[i-1], arr[n]);
        }
    }
    

    【讨论】:

      【解决方案3】:

      您正在寻找您的条目的derangement。

      首先,您的算法在输出随机紊乱的意义上是有效的,即没有固定点的排列。然而,它有一个巨大的缺陷(您可能不介意,但值得牢记):您的算法无法获得一些混乱。换句话说,它给了一些可能的紊乱的概率为零,所以得到的分布肯定不是均匀随机的。

      如 cmets 中所建议的,一种可能的解决方案是使用拒绝算法:

      • 随机均匀地选择一个排列
      • 如果没有固定点,返回它
      • 否则重试

      渐近地,获得紊乱的概率接近1/e = 0.3679(见维基百科文章)。这意味着要获得一个混乱,您需要生成平均 e = 2.718 个排列,这是相当昂贵的。

      更好的方法是在算法的每一步都拒绝。在伪代码中,类似这样(假设原始数组在位置i 处包含i,即a[i]==i):

      for (i = 1 to n-1) {
         do {
            j = rand(i, n)   // random integer from i to n inclusive
         } while a[j] != i   // rejection part
         swap a[i] a[j]
      }
      

      与您的算法的主要区别是我们允许j 等于i,但前提是它不产生固定点。执行时间稍长(由于拒绝部分),并要求您能够检查条目是否在其原始位置,但它的优点是它可以产生所有可能的混乱(统一地,为此问题)。

      我猜应该存在非拒绝算法,但我认为它们不那么直截了当。

      编辑:

      我的算法实际上很糟糕:你仍然有机会在最后一点未打乱的情况下结束,而且分布根本不是随机的,请参阅模拟的边缘分布:

      可以在here 找到一种产生均匀分布紊乱的算法,其中包含有关该问题的一些背景、详尽的解释和分析。

      第二次编辑:

      实际上,您的算法被称为Sattolo's algorithm,并且以相同的概率产生所有循环。因此,任何不是循环而是几个不相交循环的产物的紊乱都不能用该算法获得。比如有四个元素,1和2、3和4交换的排列是乱序而不是循环。

      如果您不介意只获得循环,那么 Sattolo 算法是可行的方法,它实际上比任何统一紊乱算法都要快得多,因为不需要拒绝。

      【讨论】:

      • 您确定有一些 OP 的算法无法生成的紊乱吗?我不明白为什么。我不知道那是什么语言(Java?),但Math.random() 看起来像一个常见的函数,它返回范围 [0, 1) 内均匀分布的浮点数。鉴于此,循环中的每一步都应将data[i] 与其后面的值之一交换,选择没有偏差。这应该会产生无偏见的混乱,不是吗?你的图形模拟说明了什么?
      • 谢谢!我只是喜欢“精神错乱”这个词;当然是最好的之一。数学的。条款。曾经。我不能产生所有的紊乱这一事实对我的申请没有任何影响,尽管我脑海中有个唠叨的声音说:“但你应该正确地做到这一点。”
      • @Tom:查看我的最新编辑,了解为什么无法获得某些异常。模拟显示,在位置i,j,最初在索引i 进入的概率最终在索引j。第一行相当一致,这意味着第一个条目有相同的机会在第一个位置之外的任何地方结束。但是最后一行显示最后一个条目很有可能在倒数第二个位置结束,并且保持在原位的可能性很小。
      • 现在没时间深入研究这一切,但你有没有想过当i到达length - 2时,data[i] 必须和@切换987654341@,因为它可能仍然是原来的值?事实上,这就是 OP 的程序所做的。
      • @FelixCQ 你能告诉我你是如何绘制分布图的吗?我很感兴趣。
      猜你喜欢
      • 1970-01-01
      • 2016-07-15
      • 2022-12-15
      • 2011-09-22
      • 1970-01-01
      • 2019-07-14
      • 2015-06-30
      • 2023-03-11
      相关资源
      最近更新 更多