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
31 changes: 31 additions & 0 deletions grind75_hard/07_1235_Maximum Profit in Job Scheduling/level_1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# startTimeでソートして、繋げられるものを順番に確認していく
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

自分がこの問題を読んだときに思い付いた解法は、

  1. 時間を座標圧縮する。
  2. 圧縮後の時間 i の時点での最大の利益を求める動的計画法をする。

でした。 level 1~3 の解法は思い浮かびませんでした。

level 1 は、第一感やや複雑に感じました。 level 2~3 は、スマートだと感じました。

# 終了時刻の早いものから見ていき、最大の利益になるものを選択していく
# (最大の利益にならないものは、それ以降でも最大にならないため捨てて良い)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

あるジョブを開始する際、その時刻以前に終了したジョブの中で、最も利益の総和が大きかったものを、次以降のジョブを開始する際の選択肢として残しておく、という点がポイントのように思いました。その部分についてコメントしてあるとよいと思いました。

# 例:(start, end, profit) = ((1, 3 50), (2, 3, 10), (3, 5, 40), (4, 6, 70))
# 3項目の選択肢は
# (1, 3, 50) -> (3, 5, 40)の順番で選択すると最大の利益になる
# (2, 3, 10) -> (3, 5, 40)は最大の利益にならない(4項目以降でも最大の利益にならないので捨てる)
# 4項目の選択肢は
# (1, 3, 50) -> (4, 6, 70)の順番で選択すると最大の利益になる
# (2, 3, 10)は3項目の検討時に捨てた
class Solution:
def jobScheduling(
self, startTime: List[int], endTime: List[int], profit: List[int]
) -> int:
sorted_starts = sorted([(startTime[i], i) for i in range(len(startTime))])
max_profit = 0
end_profit_heap = [(0, 0)]
for start, i in sorted_starts:
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 のスコープがやや広いため、 job_index 等、叙述的に書いたほうが良いと思います。

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.

for文の中だけでも、nestが深くなる場合にはちゃんと名付けたほうが良さそうということですね。

max_profit_use_i = 0
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 を使う最大の利益」となっており、変数の中に何が格納されているのか分かりにくく感じました。「i 番目のジョブを使った際の最大の利益」を表したいのだと思いますので max_profit_using_current_job あたりがよいと思いました。ただ、 current に情報がないため、もう少し別の単語を使ったほうが良いかもしれません。

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.

iでも情報量変わらないかと思いましたが、ちゃんと単語で書かないと伝わらないですね。

pre_info = ()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pre_info という変数名から、変数の中に何が入っているのか想起できませんでした。「あるジョブを開始する前に終了していたジョブの中で、利益の最も大きかったもの」が入っているのだと思います。 best_end_time_and_profit あたりでいかがでしょうか?

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.

確かに中身が分からず、何を表しているか読み取るのが大変ですね。
変数名が長くなりそうで諦めてしまっていました。

while end_profit_heap and end_profit_heap[0][0] <= start:
pre_end, pre_profit = heapq.heappop(end_profit_heap)
if pre_profit + profit[i] <= max_profit_use_i:
continue
max_profit_use_i = pre_profit + profit[i]
pre_info = (pre_end, pre_profit)
heapq.heappush(end_profit_heap, (endTime[i], max_profit_use_i))
max_profit = max(max_profit, max_profit_use_i)
if pre_info:
heapq.heappush(end_profit_heap, pre_info)
return max_profit
21 changes: 21 additions & 0 deletions grind75_hard/07_1235_Maximum Profit in Job Scheduling/level_2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# DP
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 ではなく、メモ付き再帰だと思います。なお「メモ付き再帰」は競技プログラミング用語だと思います。

念のため、ループを使った DP でも書いてみていただけますか?

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.

DPとメモ付き再帰は同じものだと思っていました。
再帰の結果を保存するのがメモ付き再帰で、
ループで結果を保存するのがDP?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"メモ付き再帰"で調べたら、競技プログラミングの第3,4世代の用例が多かったので、これはあまり下の世代は使わない用語?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

自分は「メモ付き再帰」「メモ化再帰」は phoenixstarhiro か nya3jp のどちらかから聞いた気がします。自分自身は「キャッシュ付き再帰」と呼んでいた気がします。

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とメモ付き再帰は同じものだと思っていました。 再帰の結果を保存するのがメモ付き再帰で、 ループで結果を保存するのがDP?
はい、自分はそのように認識しています。

# i番目以降のjobを使った時の最大利益をメモしていく
class Solution:
def jobScheduling(
self, startTime: List[int], endTime: List[int], profit: List[int]
) -> int:
jobs = list(zip(startTime, endTime, profit))
jobs.sort()
max_profit_begin_i = [None] * len(jobs)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

max_profit_begin_i と言う変数名から、変数に格納されている値がどのようなものか想起できませんでした。 max_profits で十分だと思います。

max_profit_begin_i.append(0)

def find_max_profit(i):
if max_profit_begin_i[i] is not None:
return max_profit_begin_i[i]
profit_skip_i = find_max_profit(i + 1)
next_index = bisect.bisect_left(jobs, (jobs[i][1], 0, 0), lo=i + 1)
profit_use_i = find_max_profit(next_index) + jobs[i][2]
max_profit_begin_i[i] = max(profit_skip_i, profit_use_i)
return max_profit_begin_i[i]

return find_max_profit(0)
17 changes: 17 additions & 0 deletions grind75_hard/07_1235_Maximum Profit in Job Scheduling/level_3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class Solution:
def jobScheduling(
self, startTime: List[int], endTime: List[int], profit: List[int]
) -> int:
jobs = list(zip(startTime, endTime, profit))
jobs.sort()

@cache
def find_max_profit(i):
if i >= len(jobs):
return 0
profit_skip_i = find_max_profit(i + 1)
next_index = bisect_left(jobs, (jobs[i][1], 0, 0), lo=i + 1)
profit_use_i = find_max_profit(next_index) + jobs[i][2]
return max(profit_skip_i, profit_use_i)

return find_max_profit(0)
30 changes: 30 additions & 0 deletions grind75_hard/07_1235_Maximum Profit in Job Scheduling/level_4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# DP
class Solution:
def jobScheduling(
self, startTime: List[int], endTime: List[int], profit: List[int]
) -> int:
jobs = list(sorted(zip(startTime, endTime, profit)))
max_profit = [0] * (len(jobs) + 1)
for i in range(len(jobs) - 1, -1, -1):
profit_skip_running_job = max_profit[i + 1]
next_index = bisect_left(jobs, (jobs[i][1], 0, 0), lo=i + 1)
profit_use_running_job = max_profit[next_index] + jobs[i][2]
max_profit[i] = max(profit_skip_running_job, profit_use_running_job)
return max_profit[0]


# level_1の書き直し
class Solution:
def jobScheduling(
self, startTime: List[int], endTime: List[int], profit: List[int]
) -> int:
jobs = list(sorted(zip(startTime, endTime, profit)))
jobs.append((float("inf"), float("inf"), 0))
max_profit = 0
running_jobs = []
for job_start, job_end, job_profit in jobs:
while running_jobs and running_jobs[0][0] <= job_start:
_, max_profit_from_running_job = heapq.heappop(running_jobs)
max_profit = max(max_profit, max_profit_from_running_job)
heapq.heappush(running_jobs, (job_end, max_profit + job_profit))
return max_profit