【问题标题】:How to generate permutations where a[i] != i?如何生成 a[i] != i 的排列?
【发布时间】:2012-01-12 06:51:47
【问题描述】:

假设我有一个整数数组int a[] = {0, 1, ... N-1},其中Na 的大小。现在我需要为所有0 <= i < N 生成a 的所有排列,即a[i] != i。你会怎么做?

【问题讨论】:

  • 你可以访问 STL,还是根本就不是 C++?
  • 不,它不是 C++,但我可以用其他语言编写 next_permutation。问题是我是否可以生成 only “必需”排列而不生成其他排列。
  • 如果你检查 perm 是否有错,你的效率不会太低:大约 N!/e 会通过检查。

标签: algorithm permutation


【解决方案1】:

如果您有权访问 C++ STL,请使用 next_permutation,并在 do-while 循环中对 a[i] != i 进行额外检查。

【讨论】:

  • 注意:这个算法可能看起来效率低下,但实际上,混乱的分数(=没有元素留在其原始位置的排列)在 N 中快速收敛到 exp(-1),所以它是实际上是渐近最优的(假设每个排列都被消耗掉了)。
  • @Per 你是对的:因为 OP 无论如何都希望枚举所有排列,并且因为大约三分之一的排列是混乱的,所以该算法不会非常低效。考虑到编码时间和调试方面的节省,我认为这应该是可以接受的。感谢您的来信!
【解决方案2】:

只是预感:我认为lexicographic permutation 可能可以修改以解决此问题。

通过将奇偶元素对交换到2,1,4,3,6,5,... 来重新排列数组1,2,3,4,5,6,...,以构造具有最低字典顺序的排列。然后使用标准算法,附加约束不能将元素 i 交换到位置 i

如果数组有奇数个元素,则必须在最后进行另一次交换,以确保元素 N-1 不在位置 N-1

