From 3b5030a9b838412adfb2b516a57c222372dc757e Mon Sep 17 00:00:00 2001 From: Yuto729 Date: Sun, 9 Aug 2026 15:56:23 +0900 Subject: [PATCH] solve --- partition-equal-subset-sum/main.md | 177 ++++++++++++++++++++++++++++- 1 file changed, 174 insertions(+), 3 deletions(-) diff --git a/partition-equal-subset-sum/main.md b/partition-equal-subset-sum/main.md index 4b97739..bd79dbd 100644 --- a/partition-equal-subset-sum/main.md +++ b/partition-equal-subset-sum/main.md @@ -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))] + 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個の各ビットとして表現する + - 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 +``` + +### ナップサック問題との関連 + +- 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] +```