Перестановки, исключающие повторяющиеся символы
Я работаю над проблемой Free Code Camp - http://www.freecodecamp.com/challenges/bonfire-no-repeats-please
Описание проблемы выглядит следующим образом:
Возвращает количество полных перестановок предоставленной строки, которая не имеют повторяющихся последовательных букв. Например, "aab" должен return 2, потому что он имеет 6 полных перестановок, но только 2 из них не имеют ту же букву (в этом случае "а" ), повторяющуюся.
Я знаю, что могу решить это, написав программу, которая создает каждую перестановку, а затем отфильтровывает те, у которых есть повторяющиеся символы.
Но у меня есть это гложящее чувство, что я могу решить это математически.
Первый вопрос - могу ли я?
Второй вопрос - Если да, какую формулу я мог бы использовать?
Дальнейшее уточнение -
Приведенный в задаче пример представляет собой "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
Я прочитал много сообщений о Combinatorics и Permutations и просто, кажется, копаю более глубокую дыру для себя. Но я действительно хочу попытаться решить эту проблему эффективно, а не грубую силу через массив всех возможных перестановок.
Я разместил этот вопрос на math.stackexchange - https://math.stackexchange.com/q/1410184/264492
Математика для разрешения случая, когда повторяется только один символ, довольно тривиальна - факториал общего количества символов минус количество доступных пробелов, умноженное на повторяющиеся символы.
- "aab" = 3! - 2! * 2!= 2
- "abcdefa" = 7! - 6! * 2!= 3600
Но попытка выяснить формулу для случаев, когда повторяется более одного символа, ускользнула от меня. например "Abfdefa"
Ответы
Ответ 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
Я использовал формулу для мультимножественных комбинаций для подсчета способов размещения буквенных пар между остальными.
Общим способом, который может добиться некоторого улучшения над решением грубой силы, является перечислить способы чередования букв с повторами, а затем умножить на способы разделения остальных вокруг них с учетом пробелов, которые необходимо заполнить. Пример 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
Ответ 2
Это математический подход, который не требует проверки всех возможных строк.
Начните с этой строки:
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!
Мы также должны учитывать, что два "а" могут быть сами перестановлены в 2! (поскольку у нас есть два "а" ) пути.
Итак, общее число перестановок с двумя "а" вместе:
6! * 2! (= 1,440)
Число перестановок с 'ff'
Конечно, поскольку у нас также есть два "f" , число перестановок с "ff" будет таким же, как и с "aa" :
6! * 2! (= 1,440)
перекрывается
Если бы у нас был только один символ, проблема закончилась, и конечным результатом будет TOTAL - INVALID перестановки.
Но если мы имеем более одного повторяющегося символа, мы подсчитали некоторые из недопустимых строк два или более раз.
Мы должны заметить, что некоторые из перестановок с двумя "а" вместе, также будут иметь два "f" вместе, и наоборот, поэтому нам нужно добавить их обратно.
Как их считать?
Поскольку у нас есть два повторяющихся символа, мы рассмотрим два "сложных" поля: один для вхождений "aa" и другого для "ff" (оба одновременно).
Итак, теперь мы должны заполнить 5 ящиков: один с "aa" , другой с "ff" и 3 с остальными "b", "d" и "e".
Кроме того, каждый из этих "aa" и "bb" можно переставить в 2! пути. Таким образом, общее количество перекрытий:
5! * 2! * 2! (= 480)
ЗАКЛЮЧИТЕЛЬНОЕ РЕШЕНИЕ
Окончательное решение этой проблемы будет:
TOTAL - INVALID + OVERLAPS
И это:
7! - (2 * 6! * 2!) + (5! * 2! * 2!) = 5,040 - 2 * 1,440 + 480 = 2,640
Ответ 3
Казалось, что это довольно простая проблема, но я провел несколько часов на неправильном пути, прежде чем, наконец, выяснил правильную логику. Чтобы найти все перестановки строки с одним или несколькими повторяющимися символами, сохраняя одинаковые символы разделенными:
Начните с строки, например:
abcdabc
Отделите первые записи от повторов:
firsts: abcd
repeat: abc
Найти все перестановки первых:
abcd abdc adbc adcb...
Затем один за другим вставляйте повторы в каждую перестановку, следуя этим правилам:
- Начните с повторного персонажа, чей оригинал на первом месте - например при вставке
abc
в dbac
сначала используйте b
- Повторите два или более повторений после первого события например при вставке
b
в dbac
результаты dbabc
и dbacb
- Затем рекурсия для каждого результата с оставшимися повторяющимися символами
Я видел этот вопрос с одним повторным символом, где число перестановок abcdefa
, где два a сохраняются отдельно, задано как 3600. Однако этот способ подсчета считает abcdefa
и abcdefa
равным быть двумя различными перестановками, "потому что а меняются местами". На мой взгляд, это всего лишь одна перестановка и ее двойная, а правильный ответ - 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");
Ответ 4
Ну, у меня не будет математического решения для вас здесь.
Я думаю, вы знаете, что вы возвращаетесь назад, как я понял из вашего ответа. Таким образом, вы можете использовать Backtracking для генерации всех перестановок и пропускать определенную перестановку всякий раз, когда встречается повторение. Этот метод называется Backtracking и Обрезка.
Пусть n - длина строки решения, скажем (a1, a2,.... a n).
Поэтому во время обратного отслеживания, когда было создано только partial решение, скажем (a1, a2,.... a k), сравните значения в ak и а (к-1).
Очевидно, вам нужно указать ссылку на предыдущую букву (здесь a (k-1))
Если оба одинаковы, то вырваться из частичного решения, не дойдя до конца, и начать создавать другую перестановку из 1.
Ответ 5
Спасибо Lurai за отличное предложение. Это заняло некоторое время и немного длиннее, но вот мое решение (оно проходит все тестовые примеры на FreeCodeCamp после преобразования на JavaScript, конечно) - извинения за дрянные имена переменных (обучение тому, как быть плохим программистом тоже;)): 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
}
}