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
175 changes: 175 additions & 0 deletions problem32/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
## 取り組み方
- step1: 5分以内に空で書いてAcceptedされるまで解く + テストケースと関連する知識を連想してみる
- step2: 他の方の記録を読んで連想すべき知識や実装を把握した上で、前提を置いた状態で最適な手法を選択し実装する
- step3: 10分以内に1回もエラーを出さずに3回連続で解く

## step1
nums[i]からnums[j]までの和を全てみて一番大きいものを返せば良さそう。
累積和を用いれば、i~jまでの和がO(1)でアクセスできるので、全体としてはO(nums.length^2)で解ける。
が、10^5*10^5で10^10となるので、計算に数時間くらいかかってしまいそう。

ので、効率化できないかを考える。
求めたいのは、prefix_sum[j+1]-prefix_sum[i]をi,j(i<=j)を動かしていった際の最大の値。
jを固定して考えると、prefix_sum[i]が最小の時がprefix_sum[j+1]-prefix_sum[i]が最大となる。
したがって、jを0~len(nums)-1まで動かしていって、
その際の累積和とこれまでの累積和の最小値の差をmaximum subarrayとして更新していけば、O(nums.length)で解ける。

```python
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
maximum_subarray = -sys.maxsize - 1
Copy link

Choose a reason for hiding this comment

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

-sys.maxsize - 1はintの最小値ではなさそうです。

>>> -sys.maxsize -1
-9223372036854775808
>>> -sys.maxsize -2
-9223372036854775809

Copy link

Choose a reason for hiding this comment

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

IronPythonにはこんな説明がありました。

The largest positive integer supported by the platform’s Py_ssize_t type, and thus the maximum size lists, strings, dicts, and many other containers can have.
https://ironpython-test.readthedocs.io/en/latest/library/sys.html#sys.maxsize

Copy link
Owner Author

Choose a reason for hiding this comment

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

レビュー、実験、リンクの共有もいただきありがとうございます。

すると、maximum_subarrayの初期値は、
MIN_NUMS = -10001あたりを定義しておいて使うのが良さそうですかね。

Copy link

Choose a reason for hiding this comment

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

あー、そもそもの話として、maximum_subarray に負の小さい値を入れるというのが「番兵」であるという認識はありますか。つまり、

value_set = False
value = 0
for x in a:
    if not value_set:
        value = x
        value_set = True
    else:
        value = max(x, value)

を番兵使って、value の初期値を -inf などとしても状況によっては動くということです。

それで、入力への制約というのはだんだん守られなくなっていくものなので、番兵を使ってうまく書けないならば、使わないことも考えたほうがいいでしょう。
https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.jdtk9v35bca4

Copy link
Owner Author

Choose a reason for hiding this comment

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

レビューありがとうございます。

maximum_subarray に負の小さい値を入れるというのが「番兵」であるという認識はありますか。

はじめのループで更新される、maximum_subarrayとしては現状あり得ないだろう値くらいの認識で、番兵だとは気付けていなかったです。

それで、入力への制約というのはだんだん守られなくなっていくものなので、番兵を使ってうまく書けないならば、使わないことも考えたほうがいいでしょう。

はい。ご指摘のとおりで、制約が変わり得ることを念頭において、初めから nums[0]を初期値とする、フラグを用いて初めは特殊対応するなど検討すべきでした。

sum_prefix = 0
minimum_sum_prefix = 0
for num in nums:
sum_prefix += num
maximum_subarray = max(
maximum_subarray,
sum_prefix - minimum_sum_prefix
)
minimum_sum_prefix = min(
minimum_sum_prefix,
sum_prefix
)
return maximum_subarray
```

## step2
### 読んだコード
- https://github.com/TORUS0818/leetcode/pull/34/files
- https://github.com/hayashi-ay/leetcode/pull/36/files
- https://github.com/sakupan102/arai60-practice/pull/33/files

### 感想
- subarrayの要素の和の最大を出したいので、変数名が不適切だった。なぜ気づかなかった、
- wikipediaに取り上げられるくらい有名な問題で、別解も色々あった
- https://en.wikipedia.org/wiki/Maximum_subarray_problem
- kadaneのアルゴリズム
- 分割統治
- 総当たり(累積和なし)
- 総当たり(累積和あり) <- 初見時、提出してTLEになった
- 分割統治は、並列処理が適切な場面では選択肢になるのだろうか
- kadaneは変数が減らせるくらいが嬉しさだろうと思うので、step1の実装の方がわかりやすくいい気がする
Copy link

Choose a reason for hiding this comment

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

空間計算量がO(1)にできるのは入力が大きい場合はメリットだと思います。


