Skip to content
Open
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
177 changes: 174 additions & 3 deletions partition-equal-subset-sum/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
date: 2026-08-01
tags:
- leetcode
- review
url: https://leetcode.com/problems/partition-equal-subset-sum
---

Expand Down Expand Up @@ -73,7 +72,7 @@ class Solution:
return helper(0, array_sum // 2)
```

最初に書いたのはこれだが、偶然これでも通ってるのは、最後の要素が必ずどちらかの集合に属するから
最初に書いたのは以下だが、偶然これでも通ってるのは、最後の要素が必ずどちらかの集合に属するから

```py
class Solution:
Expand All @@ -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)

Copy link
Copy Markdown

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)

if sum_of_nums % 2 == 1:
return False

target = sum_of_nums // 2
dp = [[False] * (target + 1) for _ in range(len(nums))]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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個の各ビットとして表現する

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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]
```
Loading