Определите, близки ли две строки
Описание задачи #
Вам дано две строки. Верните true
, если обе строки являются близкими, и false
в противном случае.
Две строки считаются близкими, если одну из другой можно получить с помощью следующих операций.
Операция 1: Поменяйте местами любые два существующих символа (swap).
Например, abcde
-> aecdb
(букву b
поменяли местами с буквой e
).
Операция 2: Преобразуйте каждое появление одного существующего символа в другой существующий символ и сделайте то же самое с другим символом.
Например, aacabb
-> bbcbaa
(все буквы a
превращаются в буквы b
, а все буквы b
превращаются в буквы a
).
Вы можете использовать операции с любой строкой столько раз, сколько необходимо.
Ограничения #
- Длина каждого слова находится в диапазоне от 1 до 105
- Оба слова содержат только строчные буквы латинского алфавита
Примеры #
-
Входные данные:
word1 = "abc", word2 = "bca"
Ответ:
true]
Объяснение
Вы можете получить
word1
изword2
за 2 операции.- Примените операцию 1: «abc» -> «acb»
- Примените операцию 1: «acb» -> «bca»
-
Входные данные:
word1 = "a", word2 = "aa"
Ответ:
false
Объяснение
Невозможно получить
word2
изword1
или наоборот за любое количество операций. -
Входные данные:
word1 = "cabbba", word2 = "abbccc"
Ответ:
true
Объяснение
Вы можете получить
word1
из `word2 за 3 операции.- Примените операцию 1: «cabbba» -> «caabbb»
- Примените операцию 2: «caabbb» -> «baaccc»
- Примените операцию 2: «baaccc» -> «abbccc»
Решение #
Первая мысль, приходящая в голову, это написать некий алгоритм, который будет пытаться путем обозначенных операций привести одно слово к другому. Проблема в том, что порядок выполнений операций может быть любой, а их количество не ограничено. Пойдя по этому пути, наш алгоритм будет работать очень долго в попытках перебрать все возможные варианты. Нам нужно найти более хитрое и простое решение.
В первую очередь нужно внимательно посмотреть на примеры из задачи, а именно на третий пример. Точнее даже на состояния первой строки при переходе во вторую. Для удобства, разместим их в столбик и посчитаем частоту каждой буквы в слове.
cabbba — 1с, 2a, 3b
caabbb — 1c, 2a, 3b
baaccc — 1b, 2a, 3c
abbccc — 1a, 2b, 3c
При внимательном рассмотрении можно заметить, что при преобразовании строки, меняются буквы, но цифры частот остаются неизменными.
Назовем это сигнатурой частоты букв в слове.
То есть независимо от преобразований в слове всегда сохраняется следующая сигнатура частот — [1, 2, 3]
.
На основе этого мы можем сформулировать два достаточных условия для того, чтобы две строки были близкими.
- Сигнатура частоты букв в обоих словах должна быть одинаковой.
- Все буквы из первого слова должны присутствовать во втором слове и наоборот.
Во всех противных случаях строки не являются близкими.
Отсюда следует, что для решения задачи нам потребуется завести 2 счетчика для подсчета частоты каждой из букв в обеих словах. После подсчета у нас получатся два набора частот.
{
"c": 1,
"a": 2,
"b": 3
}
{
"c": 3,
"a": 1,
"b": 2
}
Теперь нам остается сравнить эти частоты:
- В наборах должно быть одинаковое количество ключей и их состав должен совпадать
- В наборах должны быть одинаковые значения (вне зависимости от ключей)
Чтобы оптимизировать сверку значений можно ввести вспомогательную хеш-мапу, у которой в качестве ключа будет частота (значения из наших счетчиков), а в качестве значения - счетчик. При проходе по первому набору мы будем увеличивать наш счетчик, а при проходе по второму — уменьшать. Если в итоге все счетчики окажутся равными нулю, значит набор частот в наборах одинаковый.
Таким образом, после того как мы проитерируемся по первому набору, мы получим такое промежуточное состояние вспомогательных счетчиков:
{
1: 1,
2: 1,
3: 1
}
После итерации по второму набору мы получим следующее итоговое состояние вспомогательных счетчиков:
{
1: 0,
2: 0,
3: 0
}
Для оптимизации мы можем сделать и инкремент и декремент в рамках одного цикла, так как набор ключей должен быть одинаковым.
Так как в итоговом состоянии все вспомогательные счетчики оказались равными нулю, мы можем сказать что строки cabbba
и abbccc
являются близкими по условию задачи.
Реализация #
-
package two_close_strings func closeStrings(word1 string, word2 string) bool { if len(word1) != len(word2) { return false } counter1 := make(map[rune]int) counter2 := make(map[rune]int) for _, char := range word1 { counter1[char]++ } for _, char := range word2 { counter2[char]++ } tempCounter := make(map[int]int) for k, v := range counter1 { v2, exist := counter2[k] if !exist { return false } tempCounter[v]++ tempCounter[v2]-- } for _, count := range tempCounter { if count != 0 { return false } } return true }
-
const fillCounter = (word: string): Record<string, number> => { const counter: Record<string, number> = {} for (let i = 0; i < word.length; i++) { if (!counter[word[i]]) { counter[word[i]] = 0 } counter[word[i]]++ } return counter } export const closeStrings = (word1: string, word2: string): boolean => { if (word1.length !== word2.length) { return false } const counter1 = fillCounter(word1) const counter2 = fillCounter(word2) const tempCounter: Record<number, number> = {} for (const [k, v] of Object.entries(counter1)) { const v2 = counter2[k] if (!v2) { return false } if (!tempCounter[v]) { tempCounter[v] = 0 } if (!tempCounter[v2]) { tempCounter[v2] = 0 } tempCounter[v]++ tempCounter[v2]-- } for (const [_, count] of Object.entries(tempCounter)) { if (count !== 0) { return false } } return true }
Оценка сложности #
По времени
Для формирования частот букв мы дважды итерируемся по каждому слову, то есть выполняем 2n
операций, где n
— длина слова.
Кроме того мы дважды итерируемся по частотам, но эту сложность можно считать константной, так как количество букв в английском алфавите фиксировано.
Таким образом сложность по времени линейная O(n)
.
По памяти
Сложность по памяти константная O(1)
.
Хоть мы и выделяем память для хранения мап, но они имеют фиксированный размер, так как в английском алфавите всего 26 букв.
Таким образом выделение дополнительной памяти можно считать константным.