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
41 changes: 41 additions & 0 deletions 322_coin_change/bfs.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#include <queue>
#include <vector>
class Solution {
public:
int coinChange(std::add_cv_t<int>& coins, int amount) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

std::add_cv_t<int>&ではなくstd::vector<int>& でしょうか.(寡聞にして前者を存じ上げませんでしたが,調べた限りconst volatile int&に対応することになるので,全く別物かなと思いました)

if (amount == 0) {
return 0;
}

std::vector<bool> visited(amount + 1, false);
visited[0] = true;

std::queue<int> sum;
sum.push(0);

int min_num_coins = 0;
Comment on lines +10 to +16
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

好みだとは思いますが,私ならここは「初期化」の一塊として捉え,空行を入れることはしないと思いました.


while (!sum.empty()) {
int size = sum.size();
min_num_coins++;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Google Style Guideでは後置する必要性がなければ前置するとのことです.
(あくまでスタイルの一例なので,チームの慣習やスタイルに合わせることをお勧めします)

for (int i = 0; i < size; i++) {
int current_amount = sum.front();
sum.pop();

for (auto coin : coins) {
int next_sum = current_amount + coin;

if (next_sum == amount) {
return min_num_coins;
}

if (next_sum < amount && !visited[next_sum]) {
visited[next_sum] = true;
sum.push(next_sum);
}
}
}
}
return -1;
}
};
43 changes: 43 additions & 0 deletions 322_coin_change/bfs_revised.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#include <queue>
#include <vector>
class Solution {
public:
int coinChange(std::vector<int>& coins, int target_amount) {
if (target_amount == 0) {
return 0;
}

int NOT_FOUND = -1;

std::vector<bool> visited_amount(target_amount + 1, false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

こちらのコメントをご参照ください。
Hurukawa2121/leetcode#17 (comment)

visited_amount[0] = true;

std::queue<int> amounts;
amounts.push(0);

int min_num_coins = 0;

while (amounts.empty()) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: !が抜けているようです.

int size = amounts.size();
min_num_coins++;

for (int level = 0; level < size; level++) {
int amount = amounts.front();
amounts.pop();

for (auto coin : coins) {
int next_amount = amount + coin;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

coin (= coins[i])の範囲は$[1, 2^{31} - 1]$なので,オーバーフローの危険がありそうです.

if (next_amount == target_amount) {
return min_num_coins;
}
if (next_amount < target_amount && !visited_amount[next_amount]) {
visited_amount[next_amount] = true;
amounts.push(next_amount);
}
}
}
}

return NOT_FOUND;
}
};
44 changes: 44 additions & 0 deletions 322_coin_change/dfs.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#include <stack>
#include <vector>
class Solution {
public:
int coinChange(std::vector<int>& coins, int amount) {
if (amount < 0) {
return -1;
}
if (amount == 0) {
return 0;
}

std::vector<int> min_coins(amount + 1, std::numeric_limits<int>::max());

std::vector<int> sorted_coins;
for (auto coin : coins) {
if (coin > amount) {
continue;
}
sorted_coins.push_back(coin);
}
sort(sorted_coins.begin(), sorted_coins.end());

std::stack<std::pair<int, int>> coin_and_sum;
coin_and_sum.push({0, 0});

while (!coin_and_sum.empty()) {
auto [num_coins, sum] = coin_and_sum.top();
coin_and_sum.pop();

if (sum > amount || num_coins >= min_coins[sum]) {
continue;
}

min_coins[sum] = num_coins;

for (auto coin : sorted_coins) {
coin_and_sum.push({num_coins + 1, coin + sum});
}
}

return min_coins.back() == std::numeric_limits<int>::max()? -1 : min_coins.back();
}
};
40 changes: 40 additions & 0 deletions 322_coin_change/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 322. Coin Change

## Step 1

合計額を大きいコインから順番に使って埋めていくアプローチを考えた。

- DP table を作って、それぞれのコインを使った回数を記録する。
- 大きい金額のコインから優先的に使っていきたいところだが、コインの金額がソートされていない。`std::set` を使って降順にソートされたリストにするか。
- うまくいかなさそう。例えば `coins = [1, 3, 4], amount = 6` だと厳しい。ここで答えを見る。

Bottom-up dp で解ける。`amount` を埋める最小のコイン数を求める問題は、より小さい合計値を埋めるコイン数を求めるという部分問題の集合として考えられる。`step1.cpp` に書いた。

- コインの枚数を $N$、合計額を $M$ として、時間計算量 $O(N \times M)$、空間計算量 $O(M)$。

## Step 2

Step 1 を修正したものを `step2.cpp` に書いた。

