排列不包括重复字符

时间:2015-08-29 04:09:25

标签: algorithm combinatorics

我正在处理免费代码营问题 - http://www.freecodecamp.com/challenges/bonfire-no-repeats-please

问题描述如下 -

  

返回所提供字符串的总排列数   没有重复的连续字母。例如,'aab'应该   返回2,因为它总共有6个排列,但只有2个没有排列   有相同的字母(在这种情况下'a')重复。

我知道我可以通过编写一个创建每个排列的程序然后过滤掉重复字符的程序来解决这个问题。

但我有这种啃咬的感觉,我可以用数学方法解决这个问题。

第一个问题 - 我可以吗?

第二个问题 - 如果是,我可以使用什么公式?

进一步详述 -

问题中给出的例子是“aab”,该网站称有六种可能的排列,只有两种符合非重复字符标准:

aab aba baa aab aba baa

问题是每个角色都是唯一的,所以也许“aab”最好被描述为“a1a2b”

此问题的测试如下(返回符合条件的排列数) -

"aab" should return 2
"aaa" should return 0
"abcdefa" should return 3600
"abfdefa" should return 2640
"zzzzzzzz" should return 0

我已经阅读了很多关于组合和排列的帖子,似乎正在为自己挖掘更深的洞。但我真的想要有效地解决这个问题,而不是通过一系列可能的排列来蛮力。

我在math.stackexchange上发布了这个问题 - https://math.stackexchange.com/q/1410184/264492

解决只重复一个字符的情况的数学非常简单 - 字符总数减去可用空格数乘以重复字符。

  • “aab”= 3! - 2! * 2! = 2
  • “abcdefa”= 7! - 6! * 2! = 3600

但是试图找出重复多个角色的实例的公式却让我望而却步。例如“abfdefa”

5 个答案:

答案 0 :(得分:15)

这是一种数学方法,不需要检查所有可能的字符串。

让我们从这个字符串开始:

  

abfdefa

要找到解决方案,我们必须计算排列总数(没有限制),然后减去无效排列。

总体许可

我们必须填充多个位置,这等于原始字符串的长度。让我们将每个位置视为一个小盒子。 所以,如果我们有

  

abfdefa

有7个字符,有七个框要填。我们可以用7个字符中的任何一个填充第一个,第二个用剩余的6个中的任何一个填充,依此类推。所以排列的总数没有限制,是:

  

7 * 6 * 5 * 4 * 3 * 2 * 1 = 7! (= 5,040)

无效的许可

任意排列两个相同字符的排列无效。让我们看看我们有多少人。 为了计算它们,我们将认为任何并排具有相同字符的字符将位于同一个框中。因为他们必须在一起,为什么不认为他们像一个"化合物"字符? 我们的示例字符串有两个重复的字符:' a'出现两次,并且' f'也出现了两次。

' aa' 的排列数量 现在我们只有六个盒子,因为其中一个盒子将被填满' aa':

  

6 * 5 * 4 * 3 * 2 * 1 = 6!

我们还必须考虑两个' a'可以将自己置于2中! (因为我们有两种' a')方式。 因此,有两个'的排列总数。在一起是:

  

6! * 2! (= 1,440)

&fff' 的排列数量 当然,因为我们还有两个' f,#fff'排列的数量。将与' aa':

相同
  

6! * 2! (= 1,440)

<强> OVERLAPS

如果我们只重复了一个字符,那么问题就完成了,最终结果将是TOTAL - INVALID排列。

但是,如果我们有多个重复字符,我们会将一些无效字符串计数两次或更多次。 我们必须注意到一些排列有两个&#39; a&#39;在一起,还会有两个&#39; f&#39;在一起,反之亦然,所以我们需要添加它们。 我们如何算他们? 由于我们有两个重复的字符,我们将考虑两个&#34;复合&#34;盒子:一个用于出现&#39; aa&#39;以及其他用于&#39; ff&#39; (两者同时)。 所以现在我们必须填写5个方框:一个用&#39; aa&#39;其他用#ff&#39;和3用剩余的&#39;&#39;&#39; d& #39;并且&#39; e。 此外,每一个&#39; aa&#39;和&#39; bb&#39;可以在2加权!方法。所以重叠的总数是:

  

5! * 2! * 2! (= 480)

最终解决方案

这个问题的最终解决方案是:

  

TOTAL - INVALID + OVERLAPS

那是:

  

7! - (2 * 6!* 2!)+(5!* 2!* 2!)= 5,040 - 2 * 1,440 + 480 = 2,640

答案 1 :(得分:2)

这似乎是一个直截了当的问题,但在最终弄清楚正确的逻辑之前,我花了好几个小时走错了路。要查找包含一个或多个重复字符的字符串的所有排列,同时保持相同的字符分隔:

以字符串开头:

  

abcdabc

分开重复的第一次出现:

  

第一:abcd
  重复:abc

找出第一个的所有排列:

  

abcd abdc adbc adcb ...

