Skip to content

Commit 2a73882

Browse files
committed
atcoder B39 - Taro's Job 貪欲法 + 優先度付きキュー(ヒープ)
1 parent cb17753 commit 2a73882

6 files changed

Lines changed: 951 additions & 0 deletions

File tree

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// 以下は、**Go 1.20.6** を使用した「最大収益を得るスケジューリング問題」の解法です。
2+
// **優先度付きキュー(最大ヒープ)+日ごとの貪欲処理**を用い、\*\*満点が狙える効率的な実装(O(N log N + D log N))\*\*となっています。
3+
4+
// ---
5+
6+
// ## ✅ 処理概要
7+
8+
// * 各日付に対して「開始可能な仕事の報酬」を分類
9+
// * 毎日、報酬が最大の仕事を `heap`(最大ヒープ)から選んで実行
10+
// * `container/heap` パッケージを使って独自の最大ヒープを実装
11+
12+
// ---
13+
14+
// ## ✅ Goコード(関数化・型明示・メモリ計算考慮)
15+
16+
// ```go
17+
package main
18+
19+
import (
20+
"bufio"
21+
"container/heap"
22+
"fmt"
23+
"os"
24+
"strconv"
25+
"strings"
26+
)
27+
28+
// Job は仕事の情報(開始日と報酬)を表します。
29+
type Job struct {
30+
startDay int // X_i:開始日
31+
reward int // Y_i:報酬
32+
}
33+
34+
// MaxHeap は報酬を格納する最大ヒープです。
35+
type MaxHeap []int
36+
37+
// Len はヒープの要素数を返します。
38+
func (h MaxHeap) Len() int { return len(h) }
39+
40+
// Less は最大ヒープとして動作させるための比較関数です。
41+
func (h MaxHeap) Less(i, j int) bool { return h[i] > h[j] }
42+
43+
// Swap は要素の入れ替えを行います。
44+
func (h MaxHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
45+
46+
// Push はヒープに要素を追加します。
47+
func (h *MaxHeap) Push(x any) {
48+
*h = append(*h, x.(int))
49+
}
50+
51+
// Pop はヒープから最大値を取り出します。
52+
func (h *MaxHeap) Pop() any {
53+
old := *h
54+
n := len(old)
55+
val := old[n-1]
56+
*h = old[:n-1]
57+
return val
58+
}
59+
60+
/**
61+
* 最大報酬を計算する関数
62+
* @param n int - 仕事数
63+
* @param d int - 就業可能日数
64+
* @param jobs []Job - 各仕事(開始日と報酬)
65+
* @return int - 最大得られる報酬
66+
*/
67+
func getMaxEarnings(n int, d int, jobs []Job) int {
68+
// 各日ごとにできる仕事の報酬リスト
69+
jobByDay := make([][]int, d+1) // 1-based indexing
70+
71+
for _, job := range jobs {
72+
if job.startDay <= d {
73+
jobByDay[job.startDay] = append(jobByDay[job.startDay], job.reward)
74+
}
75+
}
76+
77+
h := &MaxHeap{}
78+
heap.Init(h)
79+
total := 0
80+
81+
for day := 1; day <= d; day++ {
82+
// 今日から可能な仕事をヒープに追加
83+
for _, reward := range jobByDay[day] {
84+
heap.Push(h, reward)
85+
}
86+
// 最大報酬の仕事を1つ実行
87+
if h.Len() > 0 {
88+
total += heap.Pop(h).(int)
89+
}
90+
}
91+
92+
return total
93+
}
94+
95+
// main は標準入力を受け取り、最大報酬を出力します。
96+
func main() {
97+
scanner := bufio.NewScanner(os.Stdin)
98+
scanner.Scan()
99+
parts := strings.Fields(scanner.Text())
100+
n, _ := strconv.Atoi(parts[0])
101+
d, _ := strconv.Atoi(parts[1])
102+
103+
jobs := make([]Job, n)
104+
for i := 0; i < n; i++ {
105+
scanner.Scan()
106+
parts := strings.Fields(scanner.Text())
107+
x, _ := strconv.Atoi(parts[0])
108+
y, _ := strconv.Atoi(parts[1])
109+
jobs[i] = Job{startDay: x, reward: y}
110+
}
111+
112+
result := getMaxEarnings(n, d, jobs)
113+
fmt.Println(result)
114+
}
115+
// ```
116+
117+
// ---
118+
119+
// ## ✅ 入力例
120+
121+
// ```
122+
// 5 4
123+
// 1 1
124+
// 2 4
125+
// 2 3
126+
// 3 4
127+
// 4 2
128+
// ```
129+
130+
// ### 出力
131+
132+
// ```
133+
// 12
134+
// ```
135+
136+
// ---
137+
138+
// ## ✅ 時間・空間計算量
139+
140+
// | 項目 | 計算量 | 備考 |
141+
// | ------ | -------------------- | ------------------ |
142+
// | job分類 | O(N) | 各仕事を1回処理 |
143+
// | heap操作 | O(N log N + D log N) | push/pop は log N 回 |
144+
// | 空間使用量 | O(N + D) | jobByDay + heapサイズ |
145+
146+
// Goは静的型言語かつ高速なGCがあり、このようなヒープ・貪欲アルゴリズムに適しています。
147+
148+
// ---
149+
150+
// ## ✅ 実行方法
151+
152+
// 1. コードを `main.go` に保存
153+
// 2. 以下のように実行できます:
154+
155+
// ```bash
156+
// go run main.go < input.txt
157+
// ```
158+
159+
// ---
160+
161+
// ## ✅ 補足
162+
163+
// * `container/heap` を使って安全・効率的に最大ヒープを実装
164+
// * `bufio.Scanner` で高速な入力処理
165+
// * 型安全 (`Job`, `MaxHeap`) に処理を構造化
166+
167+
// ---
168+
169+
// ご希望があれば、**図解付きの処理フロー**や**各日ごとのヒープ状態の追跡**も提供可能です。お気軽にどうぞ。
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// 以下は、Node.js (`fs` モジュール使用, Node.js 18.16.1 準拠) を用いて、**最大収入を得るスケジューリング問題**を効率的に解くコードです。
2+
3+
// この問題は、`D日間` で `X_i日目以降に可能な仕事を最大で1日1件` という条件の下で、**貪欲法 + 優先度付きキュー(ヒープ)** を用いて最大収益を得る典型問題です(鉄則本 8.3 節に該当)。
4+
5+
// ---
6+
7+
// ### ✅ 解法概要(満点解法):
8+
9+
// * 各日 `d` について、できる仕事候補(X\_i ≤ d)を収集しておき、`最大の報酬Y_i` の仕事を選ぶ。
10+
// * 実装には **最大ヒープ** を使って毎日最も報酬の高い仕事を選ぶ。
11+
// * 時間計算量:`O(N log N + D log N)` → 満点解法
12+
13+
// ---
14+
15+
// ### ✅ 実装コード(Node.js + fs + 最大ヒープ):
16+
17+
// ```javascript
18+
const fs = require('fs');
19+
20+
/**
21+
* 最大収益を求める関数
22+
* @param {number} N - 仕事の数
23+
* @param {number} D - 就業可能日数
24+
* @param {Array<[number, number]>} jobs - 仕事一覧([開始日, 報酬])
25+
* @returns {number} - 最大収益
26+
*/
27+
function getMaxEarnings(N, D, jobs) {
28+
// 各日の仕事を格納(day: 1〜D)
29+
const jobByDay = Array.from({ length: D + 1 }, () => []);
30+
for (const [x, y] of jobs) {
31+
if (x <= D) jobByDay[x].push(y);
32+
}
33+
34+
// 最大ヒープ(優先度付きキュー)
35+
const maxHeap = new MaxHeap();
36+
let total = 0;
37+
38+
for (let day = 1; day <= D; day++) {
39+
// 今日できる仕事をヒープに追加
40+
for (const reward of jobByDay[day]) {
41+
maxHeap.push(reward);
42+
}
43+
// 今日の最良の仕事を選ぶ
44+
if (maxHeap.size() > 0) {
45+
total += maxHeap.pop();
46+
}
47+
}
48+
49+
return total;
50+
}
51+
52+
// --- ヒープ実装(最大ヒープ) ---
53+
class MaxHeap {
54+
constructor() {
55+
this.data = [];
56+
}
57+
58+
push(val) {
59+
this.data.push(val);
60+
this._heapifyUp(this.data.length - 1);
61+
}
62+
63+
pop() {
64+
if (this.data.length === 0) return null;
65+
const top = this.data[0];
66+
const last = this.data.pop();
67+
if (this.data.length > 0) {
68+
this.data[0] = last;
69+
this._heapifyDown(0);
70+
}
71+
return top;
72+
}
73+
74+
size() {
75+
return this.data.length;
76+
}
77+
78+
_heapifyUp(i) {
79+
while (i > 0) {
80+
const parent = Math.floor((i - 1) / 2);
81+
if (this.data[parent] >= this.data[i]) break;
82+
[this.data[parent], this.data[i]] = [this.data[i], this.data[parent]];
83+
i = parent;
84+
}
85+
}
86+
87+
_heapifyDown(i) {
88+
const n = this.data.length;
89+
while (true) {
90+
let largest = i;
91+
const left = 2 * i + 1;
92+
const right = 2 * i + 2;
93+
94+
if (left < n && this.data[left] > this.data[largest]) largest = left;
95+
if (right < n && this.data[right] > this.data[largest]) largest = right;
96+
97+
if (largest === i) break;
98+
99+
[this.data[i], this.data[largest]] = [this.data[largest], this.data[i]];
100+
i = largest;
101+
}
102+
}
103+
}
104+
105+
// --- 入出力処理 ---
106+
function main() {
107+
const input = fs.readFileSync('/dev/stdin', 'utf8').trim().split('\n');
108+
const [N, D] = input[0].split(' ').map(Number);
109+
const jobs = input.slice(1).map(line => line.split(' ').map(Number));
110+
const result = getMaxEarnings(N, D, jobs);
111+
console.log(result);
112+
}
113+
114+
main();
115+
116+
// ### ✅ 実行性能(目安)
117+
118+
// * 時間計算量:`O(N log N + D log N)`(`N ≦ 2e5`, `D ≦ 2000`)
119+
// * 空間計算量:`O(N + D)`(jobByDayとヒープ分)
120+
121+
// ---
122+
123+
// ### ✅ 入力例テスト(標準入力)
124+
125+
// ```
126+
// 5 4
127+
// 1 1
128+
// 2 4
129+
// 2 3
130+
// 3 4
131+
// 4 2
132+
// ```
133+
134+
// ```
135+
// 出力:
136+
// 12
137+
// ```
138+
139+
// ---
140+
141+
// ご希望があれば、このコードの**図解付きステップ解説**も提供可能です。

0 commit comments

Comments
 (0)