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
49 changes: 49 additions & 0 deletions 1000.Minimum-Cost-to-Merge-Stones/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 1000. Minimum Cost to Merge Stones

## step1
30m以上考えて分からなかったので Hint として以下を見て解いた。合計で1h以上かかった。

k=2までは自力で考えられたが、k > 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.

自分も自力でたどり着けませんでした。 k=2 の場合は、連鎖行列積問題に似ているように感じました。
https://en.wikipedia.org/wiki/Matrix_chain_multiplication

最後のマージの直前の状態を作るための最小をコストを先に求めたあと、マージのコストを追加する、という点がポイントのようですが、自力で発想できる気がしませんでした。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

連鎖行列積問題は初めて知りました。二つの行列をまとめる処理や動的計画法で解ける点は似ていそうです。

よく復習しようと思います。


https://discord.com/channels/1084280443945353267/1200089668901937312/1233370731371298897

思考過程:

k=2のときの全探索(カタラン数)の枝刈りを考える。
部分最適性が成り立つので、考える区間を徐々に短くしていけば良い。
漸化式は

dp[i][j] = min_{i \leq mid < j} (dp[i][mid] + dp[mid+1][j]) + sum(i, j)

k > 2 のときには次元をもう一つ増やして、区間をいくつのpileとみなすかを考える。漸化式は

2 <= num_pile <= k:

dp[i][j][num_pile] = min_{i \leq mid < j} (dp[i][mid][1] + dp[mid+1][j][num_pile - 1])

num_pile == 1:

dp[i][j][1] = dp[i][j][k] + sum(i, j)


## step2

見直してもう少し改善できることに気が付く。合体できる境界は k - 1 刻みにしか現れないので k > 2 のときにも2次元のままで考えられる。

計算量は O(n^3 / k) になった。

再帰でも書く。

相互再帰でも書く。

かなり時間がかかった。

## step3
要復習。

TODO: ボトムアップ、再帰、相互再帰





32 changes: 32 additions & 0 deletions 1000.Minimum-Cost-to-Merge-Stones/step1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import itertools
import math

class Solution:
def mergeStones(self, stones: list[int], k: int) -> int:
num_stones = len(stones)
if (num_stones - 1) % (k - 1) != 0:
return -1

prefix_sum = list(itertools.accumulate(stones, initial=0))

dp = [[[float("inf")] * (k + 1) for _ in range(num_stones)] for _ in range(num_stones)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

min_cost あたりが良いと思います。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

min_cost としました。dp はなるべく使わないように気をつけます。


for i in range(num_stones):
dp[i][i][1] = 0

for length in range(2, num_stones + 1):
for i in range(num_stones - length + 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.

i と j の出現回数が多く、登場するたびに i と j が何を表すかを考える認知負荷が高そうだと感じました。 left と right あたりにしたほうが認知負荷が低くなりそうだと思いました。

j = i + length - 1
for num_pile in range(2, min(k, length) + 1):
for mid in range(i, j):
dp[i][j][num_pile] = min(dp[i][j][num_pile], dp[i][mid][1] + dp[mid + 1][j][num_pile - 1])

if not math.isinf(dp[i][j][k]):
dp[i][j][1] = dp[i][j][k] + prefix_sum[j+1] - prefix_sum[i]

return dp[0][-1][1]





30 changes: 30 additions & 0 deletions 1000.Minimum-Cost-to-Merge-Stones/step1_revised.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import itertools
import math

class Solution:
def mergeStones(self, stones: list[int], k: int) -> int:
num_stones = len(stones)
if (num_stones - 1) % (k - 1) != 0:
return -1

prefix_sum = list(itertools.accumulate(stones, initial=0))

min_cost = [[[float("inf")] * (k + 1) for _ in range(num_stones)] for _ in range(num_stones)]

for left in range(num_stones):
min_cost[left][left][1] = 0

for length in range(2, num_stones + 1):
for left in range(num_stones - length + 1):
right = left + length - 1
for num_pile in range(2, min(k, length) + 1):
for mid in range(left, right):
min_cost[left][right][num_pile] = min(
min_cost[left][right][num_pile],
min_cost[left][mid][1] + min_cost[mid + 1][right][num_pile - 1]
)

if not math.isinf(min_cost[left][right][k]):
min_cost[left][right][1] = min_cost[left][right][k] + prefix_sum[right + 1] - prefix_sum[left]

return min_cost[0][-1][1]
25 changes: 25 additions & 0 deletions 1000.Minimum-Cost-to-Merge-Stones/step2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import itertools

class Solution:
def mergeStones(self, stones: list[int], k: int) -> int:
num_stones = len(stones)
if (num_stones - 1) % (k - 1) != 0:
return -1

prefix_sum = list(itertools.accumulate(stones, initial=0))

dp = [[0] * num_stones for _ in range(num_stones)]

for length in range(2, num_stones + 1):
for i in range(num_stones - length + 1):
j = i + length - 1
dp[i][j] = float("inf")

for mid in range(i, j, k - 1):
dp[i][j] = min(dp[i][j], dp[i][mid] + dp[mid + 1][j])

if (length - 1) % (k - 1) == 0:
dp[i][j] += prefix_sum[j + 1] - prefix_sum[i]

return dp[0][-1]

45 changes: 45 additions & 0 deletions 1000.Minimum-Cost-to-Merge-Stones/step2_mutual_recursion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from functools import cache
import itertools

class Solution:
def mergeStones(self, stones: list[int], k: int) -> int:
num_stones = len(stones)
if (num_stones - 1) % (k - 1) != 0:
return -1

@cache
def min_reduce_cost(i, j):
if i == j:
return 0

minimum = float("inf")
for mid in range(i, j, k - 1):
right_len = j - (mid + 1) + 1
if (right_len - 1) % (k - 1) == 0:
right_cost = min_merge_cost(mid + 1, j)
else:
right_cost = min_reduce_cost(mid + 1, j)

minimum = min(minimum, min_merge_cost(i, mid) + right_cost)

return minimum

prefix_sum = list(itertools.accumulate(stones, initial=0))

@cache
def min_merge_cost(i, j):
length = j - i + 1

if length == 1:
return 0

if (length - 1) % (k - 1) != 0:
return float("inf")

base_cost = min_reduce_cost(i, j)
if base_cost == float("inf"):
return float("inf")

return base_cost + prefix_sum[j + 1] - prefix_sum[i]

return min_merge_cost(0, num_stones - 1)
27 changes: 27 additions & 0 deletions 1000.Minimum-Cost-to-Merge-Stones/step2_recursion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from functools import cache
import itertools

class Solution:
def mergeStones(self, stones: list[int], k: int) -> int:
num_stones = len(stones)
if (num_stones - 1) % (k - 1) != 0:
return -1

prefix_sum = list(itertools.accumulate(stones, initial=0))

@cache
def min_cost(i, j):
if i == j:
return 0

minimum = float("inf")
for mid in range(i, j, k - 1):
minimum = min(minimum, min_cost(i, mid) + min_cost(mid + 1, j))

length = j - i + 1
if (length - 1) % (k - 1) == 0:
minimum += prefix_sum[j + 1] - prefix_sum[i]

return minimum

return min_cost(0, num_stones - 1)