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
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"files.associations": {
"string": "cpp",
"string_view": "cpp"
}
}
143 changes: 143 additions & 0 deletions 929.-Unique-Email-Addresses/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# step1
愚直に@の手前までは"."を無視して"+"が出現したら後は入れないとし、@発見後はそのままコピーし構築する方法。

他にあるとしたら正規表現とか?
```cpp
#include <set>
#include <vector>

class Solution {
public:
int numUniqueEmails(std::vector<std::string>& emails) {
std::set<std::string> result;
for (const auto& email : emails) {
std::string normalized_email;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

比較目的で正規化する場合は Canonicalize と言うようです。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

コメントありがとうございます。確かに「データを正規化する」とった文脈とは意味合いが違いますね。


auto it = email.begin();
bool findplus = false;
while (*it != '@') {
if (findplus) {
++it;
continue;
}
if (*it == '.') {
++it;
continue;
}
if (*it == '+') {
findplus = true;
++it;
continue;
}
normalized_email.push_back(*it);
++it;
}

while (it != email.end()) {
normalized_email.push_back(*it);
++it;
}
result.insert(std::move(normalized_email));
}
return result.size();
}
};
```

# step2
STLの文字列の機能を使った実装
```cpp
#include <algorithm>
#include <set>
#include <vector>

class Solution {
public:
int numUniqueEmails(std::vector<std::string>& emails) {
std::set<std::string> seen;
for (const auto& email : emails) {
seen.insert(std::move(normalize_email(email)));
}
return seen.size();
}
private:
std::string normalize_email(const std::string& email) {
auto at_position = email.find('@');
auto local = email.substr(0, at_position);
auto at_domain = email.substr(at_position);
if (auto plus_position = local.find('+'); plus_position != std::string::npos) {
local.resize(plus_position);
}
// C++20以降ではstd::erase(local, '.');でも可
local.erase(std::remove(local.begin(), local.end(), '.'), local.end());
return local + at_domain;
}
};

```
あるいはをやや不自然な感じもするがrangesを使った実装
```cpp
#include <ranges>
#include <set>
#include <vector>

class Solution {
public:
int numUniqueEmails(std::vector<std::string>& emails) {
std::set<std::string> seen;
for (const auto& email : emails) {
auto name_ranges = email | std::views::split('@');
auto it = name_ranges.begin();
auto local_range = *it;
auto domain_range = *(++it);
auto filtered_local_range = local_range | std::views::take_while([](const auto c) { return c != '+'; }) |
std::views::filter([](const auto c) { return c != '.'; });
std::string processed_email = std::ranges::to<std::string>(filtered_local_range);
processed_email += '@';
processed_email += std::ranges::to<std::string>(domain_range);
seen.insert(std::move(processed_email));
}
return seen.size();
}
};

```

## 他の人のPRを読む
- https://github.com/shintaroyoshida20/leetcode/tree/feature/hash-map/unique-email-addresses
ちゃんと入力が正しいことをチェックすると大変そう。RFCは気になったときに目を通したい…
異常系も考慮するなら、ユーザーの入力のバリデーションの場面なら例外を投げてやり直させる/データ処理の場面なら不適データは無視する、といった形にする気がする。
- 整理できていなかったが、Pythonではstr1+=cとしたときに元の文字列の再構築が起きるためループで順次足すのはまずいが、C++では問題ない。https://github.com/Ryotaro25/leetcode_first60/pull/15/files#r1641792391
>C++ は、文字列は mutable なので特に問題はないです。Java、Python は immutable です。
ちなみに、こういうのはよく知らない言語を触るときに一番初めに確認することの一つです。


# step3
```cpp
#include <set>
#include <string>
#include <vector>

class Solution {
public:
int numUniqueEmails(const std::vector<std::string>& emails) {
std::set<std::string> seen;
for (const auto& email : emails) {
seen.insert(std::move(normalized_email(email)));
}
return seen.size();
}
private:
std::string normalized_email(const std::string& email) {
auto at_position = email.find('@');
auto local = email.substr(0, at_position);
auto at_domain = email.substr(at_position);
if (auto plus_position = local.find('+'); plus_position != std::string::npos) {
local.resize(plus_position);
}
std::erase(local, '.');
return local + at_domain;
}
};
Comment on lines +130 to +141
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ここを関数に切り出すのは賛成です。
ただ最初に step2-2.cpp が最終解なのかなと想ったので、関数化したほうがいいかもしれません、的なコメントをしそうになりました。stepX.cpp を作るのであれば、step3.cpp もあるとレビューしやすいかなと思いました (それか全て md に書くか)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

コメントありがとうございます。IDEの補助がないとつらいときに補助的にファイルを作っていたのですがレビューワーから見るとわかりにくいですね。紛らわしくならないように気を付けます

```

28 changes: 28 additions & 0 deletions 929.-Unique-Email-Addresses/step2-1.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#include <algorithm>
#include <set>
#include <string>
#include <vector>

