如何提高生成下一个字典序排列的算法效率?

9

需要注意的是,我手工在纸上进行了数学推导以得出上述证明。我不确定仅使用现代计算机这种媒介是否能够得出这些证明。

下文中,“效率”一词的定义是指在最短时间内完成算法的离散部分或整个算法,无论是在数学上、编程上还是计算上。

在进一步研究如何从原始集合或当前排列生成下一个字典序排列的过程时,重新阅读了@chqrlie在Permutations without recursive function call中的回答后,我试图在纸上写下索引,以观察这些索引之间是否存在任何可以用来执行特定任务的数学关系。

我发现了几个有趣的真相,并在下面证明了它们。

例如,当我们写出以下值时:

a,b,c

或者
abc

或者,相反地编写表示值的索引。
0,1,2

或者

012

既然我们已经知道一个集合,

abc

我们可以通过交换集合中最后两个值或索引来生成下一个字典序排列。
acb

或者

021

我们可以忽略值,这些值可以是任何类型的数据,并专注于使用索引进行检查,因为离散数字更适合相互关联可能的关系,而不是可能无限多的多样化值。
因此,对于原始集的第二个字典索引来说,
abc

我们可以将索引的值表示为数字,并观察。
0,2,1

或者

021

首先需要索引的是

012

第二个是
021

我们知道原始集合的 .length3,如果我们记住原始值集合的 .length,我们就可以删除开头的

0

将索引减少到指定数量的位置

12

并且

21

分别是 0。当下一个操作的结果小于原始集时,可以将 0 作为从原始集引用索引的索引以获取索引 0 处的值。

当我们尝试绘制 1221 之间的潜在关系时,我们发现

21 - 12 = 9

当我们继续时,我们发现下一个词典索引是:
102

减去前面的索引

102 - 21 = 81

其中102是下一个字典序排列,表示为以下数值

bac

这为我们提供了指数之间的常见关系,这些指数用数字表示为九

9

这种关系对于无限数量的以数字表示的输入值是明显且可重复的。我们可以绘制这些关系的图形,这取决于观察者的视角,可以看作两个斜率,当从结果排列集合的第一个值开始绘制图形时,具有倒置的顶点偏移。

// graph 1
0,9,81

// graph 2
abc 012 0
acb 021 1 9
bac 102 2 81
bca 120 3 18
cab 201 4 81
cba 210 5 9

 /\/\
/    \

我们可以观察到,在倾斜斜率上的数字与相应的赤纬斜率上的数字是相同的,这是在将可能的排列总数的分数除以一半的倒数后进行的,例如,对于六个排列,我们减去一个,剩下五个,记住我们仍需要奇数索引集,我们使用倒置的顶点处的数字作为枢轴,剩下四个索引,分别称为倾斜和赤纬斜率。
我们可以观察到,在赤纬斜率图中的数字与相应的倾斜斜率图中具有相同y坐标的相邻角度是相同的。
因此,以下我证明了一个无限的置换集合可以通过利用数字九的加法或乘法来计算、生成、过滤和派生一个无限的输入集合。
9

通过匹配仅包括输入数字索引编号的数字,而不在集合中重复,来计算排列数量。

此外,我证明只需要将索引作为斜率数字或者可能的排列总数除以二加一,即可得出给定输入的排列总数。

正如本文前言所强调的那样,这些计算可能没有经过数小时手动计算才得以实现。屏幕无法像纸张一样提供相同的介质;也无法从各个物理维度查看纸张。

使用编程语言表达算法是另一个任务。

以下是使用“JavaScript”编程语言实现的发现、证明和表达的进展。第一个版本具有不准确的RegExp,如 @Tushar 指出的那样,已更正RegExp,尽管不正确的RegExp返回相同结果。

给定输入为数组

var arr = ["a", "b", "c", "d"];

// version 1

function getNextLexicographicPermutation(arr) {
  var len = arr.length;
  var idx = arr.map(function(_, index) {
    return index
  });
  var re = new RegExp("[" + len + "-9]|(.)(?!=\\1)");
  var n = Math.pow(10, len - 1);
  var p = 9;
  var last = Number(idx.slice().reverse().join(""));
  var res = [idx];
  var curr = Number(res[res.length - 1].join(""));
  while (curr < last) {
    for (var prev = curr, next, k; prev <= last; prev += p) {
      if ((re.test(prev))) {
        k = String(prev);
        if (k.length >= len - 1) { //  && Math.max.apply(Math, j) < len
          next = [];
          for (var i = 0; i < k.length; i++) {
            if (next.indexOf(Number(k[i])) == -1 
              && idx.indexOf(Number(k[i])) !== -1) {
                next.push(Number(k[i]))
            }
          }
          if (prev < n && next.length === len - 1 
             || next.length === len && prev > curr) {
               res[res.length] = next.length < len 
                                 ? [0].concat.apply([0], next) 
                                 : next;
          }
        }
      }
      curr = prev;
    }
  };
  return res.map(function(value) {
    return value.map(function(index) {
      return arr[index]
    })
  })
}