【讨论】:

    【解决方案3】:

    这是python中的一个小递归方法:

    def perm(array,permutation = [], i = 1):
        if len(array) > 0 :
            for element in array:
                if element != i:
                    newarray = list(array)
                    newarray.remove(element)
    
                    newpermutation = list(permutation)
                    newpermutation.append(element)
    
                    perm(newarray,newpermutation,i+1)
        else:
            print permutation
    

    运行perm(range(1,5)) 将给出以下输出:

    [2, 1, 4, 3]
    [2, 3, 4, 1]
    [2, 4, 1, 3]
    [3, 1, 4, 2]
    [3, 4, 1, 2]
    [3, 4, 2, 1]
    [4, 1, 2, 3]
    [4, 3, 1, 2]
    [4, 3, 2, 1]
    

    【讨论】:

      【解决方案4】:

      这里有一些 C++ 实现了一个基于递归双射证明的算法

      !n = (n-1) * (!(n-1) + !(n-2)),
      

      其中!nn 项目的混乱数。

      #include <algorithm>
      #include <ctime>
      #include <iostream>
      #include <vector>
      
      static const int N = 12;
      static int count;
      
      template<class RAI>
      void derange(RAI p, RAI a, RAI b, int n) {
          if (n < 2) {
              if (n == 0) {
                  for (int i = 0; i < N; ++i) p[b[i]] = a[i];
                  if (false) {
                      for (int i = 0; i < N; ++i) std::cout << ' ' << p[i];
                      std::cout << '\n';
                  } else {
                      ++count;
                  }
              }
              return;
          }
          for (int i = 0; i < n - 1; ++i) {
              std::swap(a[i], a[n - 1]);
              derange(p, a, b, n - 1);
              std::swap(a[i], a[n - 1]);
              int j = b[i];
              b[i] = b[n - 2];
              b[n - 2] = b[n - 1];
              b[n - 1] = j;
              std::swap(a[i], a[n - 2]);
              derange(p, a, b, n - 2);
              std::swap(a[i], a[n - 2]);
              j = b[n - 1];
              b[n - 1] = b[n - 2];
              b[n - 2] = b[i];
              b[i] = j;
          }
      }
      
      int main() {
          std::vector<int> p(N);
          clock_t begin = clock();
          std::vector<int> a(N);
          std::vector<int> b(N);
          for (int i = 0; i < N; ++i) a[i] = b[i] = i;
          derange(p.begin(), a.begin(), b.begin(), N);
          std::cout << count << " permutations in " << clock() - begin << " clocks for derange()\n";
          count = 0;
          begin = clock();
          for (int i = 0; i < N; ++i) p[i] = i;
          while (std::next_permutation(p.begin(), p.end())) {
              for (int i = 0; i < N; ++i) {
                  if (p[i] == i) goto bad;
              }
              ++count;
          bad:
              ;
          }
          std::cout << count << " permutations in " << clock() - begin << " clocks for next_permutation()\n";
      }
      

      在我的机器上,我得到了

      176214841 permutations in 13741305 clocks for derange()
      176214841 permutations in 14106430 clocks for next_permutation()
      

      恕我直言,这是洗礼。可能双方都需要进行改进(例如,重新实现next_permutation,使用仅扫描已更改元素的混乱测试);留给读者作为练习。

      【讨论】:

      • 另外,derange() 不会按 lex 顺序生成排列。我不知道这对你有没有问题。
      • 先生,您是如何设计这种递归关系的。实际上,我对设计这种递归关系的方法很感兴趣。所以如果你能给我一些帮助,那将是非常有帮助的。
      • 我不是 Per 的派生者,但我猜是 Euler。请参见此处:oeis.org/wiki/Number_of_derangements - 取所有排列(阶乘)并减去所有具有固定点的排列;)。另请注意:!0 = 1、!1 = 0、!2 = 1 等。
      【解决方案5】:

      如果您想避免其他人建议的过滤方法(按字典顺序生成排列并跳过具有固定点的排列),那么您应该基于循环符号而不是单行符号生成它们 (discussion of notation) .

      n 排列的循环类型是n 的一个分区,它是一个弱递减的正整数序列,总和为n。排列没有固定点的条件等价于它的循环类型没有1s。例如,如果n=5,那么可能的循环类型是

      5
      4,1
      3,2
      3,1,1
      2,2,1
      2,1,1,1
      1,1,1,1,1
      

      其中只有53,2 对这个问题有效,因为所有其他都包含1。因此,策略是生成具有至少2 的最小部分的分区,然后对于每个这样的分区,生成具有该循环类型的所有排列。

      【讨论】:

      • related article 讨论相同的方法(即紊乱作为由非平凡循环因子组成的排列)
      【解决方案6】:

      您正在寻找的排列称为错位。正如其他人所观察到的,可以通过生成均匀随机分布的排列然后拒绝具有固定点的排列(其中 a[i] == i)来生成均匀随机分布的紊乱。拒绝方法在时间 e*n + o(n) 中运行,其中 e 是欧拉常数 2.71828...。类似于@Per 的替代算法运行时间为 2*n + O(log^2 n)。但是,我能找到的最快的算法,即早期拒绝算法,运行时间为 (e-1)*(n-1)。不是等待排列生成然后拒绝它(或不拒绝),而是在构建排列时对固定点进行测试,从而尽可能早地拒绝。这是我在 Java 中对异常的早期拒绝方法的实现。

        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 found = false;
      
          while (!found) {
            for (int i=0; i<n; i++) result[i] = i;
            boolean fixed = false;
            for (int i=n-1; i>=0; i--) {
              int j = rand.nextInt(i+1);
              if (i == result[j]) {
                fixed = true;
                break;
              }
              else {
                int temp = result[i];
                result[i] = result[j];
                result[j] = temp;
              }
           }
            if (!fixed) found = true;
          }
      
          return result;
        }
      

      有关替代方法,请参阅我在 Shuffle list, ensuring that no item remains in same position 的帖子。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-05-10
        • 2015-08-26
        • 1970-01-01
        • 2014-09-17
        相关资源
        最近更新 更多