class Solution {
public:
int numUniqueEmails(std::vector<std::string>& emails) {
std::set<std::string> seen;
for (const auto& email : emails) {
seen.insert(normalize_email(email));
}
return seen.size();
}

private:
std::string normalize_email(const std::string& email) {
auto at_position = email.find('@');
auto local = email.substr(0, at_position);
auto at_domain = email.substr(at_position);
if (auto plus_position = local.find('+'); plus_position != std::string::npos) {
local.resize(plus_position);
}
local.erase(std::remove(local.begin(), local.end(), '.'), local.end());

return local + at_domain;
}
};
23 changes: 23 additions & 0 deletions 929.-Unique-Email-Addresses/step2-2.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#include <ranges>
#include <set>
#include <vector>

class Solution {
public:
int numUniqueEmails(std::vector<std::string>& emails) {
std::set<std::string> seen;
for (const auto& email : emails) {
auto name_ranges = email | std::views::split('@');
auto it = name_ranges.begin();
auto local_range = *it;
auto domain_range = *(++it);
auto filtered_local_range = local_range | std::views::take_while([](const auto c) { return c != '+'; }) |
std::views::filter([](const auto c) { return c != '.'; });
std::string processed_email = std::ranges::to<std::string>(filtered_local_range);
processed_email += '@';
processed_email += std::ranges::to<std::string>(domain_range);
seen.insert(processed_email);
}
return seen.size();
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

後学のため、問題文+例+制約を読んでから、どのように道筋を立てたか教えて頂けないでしょうか?
図にする。プロセスの整理をする。なんでも良いです。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@Apo-Matchbox
コメントありがとうございます。この問題の場合ですと、

  1. 結局入力のemailたちを送り先が同じものを同一視した場合に送り先はいくつあるか?と言い換えられるので、各emailを正規化したあとstd::set (or std::unordered_set)に入れて最後に個数を返せばよさそうだな。
  2. 一つのemailを正規化するには、@の前後で分割して、local_nameの部分は.を取り除き+以降を無視してdomain_nameとくっつければよさそう。文字列のライブラリを使えばすぐできそうだが、それを手でやる趣旨の出題だと思う。
  3. 2を実現するにはemailを前から見て行って条件を満たすように文字を付け加えればよい。local_nameとdomain_nameではロジックがちょっと違うからそれぞれwhileループで加えていこう。(まとめて書くこともできるがやや複雑になる)
  4. whileのループ中で+まで到達したかどうかはフラグで管理して条件分岐すればよい。ループに関してはコメント集で「引継ぎ」で見ると良いかもしれませんhttps://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.xzxd7jwvkwc5
  5. 正規化の手順を振り返る。本質的に正規化をするにはemailの各文字を見るほかないのでO(n)以上に効率がよくなることはなさそう。
  6. 1について正規化されたemailを分類するには比較をしないといけないのでsetに入れる以上に効率的にはならなさそう。

といった思考プロセスだったかと思います。

まとめると、

  • 詳細はいったん置いといて部分問題に分割して全体の流れを考える。
  • 部分問題を解決する。
  • それぞれによりよい方法がないか吟味する。
    といったプロセスで考えることが多いです。

この問題の場合一次元的で図にするほどでもないですが、グラフやグリッド(二次元座標)、場合分けや状態遷移があるとき、問題文が複雑…などは図を書くのは大変有効です。
コーディング練習会の進め方でもstep1は分からなければ5分で見てもよいとありますし、知らないアルゴリズムは頑張ってもひらめかないので無理に自分に考え出さなくてもよいというスタンスで進めています。(実際mediumだと所見で解けないことのほうが多いです。)

制約は問題文の状況をspecifyする要素ですが、実際の面接試験などでは面接官とコミュニケーションをして把握するものらしいですし、実際の業務でもどのような入力がありうるか?どれくらいの処理時間で終わらせないといけないか?どいういったインターフェースにするか?というのは所与の要素というより適宜考えてデザインしていかないといけないものかと思います。
leetcodeで正解するためには制約の状況だけ考えれば十分ですが、コーディング練習会の趣旨としては、制約にない状況も含め幅を広げて考えるのがよいのかなと思います。(といいつつ私も想定外の入力に対してどう例外処理をするかとか、入力データに対してどれくらいの処理時間になるかの見積もりなどきちんとできていなのですが…)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@maeken4
丁寧な回答を頂きありがとうございます。
他の方のプロセスを確認できて大変参考になりました。

問題の主旨と解答、他の方法を考えたりとしてますが、解法が出てこないですね。
解法を読んでも分からないことも沢山です。

コーディング練習会の進め方でもstep1は分からなければ5分で見てもよいとありますし、知らないアルゴリズムは頑張ってもひらめかないので無理に自分に考え出さなくてもよいというスタンスで進めています。(実際mediumだと所見で解けないことのほうが多いです。)

少しずつ、知識と選択肢と出来る事を増やしていくシンプルな事でしか解決しないですね。
焦らず一つひとつの問題と向き合ってみます。