getNextLexicographicPermutation(arr);

数组 arr 的索引数字之间的数值差异图表如下:

// graph 3
// reflecting input `abcd`
[9,81,18,81,9,702,9,171,27,72,18,693,18,72,27,171,9,702,9,81,18,81,9]

// version 2.0 without using `RegExp`

function getNextLexicographicPermutation(arr) {
  var len = arr.length;
  var idx = arr.map(function(_, index) {
    return index
  });
  var n = Math.pow(10, len - 1);
  var p = 9;
  var last = Number(idx.slice().reverse().join(""));
  var res = [];
  var curr = Number(idx.join(""));
  var prev, k, next;

  while (curr <= last) {
    prev = curr;            
    k = String(prev).split("").map(Number);
    if (k.every(function(v) {
        return idx.indexOf(v) !== -1
      }) && k.length >= len - 1) {
      next = [];
      for (var i = 0; i < k.length; i++) {
        if (next.indexOf(Number(k[i])) == -1 
          && idx.indexOf(Number(k[i])) !== -1) {
            next.push(Number(k[i]))
        }
      }
      if (prev < n && next.length === len - 1 
          || prev > n && next.length === len)) {
        res[res.length] = next.length < len 
                          ? [0].concat.apply([0], next) 
                          : next;
      }
    }
    prev += p;
    curr = prev;
  };
  return res.map(function(item) {
    return item.map(function(v) {
      return arr[v]
    })
  })
}
getNextLexicographicPermutation(arr)

第二个版本通过将位掩码替换为 RegExp(由 @lleaff 在 最有效的方法检查范围内的数字,而不重复 中回答),大大提高了效率。由于忽略了对所执行的确切测试进行评论,因此使用 DevTools 生成的相关配置文件,无法精确重现以前问题中发布的数字和时间,除非花更多时间验证已发布的数字。当输入集的 .length 为十时,浏览器选项卡可能已崩溃。重要的是,位掩码测试版本比 RegExp 测试版本更高效。
// version 2.1, substituting bitmask for `RegExp`

function getNextLexicographicPermutation(arr) {
  function checkDigits(min, max, n) {
    var digits = 0;
    while (n) {
      d = (n % 10);
      n = n / 10 >> 0;
      if (d < min || d > max || (digits & (1 << d)))
        return false;
      else
        digits |= 1 << d;
    }
    return true;
  }
  var len = arr.length,
    idx = arr.map(function(_, index) {
      return index
    }),
    p = 9,
    min = 0,
    max = len - 1,
    last = Number(idx.slice().reverse().join("")),
    res = [],
    curr = Number(idx.join("")),
    next;

  while (curr < last) {
    next = (curr += p);
    if (checkDigits(min, max, next)) res[res.length] = next;
    curr = next;
  };

  return res.map(function(item) {
    var item = String(item).split("").map(Number);
    item = item.length < arr.length ? [0].concat(item) : item;
    return item.map(function(index) {
      return arr[index]
    }).filter(Boolean)
  })
}    
getNextLexicographicPermutation(arr);

这些笔记和过程花费了大部分时间,一年多以前才展示和证明。主要是思考如何同时为斜率两侧获取指数,只使用倾斜度指数,而不编码算法。

大部分的数学工作在于尝试推导相邻九的倍数之间的进一步相关性,以便计算下一个九的倍数的确切值。

9 

对于将数字增加九并从结果中过滤重复值的操作。我还没有能够解密相邻的九的倍数之间的相互关系,以至于乘法或除法可以替代加法和排除。

决定最终创建概念证明,证明从无限输入集合中仅使用倾斜度或仅使用可能排列的前半部分加一的索引作为数字,可以生成无限数量的排列。

// version 3, generate second half of permutations using indexes of first 
// half of permutations