然后,按照以下规则逐个将重复插入每个排列中:

  
      
  • 从重复角色开始,其原作首先出现在第一位   例如将abc插入dbac时,请先使用b
  •   
  • 在第一次出现后重复两次或更多次   例如将b插入dbac时,结果为dbabcdbacb
  •   
  • 然后使用剩余的重复字符递归每个结果
  •   

我已经看到这个问题有一个重复的字符,其中两个a的单独abcdefa的排列数分别为3600.但是,这种计算方式考虑了abcdefaabcdefa是两个截然不同的排列,“因为a被交换了”。在我看来,这只是一个排列和它的双重,正确的答案是1800;下面的算法将返回两个结果。

function seperatedPermutations(str) {
    var total = 0, firsts = "", repeats = "";
    for (var i = 0; i < str.length; i++) {
        char = str.charAt(i);
        if (str.indexOf(char) == i) firsts += char; else repeats += char;
    }
    var firsts = stringPermutator(firsts);
    for (var i = 0; i < firsts.length; i++) {
        insertRepeats(firsts[i], repeats);
    }
    alert("Permutations of \"" + str + "\"\ntotal: " + (Math.pow(2, repeats.length) * total) + ", unique: " + total);

    // RECURSIVE CHARACTER INSERTER
    function insertRepeats(firsts, repeats) {
        var pos = -1;
        for (var i = 0; i < firsts.length, pos < 0; i++) {
            pos = repeats.indexOf(firsts.charAt(i));
        }
        var char = repeats.charAt(pos);
        for (var i = firsts.indexOf(char) + 2; i <= firsts.length; i++) {
            var combi = firsts.slice(0, i) + char + firsts.slice(i);
            if (repeats.length > 1) {
                insertRepeats(combi, repeats.slice(0, pos) + repeats.slice(pos + 1));
            } else {
                document.write(combi + "<BR>");
                ++total;
            }
        }
    }

    // STRING PERMUTATOR (after Filip Nguyen)
    function stringPermutator(str) {
        var fact = [1], permutations = [];
        for (var i = 1; i <= str.length; i++) fact[i] = i * fact[i - 1];
        for (var i = 0; i < fact[str.length]; i++) {
            var perm = "", temp = str, code = i;
            for (var pos = str.length; pos > 0; pos--) {
                var sel = code / fact[pos - 1];
                perm += temp.charAt(sel);
                code = code % fact[pos - 1];
                temp = temp.substring(0, sel) + temp.substring(sel + 1);
            }
            permutations.push(perm);
        }
        return permutations;
    }
}

seperatedPermutations("abfdefa");

基于此abfdefa字符串的结果数量的逻辑计算,包含5个“第一个”字符和2个重复字符(A和F),将是:

  
      
  • 5个“第一个”字符创建5个! = 120排列
  •   
  • 每个角色可以分为5个位置,每个位置有24个排列:
      A****(24)
      *A***(24)
      **A**(24)
      ***A*(24)
      ****A(24)
  •   
  • 对于这些位置中的每一个,重复字符必须在其“第一”之后至少有2个位置,因此分别产生4,3,2和1个位置(对于最后位置,重复是不可能的)。插入重复的字符后,这会产生240个排列:
      A*****(24 * 4)
      *A****(24 * 3)
      **A***(24 * 2)
      ***A**(24 * 1)
  •   
  • 在每种情况下,将重复的第二个字符可以在6个位置,重复字符将有5个,4个,3个,2个和1个位置。但是,第二个(F)字符不能与第一个(A)字符位于同一位置,因此其中一个组合始终是不可能的:
      A******(24 * 4 *(0 + 4 + 3 + 2 + 1))= 24 * 4 * 10 = 960
      *A*****(24 * 3 *(5 + 0 + 3 + 2 + 1))= 24 * 3 * 11 = 792
      **A****(24 * 2 *(5 + 4 + 0 + 2 + 1))= 24 * 2 * 12 = 576
      ***A***(24 * 1 *(5 + 4 + 3 + 0 + 1))= 24 * 1 * 13 = 312
  •   
  • 和960 + 792 + 576 + 312 = 2640,预期结果。
  •   

或者,对于像abfdefa这样的任何字符串,有2个重复:
formula for 2 repeats
其中F是“第一”的数量。

要计算没有相同排列的总数(我认为更有意义),你将这个数除以2 ^ R,其中R是数字或重复。

答案 2 :(得分:1)

嗯,我这里没有任何数学解决方案。

我猜你知道回溯是因为我从你的答案中得到了回应。所以你可以使用Backtracking生成所有排列,并且在遇到重复时跳过特定的排列。此方法称为回溯和修剪

n 为解决方案字符串的长度,例如(a1,a2,.... a n )。 因此,在回溯期间,只有部分解决方案形成时,比如(a1,a2,.... a k )比较 ak 处的值,的第(k-1)即可。 显然你需要维护对前一个字母的引用(这里是(k-1))

如果两者相同,则从部分解决方案中突破,而不是到达结尾并开始从 1 创建另一个排列。