### 勉強

[参照](https://github.com/potrue/leetcode/pull/40/changes#r2264799064):`NOT_FOUND` に何を入れるかは意見が分かれている。

- めちゃくちゃ大きい値を入れておく派の人がいる。
- [オーバーフロー回避](https://github.com/5103246/LeetCode_Arai60/pull/38/changes#r2658821437)?

[参照](https://github.com/potrue/leetcode/pull/40/changes#r2264798356):`i` は確かにわかりづらい。

[参照](https://github.com/seal-azarashi/leetcode/pull/37/changes#r1830469401):BFS / DFS による実装。

- グラフ探索のイメージになる。ノードを現在の `sum`、エッジをコインの金額(ないしはその加算)とみなすと、`amount` というノードまでの最短距離を求める問題として見ることができる。
- DFSは `dfs.cpp` に書いた。
- 最初 `std::set` を使って書こうとしたが `amount` が大きすぎるケースの枝刈りに失敗したので、`sorted_coins` を作るタイミングで値の判定を入れることにした。
- 最短距離の問題なので無駄に掘ってしまうのはよくない。
- BFSは `bfs.cpp` に書いた。
-
- Queue を使う / 使わない、使う場合には何を入れておくか、アプローチは色々あるっぽい。あとは引き算でやるとか。

## Step 3

Bottom-up dp を `step3.cpp` に書いた。メモの名付けがわかりづらい気がしたので `fewest_coins` とした。あと BFS を練習して、わかりづらいところを変えたものを `bfs_revised.cpp` に書いた。
19 changes: 19 additions & 0 deletions 322_coin_change/step1.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#include <vector>
class Solution {
public:
int coinChange(std::vector<int>& coins, int amount) {
int NOT_FOUND = amount + 1;
std::vector<int> memo(amount + 1, NOT_FOUND);
memo[0] = 0;

for (int i = 1; i <= amount; i++) {
for (auto coin : coins) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Google Style Guideによると

型推論は、型推論によって、そのプロジェクトに馴染みのないコード読者にとっても、コードが読みやすくなることを期待できる場合や、あるいは、型推論によってコードをより安全にできる場合に限って使用します

とのことなので,ここはintで良いと感じました.
(あくまでスタイルの一例なのでご参考までに)

if (i >= coin) {
memo[i] = std::min(memo[i], memo[i - coin] + 1);
}
}
}

return memo[amount] == NOT_FOUND? -1 : memo[amount];
}
};
19 changes: 19 additions & 0 deletions 322_coin_change/step2.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#include <vector>
class Solution {
public:
int coinChange(std::vector<int>& coins, int target_amount) {
int NOT_FOUND = std::numeric_limits<int>::max() / 2;
std::vector<int> memo(target_amount + 1, NOT_FOUND);
memo[0] = 0;

for (int amount = 1; amount <= target_amount; amount++) {
for (auto coin : coins) {
if (amount >= coin) {
memo[amount] = std::min(memo[amount], memo[amount - coin] + 1);
}
}
}

return memo[target_amount] == NOT_FOUND? -1 : memo[target_amount];
}
};
20 changes: 20 additions & 0 deletions 322_coin_change/step3.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#include <vector>
class Solution {
public:
int coinChange(std::vector<int>& coins, int target_amount) {
int NOT_FOUND = std::numeric_limits<int>::max() / 2;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

std::numeric_limits<int>::max() / 2は意味が分かりづらかったです。
std::numeric_limits<int>::max() - 1の方がわかりやすい気がします。
また、オーバーフロー回避の目的である旨のコメントがあるとより良いと思いました。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

自分は std::numeric_limits::max() / 2 でもそれほど意味が分かりづらいと感じませんでした。また、 std::numeric_limits::max() / 2 にしておくと、 std::numeric_limits::max() / 2 を 2 回足してもオーバーフローしないという性質があり、バグが回避できる場合があると思います。趣味の範囲かもしれません。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

この文脈で「見つからない」という変数名は少し浮いているように思ったので、特別良い案は思い浮かびませんが、UNREACHABLE や UNACHIEVABLE などの方が、達成不可能なコイン枚数だという意味が伝えやすいかなと思いました。


std::vector<int> fewest_coins(target_amount + 1, NOT_FOUND);
fewest_coins[0] = 0;

for (int amount = 1; amount <= target_amount; amount++) {
for (auto coin : coins) {
if (amount >= coin) {
fewest_coins[amount] = std::min(fewest_coins[amount], fewest_coins[amount - coin] + 1);
}
}
}

return fewest_coins[target_amount] == NOT_FOUND? -1 : fewest_coins[target_amount];
}
};