function getNextLexicographicPermutation(arr) {

    for (var l = 1, i = k = arr.length; l < i ; k *= l++);

    function checkDigits(min, max, n) {
        var digits = 0;
        while (n) {
            d = (n % 10);
            n = n / 10 >> 0;
            if (d < min || d > max || (digits & (1 << d)))
                return false;
            else
                digits |= 1 << d;
        }
        return true;
    }

    var len = arr.length
    , idx = arr.map(function(_, index) {
        return index
    })
    , p = 9
    , min = 0
    , max = len - 1
    , last = Number(idx.slice().reverse().join(""))
    , curr = Number(idx.join(""))
    , res = []
    , diff = []
    , result = []
    , next;

    while (res.length < (k / 2) + 1) {
        next = (curr += p);
        if (checkDigits(min, max, next)) res[res.length] = next;      
        curr = next;
    };

    for (var i = 0; i < res.length; i++) {
      var item = res[i];
      item = String(item).split("").map(Number);
      item = (item.length < arr.length ? [0].concat(item) : item)
             .map(function(index) {
                return arr[index]
             }).filter(Boolean);
      result.push(item)
    }

    res.reduce(function(a, b) {
      diff.push(b - a);
      return b
    });

    for (var i = 0, curr = res[res.length - 1], n = diff.length - 2
        ; result.length < k;  i++, n--) {
          curr = curr + diff[n];
          result.push(
            String(curr).split("")
            .map(function(index) {
                return arr[index]
            })
          );
    }
    return result;
}
getNextLexicographicPermutation(arr);

算法开发的另一个潜在步骤是给定任意.length输入,能够通过仅使用单个乘法、除法、代数、三角函数或微积分公式,在数学上计算索引和集合的第n个排列的值。

请在答案中包含可重复的基准测试。原因是不能确切地记住如何在DevToolsProfiles中推导出数字,但如果记得正确,那么在使用时会在相应部分的开头和结尾使用console.time()console.timeEnd()console.profile()。备份您的实验;您永远不知道硬盘或操作系统何时会崩溃。通常可以从磁盘中检索数据,但这需要时间和精力。以与保存算法版本相同的方式保存测试结果,以便能够自我复制和供他人复制。原始测试的全部范围已经丢失。

在浏览器标签崩溃之前可以派生多少排列的测试残留只有从不同操作系统检索到的注释。

// 362880 876543210 876543219 
var arr = ["a", "b", "c", "d", "e", "f", "g", "h", "i"];

如果我回忆得正确,当将"j"添加到数组时,Chromium浏览器中的活动选项卡会崩溃。第一个数字是从集合“a”到“i”的排列总数;接下来的两个数字可能是,在版本3之前的某个版本上进行的两个测试的结果,尽管不确定,这是今天所构成的。

这是现在在stackoverflow.com发布上述披露的另一个原因,以保留算法的原则和已完成的代码工作,减少一些灾难性的破坏所有原始笔记和工作的风险。例如,连续几天清醒地尝试解释数字之间的模式和关系,忽略在代码注释中记录所有特定测试模式,尝试将算法移植到代码;或者正如@JaromandaX所描述的情况"PEBCAK"

新的观点可能能够从效率的不同角度看待该算法。

我们可以通过一些保存的代码版本重现一些结果的图表,例如使用console.time()console.timeEnd()performance.now()或其他适当的涉及完成该功能所需时间的测试,这些都可以重现。

// console.time("version N");
// getNextLexicographicPermutation(arr);
// console.timeEnd("version N");

var tests = [];

for (var i = 0; i < 10000; i++) {
  var from = window.performance.now();
  getNextLexicographicPermutation(arr);
  tests.push(window.performance.now() - from);
}

for (var i = 0, k = 0; i < tests.length; i++) {
  k += tests[i];
}

var avg = k/tests.length;

// version 1 `avg`: 0.2989265000001993
// version 2.0 `avg`: 0.8271295000007376
// version 2.1 `avg`: 0.17173000000003666
// version 3 `avg`: 0.12989749999987543

作为一个脚注,我想提一下,我使用了该算法的原则来推导出在Fastest way to generate all binary strings of size n into a boolean array?中预期的结果,其中数字九再次出现作为倾斜坡度中的关键数字,并且相应的偏角也是可观测的。虽然没有对该问题描述的具体输入和结果形式进行进一步的测试和自动化。该答案证明了忽略值并仅使用类似波浪线的增量模式应用于单个数字初始数字零,以推导出集合的无限排列的方法是可行的。
问题:
  1. How can the algorithm be improved as to efficiency; both computationally and mathematically?

  2. Can we create the indexes for both inclination and declination slope at the same time; rather than determining the declination slope after the inclination slope has been calculated?

    1. Are there a mathematical relationships or patterns between the indexes as numbers, that is, for example, the set of numbers at graph 1, graph 2 or graph 3, derived from the input of any four values, for example

      abcd

    or

    ["a", "b", "c", "d"]
    

    that the author has not yet to recognize; which could be used to further reduce the number of computations currently implemented to derive results?


