-
Notifications
You must be signed in to change notification settings - Fork 0
Partition Equal Subset Sum #102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,6 @@ | |
| date: 2026-08-01 | ||
| tags: | ||
| - leetcode | ||
| - review | ||
| url: https://leetcode.com/problems/partition-equal-subset-sum | ||
| --- | ||
|
|
||
|
|
@@ -73,7 +72,7 @@ class Solution: | |
| return helper(0, array_sum // 2) | ||
| ``` | ||
|
|
||
| 最初に書いたのはこれだが、偶然これでも通ってるのは、最後の要素が必ずどちらかの集合に属するから | ||
| 最初に書いたのは以下だが、偶然これでも通ってるのは、最後の要素が必ずどちらかの集合に属するから | ||
|
|
||
| ```py | ||
| class Solution: | ||
|
|
@@ -100,8 +99,180 @@ class Solution: | |
|
|
||
| ### 実装2 | ||
|
|
||
| ボトムアップバージョン | ||
| ボトムアップバージョンを書いてみる。 | ||
| 久しぶりに書いたのでちょっと詰まったところがある | ||
|
|
||
| - DPテーブルの形状は、再帰関数の引数がそのまま表の行と列に対応する。 | ||
| - 0 <= i < len(nums), 1 <= j <= target | ||
| - dp[i][j]: nums[:i + 1]から選んで和がjとなるsubsetを作れるかどうか | ||
| - 再帰の分岐は「nums[i]を使うとき」と「nums[i]を使わないとき」なので、そのままボトムアップ版でも上記の条件を使う | ||
|
|
||
| ```py | ||
| class Solution: | ||
| def canPartition(self, nums: List[int]) -> bool: | ||
| # dp[i][j]: nums[:i + 1]から選んで和がjとなるsubsetを作れる. 0/1ナップザック | ||
| sum_of_nums = sum(nums) | ||
| if sum_of_nums % 2 == 1: | ||
| return False | ||
|
|
||
| target = sum_of_nums // 2 | ||
| dp = [[False] * (target + 1) for _ in range(len(nums))] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dp という変数名は Dynamic Programming の略だと思うのですが、読み手にとってあまり有益な情報になっていないように思います。中にどのような値が含まれるかを端的に表した英単語や英語句を付けると良いと思います。 can_make_sum あたりはいかがでしょうか? |
||
| for i in range(len(nums)): | ||
| if nums[i] > target: | ||
| continue | ||
|
|
||
| for j in range(i, len(nums)): | ||
| dp[j][nums[i]] = True | ||
|
|
||
| for i in range(len(nums)): | ||
| for j in range(1, target + 1): | ||
| if j < nums[i]: | ||
| dp[i][j] = dp[i - 1][j] | ||
| continue | ||
|
|
||
| # nums[i]を使わないときとnums[i]を使うとき | ||
| dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]] | ||
|
|
||
| return dp[len(nums) - 1][target] | ||
| ``` | ||
|
|
||
| ### 実装3 | ||
|
|
||
| 上記のDPテーブルは1次元に落とせる | ||
|
|
||
| ```py | ||
| class Solution: | ||
| def canPartition(self, nums: List[int]) -> bool: | ||
| sum_of_nums = sum(nums) | ||
| if sum_of_nums % 2 == 1: | ||
| return False | ||
|
|
||
| target = sum_of_nums // 2 | ||
| dp = [False] * (target + 1) | ||
| dp[nums[0]] = True | ||
| for i in range(1, len(nums)): | ||
| for j in range(1, target + 1): | ||
| # for j in range(target, 0, -1) が正しい | ||
| if j < nums[i]: | ||
| continue | ||
|
|
||
| if j == nums[i]: | ||
| dp[j] = True | ||
| continue | ||
|
|
||
| dp[j] = dp[j] or dp[j - nums[i]] | ||
|
|
||
| return dp[target] | ||
| ``` | ||
|
|
||
| -> 間違い。上記は重複ありナップザックのときの解法になってしまう。内側のjのループで複数回`nums[i]`を用いた結果になることがある。 | ||
|
|
||
| - jのループを逆向きにすれば解決する | ||
| - なぜ? -> jを順方向に回すと、`j - nums[i]`はすでにいくつか前のループで更新されている、つまりnums[i]を一度使った結果を元にもう一度numを使えてしまうが、降順にすると、`j - nums[i]`はまだ更新されていない領域なので前の行の値が保たれる = 更新は外側のループ1回につき1回だけとなるので、0/1ナップサックの要件を満たす。 | ||
|
|
||
| ```py | ||
| class Solution: | ||
| def canPartition(self, nums: List[int]) -> bool: | ||
| total = sum(nums) | ||
| if total % 2: | ||
| return False | ||
|
|
||
| target = total // 2 | ||
|
|
||
| # dp[j]: ここまで見た要素から選んで和 j を作れるか | ||
| dp = [False] * (target + 1) | ||
| dp[0] = True # 空集合 | ||
|
|
||
| for num in nums: | ||
| if num > target: # 単独で超える要素は絶対に使えない | ||
| continue | ||
|
|
||
| for j in range(target, num - 1, -1): # 降順。num 未満は変化しないので下限は num | ||
| dp[j] = dp[j] or dp[j - num] | ||
| if dp[target]: # 早期リターン | ||
| return True | ||
|
|
||
| return dp[target] | ||
| ``` | ||
|
|
||
| ### 実装3 | ||
|
|
||
| bitset DP | ||
|
|
||
| - 上記一次元DPは[True, False, True, ...]という真偽値の列だが、これを整数1個の各ビットとして表現する | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. この解法は思いつきませんでした。面白い解法だと思います。 Python は int が表現できる範囲に上限が実質ないため、このような書き方ができるのだと思います。また、 CPython では、int は、ある程度の範囲を超えると、多倍長整数として扱われ、桁数に応じた時間計算量と空間計算量がかかる点に注意が必要だと思います。 |
||
| - T -> 1, F -> 0 | ||
| - 第jビットがdp[j]に対応 | ||
| - `dp[j] = dp[j] or dp[j - num]`は、dp[j - num]のビットが立っているならdp[j]を立てる = 全てのビットをnumだけ左にずらして元と重ね合わせる(重ね合わせ = 和 = orに対応) | ||
| - `bits |= bits << num` | ||
|
|
||
| - 向きの問題が消える | ||
| - 上記の更新式で全てのjに対して一括で更新がかかるので、jの走査向きによって、「更新ずみの値を読んでしまう」という問題がなくなる | ||
|
|
||
| 計算量は同じ | ||
|
|
||
| ```py | ||
| class Solution: | ||
| def canPartition(self, nums: List[int]) -> bool: | ||
| total = sum(nums) | ||
| if total % 2: | ||
| return False | ||
| target = total // 2 | ||
|
|
||
| bits = 1 # 和 0 のみ作れる | ||
| for num in nums: | ||
| bits |= bits << num | ||
| return bits >> target & 1 == 1 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 演算子の優先順位が分かりにくく感じました。適宜 () で優先順位を明示したほうが、読み手にとって読みやすくなると思います。 return (bits >> target) & 1 == 1 |
||
| ``` | ||
|
|
||
| ### ナップサック問題との関連 | ||
|
|
||
| - 0/1 ナップサック | ||
| - 容量Wのリュックがある。品物iは重さw[i]、価値v[i]。各品物は使うか使わないかの二択。容量を超えない範囲で価値の合計を最大化する | ||
| - 品物を入れるか入れないかで場合分け | ||
| - 1次元DPで解く場合は内側ループを降順に回す | ||
|
|
||
| - 本問題は、 | ||
| - 品物i -> nums[i] | ||
| - 重さ -> nums[i] | ||
| - 価値 -> nums[i] | ||
| - 容量 -> sum(nums) // 2 | ||
| - 最大化した価値 -> 作れる和の最大値 | ||
| - 答え -> 最大値が容量ちょうどか? | ||
| と設定できる | ||
|
|
||
| DP配列に格納される値 | ||
|
|
||
| - ナップサック -> 価値の最大値 | ||
| - 本問題 -> 和をちょうど作れるかどうかのbool | ||
|
|
||
| - 無限ナップサック | ||
| - 同じ品物を何度も選ぶことができる | ||
| - [[leetcode/coin-change/main|Coin Change]] | ||
|
|
||
| ### 関連 | ||
|
|
||
| [[部分配列系問題のパターン]] | ||
|
|
||
| ## Step3 | ||
|
|
||
| ```py | ||
| class Solution: | ||
| def canPartition(self, nums: List[int]) -> bool: | ||
| total = sum(nums) | ||
| if total % 2 == 1: | ||
| return False | ||
|
|
||
| target = total // 2 | ||
| dp = [False] * (target + 1) | ||
| dp[0] = True | ||
| for i in range(len(nums)): | ||
| # nums[:i + 1]から要素を選ぶとき | ||
| for j in range(target, 0, -1): | ||
| # 合計がjを満たす組み合わせがあるかどうか | ||
| if j < nums[i]: | ||
| continue | ||
|
|
||
| dp[j] = dp[j] or dp[j - nums[i]] | ||
|
|
||
| return dp[target] | ||
| ``` | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
こちらのコメントをご参照ください。
h-masder/Arai60#18 (comment)