##### kadaneのアルゴリズム
step1の実装の`maximum_subarray`の更新部分`sum_prefix - minimum_sum_prefix`を式変形していくと、
`-> sum_prefix - min(prev_minimum_sum_prefix, prev_sum_prefix)`
`-> num + prev_sum_prefix - min(prev_minimum_sum_prefix, prev_sum_prefix)`
`-min(A, B) = max(-A, -B)`であることから、
`-> num + max(prev_sum_prefix - prev_minimum_sum_prefix, prev_sum_prefix - prev_sum_prefix)`
`-> num + max(prev_sum_prefix - prev_minimum_sum_prefix, 0)`
`num`を共通因子としてmaxの中に入れて、
`-> max(num, num + prev_sum_prefix - prev_minimum_sum_prefix)`
ここで、`prev_sum_prefix - prev_minimum_sum_prefix`は、
`sum_prefix - minimum_sum_prefix`の1つ前の結果だったということになるので、
`sum_prefix - minimum_sum_prefix`を`current_max_sum`とでもおいて、
`current_max_sum = max(num, num + current_max_sum)`を更新していけば良いという話。
Comment on lines +54 to +65

Choose a reason for hiding this comment

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

一言で言うと、こういうことですね。
olsen-blue/Arai60#32 (comment)


```python
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
max_sum = -sys.maxsize - 1
current_max_sum = -sys.maxsize - 1
Copy link

Choose a reason for hiding this comment

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

_so_far と最後につけて、「ここまでの」を意味する変数名にする方もいらっしゃるようです。

Copy link
Owner Author

Choose a reason for hiding this comment

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

共有ありがとうございます。
_so_far、応用範囲が広くてかなり使えそうです。

for num in nums:
current_max_sum = max(num, num + current_max_sum)
max_sum = max(max_sum, current_max_sum)
return max_sum
```

##### 分割統治
配列を半分にして、1.左半分と2.右半分, 3.分割する場所を跨ぐ部分配列の中で和が最大になるものを探すという発想。
1.2.は再帰的に出せば良い。
3.は真ん中から左端に向かって最大値を記録していく + 真ん中+1から右端に向かって最大値を記録していき、2つの和を出す。

再帰の深さは`logN`で3.の処理が`N`なので、O(NlogN)で解ける。


```python
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
def get_max_sum(left: int, right: int) -> int:
if left == right:
return nums[left]
center = (left + right) // 2
left_max_sum = get_max_sum(left, center)
right_max_sum = get_max_sum(center + 1, right)
center_max_sum = get_center_max_sum(left, right)
return max(
left_max_sum,
right_max_sum,
center_max_sum
)

def get_center_max_sum(left: int, right: int) -> int:
if left == right:
return nums[left]
center = (left + right) // 2

left_max_sum = -sys.maxsize
left_direction_prefix_sum = 0
for i in range(center, left - 1, -1):
left_direction_prefix_sum += nums[i]
left_max_sum = max(
left_max_sum,
left_direction_prefix_sum
)
right_max_sum = -sys.maxsize
right_direction_prefix_sum = 0
for i in range(center + 1, right + 1):
right_direction_prefix_sum += nums[i]
right_max_sum = max(
right_max_sum,
right_direction_prefix_sum
)
return left_max_sum + right_max_sum

return get_max_sum(0, len(nums) - 1)
```

## step3
分割統治も応用が効きそうなので、3回解いて定着させておく。

```python
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
max_subarray_sum = -sys.maxsize - 1
prefix_sum = 0
min_prefix_sum = 0
for num in nums:
prefix_sum += num
max_subarray_sum = max(
max_subarray_sum,
prefix_sum - min_prefix_sum
)
min_prefix_sum = min(
min_prefix_sum,
prefix_sum
)
return max_subarray_sum
```

```python
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
def get_max_sum(left: int, right: int) -> int:
if left == right - 1:
return nums[left]
center = (left + right) // 2
left_max_sum = get_max_sum(left, center)
right_max_sum = get_max_sum(center, right)
center_max_sum = get_max_center_sum(left, right)
return max(left_max_sum, center_max_sum, right_max_sum)

def get_max_center_sum(left: int, right: int) -> int:
center = (left + right) // 2
left_max_sum = right_max_sum = -sys.maxsize - 1
left_prefix_sum = right_prefix_sum = 0
for i in range(center - 1, left - 1, -1):
left_prefix_sum += nums[i]
left_max_sum = max(left_max_sum, left_prefix_sum)
for i in range(center, right):
right_prefix_sum += nums[i]
right_max_sum = max(right_max_sum, right_prefix_sum)
return left_max_sum + right_max_sum

return get_max_sum(0, len(nums))
```
Copy link

Choose a reason for hiding this comment

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

ただの感想なんですが、この問題の分割統治あまりきれいじゃない感じがしてしまいます。
centerをまたぐところ部分の計算が煩雑に感じます。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ご意見ありがとうございます。
自分は、「centerを必ず使う累積和の最大を見つける」というのは自然な気がしましたが、
やはり左右の分割だけで済まないのはちょっと抵抗感ありました。

Copy link

Choose a reason for hiding this comment

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

左右の分割 + O(n) は、たとえば、最大の内点を含まない長方形などでたまに出てくる気がします。今回はどんぴしゃという感じはしないですね。
https://en.wikipedia.org/wiki/Largest_empty_rectangle

Copy link
Owner Author

Choose a reason for hiding this comment

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

面白い類題の共有ありがとうございます。

真ん中で平面を分割して左右で部分問題を解き、
中央を跨ぐ最大の空の矩形を見つけていくのですね。

確かにこのケースだと、左右の分割 + 中央を考慮した処理を使うのが自然な気がしました。