2
@ibrahimmahrir 是的。我选择了stackoverflow.com来发布我的发现、使用javascript的进展和发现。手写笔记和数学计算太多而且各不相同,现在尝试编译还为时过早。这个问题本身花费了大约七个小时来撰写。 - guest271314
2
@Barmar,那么所有主题为“效率”的问题都应该被删除并转移到codereview,尽管它们显然不是。每个用户的投票都是他们自己决定如何使用的。在这里发布问题,因为SO的用户非常有直觉和实现各种算法的能力。如果将其删除并转移到codereview,则可以删除该问题并将数学部分发布在某些同行评审期刊中,或者只是撰写一份白皮书。对于这个用户来说,代码并不那么重要。虽然有兴趣找到消耗最少离散资源和时间的结果路径。 - guest271314
4
好的,你已经让我说服了,我撤回了我的投票。现在请不要再向我发送关于这个问题的评论。 - Barmar
2
由于我不打算回答这个问题,我将分享一些可能对其他人有价值的信息。序列9,81,18,81,9,702,9,171,27,72,18,693,18,72,27,171,9,702,9,81,18,81,9可以放入整数序列在线百科全书(OEIS®)的搜索中。然后,这将提供到其他参考资料、其他相关序列、序列的见解甚至一些用于生成序列的源代码的链接。 - Guy Coder
2
很遗憾,我们不能通过好的评论获得赏金点数。 - Guy Coder
显示剩余66条评论
3个回答

14

TL;DR版本

不幸的是,提高此算法的性能的唯一方法是摒弃它并使用更好的替代方案。

"真理"是否显而易见?(剧透警告:是的,它们很明显)

在您的长文中,我看到您发现了两个“真理”:

  1. 如果将数组索引写为字符串,然后将这些字符串重新解释为数字进行两次连续排列,则差异将是9的倍数

  2. “斜率图”是对称的

不幸的是,这两个事实都非常明显,其中一个甚至不是真的。

第一个事实只有在数组的长度小于10时才成立。如果它超过了10,即一些索引被“映射”到2个字符,则不再成立。如果您知道9的可除性规则(在十进制系统中):数字的总和应该是9的倍数,那么显然是正确的。显然,如果两个数字具有相同的数字,则它们具有相同的模9余数,因此它们的差异是9的倍数。此外,如果您使用基数大于数组长度的任何系统解释字符串,由于同样的原因,差异将是base - 1的倍数。例如,让我们使用8进制系统(列为:排列,排列索引,索引字符串,从8进制转换为十进制的索引字符串,差异):

abc 0 012 10  
acb 1 021 17   7
bac 2 102 66  49
bca 3 120 80  14
cab 4 201 129 49
cba 5 210 136  7

如果你总是使用大于数组长度的系统来进行计算,这个事实将会成立(但你可能需要想出新的数字)。
第二个陈述也很明显,并且是“字典序”定义的直接结果。对于每个索引i,如果我求出第一个i个排列和最后一个i个排列的索引数组之和,那么这个和将始终相同:数组中所有值都等于数组长度。例如:
1. abc 012 - cba 210 => 012 + 210 = 222  
2. acb 021 - cab 201 => 021 + 201 = 222
3. bac 102 - bca 120 => 102 + 120 = 222

这很容易理解,如果你考虑一个负索引数组的排列,例如[-N, -(N-1), ..., -1, 0]。显然,从该数组起始位置开始的第i个排列与正数数组[0, 1, 2, ... N]末尾位置开始的第i个排列仅在符号上相反。 其他问题
  1. 索引之间是否存在数学关系或模式,例如图1、图2或图3中的数字集合,是从任意四个值的输入派生出来的吗?
是的,确实有。实际上这正是你在问题中链接的答案Permutations without recursive function call能够工作的原因。但我怀疑是否有比该答案提供的算法更有效的算法。实际上,该答案试图将所请求排列的位置转换为一个变量基数数字系统中的值,其基数范围从1到数组的长度不等。(对于一个更广泛的变量基数数字系统的例子,请考虑如何将毫秒转换为天-小时-分钟-秒-毫秒。你实际上使用基数1000-60-60-24-无限的数字系统。所以当你看到12345 天 8 小时 58 分钟 15 秒 246 毫秒时,你将其转换为毫秒,即((((12345 * 24 + 8) * 60) + 58 * 60) + 15) * 1000 + 246,也就是将该记号表示为12345 (没有基数/无限制) 天 8 (基数24) 小时 58 (基数60) 分钟 15 (基数60) 秒 246 (基数1000) 毫秒)。

在排列中,你可能需要完成两个不同的任务:

  1. 生成第 i 个排列。你在SO答案中提供的算法是相当高效的。我怀疑还有更好的算法。

  2. 生成所有排列或给定一个排列生成下一个排列。这似乎是你尝试用你的代码实现的。在这种情况下,简单的算法分析给定的排列,找到排列没有排序的第一个位置,并进行交换+排序是相当高效的(这就是拖延者似乎要实现的,但我没有深入研究)。我再次怀疑还有更好的算法。数字算法不能更加高效的主要障碍之一是因为对于它在一般情况下工作(即长度>= 10),你需要在大基数中使用长除法,而这些操作不再是O(1)。


