Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions leetcode/17/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Step 1

各桁の間に関係性がなさそうなので、素直に全通り生成していく問題かなと思った。

> `1 <= digits.length <= 4`

で、1 つの digit が最大 4 つの文字を取りうるので、`4 ^ 4 = 2 ^ 8 = 2 ^ 10 / 2 ^ 2 = 1024 / 4 = 256` 通りの結果を最悪作ることになる。
木を考えると、一段目が 4、次が16、64、256 と続いて、全部で 340 のノードが存在し得るが、これは C++ が 1秒間で 10^8 - 10^9 回の処理を行えることを考えると、実行時間は問題にならないように思う。
最終的に長さ 4 のstring を 256 個持つことになるが、char が 1 byte、4 つで 4 bytes、std::stringのサイズに詳しくないので一旦そこは無視しても、256 * 4 bytes = 1 KB。特に問題になるような大きさではないだろう。
(追記: std::string は、この文字列の長さなら私の環境で 1 つ 24 bytes, なので最終結果の保持に 256 * 24 bytes = 6KB)

時間計算量: O(n * 4 ^ n) (string construction + generate all possible combinations)
空間計算量: O(n * 4 ^ n) (the result holds 4 ^ n strings with length of n)

テストケースは無事にパスしたのだが、LeetCode 上の実行時間・補助空間使用量の順位が最下位に近い。constant 倍くらいしか違わなそうだが、私の解法よりも効率の良い解法がありそうだ。~Step 2 で見てみることにする。~ と思ったのだが、これ backtracking で行けるのでは? C++のsyntaxを思い出すことにいっぱいいっぱいになってしまっていたが、Pythonだったらnested functions を書くのが楽なので helper 関数を用意して backtracking という方法がすぐに思いついたのにな、悔しい。

-> `step1_backtracking.py`

途中結果をもつ `list[str]` を使い回すことで、`step1.cpp`のように途中結果を全て保持しなくて済んでいる。

ところで、digit -> characters の hashmap は書き下した方がわかりやすいと思う。これ以上の工夫は思いつかず、書き下している方が将来キー配置の変更があったとしても対応しやすく、差分が理解しやすいように思う。

# Step 2

ざっと std::string のサイズについてGoogleした限りこれだ!と思うリファレンスはなかったが、色々見た上での私の理解は以下の通り。
環境によって std::string のオブジェクトサイズは 24\~32 bytes。(私の手元の環境だと24 bytes だった)
短い文字列は Short String Optimization (SSO) によりオブジェクト内部に格納されるが、
一定の長さを超えると SSO が適用されなくなり、ヒープ領域に文字列用のメモリが別途確保される。
46 changes: 46 additions & 0 deletions leetcode/17/step1.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#include <map>
#include <utility>
#include <stack>
#include <string>
#include <vector>

class Solution {
public:
std::vector<std::string> letterCombinations(std::string digits) {
std::map<char, std::vector<char>> digit_to_characters = {
{'2', {'a', 'b', 'c'}},
{'3', {'d', 'e', 'f'}},
{'4', {'g', 'h', 'i'}},
{'5', {'j', 'k', 'l'}},
{'6', {'m', 'n', 'o'}},
{'7', {'p', 'q', 'r', 's'}},
{'8', {'t', 'u', 'v'}},
{'9', {'w', 'x', 'y', 'z'}},
};

std::vector<std::string> possible_combinations;
std::stack<std::pair<int, std::vector<char>>> combinations_in_progress;
combinations_in_progress.push({0, {}});
while (!combinations_in_progress.empty()) {
auto [digit_index, combination_in_progress] = combinations_in_progress.top();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不要なオブジェクトのコピーが走っているところがいくつかありそうです。こことか。std::moveした方が良さそうです。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。自分の中で欠けていた視点だったので、参考になりました。

combinations_in_progress.pop();

if (digit_index == digits.size()) {
std::string possible_combination(
combination_in_progress.begin(),
combination_in_progress.end()
);
possible_combinations.push_back(possible_combination);
continue;
}

for (char possible_character : digit_to_characters[digits[digit_index]]) {
std::vector<char> next_combination(combination_in_progress);
next_combination.push_back(possible_character);
combinations_in_progress.push({digit_index + 1, next_combination});
}
}

return possible_combinations;
}
};
27 changes: 27 additions & 0 deletions leetcode/17/step1_backtracking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class Solution:
def letterCombinations(self, digits: str) -> list[str]:
digit_to_characters: dict[str, list[str]] = {
"2": ["a", "b", "c"],
"3": ["d", "e", "f"],
"4": ["g", "h", "i"],
"5": ["j", "k", "l"],
"6": ["m", "n", "o"],
"7": ["p", "q", "r", "s"],
"8": ["t", "u", "v"],
"9": ["w", "x", "y", "z"],
}

