問題解決のためのプログラミング一巡り 金子 知適 ([email protected]) 2014 年度 (2 月初旬公開版 r8) 概要 この資料は,東京大学教養学部前期課程で開講されている全学自由研究ゼミナール「実践的プログラミン グ」の 2014 年までの配布資料を多少編集して,自習用資料として整形したものである. プログラミングやアルゴリズムを学ぶためには,教科書を読むだけでなく実際に作ることが有用である. 幸いにも,作成したプログラムの「正しさ」を自動で判定する,オンラインジャッジというシステムが国 内外で充実し,優れた自習環境が整いつつある.この資料は,それらで学ぶ敷居をもう少し下げて,入門 の段階の学習者が問題解決の面白さを味わう手助けを行う意図で準備された.たとえば,ほとんどの例題 には,穴埋めや日本語で書かれた擬似コードを C++に翻訳するだけで完成するような,ヒントが用意され ている.プログラミング言語の基本的な文法を理解していれば,ヒントを追いながら手法の流れを理解し たり,入出力データでプログラムの正しさをテストするなどの経験を積みながら,ヒントなしでもプログ ラムを組む力が身についてゆくと期待している.この資料自体は様々な点で作成途上のものであるが,手 軽な資料が現状で少ないため,活用の機会もあるかもしれないと考えて 2013 年より公開している. 目次 第 1 章 はじめに 1.1 1.2 資料の構成 1.3 1.4 オンラインジャッジシステム . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 教科書・参考書 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 実践例 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.4.1 1.4.2 1.4.3 ファイルの保存/コンパイル . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.4.4 1.4.5 実行例 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 標準入出力とリダイレクション . . . . . . . . . . . . . . . . . . . . . . . . . . . . . コンパイル . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . リダイレクションとファイルを用いたテスト . . . . . . . . . . . . . . . . . . . . . . 5 5 5 6 7 7 7 8 9 9 第 2 章 入出力と配列・シミュレーション 2.1 11 言語機能: 配列, ループ, 条件分岐,関数 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 2.2 2.1.1 配列, ループ, 条件分岐 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 2.1.2 関数 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 最大値,最小値 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.2.1 2.2.2 2.3 2.4 プログラム作成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 動作テスト . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 計算時間の見積と試行回数 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 配列,std::vector, std::array と操作 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 2.4.1 データ列の表現 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 2.5 2.4.2 列の操作 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 練習問題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 2.6 いろいろな問題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 第 3 章 整列と貪欲法 3.1 3.2 26 さまざまな整列 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 3.1.1 3.1.2 数値の整列 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 3.1.3 3.1.4 ペアと整列 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 文字列の整列 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 大きい順に整列 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 貪欲法 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 第 4 章 動的計画法 (1) 4.1 4.2 4.3 35 経路を数える . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 最適経路を求めて復元する . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 さまざまな動的計画法 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 1 4.4 練習問題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 第 5 章 分割統治 (1) 5.1 二分探索 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 5.1.1 5.2 43 データ列中の値の検索 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 5.1.2 制約を満たす最小値を求める . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 Merge Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 5.2.1 Inversion Count . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 5.3 5.2.2 練習問題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 木のたどり方 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 5.4 空間充填曲線 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 第 6 章 基本データ構造 (string, stack, queue, string, set, map) 6.1 6.2 スタック (stack) とキュー (queue) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 6.2.1 6.2.2 6.2.3 6.3 6.4 6.5 スタック . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 キュー . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 優先度付きキュー . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 文字列と分割・連結・反転 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 集合 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 連想配列 (map) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 6.5.1 6.5.2 6.6 52 文字列 (string) と入出力 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 文字列に対応する数字 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 文字列の出現回数 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 6.5.3 連想配列の要素の一覧 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 練習問題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 6.6.1 出現頻度 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 6.6.2 6.6.3 文字列と数値の対応 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 6.6.4 集合の応用 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 区間の管理 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 第 7 章 グラフと木 66 7.1 7.2 概要: グラフ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 7.3 7.4 木 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 一筆書きの判定 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 「親」に注目した,木の表現 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 7.4.1 7.5 最小共通祖先 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 7.4.2 親への移動 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 木の話題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 7.5.1 木と動的計画法 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 7.5.2 7.5.3 木の直径 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 7.5.4 木の正規化 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 木の中心 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 2 第 8 章 全域木と Disjoint Set (Union-Find Tree) 8.1 8.2 74 Disjoint Set (Union-Find Tree) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 8.1.1 練習問題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 全域木 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 8.2.1 クラスカル法 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 8.2.2 色々な全域木 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80 第 9 章 繰り返し二乗法と行列の冪乗 9.1 9.2 概要 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 言語機能: struct と再帰関数 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 9.2.1 9.2.2 9.2.3 9.3 9.4 9.5 81 long long . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 struct . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 再帰関数 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 正方行列の表現と演算 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 練習: フィボナッチ数 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 9.4.1 様々な計算方針 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 9.4.2 行列で表現する . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 応用問題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 第 10 章 グラフの探索 87 10.1 グラフの表現: 隣接リストと隣接行列 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 10.2 幅優先探索 (BFS) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 10.3 深さ優先探索 (DFS) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 10.3.1 深さ優先探索の応用 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 10.4 二部グラフの判別 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 10.4.1 グラフの走査 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 10.4.2 合流/ループの検査 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 10.5 様々なグラフの探索 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 第 11 章 最短路問題 98 11.1 重み付きグラフと表現 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98 11.2 全点対間最短路 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98 11.2.1 例題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 11.2.2 負の重みを持つ辺がある場合 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 11.3 単一始点最短路 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 11.3.1 緩和 (relaxation) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 11.3.2 Bellman-Ford 法 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102 11.3.3 Dijkstra 法 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 11.3.4 手法の比較と負辺 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 11.4 練習問題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 第 12 章 平面の幾何 (1) 109 12.1 概要: 点の表現と演算 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 12.2 三角形の符号付き面積の利用 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 12.2.1 多角形の面積 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 3 12.2.2 平行の判定 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 12.2.3 内外判定 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 12.2.4 凸包 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 12.3 様々な話題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114 12.4 応用問題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 第 13 章 簡単な構文解析 119 13.1 四則演算の作成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 13.1.1 足し算を作ってみよう . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 13.1.2 カッコを使わない四則演算の (いい加減な) 文法 . . . . . . . . . . . . . . . . . . . . 122 13.1.3 四則演算: カッコの導入 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 13.1.4 まとめ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 13.2 練習問題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124 第 14 章 整数と連立方程式 127 14.1 素因数分解、素数、ユークリッドの互除法など . . . . . . . . . . . . . . . . . . . . . . . . . 127 14.2 連立方程式を解く . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130 14.2.1 言語機能: valarray . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130 14.2.2 連立方程式 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130 14.3 その他の練習問題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131 第 15 章 補間多項式と数値積分 133 15.1 Lagrange 補間多項式 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133 15.2 数値積分とシンプソン公式 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136 15.2.1 台形公式 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136 15.2.2 シンプソン公式 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136 15.3 道具としての Fast Fourier Transform (FFT) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138 第 16 章 区間の和/最大値/最小値と更新 140 16.1 累積和 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140 16.2 Binary Indexed Tree (Fenwick Tree) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142 16.3 Segment Tree と Range Minimum Query . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145 第 17 章 おわりに 147 付 録 A バグとデバッグ 149 A.1 そもそもバグを入れない . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149 A.2 それでも困ったことが起きたら . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151 A.2.1 道具: assert . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151 A.2.2 道具: GLIBCXX DEBUG (G++) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 A.2.3 道具: gdb . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 A.2.4 道具: valgrind . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153 A.3 標本採集: 不具合の原因を突き止めたら . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 4 第 1 章 はじめに 1.1 資料の構成 各章が 90 分の演習時間を想定して作られている 1 . 「例題」やヒントがついた易しい「練習問題」は,主 に未経験者を想定して用意されていて,一旦理解した後であれば 5-10 分で解けるものが多い.しかし,初 めて取り組む場合は時間を 5 倍程度長めに見積もることをお勧めする.経験者向けには,難易度の異なる 複数の練習問題が紹介されている.所要時間は熟達度で異なるが,問題名に印 ⋆ が一つつくと,難易度が 5 倍程度 (たとえば回答作成に要する時間で測ったとして) 難化する目安である.また後ろの章の知識を前提 としている場合もある.そのため,各章の問題を全て解いてから次に進むのではなく,印なしの易しい問 題を解いたら一旦次の章に進むことを勧める.一旦ひと通り例題を解いてどのような話題があるか目を通 すと,二週目には解ける問題が増えていることだろう.さらに印 ⋆ を二つ以上持つ問題は,その章の内容 と多少は関係があっても解法と直接の関係がない場合もある.これは,どの戦略が有効かの見極めも,問 題解決を学ぶ面白さの一つであるため. 凡例 資料内へのリンクは深緑で示される (例: 1 章,文献 [3]).また,外部へのリンクは青で示される (例: http://www.graco.c.u-tokyo.ac.jp/icpc-challenge/). 教養学部前期課程実践的プログラミング履修 (予定) 者への補足 これから開講されるセミナーが,この資 料の予習を前提とすることは*ない*. すなわち,未経験者向けの題材が,経験者向けの練習問題と並んで 引き続き提供される.扱うテーマはこの資料と重なる部分もあれば重ならない部分もある. 1.2 教科書・参考書 この資料の読者としては,繰り返しや条件分岐を短いコードならは思い通りにかける状態であること, 再帰についても習ったことがあることが想定されている.そのため本当に初めてプログラミングに触れる 場合は,軽く他著で学ぶことを勧める.既に購入済みの書籍があればそれで十分だが,新たに購入する場 合は「オンラインジャッジではじめる C/C++プログラミング入門」[1] が,AOJ を使っている点で本資料と の接続が良い. 本文中で,参考書 攻略 [2] として「プログラミングコンテスト攻略のためのアルゴリズムとデータ構造」 に,参考書 [3] として「プログラミングコンテストチャレンジブック第二版」に言及することがある (言う までもなくこの分野の名著である).また,学習時間 (と予算) にゆとりがある者には, 「アルゴリズムデザ イン」[4] の 6 章までを時間をかけて読み進めることを勧める.ページ数が多いが,その分丁寧に書かれて いるので,類書の中では初学者に適すると思われる (ただし筆者は英語版で読んだので,日本語版の評価は 予想である).さらに深く学ぶ場合は, 「アルゴリズムイントロダクション」[5] も,時間をかけて学習する 価値がある. 1 当初はそうであったが,整理の都合で現在では分量が適切でない章もあるかもしれない. 5 1.3 オンラインジャッジシステム 本資料の問題は,以下のオンラインジャッジから採録している.資料作成時に担当者が各オンラインジャッ ジの利用条件を探した範囲では,この資料内での各問題への参照は問題ないと判断したが,お気づきの際 は随時連絡されたい. • Aizu Online Judge (AOJ) http://judge.u-aizu.ac.jp • Peking University Judge Online for ACM/ICPC (POJ) http://poj.org • Codeforces http://www.codeforces.com/ • MAIN.edu.pl http://main.edu.pl/en 出典の記述の際に日本国内の ACM-ICPC 及び ACM-ICPC OB/OG 会 (JAG) のものは,区別がつく範囲で 簡略に示した.たとえば国内予選は日本の ACM-ICPC のものを,模擬国内予選は ACM-ICPC OB/OG 会主 催の恒例の練習会を指す. アカウント作成 初めての場合はまず AOJ のアカウントを作成する.各システムとも無料で使うことができる.なお,各 オンラインジャッジは,運営者の好意で公開されているものであるから,迷惑をかけないように使うこと. 特にパスワードを忘れないこと. AOJ のアカウント作成 (初回のみ) ページ右上の Register/Setting からアカウントを作成する.User ID と Password を覚えておくこと (ブラウザに覚えさせる,もしくは暗号化ファイルにメモする).この通信は https でないので,注意.Affiliation は the University of Tokyo 等とする. E-mail や URL は記入不要. ここで,自分が提出したプログラムを公開するかどうかを選ぶことができる.公開して (“public” を選択) いれば,プログラムの誤りを誰かから助けてもらう際に都合が良いかもしれない.一方, 「他者のコード片 の動作を試してみる」というようなことを行う場合は,著作権上の問題が発生しうるので,非公開の方が 良いだろう (“private” を選択). AOJ への提出 (毎回) ログイン後に問題文を表示した状態で,長方形に上向き矢印のアイコンを押すと, フォームが表れる. 自分の提出に対応する行 (“Author” を見よ) の “Status が “Accepted” なら正答. 6 正答でなかった時 様々な原因がありうるので,まずジャッジの応答がどれにあてはまるか,システムの 使い方を誤解していないかなどを説明を読んで確認する.Submission notes (http://judge.u-aizu. ac.jp/onlinejudge/submission_note.jsp), Judge’s replies (http://judge.u-aizu.ac.jp/ onlinejudge/status_note.jsp), チュートリアル (http://judge.u-aizu.ac.jp/onlinejudge/ AOJ_tutorial.pdf) などの資料がある. 一般的にプログラムが意図したとおりに動かないことは,誰でも (熟達者でも!) しばしばあることであ る.組み上げたプログラムが動かなかったとしても,何から何までダメという事はなく,多くの場合はほ んの少しの変更で解決することが多い.そこで,どの部分までは正しく動いているか,各部品ごとに動作 確認をする方針が有効である.コンピュータでのプログラミングは copy や undo ができることが長所であ るから,(料理で食材を無駄にしてがっかりするようなことは起こらない),臆せず色々試すと良い. 困った状況から復帰するノウハウも多少も存在する (付録 A) ので,徐々に身につけると有用であろう. 一方で,経験が少ない段階では,15 分以上悩まないことをお勧めする.手掛かりなく悩んで時間を過ごす ことは苦痛であるばかりでなく,初期の段階では学習効果もあまりないので,指導者や先輩,友達に頼る, あるいは一旦保留して他の問題に取り組んで経験を積む方が良いだろう.相談する場合は, 「こう動くはず なのに (根拠はこう),実際にはこう動く」と問題を具体化して言葉にしてゆくと解決が早い.なお,悩ん で意味がある時間は,熟達に応じて 2 時間,2 日間等伸びるだろう. 1.4 1.4.1 実践例 ファイルの保存/コンパイル 各章では,サンプルや機能確認のために複数のコード片を扱う.そこで,混乱を避けるため,フォルダ を作って,テーマごとに別の名前をつけてファイルに保存すると良い.そして,それぞれを動作可能に保 つ.一方,お勧めしない方法は,一度作ったファイルを継ぎ足しながら,一つの巨大なファイルにするこ とである.そうしてしまうと,後から,2 種類のコードを実行して差を比べるようなことが難しくなる. 「ターミナル」(MacOSX の場合) の動作例は以下の通り: $ mkdir programming # (フォルダ作成,最初の一回のみ) $ mkdir programming/chapter1 # (各章毎に行う) $ cd programming/chapter1 # (カレントディレクトリを変更.$HOME/programming/chapter1/ 以下にソースコードを 保存) $ g++ -Wall sample1.cc # (-Wall は警告を有効にするオプションで,何かメッセージが出た場合は解消することが望ま しい.読み方が分からないメッセージが出た場合は,誰かと相談する) $ ./a.out # (実行) 1.4.2 1 2 3 4 5 6 7 8 9 10 標準入出力とリダイレクション 各問題では標準入出力を扱い,プログラムの正しさを入力に対する出力で判定する.入力では,入力デー タがある限り読み込んで処理する場合も多い.そこで,初めに例を挙げる.以下環境は基本的に MacOSX を想定するが,ubuntu や cygwin 等でも動作すると思われる. 7 例題 年を読み込んで,うるう年かどうかを判定し,日数を出力するプログラムを作る (いい加減な) プログラム例 (C++): leap.cc C++ 1 2 3 4 5 6 7 8 9 10 11 12 // 4 年に一度? #include <iostream> using namespace std; int main() { int year; while (cin >> year) { // cin は bool に自動変換されるので入力が読める限り ループ if (year % 4 == 0) cout << 366 << endl; else cout << 365 << endl; } } C の場合: leap.c C 1 2 3 4 5 6 7 8 9 10 11 /* 4 年 に 一 度 ? */ #include <stdio.h> int main() { int year; while (˜scanf("%d", &year)) { /* s c a n f が E O F を 返 す ま で ル ー プ す る 略 記 */ if (year % 4 == 0) printf("%d\n", 366); else printf("%d\n", 365); } } 注意: この資料では,これ以降 C 言語をサポートしない.この資料は C++や Java で提供されるような高 機能な標準ライブラリの使用を前提として書かれているため,C 言語に慣れている場合であっても部分的 に C++を取り入れることを強く勧める.この資料の演習に必要な機能は C++全体のほんの一部であるので, そこだけ借りて使いながら,他は C 言語のつもりで書けば良い.つまり,一般に C++を学び直すことより も苦労は少ないはずである.もちろん一旦熟達した後であれば,本資料のかなりの問題を C 言語でも解け ると思われるが,そのような技術を持つ読者にはこの資料は不要のはずである. 1.4.3 コンパイル (以降$記号は,ターミナルへのコマンド入力を示す) C++の場合: 1 $ g++ -Wall leap.cc C の場合: 8 1 $ gcc -Wall leap.c 1.4.4 実行例 実行例: 以下,斜体はキーボードからの入力を示す.終了は Ctrl キーを押しながら c または d をタイプ する.この操作をˆC やˆD と表記する. 1 2 3 4 5 6 7 8 9 10 $ ./a.out 2004 366 1999 365 1900 366 2000 366 ˆD 1.4.5 リダイレクションとファイルを用いたテスト 実行するたびに毎回キーボードをタイプするのは煩雑であるから,自動化したい.そこで,リダイレク ションとファイルを用いたテストを解説する.早い段階で身につけることが望ましい. 正しい入出力例をエディタで作成し,cat コマンドで中身を確認する: 1 2 3 4 5 6 7 8 9 10 $ cat years.input 2004 1999 1900 2000 $ cat years.output 366 365 365 366 リダイレクションを使った実行 (キーボード入力の代わりにファイルから読み込む): 1 2 3 4 5 $ ./a.out < years.input 366 365 366 366 実行結果をファイルに保存 (画面に表示する代わりにファイルに書き込む): 9 $ ./a.out < years.input > test-output $ cat test-output 366 365 366 366 1 2 3 4 5 6 自動的な比較: $ diff -u test-output years.output --- years.output Fri Oct 14 10:53:52 2005 +++ test-output Fri Oct 14 10:53:56 2005 @@ -1,4 +1,4 @@ 366 365 -365 +366 366 1 2 3 4 5 6 7 8 9 4 行目あたりが違うことを教えてくれる 資料: • HWB 15 コマンド http://hwb.ecc.u-tokyo.ac.jp/current/information/cui/ • HWB 14.4 コマンドを使ったファイル操作 http://hwb.ecc.u-tokyo.ac.jp/current/information/filesystem/cui-fs/ 10 第 2 章 入出力と配列・シミュレーション 概要 様々な解法につながる基本として,入力データを逐一調べる問題から始めて,配列と周辺の基本操作 をとりあげる. 初めてオンラインジャッジを使う場合を想定して,標準入出力の取り扱いに慣れる.また,プログラ ムを一度に作成するのではなく,部品ごとに作ってテストするスタイルを身につける.さらに,デー タの量に応じて,適切なアルゴリズムが必要になることを経験する. 2.1 言語機能: 配列, ループ, 条件分岐,関数 この章以降に前提とする言語機能を紹介する.これらの意味が分からない場合は,入門書を参照のこと. 2.1.1 C++ C++ 2.1.2 C++ 配列, ループ, 条件分岐 1 2 3 4 5 int main() { int A[4] = { 0, 1, 2, 3, }; for (int i=0; i<4; ++i) cout << A[i] << endl; } 1 2 3 4 5 6 7 8 int main() { int A[4] = { 0, 1, 2, 3, }; int i=0; while (i<4 && A[i]!=2) { cout << A[i] << endl; ++i; } } 関数 1 2 3 4 5 double pi() { return 3.14; } int main() { cout << 3.14 << endl; cout << pi() << endl; } C++ 11 1 2 3 4 5 2.2 int sum(int a, int b) { return a+b; } int main() { cout << 3+5 << endl; cout << sum(3,5) << endl; } 最大値,最小値 例題 ICPC Score Totalizer Software (国内予選 2007) 入力として与えられる数値列から最大値と最小値を除いた平均値を求めよ. 入力はそれぞれが競技者の演技ひとつに対応するいくつかのデータセットからなる. 入力の データセット数は 20 以下である.データセットの最初の行はある演技の採点に当たった審判の数 n (3 ≤ n ≤ 100) である.引き続く n 行には各審判のつけた 点数 s (0 ≤ s ≤ 1000) がひとつず つ入っている. n も各 s も整数である.入力中にはこれらの数を表すための数字以外の文字は ない.審判名は秘匿されている. 入力の終わりはゼロひとつの行で示される. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1147& lang=jp “Sample Input” を順に解釈すると,以下のように 4 つのデータセットからなることが分かる. • 3 (n) 1000 342 0 (s) • 5 (n) 2 2 9 11 932 (s) • 5 (n) 300 1000 0 200 400 (s) • 8 (n) 353 242 402 274 283 132 402 523 (s) • 0 (入力の終わり) #-------3 # データは 3 つ, N=3 1000 # 最大値 342 0 # 最小値 #-------5 # データは 5 つ, N=5 2 # 最小値 2 9 11 932 # 最大値 #-------5 # データは 5 つ 12 300 1000 # 最大値 0 # 最小値 200 400 #-------8 # データは 8 つ 353 242 402 274 283 132 402 523 #-------0 # これでおしまい 同様に “Sample Ouput” を解釈すると,上記の各データセットに対して一つ数値が出力されていることが 分かる. • 342 (そのまま) • 7 (2, 9, 11 の平均) • 300 (300, 200, 400 の平均) • 326 (... の平均) 2.2.1 プログラム作成 以下に,審判の得点の合計を計算するプログラムと最大得点を計算するプログラムを示す.(問題が求め るものは合計や最大値ではないので,これらは問題への直接の回答ではない).必要ならばこれらのプログ ラムを参考に,問題への回答を作成せよ. 以降は,C++でコード例を示すが,C 言語に馴染んでいる読者も (基本は共通なので) さしあたり 網掛け 部 分を覚えて使えるようになると良い. C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 #include < iostream > using namespace std ; int N, S; int main() { while ( cin >> N && N>0) { int sum = 0; for (int i=0; i<N; ++i) { cin >> S; sum += S; } cout << sum << endl ; } } 13 C++ 1 2 3 4 5 6 7 8 9 Ruby 1 2 3 4 5 6 7 8 9 10 11 12 13 Ruby 1 2 3 4 5 6 // (一部略) while ( cin >> N && N>0) { int largest = 0; for (int i=0; i<N; ++i) { cin >> S; if (largest < S) largest = S; } cout << largest << endl ; } while true judges = gets.to_i # 数字を入力 break if judges == 0 a = [] for i in 0..judges-1 a << gets.to_i end # ここまでに配列 a に各審判のスコアが入っている sum = 0 for i in 0..judges-1 sum = sum + a[i] end puts sum end while true judges = gets.to_i break if judges == 0 a = (1..judges).map { gets.to_i } puts a.max end コンパイルの仕方などは 1.4 節を参照. 2.2.2 動作テスト サンプル入力を用いたテスト 問題文中の “Sample Input” に対する出力が合うことを確認してみよう.実行方法などは 1.4 節を参照. 審判データを用いたテスト この問題は審判が用いた秘密の入出力が公開されている.これを利用して, プログラムの誤りの有無を さらに手元で試験することができる.(プログラムが “Sample Input” には正しく振舞うが,他のデータには 誤ることを発見できるかもしれない) • 入力 http://www.logos.ic.i.u-tokyo.ac.jp/icpc2007/jp/domestic/datasets/A/ A1 • 出力 http://www.logos.ic.i.u-tokyo.ac.jp/icpc2007/jp/domestic/datasets/A/ A1.ans 14 計算時間の見積と試行回数 2.3 コンピュータが得意な解法は全部を力づくで試すことである.実際に多くの問題をそれで解くことが出 来る. 一方で,多くのコンテストの問題では実行時間に制限がついているので,実行時間を見積もり間に合う 解法を考案することが必要な場合もある.またコンテストを離れて実用に用いるプログラムでも,何らかの 実行速度に関する要請がある場合が多い.プログラムを書き終えてから速度に問題があることが分かると, 書き直しが困難な場合もあるので,プログラムを書く*前*に何らかの見通しを得ておくことが望ましい. 現実の計算機は複雑な装置であるから,正確な実行時間の予想は簡単ではない.よって単純なモデルに 基づく目安を考える.たとえば,プログラムを実行する過程で,加減乗除のような基本演算が何回行われ るかを数える.このモデルでは加算と除算では速度が異なるとか,同時に二つの命令を実行される場合が あるなどの,細かい点は無視している.目安であるので現実との対応関係は別に議論が必要だが,役に立 つ場面も多い.(たとえばメモリ参照や new/delete はそれぞれ 10 倍,100 倍と遅い場合がある.) 作成した解法が入力に対応する数値 N に関しておよそ何回の基本演算を行うかを見積もったら,いくつ かの N に対応する入力で実験し,実際の計算秒数との対応を取る.ジャッジの環境と手元が異なるなど, 実験できないような場合は,大まかに 1GHz のコンピュータは 1 秒間に 1G 回の 10 分の 1 くらいの基本演 算が出来ると考えるという方法がある.たとえば,参考書 [3, p. 20] には,1 000 万回なら「たぶん間に合 う」,1 億回なら「間に合わなくてもやむを得ない」と述べられている.実際のところは問題ごとの秒数制 限と,オンラインジャッジのハードウェアにより大きく異なる場合がある. 問題 (模擬国内予選 2007) Space Coconut Grab 宇宙ヤシガニの出現場所を探す.エネルギー E が観測されたとして,出現場所の候補は, x + y 2 + z 3 = E を満たす整数座標 (x,y,z) で,さらに x+y+z の値が最小の場所に限られると いう.x+y+z の最小値を求めよ.(x,y,z は非負の整数,E は正の整数で 1,000,000 以下,宇宙ヤ シガニは,宇宙最大とされる甲殻類であり,成長後の体長は 400 メートル以上,足を広げれば 1,000 メートル以上) http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2012 まず,全部試して間に合うかを考える.(間に合うならそれが一番実装が簡単なプログラムである) √ √ 変数 x,y,z の動く範囲を考えると,それぞれ x:[0,E], y:[0, E], z:[0,3 E] である.それら (x,y,z) の組み合 わせは,E の最大値は 1,000,000= 106 であるから,最大 106 · 103 · 102 = 1011 である.制限時間が 8 秒で あることを加味しても,この方針では間に合いそうにない. 減らす指針として,全ての (x,y,z) の組み合わせを考える必要はなく,x + y 2 + z 3 = E を満たす範囲だ けで良いことを用いる.たとえば,x と y を決めると,E から z は計算できる (z が整数とならない場合は 無視して良い).この方針で調べる種類は,(x,y) の組み合わせの種類である,106 · 103 となる.この値はま だ大きいが,先ほどと比べると 1/100 に削減できている.上記と同様に,x と z を決めて y を求める,z,y を決めて x を求めることもできる.それらの方針の場合に,調べる組み合わせの種類を求めよ.一番少な いものが間に合う範囲であることを確認し,実際に実装して AOJ に提出して確かめる. 15 問題 (模擬国内予選 2007) Square Route 縦横 1500 本程度の道路がある街で,正方形の数を数えてほしい. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2015 注: 全ての 4 点を列挙して正方形になっているかどうかを試すと間に合わないので,工夫した方法が必要 である. 2.4 配列,std::vector, std::array と操作 データの列を表す方法と,操作を簡単に紹介する. 2.4.1 C++ データ列の表現 1 2 // 長さ 50 の整数の配列を用意し各要素を 0 で初期化 (A[0] から A[49] まで有効) int A[50] = {}; C の配列は,定義の際に定数で長さを与える必要がある.この資料で扱う問題を解く場合は,必要な最 大値を見積もって多少余分に取っておく. C++の場合は,vector を用いることで長さを実行時に決めることができる. C++ 1 2 3 4 5 #include <vector> using namespace std; int N = ...; // 長さ N の整数の配列を用意し各要素を 0 で初期化 (A[0] から A[N-1] まで有効) vector<int> A(N, 0); Ruby の Array も同様である. Ruby 1 2 3 n = 50 # 長さ n の配列を用意し各要素を 0 で初期化 (a[0] から a[n-1] まで有効) a = Array.new(n,0) C++の新しい規格である C++11 では,array というデータ型が導入されている.これは vector と同様の インターフェースを持ち,一方定義の際に長さを定数で与える必要があるという点で,配列と vector の中 間の性質を持つ. C++11 1 2 3 4 #include <array> using namespace std; // 長さ 50 の整数の配列を用意し各要素を 0 で初期化 (A[0] から A[49] まで有効) array<int,50> A = {}; C++11 から導入された機能を使う場合は,このセミナーの iMac 環境では g++の代わりに g++-mp-4.8 を用い,オプション-std=c++11 を与える.すなわち sample.cc をコンパイルする場合は以下のように なる. 16 $ g++-mp-4.8 -std=c++11 -Wall sample.cc 1 AOJ に提出する場合も,C++ではなく C++11 を選ぶ.C++11 で書く場合は生の配列よりも,array を使うほう がお勧めである.関数の引数で受ける場合にも型が保存されることや,g++ 4.8 ではマクロ GLIBCXX DEBUG を定義しておくことで範囲外参照を発見できるなどのメリットがある. C++11 1 2 3 4 5 #include <array> int main() { std::array<int,3> a; a[4] = 5; } a[2] までのところを a[4] にアクセス $ g++-mp-4.8 -std=c++11 -Wall sample.cc $ ./a.out # 何も起こらない $ g++-mp-4.8 -D_GLIBCXX_DEBUG -std=c++11 -Wall sample.cc $ ./a.out /.../c++/debug/array:152:error: attempt to subscript container with out-of-bounds index 4, but container only holds 3 elements. Objects involved in the operation: sequence "this" @ 0x0x7fff6edfab00 メッセージは読みにくいが異常検知に成功 { type = NSt7__debug5arrayIiLm3EEE; } Abort trap: 6 2.4.2 1 2 3 4 5 6 7 8 9 10 11 12 13 列の操作 このような列に対して,様々な操作が用意されている.どのような操作が用意されているかは言語による. 全ての要素を処理する 配列の全ての要素を処理する場合は,for 分を用いることが一般的である.要素数 (この場合は 5) を間違 えないように,注意を払うこと. C++ 1 2 3 int A[5] = {0,1,2,3,4}; for (int i=0; i<5; ++i) printf("%d\n", A[i]); Ruby の場合は,for 文を使うことも可能だが,each という機能にお任せすることもできる. Ruby 1 2 a = Array.new(5) a.each{|e| puts e} C++11 でも同様のことができる. C++11 1 for (auto e:A) cout << e << endl; vector であれば C++でも大きさを吸収可能である 17 C++ 1 2 3 4 5 vector<int> A(50); for (size_t i=0; i<A.size(); ++i) cout << A[i] << endl; for (vector<int>::const_iterator p=0; p!=A.end(); ++p) cout << *p << endl; 整列 sort C++と Ruby では,標準関数として sort や sort!が用意されている. C++ 1 2 3 4 5 6 7 8 #include <algorithm> #include <iostream> using namespace std; int A[5] = {3,5,1,2,4}; int main() { sort(A,A+5); // 半開区間 [l,r) で指定する.sort(&A[0], &A[5]) と同じ意味 ... // cout に A を出力してみよう } 配列を整列させる範囲を指定するには,&A[0] のように配列の要素を指すポインタを用いる.一次元配列 の場合に単に A と書けば,先頭要素を指すポインタに自動的に変換される.配列でなく vector や array の範囲を指定するには,A.begin(),A.end() という表記を用いる.この begin や end はポインタを 一般化したイテレータを返す関数で,名前の通りの場所を指し示す.他に A.begin()+2,A.begin()+5 のように一部の区間を指定することもできる.さらに C++11 では begin(A) とすることで配列も vector も共通に扱うことができる. Ruby 1 2 3 a = [3,5,1,2,4] a.sort! p a 反転 reverse C++ Ruby 1 2 3 4 5 6 7 8 #include <algorithm> using namespace std; int A[5] = {3,5,1,2,4}; int main() { sort(A,A+5); reverse(A,A+5); // 与えられた範囲を逆順に並び替え ... // cout に A を出力してみよう } 1 2 3 4 a = [3,5,1,2,4] a.sort! a.reverse! # a を逆順に並び替え p a 18 回転 rotate C++で rotate(a,b,c) は範囲 [a,b) と範囲 [b,c) を入れ替える. C++ 1 2 3 4 5 6 7 #include <algorithm> int A[7] = {0,1,2,3,4,5,6}; int main() { rotate(A,A+3,A+7); // vector の場合は rotate(A.begin(),A.begin()+3,A.begin()+7); // A を出力してみよう Ô 3 4 5 6 0 1 2 } Ruby は,a[p,q] で配列 a の位置 p から q 文字を参照したり,置き換えるという強力な機能を持つ.こ れを用いると回転も簡単に実現可能である Ruby 1 2 3 a = [0,1,2,3,4,5,6] a[0,7] = a[3,4]+a[0,3] p a # Ô [3, 4, 5, 6, 0, 1, 2] permutation の列挙 他に変わった機能として,C++には next permutation という関数がある.典型的な使い方は以下の ように do .. C++ 1 2 3 4 5 6 while ループと組み合わせて,全ての並び替えを昇順に列挙することである. #include <algorithm> int A[4] = {1,1,2,3}; // あらかじめ昇順に並べておく // sort(A, A+4); // 今回は昇順に並んでいるが初期値によっては必要 do { cout << A[0] << A[1] << A[2] << A[3] << endl; } while (next_permutation(A,A+4)); 出力を確認せよ (実際に場合が尽くされているか?): 1 2 3 4 5 6 $ ./a.out 1123 1132 ... 3121 3211 next permutation は,まだ試していない組み合わせが (正確には全ての組み合わせを昇順に並べた時の 次の要素が) ある場合,配列の中身を並び替えて真を返す.そうでない場合は偽を返す.next permutation が偽を返すとループが終了する.(配列の要素が昇順に並んでいない状態に初期化して (たとえば int A[4] = {2,1,1,3};),上記のコードを実行すると,出力はどのように変わるか?) もしこの関数を自作する場合には,間違える箇所が多いと予想されるので,入念にテストを行うこと. 19 練習問題 2.5 問題 (国内予選 2004) Hanafuda Shuffle 花札混ぜ切りのシミュレート: 初めに 1, . . . , N の数値を持つ N 枚のカードが一番下が 1, 一 番上が N の順に整列されて山になっている.図のような二つのパラメータ p, c で規定される シャッフルを R 回行った後の,一番上のカードを求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1129& lang=jp 入力例と解釈 サンプル入力の 5 2 3 1 3 1 を解釈すると,まずカードは 5 枚,シャッフルは二回である.一度目で [5, 4, 3, 2, 1] が [3, 5, 4, 2, 1] となり二回目で [4, 3, 5, 2, 1] となる.従って,山の一番上は 4. 入力とプログラムの骨組み まずは入力部分を作り,正しく読み込めていることを,n や p,c を表示するこ とで確認すると良い.続いて,山を整数の列 (配列または vector, array など) で表現する.シャッフルは回転 (rotate) で実装すると良い. Ruby C++ 1 2 3 4 5 6 7 8 9 10 11 12 while line = gets n, r = line.split(" ").map{|i| i.to_i} # n,r 読み込み break if n == 0 # n 枚の山を作る # 作った山を表示してみよう r.times { p, c = gets.split(" ").map{|i| i.to_i} # シャッフル p,c を行う # シャッフル毎に山全体を表示してみよう } # 山の先頭を出力 end 1 2 3 4 5 6 7 8 9 10 11 12 13 int N, R, p, c; int main() { while (cin >> N >> R && N) { // 山全体を作る // 作った山を表示してみよう for (int i=0; i<R; ++i) { cin >> p >> c; // シャッフル p,c を行う // シャッフル毎に山全体を表示してみよう } // 山の先頭を出力 } } 20 問題 Rummy (UTPC2008) 9 枚のカードを使うゲーム.手札が勝利状態になっているかどうかを判定してほしい.勝利状 態は,手札が 3 枚ずつ 3 つの「セット」になっていること. セットとは,同じ色の 3 枚のカー ドからなる組で,同じ数 (1,1,1 など) または連番 (1,2,3 など) をなしているもの. カードは,赤緑青の三色で,数字は 1-9 まで. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2102& lang=jp 入力例と解釈 1 1 1 3 4 5 6 6 6 R R B G G G R R R 3,4,5 と 6,6,6 はセットだが 1 は色が違う Ô 勝利状態ではない 2 2 2 3 3 3 1 1 1 R G B R G B R G B 1,1,1 等同じ数で揃えようとするセットにならないが,1,2,3 の連番を同色で揃えられる Ô 勝利 状態 解答方針 人間が役に当てはまるかを検討する場合,賢い (比較的複雑な) 試行を行うことで比較的少ない 試行錯誤で最終的な判断にいたる,と想像される. 「人間がどう考えるか」をコンピュータ上で実現するこ とは人工知能の興味深い目標となることが多いが,コンピュータにとって最も簡単な方法は別に存在する ことも多い. (例: 122334 という並びを見て,慣れた人間は一目で 123, 234 と分解できるが,この操作を例 外のない厳密なルールにできるだろうか?) ここでは,(1) カードの全ての並び替えを列挙する (2) 各並び順につき,前から 3 枚づつセットになって いることを確かめる,という方針で作成しよう.このような, 「単純な試行で全ての可能性を試す」アプロー チはコンピュータで問題解決を行う時に適していることが多い. 入力の処理 例によって,入力をそのまま出力することから始める.card[i] (0 ≤ i ≤ 8) が i 番目のカード を表すことにする. C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 #include <iostream> #include <string> using namespace std; int T, card[16]; int main() { cin >> T; for (int t=0; t<T; ++t) { for (int i=0; i<9; ++i) { cin >> card[i]; card[i]//を出力 } string color; for (int i=0; i<9; ++i) { 21 14 15 16 17 18 色の変換 cin >> color; color//を出力 } } } 入力を処理した段階で,各カードは ⟨ 色, 数 ⟩ という二つの情報の組み合わせからなっているが, これを整数で表現すると今後の操作で都合が良い.そこで,赤の [1,9] のカードをそれぞれ [1,9] で,緑の [1,9] のカードをそれぞれ [11,19], 青の [1,9] のカードをそれぞれ [21,29] で表現することにしよう.(例 G3 を 13 で表し,B9 を 29 で表す) 1 C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 セットの判定 cin >> T; for (int t=0; t<T; ++t) { for (int i=0; i<9; ++i) { cin >> card[i]; } string color; for (int i=0; i<9; ++i) { cin >> color; if (color == "G") card[i] += 10; else if (color == "B") card[i] += 20; card[i] //を出力して確認 } } 続いて,3 枚のカードがセットになっているかどうかを判定する.問題にあるようにセッ トになる条件は二つある.それぞれを別に作成しテストする. C++ 1 2 3 bool is_good_set(int a, int b, int c) { return is_same_number(a, b, c) || is_sequence(a, b, c); } 一つの条件は,同じ色かつ同じ数値の場合である.card[i] においては,異なる色は異なる整数で表現 されているので,単に数値が同じかどうかを調べれば良い. C++ 1 2 3 bool is_same_number(int a, int b, int c) { // a と b と c が同じなら真,それ以外は偽 } もう一つは,同じ色かつ連番の場合である.card[i] においては,異なる色は異なる整数で表現されて いて,かつ,0 や 10 という数値は存在しないので,単に数値が連番かどうかを調べれば良い. C++ 1 2 3 bool is_sequence(int a, int b, int c) { // a+2 と b+1 と c が等しければ真,それ以外は偽 } 練習: なぜ「a-2 と b-1 と c が等しい」という降順の連番を考慮する必要がないのか考察しなさい.(後回 し可) テスト例: 1 この変換は色の異なる全てのカードが異なる数値になれば良いので青を 22 [100,109] 等であらわしても構わない. C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 int main() { // is same number のテスト cout << is_same_number(3, cout << is_same_number(3, // is sequence のテスト cout << is_sequence(3, 4, cout << is_sequence(3, 3, // is good set のテスト cout << is_good_set(3, 4, cout << is_good_set(3, 3, cout << is_good_set(3, 3, cout << is_good_set(5, 4, } 勝利状態の判定 C++ 1 2 3 4 5 1 2 3 4 5 6 7 8 9 5) << endl; // 真 3) << endl; // 偽 5) << endl; 3) << endl; 30) << endl; 3) << endl; // // // // 真 真 偽 偽 global 変数 card が勝利状態にあるかどうかを判定する bool is_all_good_set() { return // ((card[0],card[1],card[2] が good set) // かつ (card[3],card[4],card[5] が good set) // かつ (card[6],card[7],card[8] が good set)); } 全体の組み立て C++ 4, 5) << endl; // 偽 3, 3) << endl; // 真 全ての並び順を作るコードと is all good set() を組み合わせると,ほぼ完成である. int win() { // card をソートする do { if // この card の並び順が勝利状態なら return 1; } while (next_permutation(card, card+9)); // 全ての組み合わせを試したが勝利状態にならなかった return 0; } 各データセットを読み込み後にこの関数 win() を呼び,その返り値の 1 または 0 を出力するプログラム を作成せよ.手元でテスト後に,AOJ に提出して accepted になることを確認せよ. 2.6 いろいろな問題 問題 かけざん (夏合宿 2012) 決められた手順が何ステップ続くかを判定する.(初めに,有限ステップで終了する証明を行 うか,無限に続く例を作成せよ.) http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2424 23 問題 (夏合宿 2011) Starting Line ニンジンで加速しながら,ゴールまで走ろう. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2298 問題 (国内予選 2004) Water Tank⋆ いくつかの仕切りのある水槽がある.指定された位置と時刻の水位を求める. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1133& lang=jp 問題 (アジア地区予選 2009) Twenty Questions⋆ それぞれの object を区別するのに,最善の質問の仕方を考える.答え次第で次の質問を変え て良い. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1302 問題 ほそながいところ ⋆⋆ (夏合宿 2012) 馬車 n 台の出発時刻を調整して,条件を満たしながら全ての馬車がゴール地点に到着するま でにかかる時間の最小値を求める. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2427 問題 Guesswork⋆⋆ (11th Polish Olympiad in Informatics) 9 個の数が順に与えられるので,一つづつ与えられた時点でそれが全体の何番目かを答える. 9 個すべてに回答した時点ですべて正解だったら勝ち.勝率を (100 点がとれるくらいまで) 最 大化する戦略を実現せよ. http://main.edu.pl/en/archive/oi/11/zga 難易度が高い問題は,一学期履修した後に取り組むことが適切な場合もある. 問題 Ants⋆ (Polish Collegiate Programming Contest 2011) 2 匹のアリが木 (tree) の上を一定速度で歩いている.二回目に出会う時刻を求めよ. • 片方はもう片方の二倍の速さで進む • 枝を下がる場合は上がるより二倍の速さで進む 24 (問題 続き) • 根 (root) から,それぞれ別の方向にスタートする • アリ同士がぶつかると向きを反対にして進む • 根に到達した場合は裏に回ってから向きを反対にして進む • 木の情報は,片方の蟻がたどる枝の順の上り/下りで与えられる. (大きいのでメモリ上には保持できない) http://main.edu.pl/en/archive/amppz/2011/drz main.edu.pl のアカウントは AOJ とは別に作成が必要である (初回のみ).“Form/Year (a number)” はポー ランド語バージョンの Klasa/Rok の英訳が Class/Year となっているので学年を指すかもしれない. 25 第 3 章 整列と貪欲法 概要 ある基準に従ってデータを並べ替えることを整列 (sort) という.ほとんどの言語の標準ライブラリで は,整列の方法が提供されているので,まずはそれの使い方を習得しよう.この資料では整列された 結果を得られれば良いという立場をとるが,整列の手法そのものに興味がある場合は,たとえば 参考 書 攻略 [2, pp. 51–(3 章)] を参照されたい. データの整列は,問題解決の道具として活躍する場合もある.それらには,最適化問題や配置問題な ど,整列とは関係がない見た目の問題も含まれる. 3.1 3.1.1 さまざまな整列 数値の整列 C++ Ruby 1 2 3 4 5 6 7 8 #include <algorithm> #include <iostream> using namespace std; int A[5] = {3,5,1,2,4}; int main() { sort(A,A+5); // 半開区間 [l,r) で指定する.sort(&A[0], &A[5]) と同じ意味 ... // cout に A を出力してみよう } 1 2 3 a = [3,5,1,2,4] a.sort! p a 例題 Sort II (AOJ) 与えられたn個の数字を昇順に並び替えて出力するプログラムを作成せよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=10029 回答例: C++ 1 2 3 4 5 int N, A[1000000+10]; int main() { cin >> N; for (int i=0; i<N; ++i) cin >> A[i]; // A の入力 ... // A をソートする 26 6 7 8 for (int i=0; i<N; ++i) cout << (i?" ":"") << A[i]; // A の出力 cout << endl; } 文字列の整列 3.1.2 例題 Finding Minimum String (AOJ) N 個の小文字のアルファベットのみからなる文字列の,辞書順で先頭の文字列を求める.問 題文中に記述がないが N は 1000 を越えない. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=10021 補足: 辞書式順序は,大文字小文字が混ざる場合は C++の std::string の比較演算子の比較順序とは 異なる.(が,今回は小文字のみなのでその心配はない) C++ Ruby 3.1.3 1 2 3 4 5 6 7 8 9 10 11 12 13 #include <algorithm> // sort のため #include <string> // string (文字列) のため #include <iostream> using namespace std; string A[1000]; int N; int main() { cin >> N; if (N > 1000) abort(); for (int i=0; i<N; ++i) cin >> A[i]; ... // 整数の時と同様に A を整列 (sort) する cout << A[0] << endl; } 1 2 3 4 5 6 7 N = gets.to_i A = [] (1..N).each { A << gets.chomp } ... // 整 数 の 時 と 同 様 に A を 整 列 (sort) す る puts A[0] ペアと整列 ペア ペア (組み) の表現を導入する.⟨ 開始時刻, 終了時刻 ⟩ や ⟨ 身長, 体重 ⟩,⟨ 学籍番号, 点数 ⟩ など,現実世 界の情報にはペアとして表現することが自然なものも多い. C++ 27 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Ruby 1 2 3 4 #include <utility> // pair のため #include <iostream> using namespace std; int main() { pair<int,int> a(2,4); // 整数のペア cout << a.first << ’ ’ << a.second << endl; // 2 4 と表示 a.first = 3; a.second = 5; cout << a.first << ’ ’ << a.second << endl; // 3 5 と表示 a = make_pair(10, -30); cout << a.first << ’ ’ << a.second << endl; // 10 -30 と表示 pair<double,char> b; // 小数と文字のペア b.first = 0.5; b.second = ’X’; cout << b.first << ’ ’ << b.second << endl; // 0.5 X と表示 } a p b p = a = b [3,5] # [3,5] と表示 [0.5,"X"] # [0.5,"X"] と表示 ruby の場合は配列を手軽に使えるので,取り立てて pair を区別せず配列を使う. C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 /* C 言 語 の 場 合 */ #include <stdio.h> struct pair { int first; int second; }; int main() { struct pair c; c.first = 3; c.second = -5; printf("%d,%d\n", c.first, c.second); return 0 } C 言語でも構造体 (struct) を用いることで同様の操作が可能だが,使う型 (この場合は int) 毎に構造 体を定義する手間が必要となる.今後も同様の苦労があるため,C++を用いることを進める. ペアの配列と整列 続いて,ペアの配列を扱う.たとえば,一人の身長と体重をペアで表現する時に,複数の人の身長と体 重のデータはペアの配列と対応づけることができる.前回,整数の配列を整列 (sort) したように,ペアの 配列も整列することができる.標準では,第一要素が異なれば第一要素で順序が決まり,第一要素が同じ 時には第二要素が比較される. C++ 28 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Ruby 3.1.4 1 2 3 4 5 6 7 8 9 10 11 #include <utility> // pair のため #include <algorithm> // sort のため #include <iostream> using namespace std; int main() { pair<int,int> a[3]; // 整数のペア a[0] = make_pair(170,60); a[1] = make_pair(180,90); a[2] = make_pair(170,65); for (int i=0; i<3; ++i) // a[0] から a[2] まで表示 cout << a[i].first << ’ ’ << a[i].second << endl; // 170 60 // 180 90 // 170 65 と表示されるはず sort(a, a+3); // a[0] から a[2] まで整列 for (int i=0; i<3; ++i) // a[0] から a[2] まで表示 cout << a[i].first << ’ ’ << a[i].second << endl; // 170 60 // 170 65 // 180 90 と表示されるはず } a a a a = []; << [3,5] << [2,9] << [3,6] p a # [[3, 5], [2, 9], [3, 6]] と表示 p a[0] # [3, 5] と表示 a.sort! p.a # [[2, 9], [3, 5], [3, 6]] と表示 大きい順に整列 標準の sort 関数は小さい順に並べ替える.大きい順に並べ替えるにはどのようにすれば良いだろうか? 1. 小さい順に並べ替えた後に,並び順を逆転させる (当面これで十分) C++ 1 2 3 4 5 6 int A[5] = {3,5,1,2,4}; int main() { sort(A,A+5); reverse(A,A+5); // 与えられた範囲を逆順に並び替え ... // cout に A を出力してみよう } Ruby 29 1 2 3 4 a = [3,5,1,2,4] a.sort! a.reverse! # a を逆順に並び替え p a 2. 各要素を負にした配列を整列する C++ 1 2 3 4 5 6 int A[5] = {3,5,1,2,4}, B[5]; int main() { for (int i=0; i<5; ++i) B[i] = -A[i]; sort(B,B+5); ... // cout に-B[i] を出力してみよう } 3. 比較関数を渡す C++11 1 2 3 4 5 int A[5] = {3,5,1,2,4}; int main() { sort(A,A+5,[](int p, int q){ return p > q; }); ... // cout に A[i] を出力してみよう } 文法の説明は省略するが [](int p, int q) return p > q; の部分が 2 つの整数の並び順を 判定する匿名関数である. 貪欲法 3.2 問題 カントリーロード (UTPC 2008) カントリーロードと呼ばれるまっすぐな道に沿って, 家がまばらに建っている. 指定され た数までの発電機と電線を使って全ての家に給電したい.家に電気が供給されるにはどれかの 発電機に電線を介してつながっていなければならず, 電線には長さに比例するコストが発生す る. できるだけ電線の長さの総計が短くなるような発電機 および電線の配置を求める. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2104 ヒント: 発電機が一つなら,端から端まで全ての家を電線でつなぐ必要がある.発電機が 2 つなら,端 から端まで全ての家を電線でつないだ状態から,家と家の間の一箇所に電線を引かずに節約することがで きる.つまり,一番長い一箇所を節約するのが得.発電機が 3 つなら,端から端まで全ての家を電線でつ ないだ状態から,家と家の間で一番長い部分と二番目に長い部分には電線を引かずに節約することができ る.... 回答例 C++ 30 1 2 3 4 5 6 7 8 9 10 11 int T, N, K, X[100000+10], A[100000+10]; int main() { cin >> T; // データセットの数を読み込む for (int t=0; t<T; ++t) { ... // データセットの数を読み込む for (int i=0; i<N; ++i) cin >> X[i]; // 家の位置を入力 for (int i=0; i+1<N; ++i) A[i] = X[i+1]-X[i]; // 家と家の間 ... // 配列 A を整列する ... // 配列 A の先頭 max(0,N-1-(K-1)) 個の和を出力する } } 問題 Stripies (Northeastern Europe 2001) √ 質量が m1 , m2 である 2 体合体すると,2 m1 · m2 となる種族がある.初期状態を与えられ るので,全てが合体した時の最小の質量を求めよ. http://poj.org/problem?id=1862 小数点以下 3 桁を出力するには printf("%.3f\n", ret); を用いる. 問題 (模擬国内予選 2008) Princess’s Marriage 護衛を上手に雇って,道中に襲われる人数の期待値を最小化する.護衛は 1 単位距離あたり 1 の金額で,予算がある限り,自由に雇える. 入力は,区間の数 N, 予算 M に続いて,距離と 1 単位距離あたりの襲撃回数の期待値のペア ⟨D,P⟩ が N 個. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2019 考え方: せっかく護衛を雇うなら,もっとも危険な区間を守ってもらうのが良い.一番危険な道を選び, 予算がある限りそこに護衛を雇う.予算の残額で道の区間全てをカバーできない場合は,安全になる区間 と,危険なままの区間に道が分かれる.予算が残っている限り,2 番目に危険な道,3 番目に危険な道の順 に同様に繰り返す.残った危険な区間について,期待値の和が答えとなる.この計算課程では,⟨ 危険度, 長さ ⟩ のペアで道を表現し,危険な順に整列しておくと便利である. (ここで危険度は,距離 1 移動する間 に襲われる回数の期待値を表す) 回答例 (入力と整列): C++ 1 2 3 4 5 6 7 8 9 10 11 int N, M; pair<int,int> PD[10010]; int main() { while (cin >> N >> M && N) { int d, p; for (int i=0; i<N; ++i) { cin >> d >> p; PD[i] = make_pair(p, d); // PD[i].first は道 i の危険度 // PD[i].second は道 i の長さ } 31 Ruby ... // PD を大きい順に整列しよう // 整列がうまく行ったか,PD を表示してみよう // うまく整列できたら,次は答えを計算しよう 12 13 14 15 16 } 1 2 3 4 5 6 7 8 9 10 11 12 while line = gets pN, pM = line.split(" ").map{|s| s.to_i} break if pN == 0 pd = [] # <p,d>をしまう配列 (1..pN).each { # pN 回何かする d, p = gets.split(" ").map{|s| s.to_i} pd << [p, d] } # pd を大きい順に並べてみよう # pd を出力してみよう # 並べ替えがうまく行っていたら答えを計算してみよう end } 回答例 (答えの計算): C++ 1 2 3 4 5 6 7 8 9 10 11 int S = 0; for (int i=0; i<N; ++i) S += 道 [i] の 危 険 度 * 道 [i] の 長 さ ; // 予算 0 の時の答えが,現在の S の値 for (int i=0; i<N; ++i) { if (M <= 0) break; int guarded = M と 道 [i] の 長 さ の 小 さ い 方 ; // 雇う区間 S -= 道 [i] の 危 険 度 * guarded; M -= guarded; } S が答え • 予算が 0 の場合は,答えとして必要な “刺客に襲われる回数の期待値” である S は,各道 i について ∑ S = i Pi · Di となる. • 予算が M の場合,S から護衛を雇えた区間だけ期待値を減らす.例えば道 j の区間全部で護衛を雇 うなら Pj · Dj だけ S を減らすことができる.もし残り予算 m が道の長さ Dj より小さく道の一部 だけしか護衛を雇えないなら S から減らせるのは Pj · m だけである. 問題 Cleaning Shifts (USACO 2004 December Silver) 連続する T 日の掃除当番を決めたい.各「牛」は何日目から何日目まで (境界を含む) 区間働 けることがわかっている.当番が不在の日の内容に牛を配置するとき,最低何頭必要か答えよ. 不可能な場合は-1 を出力せよ. http://poj.org/problem?id=2376 ヒント: 当番に穴を空けない中で,もっとも遅くまで担当できる牛を採用してゆく. 32 t best day 回答例 (入出力) C++ 1 2 3 4 5 6 7 8 9 10 11 int /*牛の総数*/N, /*掃除日数*/T; pair<int,int> C[25010]; // C[i].first が担当開始日,C[i].second が担当終了 日 int solve() { // N,T,C を元に回答を計算する } int main() { scanf("%d%d", &N, &T); for (int i=0; i<N; ++i) scanf("%d%d", &C[i].first, &C[i].second); printf("%d\n", solve()); } 回答例 (計算) C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int solve() { sort(C, C+N); // 掃除開始日の早い順に整列 int /*牛番号*/i = 0, /*次の担当の掃除開始日*/t = 1, /*採用数*/c = 0; while (i<N && t<=T) { int best = 0; // 次の雇う予定の牛の担当終了日 while (i<N && 牛 i の 担 当 開 始 が t よ り 遅 く な ら な い 間 ) { ... // 牛 i の担当終了が best より遅ければ best を更新 ++i; } if ( 担 当 可 能 な 牛 が 一 頭 も 居 な け れ ば (best<t)) return -1; t = best+1; ++c; } return /* T ま で 掃 除 終 了 し て い る か ?*/ ? c : -1; } 戦略: t − 1 日目まで掃除が終わっていたとする.t 日目を含む区間に掃除をできる牛がいなければ,解 はない (-1 を出力).もし複数の牛が t 日目を担当可能であれば,なるべく長く担当してもらえる牛を雇う のが良い (*).その牛が s 日まで担当したとすると,t = s + 1 として,全体の区間が終わるまで,牛を雇 い続ける. 他の牛を雇っても最適解になる可能性はあるが,(*) の戦略をとった場合と比べて,雇う牛の数を減らす ことはできないことが証明できる. 問題 Radar Installation (Beijing 2002) 全ての島が見えるようにレーダーを配置する http://acm.pku.edu.cn/JudgeOnline/problem?id=1328 ヒント: 33 • 島が観測できるレーダーの区間を列挙したとする.まだレーダにカバーされていない島を一番沢山カ バーできる場所にレーダを配置し,それを繰り返すという戦略を考える.この戦略が最適な配置より 多くのレーダを必要とするような島の並びの例を示せ • 左から順にレーダを配置するとする.(まだレーダにカバーされていないなかで) 見えはじめるのが最 も左にある島を選び,その島が見える区間の左端にレーダを配置するとする.この戦略が最適な配置 より多くのレーダを必要とするような島の並びの例を示せ • 左から順にレーダを配置するとする.(まだレーダにカバーされていないなかで) 見えはじめるのが最 も左にある島を選び,その島が見える区間の右端にレーダを配置するとする.この戦略が最適な配置 より多くのレーダを必要とするような島の並びの例を示せ • 左から順にレーダを配置する正しい戦略 A を示し,他のどのような配置も戦略 A による配置よりレー ダ数が小さくならないことを背理法で証明せよ 問題 Fox and Card Game (Codeforces 388 C) 先手は山の一番上からカードをとり,後手は一番下から取る時,それぞれが最善を尽くした 時の得点を求める. FoxandCardGame 問題 (模擬国内予選 2005) Make Purse Light 財布の中身を軽くする http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2007 34 第 4 章 動的計画法 (1) 概要 全ての可能性を調べ尽くすことが難しいような問題も,小さな問題を予め解いて解を表に覚えておく などの整理を適切に行うことで簡単に解けるようになる場合もある.大小の問題の関係を表す式を立 てて,それをプログラムにしてみよう.動的計画法は,問題が持つ部分構造最適性を利用して効率の 良い計算を実現する方法である. 経路を数える 4.1 例題 (PC 甲子園 2007) Kannondou 階段を1足で 1,2,3 段上がることができる人が,n (< 30) 段登るときの登り方の数を,適当 に整形して出力する. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0168 各段への登り方の数を配列で表現する.漸化式: i 段に居るために Ai 通りの登り方があるとする.登る 前は A0 = 0, 答えは An 1 A i−1 Ai = Ai−1 + Ai−2 A +A +A i−1 i−2 回答例 (数の計算) C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 #include <algorithm> #include <iostream> using namespace std; int A[128], N; int main() { A[0] = 1; for (int i=1; i<=32; ++i) { A[i] = A[i-1]; if (...) A[i] += A[i-2]; if (...) A[i] += A[i-3]; } // A[.] を適当に出力してみる } Ruby 35 i−3 i=0 i=1 i=2 (otherwise) (4.1) 1 2 3 4 5 6 7 8 a = Array.new(32) a[0] = 1 (1..32).each{|i| a[i] = a[i-1] a[i] += a[i-2] if ... a[i] += a[i-3] if ... } p a 回答例 (入出力) C++ 1 2 while (cin >> N && N) cout << ((A[N]+..)/10+...)/365 << endl; 整数で切り上げるには,double で計算して ceil を使うか,割る前に除数-1 を足せば良い. Ruby 1 2 3 4 5 while true n = gets.to_i break if n == 0 puts ((a[n]+9)/10+364)/365 end 問題 平安京ウォーキング (UTPC2009) グリッド上の街をスタートからゴールまで,(ゴールに近づく方向にのみ歩く条件で) 到達す る経路を数える.ただし,マタタビの落ちている道は通れない. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2186 なお,マタタビがなければ組み合わせ (目的地までに歩く縦横の道路の合計から縦の道路をいつ使うか) の考え方から,直ちに計算可能である. マタタビがある場合は,交差点毎に到達可能な経路の数を数えてゆくのが自然な解法で,サンプル入力 3 つめの状況は,下の図のようになる. 0, 0 1, 0 2, 0 3, 0 4, 0 1 0 0 0 0 0, 1 1, 1 2, 1 3, 1 4, 1 1 1 1 1 1 0, 2 1, 2 2, 2 3, 2 4, 2 1 2 3 4 5 0, 3 1, 3 2, 3 3, 3 4, 3 0 2 5 9 5 交差点の座標と通れる道 (右または下に移動可) 各交差点に到達可能な経路の数 ある交差点に (x, y) に到達可能な数 Tx,y は,そこに一歩で到達できる交差点 (通常は左と上) 全ての値が 定まっていればそれらの和として計算可能である. 36 Tx,y 0 1 0 = Tx−1,y Tx,y−1 T +T x−1,y x,y−1 (x, y) が範囲外 (x, y) = (0, 0) 上にも左にもマタタビ 上のみにマタタビ 左のみにマタタビ 上にも左にもマタタビなし マタタビの入力が多少冗長な形式で与えられるので,以下のように前処理して,移動不可能なことを示 す配列などに格納しておくと使い勝手が良い. • x 座標が同じ – (x1, max(y1,y2)) には上から移動不可 • y 座標が同じ – ((max(x1,x2), y1) には左から移動不可 入力例 Ruby 4.2 1 2 3 4 5 6 7 8 9 10 11 dataset = gets.to_i dataset.times { gx, gy = gets.split(" ").map(&:to_i) p [gx,gy] matatabi = gets.to_i # 問題文では変数 p を使っているが,表示コマンドの p と名前を分けた (0..matatabi-1).each{|i| x1,y1, x2,y2 = gets.split(" ").map(&:to_i) p [x1,y1, x2,y2] } } 最適経路を求めて復元する 問題 Spiderman (Tehran 2003 Preliminary) 指定の高さ H[i] だけ登るか降りるかを繰り返すトレーニングメニューを消化して地面に戻る. メニュー中で必要になる最大の高さを最小化する. http://poj.org/problem?id=2397 この問題では,合計値ではなく最小値が必要とされる. i 番目の上下移動を Hi (i は 0 から),i 番目のビルで高さ h で居るために必要な最小コスト (=経路中の最 大高さ) を Ti [h] とする.それらの値を T0 [0] = 0(スタート時は地上に居るので), 他を ∞ で初期化した後, 隣のビルとの関係から Ti と Ti+1 を順次計算する. { max(Ti [h − Hi ], h) · · · i 番目のビルから登った場合 (h ≥ Hi ) Ti+1 [h] = min Ti [h + Hi ] · · · 同降りた場合 ゴール地点を M として,ゴールでは地上に居るので,TM [0] が最小値を与える. 37 地面に,めりこむことはないので,非負の高さのみを考える.また登り過ぎる とゴール地点に降りられなくなるので適当な高さまで考えれば良い. さらに,この問題では最小値だけではなく最小値を与える経路が要求される.ど ちらかの方法で求められる: 1. 最小値 TM [0] を求めた後,ゴールから順にスタートに戻る.隣のビルから 登ったか降りたかは,Ti と Ti+1 の関係から分かる.(両方同じ値ならどちらでも良い). 2. Ti+1 [h] を更新する際に,登ったか降りたかを Ui+1 [h] に記録しておく.TM [0] を求めた後に,UM [0] から順に UM −1 [H0 ] までたどる 4.3 さまざまな動的計画法 最長共通部分列と編集距離 文字列などデータ列から,一部の要素を抜き出して並べた列 (あるいは一部 の要素を消して詰めた列) を部分列という.二つのデータ列に対してどちらの部分列にもなっている列を共 通部分列という.共通部分列の中で長さが最大の列を最長共通部分列と言う.最長共通部分列は複数存在 する場合がある. 問題 Dynamic Programming - Longest Common Subsequence (AOJ) 最長共通部分列の長さを求めよ http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ALDS1_ 10_C 考え方: 文字列 X の先頭 i 文字部分を切り出した部分列を Xi と表す.X0 は空文字列とする.文字列 X と文字列 Y の最長共通部分列は,その部分問題である Xi と Yj の最長共通部分列の長さ Li,j を利用して 求めることが出来る. Li,j 0 = 1 + Li−1,j−1 max(Li,j−1 , Li−1,j ) i ≤ 0 または j ≤ 0 X の i 文字目と Y の j 文字目が同じ文字 otherwise この計算は二次元配列を利用して,効率よく行うことが出来る.情報科学の「パターン認識」の章や参考 書 攻略 [2, pp. 253–] を参照. 問題 Combinatorial - Edit Distance (Levenshtein Distance) (AOJ) 二つの文字列の編集距離を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=DPL_1_E& lang=jp ナップサック問題 価値と重さを持つ宝物がいくつかあるので,ナップサックの制限 (運べる重さの総合) を超えないように価値が高いものを選びたい.なお,液体のように自由に量を調整できる場合は,重さあ たりの価値が最も高いものから貪欲に選べば良い. 38 問題 Combinatorial - 0-1 Knapsack Problem (AOJ) 各品物を最大 1 つまで選択できる 0-1 ナップザック問題で,制約を満たす価値の合計の最大 値を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=DPL_1_B& lang=jp 容量が整数で比較的小規模 (配列に確保可能) な場合は,以下の方法が有力であ る.まず品物に通し番号をつける (並び順は何でも良い).続いて,部分問題とし て,品物 i までしかない世界で,ナップサックの重さ制限が c だった場合の,運 べる価値の合計の最大値 Vi,c を考える. { 0 Vi,c = max(価値 i + Vi−1,c−重さ j , Vi−1,c ) i ≤ 0 または c ≤ 0 Vi,c は,i 番目の品物を選ぶ場合と選ばない場合の最大値で,それぞれ Vi−1,∗ を用いて表現される.この計 算は二次元配列を利用して,効率よく行うことが出来る.参考書 攻略 [2, pp. 416–] 参照. 問題 Combinatorial - Knapsack Problem (AOJ) 各品物をいくつでも選択できるナップザック問題で,制約を満たす価値の合計の最大値を求 めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=DPL_1_C& lang=jp ほぼ同様の考え方になるが,漸化式が少し変化する. 類題として,個数制約付きナップサック問題参考書 [3, p. 302] も参照. 問題 Combinatorial - Longest Increasing Subsequence (AOJ) 最長増加部分列 (Longest Increasing Subsequence) を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=DPL_1_D& lang=jp 最長増加部分列 4.4 問題末尾の解説や参考書 攻略 [2, pp. 421–] 参照. 練習問題 39 問題 (アジア地区予選 2007) Minimal Backgammon 直線上のすごろくでゴールできる確率を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1277 問題 Coin Changing Problem (AOJ) ぴったり支払うときの,コインの最小枚数を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=DPL_1_A& lang=jp 注: 日本のコインは,大きなコインの額を小さいコインが割り切るという性質があるため,大きなコイン を使える限り使うほうが良い.つまり,支払額が 500 円を超えていれば,500 円玉を使うのが良い.一方, この問題はそうでない状況も取り扱う.150 円玉,100 円玉,1 円玉という硬貨のシステムで 200 円払うと きには,150 円玉を使うと枚数が最小ではない.参考書 攻略 [2, pp. 412–] 参照. 問題 (模擬地区予選 2009) Eleven Lover⋆ 数字列中の 11 の倍数を数える http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2182 方針例: 左からヒトケタづつ見てゆく.i 桁目に注目するとして,i 桁目で終わる数で 11 で剰余を取ると 0 ≤ j < 11 になる数の個数を Ai,j とおく.Ai,. を Ai−1,. と i 桁目の数値から計算する. 問題 (アジア地区予選 2013) Restore Calculation⋆ ?を含む,二つの整数とその加算結果が与えられる.演算の整合性を満たすような?の埋め方 が何通りあるか求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2566 問題 (夏合宿 2009) Magic Slayer⋆ 単体攻撃の魔法と全体攻撃の魔法を上手に使って,敵を全滅させろ.各魔法には,単体/全体 の区別の他に,使用 MP と与えるダメージ (Damage) が定められている.敵はいくつのダメー ジで倒されるかとして HP の数値が与えられる. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2156 解説: http://acm-icpc.aitea.net/index.php?plugin=attach&refer=2009%2FPractice% 2F%B2%C6%B9%E7%BD%C9%2F%B9%D6%C9%BE&openfile=2b.pdf 魔法の名前は無視して良い. 40 単体魔法のみで敵が一体の場合を考える.合計でダメージ d を与えるのに必要な MP の最小値を Ad と すると { Ad = d≤0 0 mink (Ad−Damage + MPk ) (4.2) d > 0 最後に魔法 k を使った k 敵 N 体を単体魔法のみで倒す場合は,それぞれの HP に必要な分を合計すれば良い. 全体魔法がある場合は,全体に合計ダメージ d を与えるのに必要な最小な MP を同様に計算し,組み合 わせる. 問題 Mushrooms⋆ (Algorithmic Engagements 2010) 直線を歩いてキノコを採集する. http://main.edu.pl/en/archive/pa/2010/grz scanf 推奨. 問題 輪番停電計画 ⋆ (国内予選 2011) 上手に区間を分割する. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1176& lang=jp 長方形に対する値を管理するタイプ. 問題 (夏合宿 2011) Quest of Merchant⋆ 様々な行動を総合して (問題文参照) 収益を最大化せよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2296 問題 (夏合宿 2012) Hakone⋆⋆ (箱根駅伝の) 順位変動 (このチームは順位をあげて 5 位で通過などの情報) から,前の中継所 での順位として何通り考えられるか求める. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2439 問題 Ploughing⋆⋆ (13th Polish Olympiad in Informatics) 畑を,縦 (幅 1 列) か横 (1 行) にスパっと切りとることを繰り返して,区分けする.どの範囲 も数の合計が K 以下になるようにする.条件を満たす最小の分割数を求めよ. http://main.edu.pl/en/archive/oi/13/ork 41 問題 Barricades (Algorithmic Engagements 2007) 問題: 道路の一部をバリケードで塞いで連結な k 都市を他の都市から侵入不能にしたい。 http://main.edu.pl/en/archive/pa/2007/bar 42 第 5 章 分割統治 (1) 概要 分割統治法は,問題を小さな問題に再帰的に分解してそれぞれを解いたうえで,順次それらを組み合 わせて全体の解を得る技法を指す.元の問題より小さな問題を扱う点は動的計画法と共通だが,分割 統治では分割した問題間に重なりがない場合を扱う.たとえば二分探索は分割統治の一つの手法であ るが,フィボナッチ数の計算は分割統治とは通常呼ばれない. この章では、アイ (i) とジェイ (j)、イチ (1) とエル (l) が特に紛らわしいので注意のこと. 5.1 二分探索 問題を半分に分割して,片方のみを扱えば十分な場合として二分探索をみてみよう.例として,辞書や 名簿のように順番に並んでいるデータ列にある要素が存在するかどうかを判定する場合を考える.たとえ ば “Muller” という姓を,全体が 1024 ページの名簿から探す場合に,1024 ページの真ん中の 512 ページか ら始める.そのページが “M” より後なら,前を調べる. “M” 以前なら,後を調べる.いずれの場合でも, 探す範囲を半分に絞ることが出来るといった具合である. この探し方は,1 ページ目から順に 2,3 ページと一ページづつ最終ページまで名簿を探すより,速い.名 簿のページ数を n とすると,調べるページ数は O(n) と O(log(n)) の差がある. なお,次の節で紹介するマージソートや inversion count では,分割後に両方の領域について処理 する必要がある. 5.1.1 データ列中の値の検索 C++の標準ライブラリに収められている binary search は整列済の配列から二分探索により要素の有 無を判定する. C++ 1 2 3 4 5 6 7 8 #include <iostream> #include <algorithm> sort(S, S+L); // 追加: 配列 S 内を昇順に並び替えておく ... if (binary_search(S, S+L, a))) { // a が S 内にある } 配列 A[] から value を探す場合を多少形式的に書くと 1. 調べる区間を [l, r) とする (範囲を表すには,left, right あるいは first, last などがしばしば用いられる). 0 ページ目から 1023 ページ目までを探す場合は,初期状態として l=0, r=1024 とする 43 2. l+n <= r となったら,探す範囲は最大で n ページしか残っていないので,[l,r) の範囲を順に探 す.(典型的には n==1, 実用的には n==10 程度に取る場合もある.) 3. 区間の中央を求める m=(l+r)/2 4. value < A[m] なら (次は前半 [l,m) を探したいので)r=m,そうでなければ (A[m] <= value す なわち A[m]==value の場合を含む) 後半 [m,r) を探したいので l=m として 2 へ.(ここで m==l または m==r の場合は無限ループとなる.そうならないことを確認する.) というような処理となる. C++ 1 2 3 4 5 6 7 8 bool bsearch(const int array[], int left, int right, int value) { while (left + 1 < right) { int med = (left+right)/2; if (array[med] > value) right = med; else left = med; } return left < right && array[left] == value; } 自分で二分探索を実装したら,Search II で動作を確認することができる. 例題 Search II (AOJ) 整数の配列 S(売りたいリスト) と T(買いたいリスト) が与えられる.T に含まれる整数の中で S にも含まれる個数を出力せよ. (入力の範囲)S の要素数は 10 万以内,T は 5 万以内.S と T の要素数はそれぞれ 100 以内. 配列に含まれる要素は,0 以上 107 以下. (注意)T の要素は互いに異なるが,S の要素は重複がある場合がある. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=10031 制約を満たす最小値を求める 5.1.2 配列から値を探す状況以外にも,二分探索の考え方を用いると綺麗に解ける問題もある. 例題 Search - Allocation (AOJ) 条件に従って指定の荷物を全て詰めるような,トラックの最大積載量を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ALDS1_4_ D ヒント: トラックの最大積載量 P として二分探索し,全ての荷物を積める最小値を求める.積みきれる 最小値がほしいので, 「積みきれない最大値」が存在する範囲を [l,h) で表現し,上限 h を減らす条件を ok として表現する.求める答えは h となる. C++ 44 1 2 3 4 5 6 7 8 9 10 11 12 bool ok(int P) { ... // 最大積載量 P のトラックに前から順に積み込んだ時の必要台数が K 以内かを返す } int main() { // 問題読み込み int l = 0, h = // 大きな数; while (l+1 < h) { int m = (l+h)/2; if (ok(m)) h = m; else l = m; } printf("%d\n", l); } 関数 ok は,たとえば以下のように作成できる: • 現在のトラックに積んだ重さ (初期値 0) とトラック台数 (初期値 1) を表す変数を作成 • 各荷物について,現在のトラックに積んで最大積載量を超えないならそのまま積む (現在のトラック に積んだ重さを増やす)/そうでなければ新しいトラックに積む (トラックの台数を増やして,現在の トラックに積んだ重さをその荷物に設定) • 最後に台数を K と比較 問題 Aggressive Cows⋆ (USACO 2005 February Gold) 直線上に N 棟の牛舎 (個室) がある.C 頭の牛を,なるべく互いを離して入れたい. (cin の代わりに scanf を使ってください) http://poj.org/problem?id=2456 今回は条件を満たす最大値がほしいので,下限 l を増やせる条件を関数 ok として表現する.求める答 えは l となる.あらかじめ小屋の候補を昇順に並べてておくと,ok 関数は, • 牛を左端の小屋に配置する (小屋がなければ失敗) • 距離 M 以内の小屋を考慮から消す というステップを牛の数だけ繰り返し,全頭まで配置できれば成功として判定できる. 問題 (模擬地区予選 2009) Water Tank タンクの容量と時間毎の水の使用量が与えられるので,最低限必要な給水速度を求める.(何 周してもジリ貧にならない量が求められている) (同名の別問題もあるので注意) http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2180 ある給水速度を仮定した時に,それで生活できるかどうかを判定する関数を ok とする.求める速度を 含む区間を [lo, hi] で表現し,生活できるか出来ないかで範囲を半分に狭めてゆく.区間が十分狭くなった ら終了して出力する.考え方と効果は二分探索 (binary search) とほぼ同様だが,連続区間を扱う場合は二 分法 (bisection) と呼ばれることが多い. 45 C++ double lo = sum/86400, hi = 1e6; // 1e6 = 106 while (hi-lo>1e-6) { double m = (hi+lo)/2.0; if (ok(m)) hi = m; else lo = m; } printf("%.10f\n", lo); 1 2 3 4 5 6 7 原理は単純なのだが,関数 ok の実装においてタンクの最大容量以上は水を貯められないなどの処理で 慎重さが必要な点があるので,次の節の inversion count の問題のほうが易しいかもしれない. 5.2 Merge Sort 分割統治の例として,Merge Sort という整列手法を取り上げる.この手法は,与えられた配列を半分に分 割し,また元の大きさに組み立てる際に要素を整列する.図は,8,1,4,3,2,5,7,6 という配列を整列する際の 処理の流れを示している.上半分で分割し,下半分で整列が行われる.赤字は併合の際に右側から来た要素 を示す.具体的なソースコードは,次に紹介する merge and count とほぼ同様なので省略する.配列の 要素数を N とすると,行が O(log N ) あり,各行あたりで必要な計算が O(N ) なので,全体で O(N log N ) という計算量が導かれる. 8,1,4,3,2,5,7,6 8,1,4,3 8,1 8 1 2,5,7,6 4,3 3 4 1,8 2,5 2,5 1,3,4,8 7 5 2 3,4 7,6 6 6,7 2,5,6,7 1,2,3,4,5,6,7,8 5.2.1 Inversion Count 配列内の要素のペアで,A[i] > A[j] (i<j) のものを数えたい.例えば配列 [3 1 2] の中には (3,1) と (3,2) の二つのペアの大小関係が逆転している.愚直に次のようなコードを書くと,要素数 N の自乗に 比例する時間がかかる O(N 2 ). C++ 46 1 2 3 4 5 6 7 8 9 10 int N, A[128]; int solve() { int sum = 0; for (int i=0; i<N; i++) { for (int j=i+1; j<N; j++) { if (A[i] > A[j]) ++sum; } } return sum; } Merge sort の応用で,半分に分割しながら数えると,O(N log N ) で求めることができる. C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int N, A[ た く さ ん ]; // A は元の配列 int W[ た く さ ん ]; // W は作業用配列 int merge_and_count(int l, int r) { // range [l,r) if (l+1 >= r) return 0; // empty if (l+2 == r) { // [l,r) == [l,l+1] 要素 2 つだけ if (A[l] <= A[l+1]) return 0; // 逆転はなし swap(A[l], A[l+1]); return 1; // 逆転一つ } int m = (l+r)/2; // [l,r) == [l,m) + [m,r) int cl = merge_and_count(l, m); // 左半分を再帰的に数える int cr = merge_and_count(m, r); // 右半分を再帰的に数える int c = 0; // 左と右を混ぜるときの逆点数 int i=l, j=m; // i は [l,m) を動き, j は [m,r) を動いて, int k=l; // 小さいものから W[k] に書き込む while (i<m && j<r) { // A[i] と A[j] を比べながら進む if (A[i] <= A[j]) W[k++] = A[i++]; // 左半分の方が小さく逆転なし else { W[k++] = A[j++]; c += XXX; // 左半分の方が大きい,左半分で未処理の要素だけ飛び越える } } while (i<m) W[k++] = A[i++]; // 左半分が余った場合 while (j<r) W[k++] = A[j++]; // 右半分が余った場合 assert(k == r); copy(W+l, W+r, A+l); return cl + cr + c; } 練習問題 5.2.2 問題 Recursion / Divide and Conquer - The Number of Inversions (AOJ) 与えられた数列内の,大小関係が逆転しているペアの個数を求める http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ALDS1_5_ D 47 問題 Ultra-QuickSort (Waterloo local 2005.02.05) 類題: 数が大きいので long long を使うこと. http://poj.org/problem?id=2299 問題 Japan⋆ (Southeastern Europe 2006) 長方形の島の東西に道路が走っている.交わる箇所の数を求めよ http://poj.org/problem?id=3067 解き方の例: (東, 西) のペアでソートすると前 2 問と同じ問題になる.この問題は,累積和を管理しても 解ける. 5.3 木のたどり方 D1,7,13 B2,4,6 A3 E8,12 C5 G9,11 nil F10 nil 例 1: 数字は訪問順序を表す 根から始めて,下のような再帰的な手続き (深さ優先探索,10 章) で,辺をたどりながら各頂点を一巡す ることを考える: 1. 左の子が存在し,かつ未訪問なら,左の子を訪問する 2. (そうではなくて) 右の子が存在し,かつ未訪問なら,右の子を訪問する 3. (そうではなくて) 親があれば,親へ戻る 4. いずれでもなければ,(根に帰ってきたので) 終了 図の「例 1」の木であれば,DBABCBDEGFGED と通る.例から分かるように,子供をもつ頂点は複数 回 (正確には次数) 通過する. この訪問順を基本としたうえで,各頂点を一度だけ処理する方法として,preorder (自分, 左, 右), inorder (左, 自分, 右), postorder (左, 右, 自分) などが用いられる.それぞれの方法では,子供と自分の優先順位が異 なる. 48 DBABCBDEGFGED preorder DBA C inorder postorder 問題 EGF ABC DE FG A CB FGED Tree - Reconstruction of a Tree (AOJ) ある二分木について, preorder (root, left, right) で出力した頂点のリストと,inorder (left, root, right) で出力したリストが与えられる.(元の木を復元し) その頂点を postorder (left, right, root) で出力せよ.なお,元の木で,異なる頂点には異なるラベルがついている. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ALDS1_7_ D 問題 Tree Recovery⋆ (Ulm Local 1997) 上に同じ (複数ケースが与えられる点と,書式が少し異なる) http://poj.org/problem?id=2255 ヒント: • ルートを A, その左側の部分木を L (null の場合もある), 右部分を R とする.preorder では,AL’R’ の ように並んでいる.(L’ と R’ はそれぞれの preorder 表記). inorder では,L”AR” のように並ぶ (L” と R” はそれぞれの inorder 表記) • preorder 表記の先頭から直ちに,ルート A が特定できる • L, R 部分は,inorder 表記から A を探すことによって,要素を特定できる • 文字数は,L’ と L”, R’ と R” で等しいことに注意 • L’ と L” から,左側の木を復元する問題は,元の問題の小さくなったバージョンである. 回答例: C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <iostream> #include <string> using namespace std; string preorder, inorder; // preorder の [fp,lp) の範囲と,inorder の [fi,li) の範囲について // 木を postorder で表示 void recover(int fp, int lp, int fi, int li) { int root; // preorder[fp] == inorder[root] となるような root を求める if // (左側が存在すれば) recover(fp+1, fp+(root-fi)+1, fi, root); // 左側を表示 if // (右側が存在すれば) recover(fp+(root-fi)+1, lp, root+1, li); // 右側を表示 cout << inorder[root]; // root を表示 } int main() { 49 17 18 19 20 21 while (cin >> preorder >> inorder) { recover(0, preorder.size(), 0, inorder.size()); cout << endl; } } 呼び出し関係を図にすると,以下のようになる.図中,赤字が (その時点での部分木における) 根を表す. 頂点内の文字列は,その頂点以下の部分木に対する,preorder 表記 (上段, fp と lp の範囲) と inorder 表記 (下段, fi と li の指す範囲). DBACEGF ABCDEFG BAC ABC A EGF EFG C GF FG nil F 5.4 nil 空間充填曲線 問題 Recursion / Divide and Conquer - Koch Curve (AOJ) コッホ曲線の頂点を計算せよ. http://judge.u-aizu.ac.jp/onlinejudge/ description.jsp?id=ALDS1_5_C&lang=jp 問題 (世界大会 2003) Riding the Bus⋆⋆ 正方形内に描かれた Peano curve 上に格子点がある.与えられた二点間の距離を求めよ.距 離とは,与えられた点から最短の格子点への距離 (複数ある場合は x,y 座標が小さいものまで) と,格子点間の Peano curve 上の道のり.(誤差の記述がちょっと心配) https://icpcarchive.ecs.baylor.edu/index.php?option=com_ onlinejudge&Itemid=8&category=37&page=show_problem&problem=724 解き方の例: 全体を 9 分割して,関係ある場所だけを細かく探す. 50 問題 Plotter⋆⋆ (Algorithmic Engagements 2011) 与えられた点をいつペンが通るか. http://main.edu.pl/en/archive/pa/2011/plo 51 第 6 章 基本データ構造 (string, stack, queue, string, set, map) 概要 この章では,C++や Java など多くの言語で標準ライブラリとして提供されている「道具」として,文 字列,スタック,(優先度付き) キュー,集合,連想配列を紹介する.道具の紹介が退屈な読者は,一 旦飛ばして先に進んでおいて,あとで必要になってから戻ってくる読み方も可能である.なお,この 資料では,まず標準ライブラリを使いこなすことを勧める立場を取る.これらの道具を自作すること も筋力トレーニングとしては有用であるが初学者には向かず,また実用的な状況では標準ライブラリ を使う方が望ましいためである.標準ライブラリの利用には,多くの人に使われているのでバグがな いことが期待される,他の人も性質を良く知っているため共同作業に向いている,などの利点がある. 6.1 C++ 文字列 (string) と入出力 1 2 3 4 5 6 7 8 #include <iostream> #include <string> using namespace std; int main() { string word; // 文字列型の変数 word を定義 cin >> word; // 標準入力から一単語読み込む (空白や改行文字で区切られる) cout << word << word << endl; // 2 回表示する } hello(改行) と入力すると hellohello と出力される. C++の string クラスは,C の文字列よりもかなり便利なので,早めに慣れておくことをお勧めする. C++ 1 2 3 4 5 6 7 8 9 10 Ruby 1 2 #include <string> string word; // 定義 string word2="ABCD"; // 定義と同時に初期化 word = "EF"; // 代入 string word3 = word + word2; // 連結 char c = word[n]; // n 文字目を取り出す word[n] = ’K’; // n 文字目に代入 (n<word.size() でないと破綻) cin >> word; // 一単語読み込み (改行や空白文字で分割される) cout << word.size() << endl; // 文字数表示 if (word.find("A") != string::npos) ... // もし word に A が含まれるな ら... word = gets.chomp # gets で一行読み込んで,chomp で行末の改行文字を取り去る puts word+word # 2 つつなげたものを表示する 1 単語読み込むことと 1 行読み込むことは意味が異なるが,今回は違いに踏み込まない. 52 6.2 スタック (stack) とキュー (queue) スタックとキュー (参考書 攻略 [2, pp. 80–], 参考書 [3, pp. 31–]) は,データを一時的に保存したり取り出し たりするためのデータ構造である.どちらもデータを 1 列に並べて管理して,新しいデータを列の (どちら かの) 端に保存し,また取り出す際も (どちらかの) 端から取り出す. スタックは,後に保存したデータを,先に取り出すデータ構造である.たとえ ば,仕事をしている最中に急な案件が発生したので,今取り組んでいる仕事を「仕 事の山」の先頭に積んでおいて,急な案件を処理し,それが終わったら仕事の山の 先頭から再び処理を続けるような状況で有用である.もちろん急な案件の処理中 に,さらに急な案件が発生した場合も同様に仕事の山に積んだり下ろしたりする. キューは,先に保存したデータを,先に取り出すデータ構造である.飲食店に 到着した人は列の後ろに並び,列の中で一番早く到着した人から順に店内に案内 される状況に相当する. 優先度付きキューでは,早く並んだ順ではなく 各データの優先度の高い順に取り出される.現 実にはぴったりした例がないが,待っている人 の中から優先度の高い順に診療が行われるよう な状況や,高い買値を提示した順に品物を入手 できる市場などがもしあれば相当するだろう. なお,C++の標準ライブラリで提供されているでは取り出し操作が,先頭要素の参照 top() あるいは front() と先頭要素の削除 pop() という二つの操作に分離されていることに注意 1 .通常の使用状況で は,先頭の要素を得つつスタックやキューからは取り除きたいので,両者の操作を続けて行う. 6.2.1 スタック スタックに格納したデータは,top() 及び pop() の操作により遅く入れた順に取り出される. C++ Ruby 1 これは 1 2 3 4 5 6 7 8 9 10 11 12 13 14 1 2 3 4 5 #include <stack> #include <iostream> using namespace std; int main() { stack<int> S; // int を格納するスタック S.push(3); // 先頭に追加 S.push(4); S.push(1); while (! S.empty()) { // 要素がある間 int n = S.top(); // 先頭をコピーして S.pop(); // 先頭要素を廃棄 cout << n << endl; // 1, 4, 3 の順に表示される } } stack = [] # (配列を Stack として扱う) stack.unshift(3) # 要素を先頭に追加 stack.unshift(4) stack.unshift(1) while stack.size > 0 # 要素がある間 exception safety のためである. 53 6 7 8 n = stack.shift # 先頭から取り出す p n end 例題 Reverse Polish Notation (AOJ) 「逆ポーランド記法」で与えられる文法を読んで、値を計算する http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ALDS1_3_ A&lang=jp ヒント: 問題分末尾にある「解説」も参照.参考書 攻略 [2, p.82] に詳しい解説が掲載されている. C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <string> #include <stack> #include <iostream> using namespace std; int main() { string word; stack<int> S; while (cin >> word) { // 入力がある限り読み込む if (word == "+") { // 数を 2 つ pop して、和を push する } else if (word == "-") { // 数を 2 つ pop して、差を push する } else if (word == "*") { // 数を 2 つ pop して、積を push する } else { // word を数値にして push する } } // S の先頭要素を表示する。 } このプログラムは入力が続くかぎり読み込む。キーボードから入力の終わりを与えるには “ˆD” (Ctrl キー を押しながら “d” を押す) を用いる。 数をあらわす文字列を整数に変換するには、C++11 の場合は stoi(word) を、そうでない場合は <cstdlib> を include して、atoi(word.c str()) などとする。 6.2.2 キュー キュー (参考書 [3, p. 32]) は,データの格納と取り出しができるデータ構造で,入れた順に取り出される ものである.Ruby では配列をキューとして扱うと便利である. Ruby 1 2 3 4 Q Q Q Q = [] # (配列を Queue として扱う) << 3 # 要素を末尾に追加 << 4 << 1 54 5 6 7 8 9 while Q.size > 0 # 要素がある間 n = Q.shift # 先頭から取り出す p n end # 3, 4, 1 の順に表示される C++では,queue というテンプレートクラスが用意されている.入れた (push した) データを,front() 及 び pop() の操作により取り出す. C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <queue> #include <iostream> using namespace std; int main() { queue<int> Q; // int を格納するキュー Q.push(3); // 末尾に追加 Q.push(4); Q.push(1); while (! Q.empty()) { // 要素がある間 int n = Q.front(); // 先頭をコピーして Q.pop(); // 先頭要素を廃棄 cout << n << endl; // 3, 4, 1 の順に表示される } } C 言語の場合は,大きめの長さの配列と,現在使用中の区間を表す head と tail の二つの添字を用いて,同 等の機能を実現できる. 例題 Round-Robin Scheduling (AOJ) 複数の計算を少しづつ処理する様子をシミュレーションしてみよう。 http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ALDS1_3_ B&lang=jp ヒント: 問題分末尾にある「解説」も参照.参考書 攻略 [2, p.82] に詳しい解説が掲載されている. 問題 Areas on the Cross-Section Diagram (AOJ) 与えられた地形に雨が降った際に,できる水たまりの面積を出力する. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ALDS1_3_ D 問題 Subsequence (Southeastern Europe 2006) 配列 A の連続する部分列について,その和が S 以上となるものので最小の長さを求めよ. http://poj.org/problem?id=3061 いわゆるしゃくとり法 (参考書 [3]pp. 135–137). たぶん,cin だと遅いので,scanf を使う. 55 問題 Sum of Consecutive Prime Numbers (アジア地区予選 2005) 与えられた整数を,連続する素数の和として表せる種類を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1257 初めにエラトステネスの篩 (参考書 [3] 112 ページ) などの手法で,10 000 までの素数を求めておく.あ とは上の問題と同じ. 優先度付きキュー 6.2.3 優先度付きキュー (参考書 [3, p. 32]) は,データの格納と取り出しができるデータ構造で,まだ取り出さ れていない中で大きな順に取り出されるものである. 例題 Priority Queue (AOJ) 整数を入力として受け入れ、大きい順に取り出すことができる、優先度付きキュー (priority queue) を作成せよ。 http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ALDS1_9_ C まずは,STL の priority queue を用いて,accepted を得られることを確認すると良い.自分で優先 度つきキューを実装する場合は,配列または vector 上に二分ヒープ (binary heap) を作成することが簡便. 詳しくは参考書 攻略 [2, 10 章,pp. 232–] を参照. C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <queue> #include <iostream> using namespace std; int main() { priority_queue<int> Q; // int を格納する優先度つきキュー Q.push(3); // 追加 Q.push(4); Q.push(1); while (! Q.empty()) { // 要素がある間 int n = Q.top(); // 先頭をコピーして Q.pop(); // 先頭要素を廃棄 cout << n << endl; // 4, 3, 1 の順に表示される } } Ruby の場合は,今のところオンラインジャッジで使えるライブラリがなさそうなので,自分で二分ヒー プなどを実装する必要がある. Ruby 1 2 3 4 5 6 class PriorityQueue def initialize # 配列で二分木を実現する i 番目の要素の左の子供は 2i+1, 右の子供は 2i+2 # 親の要素は,子供のどちらより優先度が高いとする @array = [] @size = 0 56 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 end def push(a) # 最後に仮おきした後,優先度が先祖より高ければ引き上げる @array[@size] = a heapify_up(@size) @size += 1 end def pop() # 根の要素 (優先度最大) を取り出した後,最後の要素を根に仮置きし,調整 raise unless @size > 0 ans = @array[0] @size -= 1 @array[0] = @array[@size] heapify_down(0) ans end def size @size end def swap(p,q) @array[p], @array[q] = @array[q], @array[p] end def equal_or_better(p,q) (@array[p] <=> @array[q]) <= 0 end def heapify_up(n) while n > 0 parent = (n-1)/2 break if equal_or_better(parent, n) swap(parent,n) n = parent end end def heapify_down(n) while true l, r = n*2+1, n*2+2 break if @size <= l child = l child = r if r < @size && equal_or_better(r, l) break if equal_or_better(n, child) swap(child, n) n = child end end end Q = PriorityQueue.new Q.push([50, 1]); Q.push([20, 2]); Q.push([30, 3]); Q.push([10, 4]); Q.push([80, 5]); while Q.size() > 0 cur = Q.pop(); # 最小の要素を取り出す p cur end 57 6.3 文字列と分割・連結・反転 C++ 1 2 3 4 5 6 7 string word = "hello"; for (size_t i=0; i<=word.size(); ++i) { // size() は文字列の長さ string l = word; l.resize(i); // i 文字目より左の文字列 string r = word.substr(i); // i 文字目以降の文字列 cout << l << ’ ’ << r << endl; } 実行例: hello h ello he llo hel lo hell o hello 練習: string word = "hello"; の hello を別の単語にして動かしてみよう. Ruby 1 2 3 4 5 6 7 8 9 word = "hello" s = word.split("") (0..s.length).each {|l| a = s.first(l) b = s.last(s.length-l) # p a # p b puts a.join("")+" "+b.join("") } 連結には,+オペレータを用いる.C++も ruby も同じ. C++ 1 2 3 4 string a = "AAA"; string b = "BBB"; string c = a+b; // 連結 cout << c << endl; // AAABBB 反転には,reverse という関数を用いる.範囲は word.begin() と word.end() で指定する.配列 の場合と近い記法で,reverse(&word[0], &word[0]+word.size()); と書くこともできる. C++ Ruby 1 2 3 4 #include <algorithm> string word = "hello"; reverse(word.begin(), word.end()); cout << word << endl; 1 2 3 a = "hello" b = a.reverse puts b 58 6.4 集合 C++で集合を表現するために標準ライブラリに set というデータ構造が用意されている.まずは整数 の集合の例を紹介する.様々な操作が set に用意されているが,ここでは挿入 (insert),全体の要素数 (size),指定した要素の数/有無 (count) を使用する. C++ 1 2 3 4 5 6 7 8 9 10 11 12 #include <set> typedef set<int> set_t; set_t A; cout << A.size() << endl; // 初めは空なので 0 cout << A.count(3) << endl; // 3 は含まれていないので 0 A.insert(1); A.insert(3); A.insert(5); A.insert(3); cout << A.size() << endl; // 1,3,5 の 3 つ cout << A.count(3) << endl; // 1 個 (set の場合は最大 1 個) cout << A.count(4) << endl; // 0 個 文字列の集合も同様に使うことができる. C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 #include <set> #include <string> typedef set<string> set_t; set_t A; cout << A.size() << endl; // 初めは空なので 0 cout << A.count("hello") << endl; // "hello"は含まれていないので 0 A.insert("hello"); A.insert("world"); A.insert("good morning"); A.insert("world"); cout << A.size() << endl; // "hello", "good morning", "world" cout << A.count("world") << endl; // 1 個 cout << A.count("hello!!!") << endl; // 0 個 ruby の場合は Hash を用いると同様の機能を得られる.2 挿入には a.insert(element) の代わりに a[element]=1 を,指定した要素の数・有無には a.count(element) の代わりに a[element] を,集 合の要素数は a.size() の代わりに a.length をそれぞれ用いる. Ruby 1 2 3 4 5 6 7 8 9 all = Hash.new(0) puts all.length # 0 all["hello"] = 1 all["world"] = 1 all["good morning"] = 1 all["world"] = 1 puts all["world"] # 1 puts all["hello!!!"] # 0 puts all.length # 3 2 集合も用意されており require ’set’ とすると使えるので,興味のあるものは調べると良い. 59 例題 Search - Dictionary (AOJ) 単語が辞書にある単語かどうかを判定せよ http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ALDS1_4_ C&lang=jp 6.5 連想配列 (map) 個人番号に対する名前を管理する場合には,配列を用いることが一般的である. C++ 1 2 3 string name[30]; name[0] = "kaneko"; name[1] = "fukuda"; では,名前に対する個人番号を管理するにはどうすれば良いか. C++ 1 number["kaneko"] = 0; // こんなふうに書きたい 連想配列とは,key に対する value を管理する抽象データ型で,上記のような機能を提供する.C++で は map, Ruby では Hash が標準で用意されている. 6.5.1 C++ Ruby 文字列に対応する数字 1 2 3 4 5 6 7 8 9 10 11 1 2 3 #include <iostream> #include <string> #include <map> using namespace std; int main() { map<string,int> table; // 文字列を数値に対応させる map table["taro"] = 180; table["hanako"] = 160; cout << table["taro"] << endl; // 180 cout << table["ichirou"] << endl; // 0 } table["hanako"] = 160 puts table["taro"] # 180 puts table["ichirou"] # 0 (*) このような例では,key が文字列,value が整数である. 60 6.5.2 文字列の出現回数 key が存在しない場合の振る舞いは,処理系と設定に依存するが,上記の例では 0 になるように用いて いる.これにより出現回数の計測を簡単に行うことができる. C++ Ruby 6.5.3 C++ 1 2 3 4 1 2 3 4 table["ichirou"]+=1; cout << table["ichirou"] << endl; // 1 table["ichirou"]+=1; cout << table["ichirou"] << endl; // 2 table["ichirou"] += 1 puts table["ichirou"] # 1 table["ichirou"] += 1 puts table["ichirou"] # 2 連想配列の要素の一覧 1 2 3 4 5 6 7 8 9 10 11 12 13 14 map<string,int> phone["taro"] = phone["jiro"] = phone["saburo"] phone; 123; 456; = 789; for (map<string,int>::iterator p=phone.begin(); // i=0 に相当 p!=phone.end(); // i<N に相当 ++p) { cout << p->first << " " << p->second << endl; } // 出力順は // jiro 456 // saburo 789 // taro 123 赤字で書いた部分が定型句であるが,初見では把握が難しいため,そのまま用いてほしい.配列の場合は添字 として整数 i を用いるが,連想配列の場合は複雑な処理があるため,整数の代わりに map<..,..>::iterator という型を用いる (C++11 では,ここは auto と書けるようになる).また初期値と終了値も,0 や N の代 わりに begin() と end() を用いる.ループの中で,各要素の key は p->first, value は p->second で参照する. Ruby 1 2 3 4 5 6 7 8 9 10 11 phone = Hash.new phone["taro"] = 123; phone["jiro"] = 456; phone["saburo"] = 789; phone.each{|key,value| p [key, value] } # ["taro", 123] # ["jiro", 456] # ["saburo", 789] Ruby の場合は,より簡潔な記法が可能である.ループ中に使いたい変数を|key,value|の部分で指定 する. 61 練習問題 6.6 問題 Organize Your Train part II (国内予選 2006) 貨物列車の並べ替え.何通りの可能性があるか? http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1142& lang=jp C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int M; string S; int main() { cin >> M; for (int i=0; i<M; ++i) { cin >> S; ... all; // 文字列の集合 ... // all に S を挿入 for (size_t i=1; i<S.size(); ++i) { string L; ... // L を S の [0,i) 文字に設定 string R; ... // R を S の [i,S.length()) 文字に設定 string L2, R2; ... // L2 を L の逆順に設定 ... // R2 を R の逆順に設定 ... // all に R+L, R2+L, L+R2.. 色々挿入 } ... // all の要素数 (all.size()) を出力 } } 出現頻度 6.6.1 問題 (PC 甲子園 2004) English Sentence 与えられた文字列について,出現頻度が最も高い単語と,最も文字数が多い単語を出力する http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0029& lang=jp 回答例 C++ 1 2 3 4 5 6 7 #include <iostream> #include <string> #include <map> using namespace std; map<string,int> table; int main() { string /*入力用*/word, /*最大頻度*/frequent, /*最長*/longest; 62 8 9 10 11 12 13 14 15 size_t N=0; /*文字列 frequent の頻度 */ while (cin >> word) { table[word] を 一 つ 増 や す table[word] が N より 大 きけ れ ば, N と frequent を , table[word] と word に更新 word.size() が longest.size() よ り 大 き け れ ば longest を word に更新 } frequent と longest を出力 } string 型の文字列 s の長さは s.size() で入手することができる. 文字列と数値の対応 6.6.2 問題 (模擬地区予選 2010) Era Name (架空の) 西暦と (架空の) 元号の対応が与えられるので,整理して記憶する.続いて,西暦が 質問として与えられるので,元号がわかっていれば元号を,そうでなければ “Unknown” と答 える. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2242 問題補足: 入力例の”showa 62 1987”は, • 昭和元年 (1 年) の西暦は,1926 年である • 昭和は,(少なくとも)1987 年まで続いた という二つの情報をあらわす.他に情報がなければ,1988 年が昭和であるかは分からない (昭和と平成の 間に別の元号が使われているかもしれない) ので,“Unknown” と答える. 回答例: 各情報を,西暦とその年が 1 年である元号の対応表と,元号が最大何年まで続いたかの二つに分 けて記録する.前者には map<int,string>を,後者には map<string,int>を用いる. C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int N, Q; int main() { while (cin >> N >> Q && N) { map<int,string> start; // 西暦 Ô その年に始まった元号の名前 map<string,int> finish; // 元号 Ô 何年まで続いたことがわかっ ているか string name; for (int i=0; i<N; ++i) { int local, western; cin >> name >> local >> western; start[...] = name; // western と local から 1 年目の西暦を求める finish[name] = ...; // western の方が finish[name] より遅ければ更新 } for (int i=0; i<Q; ++i) { int western; cin >> western; bool ok = false; for (/* s t a r t を 順 番 に 見 て */) { if (/*開始年が western 以 前 で 終 了 年 が western 以 降 な ら */) { 63 21 22 23 24 25 26 27 28 29 30 cout << /*元号の名前*/ << ’ ’ << /*元号で何年目か*/ << endl; ok = true; break; } } if (! ok) cout << "Unknown\n"; } } } なお,この問題は,終了年と元号の関係を map で管理し,lower bound で検索するほうが効率が良い. 余力があれば試すと良い. 6.6.3 区間の管理 問題 (模擬国内予選 2009) Restrictive Filesystem⋆ 単純な管理を行うファイルシステムでの,読み書きと消去を実装し,各時点で読まれるデー タを答えよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2152 回答例: ディスクの状態を,区間 pair<int,int>とファイルの ID を対応させる連想配列 map<pair<int,int>, int>で表現して,読み書きと消去をシミュレートする. 6.6.4 集合の応用 問題 Sweets⋆ (Algorithmic Engagements 2010) 正の数が n ≤ 24 個ある.兄弟 A, D, B でそれらを分ける.A ≥ D ≥ B を満たす分け方の中 で,A − B が最小の分け方を探す. http://main.edu.pl/en/archive/pa/2010/cuk 解説されている解法を担当者が実装したつもりのものでも tle なので (最大ケース 5 秒くらい), “Useful Resources” からデータをダウンロードして正しければ時間は気にしないことが良さそう. 問題 Building Blocks⋆ (15th Polish Olympiad in Informatics) N 本の棒グラフがある.どこか連続する K(≤ N ) 本を選んで,同じ高さに揃えたい.高さを 1 増やすのも減らすのも同じコストがかかる.コストの総和の最小とその時のグラフの高さを 出力せよ. http://main.edu.pl/en/archive/oi/15/klo 入出力は scanf でなく cin を用いても問題ない.STL のデータ構造の組み合わせで解くことができる. 64 問題 Pilot⋆ (17th Polish Olympiad in Informatics) パイロットがどの程度まっすぐ飛べるかどうかを計算する。時刻毎の位置 (一次元) が数列 ai として与えられる。数列の長さは最大 3,000,000 である。ずれてよい範囲として t が与えられ る。数列の範囲 [i,j] のうち、|ak − al | <= t, ∀k, l i ≤ k, l ≤ j という条件をみたす最大の長さ を求めよ。http://main.edu.pl/en/archive/oi/17/pil 65 第 7 章 グラフと木 7.1 概要: グラフ 接続関係に焦点をあてて世の中をモデル化する際に,グラフがしばしば用いられる.路線図,物流,血 縁関係,などなど.(参考書 [3, 2-5 節, p. 87] あれもこれも実は “グラフ”) a b d c 用語 グラフは,頂点 (点, 節点,vertex; 複数形は vertices) と辺 (枝,線,edge, arc) からなる.頂点集合 V と辺 集合 E でグラフ G = (V, E) を表す. 例: 3 つの頂点 V = {1, 2, 3} の辺を全て結んだグラフ (=三角形) は,E = {{1, 2}, {2, 3}, {3, 1}} 頂点 v と辺 e に対して v ∈ e となる時,v は e に接続する.頂点 v に対して,v の接続辺の個数 |E(v)| を 次数 (degree) という.二つの頂点が共通の接続辺を持つ場合にその頂点は隣接するという.隣接する頂点 をリストにして並べたものをパス (path, trail, walk で細かくは異なる意味を持たせるので,詳しく学ぶ際に は注意) と呼ぶことにする. 例: 三角形の各頂点の次数は 2 グラフの辺をちょうど一回づつ通る閉路/パスを Euler circuit/trail (オイラー閉路/路) という. 判定法: 連結であること,全ての頂点の次数が偶数であること/パスの始点と終点を除いて偶数であること. a b c Euler 閉路あり (e.g., abca) a d a d b c b c 閉路はないが全ての辺を辿れる (adcabc) 66 全ての辺をたどれない 一筆書きの判定 7.2 例題 (PC 甲子園 2005) Patrol 問題: パトロールする街路を一筆書きで辿れるかを判定せよ.始点と終点は指定されている. 連結であることは保証されている. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0086 回答例 C++ 1 2 3 4 5 6 7 8 9 10 11 12 int main() { while ( 辺 a,b を 読 み 込 む ) { // 頂点 a の次数を増やす; // 頂点 b の次数を増やす; if (a == 0) { // 始点から終点に一筆書き出来るか (*) を判定して出力 // (*) 頂点番号 1 と 2 の字数が奇数,かつ, // 頂点番号 3 以上の頂点の次数が全て偶数 // 次のテストケースのために,全ての頂点の次数を 0 に戻す } } } 問題 Play on Words⋆ (Central Europe 1999) 単語が与えられるので,全ての単語を「しりとり」で繋げられるかを判定せよ.初めと終わ りの単語は自由に選んで良い.(連結であることは保証されていない) 筆者注: 時間制限が厳しいので入力には scanf を使うと良い http://poj.org/problem?id=1386 回答例 アルファベットを頂点としたグラフを考える.たとえば news という単語を,頂点 n から s への辺 と考える.ここで反対向きには使えないことから,(先ほどと異なり) 辺に向きがある (有向グラフという). 有向グラフの次数は,辺の向きに対応して,入次数 (in-degree) と出次数 (out-degree) を区別して扱う. 有向グラフがオイラー閉路を持つ必要十分条件は,連結であることと,全ての頂点の入次数と出次数が 等しいことである.閉路でない場合は,パスの始点/終点で入次数が出次数より一つ多い/少ない. 問題 A と異なり,連結性が保証されないので,自分で判定する.(10 章参照) 7.3 木 特殊な (連結で閉路がない) グラフを木という.木は,一般のグラフより扱いやすい.木で表せるものに は,式 (3 + (5 - 2)),階層ファイルシステム (リンクなどを除く),自分の祖先を表す家系図 (いとこなど血 縁間の結婚がない場合),インターネットのドメイン名などがある. 67 a a b a c d b c d 木1 木2 e 木3 定義 無向グラフ G に対して,全ての 2 頂点 v, w に足して v − w パスが存在する時に G を連結と呼ぶ.閉路 を含まないグラフを森 (forest) または林と呼ぶ.連結な森を木 (tree) という. グラフ T の頂点数が n として,T が木であることと以下は同値である: • T は n − 1 個の辺からなり,閉路を持たない • T は n − 1 個の辺からなり,連結である • T の任意の 2 頂点に対して,2 頂点を結ぶパスが一つのみ存在する • T は連結で,T のどの辺についても,それを T から取りのぞいたグラフは非連結 • T は閉路を含まず,T の隣接しない 2 頂点を xy をどのように選んでも,xy をつなぐ辺を T に加え たグラフは閉路を含む 木の頂点の特別な一つを根 (root) と呼ぶ.グラフを図示する際には,根を一番上または下に配置するこ とが多い.次数 1 の点を葉 (leaf) と呼ぶ. 7.4 「親」に注目した,木の表現 N 個の節点を持つ「木」を計算機上で表す方法の一つに,各節点に 1 から N までの数字の番号をふり, 各節点の親を一次元配列 (以下,parent の頭文字を用いて P[] と表記する) で管理する方法がある.木には, たかだか 1 つの親を持つという性質がある.そこで,節点 i に対応する P [i] の数値に,節点 i が親を持つ 場合にはには親の番号,親を持たない場合は −1 または自分自身などの特殊な番号を割り当てる. なお,各節点で子を複数持ちうるので,子を管理する場合は一次元配列より複雑な表現 (隣接リスト,隣 接行列など 10.1) が必要となる. 木 1 1 2 5 3 4 2 3 4 配列表現 i P[i] 1 -1 i P[i] 1 -1 2 1 68 3 1 4 1 i P[i] 1 3 2 5 1 3 5 4 3 5 -1 7.4.1 最小共通祖先 動物 両生類 爬虫類 蛙 哺乳類 猫 犬 ある木について,木の二つの節点に共通する祖先でもっとも近い節点を最小共通祖先 (lowest common ancestor) と呼んで LCA と略す.図の例で,犬と猫の LCA は哺乳類.蛙と犬の LCA は動物.なお,発展 的な話題になるが,一つの木について何度も LCA を求める場合には,事前に準備をしておくと効率的に求 められる (16.3 章参照). 例題 Nearest Common Ancestors (Taejon 2002) 一つの木と,その木の節点が二つ与えられる.二つの節点に共通するもっとも近い祖先を求 めよ. http://poj.org/problem?id=1330 いくつかのステップに分割して解いてみよう: 1. 入力を目で理解する. 一行目に数字 T があり, 「木の情報と LCA を求める節点二つ」が T セット続 いている.Sample Input の木を紙に書いてみる. 2. 木の入力を読み込む C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> using namespace std; int N; int P[10010]; int main() { int T; cin >> T; for (int t=0; t<T; ++t) { // t 番目の木について P[] を 全 て -1 に 初 期 化 cin >> N; for (int i=0; i<N-1; ++i) { int p, c; cin >> p >> c; P[c] の 値 を p に 設 定 ; } // この段階で // P[根の番号] == -1 // P[c] == p (c が親 p を持つとき) // という状態である int A, B; cin >> A >> B; // LCA を求める二つの節点 A と B 69 22 23 24 XXXXX } // 木に関するループ終了 } “XXXXX” の部分で,各 P[] を表示して確認せよ.各節点の親は,紙に描いた木と一致するか? 3. LCA を求める準備として,ある節点 x から根までの節点を全て求めることを考える.考え方として は,x の親は P[x] で求められるから,親の親,親の親の親などとたどっていけばいずれは根に到達す る.親をたどる際に節点を表示して,確認してみよう. 4. 節点 A と B のそれぞれについて,根までのパス (頂点番号列) の共通要素を求める.一番根から遠い ものが求める答え. 5. POJ への提出.アカウントは上部の ページ “Register” から作成する.提出は,ページ下部の “Submit” から行う. なお,同じ木に対して節点を変えながら何度も LCA を求める場合は,前処理をしておくことで一回あた り O(log N ) で求めることができる.(参考書 [3, p. 274]) 親への移動 7.4.2 問題 Marbles on a tree (Waterloo local 2004.06.12) N 個の節点を持つ木が与えられる.木の各節点には,果物がない場合と,一つあるいは複数 の果物がある場合がある.果物の合計は N 個である.各節点が一つづつ果物を持つようにする ためには,最低何回の移動が必要か.一つの果物を一つの辺にそって移動すると 1 回と数える. http://poj.org/problem?id=1909 入力のサンプルコードを示す: C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <cstdio> using namespace std; int N, P[10010], M[10010], V[10010]; int main() { while (˜scanf("%d", &N) && N) { fill(P, P+N, -1); int /*葉の数*/L=0, id, c; for (int i=0; i<N; ++i) { scanf("%d", &id); --id; // [1,N] を [0,N-1] に scanf("%d %d", M+id, V+id); // marble の数, vertex の数 for (int j=0; j<V[id]; ++j) { scanf("%d", &c); --c; // id(親) c(子供) P[c] = id; } if (V[id] == 0) ++L; // V が 0 なら id は葉 } for (int i=0; i<N; ++i) { // 親を出力してみる printf("%d’s parent is %d\n", i+1, P[i]+1); } } } 70 1(2) 2(1) 3(0) 5(3) 4(1) 6(0) 7(0) 8(2) 9(0) 図 7.1: Sample 1 1(1) 2(0) 3(-1) 5(2) 4(0) 6(-1) 7(-1) 8(1) 9(-1) 図 7.2: Sample 1(過不足) (poj は cin を使うと時間制限になる問題があるため,scanf を勧める) 方針: 各節点について親から/へ移動する果物を考える.たとえば親から 3 つもらって 5 つ返すのは無駄 で,それなら差し引き 2 つ送るだけで良い.つまり,各節点について,まずは子供の間で不足と余剰の調 整を行ない,残った分を親に調整を依頼すると良い. C++ 1 2 3 4 5 6 7 8 9 10 11 // 節点が調整済みかを表す配列を用意する.初めに葉を調整済みとしておく. // 各節点での過不足を表す配列を用意する.初期値は,果物の配置数-1 while // 根が調整済みでない for // 全ての節点 i について if // i が調整済みでない かつ i の子供が全て調整済みなら // i での過不足を親に押し付ける // i を調整済みと記録 } } } // 各節点での過不足の合計が答え 1(1) 3(0) 4(-1) 図 7.3: 葉を調整 (2=0, 5=2, 6=-1, 7=-1, 8=1, 9=-1 を消去) 71 7.5 木の話題 以下は 10 章のグラフの探索に馴染んでから取り組むことが適切. 7.5.1 木と動的計画法 問題 TELE⋆ (Croatia OI 2002 Final Exam - Second Day) (意訳) 放送局が番組を配信する計画を立てる.受信できた場合に視聴者が払う額はあらかじ め与えられる.配信路は木構造になっている.木の節点は,根が放送局自身,葉が視聴者.他 が中継装置である.赤字にならずに配信できる人数は何人か? http://poj.org/problem?id=1155 ヒント: 節点 i から j 人に配信するコスト T[i][j] のようなものを管理しながら根から深さ優先探索 を行う.各節点で,たとえば二つの子を持つ内部節点で 3 人に配信する場合の収益は、(0,3), (1,2),... (3,0) のようなすべての割り当ての中から最大を選ぶ必要がある. 問題 Heap⋆⋆ (POJ Monthly–2007.04.01) ノードに整数が書かれた二分木がある.整数を書き換えて,どのノードに対しても,(左の 子孫たち) < (右の子孫たち) ≤ (自分自身) を満たすようにしたい.最小何か所書き換えればよ いか? 木は完全二分木とは限らず,後ろのほうが詰まっていない場合がある. http://poj.org/problem?id=3214 入力形式注意 7.5.2 木の直径 問題 Fuel⋆ (Algorithmic Engagements 2011) 木と,歩いて良い辺の数が与えられるので,訪れる頂点の数を最大化した観光ツアーを設計 せよ (walk; 同じ辺を複数回通って良い,始点と終点は別で良い) http://main.edu.pl/en/archive/pa/2011/pal 問題 (国内予選 2014) Bridge Removal⋆ 群島の橋を渡りながら全ての橋を撤去しながら職人の,最短時間を求める. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1196& lang=jp 72 7.5.3 木の中心 問題 Cave⋆⋆ (11th Polish Olympiad in Informatics) 木の節点のどこかに宝が隠されている。質問に対する答えを元にその宝を見つける.質問回 数を最小にするような質問戦略で必要になる、最大の質問回数を求めよ. 質問は,節点を一つ選んで,そこに宝があるかを聞く.宝がある場合はそこでゲームが終了 し,そうでない場合は,どちら方向に宝があるかが隣接する節点により示される. http://main.edu.pl/en/archive/oi/11/jas 類題: Ciel the Commander http://codeforces.com/problemset/problem/322/E 7.5.4 木の正規化 問題 部陪博士,あるいは,われわれはいかにして左右非対称になったか ⋆⋆ (国内予選 2007) 式を表す二分木を与えられた規則で正規化せよ http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1152& lang=jp (面倒なので経験者向け.なお,ICPC ではなるべく端末専有時間を短く解くことが求められる) 73 第 8 章 全域木と Disjoint Set (Union-Find Tree) 概要 グループを管理する Disjoint Set (Union-Find Tree) というデータ構造を紹介する.またそれを使ってグ ラフの最小重み全域木を求めてみよう. 8.1 Disjoint Set (Union-Find Tree) 二つの要素が同一グループに属するかどうかを効率的に判定する手法の一つが、Union Find 木 (参考書 攻略 [2, pp. 318–], 参考書 [3, pp. 81–]) である。Disjoint-set とも呼ばれる。グループ同士を併合する操作がで きるが,分離はできない (c.f. link-cut tree). 1 2 3 5 4 6 7 8 9 i 1 2 3 4 5 6 7 8 9 P[i] 1 2 3 3 5 5 5 7 8 {1}, {2}, {3, 4}, {5, 6, 7, 8, 9} の 4 つのグループ 判定 同じ木に属するなら同じグループで,そうでなければ異なるグループである.それを,根 (root) を 調べることで行う.例: • Q1. 6 と 8 は同じグループに属するか? 6 の属する木の根 (root) は 5 で,同様に 8 の根は 5 • Q2. B と D は同じグループに属するか? 2 の属する木の根は 2 で,4 の根は 3 Ô 併合 Ô 同じグループ. 異なるグループ. 併合は,二つの節点を同一グループにまとめる操作で,一方の節点の属する木の根をもう一方の節 点の属する木の根につなげることで実現する.どちらをどちらにつなげても,意味は変わらない.(小さな (低い) 木を大きな (高い) 木の下につける方が効率が良い,が,この資料では割愛する) 74 1 1 2 2 i P[i] 1 1 2 2 i P[i] 元のグラフ 1 1 2 1 1 と 2 を併合したグラフ 5 3 5 3 4 6 6 7 7 8 4 8 9 9 i P[i] 3 3 4 3 5 5 6 5 7 5 元のグラフ 効率化: パス圧縮 8 7 9 8 i P[i] 3 5 4 3 5 5 6 5 7 5 8 7 9 8 4 と 8 を併合したグラフ (グループ毎併合するためには根同士を接続) 節点の根を求める操作は,根からの距離が遠いほど時間がかかる.そこで,一度根を 調べたら,関係する接点をなるべく根に直接つなげるようにつなぎ替えると効率の改善に有効である. 5 3 5 6 4 7 3 8 4 6 7 8 9 9 i P[i] 3 5 4 3 5 5 6 5 元のグラフ コード例: 7 5 8 7 9 8 i P[i] 3 5 4 3 5 5 6 5 7 5 8 5 9 5 root(9) = 5 を調べたついでに木を変形 以下に,初期化,判定,併合のコード例を示す.なお,初見でコードの理解が難しい場合は, 一度書き写して動作を試した後に再度理解を試みるのが良い.(rank の概念は無視している.) C++ 1 2 3 int P[10010]; // 0 から 10000 までの頂点を取り扱い可能 void init(int N) { // 初期化 はじめは全ての頂点はバラバラ for (int i=0; i<N; ++i) P[i] = i; 75 4 5 6 7 8 9 10 11 12 13 14 } int root(int a) { // a の root(代表元) を求める if (P[a] == a) return a; // a は root return (P[a] = root(P[a])); // a の親の root を求め,a の親とする } bool is_same_set(int a, int b) { // a と b が同じグループに属するか? return root(a) == root(b); } void unite(int a, int b) { // a と b を同一グループにまとめる P[root(a)] = root(b); } 実行例: C++ 1 2 3 4 5 6 7 8 int main() { init(100); cout << is_same_set(1, 3) << endl; unite(1,2); cout << is_same_set(1, 3) << endl; unite(2,3); cout << is_same_set(1, 3) << endl; } 上記のクラスカル法内で,辺 e を加える度に unite で辺の両端の頂点をグループ化する.辺 e の両端 を a, b として,is same set(a,b) が真であれば辺 e を加えると閉路ができる (e と別に a, b パスがある ので). 例題 Disjoint Set: Union Find Tree (AOJ) Disjoint Set でグループを管理せよ http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=DSL_1_A& lang=jp 問題 (会津大学プログラミングコンテスト 2003) Fibonacci Sets i 番目の Fibonacci 数を f[i] と表記する.ある i, j (1 ≤ i, j ≤ V ) について,f[i]%1001 と f[j]%1001 の差の絶対値が d 未満だったら,ノード i と j は同じグループに属するとする.V と d が与えられた時に,グループの数を数えよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1016 回答例: C++ 1 2 3 4 5 6 7 8 int F[1001]; int main() { F[0] = 1, F[1] = 2; // ... F[i] に i 番目の Fibonacci 数%1001 をあらかじめ計算, 代入しておく while (cin >> V >> D) { // 木を初期化 for (int i=1; i<=V; ++i) for (int j=i+1; j<=V; ++j) 76 9 10 11 12 13 if // (F[j] と F[i] の差の絶対値が D 未満だったら) // グループ i とグループ j を同一化; // i∈[1,V] に関して root(i) == i であるような数を数えて出力 } } 細かく作ってテストする • Fibonacci 数の計算: F[2]..F[1000] までを for 文で代入する (9.4.1 の方針 3 で a[2] の代わりに F を 用いることと,1001 の剰余を取りながら行う点が差).つまり,i を小さい方から大きくしてゆけば, 定義通りに F[i] = F[i-2]+F[i-1]; と計算できる.オーバーフローしないように計算途中でも 1001 の剰余を取る.(表示して確認する) • Union-find 木を作る: 基本的には資料通り (初期化して,木を表示して確認する.いくつか併合しては木を表示して確認する.) • Fibonacci 数での動作作成: 小さな V(たとえば 10) と適当な D を与えて,木を表示し,手計算と一致 するかどうかをテストする. • グループの数を数える: root(i) == i である個数ををテストする. 8.1.1 練習問題 問題 (夏合宿 2009) Marked Ancestor マークされている最も近い祖先を求める.初めは根だけがマークされている. 注意: 合計は int を越えるので,long long を用いる. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2170 補足: 下記の図で,初めに「猫」の “nearest marked ancestor” は「動物」,もし「哺乳類」がマークされ た後なら, 「猫」の “nearest marked ancestor” は「哺乳類」. 動物 両生類 爬虫類 蛙 哺乳類 猫 犬 v ヒント: まず,Mark や Query などの命令列を全て読み込み,後ろからマークを外してゆくように処理 してゆくとすると...? 77 問題 (アジア地区予選 2002) True Liars 真実のみいう人 (=正直者) と嘘だけ言う人住む島があって,それぞれの合計人数はわかって いる. 「人 x が人 y を正直者である/ない」と言ったという情報を総合して,割り当てが一意に定 まるならばそれを求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1238 ヒント: まず正直者かどうかが同じである人を集めてグループ化する.次に各グループについて正直者か そうでないかを割り当ててみる. 問題 (アジア地区予選 2012) Never Wait for Weights⋆ (意訳) 要素が属するグループだけでなく,要素間の距離を管理せよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1330 回答方針: 根との距離をデータに加えた Union-find 木を作れば良い. 問題 (アジア地区予選 2013) Everlasting –One–⋆ 転職できる仕事をグループにして,行き来できないグループがどれだけあるかを知りたい. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2568 8.2 全域木 連結なグラフ G の頂点全てと辺の全てまたは一部分を用いて構成される木を全域木 (spanning tree) また は全点木と呼ぶ.辺に重みがついている場合に,全域木に含まれる辺の重みの合計が最小であるような木 を最小 (重み) 全域木 (minimum spanning tree) と呼ぶ. メモ: グラフが連結なら全域木が存在する.全域木は一つとは限らない (完全グラフの場合は nn−2 個も ある).最小全域木も一つとは限らない.最小全域木を求める問題は,最小全域木のうちの一つを求める問 題を指すことが多い. a d a d a d a d b c b c b c b c 元のグラフ 8.2.1 赤い部分グラフは全域木 全域木でない (非連結) 全域木でない (閉路) クラスカル法 最小重み全域木を求める方法として,有名な方法にプリム法とクラスカル法があるが,ここでは後者を 紹介する. クラスカル法は以下のように動作する (参考書 [3, pp. 101–]): 78 • 辺を重みの小さい順にソートする • T を作りかけの森 (最初は空,閉路を含まないグラフ,連結とは限らない) とする • 重みの小さい順に,各辺 e に対して以下の操作を行う T + e (グラフ T に辺 e を加えたグラフ) が閉路を含まなければ T に e を加える 1 a 3 4 b 5 a d 2 c 元のグラフ 1 3 4 b 5 a d 2 辺 ad (重み 1) を採用 b 5 a d 3 4 c 1 2 辺 cd (重み 2) を採用 d 3 4 c 1 b 5 2 c 辺 ab (重み 4) を採用 T + e が閉路を含むかどうかを効率的に判定する手法の一つが,union-find tree である. 例題 Graph II - Minimum Spanning Tree (AOJ) 与えられたグラフの最小全域木の重みの総和を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ALDS1_ 12_A 問題 Stellar Performance of the Debunkey Family (PC 甲子園 2008) 問題: 街を連結に保ったまま,橋の維持費用を最小化したい. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0180 入出力例 入力を読み込んで,維持コストの少ない辺の順番に出力するコードは,たとえば以下のように作 ることができる.今日の主眼は,クラスカル法の実習にあるので,これをそのままコピーしても問題ない. C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <algorithm> int N, M, A[10010], B[10010], COST[10010]; pair<int,int> bridge[10010]; // コストと橋番号のペア int main() { while // (N と M を読み込み,N が 0 より大きければ処理する) for (int i=0; i<M; ++i) { // A[i] と B[i] と COST[i] を読み込む; bridge[i].first = COST[i]; bridge[i].second = i; } sort(bridge, bridge+M); // コストの小さい順に整列 for (int i=0; i<M; ++i) { int cost = bridge[i].first; int a = A[bridge[i].second]; int b = B[bridge[i].second]; // 「a から b に cost の橋がかかっている」と表示 } } } コード中の pair<int,int>とは struct pair { int first, second; }; に相当. 79 回答例: C++ 8.2.2 上記の準備ができているとして,回答の骨子は以下のようになる. 1 2 3 4 5 6 7 8 int 合 計 = 0; for (int i=0; i<M; ++i) { //安い橋から順に if // (端の両端の節点が既に連結だったら) continue; // 橋の両端の節点を同一グループに併合する; // 合計に,橋のコストを加える; } // 合計を出力; 色々な全域木 問題 (アジア大会 2007) Slim Span⋆ 最小の辺の重みと最大の辺の重みの差が最小の全域木を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1280 問題 (模擬国内予選 2013) Sinking islands⋆ 沈みゆく島にどのように (問題文参照) 橋をかけるかを求める. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2511 問題 Byteland⋆ (1st Junior Polish Olympiad in Informatics) 同じ重みの辺が存在する場合は,最小重み全域木は複数通り存在しうる.各辺毎に minimum spanning tree に含まれうるかを調べる http://main.edu.pl/en/archive/oig/1/baj 時間制限は最大 15 秒確保されれているようだが,1.5 秒くらいで解ける.クラスカル法が正しい解を与 える証明と関連. 問題 (アジア地区予選 2014) There is No Alternative⋆ どのような最小重み全域木を作っても必ず使う辺を求める. http://judge.u-aizu.ac.jp/onlinejudge/cdescription.jsp?cid= ICPCOOC2014&pid=F c.f. 類題 http://codeforces.com/problemset/problem/160/D 80 第 9 章 繰り返し二乗法と行列の冪乗 概要 9.1 適切な手法を用いると,計算時間を短縮できることを体験する.この手法が適用可能な問題では,10 億 ステップ後のシミュレーション結果を簡単に求めることもできる. 繰り返し二乗法 応用先: • すごろくでサイコロで N (0 ≤ N ≤ 231 ) が出た時に,行ける場所を全て求める • 規則に従って繁殖する生物コロニーの N ターン後の状態を求める • 規則に従った塗り分けが何通りあるかを求める (参考書 [3, p. 114]) 例: 38 = 6561 の計算 • 3·3·3·3·3·3·3·3 Ô 7 回の乗算 • int a = 3*3, b = a*a, c = b*b; 練習: 3128 の場合は? Ô Ô 3 回の乗算 log(128) 回の乗算 関連: • オーバーフローに注意: int で表せる範囲は,この環境では約 20 億くらい • 剰余の扱い: (a*b)%M = ((a%M)*(b%M))%M 例題 Elementary Number Theory - Power (AOJ) 2 つの整数 m, n について、mn を 1 000 000 007 で割った余りを求めよ http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=NTL_1_B& lang=jp 参考書 攻略 [2, pp. 445–] 参照. 9.2 言語機能: struct と再帰関数 この章では long long と struct を使う.struct の書式に不安がある場合は,入門書を復習のこと. 再帰関数は,この段階では場合によってはとばしても良い. 81 9.2.1 long long 本資料が前提とする環境で,GCC, G++で 64bit 整数を用いる場合は,long long という型を用いる. C++ 9.2.2 1 2 3 4 long long a = 1000000; int b = 1000000; cout << a * a << endl; // 1000000000000 cout << b * b << endl; // -727379968 (オーバーフロー) struct C++ 1 2 3 struct Student { int height, weight; }; 使用例: C++ 9.2.3 1 2 3 4 5 6 7 8 9 10 Student a; a.height = 150; a.weight = 50; cout << a.height << ’ ’ << a.weight << endl; Student b = { 170, 70 }; cout << b.height << ’ ’ << b.weight << endl; Student c = { 180 }; // weight == 0 Student d = { }; // height, weight == 0 再帰関数 C++ 1 2 3 4 int factorial(int n) { // 階乗 if (n <= 1) return 1; return n*factorial(n-1); // 自分を呼び出す } 使用例: C++ 9.3 1 cout << factorial(5) << endl; 正方行列の表現と演算 メモ:以下に 2x2 の行列のサンプルコードを掲載する.配列や vector, valarray 等のデータ構造を用いると NxN の行列を自作することもできる.研究や仕事で必要な場合は,専用のライブラリを使う方が無難. C++ 1 2 3 struct Matrix2x2 { int a, b, c, d; // a,b が上の行,c,d が下の行とする }; 82 表示も作っておこう C++ 1 2 3 4 5 6 void show(Matrix2x2 A) { cout << "[ " << endl << A.a << ’ ’ << A.b << endl << A.c << ’ ’ << A.d << endl << "]" << endl; } 行列同士の乗算 C++ 1 2 3 4 5 6 7 8 9 10 続いて乗算を定義する.以下の mult は行列 A, B の積を計算する. // returns C = A*B Matrix2x2 mult(Matrix2x2 A, Matrix2x2 B) { Matrix2x2 C = {0}; // 0 で初期化 C.a = A.a * B.a + A.b * B.c; C.b = A.a * B.b + A.b * B.d; C.c = A.c * B.a + A.d * B.c; C.d = A.c * B.b + A.d * B.d; return C; } // (注) 冪乗計算はすぐにオーバーフローするので注意: ここに細工をすることも多い 使用例: C++ 1 2 3 4 5 Matrix2x2 A = {0,1, 2,3}, B = {0, 1, 2, 0}; show(A); show(B); Matrix2x2 C = mult(A, B); show(C); 冪乗の計算 (繰り返し自乗法) C++ 1 2 3 4 5 6 7 8 9 10 11 12 次のコードは行列 A の p(> 0) 乗を計算し, O に書きこむ. // O = Ap Matrix2x2 expt(Matrix2x2 A, int p) { if (p == 1) { return A; } else if (p % 2) { Matrix2x2 T = expt(A, p-1); return mult(A, T); } else { Matrix2x2 T = expt(A, p/2); return mult(T, T); } } 使用例: C++ 1 2 3 Matrix2x2 A = {0,1, 2,3}; Matrix2x2 C = expt(A, 3); // A の 3 乗 show(C); 83 9.4 練習: フィボナッチ数 問題 Fibonacci (Stanford Local 2006) フィボナッチ数列の,n 項目の値を 104 で割った余りを計算しなさい. 0 ≤ n ≤ 1016 http://poj.org/problem?id=3070 9.4.1 様々な計算方針 方針 1: 定義通りに計算する C++ 1 2 3 4 5 int fib(int n) { if (n == 0) return 0; if (n == 1) return 1; return fib(n-2)+fib(n-1); } 1 2 3 4 int main() { for (int i=1; i<1000; ++i) cout << "fib" << i << " = " << fib(i) << ’\n’; } 使用例: C++ n = 30 くらいで,先に進まなくなる. 方針 2: 一度行なった計算を記憶する table という配列を用意し,一度行なった計算を記憶させてみよう.この工夫は,メモ化 (memoization, tabling) などと呼ばれる,応用範囲の広いテクニックである. C++ 1 2 3 4 5 6 7 8 int table[2000]; // 2000 まで答えを記憶 int fibmemo(int n) { if (n == 0) return 0; if (n == 1) return 1; if (table[n] == 0) // もし初見なら table[n] = fibmemo(n-2)+fibmemo(n-1); // 計算して覚える return table[n]; // 覚えていた値を返す } こんどは,n=1000 でもすぐに答えを得られる.(オーバーフローしているが,ここでは一旦無視する) ただし,この方法では 配列 table の要素数までしか計算することができない.問題では,n の最大値は 1016 なので,現実的でない. 84 方針 3: 小さい方から計算する n に比例する時間で計算可能である.この方法では,109 くらいなら待っていれば終わるが,1016 は時 間がかかりすぎる. C++ 9.4.2 1 2 3 4 5 6 7 8 9 10 11 int fibl(int n) { int a[2] = {0,1}; for (int i=2; i<=n; ++i) { // 不変条件: ループ開始時に,a[0] と a[1] はそれぞれ // Fib(i-1) と Fib(i-2) (i が奇数) // Fib(i-2) と Fib(i-1) (i が偶数) // に相当 a[i%2] = (a[0]+a[1])%10000; } return a[n%2]; } 行列で表現する ( ( A= 1 1 1 0 ) Fn+2 Fn+1 ) ( 1 1 1 0 = )( Fn+1 Fn ) とすると,結合則から以下を得る: ( Fn+1 Fn ( ) n =A F1 F0 ) ( n =A 1 0 ) . この方針であれば,n が 1016 まで大きくなっても現実的に計算できる.ただし,n が int で表せる範囲 を超えるので,expt() の引数などでは long long を用いること.また,普通に計算すると要素の値も int で表せる範囲を越えるので, • (a*b)%M = ((a%M)*(b%M))%M • (a+b)%M = ((a%M)+(b%M))%M などの性質を用いて,小さな範囲に保つ. 動作確認には,小さな n に対して,方針 3 の手法などと比較して答えが一致することを確認すると良い. 104 の剰余は,F10 = 55, F30 = 2040 などとなる. 9.5 応用問題 問題 One-Dimensional Cellular Automaton (アジア地区予選 2012) (意訳) 行列を T 乗する (0 ≤ T ≤ 109 ) http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1327 NxN の行列の実装例: 85 C++ 1 2 3 4 5 6 7 8 9 10 11 struct Matrix { valarray<int> a; Matrix() : a(N*N) { a=0; } }; Matrix multiply(const Matrix& A, const Matrix& B) { Matrix C; for (int i=0; i<N; ++i) for (int j=0; j<N; ++j) C.a[i*N+j] = (A.a[slice(i*N,N,1)]*B.a[slice(j,N,N)]).sum()%M; return C; } 問題 行けるかな?⋆ (UTPC 2008) サイコロが巨大なすごろく (目の合計が 231 まで) http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2107 • U ターン禁止の表現: 有向辺 (道+向き) に番号を振り,ある番号 (が表す辺) から次にどの番号 (があ らわす辺) に行けるかを表す,遷移行列を作る • n ターン後に行ける場所には,n 乗した遷移行列と初期位置を表すベクトルの積が対応する. • 隣接行列などのグラフの表現は,10 章に目を通した後に戻ってくることを推奨. 問題 概要: (3 + Numbers⋆ (GCJ 2008 Round1A C) √ n 5) の最後の 3 桁を求める http://code.google.com/codejam/contest/32016/dashboard#s=p2 (参考書 [3, p. 239]) GCJ への提出方法 • プログラムを作る: Sample で確認する • “Solve C-small” をクリックして C-small-practice.in をダウンロードする • ./a.out < C-small-practice.in > output.txt のようにリダイレクションで,出力を作 る 豆知識: ./a.out < C-small-practice.in | tee output.txt のように tee というコマ ンドを使うと,ファイルに保存しながら端末に出力してくれるので,進行状況が分かる. • “Submit file” から output.txt を提出する.(即座に判定が表示される) • 正解したら, “Solve C-large” から,large も解く 問題 Leonard Numbers⋆⋆ (POI Training Camps 2008) Fibonacci 数に似た,Leonard 数の和を求めよ http://main.edu.pl/en/archive/ontak/2008/leo 86 第 10 章 グラフの探索 概要 連結なグラフの辺を全てたどる方法として,幅優先探索 (BFS) と深さ優先探索 (DFS) を紹介する. 10.1 グラフの表現: 隣接リストと隣接行列 各節点の親を覚えておくと木構造で親をたどることができたが,一般のグラフの節点を訪問するために は,より便利なデータが必要となる. この資料では,グラフを隣接行列 (adjacency matrix) で表現する.節点 i から j への辺が存在する時,行 列の i 行目 j 列目の値を 1 に,そうでないときに 0 とする.辺に向きのない,無向グラフを扱う場合には 行列は対称になる. a b d 0 1 1 0 0 0 1 1 0 0 1 1 0 1 1 0 a→b ··· b→a b→c ··· c 左のグラフの a, b, c, d をそれぞれ 0, 1, 2, 3 の数値に対応させると,隣接行列は右のようになる.すなわ ち 0, 1, 2, 3 行目が a, b, c, dから出る辺を表し,0, 1, 2, 3列目が a, b, c, dに入る辺を表す. なお,辺のコストや経路の数などを表すために,行列の要素に 1 以外の値を今後使うこともある. また,グラフの表現の中で隣接行列は以下の観点で比較的「贅沢な」表現方法である.都市の数を N と すると,常に N 2 に比例するメモリを使用する.グラフが疎な場合,すなわち辺の数が N 2 よりもかなり 少ない場合は,隣接行列で値が 0 である要素が多くなり無駄が多い.たとえば「木」の場合は,辺の数は N − 1 しかない.また,現実のグラフも電車の路線図のように疎であることが多い.そのような場合は,隣 接リストなどの表現を検討すると良い.(参考: 参考書 攻略 [2, pp. 264–(12 章)], 参考書 [3, pp. 90, 91]) 例題 Graph (AOJ) 有向グラフの隣接リストが与えられるので,隣接行列に変換する. 初めにグラフの節点の数 N が与えられ,それに続いて N 行が与えられる.各行は一つの節 点に対応し,その節点から出ている辺を表す.初めの数 u が節点の番号,続く数 k が辺の数, その後に続く k 個の数が各辺の行き先に対応している.(各行に含まれる数字の個数は,行毎に 異なりうる) 87 (例題 続き) http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ALDS1_ 11_A この問題のように多くのケースでは,頂点番号を 1, . . . , N でつけている.一方,C++などでは配列の添 字は 0 から始まるので,1 を引いて処理すると良いことも多い.Ruby による回答例を示す.この資料では, 問題文中に出てくる変数を (あとで値を変更する必要がない場合は) 大文字で表記し定数として扱う. Ruby 1 2 3 4 5 6 7 8 9 10 11 N = gets.to_i G = (1..N).map{ Array.new(N, 0) } N.times{ u,k,*v = gets.split(’ ’).map(&:to_i) # u,k は数, v は配列 v.each {|vi| # v の各要素 vi について # (u-1) と (vi-1) をつなげる } } G.each{|r| puts r.join(’ ’) } 10.2 幅優先探索 (BFS) 問題 Breadth First Search (AOJ) 節点 1 を始点に幅優先探索を行い,各節点の始点からの距離を表示せよ (入力の形式は例題 “Graph” と同じ) http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ALDS1_ 11_C 幅優先探索 (参考書 攻略 [2, pp. 282–], 参考書 [3, p. 36]) は,出発地に近い頂点から順に訪問する.キュー (6.2.2) というデータ構造を用いる.キューに入れた (push した) データは,front() 及び pop() の操作により 早く入れた順に取り出される. 1 (0) 2 (1) 3 (2) 4 (1) (サンプル入力: カッコ内は節点 1 からの距離) この問題の入力の形式は例題 “Graph” と同じで,サンプル入力も全く同じなので,入力は作成済で, G[s][t]==1 の時に (かつその時に限り),節点 s から節点 t に移動可能とする. 88 Ruby 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Q = [0] # 始点 0 をいれたキュー (配列) D = Array.new(N,-1) D[0] = 0 # 始点への距離は 0, 他の距離は-1 としておく while Q.size > 0 p ["debug", Q] # 各ステップでの Q の動作を確認 (後で消すこと) cur = Q.shift (0..N-1).each {|dst| if ... then # cur から dst に移動可能で、dst が未訪問だったら D[dst] = D[cur]+1 Q << dst # Q に dst を詰める end } end # D を表示 実行例は以下のようになる ["debug", [0]] ["debug", [1, 3]] ["debug", [3]] ["debug", [2]] キューには始点から辿れる節点が順に入れられる.8 行目の条件で,dst が未訪問かどうかを判別しない と,ループがあるグラフで無限ループしてしまう (たとえば 1 から 2 に辺があり,2 から 1 にも辺がある場 合に,1-2-1-2-1 と移動し続ける).未訪問かどうかは,D[dst] を見ると判別できる. C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <queue> int D[...]; void bfs(int src) { cerr << "bfs root = " << src << endl; queue<int> Q; // 整数を管理するキューの定義 Q.push(src); D[src] = 0; // 出発点 while (! Q.empty()) { int cur = Q.front(); // 先頭要素を取り出す Q.pop(); // 動作確認用表示 cerr << "visiting " << cur << ’ ’ << D[cur] << endl; for (...) { // 各行き先 dst に対して if (..) { // cur から dst に辺があり,dst が未訪問なら D[dst] = D[cur]+1; // Q.push(dst); // dst を訪問先に加える } } } } 89 10.3 深さ優先探索 (DFS) 問題 Depth First Search (AOJ) 番号の若い順に節点を深さ優先探索で訪問する時に,その訪問順序を表示せよ 「未発見の頂点が残っていれば、その中の1つを新たな始点として探索を続けます。」ことに 注意. (入力の形式は例題 “Graph” と同じ) http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ALDS1_ 11_B 深さ優先探索 (参考書 攻略 [2, pp. 273–], 参考書 [3, p. 33]) は,全ての節点を訪問する別の手法で,現在訪 問中の頂点に近い頂点から順に訪問する.まずは再帰的手続きを用いた実装を紹介する. 1 2 4 3 5 1 6 時刻 1: 頂点 1 から開始 1 2 4 3 5 2 4 3 5 時刻 7: 親に戻る C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 4 3 5 6 時刻 2: 行き先候補 2 と 3 から 2 を選択 1 6 時刻 3: 同様に 3 を選択 1 2 2 4 3 5 6 時刻 5: 同様に 6 まで進むと行き先がなく 1 6 2 4 3 5 6 時刻 9: 未訪問の行き先があれば進む int time = 0 void dfs(int cur) { // cur を訪問 // cur の訪問時刻を記録 time += 1; // 動作確認用表示 cerr << "visiting " << cur << ’ ’ << time << endl; for (dst ...) { // 全ての節点 dst について if (...) { // cur から dst に辺があり,dst を未訪問なら dfs(dst) } } // cur の訪問終了時刻を記録 time += 1; // 関数の終わりで親に戻る (青矢印) } 90 Ruby 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 def dfs(src) D[src] = $time # 訪問時刻を記録 $time += 1 (0..N-1).each {|dst| if ... then # src から dst に移動可能で、dst が未訪問だったら dfs(dst) end } F[src] = $time # 訪問終了時刻を記録 $time += 1 # 関数の終わりで親に戻る (青矢印) end D = Array.new(N) # D[i] の初期値は nil F = Array.new(N) $time = 1 (0..N-1).each {|id| # 番号が若い節点から if ! D[id] then # D[id] が未訪問だったら dfs(id) # dfs を始める end } # 出力 ループの検出と非連結のグラフ 1 2 5 3 4 6 どのようなグラフを対象とするかは予め想定する必要があり,この問題では上記のようなグラフも与え られうる.まず頂点 3 まで進んだ時点で,頂点 1 に進まないように注意しよう (無限ループになる).防ぐ ためには,行き先候補の訪問時刻を確認して既に尋ねたことがあるかどうかを識別すると良い.このよう な,訪問済の頂点に戻る辺を back edge と言う場合がある. またこの問題では,頂点 1 を出発点にしての DFS を終えたら,次は頂点 5 を出発点にしての DFS を始 め,すべての頂点を訪問するまで続けることが求められる.DFS を終えたら,頂点番号を増やしながら未 訪問の頂点がないかを確認し,見つかればそこを出発点に DFS を行えば良い. スタックを明示的に使った DFS (参考) 通常は,先に紹介した再帰を使った実装で十分である.但し,再帰の段数が深くなる場合はプロセスに 割り当てられた stack 領域 (データ構造の stack とは区別せよ) が足りなくなり,segmentation fault などが起 こることがある.実用上は,環境に応じた方法を用いて stack 領域の割り当てを増やせば十分であることが 多いが,コンテストにおいてはそのような手段を用いることができない場合も多い. スタック (6.2.1) というデータ構造を用いる実装を次に示す.スタックに入れた (push した) データは,top() 及び pop() の操作により遅く入れた順に取り出される.一目でわかるように,BFS の実装とほとんど差が ない (しかし queue と stack の違いから実際の探索の振る舞いは大きく異なる). 91 C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void dfs(int src) { // cerr << "dfs root = " << src << endl; stack<int> S; S.push(src); while (! S.empty()) { int cur = S.top(); S.pop(); if (...) { // 今回が cur への三度目以降の訪問 // 何もしない (後で訪問する予定だったが、 // 子孫節点経由で先に訪ねてしまったケース) } else if (...) { // 今回が cur は二度目の訪問 // (*) に対応する子孫を巡り終わった後なので、cur の離脱時刻を記録 } else if (...) { // cur が初めての訪問の場合 // 初訪問時刻を記録 S.push(cur); // (*) 子孫の訪問を全て終えたらもう一度自分に戻る for (...) { // 全ての子節点 dst について if (...) { // cur から dst に辺があり,dst が未訪問なら S.push(dst); // todo 一覧に加える } } } } なお,節点を一度づつ訪問すれば十分な場合は 17 行目の push は不要である.今回は訪問時刻だけでな く離脱時刻が必要のために,訪問と離脱でつごう二回づつ各節点をスタックに入れている.また 18 行目の for 文は,問題で番号の若い節点から訪問することが求められている点とスタックは遅く入れた順に取り 出される点を考慮して,順番を調整する必要がある. 10.3.1 深さ優先探索の応用 グラフのいくつかの情報は DFS を行うことで知ることができる. • 連結性: ある頂点から DFS を行いすべての頂点を訪問できれば連結.そうでなければ,複数の非連結 のグラフからなる.(無向グラフの場合) • 二部グラフかどうかの判定: 辺の両端の色が異なるように赤と青で塗り分けられるか.DFS で色を塗 りながら辺をたどり矛盾がないかを調べれば良い • 連結の強さ: 取り除くとグラフが非連結になる頂点 (関節点) や,取り除くとグラフが非連結になる辺 (橋) なども DFS により求めることができる. 10.4 二部グラフの判別 92 問題 A Bug’s Life (TUD Programming Contest 2005) 性別の分からない虫がいる. 「虫 i と虫 j の性は反対である」という情報が与えられた時に,矛 盾しないかどうかを答えよ. (または) グラフが二部グラフになっているかどうかを答えよ http://poj.org/problem?id=2492 サンプル入力の解釈: 1 2 3 不整合 1 3 2 4 整合 データの保持 C++ 1 2 3 4 5 6 // 問題分で与えられる最大数 int bugs, edges; // 隣接行列: e[i][j] が true なら,i<->j に辺がある bool e[2010][2010]; // 各虫の色 0: 未定, 1,-1: 男女 int color[2010]; 入出力例: C++ C++ 1 2 3 4 5 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 虫 id を id color 色に割り当てて整合するかどうかを返す // 整合する Ô true, しない Ô false bool search(int id, int id_color) { // ここを 3 種類作る } int main() { int scenarios; scanf("%d", &scenarios); for (int t=0; t<scenarios; ++t) { // この for 文のブロックが一つの問題 fill(&e[0][0], &e[0][0]+2010*2010, 0); // 0 に初期化 fill(&color, &color+2010, 0); // 0 に初期化 scanf("%d %d", &bugs, &edges); for (int j=0; j<edges; ++j) { int src, dst; scanf("%d %d", &src, &dst); e[src][dst] = e[dst][src] = 1; // 両方向に通行化 } bool ok = true; for (int j=1; j<=bugs; ++j) // 全ての虫について if (color[j] == 0 && !search(j, 1)) { ok = false; // 一回でも失敗したら,不整合 break; } 93 20 21 22 23 24 25 26 27 28 if (t) printf("\n"); printf("Scenario #%d:\n", t+1); if (! ok) printf("Suspicious bugs found!\n"); else printf("No suspicious bugs found!\n"); } } 問題文中に注意が有る通り,poj の cin は,scanf の 10 倍以上遅いので,scanf を使う. 10.4.1 グラフの走査 Bug’s life を解くには, 「全ての辺を通」って,各頂点を 1 と-1 で塗り分けられる (隣接する頂点の数字が 異なる) ことを確認すれば良い.グラフで, 「全ての辺を通」る方法である,幅優先探索 (BFS) と深さ優先 探索 (DFS) でとくことができる.どちらでも良いが,ここでは DFS で作成してみる. Bug’s life の sample 入力は小さすぎるため,少し複雑なグラフで動作を確認する. 1 2 4 5 3 6 7 1 7 6 1 2 1 4 1 5 2 3 5 6 5 7 10.4.2 合流/ループの検査 一般のグラフでは,ループを持つ場合がある.例題では,奇数のループと偶数のループで動作を変える 必要があるので二種類以上作る. 94 1 1 2 2 3 3 1 3 4 2 4 1 5 4 1 2 1 4 4 1 3 3 1 2 1 3 3 4 1 2 1 3 2 3 5 3 4 4 5 3 5 2 4 1. 前述の bfs や dfs に,上記のデータを与えて,どのような挙動 (頂点の訪問順) になるかを確認する 2. 上記のコード例内の,if (color[j] != 0) {....} の部分を適切に加筆することで,不整合の 場合は false を返すようにせよ. poj への提出の際は,動作確認用の cerr への出力は消しておくこと. 10.5 様々なグラフの探索 グラフの辺と頂点を問題文中で明示的に与えられない場合でも,自分でグラフを構成して幅優先探索 (BFS) あるいは深さ優先探索 (DFS) を行うことで解ける問題もある. 問題 (国内予選 2004) Red and Black 上下左右への移動だけで,行けるマスの数を求める. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1130& lang=jp 各マスを頂点として,移動可能な隣接する頂点同士に辺を張る.頂点に通し番号をつける必要はない. 95 マス (x,y) の上下左右 0, 0 1, 0 2, 0 3, 0 4, 0 5, 0 0, 1 1, 1 2, 1 3, 1 4, 1 5, 1 0, 2 1, 2 2, 2 3, 2 4, 2 5, 2 0, 3 1, 3 2, 3 3, 3 4, 3 5, 3 0, 4 1, 4 2, 4 3, 4 4, 4 5, 4 0, 5 1, 5 2, 5 3, 5 4, 5 5, 5 0, 6 1, 6 2, 6 3, 6 4, 6 5, 6 0, 7 1, 7 2, 7 3, 7 4, 7 5, 7 0, 8 1, 8 2, 8 3, 8 4, 8 5, 8 上下左右に隣接するマスは,(x+1,y), (x-1,y), (x,y+1), (x,y-1) の 4 つがありうる.但 し,地図をはみ出していないかどうか注意が必要. 方向の表現 実際には,上下左右の移動を手で書くのはバグの元であるので避けたほうが良い. const int dx[]={1,0,-1,0}, dy[]={0,-1,0,1}; のような配列を用意すると,探索内で似た様 な 4 行が並ぶ部分を,for 文で纏めることができる.即ち,(x,y) の隣のマスの一つは,(x+dx[i], y+dy[i]) で ある. マスに移動かどうか (x,y) に行けるかどうか,(1) 地図をはみ出していなくて,(2) 壁でない,ことを調べ る関数を作っておくと便利である.(1),(2) の順序でテストすること. C++ 1 2 3 4 5 回答骨子: bool valid(int x, int y) { return x が [0,W] の 範 囲 && y が [0,H] の 範 囲 && (x,y) が 壁 で な い ; } ’@’ の位置 (x,y) を探してそこから,深さ優先探索あるいは幅優先探索で訪問できた頂点の数 が答え. 問題 (国内予選 2006) Curling 2.0⋆ 氷の上を滑らせながら最小何回でゴールに到達できるか,10 回以内にはできないかを求める. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1144& lang=jp 今回は,パックの位置が移動するだけでなく壁が壊れるので,両方をモデル化する必要がある. 96 問題 Articulation Points⋆ (AOJ) 関節点を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=GRL_3_A& lang=jp 根についてと,根以外の頂点についてそれぞれ判定する.後者の方針: 各頂点を訪問する際に,根からの 深さ (距離; たどった辺の数) を記録する.探索中に back edge を見つけた場合は,辺の先の頂点の深さを見 て,その頂点がどこまで上に行けるかを管理する.各親子について,親の深さと子がどこまで上に上がれ るかを元に判定することができる. 問題 Bridges⋆ (AOJ) 橋を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=GRL_3_B& lang=jp 問題 (アジア地区予選 2002) Map of Ninja House⋆ 探索履歴から地図を復元する http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1236 問題 (模擬国内予選 2007) Karakuri Doll⋆ からくり人形師の JAG 氏作の人形の動作を確認せよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2017 97 第 11 章 最短路問題 こんな問題 10 B C 20 A 5 15 E 35 D A から D まで最も安くて幾らで行ける? A..E は町. 町をつなぐ道路は規定の料金がかかる. Ô 経路 {A,B,C,D} がコスト最小で 35 (最初に E に移動する と損) 11.1 重み付きグラフと表現 辺に重みが付いたグラフ上の最短路問題を扱う.たとえば,グラフの節点が都市,辺が移動手段,辺に ついたコストが所要時間だとすれば,最短路問題は早く目的地に移動する問題と対応する.コストが通行 料だとすると,最も安く目的地につく手段を求めることと対応する.辺に向きがある有向グラフの場合は, 一方通行を表現可能である.なお,向きがない場合は,有向グラフで逆向きのコストが常に等しい特殊ケー スと考えることができる. まずは,コストは非負であると仮定し (通行料を払うことはあっても報奨金をもらうことはない),後に 一般の場合を考える.この章の前半でのグラフの表現としては,もっとも簡単な隣接行列を用いる (10.1, 参考書 [3, pp. 90, 91]).隣接行列 K の i, j 要素 K[i][j] は,i から j に有向辺があればそのコスト,ない場 合は ∞ と表現する. 11.2 全点対間最短路 最短路問題を解くアルゴリズムには様々なものがあるが,まず全ての節点間の最短路を用いる Floyd- Warshall を覚えてくとよい (参考書 [3, p. 97]).見て分かるように for 文を 3 つ重ねただけの,簡潔で実装 が容易なアルゴリズムである. 1: 2: 3: 4: ▷ i から j への最短路のコスト K[i][j] を全て計算 ▷ 初期値は K[i][j] = dij (i,j 間に辺がある場合) procedure F LOYD -WARSHALL(int K[][]) for k = 1..N do for i = 1..N do ▷ または K[i][j] = ∞ (ない場合) ▷ 都市番号が 1..N でない場合は,適宜変更すること ▷ 添字 k を i や j と入れ替えると動かないので注意! for j = 1..N do 98 if K[i][j] > K[i][k] + K[k][j] then K[i][j] ← K[i][k] + K[k][j]; 5: 6: end if end for end for 7: 8: 9: 10: 11: ▷ k を経由すると安い場合に更新 end for end procedure 動作の概略は以下の通り: 初期状態で K[i][j] は直接接続されている辺のみ通る場合の移動コストを表す. アルゴリズム開始後,はじめに k = 1 のループが終了すると,K[i][j] は,i − j と移動する (直接接続されて いる辺を通る) か「i − 1 − j と順に移動する」場合の最小値を表す.k = 2 のループが終了すると,K[i][j] は,i − j または i − 1 − j または i − 2 − j または i − 1 − 2 − j または i − 2 − 1 − j と移動するルートの最 小値を表す.一般に k = a のループが終了時点で K[i][j] は,経由地として 1..a までを通過可能なパスのコ ストの最小値を表す. 証明概略 a a 都市 a までを経由地に含む i から j の最短路 (の一つ) を Dij と表記する.a ≥ 2 の時 Dij は,a を含 む場合と含まない場合に分けられる.含まない場合は,都市 a に立ち寄っても遠回りになる場合で, a−1 Dij と同一である.含む場合は,i から a を経由して j に到達する場合である.ここで,i から a ま での移動や a から j までの移動で a を通ることはない (ものだけ考えて良い).(各辺のコストが非負な ので,最短路の候補としては,各都市を最大 1 度だけ経由するパスのみを考えれば十分である.) 従っ a−1 a−1 て i から a までの移動や a から j までの移動の最短路はそれぞれ,Dia と Daj である.a に立ち寄 a−1 a−1 a−1 a る場合と立ち寄らない場合を総合すると,Dij = min(Dij , Dia + Daj ) となる. 11.2.1 例題 例題 A Reward for a Carpenter (PC 甲子園 2005) 大工がどこかへ行って戻ってくる.(原文参照) http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0117 入出力 今回の入力はスペース区切りではなくカンマ (,) で区切られて与えられる.このようなデータを読 む場合には scanf を用いると楽ができる. C++で使う場合の注意点としては,cstdio を include することと,scanf を使う場合は cin は使わな いこと. C++ 1 2 3 4 5 6 7 8 9 10 #include <iostream> #include <cstdio> using namespace std; int N, M, A, B, C, D, x1, x2, y1, y2; int main() { scanf("%d%d", &N, &M); for (int i=0; i<M; ++i) { scanf("%d,%d,%d,%d", &A, &B, &C, &D); cerr << "read " << A << ’ ’ << B << ’ ’ << C << ’ ’ << D << endl; 99 11 12 13 14 Ô Ô // A // B B がコスト C A がコスト D } } 上限はいくつ? 街の数は最大 20 であるから,行列 K は十分に大きく設定する.注意点としては,街の番 号は 1 から 20 で与えられることと,C++の配列の先頭は [0] であることである.今回は配列を大き目に確 保して,必要な部分のみを使う ([0] は使わない) ことを勧める. C++ 1 int K[32][32]; プログラムとして実装するうえでは ∞ として有限の数を用いる必要がある.この数は,(1) どのような 最短路よりも大きな数である必要がある.最短路の最大値は全ての辺を通った場合で,各辺のコストの最 大値と辺の数の積で見積もることができる. (2) 2 倍してもオーバーフローしないような,大きすぎない数 である必要がある.(手順中 5, 6 行目で加算を行うため) C++ 1 const int inf = 1001001001; 多くの場合は 10 億程度の値を使っておけば十分である.(見積もりを越えないことを検算すること) 隣接行列の初期化 入力を読み込んで隣接行列を設定する部分をまず実装しよう.そして,隣接行列を表 示する関数 void show() を作成し,表示してみよう.表示部分は前回の関数を流用可能である.ただし, 今回は 0 列目と 0 行目は使わないことに注意. サンプル入力に対しては以下の出力となることを確認せよ.(inf の代わりに具体的な数が書かれていて も問題ない.また桁が揃っていなくても,自分が分かれば問題ない.) inf 2 3 2 inf inf 4 inf inf 4 inf 4 inf 3 inf inf inf 1 2 inf inf 2 2 inf inf inf inf inf 1 1 inf inf 2 1 2 inf Floyd-Warshall 続いて,最短路のコストを計算する Floyd-Warshall を実装する.もっとも外側の k に関 するループを行う度に,行列 K がどのように変化するかを確認すると良い. 最初のループ (k=1) 終了後 inf 2 4 4 inf inf 2 3 2 4 5 4 6 7 2 6 4 6 3 inf inf inf 1 1 inf inf 2 inf inf 2 inf 1 inf 2 1 inf 最終状態 100 4 2 2 4 4 6 4 5 5 3 5 4 3 2 4 5 4 2 3 2 3 2 2 2 3 3 3 1 1 1 3 4 2 1 2 2 回答の作成 さて問題で要求されている回答は, 「大工の褒美」であり,それは「柱の代金」-「殿様から大 工が受け取ったお金」-「大工の町から山里までの最短コスト」-「山里から大工の町までの最短コスト」で ある.行列 K の参照と,適切な加減算で,回答を計算し出力せよ. Accept されたら他の方法でも解いてみよう. 11.2.2 負の重みを持つ辺がある場合 例題 Shortest Path - All Pairs Shortest Path (AOJ) 全頂点間の最短路を求めよ.ただし負の重みがありうる. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=GRL_1_C これまでは非負の重みを持つ辺のみを考えていたが,負の重みを持つ辺 (負辺) としてモデル化すること が適切な場合もありうる.たとえば,ある区間では通行料を払う代わりに小遣いをもらえるスゴロクなど を考えよう.そのような場合に,最短路の概念はどう変化するだろうか? コストが負でありうる場合は非 負の場合に成り立つ性質のいくつかは成り立たないため,注意が必要である.特に,コストの総和が負で ある閉路,負閉路 (negative weight cycle) がある場合には,そこを回り続けるとコストは下がり続けるため に最短路が定義できない.始点から終点までの途上に負閉路が無ければ最短路は定義可能で,(正ではない かもしれないが) 最小のコストが定まる. Floyd-Warshall 法は幸い負閉路があっても動作し,終了時各点 i について K[i][i] < 0 であるなら点 i を 含む閉路が存在する.ただし実装にあたっては,辺の有無に注意を払う必要がある.たとえば上記の手順 で 5 行目の,“if K[i][j] > K[i][k] + K[k][j]” という部分に,辺 ik と kj の存在を条件に加えること.辺の コストが非負であれば存在しない辺のコストを inf とすることで辺の存在をついでに確認できたが,負辺が 存在する場合は片方が inf でも足すと小さくなることがある. 11.3 単一始点最短路 始点を一つ定めて始点から各点への最短路を求める問題は,全点対間で最小距離を求めるよりも効率的 に解くことができる.往復のコストを求める場合は,往路と復路のそれぞれで単一始点最短路問題を 2 回 解くと解が得られる. 11.3.1 緩和 (relaxation) A dA = 0 10 B 20 dB = ∞ C dC = ∞ 101 5 D dD = ∞ はじめに,上のような単純なグラフを考え,A を始点として D までの距離を求める問題を考える. 定義: d[x] を A から x までの最短コストの上限とする. 初めに,d[A] = 0 (A から A まではコストがかからない),d[B] = d[C] = d[D] = ∞ (情報がないので ∞) と定める. A 10 dA = 0 B 20 C 5 dC = ∞ dB =10 D dD = ∞ 定義:緩和操作 ある辺 (s,t) とそのコスト,w(s, t) に対して,d[t] > d[s] + w(s, t) である場合に,d[t] = d[s] + w(s, t) と d[t] を減らす操作を緩和と呼ぶ.δ[t] を t までの真の最小距離とすると δ[t] ≤ δ[s] + w(s, t) であるので,全 ての節点で δ[n] ≤ d[n] が保たれている状態で,この操作を行っても δ[t] ≤ d[t] が保たれる.辺 AB に着目 すると,d[B] = min(d[B], d[A] + 10) = 10 となり,d[B] は ∞ から 10 に変化する. A 10 dA = 0 A B 20 dB =10 10 dA = 0 B C 5 dC =30 20 dB =10 C dC =30 D dD =∞ 5 D dD =35 同様に,d[C] = min(d[C], d[B] + 20) = 30,d[D] = min(d[D], d[C] + 5) = 35,と進めると D までの距 離が求まる.d が変化しなくなるまで緩和を繰り返すと,真の最短コストが得られる.どの順番に緩和を行 うかで効率が異なる.以下,頂点の集合を V , 有向辺の集合を E, 辺 uv の重みを w(u, v), 始点を vs で表す. 11.3.2 Bellman-Ford 法 (参考書 [3, p. 95]) 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: procedure B ELLMAN -F ORD(V, E, w(u, v), vs ) for v ∈ V do d[v] ← ∞ ▷ 初期化: 頂点までの距離の上限は ∞ end for d[vs ] ← 0 for (|V | − 1) 回 do ▷ 初期化: 始点までの距離は 0 ▷ |V | 回以上でも可 for 辺 (u, v) ∈ E do d[v] ← min(d[v], d[u] + w(u, v)) ▷ 緩和 end for end for end procedure • 緩和の順番は? – 適当に一通り • いつまで続ける? 停まる? – (頂点の数-1) 回ずつ全ての辺について繰り返す • 本当に最短? – 最短路の長さは最大 |V | − 1 であることから証明 (負閉路がない場合) 102 11.3.3 Dijkstra 法 (参考書 [3, p. 96]) 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: procedure D IJKSTRA(V, E, w(u, v), vs ) for v ∈ V do d[v] ← ∞ end for d[vs ] ← 0 S←∅ Q←V ▷ 初期化: 始点から始点までの距離は 0 ▷ 最短距離が確定した頂点の集合,最初は空 ▷ 最短距離が未確定の頂点の集合,最初は全て while Q ̸= ∅ do select u s.t. arg minu∈Q d[u] ▷ 最短距離が未確定の頂点がなくなるまで繰り返し ▷ 「最短距離が未確定の頂点」で d[u] が最小の u を選択 S ← S ∪ {u}, Q ← Q \ {u} for v ∈ Q s.t. (u, v) ∈ E do d[v] ← min(d[v], d[u] + w(u, v)) 14: end for end while 15: end procedure 13: ▷ 初期化: 始点から各頂点までの距離の上限は ∞ ▷ u までの最小距離は確定 ▷ 緩和 • 緩和の順番は? – 最短コストが確定している頂点 u ∈ S から出ている辺の行き先で最もコストが低い 頂点 v (線形探索または priority queue 等で管理) • いつまで続ける? 停まる? – Q が空になると停止 • 本当に最短? – 背理法で証明 (w が非負の場合) 11.3.4 手法の比較と負辺 頂点の数を V とすると,Floyd-Warshall 法は,for 文の内側を見て分かる通り,V 3 回の基本演算が行わ れる.制限が 1 秒程度であるとすると,V = 100 程度であれば余裕であるが,V = 1, 000 になるともう難 しい.オーダ記法を用いると O(V 3 ) となる.辺の数を E とすると,Bellman-Ford 法が O(V E), Dijkstra 法 が (実装によるが) O(V 2 ) または O(E log V ) 程度で,少し効率が良い.辺の数 E は,完全グラフでは V 2 程度,木に近い場合は V 程度なので,Bellman-Ford 法や Dijkstra 法が Floyd-Warshall 法よりどの程度早く なるかどうかはグラフの辺の数にも依存する. 問題 Single Source Shortest Path I (AOJ) 都市 0 から全ての都市への最短路を求めよ.都市には 0 から |V | − 1 までの番号がついてい る.入力は,1 行目に都市の数 |V |,続く |V | 行に各節点からの接続情報が与えられる. 詳しくは問題文を参照.都市や辺の数が多いので,隣接行列ではなく隣接リストで表現する と良い. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=GRL_1_A Bellman-Ford 法で解いてみよう.サンプル入力 1 のグラフに対して,与えられた辺の順に処理を行った 場合の動作は次のようになる.(この例では偶然全ての辺を一度づつ見るだけで最短距離が求まったが,グ ラフの形と辺の並び順に応じて一般には |V | − 1 回必要である.) 103 d1 = ∞ d0 = 0 0 1 4 1 0 4 1 d2 = ∞ d3 = ∞ d2 = ∞ 2 d2 =4 0 2 d3 = ∞ d2 =3 d0 = 0 1 0 4 2 d2 = 3 1 C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 3 d3 = ∞ 1 5 2 3 2 d3 =4 d2 = 3 辺 {2,3} で緩和: d2 + 1 = 4 < ∞ 1 d1 = 1 1 5 2 d3 = ∞ 辺 {1,2} で緩和: d1 + 2 = 3 < 4 d1 = 1 4 3 5 2 3 辺 {0,2} で緩和:d0 + 4 = 4 < ∞ 1 1 辺 {0,1} で緩和: d0 + 1 = 1 < ∞ d0 = 0 d1 = 1 1 0 1 4 1 5 2 2 5 2 1 3 サンプル入力 1 の初期状態 d0 = 0 d1 = 1 1 0 1 d0 = 0 1 5 2 2 4 d1 =1 d0 = 0 1 3 d3 = 4 辺 {1,3} では更新が起こらない: d1 + 5 = 6 > 4 int V, E, R, S[500010], T[500010], D[500010]; // 問題で与えられる入力 int C[100010]; // 各頂点までの最短距離の上限 // 無限を表す定数を全頂点をたどる最大超に設定 const int Inf = 10000*100000+100; int main() { cin >> V >> E >> R; for (int i=0; i<E; ++i) cin >> S[i] >> T[i] >> D[i]; // 各辺を入力 ... // C を初期化: C[R] を 0 に,他を Inf に for (int t=0; t<V; ++t) { // V 回繰り返す bool update = false; for (int i=0; i<E; ++i) { int s = S[i], t = T[i], d = D[i]; // i 番目の辺の s,t,d に対して if (C[s] < Inf && ...) { // 辺 s,t で緩和できるなら C[t] = ...// C[t] を更新; update = true; // 更新が起こったことを記録 } } if (!update) break; // 辺を一巡して更新がなければ打ち切って良い } ... // 出力 } 104 Ruby 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 V,E,r = gets.split(’ ’).map(&:to_i) # 入力 Edge = [] E.times{ s,t,d = gets.split(’ ’).map(&:to_i) Edge << [s, t, d] } Inf = 10000*100000+100 # 全頂点をたどると最大 C = Array.new(V,Inf) # 各頂点までの始点からの距離の上限 C[r] = 0 # 始点 V.times{ # V 回 count = 0 Edge.each{|s,t,d| # 全ての辺 (s から t, コスト d) に対して if C[s] < Inf && C[t] > C[s]+d C[t] = ... # C[t] を更新 count += 1 end } break if count == 0 # 更新されなければ終了して良い } V.times{|i| puts (C[i] == Inf) ? "INF" : C[i] } Dijkstra 法 (11.3.3 節) で解く場合は,優先度つきキュー (priority queue, 6.2.3 章参照) を使うと便利であ る.C++には標準ライブラリとして priority queue があるのでそれを使うと良い.Ruby の場合は,今 のところオンラインジャッジで使えるライブラリがなさそうなので,自分で二分ヒープなどを実装する. 始点からの距離と都市のペア pair<int,int>を管理し,手順 9 行目で,距離の近い順に都市を取り出 す.そのために,手順 12 行目で更新が行われた頂点を優先度つきキューに push する.本来はキュー内部 にある頂点の距離を減らすことが出来ると効率が良いが,多くの標準ライブラリの実装では難しい.代わ りに重複して push し,二度目以降に取り出した頂点は無視する.注意点として,C++の標準ライブラリ の priority queue は大きい順に要素を取り出すので,比較関数を指定して動作を変更するか,距離の符号 を反転させて与えるような工夫が必要となる. 上記の方針で実装した Dijkstra 法の動作例を,以下に示す.優先度つきキュー P は,⟨ 始点からの距離, 頂 点番号 ⟩ のペアを要素として持ち,距離の小さい順に,先頭要素 (左端) が取り出されるとする.図のグラ フ 1 つが手順 8 行目からのループの 1 回の実行に対応する. 105 d0 = 0 0 d1 = ∞ d0 = 0 1 0 1 4 1 3 2 d3 = ∞ d2 =4 P = (⟨0, 0⟩) d1 = 1 1 1 4 5 4 2 1 d2 =3 2 d3 =6 d2 = 3 4 1 d2 = 3 1 3 d3 =4 P = (⟨4, 2⟩, ⟨4, 3⟩, ⟨6, 3⟩) d0 = 0 d1 = 1 1 0 1 5 2 d3 = ∞ 頂点 2 を確定して頂点 3 を緩和 P = (⟨3, 2⟩, ⟨4, 2⟩, ⟨6, 3⟩) d0 = 0 d1 = 1 1 0 1 4 3 5 2 3 頂点 1 を確定して頂点 2, 3 を緩和 2 1 ⟨0, 0⟩ を取り出し頂点 0 を確定,頂点 1, 2 を緩和 P = (⟨1, 1⟩, ⟨4, 2⟩) d0 = 0 d1 = 1 1 0 1 初期状態 2 5 2 4 d2 = ∞ 0 1 5 2 2 d0 = 0 d1 =1 1 5 2 3 2 d3 = 4 d2 = 3 1 3 d3 = 4 頂点 2 は既に確定しているので ⟨4, 2⟩ は無視 ⟨4, 3⟩ を取り出し頂点 3 が距離 4 で確定 P = (⟨4, 3⟩, ⟨6, 3⟩) P = (⟨6, 3⟩) 問題 Single Source Shortest Path II (AOJ) 重みが負の辺がある場合に,都市 0 から全ての都市への最短路を求めよ.詳しくは問題文を 参照. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=GRL_1_B 重みが負の辺がある場合は,Dijkstra 法は正しく動作しない. Bellman-Ford 法は最短路が定義される場 合には正しい解を発見可能で,負閉路の存在は頂点の個数 |V | 回目の更新を試みて成功するかどうかで確 認できる. 106 問題 Wormholes (USACO 2006 December Gold) ワームホールを通じて過去に戻れるルートを探す.出発地はどこを選んでも良いので,その 場所の過去の時刻に戻れれば良い. http://poj.org/problem?id=3259 11.4 練習問題 問題 (国内予選 2012) Railway Connection⋆ 最も安い運賃の経路を求める http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1182& lang=jp 問題 崖登り ⋆ (国内予選 2007) 崖を登る最短の時間を求める http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1150& lang=jp 問題 (冬合宿 2008) Magical Dungeon⋆ ヒットポイント H を持つ勇者が部屋 S から移動して部屋 T に到着した瞬間にモンスターと戦 う.最大いくつのヒットポイントで戦闘を迎えられるか? 通路には正負の数が割り当てられて いて,正ならヒットポイントを回復し,負なら失う.ヒットポイントが 0 以下になるような移 動はできない.ヒットポイントが H を超えるような移動は可能ではあるが,ヒットポイントは 最大 H までしか回復しない. 問 題 と デ ー タ セット: http://acm-icpc.aitea.net/index.php?2007% 2FPractice%2F%E5%86%AC%E5%90%88%E5%AE%BF%2F%E5%95%8F%E9%A1%8C%E6% 96%87%E3%81%A8%E3%83%87%E3%83%BC%E3%82%BF%E3%82%BB%E3%83%83%E3%83% 88 (day3, C) 注: 負の閉路をぐるぐるまわって回復してから戦うようなケースを考える必要がある.Bellman-Ford 法 の応用で解くことができるが,H が大きいケースを効率的に対処するには,少し工夫が必要. 107 問題 壊れたドア ⋆ (国内予選 2011) どこかのドアが壊れている条件で,最も都合が悪い場所で壊れたドアが見つかった時点から の迂回を考慮した最短路を求める. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1178& lang=jp 問題 The Most Powerful Spell⋆⋆ (国内予選 2010) 作成可能な中で,辞書順で最も早い呪文を求める. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1169& lang=jp 問題 Sums⋆ (10th Polish Olympiad in Informatics) 整数の集合 A が与えられる.質問として与えられる数が,A の要素の和で表せるかどうかを 答えよ.(正確な条件は原文参照) http://main.edu.pl/en/archive/oi/10/sum 問題 Kangaroos⋆⋆ (Algorithmic Engagements 2011) 複数のレンズのそれぞれについて,カンガルーの観察ポイントとして適する連続区間の最大 の長さを求めよ. http://main.edu.pl/en/archive/pa/2011/kan (この問題は最短路とは関係はほとんどない) 108 第 12 章 平面の幾何 (1) 12.1 概要: 点の表現と演算 概要 さまざまな状況で計算機で図形的問題を扱う機会に出会う.ここでは幾何の問題を扱う基本を紹介し たい (詳しく学ぶには専門の授業を受講されたい).たとえば,平行や図形の内外などよく馴染んだ概 念を, 「符号付き三角形の面積」という道具で,表してみよう.人間には簡単な図形の概念でも,プロ グラムとして記述する場合は直感的ではない方法適している場合がある.また浮動小数 (double x, y など) を扱うので,誤差に注意する必要も生ずる. 攻略 平面上の点を C や C++で表現する場合には,たとえば構造体を用いる方法がある (参考書 [2, pp. 365– (16 章)]).この資料では,少し楽をして以下のように複素数で表現する (実部 real() を x に,虚部 imag() を y に対応させる): Y P (-2,1) X O Q C++ 1 2 3 4 5 6 7 8 9 10 11 #include <complex> #include <cmath> typedef complex<double> xy_t; xy_t P(-2, 1), Q; // 初期化 cout << P << endl; // (debug 用) 表示 cout << P.real() << endl; // x 座標 cout << P.imag() << endl; // y 座標 Q = P + xy_t(5, -2); // 点 Q は点 P を (5,-2) だけ平行移動した位置とする Q *= xy_t(cos(a), sin(a)); // 点 Q を原点を中心に a(ラジアン) だけ回転 cout << abs(P) << endl; // ベクトル OP の長さ cout << norm(P) << endl; // norm(P) = abs(P)2 C (gcc) の場合 C++ 1 2 3 4 5 6 7 #include <complex.h> #include <math.h> complex a = 0.0 + 1.0I; // 初期化 complex b = cos(3.14/4) + sin(3.14/4)*I; printf("%f %f\n", creal(a), cimag(a)); // 実部,虚部 a *= b; // 乗算 printf("%f %f\n", creal(a), cimag(a)); 109 よく使う演算 Y Y a+b b b a a θ b cos θ X X 内積: |a||b| cos θ C++ 1 2 3 4 5 6 クロス (外) 積: 網掛け部分の符号付き面積 // 内積: a.x*b.x +a.y*b.y double dot_product(xy_t a, xy_t b) { return (conj(a)*b).real(); } // クロス (外) 積, ベクトル a,b が作る三角形の符号付き面積の二倍: a.x*b.y - b.x*a.y double cross_product(xy_t a, xy_t b) { return (conj(a)*b).imag(); } // 投影 原点と b を結ぶ直線に点 p を投影 xy_t projection(xy_t p, xy_t b) { return b*dot_product(p,b)/norm(b); } 三角形の符号付き面積の紹介が本章前半の主要なテーマである.これは原点と点 a,b が作る三角形の面 積を,符号付きで求める.符号は,原点と点 a,b がこの順で反時計回りの位置関係にある場合に正,時計 回りの場合に負となる.面積だけでなく,これから見るように向きの判定にも用いられる. 内積は,直線上に点を投影する際に便利である. 12.2 三角形の符号付き面積の利用 12.2.1 多角形の面積 例題 (PC 甲子園 2005) Area of Polygon 凸多角形の面積を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0079 Y p2 b = p2 − p0 p1 a = p1 − p0 p0 X (単純) 多角形は三角形に分解できるので,三角形の面積が計算できれば,多角形の面積が計算できる.特 に凸多角形の場合は,一つの頂点とその頂点を含まない辺が構成する三角形で綺麗に分割できる. 110 C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 xy_t P[110]; int main() { // 入力例 読み込んだ点の個数を N とする int N=0; double x, y; while (˜scanf("%lf,%lf", &x, &y)) { P[N++] = xy_t(x,y); } // 面積計算 double sum = 0.0; for (int i=0; i+2<N; ++i) { xy_t a=P[0], b=P[i+1], c=P[i+2]; sum += ... // 三角形 abc の面積を合計 } printf("%.6f\n", abs(sum)); } 例題 Polygon - Area (AOJ) 多角形の面積を計算する.凸とは限らないが,頂点は反時計回りで与えられる. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=CGL_3_A& lang=jp (類 題: Area of Polygons (国 内 予 選 1998) http://judge.u-aizu.ac.jp/ onlinejudge/description.jsp?id=1100 頂点が与えられる向きのみ異なる) 頂点が半時計回りに与えられる場合は,先ほどの分割を行うと三角形に重なりが生ずるが,頂点が時計 回りに与えられる場合は符号付き面積の符号を利用して合計すると,(不思議なことに?) 正しい面積を得ら れる. 12.2.2 平行の判定 例題 (PC 甲子園 2003) Parallelism 概要: A = (x1, y1), B = (x2, y2), C = (x3, y3), D = (x4, y4) の異なる4つの座標点が与えられた とき,直線 AB と CD が平行かどうかを判定せよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0021& lang=jp 回答方針: ベクトル AB とベクトル CD からなる三角形が面積を持つかどうかを判定すれば良い C++ 1 2 3 4 5 6 const double eps = 1e-11; double x[4], y[4]; int N; int main() { cin >> N; // 問題数 for (int t=0; t<N; ++t) { 111 7 8 9 10 11 12 13 14 15 16 for (int i=0; i<4; ++i) cin >> x[i] >> y[i]; // x0,y0..x3,y3 xy_t a[2] = { xy_t(x[0],y[0]) - xy_t(x[1],y[1]), xy_t(x[2],y[2]) - xy_t(x[3],y[3]) }; bool p = abs(a[0] と a[1] の 符 号 付 き 面 積 ) < eps; cout << (p ? "YES" : "NO") << endl; } } 補足: 向きや角度の判定には,sin や arg などのライブラリ関数を用いることもできるが,計算誤差の 観点から可能な限り符号付き面積で計算するほうが良い.たとえば三角関数の汎用的な実装方法では Taylor 展開が用いられる.1 double などの浮動小数を用いる時には, 12 の冪乗の和で表される数値以外は,必 然的に誤差を含む.この問題での入力は,絶対値が 100 以下かつ各値は小数点以下最大 5 桁までの数値と 数値誤差の取り扱い ⋆ 明示されたので,105 倍して整数 (long long) で扱えば誤差の影響を避けることができる.あるいは,サ ンプルコードの eps のように,誤差の範囲を予測する方法もある.二つのベクトル (a, b) と (c, d) にそれ ぞれの要素に誤差が加わった時に,(1) 平行の場合に |ad − bc| の取る最大値 (誤差がなければ 0) と,(2) 平 行でない場合に |ad − bc| の取る最小値を比較して,(1)< 閾値 <(2) となるよう閾値をとる.粗く見積もる と (1) は最大 (4 · 100) · (100 · 2−54 ) ≈ 2.2 · 10−12 程度 (100 · 2−54 は 100 までの数を double で表した時の 表現誤差,400 は |(a + ε)(d + ε) − (b + ε)(c + ε)| を展開した時の ε にかかる係数の見積もり), (2) は 10−10 程度 (入力が表現可能な 10−5 値の自乗より).なお,環境依存になるが比較的新しい Intel や AMD の CPU と比較的新しい gcc を用いる場合は,long double や float128 などを用いることで 80 bit や 128 bit というより良い精度で演算することもできる.それぞれ注意点があるので,使用する場合は文献を調査の こと. 12.2.3 内外判定 例題 (PC 甲子園 2003) A Point in a Triangle 平面上に (x1, y1), (x2, y2), (x3, y3) を頂点とした三角形と点 P(xp, yp) がある.点 P が三角形 の内部 (三角形の頂点や辺上は含まない) にあるかどうかを判定せよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0012& lang=jp 回答例: 三角形の各点を a,b,c とすると,3つの三角形 pab, pbc, pca の符号付き面積を考える.p が abc の内部にあれば符号は一致し,外部にあれば一致しない. 1 参考: FreeBSD の実装 https://svnweb.freebsd.org/base/release/10.1.0/lib/msun/src/k_cos.c?view= markup 112 b b p 左 c p 左 左 右 c 左 左 a 点が図形の内部 C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 a 点が図形の外部 double x[4], y[4]; int main() { while (true) { for (int i=0; i<4; ++i) cin >> x[i] >> y[i]; if (!cin) break; xy_t a(x[0],y[0]), b(x[1],y[1]), c(x[2],y[2]), p(x[3],y[3]); // pab の符号付き面積の 2 倍は,cross product(a-p,b-p) // pbc の符号付き面積の 2 倍は,cross product(b-p,c-p) // pca の符号付き面積の 2 倍は,cross product(c-p,a-p) bool ok = 符 号 が 揃 っ て い る cout << (ok ? "YES" : "NO") << endl; } } 凸とは限らない多角形の内外判定については,次の節を参照. 12.2.4 凸包 問題 Convex Polygon - Convex Hull⋆ (AOJ) 与えられた点の集合の凸包を求めよ.凸包が何かは類題の図を参照. (注: 辺上の点を出力させる,少し特殊な設定である) http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=CGL_4_A& lang=jp (類題: Enclose Pins with a Rubber Band (PC 甲子園 2004) http://judge.u-aizu.ac.jp/ onlinejudge/description.jsp?id=0068 こちらの方が素直な設定) 回答例: 点を X 座標でソートし,最小値 (左端の点) から順に右に向かい,まずは下半分の外周を構成す る.途中現在の進行方向より右に向かうことになったら,(本来凸法に含まれない点を入れてしまった状況 であるので) 原因となった点をすべて取り除く.上半分も右端から同様に行う. 点の個数を N とすると,上記の半周を求める手続きは O(N ) でできるため,ソートに要する時間を含め て全体の計算時間は O(N log N ). 113 12.3 様々な話題 例題 (PC 甲子園 2005) A Symmetric Point 点 Q と線対称の位置にある点を出力せよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0081 回答例: 直線上に Q を投影した点を S とする.すると求める点 R は,S をベクトル QS だけ平行移 動した位置にある.カンマ区切りで与えられる入力の処理と,また小数点以下の出力桁数の制御には, #include<cstdio>して以下のように scanf と printf を用いるのが簡便である. C++ 1 2 3 4 5 6 7 8 9 10 11 #include <cstdio> double X1,Y1,X2,Y2,XQ,YQ; int main() { while (˜scanf("%lf,%lf,%lf,%lf,%lf,%lf", &X1, &Y1, &X2, &Y2, &XQ, &YQ)) { xy_t P1(x1,y1), P2(x2,y2), Q(xq,yq); xy_t R = ...; // 線対称の点を計算 printf("%.6f %.6f\n", real(R), imag(R)); } } 問題 Polygon - Polygon-Point Containment (AOJ) 凸とは限らない多角形について点の内外を判定せよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=CGL_3_C& lang=jp (注: 通常は点が辺上かどうかを判定することは難しいが,今回は整数座標なので可能) 回答例: 調べたい点から任意の方向に半直線を伸ばし,横切る辺の数を数える.偶数ならば外部,奇数な らば内部である.線分が直線と交点を持つかどうかなどの関数を事前に用意しておく必要がある.半直線 が頂点のすぐ近くを通過する場合は,誤差の影響を心配しないで済むよう,角度を変えたほうが無難. 問題 Point Set - Closest Pair⋆ (AOJ) 最近点対を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=CGL_5_A& lang=jp 点の個数を N して,点のペアを全て試すと O(N 2 ) の時間が必要だが,分割統治により O(N log N ) で 可能.なお O(N ) の乱択アルゴリズムも存在する. 分割統治の方針: X 座標毎と Y 座標毎のそれぞれで点をソートしておく.X 座標で点を半分に分け,左 と右でそれぞれ最近点対を再帰的に求める.求めた二つの最小距離の小さい方を d とする.全体の最近点 114 対は,左のみの最近点対,右のみの最近点対,左の点と右の点の最近点対のいずれかである.後者は左右 の区切りの線から距離 d 以内のものについて Y 座標順に並べた時に,定数 (たとえば 8) 以内のペアのみ候 補となる性質を用いると,点の個数に対して線形の計算ステップで判定可能である.証明は区切りの線の 周囲の適当な正方形グリッドを考えると,d の制限で点があまり密に存在できないことから.なお,実装 では,毎回 Y 座標順にソートしていると O(N log N ) を実現できない.初めに一度全体をソートしておき, 分割の際にそこから振り分けると良い.また分割の際には,複数の点が同じ Y 座標を持つ場合に注意を払 う必要がありうる.実用的には,初めに全体をランダムに回転させると避けられる. 12.4 応用問題 問題 (夏合宿 2012) ConvexCut 与えられた図形に,どの角度で分割しても同じ面積で切れるような点が存在するかを求める. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2442& lang=jp 多角形の頂点が偶数であり,かつ組みになるべき辺が全て平行である時に限られること,あるいは組に なる頂点の中点が等しいことなどで判定できることが証明できる. 回答例 (入出力) C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <complex> #include <iostream> #include <cstdio> using namespace std; int N, x, y; typedef complex<double> xy_t; xy_t P[60]; void solve() { ... } int main() { while (cin >> N) { for (int i=0; i<N; ++i) { cin >> x >> y; P[i] = xy_t(x,y); } solve(); } } 回答例 (中点の計算) C++ 1 2 3 4 5 6 7 8 void solve() { // 奇数だったらそのような点はない ... xy_t a = (P[0]+P[N/2])*0.5; // P[0] と P[N/2] の中点 for (int i=1; i<N/2; ++i) { xy_t b = ... // P[i] と P[i+N/2] の中点 // a と b が誤差を加味しても一致していなければ,abs(a-b) > eps,求める点はない } 115 9 10 printf("%.5f %.5f\n", a.real(), a.imag()); } 問題 (国内予選 2004) Circle and Points⋆ xy 平面上に N 個の点が与えられる.半径 1 の円を xy 平面 上で動かして,それらの点をな るべくたくさん囲むようにする.このとき,最大でいくつの点を同時に囲めるかを答えなさい. ここで,ある円が点を「囲む」 とは,その点が円の内部または円周上にあるときをいう. (こ の問題文には誤差に関する十分な記述がある) http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1132& lang=jp 候補となる円の位置は無数にあるので,候補を絞る.(「ぎりぎりを考えよ」参考書 [3, p. 229]) 同時に囲める最大の点を n として,n 個を囲んだ円があったとする.もしその円に点が接してないので あれば,内部の点のどれかが接するまで動かしても,囲んでいる点の数は変わらない.すなわち,点のど れかと接する円だけを考えれば十分である (残りの円を考慮に入れても,答えは変わらない).しかしその ような円はまだ無数にある. n 個を囲んだ円があり,一点が円上に乗っているとする.その点を中心に円を回転させると,内部の点 が新たに円と接するまで動かしても,囲んでいる点の数はかわらない.すなわち,2 点を通る円だけを考 えれば十分である (残りの円を考慮に入れても,答えは変わらない).そのような 2N 2 程度しかない. 例外: 答えが 1 の時 回答例: C++ 1 2 3 4 5 6 7 8 9 10 11 int main() { // 最大値=1 for (/*点p*/) { for (/*点q*/) { // p!=q if (/* p q を 通 る 円 が あ れ ば */) { // 0 個,1 個,2 個の場合がある /*全ての点を確かめながら,内部の個数を数える*/ /*最大値を越えていれば更新する*/ } } } } 問題 (国内予選 2008) Roll-A-Big-Ball 条件を満たす最大の大玉の大きさを求める. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1157& lang=jp 116 問題 (国内予選 2012) Chain-Confined Path⋆ 円環を通る最短経路を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1183& lang=jp 始点,終点,各円の交点を考えて,各点の間を直線で移動可能かを求めた後は,通常の最短路問題となる. 問題 (模擬地区予選 2009) Neko’s Treasure⋆ 壁をいくつ乗り越えるかを求める (問題文参照) http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2181 問題 (アジア大会 2003) Area of Polygons⋆ 網掛け部分の面積を求める. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1242 考え方: 薄切りにして和を求めるだけなのだが,複数の線が同じますを通る場合など,注意点がそれなり にある. 参考: http://www.ipsj.or.jp/07editj/promenade/4501.pdf 問題 (夏合宿 2012) Treasure Hunt⋆ 領域内の宝を (効率的に) 数える http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2426 純朴に数えていると時間がかかりすぎるので,点が与えられた時点で前処理を行い,質問に答える準備 を整えておく.質問に効率的に答えるためのデータ構造としては,たとえば四分木 (quad tree) を使うこと ができる. 問題 Army Training⋆⋆ (Algorithmic Engagements 2010) 平面上の 1000 個までの点が与えられる.それらを適当につないで単純多角形を作った際に, 領域に含まれる点の個数を数えよ. http://main.edu.pl/en/archive/pa/2010/pol 117 問題 Altars⋆⋆ (6th Polish Olympiad in Informatics) 中国では悪霊は直線上に進むと信じられているという枕. 長方形の寺院があり,中央に祭壇がある.寺院の壁は,東西または南北のいずれかの方向か らなる.寺院の入り口は,ある辺の中央にある.外部から祭壇に視線が通っているかどうかを 調べよ. http://main.edu.pl/en/archive/oi/6/olt 問題 Fish⋆⋆ (Algorithmic Engagements 2009) 毎日同じ時刻に起床/就寝する魚がいる.寝て起きると,海流の影響で一マスずれていること がある.昨日の同時刻の自分の位置が見えるように泳ぐ.加減速は自在.一日の魚の動きを観 察したルートの記録がたくさんあるが,日付が分からない.最小で何尾の魚を観察していると 言えるか. archipelago=群島 http://main.edu.pl/en/archive/pa/2009/ryb 118 第 13 章 簡単な構文解析 こんな問題 特定の文法で書かれた文字列を計算機に理解させよう: • (V |V )&F &(F |V ) Ô • 35=1?((2*(3*4))+(5+6)) • 4*x+2=19 Ô F (真偽値の計算) Ô ’+’ (演算子の推定) x=4.25 (方程式を解く) • C2H5OH+3O2+3(SiO2) == 2CO2+3H2O+3SiO2 (分子量の計算) 13.1 四則演算の作成 13.1.1 足し算を作ってみよう 大域変数: C++ 1 2 3 const string S = "12+3"; size_t cur = 0; // 解析開始位置 int parse(); 1 2 3 4 5 int main() { int a = parse(); assert(a == 15); assert(cur == S.size()); } 実行例: C++ assertって何? (再掲) ソースコード C++ 1 2 3 4 5 6 #include <cassert> int factorial(int n) { assert(n > 0); // (*) if (n == 1) return 1; return n * factorial(n-1); } 実行例 C++ 119 1 2 cout << factorial(3) << endl; // 6 を表示 cout << factorial(-3) << endl; // (*) の行番号を表示して停止 足し算の (いい加減な) 文法 Expression := Number ’+’ Number Number := Digit の繰り返し Digit := ’0’ | ’1’ | ... | ’9’ 読みかた: (参考: (Extended) BNF) • P := Q Ô P という名前の文法規則の定義 • A B Ô A の後に B が続く • ’a’ Ô 文字 a • x | y Ô x または y 文法通りに実装する (Digit) Digit := ’0’ | ’1’ | ... C++ 1 2 3 4 5 6 7 | ’9’ #include <cctype> int digit() { assert(isdigit(S[cur])); // S[cur] が数字であることを確認 int n = S[cur] - ’0’; // ’0’ を 0 に変換 cur = cur+1; // 一文字進める return n; } 文字の数字への変換: C や C++では文字はその文字を表す数字で管理される.言語の規格では文字コードが規 定されていないが,現状で我々が使う環境では ASCII コードと考えて良い.その中で,’0’,’1’,’2’,. . .,’a’,’b’,’c’ などの文字の順番でコードが割り当てられていることを利用すると,上記の減算により’0’ からいくつ後の 文字かが分かり,それがすなわち求めたい数値である.ASCII コード表を確認する手段の一つは,man コ マンドが簡便で,ターミナルで man ascii とすると良い. 文法通りに実装する (Number) Number := Digit の繰り返し C++ 1 2 3 4 5 6 int number() { int n = digit(); while (cur < S.size() && isdigit(S[cur])) // 次も数字か 1 文字先読 n = n*10 + digit(); return n; } 120 文法通りに実装する (Expression) Expression := Number ’+’ Number C++ 1 2 3 4 5 6 7 8 int expression() { int a = number(); char op = S[cur]; cur += 1; int b = number(); assert(op == ’+’); return a + b; } 足し算だけならこれで動くはずである: C++ テスト 1 2 3 4 5 6 7 8 const string S = "12+3"; size_t cur = 0; // 解析開始位置 .. int parse() { return expression(); } int main() { int a = parse(); cout << a << endl; // 15 が出力されるはず; } “12+5” 以外にも “1023+888” など試してみよう 拡張: 引き算を加えよう “12+5” を “12-5” としてみよう 方法: expression 関数で op が’+’ か’-’ を判定する C++ 1 2 if (op == ’+’) return a + b; else return a - b; (assert も適切に書き換える) 拡張: 3 つ以上足す “12+5” を “1+2+3+4” としてみよう expression を書き換えて,複数回足せるようにする C++ 1 2 3 4 5 6 7 8 9 10 11 int expression() { int sum = number(); while (S[cur] == ’+’ || S[cur] == ’-’) { // 足し算か引き算が続く間 char op = S[cur]; cur += 1; int b = number(); if (op == ’+’) sum に b を た す ; else sum か ら b を 引 く ; } return sum; } 121 次の拡張 • 掛け算,割り算に対応: 演算子の優先順位が変わるので新しい規則を作る • (多重の) カッコに対応: 同新しい規則を作って再帰する 13.1.2 カッコを使わない四則演算の (いい加減な) 文法 四則演算の (いい加減な) 文法 Expression := Term { (’+’|’-’) Term } Term := Number { (’*’|’/’) Number } Number := Digit { Digit } 読みかた: (参考: (Extended) BNF) • A B Ô A の後に B が続く • {C} Ô C の 0 回以上の繰り返し 例: 5*3-8/4-9 • Term: 5*3, 8/4, 9 • Number: 5, 3, 8, 4, 9 四則演算の実装 (Term) Term := Number { (’*’|’/’) Number } C++ 1 2 3 4 5 6 7 8 9 10 int term() { int a = number(); while (cur < S.size() && (S[cur] == ’*’ || S[cur] == ’/’)) { char op = S[cur++]; int b = number(); if (op == ’*’) a *= b; else a /= b; } return a; } 四則演算の実装 (Expression) Expression := Term { (’+’|’-’) Term } C++ 122 1 2 3 4 5 6 7 8 9 10 int expression() { int a = term(); while (cur < S.size()) && (S[cur] == ’+’ || S[cur] == ’-’)) { char op = S[cur++]; int b = term(); if (op == ’+’) a += b; else a -= b; } return a; } 13.1.3 四則演算: カッコの導入 式全体を表す Expression が,カッコの中にもう一度登場 カッコを導入した文法 Ô 再帰的に処理 Expression := Term { (’+’|’-’) Term } Term := Factor { (’*’|’/’) Factor } Factor := ’(’ Expression ’)’ | Number factor() の実装例は以下: C++ 1 2 3 4 5 6 7 8 9 int expression(); // 前方宣言 int factor() { if (S[cur] != ’(’) return number(); cur += 1; int n = expression(); assert(S[cur] == ’)’); cur += 1; return n; } term() の実装も,文法に合わせて調整すること. 13.1.4 まとめ 実装のまとめ: • 文法規則に対応した関数を作る • 帰り値の型は解析完了後に欲しいものとする Ô – 四則演算 整数 – 多項式 Ô 各次数の係数 – 分子式 Ô 分子量,各原子の個数… 文法の記述: • 注意点: 演算子の優先順位や左結合や右結合 • 制限: 1 文字の先読みで適切な規則を決定できるように (LL(1)) 123 P := A { ’+’ A } の繰り返しを再帰で記述する? 補足 • P := P ’+’ A | A Ô このまま実装すると P でずっと再帰 • 右結合に変換すると一応解析可能 – P := A P’ – P’ := ’+’ A P’ | ϵ (ϵ は空文字列) 13.2 練習問題 問題 (PC 甲子園 2005) Smart Calculator 電卓を作る http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0109& lang=jp 回答例: C++ 1 2 3 4 5 6 7 8 9 10 11 12 /*const*/ string S; // 値を変更するので const 属性を削除 ... int main() { int N; cin >> N; for (int i=0; i<N; ++i) { cur = 0; cin >> S; S.resize(S.size()-1); // 最後の=を無視 cout << expression() << endl; } } 問題 (アジア地区予選 2003) Molecular Formula 各原子の原子量を元に,各分子の分子量を求める. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1244 初めに表を作成して,原子を原子量に変換できるようにしておけば,後は乗算と加算の演算. 問題 如何に汝を満足せしめむ? いざ数え上げむ (国内予選 2008) 論理式を満たす変数の割り当て方を求める. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1155& lang=jp 124 回答例: 1. 変数がなければ式の値を求めることは簡単である.すなわち,文字列内部の P, Q, R をそれぞれ 0,1,2 に置換してから構文解析して求めれば良い.P, Q, R への値の割り当ては 33 通りあるので,それだけ 繰り返す. 2. (特に C++11 でお勧め)P, Q, R への値の割り当てを整数の配列 int a[3] で表すとする.割り当て a を引数に取り,その割り当てでの式の値を返す関数を構文解析で作成する.構文解析結果を活用する 部分は以下のようになる: C++11 1 2 3 4 5 6 7 8 9 10 11 12 int solve() { cur = 0; auto tree = parse(); int count = 0; for (int p:{0,1,2}) for (int q:{0,1,2}) for (int r:{0,1,2}) { int a[] = {p, q, r}; if (tree(a) == 2) ++count; } return count; } 割り当てに対する式の値を返す関数は細かい関数を組み合わせて作る.たとえば文字 c が数を表す場 合,割り当てによらず同じ値を返す定数関数を作成する return [=](int[3]){ return c-’0’; };.アルファベットだった場合は,引数に応じた値を返すので return [=](int a[3]){ return a[c-’P’]; }; というようにすれば良い.括弧でくくられた二項演算子の場合,四則演算の時と同 じように左側と右側を解析した関数を left, right などと作成したうえで return [=](int a[3]) { return min(left(a), right(a)); }; のように左右の関数を呼び出す関数を作成 する. C++11 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <functional> typedef function<int(int[3])> node_t; node_t parse() { char c = S[cur++]; if (isdigit(c)) return [=](int[3]){ return c-’0’; }; if (isalpha(c)) return [=](int a[3]){ return a[c-’P’]; }; node_t left = parse(); if (c == ’-’) // left(a) に対して’-’ の演算を行う return [=](int a[3]) { return ...; }; assert(c == ’(’); char op = S[cur++]; node_t right = parse(); ++cur; // ’)’ // 以下二項演算子なので left(a) と right(a) に対して.. if (op == ’*’) return [=](int a[3]) { return ...; }; // ’*’ の演算を行う return [=](int a[3]) { return ...; }; // 同’+’ の演算を行う } 125 問題 Equation Solver (Ulm Local 1997) 簡単な方程式を解く http://poj.org/problem?id=2252 回答例: 右辺と左辺をそれぞれ解析し,一次の係数と定数項を両辺で比較. 問題 (アジア地区予選 2010) Matrix Calculator 行列の掛け算をしよう http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1314 問題 (アジア地区予選 2011) ASCII Expression ASCII art 風に描かれた算数を計算する. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1322 問題 (アジア地区予選 2009) Chemist’s Math 与えられた反応に必要な各物質の比を求める. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1300 回答例: 各分子の構成を調べて,連立方程式を解く. 問題 Questions⋆⋆ (Algorithmic Engagements 2008) P 人の王子と魔法使いのそれぞれの知識状態を上手にシミュレートして,質問になんと答え るかを当てる. http://main.edu.pl/en/archive/pa/2008/pyt 補足 • Limitations: に,可能な変数の組み合わせは最大 600 とか,m計算途中の変数の値は絶対値 100 万を 越えないなど,重要なことが書いてある • サンプル入力と解説で “S 1 7 All sons know that there are less than 3 golden crowns.” とあるが,すぐ後 に “M 1 7” があるのでこの説明で正しい.そうでなければ変数 7 の実際の値は, “S 1 7” からは読み 取れない. • 担当者の回答は 160 行くらい. 126 第 14 章 整数と連立方程式 14.1 素因数分解、素数、ユークリッドの互除法など 例題 Prime Factorize (AOJ) 整数を素因数分解する http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=NTL_1_A& lang=jp 小さい数から順に割り切れるか試すと良い.被除数の平方根より大きな数で割る必要はない (なぜか?) 例題 Greatest Common Divisor (AOJ) 最大公約数を求める。 http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ALDS1_1_ B&lang=jp ユークリッドの互除法というアルゴリズムが定番である.上記の問題の解説や参考書 攻略 [2, pp. 441– 443],参考書 [3, pp. 107–] 参照.最大公約数を利用して簡単に最小公倍数も求められる (http://judge. u-aizu.ac.jp/onlinejudge/description.jsp?id=NTL_1_C&lang=jp ). 例題 (PC 甲子園 2004) Sum of Prime Numbers 与えられた数 n に対して,素数を小さい方から並べた時に n 番目の素数までの和を出力せよ。 http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0053 エラトステネスの篩というアルゴリズムが定番である.はじめに,数が素数かどうかを記録する大きな 配列を用意し,すべての数が素数であるとしておく.2 の倍数を 2 を除いて消去する,3 以外の 3 の倍数を 消去する,5 以外の 5 の倍数を消去する.というように「まだ消されていない中で最大の数を素数として 残した後、その倍数を消去する」という手順を繰り返す.判定したい最大の素数の平方根より大きな素数 については,その倍数を消す必要はない.参考書 攻略 [2, pp. 438–439] 参照. 127 問題 Galaxy Wide Web Service (夏合宿 2009) Web サービスに,様々な惑星から周期的なアクセスがある.惑星 i の,一日の時間は 1 ≤ di ≤ 24 時間で,今の時刻は ti 時である.各惑星からのアクセス量はその惑星の時刻のみに依存して決 まる.(幸いなことにどの惑星の時刻も地球の 1 時間が単位である.) 最もアクセスが多くなる 時間帯のアクセス量を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2162 方針: • もし全周期求められればだいたい分かる サンプル入力のように (1 2 3 4) と (2 1) という惑星であれば、 地球時間 0 1 2 3 4 5 6 7 惑星 1 1 2 3 4 1 2 3 4 惑星 2 2 1 2 1 2 1 2 1 合計 3 3 5 5 3 3 5 5 と,アクセス数は 4 時間の周期の繰り返しとなるので答えは 5 • 全周期は? 惑星の時間数の最小公倍数.1 から 24 まで全ての時間が揃うと,5354228880 になる (無理) • 13, 17, 19, 23 を特別扱いする.残りの数の最小公倍数は 55440 となる.この時刻であれば列挙可能 である.特別扱いした 4 つの数は,他のどの周期とも互いに素なので互いの最大値が重なる時刻がや がてあらわれる.つまり別に最大を求めて,全ての和を取ると良い. 入力と整形: 1-24 時間周期の惑星毎にアクセス数をまとめる (t 時間のずれも吸収する) C++ Ruby 1 2 3 4 5 6 7 8 9 10 11 1 2 3 4 5 6 7 8 9 10 11 12 while (cin >> N && N) { int Q[25][25] = {{0}}; // Q[d][i] d 周期の i 時間目の量 (0<=i<d) for (int i=0; i<N; ++i) { cin >> d >> t; for (int j=0; j<d; ++j) { cin >> q; Q[d][(j+d-t)%d] += q; } } //この辺で答えを計算する } while true $N = gets.to_i break if $N == 0 $Q = {} (1..$N).each { d, t, *q = gets.split(" ").map{|s| s.to_i} q.rotate!(t) unless $Q[d] $Q[d] = q else (0..d-1).each {|i| $Q[d][i] += q[i] } end 128 13 14 15 } # この辺で答えを求める end ここまでの処理で,1 ≤ d ≤ 24 に対して Q[d][0] から Q[d][d-1] にその周期のアクセス数が格納されてい るので,適宜組み合わせる. C++ Ruby 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 10 11 12 const int L = 16*9*5*7*11; int sum = 0, T[L] = {0}; for (int d=1; d<=24; ++d) { if (d == 13 || d == 17 || d == 19 || d == 23 || d == 1) sum += /*(Q[d][0] か ら Q[d][d-1] の 最 大 値 ) */; else for (int i=0; i<L; ++i) T[i] += Q[d][i%d]; } cout << sum + /*(T[0] か ら T[L-1] の 最 大 値 ) */ << endl; L = 16*9*5*7*11 # 答えを計算するあたり sum = 0 $T = Array.new(L,0) $Q.each{|d,q| if d == 13 || d == 17 || d == 19 || d == 23 || d == 1 sum += q.max else (0..L-1).each { |i| $T[i] += q[i%d] } end } # sum と$T.max が答え この問題では 1 から 24 までという制約があったために最小公倍数をあらかじめ手で計算したが,計算機 を用いて計算する場合はユークリッドの互助法 (参考書 [3, pp. 107–]) を用いると良い. C++ 1 2 3 4 int gcd(int a, int b) { if (a < b) swap(a,b); return b == 0 ? a : gcd(b, a%b); } 問題 Extended Euclid Algorithm (AOJ) 与えられた2つの整数 a、b について ax + by = gcd(a, b) の整数解 (x, y) を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=NTL_1_E& lang=jp 拡張ユークリッド互除法で求められる.ユークリッド互除法が行っている計算で付随して求められ情報 を活用する.一歩が a メートルの人と b メートルの人がそれぞれ何歩づつ歩くと... などの問題をとくとき に有用である. 129 14.2 連立方程式を解く 14.2.1 言語機能: valarray 数値ベクトル (行列の行,あるいは列など) を扱う場合は valarray が便利である. C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <valarray> valarray<int> V(0, N); // 要素数 N の整数のベクトルを作成,各要素は 0 V = 3; // 全要素を 3 に V[0] = 1; V *= 10; // 全要素を 10 倍に V %= 2; // 全要素を mod2 // 最大値 最小値 合計 cout << V.max() << ’ ’ << V.min() << ’ ’ << V.sum() << endl; valarray<int> U(2, N); V += U; // 各要素の和 // slice(initial, length, step) V[slice(1,3,2)] = -3; // V[1] = V[3] = V[5] = -3 14.2.2 連立方程式 問題 (アジア地区予選 2010) Awkward Lights ライトがグリッド状に並んでいる.あるライトのスイッチを押すとそのライトに加えてマン ハッタン距離 d にあるライトの on/off の状態が変わる.全部のライトを消すことが出来るか? http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1308 考え方: 2 回以上スイッチを押しても意味がないので,各スイッチは押すか押さないかのどちらか.各ス イッチに 0 から M N − 1 の番号を振り,押したかどうかを xi ∈ {0, 1} であらわす.行列 A は M N 行/列 で,要素 aij は,スイッチ i を押すとライト j に影響があることを示すとする.行列 A とベクトル x の積 は,変化を受けたスイッチを表すベクトルとなる.各ランプが最初についていたかどうかを bi で表すとし て,Ax = b に解があるかどうかを求める (ただしすべての演算を mod 2 で行う). 掃き出していって行列左下の三角部分を 0 にした時に、どのスイッチでも消せないライトが残らなけれ ば成功 入力 C++ 1 2 3 4 5 6 7 8 9 10 typedef valarray<int> array_t; int M, N, D; int main() { while (cin >> M >> N >> D && M) { valarray<array_t> A(array_t(0, M*N+1), M*N); for (int i=0; i<M*N; ++i) { cin >> A[i][M*N]; for (int j=0; j<M*N; ++j) { int x0=i%M, y0=i/M, x1=j%M, y1=j/M; int d=abs(x0-x1)+abs(y0-y1); 130 11 12 13 14 15 16 if (d==0 || d==D) A[i][j] = 1; } } cout << solve(A) << endl; } } A は M N 行,M N +1 列の行列で,右端の列は最初についているかどうか,他の列の要素は i 個目のスイッ チを押したときに j 個目のライトが切り替わるかを表す.A[i] は i 行目を表し,各行は valarray<int>一 つに対応する.(慣れたら行列全体をひとつの valarray<int>で表したほうが効率が良い) 計算 C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int solve(valarray<array_t>& A) { int p = 0; // top → bottom for (int i=0; i<M*N; ++i) { // left → right int r = // 「p 以降で A[r][i]!=0 の行」 if (/*適切な r が 存 在 し な け れ ば */) continue; swap(A[p], A[r]); for (int j=p+1; j<M*N; ++j) if (A[j][i]) { // 行 A[j] に行 A[p] を足しこむ,A[j]%=2 を忘れずに } ++p; } for (int r=0; r<M*N; ++r) if (/* A[r][M*N] が 非 0 で , A[r] の 要 素 が そ れ 以 外 0 だ と */) return 0; return 1; } 連立方程式は「情報科学」の授業でも扱う. 問題 (夏合宿 2009) Strange Couple⋆ 道路と交差点の一覧が与えられる.標識がない交差点では U ターンも含めて等確率でランダ ムに道を選び,標識がある交差点ではそこから目的地までの最短で到達できるルートを (複数 あればランダムに選ぶ) 人がいる.出発地から目的地までに通る距離の合計の期待値を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2171 最短路と連立方程式: まず,ダイクストラ法などで目的地から各交差点までの距離を求める.続いて,各 交差点から出発した場合の期待値を変数として,隣合う交差点から出発した場合の期待値との関係から連 立方程式を立てる. 最短路の求め方は後日扱うので,まだ学んでいない場合は後で取り組むと良い. 14.3 その他の練習問題 問題 Afternoon Tea⋆ (Polish Olympiad in Informatics 2011) 紅茶とミルク、どちらをより多く飲んだか? http://main.edu.pl/en/archive/amppz/2011/her 131 問題 Making Change (codechef) ある国の硬貨の価値は D[1], ..., D[N] である.金額 C (とても大きい) をこの国の硬貨で支払 う方法は何通り?ただし D[1], ..., D[N] は相異なりどの 2 つも互いに素. http://www.codechef.com/problems/CHANGE 132 第 15 章 補間多項式と数値積分 15.1 Lagrange 補間多項式 3 点 (a, va ), (b, vb ), (c, vc ) を通る 2 次の多項式は次のように一意に定まる: L(x) = (x − b)(x − c) (x − a)(x − c) (x − a)(x − b) va + vb + vc . (a − b)(a − c) (b − a)(b − c) (c − a)(c − b) (15.1) 一般に,与えられた N 個の点を通る次数最低の多項式は一意に定まる: N −1 ∑ ∏ (x − xi ) · vx k . L(x) = (xk − xi ) k=0 i̸=k (この式でくらっとした場合は,以下を無視して 15.2 節へ進んで良い.) 問題 (アジア地区予選 2012) Find the Outlier 秘密の多項式 f (x) の次数 d と d + 3 個の点,(0, v0 ), (1, v1 ), · · · , (d + 2, vd+2 ) が与えられる. 点 (i, vi ) は f (i) = vi を満たすが,例外として一点 (i′ , vi′ ) のみ vi′ に 1.0 以上の誤りを含む.そ の点 i′ を求めよ.入力は 10−6 より大きな誤差を含まない. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1328 方針: • d + 1 個の点を選んで Lagrange 補間多項式を作る.その式の値と,選ばれなかった点が一致するかを 考える. • i′ を除いた d + 2 個の点についてだけ考えると,どの d + 1 個の点で作られた補間多項式も f と一致 し,残りの 1 点を通る. • 点 i′ を含む d + 2 個の点については,同様の補間多項式が残りの 1 つの点と一致しないような (d + 1 個と 1 個) の点の選び方がある. 133 20 10 all points 0 interpolation (x=0,1,4) -10 interpolation (x=1,2,3) 0 1 2 3 4 1 つ目のサンプル入力の例: outlier である (2,12) を外した 3 点で補間するとどの場合も同じ関数が得られ, outlier の 1 点は外れるが,補間に使用しなかった残りの一点を通る (緑破線).outlier である (2,12) を含む 3 点で補間すると,残りの 2 点を外れる (青点線). 点 f (n) を,点 (n, vn ) と (E, vE ) を除いた d + 1 個の点から求める関数 interpolate(n,E) は以下の ように作ることができる. C++ Ruby 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 1 2 3 4 5 6 7 8 int D; double V[16]; double interpolate(int n, int E) { // L(n) を補間多項式により計算する // ただし点 (n,V[n]) と (E,V[E]) は使わない double sum = 0.0; for (int k=0; k<D+3; ++k) { if (/* k が n か E な ら */) continue; double p = V[k]; for (int i=0; i<D+3; ++i) if (! (/* i が k で も n で も E で も な け れ ば */)) p *= (...-i)/(double)(k-i); sum += p; } return sum; } # 配列$V に値が入っているとする def interpolate(n, e) sum = 0.0 $V.each_index{|k| next if ... # k が n か E なら p = $V[k] $V.each_index{|i| p *= 1.0*(...-i)/(k-i) if ... # i が k でも n でも E でもなければ 134 9 10 11 12 13 } sum += p } sum end この関数 interpolate を用いると,点 (E, VE ) が異常だったと仮定して,残りの d + 2 個の点に異常 がないか調べる関数 outlier(E) を次のように簡単に作ることが出来る. C++ C++ Ruby 1 2 3 4 5 6 7 8 9 bool outlier(int E) { for (int i=0; i<D+3; ++i) { if (i==E) continue; double p = interpolate(i, E); if (/* p と V[i] の 絶 対 値 (abs) が 0.1 よ り 離 れ て い た ら */) return false; } return true; } 1 2 3 4 5 6 7 8 9 10 11 12 13 #include<iotream> #include<cmath> // この辺に上記の関数を定義する int main() { while (cin >> D && D) { for (int i=0; i<D+3; ++i) cin >> V[i]; for (int i=0; i<D+3; ++i) if (outlier(i)) { // i を無視すれば全て整合する cout << i << endl; break; } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 require ’scanf’ # 関数定義 while true d = (scanf ’\%d’)[0] break if d == 0 \$V = scanf(’\%f’*(d+3)) (0..d+2).each {|i| if outlier(i) # i を無視すれば全て整合する puts i break end } end 135 15.2 数値積分とシンプソン公式 例題 (PC 甲子園 2003) Integral 長方形の和を用いて面積の近似値を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0014& lang=jp ∫ 関数 f (x) の区間 [a, b] の定積分 b f (x)dx を求めたい.解析的に解く方法以外に,数値計算で近似値を a 求める種々の方法が存在する.もっとも簡便な方法は,区間を細かく分けて長方形の面積の和で近似する ものである.(例題の話題はここで区切り) 15.2.1 台形公式 小分けにした短冊部分について,長方形を台形に変更すると,分割数に対応する精度を高めることがで きる.分割幅を h とすると誤差は O(h) から O(h2 ) に改善する (計算誤差の影響を無視すれば,刻みを 10 倍細かく取ると,精度が 100 倍改善すると予想できる). y y = f (x) a = x0 xi xi+1 b = xn x 15.2.2 シンプソン公式 シンプソン公式 (Simpson’s rule) では,直線の代わりに二次式で近似し,精度は O(h4 ) に改善する.先ほ a+b どまでと記法が異なるが a = xi , b = xi+1 として,区間内の三点 (a, f (a)), ( a+b 2 , f ( 2 )), (b, f (b)) を使っ た補間多項式 (式 (15.1)) で表現して整理すると,その区間の面積は以下のように近似できる: ( ) ( ) ∫ b b−a a+b f (x)dx ≈ f (a) + 4f + f (b) . 6 2 a もし区間内で f (x) が二次以下の多項式であれば,計算誤差を無視すれば正確な値を得ることが出来る. 問題 (立命館合宿 2013) One 窓枠内に見えている山々と空との境界の長さを求めよ.入力の制約から山の頂点は窓枠内 (境 界含む) に存在する. http://judge.u-aizu.ac.jp/onlinejudge/cdescription.jsp?cid= RitsCamp13Day2&pid=I 136 注: 区間 x ∈ [a, b] の f (x) の周長は,f (x) の導関数を f ′ (x) として ∫b√ 1 + f ′ (x)2 dx で与えられる. 山と空の境界をどの放物線が与えるかは,場所によって異なる.そこで x 軸で区間に分けて,各区間事 の周長を合計すると良い.分割方法は,各放物線同士の交点 (あれば) や窓の下端との交点 (あれば) の x 座 a 標を列挙し,それらを用いて窓内のを分割すれば十分かつ,区間の数も問題ない範囲である.各区間では まったく山が見えないもしくは,一つの放物線が空との境界をなす.どの放物線が一番上にあるかは,区 間の中央の x での高さで比較すれば求まる.積分は,数値計算がお勧め. 問題 Intersection of Two Prisms⋆ (アジア地区予選 2010) z 軸方向に無限の高さを持つ多角柱 P1 と,y 軸方向に無限の高さを持つ多角柱 P2 の共通部 分の体積を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1313 方針 (参考書 [3, pp. 236–]): 求める立体を平面 x = a で切断した断面を考える.断面は面積を持たないか, または y 軸と z 軸に平行な長方形になるから,その面積を x 軸方向に積分して体積を求める.区間は立体 の頂点の存在する x 座標で区切る. ある x 座標での切断面での y 軸または x 軸の幅を求める関数 width(polygon, x) は,たとえば以下 のように与えられた多角形の辺を一巡して求めることができる. Ruby 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def width(polygon, x) # polygon に x-y または x-z 平面の座標が順に入っている w = [] polygon.each_index{|i| p, q = polygon[i], polygon[(i+1)%polygon.length] # 辺 pq を考える if p[0] == x w << p[1] elsif (p[0] < x && x < q[0]) || (p[0] > x && x > q[0]) x0, y0 = p[0], p[1] x1, y1 = q[0], q[1] x0, y0, x1, y1 = x1, y1, x0, y0 if p[0] > x w << y0 + 1.0*(y1-y0)*(x-x0)/(x1-x0) end } raise if w.length == 0 w.max - w.min end この関数を利用して,体積を求める関数 volume を,たとえば以下のように作成できる.なお$P1 は多 角形 P1 の (x,y) 座標を順に格納した配列,$P2 を多角形 P2 の (x,z) 座標を順に格納した配列, $X を P1 と P2 に登場する X 座標全てを格納した配列とする. Ruby 1 2 3 4 5 6 7 8 9 def volume $X.sort! $X.uniq! sum = 0.0 xmin = [($P1.min)[0], ($P2.min)[0]].max xmax = [($P1.max)[0], ($P2.max)[0]].min (0..$X.length-2).each{ |i| a, b = $X[i], $X[i+1] next unless xmin <= a && a <= xmax && xmin <= b && b <= xmax 137 10 11 12 13 14 15 16 17 18 m = (a+b)/2.0 va = width($P1, a)*width($P2, vb = width($P1, b)*width($P2, vm = width($P1, m)*width($P2, area = .. // (a,va), (m,vm), sum += area a) b) m) (b,vb) を 通 る 2 次 式 の 区 間 [a,b] の 定 積 分 } sum end 15.3 道具としての Fast Fourier Transform (FFT) (この節の説明はまだ書かれていない) 問題 Best Position⋆ (Kuala Lumpur 2014) 農耕と牧畜の生産が最大になる場所を探す.畑候補は長方形領域で,それ以内の畑パターン を重ねあわせた時に一致したマスで生産が生じる. https://icpcarchive.ecs.baylor.edu/index.php?option=com_ onlinejudge&Itemid=8&category=645&page=show_problem&problem=4820 畑候補と畑パターンをそれぞれ多項式として表現して,その積の多項式のある係数が,ある場所で重ね あわせた時の領域の和になるように工夫する. 問題 (春コンテスト 2013) Point Distance⋆ 各セル (x,y) に,分子がいくつ入っているか Cxy で表される.すべての分子のペアを考えた 時の平均距離と距離毎のヒストグラムを示せ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2560 二次元の FFT の練習.ヒストグラムは long long が必要. 問題 The Teacher’s Side of Math⋆ (アジア地区予選 2007) 解の一つが a1/m + b1/n であるような、整数係数多項式を求めるプログラムを作成せよ.(a, b は互いに異なる素数、m, n は 2 以上の整数) http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1284 ヒント: 一般に 1 の原始 m 乗根を ξ, 1 の原始 n 乗根を η として m−1 ∏ n−1 ∏ (x − a1/m ξ j − b1/n η k ) j=0 k=0 138 という多項式を作ると = m−1 ∏ n−1 ∏ j=0 k=0 {(x − a1/m ξ j )n − b} = {(x − b1/n η k )m − a} と変形できることを利用する. 数が小さいので,適当に展開しても良いし,FFT を用いても良い. 139 第 16 章 区間の和/最大値/最小値と更新 概要 ある程度大きなデータを与えられたうえで,質問ごとにデータの一部を調査するタイプの問題を考え る.事前に準備をしておく (≈ 適切なデータ構造を用いる) ことで質問に効率的に答えられるようにな る.ここでは一列あるいはグリッド上に並んだデータについて考える. 16.1 累積和 初めに,列 ai の先頭からの和 Si+1 = 例題 ∑i k=0 ak を活用する問題を考える.S0 = 0 とする. (PC 甲子園 2003) Maximum Sum Sequece 整数の並び a1 , a2 , a3 , . . . , an で、連続した 1 つ以上の項の和の最大値を求めよ. 注: もし ai が非負なら当然すべての和が最大なので,負の数を含む.負の数を含む区間が最 大になることもある. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0022& lang=jp (これは maximum subarray problem という問題で,列の長さを N として O(N ) の解き方がある.しかし, ストーリーの都合で全区間を調べて O(N 2 ) で解くことにする) ∑j−1 区間 [i,j) の和,すなわち k=i ak を定義通りに計算すると,j − i − 1 回の加算が必要である.これを ∑i j − i によらず 1 回の演算で済ませたい.それには,事前に S0 = 0, Si+1 = k=0 ak = Si + ai となるよう な補助変数 S を用意しておけば良い.これを用いると,区間 [i,j) の和は Sj − Si で求められる.たとえば, a8 + a9 + a10 + a11 = S12 − S8 である. S12 a0 a1 a2 ... a7 a8 a9 a10 a11 a12 a13 ... S8 これを用いて,すべての区間の和を調べると求める最大値を得られる. 問題 A Traveler (JOI 2009) 左右に行ったり来たりする旅程が与えられるので歩いた合計 (の剰余) を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0549 140 考え方: 旅程の要素に「左に 30 個目の宿場に移動する」という指示があったとして,30 回の加算をする ことは避けたい.そこで事前に各宿場について左端からの距離を事前に計算しておく.すると旅程の要素 数である M 回の加算で合計の距離を求められる. 二次元の累積和 同様の考え方は二次元にも応用できる: 補助変数を Si+1,j+1 = ∑i k=0 ∑j l=0 ak,l と定義する.縦 [i,i’), 横 [j,j’) の長方形領域の和は Si′ ,j ′ − Si,j ′ − Si′ ,j + Si,j となる.(大きな長方形から余分にを引いて,引きすぎ たものを調整する) つまり,長方形領域の面積によらない定数回の演算で求めることができる. a0,0 A ai,0 B ai,j C ai,j ′ A + C = Si′ ,j A + B + C + D = Si′ ,j ′ D ai′ ,j 例題 A = Si,j A + B = Si,j ′ ai′ ,j ′ Maximum Sum Sequence II (PC 甲子園 2003) 和が最大になる長方形領域を知りたい. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0098 (この問題も縦横どちらかの maximum subarray problem に変換することで,O(N 3 ) で解ける.しかし,ス トーリーの都合で全長方形区間を調べて O(N 4 ) で解く意図で掲載する) 問題 Planetary Exploration (JOI 2010) 指定の長方形区域の資源の数を知りたい. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0560 問題 Coffee Central⋆ (World Finals 2011) カフェを開くのに最適な場所を知りたい. https://icpcarchive.ecs.baylor.edu/index.php?option=com_ onlinejudge&Itemid=8&category=45&page=show_problem&problem=3133 菱形の領域は 45 度回転させると扱いやすくなる場合がある. 141 16.2 Binary Indexed Tree (Fenwick Tree) 今度は処理の途中でデータ ai の一部が変化する状況を考える.前節のように補助変数 S を使うと,質問 には O(1) で回答できて効率が良いが更新には O(N ) かかってしまう.質問回答に O(log N ) のコストを許 容することで,O(log N ) の更新を実現する手法を紹介する.(参考書 [3, pp. 160–]) 問題 Range Query - Range Sum Query (AOJ) 区間の合計値を管理せよ http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=DSL_2_B& lang=jp S1,8 S1,4 S1,2 2:0010 8:1000 4:0100 S5,6 6:0110 S1,1 1:0001 S3,3 3:0011 S5,5 5:0101 S7,7 7:0111 まず図のような,2 のべき乗の長さの区間に対する和を求めておく.ここで Si,j という表記は区間 [i,j] の和とする.前節までと表記が異なり,j を含む.また,管理対象の列は a1 と 1 から始まるとする.(0 か ら始めることもできるが,後に書くように微調整が必要である) このような準備を行うと x ≥ 1 にたいして S1,x を log x 個までの和として表現することができる.たと えば,S1,7 = S1,4 + S5,6 + S7,7 である.また,ある要素の値が変化した時には,最大 log N 個の部分和を 更新すれば良い.たとえば,a7 が更新された時は,それを含む区間である S7,7 と S1,7 を更新する. このようなデータを配列で管理するデータ構造が Fenwick tree または Binary indexed tree (BIT) である. 青字で記した要素番号 (コロンの右側は 2 進表記) を用いると次のように管理することができる.n 番目の 配列の要素 BIT[n] には,左端 l が n の最下位ビットを落とした数+1 でまた右端 r が n であるような区間 の和 Sl,r を格納する.したがって,1 から n までの和は,まず BIT[n] を加え,カバーする区間の左端 l が 1 に到達するまで,現区間の l − 1 を右端とするような新しい区間を探して足すことを繰り返せば良い. そのような区間は n の最下位ビットを落とすことで求められる.また an を更新する場合は BIT[n] の更 新後に,その区間を含むブロックを (左端を減らさないように) 右端を広げることで探すことを繰り返す. C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 int BIT[1000010], bit_size; void bit_init(int n) { fill(BIT, BIT+n, 0); bit_size = n; } int bit_sum(int n) { // [1,n] の和 int ans = 0; while (n > 0) { // 全体区間左端では 1 を一つだけ含むので最後は 0 になって終了 ans += BIT[n]; n &= n-1; // 最下位 (右端) の 1 を落とす: 左に移動 } return ans; } 142 14 15 16 17 18 19 void bit_add(int n, int v) { // n 番目の要素に v に加える while (n <= bit_size) { BIT[n] += v; n += n & (-n); // 最下位 (右端) の 1 を繰り上げる } } 参考: 0-index の場合 S0,7 S0,3 S0,1 S0,0 0:000 7:111 3:011 S4,5 1:001 S2,2 2:010 5:101 S4,4 4:100 S6,6 6:110 和: n = (n & (n + 1)) - 1 (n ≥ 0) 右に 0 のある 1 を繰り下げる 更新: n |= n + 1 (n < size) 一番右の 0 を 1 に 問題 引越し (夏合宿 2012) 小さい順に並べ替えるのに必要な体力の合計を求める. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2431 全部並べ替えると 1 から N までの総和なので,それから並べ替えなくて良い (=昇順の) 部分列の重さの 総和の最大値を引けば良い. i 番目の数字 xi で終わるような昇順の部分列の重さの和の最大値を Ai と表記すると, Ai = xi + max j<i, xj <xi Aj なので,数列を左から見てゆくと順に求められる.ここで i より小さい j について最大値を逐一調べると 時間がかかるので,xi をキーとした BIT を使って効率良く処理を行う. C++ 1 2 3 4 5 6 7 8 9 10 11 long long N, x; int main() { cin >> N; bit_init(N+1); for (int i=0; i<N; ++i) { cin >> x; long long cost = // x までの最大値 ... // x までの最大値を cost+x に更新 } cout << (1+N)*N/2 - bit_max(N) << endl; } Ruby 143 1 2 3 4 5 6 7 8 9 N = gets.to_i X = gets.split(’ ’).map{|s| s.to_i} tree = BIT_Max.new(N+1) X.each {|x| cost = ... // x ま で の 最 大 値 ... // x ま で の 最 大 値 を cost+ x に 更 新 } puts (1+N)*N/2 - tree.max(N) 区間の和を管理する BIT の代わりに,先頭からの区間 [1, n] の最大値を持つデータ構造を考える. C++ Ruby 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 long long BIT[100010], bit_size; // long long は 64bit 整数. void bit_init(int n) { fill(BIT, BIT+n, 0); bit_size = n; } long long bit_max(int n) { // [1,n] の最大値 long long ans = 0; while (n > 0) { ans = max(ans, BIT[n]); n &= n-1; } return ans; } void bit_setmax(int n, long long v) { // # [1,n] の最大値を v に更新する while (n < bit_size) { BIT[n] = max(BIT[n], v); n += n & (-n); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class BIT_Max # 1-index def initialize(n) @array = Array.new(n+1, 0) end def max(n) # [1,n] の最大値 ans = 0 while n > 0 ans = @array[n] if ans < @array[n] n &= n-1 end ans end def setmax(n, v) # [1,n] の最大値を v に更新する while n < @array.size @array[n] = v if @array[n] < v n += n & (-n) end end end 144 16.3 Segment Tree と Range Minimum Query 次に区間の最小値や最大値について考えてみる.区間 [i,j] の最小値は,残念ながら,区間 [0,i] の最小値 や区間 [0,j] の最小値からは求めることができない.そこでもう少しデータの豊富な Segment Tree を用い る.(参考書 [3, pp. 154–]). S0,7 S0,3 0 S4,7 1 S0,1 3 S2,3 4 2 S4,5 5 S6,7 6 S0,0 S1,1 S2,2 S3,3 S4,4 S5,5 S6,6 S7,7 7 8 9 10 11 12 13 14 図のような全区間を根として番号 0 とする完全二分木で,各区間のデータを保持する.葉以外の各区間 は 2 つの子を持ち,左の子が親の区間の左半分,右の子が残りを担当する.たとえば,区間 [2,6] の最小値 は図の灰色部分のように,min(S1,2 , S2,3 , S4,5 , S6,6 ) で求められる.どのような区間をとっても,調べる箇 所は各深さ毎に最大 2 までである. この実装では,ある id について, 左の子は id*2+1,右の子は id*2+2,親は (id-1)/2 である.元 の配列 ai の長さを N とすると,N が 2 のべき乗なら 2N の領域を,そうでなければ最大 4N の領域を使 用する. また,根付き木の最小共通祖先 LCA (least/lowest common ancestor) を,深さ優先探索の訪問時刻に対す る RMQ として,計算することもできる.参考書 [3, pp. 292–] 問題 Range Query - Range Minimum Query (AOJ) 区間の最小値を管理せよ http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=DSL_2_A& lang=jp 問題 Balanced Lineup (USACO 2007 January Silver) 牛が並んでいる. 区間 [a,b] で身長が最大の牛と最小の牛の差は? http://poj.org/problem?id=3264 scanf 必要. 問題 Drawing Lots⋆ (UTPC2009) あみだくじについて,選ばれた場所がアタリとなるように横線の数を追加するとして,その 最小本数を求めよ. http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2192 145 問題 Distance Queries⋆ (USACO 2004 February) 農場間 2 点の道のりを求める http://poj.org/problem?id=1986 問題 Apple Tree)⋆ (POJ Monthly–2007.08.05) りんごの数を数える http://poj.org/problem?id=3321 146 第 17 章 おわりに 資料内の誤りや改善点などに気づかれた際は,筆者 ([email protected]) まで連絡いただけると幸いであ る.現在までに多数のコメントと評価をいただいてきた.現時点で個別の謝意の表明は控えるが,ここに 感謝の意を記したい. 資料中のイメージイラストは,特に断りがなければ,https://openclipart.org/ にて public domain で提供されている画像を拝借したものである. - 筆者がコンテスト形式のプログラミングの存在を知ったのは,2004 年度の夏学期に,自主的な「勉強 会」(冬学期以降は現「実践的プログラミング」) が増原准教授 (当時) により始められた時であった. それ以降,ACM-ICPC OB/OG 会の活動,ブログなどでの技術の交換,日本語のオンラインジャッジシ ステム (Aizu Online Judge) の公開,各大学でのオリジナルコンテストの開催,プログラミングコンテ ストチャレンジブックの出版,atcoder 社の定期コンテストなど,日本の参加者のコミュニティの盛り 上がりは著しい.一定以上のレベルの学習環境は現在までに充実していると思われるので,未経験者 がより早く楽しめるレベルに達するような学習効率を高めることができれば,より発展すると期待し ている. 147 関連図書 [1] 渡部 有隆, 「オンラインジャッジではじめる C/C++ プログラミング入門」,マイナビ,2014 年, https: //book.mynavi.jp/ec/products/detail/id=25382 [2] 渡部 有隆, 「プログラミングコンテスト攻略のためのアルゴリズムとデータ構造」,マイナビ,2015 年,https://book.mynavi.jp/ec/products/detail/id=35408 [3] 秋葉 拓哉,岩田 陽一,北川 宜稔, 「プログラミングコンテストチャレンジブック」第二版,マイナビ, 2012 年, http://book.mycom.co.jp/book/978-4-8399-4106-2/978-4-8399-4106-2. shtml [4] Jon Kleinberg and Eva Tardos, 「アルゴリズムデザイン」, (浅野 孝夫,浅野 泰仁,小野 孝男,平田 富夫 訳),共立出版,2008 年, http://www.kyoritsu-pub.co.jp/bookdetail/9784320122178 [5] T. コルメン,C. ライザーソン,R. リベスト,C. シュタイン, 「アルゴリズムイントロダクション」第 3 版 総合版, (浅野 哲夫,岩野 和生,梅尾 博司,山下 雅史,和田 幸一訳),近代科学社,2013 年, http://www.kindaikagaku.co.jp/information/kd0408.htm 148 付 録A バグとデバッグ プログラムを書いて一発で思い通り動けば申し分ないが,そうでない場合も多いだろう.バグを埋める のは一瞬だが,取り除くには 2 時間以上かかることもしばしばある.さらに C や C++を用いる場合には, 動作が保証されないコードをうっかり書いてしまった場合のエラーメッセージが不親切のため,原因追求 に時間を要することもある.開発環境で利用可能な便利な道具に馴染んでおくと,原因追求の時間を減ら せるかもしれない.特にプログラミングコンテストでは時間も計算機の利用も限られているので,チーム で効率的な方法を見定めておくことが望ましい. A.1 そもそもバグを入れない 逆説的だが,デバッグの時間を減らすためには,バグを入れないために時間をかけることが有効である. その一つは,良いとされているプログラミングスタイルを取り入れることである.様々な書籍があるので, 自分にあったものを探すと良い.一部を紹介する. • 変数を節約しない 悪い例: (点 (x1,y1) と (x2,y2) が直径の両端であるような円の面積を求めている) C++ 1 2 double area = sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1))/2 * sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1))/2 * 3.1415; 1 2 double radius = sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1))/2; double area = radius*radius*3.1415; 改善の例: C++ なぜ良いか: – 人間に見やすい (x1 を x2 と間違えていないか確認する箇所が減る) – タイプ/コピーペーストのミスがない – 途中経過 (この場合は半径) を把握しやすい.(デバッガや printf で表示しやすい) • 関数を使う C++ 1 2 3 4 5 double square(double x) { return x*x; } double norm(double x1, double y1, double x2, double y2) { return square(x2-x1)+ square(y2-y1); } double circle_area(double r) { return r*r*3.1415; } • 定数に名前をつける 149 C++ 1 2 const double pi = 3.1415; const double pi = atan2(0.0,-1.0); • 変数のスコープはなるべく短くする: 変数のスコープは短ければ短いほど良い.関連して,変数の再利用は避け,一つの変数は一つの目的 のみに使うことが良い. C++ 1 2 3 4 5 6 7 8 int i, j, k; // この辺に j や k を使う関数があったとする ... int main() { for (i=0; j<5; ++i) // ああっ! cout << "Hello, world" << endl; ... } 関数毎に必要な変数を宣言すると状況が大分改善する. C++ 1 2 3 4 5 int main() { int i; for (i=0; j<5; ++i) // コンパイルエラー cout << "Hello, world" << endl; } しかし,C++ や Java など最近の言語では,for 文の中でループに用いる変数を宣言できるので,こち らを推奨する. C++ 1 2 3 4 int main() { for (int i=0; i<5; ++i) cout << "Hello, world" << endl; } • ありがちな落とし穴をあらかじめ学び避ける C 言語 FAQ http://www.kouno.jp/home/c_faq/ 特に 16 奇妙な問題 • コンパイラのメッセージを理解しておく: – “if-parenth.cc:8:14: warning: suggest parentheses around assignment used as truth value” C++ 1 2 if (a = 1) return 1; if (a =! 1) cout << "ok"; – “no return statement in function returning non-void [-Wreturn-type]” C++ 1 2 3 int add(int a, int b) { a+b; // 正しくは return a+b; } 150 A.2 それでも困ったことが起きたら A.2.1 道具: assert 実行時のテストのために C, C++ では標準で assert マクロを利用可能である.assert 文は引数で与えられ た条件式が,真であればなにもせず,偽の時にはエラーメッセージを表示して停止する機能を持つ. コード例 C++ 階乗の計算: 1 2 3 4 5 6 7 8 9 int factorial(int n) { if (n == 1) return 1; return n * factorial(n-1); } int main() { cout << factorial(3) << endl; // 3*2*1 = 6 を出力 cout << factorial(-3) << endl; // 手が滑ってマイナスをいれてしまったら,止 まらない } 上記の関数は,引数 n が正の時のみ正しく動く.実行時に,引数 n が正であることを保証したい.その ためには,cassert ヘッダを include した上で,assert 文を加える.見て分かるように assert の括弧内に,保 証したい内容を条件式で記述する. C++ 1 2 3 4 5 6 7 #include <cassert> // 追加 int factorial(int n) { assert(n > 0); // 追加 if (n == 1) return 1; return n * factorial(n-1); } このようにして factorial(-1) 等を呼び出すと,エラーを表示して止まる. Assertion failed: (n > 0), function factorial, file factorial.cc, line 3. 1 このように,何らかの「前提」にのっとってプログラムを書く場合は,そのことをソースコード中に「表 明」しておくと見通しが良い. assert は実行時のテストであるので,実行速度の低下を起こしうる.そのために,ソースコードを変 更することなく assert を全て無効にする手法が容易されている.たとえば以下のように,cassert ヘッダを include する*前*に NDEBUG マクロを定義する C++ 1 2 3 4 #ifndef NDEBUG # define NDEBUG #endif #include <cassert> 151 A.2.2 道具: GLIBCXX DEBUG (G++) G++の場合, GLIBCXX DEBUG を先頭で define しておくと,多少はミスを見つけてくれる.(http:// gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode_using.html#debug_mode.using. mode) C++ 1 2 3 4 5 6 7 #define _GLIBCXX_DEBUG #include <vector> using namespace std; int main() { vector<int> a; a[0] = 3; // 長さ 0 の vector に代入する違反 } 実行例: (単に segmentation fault するのではなく,out-of-bounds であることを教えてくれる) /usr/include/c++/4.x/debug/vector:xxx:error: attempt to subscript container 1 with out-of-bounds index 0, but container only holds 0 elements. 2 A.2.3 道具: gdb 以下のように手が滑って止まらない for 文を書いてしまったとする. C++ 1 2 3 4 5 6 7 int main() { // hello hello world と改行しながら繰り返すつもり for (int i=0; i<10; ++i) { for (int j=0; j<2; ++i) cout << "hello " << endl; cout << "world" << endl; } } gdb を用いる準備として,コンパイルオプションに-g を加える. 1 $ g++ -g -Wall filename.cc 実行する際には,gdb にデバッグ対象のプログラム名を与えて起動し,gdb 内部で run とタイプする $ gdb ./a.out (gdbが起動する) (gdb) run # (通常の実行) (gdb) run < sample-input.txt # (リダイレクションを使う場合) # ...(プログラムが実行する)... # ...(Ctrl-C をタイプするか,segmentation fault などで停止する) (gdb) bt (gdb) up // 何 回 か up し て main に 戻 る (gdb) up #12 0x080486ed in main () at for.cc:6 6 cout << "hello " << endl; (gdb) list 1 #include <iostream> 2 using namespace std; 152 1 2 3 4 5 6 7 8 9 10 11 12 13 14 3 int main() { 4 for (int i=0; i<10; ++i) { 5 for (int j=0; j<2; ++i) 6 cout << "hello " << endl; 7 cout << "world" << endl; 8 } 9 } (gdb) p i $1 = 18047 (gdb) p j $2 = 0 15 16 17 18 19 20 21 22 23 24 25 主なコマンド: • 関数の呼び出し関係の表示 bt • 変数の値を表示: p 変数名 • 一つ上 (呼び出し元) に移動: u • ソースコードの表示: list • ステップ実行: n, s • 再度実行: c • gdb の終了: q ソースコードの特定の場所に来た時に中断したり,変数の値が書き換わったら中断するようなこともで きる.詳しくはマニュアル参照. A.2.4 C++ 道具: valgrind 1 2 3 4 int main() { int p; // 初期化忘れ printf("%d\n", p); } gdb を用いる時と同様に-g オプションをつけてコンパイルする. 1 $ g++ -g -Wall filename.cc 実行時は,valgrind コマンドに実行プログラムを与える. $ valgrind ./a.out Conditional jump or move depends on uninitialised value(s) ... 153 1 2 3 A.3 標本採集: 不具合の原因を突き止めたら バグの原因を特定したら,標本化しておくと将来のデバッグ時間を減らすための資産として活用できる. 「動いたからラッキー」として先に進んでしまうと,何も残らない.本筋とは離れるが,問題の制約を見落 としたり,文章の意味を誤解したために詰まったなどの状況でも,誤読のパターンも採集しておくと役に 立つだろう. 配列の境界 C++ 1 2 int array[3]; printf("%d", array[3]); // array[2] まで 初期化していない変数 C++ 1 2 3 4 5 int array[3]; int main() { int a; printf("%d", array[a]); // a が [0,2] でなければ不可解な挙動に } return のない関数 C++ 1 2 3 4 5 6 7 int add(int a, int b) { a+b; // 正しくは return a+b; } int main() { int a=1,b=2; int c=add(a,b); // c の値は不定 } stack 溢れ C++ 1 2 3 int main() { int a[100000000]; // global 変数に移した方が良い } 不正なポインタ C++ 1 2 3 4 5 6 int *p; *p = 1; char a[100]; double *b = &a[1]; *b = 1.0; 文字列に必要な容量: 最後には終端記号’\0’ が必要 C++ 1 2 char a[3]="abc"; // 正しくは a[4] = "abc" もしくは a[] = "abc" printf("%s\n", a); // a[3] のままだと大変なことに // A[i] (i の範囲は [0, N − 1]) を逆順に表示しようとして C++ 1 2 for (unsigned int i=N-1; i>0; ++i) cout << A[i] << endl; // 整数を2つ読みたい C++ 1 2 int a, b; scanf("%d &d", &a, &b); 154
© Copyright 2024 ExpyDoc