更新(回应评论)

我声称没有办法计算出比直接计算排列序列更有效的数字序列。

我不同意这个命题。你能证明这个声明吗?

不,我甚至不知道如何以正式的方式陈述这个声明(如何定义不计算该数字序列的算法类别?)。但我有一些证据支持这一点。

首先,您可能不是已知宇宙中最聪明的人,而这是一个相对较旧和众所周知的话题。因此,发现比现有算法快得多的算法的机会很低。而且,没有人使用这种技术的事实反对了你。

另一个观点不那么武断:我在#2中建议用于按顺序生成所有排列的算法实际上相当有效,因此很难超越它。

考虑一些步骤来找到下一个排列。首先,您需要找到末尾第一个非降序的位置。假设它是k。需要进行k次比较才能找到它。然后,您需要进行一次交换和排序。但如果您聪明一点,您可能会注意到,这里的“排序”可以更快地完成,因为列表已经排序(但是按相反的顺序)。因此,在这里排序只是查找第k个元素的位置时进行反转。并且考虑到数组已排序,您可以使用具有O(log(k))复杂度的二进制搜索。因此,您需要将k+1个元素移动到内存中并进行不到k次比较。以下是一些代码:
// generates array of all permutatations of array of integers [0, 1, .., n-1]
function permutations(n) {
    // custom version that relies on a fact that all values are unique i.e. there will be no equality
    var binarySearch = function (tgt, arr, start, end) {
        // on small ranges direct loop might be more efficient than actual binary search
        var SMALL_THRESHOLD = 5;
        if (start - end < SMALL_THRESHOLD) {
            for (var i = start; i <= end; i++) {
                if (arr[i] > tgt)
                    return i;
            }
            throw new Error("Impossible");
        }
        else {
            var left = start;
            var right = end;
            while (left < right) {
                var middle = (left + right) >> 1; //safe /2
                var middleV = arr[middle];
                if (middleV < tgt) {
                    left = middle + 1;
                }
                else {
                    right = middle;
                }
            }

            return left;
        }
    };


    var state = [];
    var allPerms = [];
    var i, swapPos, swapTgtPos, half, tmp;
    for (i = 0; i < n; i++)
        state [i] = i

    //console.log(JSON.stringify(state));
    allPerms.push(state.slice()); // enfroce copy
    if (n > 1) {
        while (true) {
            for (swapPos = n - 2; swapPos >= 0; swapPos--) {
                if (state[swapPos] < state[swapPos + 1])
                    break;
            }

            if (swapPos < 0) // we reached the end
                break;

            // reverse end of the array
            half = (n - swapPos) >> 1; // safe /2
            for (i = 1; i < half + 1; i++) {
                //swap [swapPos + i] <-> [n - i]
                tmp = state[n - i];
                state[n - i] = state[swapPos + i];
                state[swapPos + i] = tmp;
            }

            // do the final swap
            swapTgtPos = binarySearch(state[swapPos], state, swapPos + 1, n - 1);
            tmp = state[swapTgtPos];
            state[swapTgtPos] = state[swapPos];
            state[swapPos] = tmp;
            //console.log(JSON.stringify(state));
            allPerms.push(state.slice()); // enfroce copy
        }
    }
    //console.log("n = " + n + " count = " + allPerms.length);
    return allPerms;
}

现在想象一下,您使用基于数字的方法进行相同的操作,并暂时假设您可以即时计算每个步骤要添加的数字。那么您现在需要多少时间?由于您必须使用长算术,并且我们知道您的加法将更改的最高位数是第k位,因此您至少需要执行k次加法和k次溢出比较。当然,您仍然必须至少写入k次内存。因此,要比上述“常规”算法更有效率,您需要一种方法来计算一个k位长的数字(您将要添加的数字),这需要的时间少于在大小为k的数组中执行二进制搜索所需的时间。这对我来说听起来非常困难。例如,仅通过相应系数乘以9(或更确切地说是N-1)可能需要更长时间来使用长算术。

那么你还有什么其他的机会吗?根本不使用长算术。在这种情况下,首先显而易见的论点是,在小的N上比较算法的性能在数学上意义不大(这就是为什么大O符号用于算法复杂度)。尽管从纯粹数学的角度来看,争取“小”的性能可能是有意义的,但对于现实世界中范围在20个元素的数组排列范围内的“大”情况,仍然可以适合一个长(64位)整数。那么不使用长算术可以获得什么?好吧,您的加法和乘法将只需要一条CPU指令。但是,然后您将不得不使用除法将数字拆分回数字,并且每个步骤都需要N次除法和N次检查(即比较)。而N始终大于k,通常更多。因此,这也不像是提高性能的好方法。
总之:建议的算法是有效的,任何基于算术的算法在算术部分可能都不太有效。