答案 3 :(得分:1)

这是考虑它的一种方式,对我来说似乎有点复杂:减去不允许的邻居的可能性数量。

例如abfdefa

There are 6 ways to place "aa" or "ff" between the 5! ways to arrange the other five 
letters, so altogether 5! * 6 * 2, multiplied by their number of permutations (2).

Based on the inclusion-exclusion principle, we subtract those possibilities that include
both "aa" and "ff" from the count above: 3! * (2 + 4 - 1) choose 2 ways to place both
"aa" and "ff" around the other three letters, and we must multiply by the permutation
counts within (2 * 2) and between (2).

So altogether,

7! - (5! * 6 * 2 * 2 - 3! * (2 + 4 - 1) choose 2 * 2 * 2 * 2) = 2640

我使用multiset combinations的公式来计算在其余部分之间放置字母对的方法。

可以通过蛮力解决方案实现某些改进的一种可推广的方法是枚举将字母与重复交错的方法,然后乘以围绕它们的其余部分的方式,同时考虑必须填充的空格。示例abfdefa可能如下所示:

afaf / fafa  =>  (5 + 3 - 1) choose 3  // all ways to partition the rest
affa / faaf  =>  1 + 4 + (4 + 2 - 1) choose 2  // all three in the middle; two in the middle, one anywhere else; one in the middle, two anywhere else
aaff / ffaa  =>  3 + 1 + 1  // one in each required space, the other anywhere else; two in one required space, one in the other (x2)

最后,乘以排列计数,完全如此:

2 * 2! * 2! * 3! * ((5 + 3 - 1) choose 3 + 1 + 4 + (4 + 2 - 1) choose 2 + 3 + 1 + 1) = 2640

答案 4 :(得分:0)

感谢Lurai提出了很好的建议。花了一段时间,有点冗长,但这是我的解决方案(当然,在转换为JavaScript后,它会传递FreeCodeCamp上的所有测试用例) - 为糟糕的变量名称道歉(学习如何成为一个糟糕的程序员;)):D

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class PermAlone {

    public static int permAlone(String str) {
        int length = str.length();
        int total = 0;
        int invalid = 0;
        int overlap = 0;
        ArrayList<Integer> vals = new ArrayList<>();
        Map<Character, Integer> chars = new HashMap<>();

        // obtain individual characters and their frequencies from the string
        for (int i = 0; i < length; i++) {
            char key = str.charAt(i);
            if (!chars.containsKey(key)) {
                chars.put(key, 1);
            }
            else {
                chars.put(key, chars.get(key) + 1);
            }
        }

        // if one character repeated set total to 0
        if (chars.size() == 1 && length > 1) {
            total = 0;
        }
        // otherwise calculate total, invalid permutations and overlap
        else {
            // calculate total
            total = factorial(length);

            // calculate invalid permutations
            for (char key : chars.keySet()) {
                int len = 0;
                int lenPerm = 0;
                int charPerm = 0;
                int val = chars.get(key);
                int check = 1;
                // if val > 0 there will be more invalid permutations to  calculate
                if (val > 1) {
                    check = val;
                    vals.add(val); 
                }
                while (check > 1) {
                    len = length - check + 1; 
                    lenPerm = factorial(len);
                    charPerm = factorial(check);
                    invalid = lenPerm * charPerm;
                    total -= invalid; 
                    check--; 
                }   
            }

            // calculate overlaps
            if (vals.size() > 1) {
                overlap = factorial(chars.size());
                for (int val : vals) {
                    overlap *= factorial(val);
                }
            }

            total += overlap;
        }
        return total;
    }

    // helper function to calculate factorials - not recursive as I was running out of memory on the platform :?
    private static int factorial(int num) {
        int result = 1;
        if (num == 0 || num == 1) {
            result = num;
        }
        else {
            for (int i = 2; i <= num; i++) {
                result *= i;
            }
        }
        return result;
    }

    public static void main(String[] args) {
        System.out.printf("For %s: %d\n\n", "aab", permAlone("aab")); //   expected 2
        System.out.printf("For %s: %d\n\n", "aaa", permAlone("aaa")); // expected 0
        System.out.printf("For %s: %d\n\n", "aabb", permAlone("aabb")); // expected 8
        System.out.printf("For %s: %d\n\n", "abcdefa", permAlone("abcdefa")); // expected 3600
        System.out.printf("For %s: %d\n\n", "abfdefa", permAlone("abfdefa")); // expected 2640
        System.out.printf("For %s: %d\n\n", "zzzzzzzz", permAlone("zzzzzzzz")); // expected 0
        System.out.printf("For %s: %d\n\n", "a", permAlone("a")); // expected 1
        System.out.printf("For %s: %d\n\n", "aaab", permAlone("aaab")); // expected 0
        System.out.printf("For %s: %d\n\n", "aaabb", permAlone("aaabb")); // expected 12
        System.out.printf("For %s: %d\n\n", "abbc", permAlone("abbc")); //expected 12
    }
}