all_combinations = []

def generate_combinations(digit_index: int, combination: list[str]) -> None:
if digit_index == len(digits):
all_combinations.append("".join(combination))
return

for character in digit_to_characters[digits[digit_index]]:
combination.append(character)
generate_combinations(digit_index + 1, combination)
combination.pop()

generate_combinations(0, [])
return all_combinations
27 changes: 27 additions & 0 deletions leetcode/17/step2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class Solution:
def letterCombinations(self, digits: str) -> list[str]:
digit_to_characters: dict[str, list[str]] = {
"2": ["a", "b", "c"],
"3": ["d", "e", "f"],
"4": ["g", "h", "i"],
"5": ["j", "k", "l"],
"6": ["m", "n", "o"],
"7": ["p", "q", "r", "s"],
"8": ["t", "u", "v"],
"9": ["w", "x", "y", "z"],
}

generated = []

def generate_combinations(digit_index: int, generating: list[str]) -> None:
if digit_index == len(digits):
generated.append("".join(generating))
return

for character in digit_to_characters[digits[digit_index]]:
generating.append(character)
generate_combinations(digit_index + 1, generating)
generating.pop()

generate_combinations(0, [])
return generated
28 changes: 28 additions & 0 deletions leetcode/17/step3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class Solution:
def letterCombinations(self, digits: str) -> list[str]:
digit_to_characters = {
"2": "abc",
"3": "def",
"4": "ghi",
"5": "jkl",
"6": "mno",
"7": "pqrs",
"8": "tuv",
"9": "wxyz",
}

def generate_combinations(
digit_index: int, generating: list[str], generated: list[str]
) -> None:
if digit_index == len(digits):
generated.append("".join(generating))
return

for ch in digit_to_characters[digits[digit_index]]:
generating.append(ch)
generate_combinations(digit_index + 1, generating, generated)
generating.pop()

generated = []
generate_combinations(0, [], generated)
return generated
47 changes: 47 additions & 0 deletions leetcode/17/step4.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#include <map>
#include <utility>
#include <stack>
#include <string>
#include <vector>

class Solution {
public:
std::vector<std::string> letterCombinations(std::string digits) {
if (digits.empty()) { return {}; }

std::map<char, std::vector<char>> digit_to_characters = {
{'2', {'a', 'b', 'c'}},
{'3', {'d', 'e', 'f'}},
{'4', {'g', 'h', 'i'}},
{'5', {'j', 'k', 'l'}},
{'6', {'m', 'n', 'o'}},
{'7', {'p', 'q', 'r', 's'}},
{'8', {'t', 'u', 'v'}},
{'9', {'w', 'x', 'y', 'z'}},
};

std::vector<std::string> possible_combinations;
std::stack<std::pair<int, std::vector<char>>> combinations_in_progress;
combinations_in_progress.push({0, {}});
while (!combinations_in_progress.empty()) {
auto top = std::move(combinations_in_progress.top());
combinations_in_progress.pop();
auto [digit_index, combination_in_progress] = std::move(top);

if (digit_index == digits.size()) {
possible_combinations.emplace_back(
combination_in_progress.begin(), combination_in_progress.end()
);
continue;
}

for (char possible_character : digit_to_characters[digits[digit_index]]) {
std::vector<char> next_combination(combination_in_progress);
next_combination.push_back(possible_character);
combinations_in_progress.emplace(digit_index + 1, std::move(next_combination));
}
}

return possible_combinations;
}
};