从这个角度来看,不明显的是派生每个离散排列之间数值差异所需的九的精确倍数。例如,从您的角度来看,对于输入“abcd”,在索引5和6处的“9,702”之间是否存在明显的数字关系? - guest271314
@guest271314,你似乎没有理解我的意思。我的意思是,我相信任何有效的排列序列算法都不会计算9的倍数序列。或者换句话说,任何有效的计算该序列的算法都将首先计算排列序列,然后推导出数字序列。如果你真的因为某种奇怪的原因想要数字序列(而不是排列),那么请计算排列序列,然后计算差异。 - SergGr
1
它们相互生成。我的观点是,没有比直接计算排列序列更有效的计算数字序列的方法。不,仅有的整数就可以生成排列。单个整数就是排列本身的计算。期望的结果是,在可能的最大N排列中,最多需要进行N次计算。也就是说,例如一个不包含嵌套循环的for循环,直接计算下一个要添加到当前数字的数字。我们可以得到N/2 + 1次总计算。 - guest271314
1
@guest271314,我再试一次:是的,计算差异背后有数学原理,但实际上这个数学原理与计算排列本身的数学原理是相同的,因此直接进行计算更加高效。使用数字进行计算只会增加复杂性,而不会简化任何事情。这就降低了效率。你能接受这个现实吗? - SergGr
如有必要,请进行调整、更正并验证测试。 - guest271314
显示剩余27条评论

2
当解决问题时,拥有许多不同的工具箱会很有帮助。适用于此问题的一种工具是整数序列在线百科全书®(OEIS®)。
正如问题中所指出的那样,这个序列是:
9,81,18,81,9,702,9,171,27,72,18,693,18,72,27,171,9,702,9,81,18,81,9

这可以通过将1、2、3、4的字典排列转换为数字来生成。
1234,1243,1324,1342,1423,1432,2134,2143,2314,2341,2413,2431,    
3124,3142,3214,3241,3412,3421,4123,4132,4213,4231,4312,4321

然后从其前一个排列中减去一个排列,例如:
1243 - 1234 = 9
1324 - 1243 = 81
1342 - 1324 = 18
...

现在OP注意到所有的差值都是9的倍数,因此除以9得到:
1, 9, 2, 9, 1, 78, 1, 19, 3, 8, 2, 77, 2, 8, 3, 19, 1, 78, 1, 9, 2, 9, 1

通过 OEIS 搜索,得到: A217626 - ... 123...m 的排列的首项差分,除以 9(其中 m 足够大,m!> n)。
关于 OP 的问题:

推导出精确数字的数学公式,该数字需要加上或乘以 9,以获得下一个字典序排列的下一个整数表示中的索引。

在链接部分是:

R. J. Cano,有关此序列的其他信息。

点击 有关此序列的其他信息
会出现一张页面,讨论了该序列的对称性。
这些元素是该序列的前4!项。对于此情况(N=4的排列),对称中心是第12个项,因为通过删除它和起始的零,只剩下偶数个项目,它们相对于对称中心的偏移量重复一次,如下所示的变换所示:
0){ 0, 1, 9, 2, 9, 1, 78, 1, 19, 3, 8, 2, 77, 2, 8, 3, 19, 1, 78, 1, 9, 2, 9, 1 }; 删除零,
1){ 1, 9, 2, 9, 1, 78, 1, 19, 3, 8, 2, 77, 2, 8, 3, 19, 1, 78, 1, 9, 2, 9, 1 }; 将其分成三部分之一为对称中心,
2){ 1, 9, 2, 9, 1, 78, 1, 19, 3, 8, 2 }; { 77 }; { 2, 8, 3, 19, 1, 78, 1, 9, 2, 9, 1 }; 删除对称中心,
3){ 1, 9, 2, 9, 1, 78, 1, 19, 3, 8, 2 }; { 2, 8, 3, 19, 1, 78, 1, 9, 2, 9, 1 }; 反射其中一部分,
4){ 1, 9, 2, 9, 1, 78, 1, 19, 3, 8, 2 }; { 1, 9, 2, 9, 1, 78, 1, 19, 3, 8, 2 }; 在此步骤后,出现了一种新的中心。
由于所有可能的排列集都有一些共同的项来自这个序列,我们可以期望在开发其他情况时再次找到这些“中心”。
关于代码/算法,链接部分还有另一个参考资料。
R. J. Cano, C语言中基于独立底盘的序列生成器模板
/*
 * ########################################## 
 * # Base independent sequencer for A217626 #
 * ##########################################
 * 
 * This program is free software.
 * Written by R. J. Cano (remy at ula.ve, Or reemmmyyyy at gmail.com)
 * On Jan 9 2014, for educational purposes and released under 
 * the terms of the General Public License 3.0 (GNU-GPL 3.0);
 * 
 * There is NO warranty not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * 
 * Note: For a large set of terms (>10!-1) the present program might be prone to data type overflows.
 * 
*/

#include <stdio.h>
#include <stdlib.h>

long _base= 10;
long _showOffset= 1;
/** Standard output field width. An aid for comparisons using MD5 checksums. **/
long _normWidth= 13; 
/** Set this to 0 for print everything in a single line. **/
long _onePerLine= 1;
/** 0 for the vector representation, 1 for the integer representation **/
long _objectToShow= 1;

long permute(long*, long*, long);
long vec2polyEval(long*, long, long);

int main(int argc, char *argv[]) {
  long v1[100],v2[100],v3[100],u[100],n,k,l,offset=0;
  _showOffset*= _onePerLine;
  /* The size of the output (n!-1 items) is no longer read from the standard input. 
     scanf("%li",&n); -- Does stop silently, therefore it is avoided. */
  n= strtol(argv[1], NULL, _base); /* Direct conversion from the command line parameter(s) */
  for(k=0; k<100; k++) {
    v1[k]= (k<n)*(k);
    v2[k]= v1[k];
    v3[k]= 0;
  }
  while(permute(v2,u,n)) {
    for(k=0;k<n-1;k++) {
      v3[k+1]=0;
      for(l=k+1;l<n;l++) {
    v3[k+1]+=(u[l]-v2[l]);
      }
    }
    if (_showOffset) printf("%li ", ++offset);
    if (!_onePerLine) printf(",");
    if (!_objectToShow) {
      for(k=0;k+n<_normWidth;k++) { printf(",0"); }
      for(k=0;k<n;k++) { printf(",%li",v3[k]); }
      printf(";");
    } else {
      printf("%li", vec2polyEval(v3,_base,n));
    }
    if (_onePerLine) printf("\n");
  }
  if (!_onePerLine) printf("\n");
  return EXIT_SUCCESS;
}

long permute(long *data, long *previous, long Size) {
  long tau, rho=Size-1, phi=Size-1;
  for (tau=0;tau<Size;tau++) previous[tau]= data[tau];
  while((rho > 0)&&(data[rho]<= data[rho-1])) rho--;
  rho--;
  if(rho<0) return 0;
  while((phi > rho)&&(data[phi]<=data[rho])) phi--;
  tau= data[rho];
  data[rho]= data[phi];
  data[phi]= tau;
  Size--;
  rho++;
  while(Size>rho) {
    tau= data[Size];
    data[Size]= data[rho];
    data[rho]= tau;
    Size--;
    rho++;
  }
  return 1;
}

long vec2polyEval(long* v, long B, long m) {
  long ans=0, pow=1, k;
  for(k=m-1;k>=0;k--) {
    ans+= v[k]*pow;
    pow*= B;
  }
  return ans;
}

例子

我使用免费的Visual Studio Community 2015来运行C代码,并将其构建为Win32控制台项目。


C:\Users\Eric\Documents\Visual Studio 2015\Projects\OEIS_A217626\Debug><b>OEIS_A217626 1</b>

C:\Users\Eric\Documents\Visual Studio 2015\Projects\OEIS_A217626\Debug><b>OEIS_A217626 2</b>
1 1

C:\Users\Eric\Documents\Visual Studio 2015\Projects\OEIS_A217626\Debug><b>OEIS_A217626 3</b>
1 1
2 9
3 2
4 9
5 1


C:\Users\Eric\Documents\Visual Studio 2015\Projects\OEIS_A217626\Debug><b>OEIS_A217626 4</b>
1 1
2 9
3 2
4 9
5 1
6 78
7 1
8 19
9 3
10 8
11 2
12 77
13 2
14 8
15 3
16 19
17 1
18 78
19 1
20 9
21 2
22 9
23 1


C:\Users\Eric\Documents\Visual Studio 2015\Projects\OEIS_A217626\Debug>OEIS_A217626 5
1 1
2 9
3 2
4 9
5 1
6 78
7 1
8 19
9 3
10 8
11 2
12 77
13 2
14 8
15 3
16 19
17 1
18 78
19 1
20 9
21 2
22 9
23 1
24 657
25 1
26 9
27 2
28 9
29 1
30 178
31 1
32 29
33 4
34 7
35 3
36 66
37 2
38 18
39 4
40 18
41 2
42 67
43 1
44 19
45 3
46 8
47 2
48 646
49 1
50 19
51 3
52 8
53 2
54 67
55 1
56 29
57 4
58 7
59 3
60 176
61 3
62 7
63 4
64 29
65 1
66 67
67 2
68 8
69 3
70 19
71 1
72 646
73 2
74 8
75 3
76 19
77 1
78 67
79 2
80 18
81 4
82 18
83 2
84 66
85 3
86 7
87 4
88 29
89 1
90 178
91 1
92 9
93 2
94 9
95 1
96 657
97 1
98 9
99 2
100 9
101 1
102 78
103 1
104 19
105 3
106 8
107 2
108 77
109 2
110 8
111 3
112 19
113 1
114 78
115 1
116 9
117 2
118 9
119 1

一项测试中,包含10个数据,可以有效检验某些算法的表现。

C:\Users\Eric\Documents\Visual Studio 2015\Projects\OEIS_A217626\Debug><b>OEIS_A217626 10</b>
1 1
2 9
3 2
4 9
5 1
6 78
7 1
8 19
9 3
10 8
...
3628790 8
3628791 3
3628792 19
3628793 1
3628794 78
3628795 1
3628796 9
3628797 2
3628798 9
3628799 1

注:答案的数量为X!-110!= 3628800。这是因为它计算的是差异,所以比阶乘少一个。
我没有将此转换为JavaScript,因为你在JavaScript方面的效率可能比我目前更好。目前我专注于Prolog和F#。
补充
如果您访问此问题以了解在计算机上枚举排列的方法,则应阅读两篇特定的论文:
Robert Sedgewick的 Permutation Generation Methods Donald E. Knuth的“计算机编程艺术”第7.2.1.2节草稿生成所有排列 书籍排列组合学由Richard P. Stanley

如果您不接受我的答案,是否有什么原因?当前的答案是否直接决定下一个数字?我已经有一段时间没有使用过Windows了,会尝试找到另一种运行C代码的方法。基准测试是什么? - guest271314
由于尚未发布任何试图或已经解决 OP 查询的数学答案,当前的悬赏仍未到期。 - guest271314
1
在@Siguza的悉心帮助下,我们应该能够使用答案中的代码公式创建JavaScript图形。在第46行添加了*9,即ret += vec2polyEval(v3, _base, n) * 9。初始基准测试,在除了上述调整之外没有尝试任何其他调整的情况下,为0.015065000000000248 https://jsfiddle.net/Lhgmtwk9/1/。 - guest271314
@guest271314 谢谢。非常感谢。 - Guy Coder
在chromium中运行@Siguza提交的main函数和@SergGr提交的permutations函数,每次循环调用10000次,重复20次。结果显示,10000 * 20次调用中,permutations总共用时0.2605399999999979main总共用时0.25556250000000047。https://jsfiddle.net/Lhgmtwk9/4/ - guest271314
如有必要,请进行调整、更正并验证测试。 - guest271314

0

我的数学和英语技能不够好,很难详细阐述这样的问题 :-| 尽管我想玩排列组合,但毕竟做了一些不那么离题的事情。请参见https://dev59.com/oaDia4cB1Zd3GeqPIbtU#43302308中的TL;DR,我有同样的感觉,稍后我会尝试证明我的算法的有效性,我需要一点时间来思考。

var a = "aceg".split(""); 
do {
  console.log(a.join(""));
} while (a = next(a));

function next (a) {
  var i, c, head, next;
  var b = a.slice();
  var n = b.length;
  for (i = 1; i < n; i++) {
    if (b[n - i] > b[n - (i + 1)]) {
      head = n - (i + 1);
      next = n - i;
      for (i = next + 1; i < n; i++) {
        if (b[i] < b[next] && b[i] > b[head]) {
          next = i;
        }
      }
      c = b[head];
      b[head] = b[next];
      b[next] = c;
      return b.slice(0, head + 1).concat(
        b.slice(head + 1).sort()
      );
    }
  }
}


.join("") 显然是必要的,以获得预期结果 https://jsfiddle.net/veo6mmc1/2/,尽管输入数组已被转换为字符串。 - guest271314
@guest271314 是的,这个函数有一个副作用,它会修改原来的数组以计算下一个数组,这确实很糟糕。不过返回值是没问题的。 - user1636522
@guest271314 是的,很容易解决,我们只需要在交换值之前复制传递的数组即可。离题的回答已更新 :-D - user1636522
@guest271314:「答案中的代码与问题中的算法和代码不同。」请再次阅读https://dev59.com/oaDia4cB1Zd3GeqPIbtU#43302308中的TL;DR。我有同样的感觉,但很难表达自己(缺乏数学和英语技能)。 - user1636522
当前的答案不符合OP所描述的要求,也没有回答问题。 - guest271314
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接