LEDAで始めるプログラミング: 複雑なアルゴリズムも手軽に実装可能 浅野哲夫・小保方幸次 北陸先端科学技術大学院大学 平成 13 年 11 月 5 日 LEDA は Algorithmic Solutions GmbH の登録商標です. 日本での LEDA の販売代理店は住商エレクトロニクス(株)です. i LEDA とは 本書は, ドイツのザールブリュッケンにあるマックスプランク研究所で開発さ れた LEDA(正式の名称は, Library of EÆcient Data types and Algorithms) のための入門書です. LEDA は「アルゴリズム+ LEDA =プログラム」の実 現を目指したソフトウェアライブラリですが, 単なるライブラリではなく, 新 たな高級計算機言語とも見なすことができるもので, ますます高度化する計 算機プログラムのベース言語として世界中の関心を集めています. このライブラリは約 10 年前に, 当時ドイツザールランド大学に所属して いた Kurt Mehlhorn(クルト・メルホーン)教授と Stefan N aher(ステファ ン・ネヤー)博士によって開発が始まって以来, 改良が加えられてきました. Mehlhorn 教授がザールランド大学のキャンパス内に新設された情報科学に関 するマックスプランク研究所 (Max Planck Institute f ur Informatik) の所長 として移籍してからは, マックスプランク研究所内のプロジェクトの一環と して機能の拡張が引き続いて行われてきました. その間, ドイツ国内はもとよ り, 世界中の多数の企業からの引き合いがあり, 実際, 多数の企業で利用され るようになりましたが, 最大の問題は, LEDA を用いてソフトウェアの製品を 作った場合のバグ対策でした. LEDA にバグがあれば, ソースコードに戻って バグを発見しなければなりませんが, ユーザである企業の責任でそのような バグを発見することは殆ど不可能です. これが LEDA の普及を妨げる最大の 要因だということで, 会社組織にしてバグ対策をしようということになったわ けです. 幾つかの名前を経て, 現在は Algorithmic Solutions Software GmbH という名前になっています. 現在では開発者である Mehlhorn 教授(マックス aher 教授(トリア大学)は陰に隠れて, Cristian Uhrig プランク研究所)と N 博士と Michael Seel が実質的なリーダーとして運営されています. LEDA の最初の目標は, アルゴリズムとプログラムの差を縮めることでし た. LEDA は C++言語で書かれたライブラリですが, 教科書でアルゴリズム が記述されているのとほぼ同じ感じでプログラムが書けるように工夫されて います. 詳細は本文で説明しますが, 様々な繰り返し構造やクラスが用意され ていて, 複雑なアルゴリズムでも実に分かりやすくプログラムを書くことが できます. 次の目標は, 豊富なデータ構造を実現して置いておくことでした. ii これによって, プログラマーは努力をせずに既存のデータ構造を使うことが できます. これ以外にも, 誤差なし計算をサポートする仕掛けなどを用意する ことでプログラマーの様々な意味での負担を著しく軽減することに成功して います. その他に LEDA の特徴を列挙すると以下のようになります. LEDA はオブジェクト指向の C++クラスライブラリで, 殆どのコンパ イラでコンパイルすることができます. ソースコードは非常に洗練されていますので, LEDA が提供している機 能を使うだけでプログラムの実行時間が短縮されます. 非常に直観的にプログラムが組めるように設計されています. プログラ ミングの初心者にとってもC言語でプログラムを書くよりもずっと簡単 であることが分かるでしょう. 信頼性の面でも LEDA はすぐれています. 幾何データを扱うプログラ ムでは予期しない縮退(たとえば, 多数の線分がちょうど 1 点で交わっ てしまうような場合)のためにプログラムが暴走してしまうことがあり ますが, LEDA で提供されている関数では縮退も十分に考慮されていま すので, 暴走の心配はありません. 頑健性も LEDA が誇るところです. 本文で詳しく説明しますが, 計算 誤差によってプログラムが誤動作をすることを避けなければなりませ んが, LEDA では桁数に制限のない整数表現や有理数表現などを用いて 計算誤差による影響を最小限に留めるための手段を豊富に提供してい ます. プログラムにはエラーがつきものですが, LEDA ではできる限りプログ ラムの出力の正しさを証明できるように工夫されています. たとえば, グラフの平面性を判定するプログラムを考えたとき, yes/no だけの答え だと信用するしかありませんが, 平面的である場合には平面へのグラフ の埋め込みを同時に出力し, 平面的でない場合には平面グラフにあって iii はならない部分グラフの存在を同時に示すようにプログラムの出力を 変更すると, 出力を見ただけで出力の正しさが検証できます. LEDA に関してはユーザマニュアルの他に, Mehlhorn と Naher による下記 の解説書が既に刊行されています. 1000 ページにもおよぶ大作で, 多数の例 題とともに LEDA のことが詳しく説明されています. 本文では LEDA Book と略記しています. "LEDA: A Platform for Combinatorial and Geometric Computing" by Kurt Mehlhorn and Stefan Naher Cambridge University Press, 1999. 最後に, LEDA は人工的な名前ですが, 小学館のランダムハウス英語辞典 を引くと, 「LEDA: ギリシア神話. レダ:スパルタの王 Tyndareus との間に Castor と Clytemnestra の 2 子を生み, また白鳥の姿で言い寄った Zeus との 間に Pollux と Helen の 2 子を生んだ女性.」とあります. 上記の本の表紙に は白鳥とその女性の絵が描かれています. 翻訳では「レダ」になっています が, 英語では「リーダ」, または「レイダ」と発音するようです. iv はじめに 情報科学関係の学科に限らず, 今では広範囲の学科で計算機の導入教育が 行われるようになってきました. 私も大学の新入生を対象にC言語を教えた 経験がありますが, どうすればプログラムを作る楽しみを伝えられるだろう かが, 最大の難関でした. C言語教育における最初の難関はポインタの概念で す. 入力のないプログラムなんて面白いはずがないのですが, C言語では入力 のための scanf 命令(関数)を書こうとするとポインタを使わなければなり ません. 教師としては, scanf 関数の中では変数に&の印をつけるものですよ と, 理由を教えずに単に記憶させるか, あるいは入力のないプログラムを延々 と教え続けるかを選択しなければなりません. C言語では最後にも難関が待ち構えています. 構造体です. 昔の良き時代 の言語である FORTRAN にはなかった概念です. もちろん, ポインタと構造 体のお陰で飛躍的に記述能力が高くなりましたが, 逆にポインタと構造体が 理解できなくて挫折した人も増加しているのではないかと思います. 問題は, 単に理解が難しいというだけではなく, 初心者に教えるためのC言語のプロ グラムには初心者の興味を引くようなものが少ないということです. それは, C言語で扱えるデータタイプが少ないことにも原因があります. とにかく, C言語を教えていても面白くないのです. 自動車の走行距離とガ ソリンの量を入力して燃費を計算するプログラムを作ってみても, そんなこ とは電卓で十分だし, 電卓の方が結局は早く計算できます. 文字列の処理にし ても, 日本語の処理が難しいということもあって, なかなか魅力のあるプログ ラムを示すのは至難の技です. 教える側が面白くないのですから, 教わる側が 面白いはずがありません. 情報科学の基礎中の基礎だから学生はやむなく理 解しようと努めるのです. 本書で紹介する LEDA は, ライブラリではありますが, 高級言語と言って も言い過ぎでない側面をもっています. まず, C言語の貧弱なデータタイプに 比べて実に多様なデータタイプが用意されています. 連結リストやスタック などのデータ構造がらみのものから, グラフの節点や辺, 幾何オブジェクトな どの多様なデータタイプが使えます. たとえば, キーボードから数値データを v 入力して, その2倍の値を画面上に出力するためのプログラムとほぼ同じ構 造をもったプログラムとして, 計算機の画面上にウィンドウを開いて, その上 でマウスを使って円を描き(円を入力として与えることに対応), 中心は同 じで半径だけが2倍の円を再びウィンドウ上に表示する(出力として与える ことに対応)というプログラムを作ることができます. C言語では数値や文 字以外に人間の目で理解できる出力を出すのはかなり面倒ですが, LEDA な ら, ウィンドウ上に様々な図形を描くことも簡単です. LEDA を使って教育した経験から言いますと, 最初から図形を扱うことが できるので, プログラムの動作を学生に理解させるのも非常に簡単ですし, 何 しろ教えていて楽しくなってきます. これが従来の教育との最大の違いでは ないでしょうか?最初にC言語におけるポインタと構造体の話をしましたが, LEDA ではポインタと構造体を使わなくても高度なプログラムを作ることが 可能です. もちろん, そのためには LEDA のプログラミングスタイルに慣れ ることが必要ですが, それは本書を読めば大丈夫です. 正直に言いますと, 実は上に述べたような教育のスタイルに私は反対でし た. 物事の原理を教えずに, いきなり利用の仕方から教えるという姿勢は間 違っているのでないかと信じていたのですが, LEDA に会って考え方が変り ました. いつでも物事の原理を理解するのは難しく, 時間がかかるものです が, 現代のように時間の流れが速い時代には, まず楽しさを伝えてプログラミ ングの世界に誘い込んだ後でゆっくりと原理を教えるのが学生にとってもい いのではないかと思うようになったのです. 本書は次のような読者に勇気を与えることを究極の目標として書いたつも りです. (1) C言語を勉強しようとしたが, プログラムを作る楽しさが分からなくて挫 折した人. (2) C言語のポインタと構造体の概念に馴染めず, 本格的なプログラムを作っ たことがない人. (3) クイックソートの動作が完全には理解できず, 何度クイックソートのプロ グラムを書いてもバグが残ってしまう人. (4) アルゴリズムを教えるのにアルゴリズム記述言語と実際のプログラムと のギャップに悩んでいる教員. vi (5) 計算誤差による誤動作をどのように修復すべきか苦しんでいるプログラ マ. (6) プログラムの高速化に苦しんでいるプログラマ. つまり, 言いたいのは, ポインタや構造体が分からなくても, クイックソー トの原理など分からなくても, 複雑なデータ構造のプログラムが書けなくて も, 本書を読んで LEDA のプログラミングスタイルを身につければ, 実用に 供しうる立派なプログラムが書けるようになるということです. これが誇張 かどうか, とにかく読んでみてください. w 最後に, 本書の刊行にあたり, 近代科学社様には社をあげて多大な励ましを 頂きました. 特に編集部の福澤富仁様には感謝します. また, LEDA の日本代 理店として住商エレクトロニクス(株)の皆様にも大変お世話になりました. 特に森下様, 福地様, 小澤様には長時間にわたるディスカッションを始め, 多面 的なサポートをして頂き, 感謝しています. 東京大学大学院生の清見様にはプ ログラミングスタイルに関して多数の詳細なコメントを頂きました. 最後にな りましたが, 終始一貫してマックスプランク研究所と Algorithmic Solutions の LEDA プロジェクトのメンバーにはサポートして頂き, 心より感謝します. Kurt Mehlhorn 教授, Stefan Naher 教授, Christian Uhrig 博士にはプログラ ムレベルでの助言をもらっただけではなく, 様々な例題のプログラムを本書 に転載することを許可してもらったり, 様々な便宜を図ってもらいました. こ こに記して感謝します. 著者を代表して 浅野哲夫 平成 13 年 8 月 8 日 vii 本書の使い方 本書の構成 本書に記載されているプログラムは LEDA-4.2 をベースに記述されています. それ以前のバージョンでは正常に動作しないことがあります.本書は本章以 外に次の 6 つの章で構成されています. 第 1 章 LEDA のインストール LEDA のインストールおよび実行環境の設 定方法について書かれています. 第 2 章 C言語と LEDA LEDA はC++言語で書かれたライブラリですから 先にC言語を勉強しておかなければなりません. ここでは, C言語の知 識がない読者を対象に基礎的なことを説明していますが, LEDA の基本 的な機能を使って説明していますので, C言語のテキストよりも楽しく 学べるはずです. 本書では学問的に正確な記述よりも初心者の直感に訴 える書き方をしていますので, 言語の専門家は眉をひそめてしまう表現 も多々ありますが, その点は予めご了承ください. C言語の知識がある 読者も LEDA によるウィンドウの取り扱いについては目を通しておい てください. 第 3 章 LEDA によるプログラミング この章では LEDA の機能をフルに活 かしたプログラミングについて説明しています. LEDA ではC言語や C++におけるデータタイプがどのように一般化されているか, どんな データ構造が提供されているかを説明します. より LEDA の雰囲気に慣 れてもらうために, この章からはプログラムもC++風に書いています. viii 第 4 章 グラフアルゴリズム グラフとは簡単に言えば接続関係を示したもの ですから, 計算誤差などに煩わされることもなく, 計算機にとっては扱い 易い対象であるはずですが, アルゴリズムのテキストに書かれたグラフ アルゴリズムの記述と実際のプログラムの間には大きなギャップがあり ます. LEDA は, グラフを扱うための様々な関数を提供するだけでなく, グラフを表現するためのデータ構造, グラフを処理するのに便利で, か つテキストのアルゴリズム記述に近い制御構造などを提供しています. また, グラフをウィンドウ上に表示するための機能も非常に強力です. 第 5 章 幾何データの取り扱い 目で見れば簡単なことも計算機にとっては非 常に難しいことがたくさんあります. この章では LEDA を使うと幾何 データの処理がどれほど簡単になるかを説明しています. たとえば, 点, 線分, 円, 多角形などのデータタイプが定義されていますから, 普通の 数値データを扱うようにこれらの幾何対象物を簡単に扱うことができ ます. また, ボロノイ図の計算を始め, 計算幾何学の分野で開発された 複雑な幾何計算のための関数も多数用意されています. 第 6 章 デモとアニメーション LEDA で用意されている各種のデモおよびア ニメーションのプログラムについて説明します. LEDA でのプログラミ ングの参考になるものから, アルゴリズムの理解に不可欠なアルゴリズ ムアニメーションに至るまで様々なレベルのものが用意されています. プログラミング言語の入門書として LEDA の豊富な機能を考えますと, LEDA を使ってプログラムを書くのは さぞ難しいだろうと間違った印象を抱きがちですが, 実際には初歩のレベル から使いやすさについて良く配慮されています. C言語を教えていて難しい のはポインタの概念ですが, LEDA ではC言語のレベルでもうまくポインタ の概念を隠しています. 細かいことですが, 変数の値を交換するのに LEDA の関数 leda swap() が用意されていて, どんなデータタイプの変数でも交換す ることが可能です. これに代表されるように, LEDA はC言語でのプログラ ムが楽になる仕掛けが非常にうまく設計されていますので, C言語を学んで ix から LEDA を学ぶのではなく, いきなり LEDA を使いながらC言語を学ぶこ とができます. C言語はよくできた言語ですが, 細かい処理ができる反面, プログラミング の初心者にとっては理解し難い概念も多いのが難点です. たとえば, ウィンド ウ上に線を引いたりすると視覚に訴えて興味を引くのですが, C言語でウィン ドウを開いて線を引こうとすると面倒な処理が多く, かなりの学習を積んだ 後でないと, そのようなプログラムを書くことができません. その点, LEDA では標準的なウィンドウなら簡単に扱えます. データの入力と出力が理解で きれば, それとまったく同じ感覚でウィンドウの取り扱いができるのです. 1 回目か 2 回目の講義でウィンドウの取り扱いを教えることもできます. 本書はC言語の知識がない読者を対象にして書かれています. もちろん基 礎はC言語ですが, LEDA を使えばC言語をすべて理解していなくても大丈 夫なのです. この章だけでC言語をすべて理解することは無理ですが, LEDA を使ってプログラムを書くには十分な知識が得られるようになっています. データ構造を駆使したプログラム開発の道具として 効率の良いプログラムを書く上で大切なのは「データ構造」です. データ 構造が重要だということはプログラマなら誰しも認識しているのですが, 実際 には余り複雑なデータ構造は使われていません. なぜでしょう. 一つは, デー タ構造は「難しい」からです. アルゴリズム関連のテキストには実に様々な データ構造の説明がありますが, 理論的な計算効率に関する解析が説明されて いるだけで, 一体どのデータ構造が実際に速いのか分かりません. また, デー タ構造自身が複雑なので, あらゆる場合を想定して誤り無く実現するのも大 変です. このような理由から効率の良いデータが余り使われてこなかったの ではないでしょうか. このように, 重要だと分かっているのにうまく使えないのがデータ構造な のです. LEDA では, どのデータ構造が実際的に効率が良いかを実験的に調 べ, 効率が良いものをライブラリの形で提供してくれていますので, プログラ マはどのデータ構造にするかを選択するだけで十分です. そもそもプログラ x ミング言語を学んですぐの読者を対象にしてスタックやキューを用いた例題 が説明できる環境を LEDA が提供してくれていることに驚くはずです. 本書でも LEDA で提供されているデータ構造について説明をしていますが, 実は本書で紹介しているのは全体のごく一部でしかありません. たとえば, 幾 何データを扱うためのデータ構造については本書ではまったく触れることが できていません. また, 平衡 2 分探索木についても, 教科書レベルで代表的な ものはほとんど実装されていますが, これについてもごく一部しか紹介でき ていません. アルゴリズム教育の参考書として 最近ではアルゴリズムに関するテキストもたくさん出回っていますが, ど のように教えるべきかについては著者にとって最大の悩みのようです. アル ゴリズムの解析に重点を置くなら, プログラム上の詳細は無視して簡潔な形 式で記述するのがいいのですが, アルゴリズムをプログラムとして実現する 場合のことも考慮するなら, プログラムに近い形で記述することになります. 両者の表現が近ければいいのですが, 現実にはかなりのギャップがあると言っ ていいでしょう. しかし, LEDA をプログラミング言語として考えるなら両 者の表現をかなり近づけることができます. LEDA はライブラリなのですが, 単なるライブラリではなく, アルゴリズムを記述する上で便利な制御構造も たくさん提供しています. つまり, LEDA はアルゴリズム記述言語と見なすこ ともできるのです. では, どれだけ近い表現が可能かが知りたくなりますが, 本書では辺に距離が定義されたグラフにおいて 1 点から他のすべての点まで の最短経路を求めるダイクストラのアルゴリズムを例として説明しています. アルゴリズムを文章だけで説明するのは至難の技です. アルゴリズム教育 で必ず出てくるクイックソートにしても, アニメーションを使って動作を教え ることができれば, 学生にも興味を持たせることができるし, 理解もずっと早 いはずです. このような観点から LEDA では多数のアニメーションプログラ ムを提供しています. これらのアニメーションを見せるだけでも LEDA を使 う価値があるのではないでしょうか. xi 計算誤差対策として C言語では数値データを固定長さの 2 進数で表現します. 長さが固定され ていますから, 当然有効桁数も固定されています. このため, 計算誤差の問題 を避けることは不可能です. 計算問題で計算結果に誤差が含まれるのも困り ますが, もっと困るのは計算誤差のせいでプログラムが異常終了してしまうこ とです. これに対処するには, まったく誤差が生じないような仕掛けを講じる か, 計算結果そのものよりもデータ間の関係を重視して, 小さな計算誤差は無 視できるような誤差に頑健なプログラムにするかどちらかです. 後者の道を 選ぶときには問題ごとに対処方法を考えないといけないことになります. そ の点, 前者の誤差なし計算に頼る方法は, 計算時間を若干犠牲にすることで容 易に実現できることが多いのです. どうするかと言いますと, LEDA で提供 されている整数と有理数を扱うデータタイプを使うのです. データが浮動小 数点数で与えられる場合は少し難しいのですが, 有理数で近似できる場合に は, 有理数どうしの四則演算の結果はまた有理数ですから, 有理数さえ誤差な しに扱うことが出来れば誤差なし計算が可能です. ただ, 有理数どうしの足し 算でも分母を共通にして計算しないといけないので, 分母と分子を表す整数 の桁数はいくらでも大きくなることがあります. そのために普通なら多倍長 の計算を実現するためのプログラムを書く必要があるのですが, LEDA で提 供されている有理数のデータタイプを使えば何も面倒はありません. 単に浮 動小数点数のデータタイプとして宣言されていた変数や定数を有理数のデー タタイプで宣言しなおすだけです. 四則演算や比較なども全く同じ形式なの で, プログラムの本体はほとんど変更する必要がないのです. 有理数のデータ タイプを使うとずいぶん遅くなるのではないかと心配されますが, LEDA で は特に注意してプログラムを書いていますから, 実際の実行時間はびっくり するほど速いのです. プログラムの高速化の道具として プログラムが一応完成して, 最後の仕事はプログラムの高速化です. この 目標を達成するために多くのプログラマが苦しんでいますが, これに対する xii 明快な解があります. そうです, 最初から LEDA でプログラムを書くのです. LEDA のプロジェクトでは様々な実験を行っていますが, 同じプログラムで も LEDA の機能を用いて書いたのと, C言語の機能だけを用いて書いたので は殆ど例外なく前者の方が実行時間の面で優れています. ときには一桁も実 行時間が変わることもあります. xiii 目次 LEDA とは i iv はじめに 本書の使い方 本書の構成 : : : : : : : : : : : : : : : : : : : : : : プログラミング言語の入門書として : : : : : : : : : データ構造を駆使したプログラム開発の道具として アルゴリズム教育の参考書として : : : : : : : : : : 計算誤差対策として : : : : : : : : : : : : : : : : : プログラムの高速化の道具として : : : : : : : : : : 第1章 : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : LEDA のインストール 1.1 インストールとコンパイル : : : : : : : : : : : : : : : : : : : : 1.1.1 インストール : : : : : : : : : : : : : : : : : : : : : : : 1.1.2 コンパイル : : : : : : : : : : : : : : : : : : : : : : : : 第 2 章 C言語と LEDA 2.1 最初のプログラム : : : : 2.2 簡単な数値計算 : : : : : 2.3 ウィンドウの基礎 : : : : 2.3.1 ウィンドウの宣言 2.3.2 ウィンドウ座標系 2.4 制御構造 : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : vii vii viii ix x xi xi 1 1 1 5 9 9 12 18 18 21 24 xiv 2.5 2.6 2.7 2.8 2.9 第3章 条件分岐 : : : : : : : : : : : : : : : : 繰り返し構造 : : : : : : : : : : : : : ループからの脱出 : : : : : : : : : : : 場合分け : : : : : : : : : : : : : : : : C言語特有の記法 : : : : : : : : : : : : : : : 2.5.1 代入文の値 : : : : : : : : : : : : : : 2.5.2 インクリメント, デクリメント演算子 2.5.3 複合代入文 : : : : : : : : : : : : : : 2.5.4 キャスト : : : : : : : : : : : : : : : : 2.5.5 条件演算子 : : : : : : : : : : : : : : 2.5.6 定数の定義 : : : : : : : : : : : : : : 2.5.7 マクロ定義 : : : : : : : : : : : : : : 関数の定義と利用 : : : : : : : : : : : : : : : 2.6.1 算術関数 : : : : : : : : : : : : : : : : 2.6.2 ユーザ定義関数 : : : : : : : : : : : : 2.6.3 乱数の生成 : : : : : : : : : : : : : : 配列の利用 : : : : : : : : : : : : : : : : : : 2.7.1 C言語の配列 : : : : : : : : : : : : : 2.7.2 配列の初期化 : : : : : : : : : : : : : 2.7.3 多次元配列 : : : : : : : : : : : : : : 文字列 : : : : : : : : : : : : : : : : : : : : : ファイル入出力 : : : : : : : : : : : : : : : : 2.4.1 2.4.2 2.4.3 2.4.4 LEDA によるプログラミング 3.1 LEDA のデータタイプ : : : : 3.1.1 integer タイプ : : : : : 3.1.2 rational タイプ : : : : 3.1.3 bigoat タイプ : : : : 3.1.4 real タイプ : : : : : : 3.2 LEDA の配列 : : : : : : : : : 3.2.1 array タイプ : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : 24 30 38 40 44 44 44 44 45 46 46 47 47 47 50 56 59 59 68 69 69 74 77 77 77 78 81 82 85 85 xv 3.3 疎配列 : : : : : : : : : : : : : : : : array 型の威力 : : : : : : : : : : : 基本データタイプ : : : : : : : : : : : : : : 3.3.1 スタック : : : : : : : : : : : : : : : 3.3.2 キュー : : : : : : : : : : : : : : : : 3.3.3 優先順位つきキュー : : : : : : : : 3.3.4 スタック, キューの応用例 : : : : : 3.3.5 優先順位つきキューによる経路探索 3.3.6 連結リスト構造 : : : : : : : : : : : 3.3.7 一方向連結リスト : : : : : : : : : : 3.2.2 3.2.3 : : : : : : : : : : : : : : : : : : : : 第 4 章 グラフとデータ構造 4.1 グラフとは : : : : : : : : : : : : : : : : : : : 4.2 グラフウィンドウ : : : : : : : : : : : : : : : : 4.2.1 グラフを作成 : : : : : : : : : : : : : : 4.2.2 グラフウィンドウのメニュー : : : : : 4.3 基礎知識 : : : : : : : : : : : : : : : : : : : : : 4.3.1 グラフの定義 : : : : : : : : : : : : : : 4.3.2 便利な繰り返し構造 : : : : : : : : : : 4.3.3 節点配列と辺配列 : : : : : : : : : : : : 4.3.4 パラメータつきグラフ : : : : : : : : : 4.3.5 ファイル入力 : : : : : : : : : : : : : : 4.4 トポロジカルソートの例題 : : : : : : : : : : : 4.5 最短経路問題 : : : : : : : : : : : : : : : : : : 4.6 グラフに関する基本アルゴリズム : : : : : : : 4.7 グラフに関する高度なアルゴリズム : : : : : : 4.7.1 最短経路に関するアルゴリズム : : : : 4.7.2 フローに関するアルゴリズム : : : : : 4.7.3 最小カットに関するアルゴリズム : : : 4.7.4 最大マッチングに関するアルゴリズム 4.7.5 最小木に関するアルゴリズム : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : 88 : 98 : 105 : 105 : 108 : 109 : 113 : 123 : 125 : 133 137 : 137 : 142 : 142 : 145 : 147 : 147 : 152 : 153 : 154 : 156 : 159 : 163 : 177 : 179 : 179 : 180 : 180 : 180 : 181 xvi 4.7.6 4.7.7 グラフの平面性に関するアルゴリズム グラフ描画に関するアルゴリズム : : : 第 5 章 幾何データの取り扱い 5.1 基本的な例題 : : : : : : : : : : : : : : : 5.1.1 point データタイプ : : : : : : : : 5.1.2 segment データタイプ : : : : : : 5.1.3 ray データタイプ : : : : : : : : : 5.1.4 line データタイプ : : : : : : : : : 5.1.5 circle データタイプ : : : : : : : : 5.1.6 polygon データタイプ : : : : : : 5.2 凸多角形に関する計算 : : : : : : : : : : 5.2.1 多角形の入力 : : : : : : : : : : : 5.2.2 凸多角形かどうかの判定 : : : : : 5.2.3 効率の良い方法 : : : : : : : : : : 5.3 凸包の計算 : : : : : : : : : : : : : : : : 5.3.1 基本的な考え方 : : : : : : : : : : 5.3.2 点のリストを用いたプログラム : 5.3.3 LEDA ライブラリの利用 : : : : : 5.4 計算誤差対策 : : : : : : : : : : : : : : : 5.4.1 凸包の計算における注意点 : : : : 5.4.2 交差判定 : : : : : : : : : : : : : : 5.4.3 有理数を用いた誤差なし計算 : : 5.4.4 有理数に基づく幾何データタイプ 5.5 ボロノイ図とその利用 : : : : : : : : : : 5.5.1 ボロノイ図を描こう : : : : : : : 5.5.2 ボロノイ図に基づいた曲線復元法 5.6 幾何アルゴリズム : : : : : : : : : : : : : 5.6.1 凸包の計算 : : : : : : : : : : : : 5.6.2 三角形分割 : : : : : : : : : : : : 5.6.3 制約つき三角形分割 : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : 182 : : : : : : : : : 182 : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : 183 : 183 : 187 : 190 : 192 : 194 : 195 : 196 : 198 : 198 : 199 : 203 : 209 : 209 : 213 : 218 : 219 : 219 : 223 : 227 : 233 : 235 : 235 : 245 : 251 : 251 : 252 : 252 xvii 5.6.4 5.6.5 5.6.6 5.6.7 5.6.8 5.6.9 5.6.10 ミンコフスキー和 : : ユークリッド最小木 ボロノイ図 : : : : : 環形: : : : : : : : : 線分交差 : : : : : : : 最近点対 : : : : : : : その他 : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : 第 6 章 デモとアニメーション 6.1 アルゴリズムの基礎に関するデモプログラム 6.2 グラフに関するデモプログラム : : : : : : : 6.3 幾何問題に関するデモプログラム : : : : : : 6.4 3 次元幾何に関するデモプログラム : : : : : 6.5 アルゴリズムの効率に関するデモプログラム プログラム一覧 : : : : : : : : : : : : : : : : : : : 索引 : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : 253 : 253 : 253 : 254 : 254 : 255 : 255 257 : 257 : 258 : 259 : 260 : 261 : 267 : 271 1 第 1 章 LEDA のインストール この章では, LEDA のインストール, LEDA プログラムのコンパイルについ て説明します. 従来はマックスプランク研究所の LEDA プロジェクトのホー ムページが窓口となっていましたが, 平成 12 年からはマックスプランク研究 所内に設立された Algorithmic Solutions Software GMBH 社に販売の権限が 移譲され, 同時に LEDA の配布に関する形態が変更されました. 最大の変更 点は, 従来教育機関に対しては無償で配布されていましたが, 平成 12 年 2 月 1 日をもってすべて有償に切り替わったことです. 教育機関に対しても, 個人 ライセンスから研究室, 学科, 大学単位のライセンスなどがあります. さらに, 基本部分だけのライセンスからソースコードを含んだ包括的なライセンスま で様々です. 日本では住商エレクトロニクス(株)が代理店として販売することになり ました. 詳細についてはホームページ http://www4.sse.co.jp/comid/engin/leda/index.html を参照して下さい. Algorithmic Solutions Software GMBH 社については下 記のホームページを参照して下さい. http://www.algorithmic-solutions.com 1.1 1.1.1 インストールとコンパイル インストール この本が出版された時点で LEDA のバージョンは 4.2 です. LEDA-4.2 は 表 1.1 の環境で動作する事が確認されています. それ以外のバージョン, 環境 で使用される場合は住商エレクトロニクスにお問い合わせください. 2 第1章 Machine Sun Sparc OS SunOS, Solaris Silicon Graphics Helett Packard DEV IBM Pentium 80486, Pentium 80486, Pentium IRIX HP-UX Digital Unix AIX Solaris Linux Windows Me/98/95/ 2000/NT LEDA のインストール コンパイラ SunPRO CC 4, 5, 6, g++ 2.95.x, CSet++ CC, g++ 2.8, 2.95.x HP C++, g++ cxx, g++ CSet++, g++ CC, g++ 2.95.x g++ 2.95.x Borland C++ 5.x Microsoft Visual C++ 5.0, 6.0 表 1.1: 動作環境 LEDA には, ソースコードパッケージとコンパイル済みのオブジェクトパッ ケージがあります. ソースコードパッケージのインストールは動作環境によっ てたいへん異なります. パッケージを解凍後, 付属の INSTALL ファイルに従っ てインストールしてください. オブジェクトパッケージは動作環境ごとに用 意されています. パッケージのファイル名は, LEDA のバージョン, Machine, OS, コンパイラの順で記述されています. たとえば, Sun Sparc, Solaris, g++ 用のパッケージはleda-4_2-sparc-solaris-g++-2_95.tar.gz となります. 環境にあったパッケージを用意してください. インストール方法は UNIX 系 と Windows 系で異なります. UNIX 系 1. UNIX 系のオブジェクトパッケージは ZIP 形式になっています. zcat LEDA-x.x-Machine-OS-Compiler.tar.gz | tar xfv -* で解凍すると 1.1. インストールとコンパイル 3 LEDA-x.x-Machine-OS-Compiler というディレクトリが作成されます. 2. 解凍したディレクトリ(以下<LEDA>とする) に移ると lib*.a ファイル があります. これは静的リンク用のライブラリです. パッケージには静 的リンク用のライブラリしか含まれていません. 動的リンク用のライブ ラリが必要なときは make shared を実行して動的リンク用ライブラリを作成してください. 実行後, 動的リ ンク用ライブラリである lib*.so ファイルが作成されます. 3. テストを兼ねてデモプログラムをコンパイルします. make demos を実行してください. ここでエラーが発生するようでしたら, 動作環境を 確認してください. 4. デモプログラムのコンパイルが無事終了したら, インクルードファイル をコピーします. インクルードファイルは <LEDA>/incl/LEDA にあります. ディレクトリごとコンパイラのインクルードファイルのインス トール場所にコピーしてください. コンパイラをインストールされた環境 によって異なりますが, 標準的なディレクトリは /usr/(local/)include です. 5. ライブラリファイルをコピーします. ライブラリファイルは<LEDA>にあ るlib*.a ファイルまたはlib*.so ファイルです. 動的リンクか静的リン クかで, どちらかを選んでください. すべてのファイルをコンパイラのラ イブラリファイルのインストール場所にコピーしてください. 標準的な ディレクトリは /usr/(local/)lib です. UNIX 系でのインストールは動作環境やコンパイラのインストール状況に 大変依存します. 上の方法で正しくコンパイルできない場合はパッケージ解 凍後に作成される INSTALL ファイルに従ってインストールしてください. 4 第1章 LEDA のインストール Windows 系 1. Windows 系のオブジェクトパッケージは自己解凍形式になっています. LEDA-x.x-win32-Compiler-lib.exe を実行してインストールを開始します. インストーラの指示に従って操 作してください. 途中で解凍先を尋ねられます. 一般的にはc:\LEDA を 指定してください. 2. インストーラはパッケージの解凍をするだけです. 後の作業はすべて手作 業で行います. まずはじめにデモプログラムをコンパイルしましょう. パッ ケージを解凍したフォルダ(以下<LEDA>とする) へ移動し, make_demo を実行してください. ここでエラーが発生するようでしたら, 動作環境を 確認してください. 3. デモプログラムのコンパイルが無事終了したら, インクルードファイルを コピーします. インクルードファイルは <LEDA>\incl\leda にありま す. フォルダごとコンパイラのインクルードファイルのインストール場 所にコピーしてください. コンパイラをインストールされた環境によっ て異なりますが標準的なフォルダは次のようになります. BC++ 5.0 C:\bc5\include\leda VC++ 5.0 C:\programs\devstudio\vc\include\leda VC++ 6.0 C:\program files\microsoft visual studio\vc98 \include\leda 4. ライブラリファイルをコピーします. ライブラリファイルは<LEDA>にあ るlib*.lib ファイルです. すべてのファイルをコンパイラのライブラリ ファイルのインストール場所にコピーしてください. 標準的なフォルダ は次のようになります. BC++ 5.0: C:\bc5\lib VC++ 5.0: C:\programs\devstudio\vc\lib VC++ 6.0: C:\program files\microsoft visual studio\vc98\lib 1.1. インストールとコンパイル 1.1.2 5 コンパイル LEDA は libL, libG, libP, libD3, libW, libGeoW の 6 つのライブラリから 構成されています. 必要に応じてこれらを作成したプログラムにリンクしま す. リンクの方法はインストールした環境によって異なりますし, コンパイラ によって多少異なります. 標準的なインストールをしたときの G++, VC++, BC++でのコンパイル方法は, 次のようになります. ここで示す Windows 系でのコンパイル方法は MS-DOS プロンプトで コマンドラインからコンパイルするため, あらかじめ MS-DOS の環境変数 の設定をする必要があります. たとえば, VC++ 6.0 では MS-DOS プロ ンプトを起動後C:\program files\microsoft visual studio\vc98\bin \vcvars32.bat を実行して環境変数を設定します. 自動的に設定するなら ば, c:\autoexec.bat などにvcvars32.bat の内容をコピーすれば, Windows の起動時に設定されます. GUI 環境でコンパイルする場合は [プロジェ クト]-[設定] から [リンク] タブを選択し, [オブジェクト/ライブラリ モジュー ル] の項目にコマンドラインで指定するライブラリを追加してください. 詳細 はコンパイラのマニュアルを参照してください. libL メインライブラリ LEDA を使用するときには必ず必要なライブラリです. integer や rational などの数値データ型(2 章)から, array や stack などの基本デー タ構造 (3 章) が含まれます. Unix 系: g++ prog.c -lL -lm VC++: cl -Tp prog.c libl.lib BC++: bcc32 -P -w- prog.c libl.lib libG グラフライブラリ グラフデータ構造とグラフアルゴリズムのライブラリです. graph, node, edge などのグラフのデータ構造や, DIJKSTRA や MAX FLOW など のグラフアルゴリズムが含まれます (4 章). グラフライブラリを使うに 6 第1章 LEDA のインストール はメインライブラリも一緒にリンクする必要があります. Unix 系: g++ prog.c -lG -lL -lm VC++: cl -Tp prog.c libg.lib libl.lib BC++: bcc32 -P -w- prog.c libg.lib libl.lib libP 平面幾何ライブラリ point, line, circle などの幾何用のデータ構造や CONVEX HULL や VORONOI などの幾何アルゴリズムが含まれます (5 章). 平面幾何ラ イブラリを使うにはメインライブラリとグラフライブラリも一緒にリ ンクする必要があります. Unix 系: g++ prog.c -lP -lG -lL -lm VC++: cl -Tp prog.c libp.lib libg.lib libl.lib BC++: bcc32 -P -w- prog.c libp.lib libg.lib libl.lib libW グラフィックツールライブラリ window をはじめとするグラフィックユーザインタフェース (GU) を提 供するライブラリです. 平面幾何のデータである point, line, circle など の入出力が容易に行えます (2 章). 通常の window のほか, グラフデー タへの GUI を提供する graphwin も含まれます (4 章). グラフィック ツールライブラリを使うには先の 3 つのライブラリのほかに UNIX で は X11 ライブラリを, VC++では Windows 関係のライブラリを一緒に リンクする必要があります. 1.1. インストールとコンパイル 7 Unix 系: g++ prog.c -lW -lP -lG -lL -lX11 -lm VC++: cl -Tp prog.c libw.lib libp.lib libg.lib libl.lib user32.lib gdi32.lib comdlg32.lib shell32.lib advapi32.lib wsock32.lib BC++: bcc32 -P -w- prog.c libw.lib libp.lib libg.lib libl.lib libD3 3 次元幾何ライブラリ d3 point, d3 line, d3 circle などの 3 次元幾何用のデータ構造や CONVEX HULL などの 3 次元幾何アルゴリズムが含まれます. 3 次元幾何 ライブラリを使うには先の 4 つのライブラリも一緒にリンクする必要 があります. Unix 系: g++ prog.c -lD3 -lW -lP -lG -lL -lX11 -lm VC++: cl -Tp prog.c libd3.lib libw.lib libp.lib libg.lib libl.lib user32.lib gdi32.lib comdlg32.lib shell32.lib advapi32.lib wsock32.lib BC++: libGeoW bcc32 -P -w- prog.c libd3.lib libw.lib libp.lib libg.lib libl.lib 幾何用グラフィックツールライブラリ window よりもさらに機能を強化した幾何用グラフィックツールを提供 するライブラリです. 幾何用グラフィックツールライブラリを使うには すべてのライブラリを一緒にリンクする必要があります. Unix 系: g++ prog.c -lGeoW -lD3 -lW -lP -lG -lL -lX11 -lm VC++: cl -Tp prog.c libgeow.lib libd3.lib libw.lib libp.lib libg.lib libl.lib user32.lib gdi32.lib comdlg32.lib shell32.lib advapi32.lib wsock32.lib BC++: bcc32 -P -w- prog.c libgeow.lib libd3.lib libw.lib libp.lib libg.lib libl.lib 8 第1章 LEDA のインストール 正しくコンパイルできないときは, 次の点を確認してください. ライブラリは必ず上の順番で指定してください. Solaris で window を使う場合は, socket, nsl と thread のライブラリが 必要なので, 次のようにコンパイルしてください. g++ prog.c -lW -lP -lG -lL -lX11 -lsocket -lnsl -lthread -lm インクルードファイルとライブラリファイルを/usr/(local/)include と /usr/(local/)lib 以外にインストールした場合は, -I オプションと -L オプションを使って場所を指定できます. インクルードファイルをイン ストールしたディレクトリを leda-include-path として, ライブラリファ イルをインストールしたディレクトリを leda-library-path とすると, 次 のように指定します. g++ prog.c -Ileda-include-path -Lleda-library-path ... 9 第 2 章 C言語と LEDA 本章ではC言語の知識があまりない読者を対象に, C言語の基礎を説明して います. ただ, 入力と出力に関してはC++の形式を使っています. これはC 言語の入出力の形式よりC++の形式の方が直感的で理解しやすいからです. ですから, コンパイラもC言語のコンパイラではなく, C++のコンパイラを 最初から使うようにしてください. また, 繰り返しますが, 本書では学問的に 正確な記述よりも初心者の直感に訴える書き方をしていますので, 言語の専 門家は眉をひそめてしまう表現も多々ありますが, その点は予めご了承くだ さい. 2.1 最初のプログラム 計算機のプログラムを組んだことのない人にとって, C言語でプログラム を組むということは大変なことのように思われがちですが, 実際には料理と 同じで慣れてしまえば結構簡単に作れてしまうものです. まずプログラムを 組むためには使用する言語を決めなければなりません. 自然言語にも英語や フランス語のように多数の異なる言語があるように, 計算機言語にも実に様々 な言語が存在します. 最近ではC言語(あるいはC++)や JAVA 言語が使 われることが多いようですが, 本書で解説しようとしている LEDA は基本的 にはC++で書かれたプログラムの集まり(ライブラリ)です. C言語のプログラムは関数の集まりと見ることができます. 関数にはプロ グラマが自分で作るべき関数と, 言語の方で既に用意されているものがあり ます. 一つ例外と言えば, main という名前の関数です. どんなC言語(ある いはC++)のプログラムにも必ずこの関数が含まれていないといけません 10 第2章 C言語と LEDA し, この main 関数が最初に実行されることになっています. しかし, 関数内 での処理は自由に決めることができます. というわけで, 最も簡単なC言語の プログラムは main 関数だけからなる次のようなものです. //program 2-1-1.c: 最も簡単なプログラム main() { } これは確かにプログラムではありますが, 何もしないので, まったく意味が ありません. では, 何か実行させてみましょう. 最も分かりやすい処理は決められた文字列をコンピュータの画面上に出力 することです. そのためには, cout << "文字列"; という \文" をプログラムに置けばいいのです. 英語などでは文の終りをピリ オドで示しますが, C言語ではピリオドの代わりにセミコロン \;" を使います. では, 画面上に \Hello, LEDA!" という文字列を出力するプログラムを作り ましょう. そのためには, cout << "Hello, LEDA!"; とすればいいのですが, これでは見づらいので, 普通は最後に改行を行います. 改行を行うには, end of line を表す \endl" を出力関数 cout に送ればいいので す. すなわち, cout << "Hello, LEDA!" << endl; とすればいいのです. このように, 出力したいものが多数あるときは, それら を \<<" という記号でつなげばいいのです. これで文字列を出力するプログラムはできたも同然ですが, 実は計算機に とって文字列を画面上に出力するのはそれほど簡単な作業ではありません. 詳 しいことは言いませんが, 文字を表しているコードの変換などに面倒なこと があり, 出力関数 cout のプログラムは決して自明なものではないのです. そ 2.1. 最初のプログラム 11 れを自分で作らなければならないとすると大変なので, 実際には作り付けの ものが用意されています. ただ, そのような作り付けの関数を利用する場合に は, それが置いてある場所を指定しないといけません. 出力関数 cout の場合 には, \iostream.h" という場所ですので, その場所の指定を行って初めて実行 可能な完全なプログラムになります. また, プログラムは自分だけが読めれば よいというものではなく, 常に他人が見ても分かるように書くことが大切で す. そのためには, プログラムの各所に適切な注釈文をつけることが大事で す. C言語では \= " から始まって \ =" で終るまでの部分に注釈を書きます が, C++では \//" からその行の終りまでが注釈となります. //program 2-1-2.c: 最初の LEDA プログラム #include <iostream.h> main() { cout << "Hello, LEDA!" << endl; } このプログラムを実際に実行するためには, 人間にとって分かりやすい形 式で書かれたプログラムを計算機が直接実行できる機械語に翻訳するための コンパイルという作業が必要になります. コンパイラを起動する方法は, 使用 計算機およびその環境によって異なります. 具体的な方法については前章で 場合に分けて詳しく説明していますので, そちらを参照してください. 演習問題 2.1.1 This is the first C program. と出力するプログラムを 作りなさい. 演習問題 2.1.2 次のプログラムを実行して出力を確かめなさい. #include <iostream.h> main() { cout << "LEDA is a software library "; cout << "developed at Max Planck Institute in Germany."; } 12 第2章 演習問題 2.1.3 C言語と LEDA 次のように出力するプログラムを作りなさい. To be, or not to be, that is the question. 2.2 簡単な数値計算 計算機にとって数値計算は最も得意とするところです. 複雑な式であって も「計算機に認識できる形式で」書いてあれば答を求めることができます. た とえば, 摂氏 38 度が華氏では何度になるかを求めるのは, 摂氏の温度を 1:8 倍 して 32 を加えればいいので, 38*1.8 + 32 と表現することができます. ここで, \ " の記号は乗算を表しています. この ようにして計算すべきものが式の形式で表現できれば, 後はこれを出力関数 cout に渡してやるだけでいいのです. 具体的には, cout << 38*1.8 + 32 << endl; とすればいいのです. これでプログラムを組むこともできますが, これでは摂 氏 38 度の変換しかしてくれません. 一般に摂氏の温度をデータとして入力し て, 対応する華氏の温度を求めるには \変数" を用いる必要があります. 変数 とは, 電卓についているメモリーキーのようなものです. 電卓のメモリーキー は 1; 2; : : : のような数字で互いに区別をしていますが, C言語では変数に自分 で好きな名前を付けることができます. 名前の付け方には, 必ず英字で始まら なければならないとか, 英字と数字以外の文字を名前の一部に含めるときに は厳しい制限があるなど, 様々なルールがありますが, 基本的には, 「英字で 始まり, 英字と数字とアンダーライン \ " からなる文字列」だと考えておけば 大丈夫です. さて, 摂氏の温度を表すのに \c" という名前の変数を用いることにしましょ う. すると, 求める華氏の温度は, c*1.8 + 32 2.2. 簡単な数値計算 13 と表すことができます. 後は変数 c にデータを入れればいいわけですが, これ には出力関数 cout に対応する入力関数 cin を使います. 具体的には, cin >> c; とすればいいのです. cout と比べてみると, <<ではなく>>と向きが逆になっ ていることが分かるでしょう. cin >> c; の場合には, 標準入力からのデー タを変数 c に入れるので, 変数に向かう方向になっていますが, cout << c; の場合には変数 c の値を標準出力に渡すので, 変数から出る方向になっている のです. したがって, cin << c; とか, cout >> c; とするのは間違いです. プログラム全体は次のようになります. //program 2-2-1.c: 摂氏から華氏への変換 #include <iostream.h> main() { int c; cin >> c; cout << c*1.8 + 32 << endl; } // 変数 c を int 型で宣言 // 入力された値を変数 c に蓄える // 算術式の値を出力し, 改行. 上のプログラムに示すように, 変数については使う前に必ず宣言をする必 要があります. 上の例では変数 c に整数の値を入力することを想定して, 変数 c が整数であるという意味で \int c;" という宣言を行っています. 上のプログラムでは華氏での温度 c 1:8 + 32 を計算して, それを直接出力 しましたが, 華氏での温度 c 1:8 + 32 を別の変数, たとえば変数 f に蓄えて おいて, 最後に変数 f の値を出力するようにすることもできます. 変数に式の 値を蓄える命令を代入命令と言いますが, プログラムでは 変数 = 算術式; の形式で書きます. 数学では \=" という記号を \等しい" ことを表すのに用い ていますが, C言語では変数に式の値を代入するのに用いています. 下のプロ グラムでは, 華氏での温度を変数 f に蓄えた後で変数 f の値を出力するよう 14 第2章 C言語と LEDA に改めたものです. ここで, 算術式とは, 1 や 0:35 のような定数あるいは変数 や関数を含んだ算術式を意味しますから, 例を挙げると次のようになります. 代入文の例: a = 1; b = 2*b+1; c = 1.5*sin(t) + 1.2*cos(t); //program 2-2-2.c: 摂氏から華氏への変換 (2) #include <iostream.h> main() { int c, f; cin >> c; f = c*1.8 + 32; cout << f << endl; } // // // // 変数 c と f を int 型で宣言 入力の値を変数 c に蓄える 算術式の値を変数 f に蓄える 変数 f の値を出力し, 改行. このプログラムでは c と f の 2 つの変数を用いていますが, 一つの変数だけ でも同じことができます. つまり, f を使わずに, 式の値を再び c に代入する c = c*1.8 + 32; という代入文を用いて出力すべき値を再び変数 c に蓄えて, 変数 c の値を出力 するのです. \=" の記号を等号と考えると方程式のように見えますが, 先に も言いましたように, これは右辺の算術式の値を左辺の変数に蓄えるという 命令なので, なんら問題はありません. 変更後のプログラムを下に示します. //program 2-2-3.c: 摂氏から華氏への変換 (3) #include <iostream.h> main() { int c; cin >> c; 2.2. 簡単な数値計算 } c = c*1.8 + 32; cout << c << endl; 15 // 変数 c の値を使って計算した算術式の値を // 再び変数 c に蓄えて, その値を出力. 整数は正の数だけでなく負の数でも構いませんが, 小数点を含む数値は表 現できません. 小数点を含む数値は \double" 型と呼ばれます. これ以外にも 様々な変数の型がありますが, それらについては追って説明することにしま しょう. さて, 上のプログラム 2-2-3.c を実行すると, カーソルが点滅する入力待ち の状態になります. これでは何を入力すべきか分からないので, 普通は何を 入力すべきかを画面上に出力します. そのように改めたのが次のプログラム です. //program 2-2-4.c: 摂氏から華氏への変換 (4) #include <iostream.h> main() { int c; cout << "摂氏の温度を入力してください "; cin >> c; c = c*1.8 + 32; cout << "華氏の温度 = " << c << endl; } プログラム 2-2-4.c の実行例 > 2-2-4 摂氏の温度を入力してください 38 華氏の温度 = 100 このように cout と cin を組み合わせるのが普通ですが, \iostream.h" の代 わりに LEDA の \LEDA/stream.h" を使うと read int() という便利な関数が 使えます. これは, () 内で指定した文字列を出力した後で入力された整数値 を値として返すというものです. これを使うと上のプログラムは次のように すっきりとした形になります. 16 第2章 C言語と LEDA //program 2-2-5.c: 摂氏から華氏への変換 (5) #include <LEDA/stream.h> main() { int c; c = read_int("摂氏の温度を入力してください "); c = c*1.8 + 32; cout << "華氏の温度 = " << c << endl; } 加算と乗算の他に減算と除算ももちろん使えます. 減算を表すのは \ " の 記号ですが, 除算については \ " の記号がキーボードにないので, スラッシュ \/" を用います. また, 分数は使えませんので, 分数はすべて除算を用いて表 現することになります. 複雑な式では括弧が必要になります. 数学では多種 類の括弧を使い分けますが, C言語ではいわゆる丸括弧 \(" と \)" だけが使え ます. たとえば, 底面の半径 r , 高さ h の円錐の体積は 13 r 2 h と表すことがで きますが, これをC言語で表現すると, 3:14 r r h=3 となります. 円周率の という記号は使えないので, 上のようにその近似値 を具体的に表現する必要があります. double 型の数値を入力するには, 関数 read real() を使うことができます. 下に示したのは円錐の体積を求めるプロ グラムです. //program 2-2-6.c: 円錐の体積を求める #include <LEDA/stream.h> main() { double r, h; r = read_real("底面の半径を入力してください "); h = read_real("円錐の高さを入力してください "); cout << "円錐の体積 = " << 3.14 * r*r*h/3 << endl; } 2.2. 簡単な数値計算 17 プログラム 2-2-6.c の実行例 > 2-2-6 底面の半径を入力してください 10.0 円錐の高さを入力してください 24.0 円錐の体積 = 2512 演習問題 なさい. 2.2.1 次の算術式の値を予想し, プログラムを作って実際に確かめ (1) 3 (2 + 4 (8 1)=3) (2) (1=2) (5 2) (9 9 (3) +3 7 演習問題 2.2.2 5=2) 次の算術式を計算するプログラムを作りなさい. (1) f1:5 + (3:14159 20:5 + 2:5) 180:0g (2:2 1:0 3:3) (2) [5:3 2:1 + f10:4 (3:3 + 1:5 2:3) + 9:5g] (13:1 4:5 + 3:8) 演習問題 2.2.3 代入命令 x=3*x+1; はどのような意味をもちますか. また, x=2; の状態からはじめて代入命令 x=3*x+1; を 3 回繰り返したとき, 変数 x の値はどのように変化しますか. また, 代入命令 x=3*x+1; を続けて 3 回実行 するのと同じ効果をもつ代入命令は何ですか. 演習問題 2.2.4 自動車の燃費を計算するプログラムを作りなさい. 走行距離 とガソリンの量を入力して, ガソリン 1 リッター当りの走行距離を算出します. 演習問題 2.2.5 分の単位を時間と分に変換するプログラムを書きなさい. た とえば, 100 分の場合は 1 時間 40 分と答えます. 演習問題 2.2.6 3点の x; y 座標を入力して, その3点を頂点とする三角形の 面積を求めるプログラムを書きなさい. 18 第2章 C言語と LEDA ウィンドウの基礎 2.3 2.3.1 ウィンドウの宣言 一般にウィンドウ上に図形を描画するプログラムを書こうとするとマニュア ルと首っ引きで, こんな面倒なことを誰が考えたと言いたくなりますが, LEDA を使うと実に簡単です. 例として, ウィンドウを開いてそこに点のデータを表 示するプログラムを LEDA で書いてみましょう. //program 2-3-1.c: ウィンドウ上に点を描く #include <LEDA/window.h> main() { point p; window W (400, 400); W.display(); } W >> p; W << p; W.read_mouse(); W.close(); // 点を表す変数 p を宣言 // W という名前のウィンドウを作る // ウィンドウ W の画面表示 // ウィンドウ上で点 p を指定し, 表示 // マウスの左ボタンが押されるまで待つ // ウィンドウを閉じる 上のプログラムの最初の行がウィンドウ W の宣言です. ここでは, 幅と 高さが共に 400 画素の大きさに指定しています(図 2.1 参照). 次の行の W.display() によって, ウィンドウ W を予め指定された場所に表示すること ができます. 表示場所を指定することもできますが, それについては後で説明 することにします. また, ここでは W という名前を使っていますが, もちろ ん別の名前を使っても構いません. さらに, 複数個のウィンドウを画面上に作 ることも可能です. さて, ウィンドウが定義できたら, ウィンドウからデータを受け取ります. この場合は受取るデータがマウス(の左ボタン)でクリックされた地点の座 標データです. LEDA には画面上の点を表すデータタイプが \point" という 名前で既に定義されていますから, point p; として変数 p の型を宣言してお けば, 2.3. ウィンドウの基礎 19 図 2.1: ウィンドウの例 W >> p; とすれば, 画面上でマウスの左ボタンのクリックで指定された点の座標データ が変数 p に代入されます. これは, 数値変数 x に値を入力するのにcin >> x; としたのと全く同じです. 同じように, W << p; とすれば, p で指定されたウィンドウ上の点に点の印 \ " を出力することが できます. 点の存在を示すのに, \ " の印の他にも \o" など幾つかの印が用 意されていますが, 何も指定しなければデフォールトの \ " 印が使われます. 最後の W.read mouse() は, ウィンドウ W 上でマウスがクリックされるま で待て, という意味です. これがないと点を表示した後, すぐにウィンドウを 閉じる関数 W.close() が実行されてしまうので, 点が確かに表示されていると ころが見えないのです. LEDA には点の他にも, 線分, 円, 多角形などを表すデータタイプが用意さ れていますから, 上と同じようにして線分, 円, 多角形などをウィンドウ上に 20 第2章 C言語と LEDA 描くプログラムを書くのも簡単なことです. 次のプログラムは, 点, 線分, 円, 多角形の順にマウスで入力するというものですが, ウィンドウ上でどのデー タタイプの入力を待っているかがウィンドウの上端に表示されますので, 特 に cout 関数を使って表示しなくても混乱はありません. //program 2-3-2.c: ウィンドウ上に様々な図形を描く #include <LEDA/window.h> main() { point p; segment s; circle c; polygon pol; window W (400, 400); W.display(); } W >> p; W << p; W >> s; W << s; W >> c; W << c; W >> pol; W << pol; W.read_mouse(); W.close(); // // // // 点 p の宣言 線分 s の宣言 円 c の宣言 多角形 pol の宣言 // // // // 点データの取得と表示 線分データの取得と表示 円データの取得と表示 多角形データの取得と表示 プログラム 2-3-2.c の実行の様子を示したのが図 2.2 です. 点を入力するのはマウスの左ボタンをクリックするだけなので簡単です. 線 分を入力するときも同じ要領です. まず, マウスの左ボタンをクリックして線 分の一方の端点を決め, 続いて再度左ボタンをクリックした場所がもう一方の 端点となります. 円の場合には最初のクリックで円の中心を定め, 次のクリッ クで円周上の 1 点を指定します. 多角形を入力するときは, 頂点を境界に沿っ て順に指定して行き, 最後の頂点を指定した後で右ボタンをクリックすると, 最後の頂点と最初の頂点が結ばれて多角形が構成されます. 正確には, 辺の左 側が多角形の内部になります. 2.3. ウィンドウの基礎 21 y 400画素 (0,100) 400 画素 x (100,0) (0,0) 図 2.2: プログラム 2-3-2.c の実行 の様子 2.3.2 図 2.3: ウィンドウのサイズと座標系 ウィンドウ座標系 point 型の変数 p に値を代入するのも LEDA なら簡単です. たとえば, (10; 20) という場所として変数 p を設定したければ, p = point(10.0, 20.0); という代入文を書くだけでいいのです. ここで若干注意が必要なのは, ウィンドウの座標系とウィンドウのサイズ との関係です. 先にウィンドウの物理的なサイズを 400 400 として宣言し ましたが, これはウィンドウを構成する画素数を表しています. ウィンドウの 座標系は画素とは関係なく, x 座標に関しては 0 から 100 まで, y 座標に関し ても 0 から 100 までに設定されています. ウィンドウを window W (400, 400) として作成した場合の説明図を図 2.3 に示します. したがって, 画面中央の点は point(50.0, 50.0) として指定する必要があり ます. 座標の指定には double 型の値を用いることも分かってもらえたと思い ます. もちろん, 正方形以外のウィンドウを開いたり, 座標系を任意に指定す 22 第2章 C言語と LEDA ることも可能ですが, 取りあえずはデフォールトで指定されているままで使 うことにしましょう. 次のプログラムは, 画面上に座標値で指定した点と線分を描画するための プログラムです. 座標系に注意して出力と見比べてみてください. //program 2-3-3.c: ウィンドウ上に様々な図形を描く #include <LEDA/window.h> main() { window W (400, 400); W.display(); // 座標値による点の指定 W << point(10.0, 20.0); W << point(20.0, 10.0); W << point(50.0, 50.0); // 両端点の座標値による線分の指定 W << segment(10.0, 10.0, 50.0, 30.0); W << segment(10.0, 30.0, 50.0, 10.0); } W.read_mouse(); W.close(); プログラム 2-3-3.c の実行の様子を示したのが図 2.4 です. 演習問題 2.3.1 変数 p; q がpoint 型で宣言されているとき, segment(p,q) とすると, 2 点 p; q を端点とする線分を指定することができます. このことを 使って, マウスで 2 点をクリックして, それらを結ぶ線分をウィンドウ上に表 示するプログラムを作りなさい. 演習問題 2.3.2 前問のプログラムを拡張して, マウスで 3 点をクリックして, それらで定まる三角形をウィンドウ上に描くプログラムを作りなさい. 2.3. ウィンドウの基礎 23 図 2.4: プログラム 2-3-3.c の実行の様子 演習問題 2.3.3 2 点間の距離は p.distance(q) のようにして求めることが できます. これを使って, マウスで 2 点 p; q を入力した後, それぞれの点を中 心とし, 他方の点を通る 2 つの円を描くプログラムを作りなさい. ただし, 点 p を中心とする半径 r の円は, circle(p, r) と表すことができます. 演習問題 2.3.4 特に指定しなければ, ウィンドウ座標系は左上角が原点 (0; 0) で, x 座標の範囲が 0 から 100 までとなっています. y 座標に関しては, 最小 値が 0 で最大値はウィンドウを指定したときの(画素数による)縦横比で決 まります. 負の軸を表現したりするには座標系の定義を変更する必要があり ますが, そのときは, W.init(xmin, xmax, ymin); と指定します. ここで, xmin として x 座標の最小値, xmax として x 座標の最 大値, ymin として y 座標の最小値を指定します. y 座標の最大値はウィンド ウの縦横比によって決まります. たとえば, W.init(-100, 100, -100) のよ 24 第2章 C言語と LEDA うにします. この機能を用いて, 座標軸を描いた後, 原点を中心とし, 半径を 50 とする円を表示するプログラムを書きなさい. 2.4 2.4.1 制御構造 条件分岐 今までに見てきたプログラムでは, プログラム内の文を上から順に実行し ていくものばかりでしたが, 複雑な処理を行おうとすると状況に応じて様々な 分岐が必要になります. たとえば, 2 つの整数 x と y を入力して, それらの差 を出力するプログラムについて考えてみましょう. 2 数 x と y の差 d は, x > y のときは d = x y , 逆に x y のときは d = y x として求めることができ ます. すなわち, 2 数の大小によって計算式が異なるのです. このような分岐 処理を行うための制御構造が \if" 文です. 具体的には, if(条件式) 文 1; else 文 2; の形式の構文です. \条件式" の部分には大小比較のような比較のための式を 書くのが普通です. その条件式を計算した結果が正しいなら文 1 を, 正しくな ければ文 2 を実行します. つまり, 条件式が成り立つかどうかで, 文 1 または 文 2 のいずれか一方だけが実行されるのです(図 2.5 参照). たとえば, 2 数 x; y の差を計算するには, if(x > y) d = x-y; else d = y-x; とします. あるいは, 先に d = x y ; として計算した後で d の値が負ならば d = y x; と変更するというのでも構いません. その場合には else 部がなく なって, 次のようになります. d = x - y; if( d < 0) d = y - x; 2.4. 制御構造 25 条件式 偽 条件式 真 文1 真 条件式 偽 偽 文1 文2 if-else構文 else部なし 真 文1 文2 文3 ブロック構造 図 2.5: 条件分岐の形式 演習問題 2.4.1 2 つの整数値を入力して, それらを昇順に出力するプログラ ムを作りなさい. 次のプログラムは, ウィンドウ上でマウスによって 2 点を指定したとき, x 座標が大きい方の点の色を変えて表示するというものです. 点 p の x 座標は p.xcoord() として参照できます. また, 今までは色を指定せずに点を表示し ていましたが, プログラムでは W.draw point() を用いて描画色を指定してい ます. //program 2-4-1.c: どちらが右? #include <LEDA/window.h> main() { point p, q; window W (400, 400); W.display(); } W >> p; W << p; W >> q; W << q; if(p.xcoord() > q.xcoord()) W.draw_point(p, red); else W.draw_point(q, red); W.read_mouse(); W.close(); // // // // // 点 p の指定 点 q の指定 x 座標の大小を比較 点 p が左にあるとき, 点 p を赤で そうでないとき, 点 q を赤で表示 26 第2章 C言語と LEDA 条件式を記述するのに不等号 > 以外にも様々な数学記号を使うことができ ます. それらをまとめたのが下の表 2.1 です. 大事なのは, 複数の条件を AND (記号 && )と OR(記号 || )を用いて組み合わせることができる点です. さ らに, NOT(記号 ! )を用いて条件を否定することもできます. 記号 == != > >= < <= && || ! 意味 例 例の意味 等しい n==2 x*x != y*y+z*z n > 3 n >= x1+ x < 0.5 t <= 2 2 <= n && n <= 5 n==0 || n==1 !(a==b || b==c) n は 2 に等しい x x は y y + z z と異なる n は 3 より大きい n は x+1 以上 x は 0.5 より小さい t は 2 以下 n は 2 以上かつ 5 以下 等しくない より大きい 以上 より小さい 以下 かつ または でない n==0 またはn==1 (a==b またはb==c) でない 表 2.1: 条件式の表現 複雑な条件式の例として閏年の判定条件を考えてみましょう. よく知られ ているように, 閏年は, 原則的に 4 年に一度訪れますが, 100 年に一度は閏年 ではなく, さらに 400 年に一度は 100 で割り切れても閏年となります. した がって, n 年が閏年であるための条件は次のように表現できます. 「(n は 400 の倍数)または(n は 100 の倍数ではないが, 4 の倍数) 」 これを AND を表す論理演算 && と OR を表す || を用いて記述すると次のよ うになります. (n % 400 == 0) || (n % 100 != 0 && n % 4 == 0) ただし, a % b は整数 a を整数 b で割ったときの余りを求めるモデュロ演算 です. 2.4. 制御構造 27 閏年判定の式では分かりやすいように適当に条件を括弧でくくっています が, 一般に論理関係子 AND && は OR || より優先順位が高いので, 括弧を省 略しても同じ意味になります. 以上より, 入力で指定された西暦の年数が閏年 かどうかを判定するプログラムは次のようになります. //program 2-4-2.c: 閏年かどうかの判定 #include <LEDA/stream.h> main() { int n; n = read_int("西暦の年数を入力してください. "); if((n % 400 == 0) || (n % 100 != 0 && n % 4 == 0)) cout << n << "年は閏年です. " << endl; else cout << n << "年は閏年ではありません. " << endl; } 今までに見てきたプログラムでは条件式が成り立つ場合も, 成り立たない 場合も, ともに一つの文を実行するだけでしたが, 一般には, 多数の文からな る処理を実行しなければならないことがよくあります. そのような場合には, 処理内容全体を一つの文と同等に扱うために, それらの処理を表す複数の文 を中括弧 と でくくります. たとえば, 条件式が成り立つときは, 文 1, 文 2 を実行し, 成り立たないときは文 3, 文 4 を実行したいときには, f g if(条件式){ 文 1; 文 2; } else { 文 3; 文 4; } のように表現します. もちろん, 文 1 とか文 4 が別の if 文であっても構いませ ん. とにかく, どんなに複雑になっていても, 条件式が成り立つときは, 次の 28 第2章 f C言語と LEDA g 左中括弧 から始まって対応する右中括弧 に至るまでの処理を実行せよ, と いうことになります. このような構造をブロック構造と言います. これから も複雑なプログラムにはこの構造が頻繁に出てきますので, 注意しましょう. ウィンドウ上に 2 点 (30; 40) と (75; 65) を対角とする長方形を描いた上で, マウスでウィンドウ上の 1 点を指定して, その点が長方形の内部にあるかど うかを答えるプログラムを作ってみましょう. まず長方形を描くには長方形 の 4 頂点を指定して, それらを順序良く線分で結べばいいので, 次のようなプ ログラムになります. W W W W << << << << segment(30,40, segment(75,40, segment(75,65, segment(30,65, 75,40); 75,65); 30,65); 30,40); // // // // 点 (30,40) と点 (75,40) を結ぶ線分を表示 点 (75,40) と点 (75,65) を結ぶ線分を表示 点 (75,60) と点 (30,65) を結ぶ線分を表示 点 (30,65) と点 (30,40) を結ぶ線分を表示 y (100,100) (0,100) 65 40 (0,0) 30 x 75 (100,0) 図 2.6: ウィンドウ内の指定された長方形 後は, マウスによって指定された点 p がこの長方形の内部にあるかどうか を確かめればいいのですが, それは点 p の x 座標が 30 と 75 の間にあり, かつ p の y 座標が 40 と 65 の間にあるかどうかで判定できます (図 2.6 参照). つ まり, 30 x 75 and 40 y 65 2.4. 制御構造 29 が成り立つかどうかを調べればいいのです. このように数学では 3 つの数を 同時に比較することがよくありますが, C言語ではいつも 2 つの数しか比較 できません. そこで, AND を用いて上の条件式を 30 x and x 75 and 40 y and y 65 とします. これをC言語で記述すると, if(30 <= x && x <= 75 && 40 <= y && y <= 65) ... となります. 点 p の x; y 座標値はそれぞれ p.xcoord() と p.ycoord() で参照で きますので, 全体のプログラムは次のようになります. //program 2-4-3.c: 長方形の内部・外部の判定 #include <LEDA/window.h> main() { point p; double x, y; window W (400, 400); W.display(); W W W W } << << << << segment(30,40, segment(75,40, segment(75,65, segment(30,65, 75,40); // 長方形を4点の座標で表示 75,65); 30,65); 30,40); W >> p; W << p; // ウィンドウ上で点を指定 x = p.xcoord(); y = p.ycoord(); // 点 p の x, y 座標値を x, y とする if(30 <= x && x <= 75 && 40 <= y && y <= 65) cout << "内部の点です " << endl; else cout << "外部の点です " << endl; W.read_mouse(); W.close(); 演習問題 2.4.2 プログラム 2-4-3.c では長方形の対角の 2 頂点をプログラ ムの中で数値的に与えましたが, ウィンドウ上で任意に指定できるように改 めなさい. 30 第2章 C言語と LEDA 演習問題 2.4.3 プログラム 2-4-3.c を改良することにより, 2 つの長方形 を入力して, それらが共通部分をもつかどうかを答えるプログラムを作りな さい. 演習問題 2.4.4 前問では 2 つの長方形が共通部分をもつかどうかだけを判定 しましたが, 共通部分をもつ場合には, さらに共通部分(必ず長方形になりま す)を別の色で表示するプログラムを作りなさい. 2.4.2 繰り返し構造 if 文はプログラムに条件分岐を導入するためのものでしたが, 繰り返し処理 も同様に大事な制御構造です. 繰り返し構造には for ループと while ループ があります. for ループは, ある変数の値を一定値ずつ変化させながら同じ処理を繰り返 し実行するときによく使われます. たとえば, 整数変数 x の値を 0; 1; 2; 3; : : : ; 9 と変化させながら x2 の値を出力するというプログラムは次のようになります. //program 2-4-4.c: 変数の値を順に変化させて平方数を出力する #include <LEDA/stream.h> main() { int x; } for(x=0; x<10; x=x+1) cout << x*x << " "; // x の値を 0 から x<10 の間 1 ずつ増やす // x の2乗の値を出力 という形をしています. 最初に文 1 を実行した後, 文 2 の式が成り立つ間, ループ本体と文 3 を繰り返し実行します(図 2.7 参照). 上の例では, 最初 に x = 0 を実行した後, 毎回 x = x + 1 として x の値を 1 だけ増やしながら, x < 10, つまり x 9 である間, x の平方数 x2 = x x を出力します. x = x +1 のところを x = x + 10 とすれば, x の値を 10 刻みで変化させることができ ます. 2.4. 制御構造 31 式1 上のプログラムにあるように, 文は, for for(文 1; 文 2; 文 3) 偽 出口 式2 真 式3 ループ本体 ループ本体 図 2.7: プログラム 2-4-5.c の実行 の様子 この考え方でウィンドウ上に点列を描いてみましょう. ウィンドウの座標 系は x, y 座標ともに 0 から 100 までとなっています. この画面に直線 y = 2x に沿って, 点列を描画してみましょう. x = 0 から始めて, x の 10 ずつ増やし ながら y = 2x 上の点 (x; 2x) をプロットすればいいわけですから, プログラ ムは次のようになります(図 2.8 参照). //program 2-4-5.c: 直線 y=2x に沿って点列をプロットする #include <LEDA/window.h> main() { int x; window W (400, 400); W.display(); for(x=0; x<100; x=x+10) // x の値を 0 から x<100 の間 10 ずつ増やす W << point(x, 2*x); // 座標 (x, 2x) の点を p とし, 画面に表示 } W.read_mouse(); W.close(); プログラム 2-4-5.c の実行結果は図 2.9 のようになります. 32 第2章 y y=2*x; C言語と LEDA (100,100) (0,100) x=x+10; x (100,0) (0,0) 図 2.8: 直線 y = 2x 直線上での点 のプロット 図 2.9: プログラム 2-4-5.c の実行 の様子 プログラム上では x の値が 0; 10; 20; : : : ; 90 と変化しますが, x = 50 のとき に y = 100 となってウィンドウの上端に達してしまうので, それ以降の点は ウィンドウの外に出てしまいます. ウィンドウ外の点は描画できないので, 最 初の 6 点だけが描画されているのです(ただし, ループの実行は最後まで繰 り返されます). x の増分をもっと細かくすれば多数の点をプロットすることができます. た だし, 点の印が \ " のままでは見にくいので, ドット \." に変更する命令 W.point_style(pixel_point); を挿入しています. //program 2-4-6.c: 直線 y=2x に沿って点列をプロットする (2) #include <LEDA/window.h> 2.4. 制御構造 33 main() { int x; window W (400, 400); W.display(); W.set_point_style(pixel_point); // 点を表示するときの図形を変更 } for(x=0; x<100; x=x+1){ W << point(x, 2*x); } W.read_mouse(); W.close(); // x を 0, 1, ... , 99 と変化させる プログラム 2-4-6.c の実行結果を示したのが図 2.10 です. 図 2.10: プログラム 2-4-6.c の実行の様子 C言語で使える繰り返し構造には for ループ以外に while ループがありま す. これはある条件式が成り立つ間ループ本体を繰り返し実行するための制 御構造ですが, 次に示すように, while をループの先頭に置くか, ループの最後 34 第2章 C言語と LEDA 尾に置くかによって次の 2 つのタイプがあります. フローチャートで表すと 図 2.11 のようになります. 2 つのタイプの while ループ while(条件式) ループ本体 do{ ループ本体 }while(条件式); while(条件式){ ループ本体 } 条件式 真 偽 do{ ループ本体 ループ本体 }while(条件式); ループ本体 真 条件式 偽 出口 出口 図 2.11: while ループの2つの形式 プログラム 2-4-5.c では変数 x の値を 0 から始めて 1 ずつ増やしながら x < 100 である間, (x; 2x) で指定される点をプロットして行きましたが, 途中で ウィンドウの外部に出てしまいました. 点がウィンドウの内部にある間だけ 繰り返すように while 文を用いて書き換えたのが下のプログラム 2-4-6.c です. 今度も x の値を 1 ずつ増やしながら点 (x; 2x) をプロットして行きますが, 毎 回 x と 2x が共に 100 未満であることを確かめて, どちらかが成り立たなくな れば繰り返しを終るようにしています. 2.4. 制御構造 35 //program 2-4-7.c: 直線 y=2x に沿って点列をプロットする (3) #include <LEDA/window.h> main() { int x; window W (400, 400); W.display(); W.set_point_style(pixel_point); } x=0; while(x<100 && 2*x < 100){ // x, y 座標が共に 100 以下なら画面内 W << point(x, 2*x); // 点 (x, 2x) を表示 x=x+1; // x 座標を1増やす } W.read_mouse(); W.close(); 演習問題 2.4.5 プログラム 2-4-7.c には省略しても問題のない冗長な部分 があります. 冗長な部分を取り除いてください. 演習問題 2.4.6 整数 n の値を入力して, 円周を n 等分する点を描くプログラ ムを作りなさい. 演習問題 2.4.7 前問では角度は違っても半径は固定していましたが, 半径も 角度と一緒に増加させるとどのような図形が描けるか, プログラムを作って 確かめなさい. 演習問題 2.4.8 前問では 0 から 2 の区間を n 等分しましたが, 0 から 8 の 間を n 等分するようにプログラムを修正すると, どんな図形が得られますか. while ループを用いると, ウィンドウ上に多数の図形を描くプログラムを簡 単に書くことができます. 先に, W >> p; とすればマウスの左ボタンをクリッ クすることによって指定された点の座標を point 型の変数 p に蓄えることがで きることを知りました. 左ボタンをクリックすると, 確かに座標データが正し 36 第2章 C言語と LEDA く変数 p に代入されますが, 右ボタンをクリックした場合には, クリックした 地点に関係なく, 変数 p には (0; 0) という値が代入されます. C言語ではある 文を実行したとき, その文自体が値をもちますが, 右ボタンをクリックしたと き, 文 \W >> p;" は値 0 をもつことになります. 値 0 は真理値の \偽"(false) に対応し, 0 以外の値は何でも真理値の \真"(true) に対応しますから, この性 質を用いて while ループの終了条件をうまく記述することができます. 次の プログラム 2-4-8.c では, while の条件式 \W >> p" の値が偽になるまで, すな わち, 右ボタンがクリックされるまで, 左ボタンのクリックによって指定され た点をウィンドウ上に表示し続けるようになっています. //program 2-4-8.c: ウィンドウ上に多数の点を描画 #include <LEDA/window.h> main() { point p; window W (400, 400); W.display(); while(W >> p) // マウスの右ボタンがクリックされるまで W << p; // 左ボタンで指定された点を画面に表示 W.read_mouse(); W.close(); } 図 2.12 はプログラム 2-4-8.c の実行例を示したものです. 同じことが点以外についても可能です. 次のプログラムは, 点, 線分, 円, 多 角形を順に入力していくというものです. 一つの図形の入力を終るには, マウ スの右ボタンをクリックすればいいのです. //program 2-4-9.c: ウィンドウ上に多数の図形を描画 #include <LEDA/window.h> main() { point p; segment s; circle c; polygon pol; window W (400, 400); 2.4. 制御構造 37 図 2.12: プログラム 2-4-8.c の実行 の様子 図 2.13: プログラム 2-4-9.c の実行 の様子 W.display(); } while(W >> p) W while(W >> s) W while(W >> c) W while(W >> pol) W.read_mouse(); W.close(); << p; << s; << c; W << pol; // // // // 点の入力 線分の入力 円の入力 多角形の入力 プログラム 2-4-9.c の実行例については図 2.13 を参照してください. 演習問題 2.4.9 プログラム 2-4-8.c では多数の点をウィンドウ上に表示す るだけでしたが, 連続して入力された点を結んで線分列が描けるようにプロ グラムを拡張しなさい. 最初の点は別として, 2番目の点からは直前に入力 38 第2章 C言語と LEDA された点を記憶しておかないといけないので, 点を表す変数が少なくとも2 つ必要になります. 演習問題 2.4.10 前問では点列に対応する線分列を出力するプログラムを考 えましたが, 点列に対応する多角形を出力するプログラムに拡張しなさい. そ のためには最後に入力された点と最初の点を結んで多角形として閉じる必要 があります. 2.4.3 ループからの脱出 for ループにしても while ループにしてもループの先頭または末尾にしか ループからの脱出口がありませんでした. しかし, 実際に複雑なプログラムを 書いているとループの途中で脱出したい場合があります. このような場合に は break 文を用います. 具体的な例として, データの総和を求めるプログラ ムについて考えてみましょう. データの総和を変数 sum に蓄えることにしま す. データ x が入力されるたびに x の値を sum に足しこんでいけばいいので すから, sum = 0; do{ データ x を入力; x の値を sum に足しこむ; } while(true); のような形でプログラムを書けばいいのですが, 上の while 文の条件式は常に 成り立つものを表していますから, これでは無限ループになってしまいます. そこで, これが最後のデータだという印を入力する必要があります. たとえ ば, 正のデータばかりの総和を求める場合には負の値をデータの終りの印に 用いることができます. つまり, 入力されたデータが負であればループから脱 出して, それまでに求めた変数 sum の値を出力するようにします. x の値が 負ならばループから抜け出したいときには, break 文を使って次のようにプロ グラムを変更します. 2.4. 制御構造 39 sum = 0; do{ データ x を入力; if( x < 0 ) break; x の値を sum に足しこむ; } while(true); break 文が実行されると, その break 文を含む最も内側のループから抜け出る ことになります. この考え方で作ったのが次のプログラムです. //program 2-4-10.c: データの総和を求める #include <LEDA/stream.h> main() { int x, sum; } sum = 0; // 和 sum を 0 に初期化 do{ x = read_int("データを入力してください "); if(x < 0) break; // 負の値が入力されると終了 sum = sum + x; // 入力の値を sum に足しこむ }while(true); cout << "総和 = " << sum; break 文は使いようによっては便利なものですが, プログラムの構造化とい う観点からは望ましいものではありません. 読みやすいプログラムを心がけ るには break 文の使用をできるだけ避けることが必要です. 上のプログラム の場合, 出口の位置をずらすことにより break 文をなくすことができます. た とえば, 次のように変更すればいいのです. //program 2-4-11.c: データの総和を求める #include <LEDA/stream.h> main() 40 { } 第2章 C言語と LEDA int x, sum; sum = 0; x = 0; // 和 sum を 0 に初期化 while(x >= 0){ // x の最初の値 0 はダミー sum = sum + x; // 負の値が入力されたら終了 x = read_int("データを入力してください "); } cout << "総和 = " << sum; 2.4.4 場合分け プログラム 2-4-7.c では, ウィンドウ上に点, 線分, 円, 多角形をこの順に描 画するものですが, 任意の順序で図形を描くにはどうすればいいでしょう. 一 つの方法は, 毎回何を描くかを入力で指定するというものです. たとえば, 次 のようなプログラムが考えられます. //program 2-4-12.c: ウィンドウ上に多数の図形を描画 (2) #include <LEDA/window.h> main() { int shape; point p; segment s; circle c; polygon pol; window W (400, 400); W.display(); do{ cout << "図形を番号で選んでください. "; shape = read_int("0:点, 1:線分, 2:円, 3:多角形, 4:終了 "); if(shape == 0){ // 0 が選ばれたとき W >> p; W << p; } else if(shape == 1){ // 1 が選ばれたとき W >> s; W << s; } else if(shape == 2){ // 2 が選ばれたとき W >> c; W << c; } else if(shape == 3){ // 3 が選ばれたとき W >> pol; W << pol; 2.4. 制御構造 } 41 } } while(0 <= shape && shape <= 3);// 0-3 以外が選ばれたとき終了 W.close(); このように幾つかの場合の中から一つを選ぶという構造はよく見かけます が, このような場合分けに適しているのが次に説明する switch case 構文で す. 上のプログラムをこの構文を用いて書きなおしたのがプログラム 2-4-13.c です. //program 2-4-13.c: ウィンドウ上に多数の図形を描画 (3) #include <LEDA/window.h> main() { int shape; point p; segment s; circle c; polygon pol; window W (400, 400); W.display(); do{ } cout << "図形を番号で選んでください. "; shape = read_int("0:点, 1:線分, 2:円, 3:多角形, 4:終了 "); switch( shape ){ case 0: { W >> p; W << p; break; } case 1: { W >> s; W << s; break; } case 2: { W >> c; W << c; break; } case 3: { W >> pol; W << pol;} } } while(0 <= shape && shape <= 3); W.close(); 上のプログラムのように, switch の後の式の値が一致する case の後の文が 実行されます. 上の例ではどのケースについても break 文がありますが, もし break 文がなければ次の case にも実行が続くことになります. これでもいいのですが, 毎回数字を入力して図形を決めるのは面倒なので, すべての作業がウィンドウ上でできるようにしてみましょう. LEDA ではこ 42 第2章 C言語と LEDA んな作業も簡単です. 上のプログラムでは毎回コマンドに相当する番号を入 力しなければなりませんでしたが, 下のプログラム 2-4-14 では, 幾つかのメ ニューを含むパネルを表示して, マウスで選ばれたメニューを実行するよう にすることができます. shapes.append("文字列"); とすれば, 指定された 文字列をもつメニューが定義されます. プログラムの実行例を示した図 2.14 を参照してください. W.choice_item("文字列", 変数, パネル名); この文を実行すると指定された文字列をラベルとするパネル(パネル名で指 定)がウィンドウ上に表示されます. このパネルのメニューをマウスで選択 すると, メニューの定義順に対応する整数(0 から始まる整数)が指定された 変数に代入されます. //program 2-4-14.c: ウィンドウ上に多数の図形を描画 (4) #include <LEDA/window.h> main() { int shape=0; list <string> shapes; point p; segment s; circle c; polygon pol; window W (400, 400); shapes.append("point"); // パネルのメニューを定義 shapes.append("segment"); shapes.append("circle"); shapes.append("poly"); shapes.append("exit"); W.choice_item("Choose a shape", shape, shapes); W.display(); do{ W.read_mouse(p); W.set_point_buffer(p); switch( shape ){ case 0: { W >> p; W << p; break;} case 1: { W >> s; W << s; break;} 2.4. 制御構造 } 43 case 2: { W >> c; W << c; break;} case 3: { W >> pol; W << pol;} } } while(shape <= 3); W.screenshot("2-4-14.ps"); W.close(); 図 2.14: プログラム 2-4-14.c の実行の様子 このプログラムには幾つか初めての表現が含まれていますが, それらにつ いては LEDA Book または LEDA マニュアルを参照してください. 便利な関 数は W.screenshot() です. 上のプログラムのように, この関数が実行される 時点でのウィンドウの内容がポストスクリプトファイルとして出力されます. ただ, Windows 上ではポストスクリプトファイルに出力することが難しいの で, たとえば, W.screenshot("2-4-14.wmf"); のように, ポストスクリプト以外の書式を指定するべきです. 44 第2章 C言語と LEDA C言語特有の記法 2.5 2.5.1 代入文の値 C言語では他の言語には備わっていない様々な演算子や記法が用意されて います. たとえば, 変数に値を代入するとき, その代入文自身も代入されたの と同じ値をもちます. 先にウィンドウ上に多数の点を描画するのに while(W >> p) W << p; としましたが, このときもクリックで指定された点の座標が変数 p に入ると ともに, W >> p という文の値にもなっていました. 右ボタンをクリックし たときには値 0 が代入されますから, while ループから抜け出たのです. 2.5.2 インクリメント, デクリメント演算子 C言語特有の演算子として, 整数変数の値を 1 増やしたり, 1 減らしたりす るためのインクリメント演算子 ++ とデクリメント演算子 -- があります. た とえば, 整数変数 n の値を 1 だけインクリメントする場合は, ++n; または n++; とします. 変数 n の値が 1 増えることにおいては同じですが, 式 ++n の値はイ ンクリメントされた後の n の値ですが, n++ の場合にはインクリメントされる 前の n の値になります. たとえば, n = 5 のときに, a = n++; を実行すると, n の値を a に代入した後で n をインクリメントしますから, 結果は n = 6; a = 5 となります. しかし, n = 5 のときに, a = ++n; を実行すると, n の値をイン クリメントした後で, その値を a に代入しますから, 結果は n = 6; a = 6 とな ります. 2.5.3 複合代入文 代入文の一般的な形式は, 変数 = 算術式; というものですが, 2.5. C言語特有の記法 45 sum = sum + x; や n = n - 2; のように右辺と左辺に同じ変数がくることが多くあります. このような代入 文を簡単に記述するために, C言語では, sum += x; や n -= 2; のような書き方が許されています. 一般に, 変数 演算子= 算術式; によって, 変数 = 変数 演算子 算術式; を表すことができます. このように, 演算子と等号を組み合わせたものを複合 代入演算子と呼びます. 2.5.4 キャスト 変数には整数を扱う int 型や, 浮動小数点数を扱うための double 型などが あります. これらの型の間の変換を明示的に行うのがキャストと呼ばれる演 算子です. キャスト演算子は, 変数や算術式の前に置いて, 変数や算術式の値 を指定した型に変換するもので, 具体的には変換したい型の名前を括弧で囲 みます. たとえば, 整数変数 x の値を 3 で割った値を double 型に変換したも のが 0:5 より大きいかどうかの条件式は, (double) x / 3 > 0.5 のように表します. もしキャスト演算子がなければ, x=3 の除算を整数どうし の除算として行ってしまうので, 小数点以下を切り捨てて商 x=3 が計算され ますので, 結果が異なってしまいます. 46 第2章 2.5.5 C言語と LEDA 条件演算子 条件式が成り立つかどうかで 2 つの値のうちどちらを選ぶかを決めること もできます. 一般的には, (条件式) ? 式 1: 式 2 の形です. 条件式が真なら式 1 が全体の値となり, そうでなければ式 2 がの 値となります. たとえば, 2 つの変数 a; b の大きい方の値を変数 c に代入する には, c = (a > b) ? a: b; とすればいいのです. これで, 行されることになります. 演習問題 なければ 2.5.6 a > b なら c = a;, そうでなければ c = b; が実 2.5.1 条件演算子を用いて, 変数が 10 で割り切れれば 0, 割り切れ 1 の値を別の変数に代入する文を書きなさい. 定数の定義 プログラムの中では様々な定数を使いますが, 単に数字の羅列よりも意味の ある文字列を用いた方がプログラムが読みやすいものです. たとえば, 1 ポンド は約 453 グラムですから, p ポンドが何グラムかを求めるには g = p * 453; という計算をすればいいのですが, この計算式を見ただけでは何の計算をし ているのか分かりません. しかし, g = p * PoundToGram; と書いてあれば意味は一目瞭然でしょう. このとき, PoundsToGram という 変数を用意して, そこに 453 という値を代入しておくというのも一つの方法 ですが, PoundToGram という文字列を \453" という文字列に置き換えても 同じことです. このような記号の置き換えを行うのが#dene 文です. たとえ ば, この場合には次のようになります. #define PoundToGram 453 2.6. 関数の定義と利用 2.5.7 47 マクロ定義 #dene 文は, 単に文字列を別の文字列に置換するだけでなく, 引数つきの マクロも定義することができます. たとえば, #define max(A, B) (((A) > (B)) ? (A): (B)) と定義すると, A; B がこの文字列置換の引数となります. このマクロを引用 するときは, たとえば次のようにします. z = max(x+1, y+r) * 3; このとき, 上の文は定義文にしたがって, 次のように展開されます. z = (((x+1) > (y+r)) ? (x+1): (y+r)) * 3; この表現には冗長な括弧があるように思われますが, 決して無駄な括弧では ありません. なぜなら, これらの括弧がないと演算子の優先順位などのせいで 場合によっては意図した通りにならないことがあるからです. 2.6 2.6.1 関数の定義と利用 算術関数 C言語では, 三角関数をはじめ, 多くの算術関数が用意されています. ただ, それらの関数を使うときには, プログラムの先頭で #include <math.h> として, その関数の定義を含んだ(数学)ライブラリーをプログラムと連結 させておかなければなりません. 三角関数としては, sin(); cos(); tan() のほか, tan 1 () を表す atan() などが 使えます. ただし, 引数として与える角度はラディアン単位の double 型数値 でなければならず, 関数の値も double 型です. たとえば, sin 45Æ の値は, sin(45.0 * 3.141592 / 180.0) 48 第2章 C言語と LEDA のように, ラディアン単位に変換した上で計算します. 三角関数のほかにも算術関数が用意されています. それらを表で示したの が表 2.2 です. いずれも, 引数, 関数値ともに double 型であることに注意して ください. 関数名 平方根 べき乗 自然対数 常用対数 指数関数 px 数学的表現 sqrt(x) pow(x, y) log(x) log10(x) exp(x) x y log x log10 x e e x 関数の型 引数の型 double double double double double double double double double double 表 2.2: 数学関数 下のプログラムは三角関数を用いてウィンドウ上に星の形を描くものです. 星は 5 つの頂点からなりますから, 星の中心から見たとき, それぞれの頂点は 72Æ の間隔で同じ円周上にあります. その円の半径を r とし, 72Æ をラディア ン単位に変換したものを t としますと, 各頂点の座標は, (r cos(k t); r sin(k t)); k = 0; 1; : : : ; 4 と表すことができますから, 画面の中心 (50; 50) の辺りに半径 20 の円に内接 する星を描くプログラムは次のようになります(図 2.15 参照). プログラム は実行結果は図 2.16 に示してあります. //program 2-6-1.c: ウィンドウ上に星の形を描く #include <LEDA/window.h> main() { double x0, y0, r, t; point p1, p2, p3, p4, p5; 2.6. 関数の定義と利用 49 頂点5 頂点4 72o 頂点 1 頂点3 頂点2 図 2.15: 星の形 window W (400, 400); W.display(); x0 = 50.0; y0 = 50.0; r = 20.0; t = 72.0 * 3.1415927 / 180.0; p1 p2 p3 p4 p5 = = = = = point(r point(r point(r point(r point(r * * * * * cos(t cos(t cos(t cos(t cos(t W.draw_segment(p1, W.draw_segment(p4, W.draw_segment(p2, W.draw_segment(p5, W.draw_segment(p3, } * * * * * 0) 1) 2) 3) 4) + + + + + x0, x0, x0, x0, x0, r r r r r * * * * * sin(t sin(t sin(t sin(t sin(t * * * * * 0) 1) 2) 3) 4) + + + + + y0); y0); y0); y0); y0); p4); p2); p5); p3); p1); W.read_mouse(); W.close(); 演習問題 2.6.1 星を青の線で描画できるようにプログラム 2-6-1.c を変更 50 第2章 C言語と LEDA 図 2.16: プログラム 2-6-1.c の出力結果 しなさい. 2.6.2 ユーザ定義関数 三角関数のようにシステム側で用意されている関数の他に, プログラマ自身 が独自の関数をプログラムの中で定義することができます. 例として, 上で考 えた星を描くプログラムについて考えてみましょう. 先のプログラムではウィ ンドウに星を一つだけ描きました. 異なる場所に 2 個の星を描く場合, プロ グラム 2-6-2.c のように, 単純に同じことを繰り返すこともできます(図 2.17 参照). //program 2-6-2.c: ウィンドウ上に星の形を描く (2) #include <LEDA/window.h> main() { double x0, y0, r, t; 2.6. 関数の定義と利用 51 point p1, p2, p3, p4, p5; window W (400, 400); W.display(); x0 = 50.0; y0 = 50.0; r = 20.0; t = 72.0 * 3.1415927 / 180.0; p1 p2 p3 p4 p5 = = = = = point(r point(r point(r point(r point(r * * * * * cos(t cos(t cos(t cos(t cos(t W.draw_segment(p1, W.draw_segment(p4, W.draw_segment(p2, W.draw_segment(p5, W.draw_segment(p3, x0 p1 p2 p3 p4 p5 = = = = = = 0) 1) 2) 3) 4) + + + + + x0, x0, x0, x0, x0, r r r r r * * * * * sin(t sin(t sin(t sin(t sin(t * * * * * 0) 1) 2) 3) 4) + + + + + y0); y0); y0); y0); y0); + + + + + x0, x0, x0, x0, x0, r r r r r * * * * * sin(t sin(t sin(t sin(t sin(t * * * * * 0) 1) 2) 3) 4) + + + + + y0); y0); y0); y0); y0); p4); p2); p5); p3); p1); 75.0; y0 = 75.0; point(r * cos(t * point(r * cos(t * point(r * cos(t * point(r * cos(t * point(r * cos(t * W.draw_segment(p1, W.draw_segment(p4, W.draw_segment(p2, W.draw_segment(p5, W.draw_segment(p3, } * * * * * 0) 1) 2) 3) 4) p4); p2); p5); p3); p1); W.read_mouse(); W.close(); では, 星を 5 個描く場合はどうでしょう. 同じことを 5 回繰り返しますか? もっと賢いやりかたがあるはずです. 52 第2章 C言語と LEDA 図 2.17: プログラム 2-6-2.c の出力結果 このような場合には, 一つのまとまった処理を関数の形にまとめることが できます. 下のプログラムでは, 5 個の点を定義して星形を描く処理を一つの 関数 draw star() として定義しています. このとき, 円の中心の座標と円の半 径をパラメータとして受け取ります. このようなパラメータのことを関数の 引数と言い, その型とともに宣言しておかなければなりません. 関数自身の型 も宣言しなければなりません. この場合には, 三角関数のように計算結果を答 えとして返すものではなく, 幾つかの処理をまとめただけのものですから, 関 数値を返さないという意味で void という型を割り当てています. このように定義した関数を呼び出すときには, その型を予め宣言しておか なければなりません. main() 関数の前に void draw_star(double, double, double); と宣言するか, あるいは, 下のプログラムのように, 呼び出す関数の前に定義 するかどちらかです. 同じように, main() も関数ですが, C言語の標準規格である ANSI 規格で は main 関数は int 型としています. そして, main 関数の最後の行に 2.6. 関数の定義と利用 53 return 0; 付けます. これはプログラムが正常に終了したことを表します. これ以降 main 関数をそのように記述します. もう一点, 今までと異なるのは, ウィンドウ W が main() 関数の外で宣言さ れていることです. 今までのように main() 関数の中だけで宣言していると, その外では参照できなくなってしまうために, どこからでも参照できるよう に外部で宣言を行っています. //program 2-6-3.c: ウィンドウ上に星の形を描く (3) #include <LEDA/window.h> window W (400, 400); void draw_star(double x0, double y0, double r) { double t = 72.0 * 3.1415927 / 180.0; point p1, p2, p3, p4, p5; } p1 = point(r * cos(t * 0) p2 = point(r * cos(t * 1) p3 = point(r * cos(t * 2) p4 = point(r * cos(t * 3) p5 = point(r * cos(t * 4) W.draw_segment(p1, p4); W.draw_segment(p4, p2); W.draw_segment(p2, p5); W.draw_segment(p5, p3); W.draw_segment(p3, p1); + + + + + x0, x0, x0, x0, x0, int main() { W.display(); draw_star(50.0, 50.0, 20.0); draw_star(75.0, 75.0, 20.0); W.read_mouse(); W.close(); r r r r r * * * * * sin(t sin(t sin(t sin(t sin(t * * * * * 0) 1) 2) 3) 4) + + + + + y0); y0); y0); y0); y0); 54 } 第2章 C言語と LEDA return 0; この例でも分かるように, 一つの関数の中で宣言された変数は, その関数の 中だけで有効です. 他の関数の中から参照できるようにするためには, 引数と して関係を明示的に示す必要があります. 変数の宣言を関数の外で行うこと もできます. その場合には, 同じファイル内のすべての関数からその変数にア クセス可能です. つまり, その変数の値を利用したり, 変数の値を変更したり することができます. このように, 関数の外部で宣言された変数のことを外部 変数と言います. 外部変数は便利ですが, 不必要に多くの外部変数を用いると 他の人が読むときに非常に読みにくいものになってしまう危険性があります. 関数を用いるとプログラムを読みやすいものにすることにおいても効果が あります. たとえば, 上の星を描くプログラムを拡張して, 星を直線上で回転 させながら移動させるプログラムを作ってみましょう. 星を a(ラディアン) だけ回転させるには, 頂点の座標を (r cos(k t + a); r sin(k t + a)); k = 0; 1; : : : ; 4 とすればいいのです. 後は, 中心の x 座標を順に増やせばいいのです. また, 星が描かれる様子を見やすくするために, 星を描くたびに 0:5 秒待つという命 令 leda wait(0.5) を最後に挿入しています. プログラムの実行例については 図 2.18 を参照してください. //program 2-6-4.c: ウィンドウ上に星の形を描く (4) #include <LEDA/window.h> window W (400, 400); void draw_star(double x0, double y0, double r, double a) { double t; point p1, p2, p3, p4, p5; t = 72.0 * 3.1415927 / 180.0; p1 = point(r * cos(t * 0 + a) + x0, r * sin(t * 0 + a) + y0); p2 = point(r * cos(t * 1 + a) + x0, r * sin(t * 1 + a) + y0); 2.6. 関数の定義と利用 } 55 p3 = point(r * cos(t * 2 + a) + x0, r * sin(t * 2 + a) + y0); p4 = point(r * cos(t * 3 + a) + x0, r * sin(t * 3 + a) + y0); p5 = point(r * cos(t * 4 + a) + x0, r * sin(t * 4 + a) + y0); W.draw_segment(p1, p4); W.draw_segment(p4, p2); W.draw_segment(p2, p5); W.draw_segment(p5, p3); W.draw_segment(p3, p1); leda_wait(0.5); int main() { double x, a; } W.display(); a = 0.0; for(x=20.0; x < 80.0; x=x+5){ draw_star(x, 50.0, 20.0, a); a += 10 * 3.1415927 / 180.0; } W.read_mouse(); W.close(); return 0; // 10 度ずつ増やす 演習問題 2.6.2 任意の角度 t を入力して, 先に描いた星を角度 t だけ反時計 回りに 回転した図形を描くプログラムを作りなさい. 演習問題 2.6.3 ウィンドウ上で 3 点を指定して, その外接円を表示するプロ グラムを作りなさい. 演習問題 2.6.4 ウィンドウ上で 3 点を指定して, その内接円を表示するプロ グラムを作りなさい. 演習問題 2.6.5 整数 n と内接円の半径 r を数値で入力して, 正 n 角形を描く プログラムを作りなさい. 56 第2章 C言語と LEDA 図 2.18: プログラム 2-6-4.c の出力結果 2.6.3 乱数の生成 C言語にも幾つかの乱数生成関数が用意されていますが, LEDA には信頼 度が高く, しかも使いやすい乱数生成のメカニズムが備わっています. たとえ ば, 計算機の中でサイコロを実現してみましょう. 1 から 6 までの整数をラン ダムに生成したいのですが, LEDA では, random_source S(1, 6); と宣言しておけば, S() とするだけで目的を達成することができます. たとえ ば, 1 から 6 までの乱数を 6 回生成するプログラムが次のようになります. //program 2-6-5.c: 1 から 6 までの乱数の生成 #include <LEDA/random_source.h> int main() { int i; random_source S(1, 6); // 1から6までの乱数生成関数 2.6. 関数の定義と利用 } for(i=0; i<8; i++) cout << S() << " "; return 0; 57 // 生成された乱数の値を出力 プログラム 2-6-5.c の実行例 > 2-6-5 3 5 6 1 2 3 4 2 毎回違った区間の乱数を生成することもできます. たとえば, 次のプログラ ムでは, [1; 6] の区間から始めて, [2; 7], [3; 8], : : :, [6; 11] の区間の乱数を生成 します. //program 2-6-6.c: 異なる区間の乱数の生成 #include <LEDA/random_source.h> int main() { int i; random_source S; } for(i=0; i<8; i++) cout << S(i+1, i+6) << " "; return 0; // 範囲指定のない乱数生成関数 // i+1 から i+6 までの乱数を生成 プログラム 2-6-6.c の実行例 > 2-6-6 3 5 4 9 7 11 12 10 上のプログラムに示すように, random souce S; と宣言しておいて, 乱数を 使うときに S(1, 6) のように () 内で区間を指定すればいいのです. 乱数とは毎回違った数が生成されるからこそ乱数なのですが, ときには同 じ乱数列を生成したい場合があります. そのような場合には, 乱数の種と呼ば れる整数を指定してから乱数の生成に移ればいいのです. 具体的には, S.set_seed(100); 58 第2章 C言語と LEDA のようにして宣言します. プログラム 2-6-6.c にこの宣言を加えてみると, 同 じ乱数列が生成されることを確かめることができます. //program 2-6-7.c: 異なる区間の乱数の生成 (2) #include <LEDA/random_source.h> int main() { int i; random_source S; } S.set_seed(100); for(i=0; i<6; i++) cout << S(i+1, i+6) << " "; return 0; // 乱数の種の設定 プログラム 2-6-7.c の実行例 > 6 > 6 2-6-7 3 5 4 10 10 2-6-7 3 5 4 10 10 整数型の乱数だけではなく, double 型の乱数も簡単に生成できます. double 型の変数とするとき, xを S >> x; とするだけで, x に 0 x < 1 の範囲の乱数が代入されます. 実際に次のプロ グラムを試してみれば分かることです. //program 2-6-8.c: double 型の乱数の生成 #include <LEDA/random_source.h> int main() { int i; double x; random_source S; 2.7. 配列の利用 } for(i=0; i<6; i++){ S >> x; cout << x << endl; } return 0; プログラム 2-6-8.c の実行例 59 > 2-6-8 0.522882 0.861528 0.580051 0.494318 0.0231972 0.976849 演習問題 2.6.6 0 から 9 までの整数乱数を 90 回生成しながら, 毎回それまで の平均値を出力するプログラムを作りなさい. 毎回の乱数値を棒グラフの形 で, 平均値は別の色の直線で結ぶようにして, 平均値が収束する様子が分かる ようにしなさい. 2.7 2.7.1 配列の利用 C言語の配列 多数の整数データを入力して, それらの最大値を求めるプログラムについ て考えてみましょう. 毎回, データを入力して, それが今までの最大値よりも 大きいなら, 最大値をその入力データで置きかえる, という操作を繰り返せば いいのです. 最初にデータの個数を入力することにすると, for ループを使っ て簡単にプログラムができます. //program 2-7-1.c: 入力データの最大値を求める #include <LEDA/stream.h> int main() 60 { } 第2章 C言語と LEDA int i, n, x, max; n = read_int("データの個数を入力してください "); cout << "データを順に入力してください "; cin >> max; // 最初のデータを max に蓄える for(i=2; i<=n; i++){ // 2 番目以降のデータを順次入力 cin >> x; // 入力データを x とする if(x > max) // x と今までの最大値を比較 max = x; // 入力の方が大きいなら, max を更新 } cout << "最大値は " << max << endl; return 0; プログラム 2-7-1.c の実行例 > 2-7-1 データの個数を入力してください 5 データを順に入力してください 12 14 21 32 19 最大値は 32 上のプログラムで, 1 回目の入力だけを特別に扱っているのは, 最大値の候 補を最初の入力データとして 2 番目以降の入力データと比較するためです. それでは, 最大の数だけでなく, 2 番目に大きい数(厳密には大きいものか ら順に並べたとき 2 番目に位置する数)を求めるにはどうすればいいでしょ う. 最も直接的な方法として, まず最大の数を求め, 次にもう一度データを入 力して, 先ほど求めた最大値とは異なる数の中で最大の数を求めるという方 法が考えられます. しかし, この方法には 2 つの重大な欠点があります. 一つ は, 言うまでもなくデータを 2 度入力しなければならない面倒さです. もう一 つの欠点は, 最大の数が複数個あった場合に間違った結果を与えてしまうこ とです. この後者の欠点は, 最大値に等しいものが複数個あるかどうかを調べ ることにすれば解消できないことはありません. データを一度だけ入力して 2 番目に大きい数を求めるには, 入力されたデー タをメモリーに蓄えておけばいいのです. C言語では, メモリー内に指定され た個数分だけ連続した場所を確保して, それらをまとめて同じ名前で参照す 2.7. 配列の利用 61 ることができます. たとえば, data という名前をつけることにして, 整数型で 100 個分の場所を確保するには, int data[100]; という宣言文を置けばいいのです. 先頭の要素は data[0] として参照します. このように宣言すれば, 一般に配 列の k 番目の値を data[k] としてアクセスすることができます(図 2.19 参照). data 5800 0 5800番地 1 5801番地 data[k] k 5800+k番地 図 2.19: メモリ上での配列 data の割り付け このように, 一つの名前で表される記憶場所の集まりを配列 (array) と言い, それぞれの場所のことを配列要素と言います. 配列にデータを入力するには, たとえば for ループを用いて配列要素に順次 アクセスしてデータを取りこむようにすれば, n 個のデータを配列に読み込む ことができます. n = read_int("データの個数を入力してください "); cout << "データを順に入力してください "; for(i=0; i<n; i++) cin >> data[i]; 62 第2章 C言語と LEDA さて, データを入力した後, 今度は最大値ではなく, 最大の数が配列の中で 何番目にあるかを調べます. k 番目の配列要素が最大値であるなら, 次に前半 部分の data[0] から data[k 1] までの最大値と, 後半部分の data[k + 1] から data[n 1] までの最大値のどちらか大きい方を求めれば, それが 2 番目に大 きい数であるといえます. このままでプログラムを作ると, 最大値の場所, すなわち k の値によって 3 通りに場合分けする必要があります. 0 番目から k 1 番目の前半部分が空で あったり, k + 1 番目から n 1 番目の後半部分が空であったりするからです. このような面倒を省くには, 配列の中央にあるかもしれない最大の要素を配 列の右端 data[n 1](左端の data[0] でもよい)に移動させればいいのです. 単に移動させると右端の配列要素 data[n 1] に蓄えられていた値がなくなっ てしまうので, 最大値をとる配列要素 data[k ] と右端の配列要素 data[n 1] の 内容を交換します. 値の交換には LEDA で用意されている関数 leda swap() が便利です. この交換により, 今求めた最大値以外のデータが data[0] から data[n 2] までに並ぶことになりますから, その中の最大値を求めれば所望 の 2 番目に大きい要素を求めたことになります(図 2.20 参照). data[0] data[0] data[1] data[1] data[k] この部分の 最大値が 2番目に 大きい値 最大値 data[n-2] data[n-1] 交換 data[n-1] 最大値 図 2.20: 2番目に大きい要素の発見 プログラムは以下のようになります. 2.7. 配列の利用 63 //program 2-7-2.c: 入力データの 2 番目に大きい値を求める #include <LEDA/stream.h> int main() { int i, n, max_ind, data[100]; n = read_int("データの個数を入力してください "); cout << "データを順に入力してください "; for(i=0; i<n; i++) cin >> data[i]; max_ind = 0; for(i=1; i<n; i++) if(data[i] > data[max_ind]) max_ind = i; leda_swap(data[max_ind], data[n-1]); max_ind = 0; for(i=1; i<n-1; i++) if(data[i] > data[max_ind]) max_ind = i; } cout << "2 番目に大きい値は " << data[max_ind] << endl; return 0; 2 番目に大きい要素を見つけることができるなら, 3 番目に大きい要素も同 じ方法で見つけることができるでしょう. 実際, 2 番目に大きい要素を配列の n 2 番目と交換して, 再度 0 番目から n 3 番目までの最大値を求めればい いのです. したがって, プログラムは program 2-7-3.c のようになります. //program 2-7-3.c: 入力データの 3 番目に大きい値を求める #include <LEDA/stream.h> int main() { int i, n, max_ind, data[100]; 64 第2章 C言語と LEDA n = read_int("データの個数を入力してください "); cout << "データを順に入力してください "; for(i=0; i<n; i++) cin >> data[i]; max_ind = 0; for(i=1; i<n; i++) if(data[i] > data[max_ind]) max_ind = i; leda_swap(data[max_ind], data[n-1]); max_ind = 0; for(i=1; i<n-1; i++) if(data[i] > data[max_ind]) max_ind = i; } leda_swap(data[max_ind], data[n-2]); max_ind = 0; for(i=1; i<n-2; i++) if(data[i] > data[max_ind]) max_ind = i; cout << "3 番目に大きい値は " << data[max_ind] << endl; return 0; 同じ要領で 4 番目に大きい要素, 5 番目に大きい要素も求めることができ ますが, この方法だとどんどんプログラムが長くなっていきます. しかし, 配 列内の探索範囲が, 最初は [0; n 1] だったのが, 2 番目では [0; n 2] となり, さらに 3 番目では [0; n 3] になっているところが違うだけであることに注 意しましょう. つまり, 区間の終りを n 1 n 2 n 3 とと順次変 えているだけで, ループの中ではいつも区間内での最大値の位置を求め, 最 後に最大値 data[max ind] と区間の最後の要素を交換するという同じ処理を 繰り返しているだけなのです. そこで, 別に m という変数を導入して, m を n 1 n 2 n 3 と変化させて同じ処理を繰り返すようにすると, ! ! ! for(m=n-1; m>=n-3; m--){ ! 2.7. 配列の利用 } 65 max_ind = 0; for(i=1; i<=m; i++) if(data[i] > data[max_ind]) max_ind = i; leda_swap(data[max_ind], data[m]); のように, 今まで 3 つの部分に分かれていた処理を1つにまとめることがで きます. この考え方で作ったのが次のプログラムです. //program 2-7-4.c: 入力データの 3 番目に大きい値を求める (2) #include <LEDA/stream.h> int main() { int i, n, m, max_ind, data[100]; n = read_int("データの個数を入力してください "); cout << "データを順に入力してください "; for(i=0; i<n; i++) cin >> data[i]; for(m=n-1; m>=n-3; m--){ max_ind = 0; for(i=1; i<=m; i++) if(data[i] > data[max_ind]) max_ind = i; leda_swap(data[max_ind], data[m]); } } cout << "3 番目に大きい値は " << data[n-3] << endl; return 0; 上のプログラムでは m の値を n 1 から n 3 まで変化させて, 3 番目に大 きい要素を求めましたが, m の値を n 1 から 1 まで変化させてみるとどう でしょう. 毎回, 残りの要素の中で最大のものを求めて, それを配列の右端と 交換しているわけですから, m = 1 の場合を処理した後では, 配列の要素はう 66 第2章 C言語と LEDA まく昇順に並んでいることになります. このようにデータを大小順に並び替 える処理をデータのソートと呼んでいます. //program 2-7-5.c: 入力データを昇順にソートする #include <LEDA/stream.h> int main() { int i, n, m, max_ind, data[100]; n = read_int("データの個数を入力してください "); cout << "データを順に入力してください "; for(i=0; i<n; i++) cin >> data[i]; for(m=n-1; m>=1; m--){ max_ind = 0; for(i=1; i<=m; i++) if(data[i] > data[max_ind]) max_ind = i; leda_swap(data[max_ind], data[m]); } } cout << "昇順にソートされたデータ" << endl; for(i=0; i<n; i++) cout << data[i] << " "; cout << endl; return 0; 配列を用いると度数分布の計算が非常に簡単になります. たとえば, LEDA の乱数がどの程度ランダムかを調べるために, 0 から 9 までの乱数を 10000 回 生成してみて, それぞれの数が何回生成されたかを求めるプログラムを作っ てみましょう. まず, サイズ 10 の整数配列 count[ ] を宣言し, 各配列要素の値を 0 に初期化 しておきます. その後で, 指定の乱数を生成し, 乱数 S () のカウント count[S()] を 1 だけ増やすのです. 2.7. 配列の利用 67 //program 2-7-6.c: 乱数の度数分布 #include <LEDA/random_source.h> int main() { int i, count[10]; random_source S(0, 9); for(i=0; i<10; i++) count[i] = 0; for(i=0; i<10000; i++) count[S()]++; } // 0 - 9 の整数乱数生成関数 // 度数を 0 に初期化 // 乱数 S() の度数を1増やす cout << "度数分布" << endl; cout << "乱数: 度数" << endl; for(i=0; i<10; i++) cout << " " << i << ": " << count[i] << endl; return 0; プログラム 2-7-6.c の実行例 > 2-7-6 度数分布 乱数: 度数 Æ 0: 1: 2: 3: 4: 5: 6: 7: 8: 9: 1023 939 1004 1026 998 964 1008 1010 1042 986 演習問題 2.7.1 1 から 1000 までの整数乱数を 300 回発生させたとき, 重複が あったかどうかを答えるプログラムを作りなさい. 68 第2章 C言語と LEDA 演習問題 2.7.2 年月日のデータを入力すると, その日が元旦から数えて 何日 目にあたるかを計算するプログラムを作れ. 演習問題 2.7.3 整数値 n を入力して, 1=n の値を(筆算の要領で)正確に計 算するプログラムを作りなさい. 1=n が有限小数のときは, そのすべての桁を 表示し, 循環小数になるときは, 循環の開始の直前に@記号を入れて, 循環部 分を 1 回だけ表示するようにしなさい. 2.7.2 配列の初期化 プログラム 2-7-6.c では, for ループを用いて最初に配列要素をすべて 0 に初 期化しました. しかし, 配列は宣言のときに同時に値を初期化することもでき ます. 具体的には, int count[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; のように, 配列要素の初期値を順に並べればいいのです. また, このように値 を 10 個並べておくと配列要素が 10 個あることが分かりますから, 配列のサ イズを省略して, int count[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; と宣言することもできます. 配列の初期化を用いて先のプログラムを書きな おすと, 次のようにすっきりした形になります. //program 2-7-7.c: 乱数の度数分布 #include <LEDA/random_source.h> int main() { int i, count[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; random_source S(0, 9); for(i=0; i<10000; i++) count[S()]++; 2.8. 文字列 69 cout << "度数分布" << endl; cout << "乱数: 度数" << endl; for(i=0; i<10; i++) cout << " " << i << ": " << count[i] << endl; return 0; } 2.7.3 多次元配列 C言語では 5 次元程度の配列まで扱えることが多いようですが, 実際には 3 次元以上の配列が使われることはほとんどないようです. よく使われるのは 2 次元配列です. 2 次元配列は, たとえば, int array[4][3]; のように宣言します. ここで最初の 4 は行数を表し, 次の 3 は列数を表して います. このように宣言すると, 行については, 第 0 行から第 3 行までの 4 行, 列に関しては第 0 列から第 2 列までの 3 からなる行列が生成されます. i 行 j 列の配列要素は array[i][j ] としてアクセスすることができます. 2.8 文字列 ここまでは数値データのみを扱ってきましたが, もちろん文字データも取 り扱うことができます. (1 個の)文字を値とする変数は, char c; のように, char(キャラクター)という型(文字型)で宣言されます. C言語 では, 複数の文字からなる文字列をキャラクター型の配列を用いて実現してい ますが, LEDA では簡便に文字列を扱えるように, キャラクターと同じように string s; として文字列を値とする変数 s を宣言できるようになっています. 70 第2章 C言語と LEDA LEDA では文字列を数値と同じように扱うことができます. 文字列の入力 と出力は cin と cout を用いて同様にできますし, 2 つの文字列の比較も可能で す. 数値の場合には大小比較を行いましたが, 文字列の場合には辞書式順序で の比較を行います. 次のプログラムは 2 つの文字列 s1 と s2 を入力して, それ らを辞書式順序で比較した結果を出力するものです. //program 2-8-1.c: 文字列の比較 #include <LEDA/string.h> int main() { string s1, s2; } cin >> s1 >> s2 if(s1 < s2) cout << s1 << " < " else if(s1 == s2) cout << s1 << " = " else cout << s1 << " > " return 0; プログラム 2-8-1.c の実行例 > 2-8-1 asano obokata asano < obokata > 2-8-1 asano asano asano = asano // 文字列変数 (LEDA 特有)の宣言 // 2つの文字列を入力:空白で区切る // 辞書式順序での比較 << s2 << endl; << s2 << endl; << s2 << endl; 文字列の比較は辞書式順序で行うと言いましたが, 正確には少し違います. たとえば, 辞書式順序では大文字と小文字の区別については曖昧ですが, プロ グラムの上では明確な区別があります. それを説明するために文字が計算機 内部でどのように表現されているかを説明しましょう. 整数が 4 バイトまたは 2 バイトで表現されるのに対して, 英字や数字など は 1 バイトで, 漢字などは 2 バイトで表現されます. 計算機内部では, 英文字 2.8. 文字列 71 などはアスキー符号で表現されるます. たとえば, 文字'A' のアスキー符号は 0100 0001 です. これを 8 ビットの 2 進数と見なすと, 10 進数表現では 65 と なります. 英小文字は別の符号をもっています. たとえば, 'a' は 97 です. 数 字も連続した符号をもっています. 数字の 0 は 48 という符号をもち, 数字の 9 の符号は 57 です. その他, キーボードのほとんどすべてのキーについてアス キー符号が定まっています. たとえば, 空白は 32, 改行は 13, Backspace は 8 などです. 変わったところでは, カーソル移動のためのキーがありますが, こ れにもアスキー符号が割り当てられています. すべての文字には整数の符号が割り当てられていますから, この符号の値 を用いて文字を整数と同じように比較することができるのです. ただし, 上で 述べたように, 大文字の符号は 65 から 90 までですが小文字の符号は 97 から 始まりますから, 大文字と小文字を比較すると, どんな文字でも大文字の方が 値が小さくなります. したがって, たとえば, 'Z' < 'a' ということになります. 2 つの文字列を連接することも簡単です. 単に 2 つの文字列変数を \+" で つなげばいいのです. たとえば, 変数 a と b に \Tetsuo" と \Asano" という文 字列が入っているとき, string c = a + " " + b; とすると, 変数 c には \Tetsuo Asano" という文字列が代入されます. 文字列変数の初期化も簡単です. string s("Tetsuo Asano"); と宣言すれば, 文字列変数 s に上の文字列を代入した状態でプログラムを始 めることができます. さらに, プログラムの途中で文字列変数にある文字列を 代入することもできます. s = "Koji Obokata"; とすればいいのです. 次のプログラムを参照してください. //program 2-8-2.c: 文字列の初期化と代入 #include <LEDA/string.h> 72 第2章 int main() { string s1("Tetsuo Asano"), s2; s2 = "Koji Obokata"; cout << s1 << ", " << s2 << endl; cout << s1 + ", " + s2 << endl; return 0; } C言語と LEDA // +は文字列の連接を表す プログラム 2-8-2.c の実行例 > 2-8-2 Tetsuo Asano, Koji Obokata Tetsuo Asano, Koji Obokata 数値を値とする配列と同様に文字列を値とする配列も使えます. 例として, 多数の単語を入力して, それぞれの単語が何度入力されたかを求めるプログ ラムについて考えましょう. まず, 単語の数を管理するための変数 n が必要です. その初期値はもちろん 0 です. その他に, 単語を蓄えるための文字列型配列と, その出現回数を蓄え るための整数型配列が必要です. さて, 文字列が入力されると, まずこの配列 に既に登録されたものかどうかを調べます. 初めての単語なら, その単語を n 番目の単語として文字列配列に登録すると同時にその度数を 1 とし, n の値を 1 増やします. 以前に登録した単語と一致するなら, すなわち, 入力の文字列 s が配列に蓄えられた文字列 words[i] と一致するなら, その単語の度数 count[i] を 1 増やすだけです. 入力の終りを示すコントロール文字 \^z"(UNIX では ^D) が入力されれば, 単語と度数の対を出力して終りです. 文字列に引き続い てコントロール文字を入力してしまうと, 直前の文字列の入力もキャンセル されてしまうので, 空白や改行をしてからコントロール文字を入力するよう にしましょう. 上の考え方でプログラムを作ってみると次のようになります. //program 2-8-3.c: 入力文字列のカウント #include <LEDA/string.h> 2.8. 文字列 73 #define MAXSIZE 100 int main() { string words[MAXSIZE]; int count[MAXSIZE]; int n=0, i; string s; } while(cin >> s){ // ^z が入力されるとループを脱出 i=0; while(s != words[i] && i<n) i++; if(i < n) count[i]++; // i<n で終ったのなら, 見つかった else { // そうでなければ, 初めての単語 words[n] = s; count[n] = 1; // 配列に登録 n++; } } cout << "入力文字列とその度数 " << endl; for(i=0; i<n; i++) cout << i <<": "<< words[i] <<" -> "<< count[i] << endl; return 0; プログラム 2-8-3.c の実行例 > 2-8-3 koji kurt koji tetsuo stefan tetsuo ^z (実際には何も表示されない 入力文字列とその度数 0: 1: 2: 3: Æ koji -> 2 kurt -> 1 tetsuo -> 2 stefan -> 1 74 2.9 第2章 C言語と LEDA ファイル入出力 今まで, 入力といえばキーボードからの入力を, 出力はCRT画面上へのも のを暗黙のうちに仮定していましたが, 多くの実用的なプログラムではファ イルからデータを入力し, 結果もファイルに出力することの方が一般的です. ファイルを扱うためにはファイル名を指定して, これからそのファイルにア クセスすることを宣言しなければなりません. このとき, ファイルからデータ を読み込む場合と, ファイルにデータを書き込む場合とで宣言のしかたが違 います. \test.dat" という名前のファイルからデータを読み込もうとするとき には, ifstream fin ("test.dat"); のように宣言します. ここで, \n" はプログラム内でこのファイルを参照す るときの名前で, 変数名のようなものです. 一方, データを書き込むために \test.out" というファイルを \fout" という名前で宣言するときには, ofstream fout ("test.out"); と宣言します. このように宣言すれば, ファイル n からデータを読み込むには, cin の代わ りにファイル名 n を用いればよく, 同様にファイル fout にデータを出力する には, cout の代わりにファイル名 fout を用いればいいのです. キーボードからデータを入力する場合には何らかの値が入力されるまで待 ちますから, 何も入力がないということはありませんでしたが, ファイルから データを入力する場合には, ファイルの末尾に到達してしまえば何も入力さ れないことになります. そのまま入力待ちの状態に入ったのでは永久ループ ですから, 別の仕掛けが必要になります. このような場合を識別するには入力 文の値を利用します. ファイル n からデータ x を入力するとき, fin >> x; としますが, ファイルの末尾に到達してデータがなくなったときには, この文 の値が \偽" の値を取ります. これをループ条件にすると, データがなくなれ 2.9. ファイル入出力 75 ばループから抜けることができます. この機能を使うと, ファイル \2-8-1.dat" に蓄えられた整数データの総和を求めてファイル \2-8-1.out" に出力するプロ グラムは次のようになります. //program 2-9-1.c: ファイル入出力 #include <LEDA/stream.h> int main() { int x, sum = 0; ifstream fin ("2-9-1.dat"); // 入力用のファイル ofstream fout ("2-9-1.out"); // 出力用のファイル } while(fin >> x) sum += x; fout << sum; return 0; // ファイル fin の終りに達するまで // 入力データを足しこむ // 出力用のファイルに和を出力 プログラム 2-9-1.c の実行例 > cat 2-9-1.dat // ファイル 2-9-1.dat の内容 1 2 3 4 5 6 7 8 9 10 > 2-9-1 // プログラム 2-9-1 の実行 > cat 2-9-1.out // ファイル 2-9-1.out の内容 55 上ではファイル名をプログラムの中で明示的に指定しましたが, 実行時に ファイル名を入力で指定するようにすることもできます. 下のプログラムは, 文字列を入力する関数 read string() を用いて書きなおしたものです. //program 2-9-2.c: ファイル入出力 #include <LEDA/stream.h> int main() // ファイルの名前を入力で指定する場合 { int x, sum = 0; string fname_in, fname_out; fname_in = read_string("入力データファイルは? "); 76 第2章 C言語と LEDA ifstream fin (fname_in); fname_out = read_string("出力データファイルは? "); ofstream fout (fname_out); } while(fin >> x) sum += x; fout << sum; return 0; 演習問題 2.9.1 上のプログラムでは入力データの入ったファイルの名前を間 違えて入力してしまうとプログラムは異常終了してしまいます. 指定された 名前のファイルが存在しなかった場合にはファイル名を再入力できるように プログラムを修正しなさい. 77 第 3 章 LEDA によるプログラミング 3.1 LEDA のデータタイプ LEDA にはC言語で用意されているデータタイプ (int, long, oat, double, char) 以外に様々なデータタイプが用意されています. ここでは, それらにつ いて説明しましょう. 3.1.1 integer タイプ 整数を扱うデータタイプとしては, int 型(16 ビットまたは 32 ビット)と 32 ビット長の long 型がありますが, いずれにしても表現可能な最大の整数が 決まっています. したがって, 非常に大きな整数を扱わないといけない場合 には, 一つの整数を一つの配列を用いて管理しなければならず, 簡単な計算で あってもプログラムは複雑なものになってしまいます. LEDA では任意の長 さの整数を扱うことができる integer 型が使えます. しかも, 四則演算も普通 と同じように適用することができます. たとえば, n = 2 から始めて n = n2 の計算を繰り返すと, 急速に大きな数になりますが, integer 型の変数を用い ると問題ありません. 下のプログラムでは同じ計算を int 型, long 型, integer 型の 3 通りで計算しながら, 途中結果を出力しています. //program 3-1-1.c: integer 型を用いた多倍長計算 #include <LEDA/integer.h> int main() { int n1, i; long n2; // n1 は int 型 // n2 は long 型 78 第3章 integer n3; } // n3 は integer 型 n3 = n2 = n1 = 2; for(i=1; i<=6; i++){ n1 = n1 * n1; n2 = n2 * n2; n3 = n3 * n3; cout << n1 << ", " << n2 << ", " << n3 << endl; } return 0; プログラム 3-1-1.c の実行例 LEDA によるプログラミング > 3-1-1 4, 4, 4 16, 16, 16 256, 256, 256 65536, 65536, 65536 0, 0, 4294967296 0, 0, 18446744073709551616 2562 = 65536 まではどのデータタイプでも同じ結果が出ていますが, 655462 の計算では int 型, long 型とも桁溢れを生じてしまって, 正しい結果が得られ ていません. 演習問題 3.1.1 f (n) = 2f (n 1) によって定義される関数 f (n) の値を計算す るプログラムを書きなさい. ただし, n は入力で与えるものとします. f (1) = 4; f (2) = 16; f (3) = 65536 であることを確かめなさい. また, f (4) の値が何 桁になるか予想してからプログラムを実行しなさい. 演習問題 3.1.2 大きな整数 x を入力して, (x + 1)2 と x2 + 2x + 1 が等しい ことを確かめるプログラムを書きなさい. 3.1.2 rational タイプ プログラムには計算誤差がつきものですが, LEDA では \誤差なし" 計算 を実現するために様々な仕組みを提供しています. 任意の長さの整数を扱う 3.1. LEDA のデータタイプ 79 integer タイプもその一つですが, その他に有理数を扱うデータタイプ rational も提供しています. 下のプログラムは, rational タイプの変数 q 1; q 2 にそれぞ れ有理数を設定した後, それらの和を計算するものです. ただ, 出力結果から も分かるように, 分母と分子が約分できる場合でも自動的には約分をしませ んから, 約分をしたい場合には q1.normalize(); として明示的に約分を実行する必要があります. //program 3-1-2.c: 有理数の和の計算 #include <LEDA/rational.h> int main() { rational q1, q2; } // q1, q2 は有理数型 q1 = rational(2, 15); // q2 = rational(5, 21); // cout << "2/15 + 5/21 = " << q1 + q2; // q1 = q1 + q2; cout << " = " << q1.normalize(); // return 0; q1 に有理数 2/15 を代入 q2 に 5/21 を代入 q1 と q2 の和の計算 有理数 q1 の約分 プログラム 3-1-2.c の実行例 > 3-1-2 2/15 + 5/21 = 117/315 = 13/35 上のプログラムでは代入文で rational タイプの変数に値を設定しましたが, 次のように初期化することもできます. rational q1(2, 15), q2(5, 21); この他にも様々な関数が用意されています. 詳しくは LEDA Book を参照 してほしいのですが, 主なものは以下の通りです. 80 第3章 q.normalize() q.numerator() q.denominator() q.inverse() q.to_double() LEDA によるプログラミング q を正規化 q の分子 q の分母 q の逆数 q を double 型に変換 これらの関数を使ったプログラムを下に示します. //program 3-1-3.c: 有理数に関連する関数 #include <LEDA/rational.h> int main() { rational q1(117,315), q2(48, 228); } cout << q1 << "の分母 = " << q1.denominator() << endl; cout << q1 << "の分子 = " << q1.numerator() << endl; cout << "117/315 = " << q1.normalize() << endl; cout << q2 << "の逆数 = " << q2.inverse() << endl; cout << q2 << "の近似値 = " << q2.to_double() << endl; return 0; プログラム 3-1-3.c の実行例 > 3-1-3 117/315 の分母 = 315 117/315 の分子 = 117 117/315 = 13/35 48/228 の逆数 = 228/48 48/228 の近似値 = 0.210526 演習問題 3.1.3 テキストのプログラムでは 2 つの有理数をプログラム内で与 えていましたが, それらを入力で与えるようにしなさい. ただし, 有理数は 13/97 のように分数表現で入力するものとします. 演習問題 3.1.4 小数点つきの数を有理数変数に入力し, これを約分した後, 有理数として出力するプログラムを作りなさい. 3.1. LEDA のデータタイプ 81 3.1.3 bigoat タイプ C言語では浮動小数点数を (s; m; e) という 3 組の数で表現します. 最初の s は符号ビットで, 0 なら正の数を, 1 なら負の数を表します. m は 0 以上 1 未満 の小数で, 仮数部 (mantissa) と呼ばれる数です. また, e は指数部と呼ばれる整 数です. IEEE 標準では, m を 52 ビットで表現されるビット列 0:m1 m2 : : : m52 , e を 11 ビットで表現します. これらの数を用いて, s X (1 + 1 52 m 2 )2 i i e 1023 i として計算される数を表されるのが IEEE 標準の浮動小数点数です. この形式で表現できる絶対値最大の数と(0 より大きい)絶対値最小の数 は, それぞれ (2 2 52 ) 21023 と 2 52 2 1023 ということになります. LEDA で提供している bigoat というデータタイプは上記の浮動小数点数 の表現を一般化したもので, 仮数部のビット長 m と指数部のビット長 e を任 意に指定することができます. さらに, 丸めの方法も指定することができるよ うになっています. 具体的には, 以下の通りです. TO_NEAREST TO_ZERO TO_INF TO_P_INF TO_N_INF EXACT 表現可能な最も近い値に近似 0 に近づく方向に近似 0 から遠ざかる方向に近似 正の無限大の方向に近似 負の無限大の方向に近似 +, -, *については正確に計算し, それ以外については最も 近い値に近似 本来は2進表現において丸めを実行するのですが, 説明を分かりやすくす るために 10 進数 z = 54371 を丸める場合を考えましょう. この数を仮数部と 指数部に分けて表現すると, z = 54371 = 0:54371 105 となります. 仮数部の長さと丸めの方法を指定して丸めたときに結果がどの ように違うかを示したのが表 3.1.3 です. 82 第3章 仮数部の長さ 3 3 2 2 丸めの方法 LEDA によるプログラミング 結果 TO NEAREST TO ZERO TO INF EXACT 0:544 105 0:543 105 0:55 105 0:54371 105 表 3.1: 指定による丸めの結果の違い 表 3.1.3 で, 仮数部の長さを 3 と指定して丸める場合, 仮数部 0:54371 の小 数第 4 位を丸めることになるので, 0:543 または 0:544 のいずれかになります. 丸めの方法を TO NEAREST とすると, 近い方の値に丸めますから, 結果は 0:544 となります. TO ZERO を指定した場合には, 0 に近い方を選ぶので, 結 果は 0:543 になります. 丸めの方法が EXACT の場合には, 仮数部の長さの指 定は無視され, 必要な桁数が確保されます. 最後に, 仮数部の長さや丸めの方法は bigfloat::set_global_prec(212); bigfloat::set_rounding_mode(TO_ZERO); のようにして指定します. 演習問題 3.1.5 bigfloat 変数の値が丸め方の違いによってどのように違う かを確かめるプログラムを書きなさい. 3.1.4 real タイプ C言語に用意されている数学ライブラリーを用いれば, 平方根の計算はで きます. そこで, たとえば, p ( 17 の値を計算してみましょう. p p p 12)( 17 + 12) 5 3.1. LEDA のデータタイプ 83 //program 3-1-4.c: 平方根を含む計算 #include <LEDA/stream.h> #include <math.h> int main() { cout << (sqrt(17.0) - sqrt(12.0)) * (sqrt(17.0) + sqrt(12.0)) - 5.0 << endl; return 0; } このプログラムを実行すると, 浮動小数点数の丸め誤差の関係で出力は正 確には 0 になりません. 計算機によって計算結果は異なるかも知れませんが, 1.77636e-015 のような非常に小さな値が出力されるはずです. しかし, どん な計算機でも値がちょうど 0 になることはないようです. このように, 浮動小 数点数を用いた計算では誤差は避けられませんが, LEDA の real データタイ プを用いると, このような場合でも正確に計算を実行することができます. ち なみに, double 型と real 型を比較するために, 次のプログラム 3-1-5.c のよう に, 同じ計算を両方の型を使って実行してみましょう. //program 3-1-5.c: 平方根を含む計算 (2) #include <LEDA/real.h> int main() // double 型と real 型での計算の違い { double da = 17.0, db = 12.0; real ra = 17.0, rb = 12.0; double dx = (sqrt(da) - sqrt(db))*(sqrt(da) + sqrt(db)) - 5; real rx = (sqrt(ra) - sqrt(rb))*(sqrt(ra) + sqrt(rb)) - 5; cout << "dx = " << dx << endl; cout << "rx = " << rx << endl; cout << "sign(rx) = " << sign(rx) << endl; cout << "rx = " << rx << endl; return 0; } 84 第3章 LEDA によるプログラミング プログラム 3-1-5.c の実行例 > 3-1-5 dx = 1.77636e-015 rx = [-6.83516728929542E-15,1.0387880968096E-14] sign(rx) = 0 rx = [0, 0] double 型での計算結果は前と同じですが, real 型の計算結果は一つの数値 ではなく, 数値対で表現されています. このような数値の表現方式は区間表現 と呼ばれ, 近似値の計算によく用いられているものです. LEDA と言えども, p p p p ( 17 12)( 17 + 12) 5 の値を代数的に正確に求めることは難しいの で, 実際には近似値を求めています. ただ, 近似値を示すだけではなく, その 近似値がどの範囲にあるかを区間で表現しているのです. 上のプログラムで は real 型の変数の符号を sign(rx) として計算していますが, この関数が呼び 出されると, LEDA は対応する区間表現が 0 を含むかどうかを調べます. も し含んでいなければ正確に符号を答えることができます. 区間が 0 を含んで いて, しかも区間の幅が十分に小さいとき(具体的な限度については LEDA Book を参照してください), 符号は 0 だと答えます. 符号の計算を行うとき 区間そのものの表現も変更されますので, 上のプログラムのように同じ変数 の値を出力しているのに, 符号の出力の前後で出力値が異なるのはそのため です. いずれにしても, 詳細については LEDA Book あるいは関連の文献を参 照してください. 上では平方根の例について説明しましたが,立方根についても同様の計算 を行うことができます. 演習問題 3.1.6 2 次方程式 x2 + ax + b = 0 の係数 a; b を入力して, 根の公式 により根を求めるプログラムを書きなさい. また, その根が元の 2 次方程式を 満たすこともプログラムの中で検算しなさい. 3.2. LEDA の配列 3.2 85 LEDA の配列 3.2.1 array タイプ C言語で用意されている配列の他に, LEDA 特有の配列を用いることがで きます. C言語ではインデックスが必ず 0 から始まりましたが, LEDA では任 意の整数インデックスから始まる配列を宣言することができます. たとえば, array<string> A(3,5); // A[3] - A[5] を要素とする配列 array<string> B(10); // B[0] - B[9] を要素とする配列 array2<int> C(1,2,4,6); // 2次元配列 C(1..2, 4..6) によって, インデックス集合が [3::5] の string 型の 1 次元配列 A, インデックス集 合が [0::9] の string 型の 1 次元配列 B , およびインデックス集合が [1::2] [4::6] の int 型の 2 次元配列 C を宣言することができます. 1 次元配列については, A[4] のように角括弧を用いて配列要素を指定しますが, 2 次元配列について は, 配列要素を C(1,5) のように丸括弧を用いて指定します. この array 型を用いると幾つか便利なことがあります. たとえば, 1 次元配 列 A にデータを入力して, そのまま出力するとき, C言語では, int A[10]; cout << "データを入力してください " << endl; for(i=0; i<10; i++) cin >> A[i]; cout << "入力データは次の通り " << endl; for(i=0; i<10; i++) cout << A[i] << " "; のようにしないといけませんが, LEDA では, 次のように簡単になります. //program 3-2-1.c: 配列への入出力 #include <LEDA/array.h> int main() { array<int> A(10); A.read("データを入力してください "); 86 } 第3章 LEDA によるプログラミング A.print("入力データは次の通り "); return 0; さらに, 1 文を付け加えるだけで入力データを昇順にソートして出力するこ ともできます. //program 3-2-2.c: 配列への入出力 (2) #include <LEDA/array.h> int main() { array<int> A(10); A.read("データを入力してください "); A.sort(); A.print("ソートの結果は次の通り "); return 0; } プログラム 3-2-2.c の実行例 > 3-2-2 データを入力してください 21 19 17 14 11 18 13 20 25 12 ソートの結果は次の通り 11 12 13 14 17 18 19 20 21 25 上の例では 10 個の要素をもつ配列を宣言して, 10 個のデータを配列に読み 込んでいますが, 配列の長さを入力で与えることもできます. C言語では変数 を用いて配列を宣言することはできませんが, LEDA ではC言語と別の形で array 型を実現していますので, それが可能なのです. また, ソート処理に関しては配列の一部だけを対象にすることもできます. たとえば, 下のプログラムでは先頭と最後の要素だけは別にして, 間の部分を ソートしています. //program 3-2-3.c: 配列への入出力 (3) #include <LEDA/array.h> int main() { 3.2. LEDA の配列 } 87 int n = read_int("データの個数を入力してください"); array<int> A(0, n-1); A.read("データを入力してください "); A.sort(1, n-2); A.print("ソートの結果は次の通り "); return 0; 特に指定しなければ昇順にソートされるので, 逆に降順にソートしようと したり, 特別な順序関係に従ってソートしようとすると比較のための関数を 指定しなければなりません. 次の例は整数型データを降順にソートするもの です. //program 3-2-4.c: 配列への入出力 (4) #include <LEDA/array.h> int dcmp(const int & p, const int & q){ int s = compare(p, q); return -s; } int main() { array<int> A(10); A.read("データを入力してください "); A.sort(dcmp); A.print("ソートの結果は次の通り "); return 0; } 演習問題 3.2.1 データの個数 n を入力で指定して n 個のデータを入力し, 最 初の半分(0 番目から (n 1)=2 番目まで)は昇順に, 後半((n 1)=2 + 1 番目 から n 1 番目まで)は降順にソートして出力するプログラムを作りなさい. 88 3.2.2 第3章 LEDA によるプログラミング 疎配列 C言語で用意されている配列は, インデックスが必ず 0 から始まる整数で したが, これは配列の任意の要素の場所を効率良く計算するためでした. 配列 の先頭の番地さえ記録しておけば, 配列の k 番目の要素は先頭の番地に k を 加えるだけで求めることができます. 反面, この方法では配列をメモリの連続 する場所にインデックスの値通りに確保しなければならないので, インデック ス集合のサイズに等しい領域が必要となります. 配列のほとんどすべての要 素に意味のあるデータを蓄える場合には問題ありませんが, 図 3.1 のように配 列のごく一部にしか意味のあるデータが蓄えられない場合にはメモリの無駄 使いとなります. このような配列を総称して, 疎配列 (sparse array) と呼びま すが, LEDA では疎配列を効率良く実現するデータ構造を提供しています. インデックス 配列 値が格納されている場所 空き場所 図 3.1: 一部にだけデータが格納されている疎配列 疎配列のためのデータ構造では, インデックス集合はいくら大きくても構 いません. 意味のある要素だけを蓄えるからです. 具体的には, 疎配列をM と するとき, M の要素集合をdom(M) として常に管理しています. ある要素M[i] に始めて値を蓄えたとき, この要素をdom(M) に加えます. もちろん, ある値 q が既にdom(M) に登録されているかどうか(属しているかどうか)を調べる こともできます. 単に, M.defined(q) 2 の値を参照して, これがtrue なら q dom(M) であると言えます. また, dom(M) のすべての要素についての処理を行う場合には, forall_defined(i, M) ループ本体 3.2. LEDA の配列 89 の形式で実現することができます. また, 今まではインデックスとして非負の整数だけを考えていましたが, LEDA の疎配列ではインデックスとして整数以外にもほとんどすべてのデー タタイプを許しています. もちろん, 配列要素のタイプも自由に決めることが できます. インデックス集合の性質により3種類のタイプ | d array, h array, map |があります. (1) d array d array (dictionary array) は, 線形順序 (linear order) が存在するような データタイプの要素をインデックスとして用います. 整数はもとより, 文字列 についても辞書式順序という線形順序が存在しますから, 文字列をインデッ クスとして用いることができます. 先に英単語を多数入力して, それぞれの英単語の出現度数を表にして出力 するプログラムについて考えました. その場合, 英単語を蓄える配列と, 対応 する英単語の出現度数を数えるための整数型配列を用いました. しかし, 英単 語そのものをインデックスとして使い, 配列の値によって出現度数を数える ことにすれば, 一つの配列だけで済みます. 具体的には, #include <LEDA/d_array.h> d_array<string, int> count; として宣言します. ここで, 最初の string は string 型の値をインデックスと して用いることを宣言し, 後の int は配列要素の型を表しています. 以前のプログラムでは, まず入力された英単語が配列の何番目に登録され ていたかを順に調べて, i 番目だと分かれば, i 番目の度数を 1 増やし, どこに も登録されていなければ, 配列の末尾に新たに登録する必要がありました. し かし, このデータタイプでは, 入力の英単語そのものをインデックスとして使 うことができますから, 探索する必要がないのです1 . また, 上のように宣言 した場合には配列要素の値は初期化されていませんが, 1 実際には探索をしないで済ませることは不可能なので探索を行ってはいます. データを平 衡2分探索木に蓄えていますので, データ数を n とするとき, log n に比例する比較だけで効 率良く検索を行っています. 90 第3章 LEDA によるプログラミング #include <LEDA/d_array.h> d_array<string, int> count(0); のようにするだけで, すべての配列要素の値を 0 として初期化しておくこと も可能です. 注意すべきことは, 今までの配列の宣言では, 配列のサイズを定数で宣言す る必要があったのですが, d array 型の配列の場合にはその必要がありません. ではどうしているのかと言いますと, LEDA の中で動的に管理をしてくれて いるのです. もちろん, 物理的なメモリーのサイズを超えてデータを蓄えるこ とはできませんが, 実行時に警告が発せられない限り大丈夫です. さて, このように配列 count を宣言しておきますと, 英単語を入力してその 度数を数える部分は次のように簡単になります. while(cin >> s) count[s]++; そう, たったこれだけで新たな英単語が入力されたときも, 過去に入力された 英単語の度数を 1 増やす処理も行われているのです. すなわち, 英単語 s が入 力されたとき, s をインデックスとする疎配列の要素(平たく言えば, 疎配列 の s 番目の要素)の値 count[s] を 1 だけ増やします. s が初めての単語であれ ば, count[s] は 0 に初期化されているので, 結果は 1 になります. d array 型にも不便なところがあります. 最終的に配列の内容を出力しよう というとき, 今までだとインデックスが連続する整数として与えられていま したから, 単純な for ループを用いてすべての配列要素を列挙することができ ました. しかし, d array 型ではインデックス集合が連続する量ではありませ んから, 列挙することが難しいのです. そこで, この点を補うために, LEDA では利用されたインデックスを順序良く列挙する仕組みが用意されています. 上でも述べましたが, 具体的には, forall_defined(s, count) という for と同じ形式の繰り返し構造が用意されているのです. この形式を用 いるとプログラム全体を次のように簡潔に記述することができます. 3.2. LEDA の配列 91 //program 3-2-5.c: 英単語の出現度数を数える #include <LEDA/d_array.h> int main() { d_array <string, int> count(0); string s; while(cin >> s) count[s]++; } cout << "入力文字列とその度数 " << endl; forall_defined(s, count) cout << s << ": " << count[s] << endl; return 0; プログラム 3-2-5.c の実行例 > 3-2-5 Koji Kurt Tetsuo Kurt Koji Kurt Stefan Tetsuo Kurt Stefan Tetsuo Koji Kurt Tetsuo ^z Koji 3 Kurt 5 Stefan 2 Tetsuo 4 これを以前のプログラム 2-8-3.c と比較してみれば, d array 型の威力がよ く理解できるでしょう. インデックスも配列要素の値もともに文字列型とすることもできます. た とえば, 英単語が入力されると対応するドイツ語単語を出力するという簡単 な英語 ドイツ語辞書を作ってみましょう. 今度は英単語がインデックスと なり, 配列要素の値がドイツ語単語となります. 同じ仕組みですから, 次のよ うなプログラムになることは容易に理解できるでしょう. //program 3-2-6.c: 英語-ドイツ語辞書 #include <LEDA/d_array.h> 92 第3章 LEDA によるプログラミング int main() { d_array<string, string>dic; dic["hello"] = "hallo"; dic["world"] = "Welt"; dic["book"] = "Buch"; dic["light"] = "Leuchte"; } string s; while(cin >> s) cout << s << " -> " << dic[s] << endl; return 0; プログラム 3-2-6.c の実行例 > 3-2-6 world world -> Welt hello hello -> hallo tetsuo tetsuo -> light light -> Leuchte ^z 上のプログラムでは 4 つの単語についてだけ対応関係をプログラム内で代 入文により設定しましたが, ファイルに英語とドイツ語の対応表を置いてお いて, そこから読み込むようにすることもできます. 下にプログラムの実行例 を示しています. 登録されている単語が入力されますと正しく変換しますが, 登録されていない単語が入力された場合には対応する単語が何も出力されま せん. 最後にインデックス, 配列要素の値ともに整数の場合を考えてみましょう. インデックスが int 型なのですから, 可能な最小のインデックスと最大のイン デックスを用いて array 型の配列を宣言すればよさそうですが, それでは非常 に大きな配列が必要になってしまうことがあります. このように, インデック 3.2. LEDA の配列 93 スになるものは離散的な値をとるけれども, その可能な値をすべて列挙する と非常に大きな数になってしまう, というのが d array 型に最も適した場合な のです. 英単語の場合とよく似ていますが, 多数の整数データを入力して, データが 入力されるたびに, その数が以前に何度入力されたかを答えるというプログ ラムを考えましょう. 0 から N 1 までの整数に制限することができるなら, サイズ N の配列を宣言した後, 配列要素を 0 に初期化しておき, 0 以上 N 未 満の整数 k が入力されるたびに配列の k 番目の値を1増やすことにより要求 に応えることができます. 問題となるのは配列の上限を決める N の値が非常 に大きいときです. メモリーの面でも問題ですが, 初期化に N に比例する時 間がかかるのが難点です. これに対して, 疎配列を用いる場合には, 上限の整 数を決めておく必要はなく, また初期化にも時間はかかりません. 反面, 毎回 の要素アクセスには素朴な方法より時間がかかります. //program 3-2-7.c: 整数の出現度数を数える #include <LEDA/d_array.h> int main() { d_array<int, int>count(0); int x; } while(cin >> x) count[x]++; cout << "入力整数とその度数 " << endl; forall_defined(x, count) cout << x << ": " << count[x] << endl; return 0; 94 第3章 プログラム 3-2-7.c の実行例 LEDA によるプログラミング > 3-2-7 45 3 7 -92 45 -309 ^z 入力整数とその度数 -309: 1 -92: 1 3: 1 7: 1 45: 2 (2) h array d array では線形順序が存在するようなインデックス集合でなければならな かったのに対して, h array (hashing array) では, ハッシュ関数が定義できる ようなインデックス集合でなければなりません. ハッシュ関数 (hash function) というのは, 与えられたデータに対してハッシュ表のインデックス(整数)を 求めるための関数のことです. たとえば, 文字列 s に対して s を構成する各文 字のアスキー符号を整数と見なして, それらの重みつき和をハッシュ表のサ イズで割った余りを求めることにすると, どんな文字列にも一定の範囲の整 数が対応することになります. ハッシュ法の基本は, 与えられたデータをその ハッシュ関数値をインデックスとする場所に蓄えることです. データが違え ばハッシュ関数値も違うなら理想的ですが, 現実には異なるデータが同じハッ シュ関数値をとることがあるので, そのための対策が種々考えられています. ハッシュ法に基づいてデータを蓄えようとすると, ハッシュ表のサイズとハッ シュ関数を適切に決めることが必要になりますが, h array を用いる場合, ハッ シュ表のサイズは気にする必要はなく, 単にハッシュ関数だけを決めればよい ことになっています. 当然, ハッシュ関数の値に上限もありません. とにかく 整数に変換することができるならハッシュ関数として使えるのです. h array では, 要素をアクセスする際にプログラム内で定義されたハッシュ関数を用い ますから, うまいハッシュ関数を定義しておけば, アクセスを効率良く, すな わち定数時間で行うことができます. d array の場合には線形順序にしたがっ て要素を蓄えていますから, 要素のアクセスには O (log n) の時間がかかりま 3.2. LEDA の配列 95 す. ただし, n は配列に蓄えられた要素数です. もっとも, forall_defined() のループで配列の全要素を列挙する場合には要素数に比例する時間しかかか りません. h array を用いて英単語の出現度数を数えるプログラムを書いてみましょ う. d array を用いたプログラム 3-2-5.c とほぼ同じですが, ハッシュ関数を陽 に定義しているところだけが異なります. //program 3-2-8.c: 英単語の出現度数を数える (2) #include <LEDA/h_array.h> int Hash(const string& x){ return (x.length()>0) ? x[0]: 0; } int main() { h_array <string, int> count(0); string s; } while(cin >> s) count[s]++; cout << "入力文字列とその度数 " << endl; forall_defined(s, count) cout << s << ": " << count[s] << endl; return 0; (3) map map は, d array や h array とは違って, ポインタなどをインデックスとし て用いるものです. たとえば, 2次元平面上の点, 線分, 円などを表すデータ タイプ point, segment, circle の値をインデックスとして用いることができる のです. たとえば, それぞれの点にある属性を表す整数をもたせる場合には, map<point, int> pt(0); のように宣言します. ここで, pt が配列の名前で, 0 は属性値を 0 に初期化し て用いることを表しています. 96 第3章 LEDA によるプログラミング 後でグラフの取り扱いについても述べますが, グラフの節点と辺を表すデー タタイプ node や edge なども map のインデックスとして用いることができ ます. 例として, ウィンドウ上でマウスを用いて点を指定することによって点集 合を構成する場合を考えてみましょう. point 型の配列を宣言して, そこに順 にデータを蓄えるというのが普通のやり方ですが, その場合には, point pt[SMAX], p; int n=0; while(W >> p) { W << p; pt[n++] = p; } のようにするのが常套手段でしょう. これで i 番目の点はpt[i] として記録 されたことになります. この方法の一つの欠点は, 予め配列のサイズを指定し て宣言しなければならないことです. 予想を超える個数の点が入力された場 合には問題が生じます. これを避けるためには, リスト構造で点集合を管理す ることも可能ですが, map を用いても同様のことが可能です. 各点について, 何番目に入力された点であるかの情報が必要な場合, map<point, int> idx; と map 型の配列 idx を宣言します. 後は点が入力されるたびに, その入力順 を配列の値として記録すればいいのです. したがって, 次のようなプログラム になります. map<point, int> idx; int n=0; while(W >> p) { W << p; idx[p] = n++; } このように, map を用いれば, 要素数が予め分からないような集合も管理す ることができます. また, 上のようにして点を登録しておけば, 登録したすべての点を列挙した り, 同じ操作を繰り返すこともできます. 具体的には, 次のように forall の繰 り返し構造を使います. 3.2. LEDA の配列 97 forall_define(p, idx) { ... } 上では点集合を管理するのに配列や疎配列を用いる方法について説明しま したが, LEDA ではリスト構造を用いることを推奨しています. たとえば, 次 のようにすれば, 点を次々と入力した後x座標の昇順にソートし, ソート順に 点を赤で塗ることができます. list <point> S; while(W >> p){ W << p; S.append(p); } S.sort(); // リスト S の要素を昇順にソート forall(p, S){ W.draw_point(p, red); W.read_mouse(); } (4) 要素の列挙と比較 d array の所でも述べましたが, 疎配列の要素をforall_defined() のルー プで列挙することができます. d array では線形順序に従って要素を管理して いましたから, 列挙すると自然にソート順に定義された要素が出力されるこ とになります. 一方, h array や map では, そもそも要素間の順序関係を仮定 していませんでしたから, ソート順に要素を列挙することはできません. た だし, h array と map ではハッシュ法に則って要素を管理していますから, う まくハッシュ関数を選んでおけば平均で O (1) の時間, すなわち定数時間で要 素のアクセスが実現できますが, d array では順序を保って管理していますか ら, アクセスには最悪で O (log n) の時間がかかることになります. ここで, n は配列に蓄えられた要素数です. 以上, 3種類の疎配列について説明してきましたが, これらは整数以外を インデックスとして用いることができるという意味で連想配列 (associative array) とも呼ばれるべきものです. これらの疎配列の特徴をまとめたのが表 3.2.2 です. 98 第3章 LEDA によるプログラミング d array h array インデックス 線形順序が必要 ハッシュタイプ アクセス時間 最悪で O (log n) 平均で O (1) ソート順 ソート順でない 要素の列挙 map 整数, ポインタなど 平均で O (1) ソート順でない 表 3.2: 疎配列の比較 演習問題 3.2.2 名前と電話番号が対になったデータを多数含んだデータファ イルからデータを読みとって, 名前を入力すると, その電話番号を答えるプ ログラムを作りなさい. また, その逆もできるようにプログラムを拡張しな さい. 3.2.3 array 型の威力 2 つの乱数列を生成して, 同じ値が何個含まれているかを調べるプログラム を作ってみましょう. 最初に, 15 個ずつ乱数を生成してみて, 目で見て同じ乱 数が幾つ生成されたかを調べてみることにしましょう. プログラムは次のよ うになります. //program 3-2-9.c: 2 つの乱数列の共通部分 (0) #include <LEDA/array.h> #include <LEDA/random_source.h> #define MAXA 15 #define MAXB 15 #define LIMIT 1500 int main() { array<int> A(MAXA), B(MAXB); random_source S; int i, j, n=MAXA, m=MAXB; S.set_seed(100); 3.2. LEDA の配列 for(i=0; A[i] for(j=0; B[j] } i<n; i++) = S(1,LIMIT); j<m; j++) = S(1,LIMIT); A.print("配列 A の内容\n"); cout << endl; B.print("配列 B の内容\n"); cout << endl; return 0; プログラム 3-2-9.c の実行例 99 > 3-2-9 配列 A の内容 1334 346 56 281 1080 6 589 919 264 278 1260 173 765 737 758 配列 B の内容 1326 1068 685 1460 197 187 1011 532 1221 1066 813 252 1072 1401 1170 ここにはたった 15 個のデータしかないのですが, 同じ値があるかどうかを 調べるのは結構面倒なものです. そこで, 配列 A の各要素について, 同じ値の ものが配列 B にも含まれているかどうかを for ループを用いて調べることに しましょう. すなわち, 配列 A の i 番目の要素に等しい要素が配列 B に含ま れているかどうかを調べるために配列 B のインデックス j を順に増やしてい き, 同じ値の要素が見つかるか, 見つからずに配列 B の範囲外に出てしまった ときにループから抜けるようにします. ループから抜けたとき, 配列 B のイ ンデックスを調べれば, 途中で同じ値のものを見つけてループから抜け出た のかどうかが分かります. この考え方に基づいてプログラムを作ります. さら に, 計算時間を測定するために, そのチェックに要する計算時間を used time() という LEDA で用意された関数を用いて計測します. //program 3-2-10.c: 2 つの乱数列の共通部分 (1) #include <LEDA/array.h> #include <LEDA/random_source.h> #define MAXA 10000 100 第3章 LEDA によるプログラミング #define MAXB 10000 #define LIMIT 1000000 int main() { array<int> A(MAXA), B(MAXB); random_source S; int i, j, n=MAXA, m=MAXB, nc; S.set_seed(100); for(i=0; i<n; i++) A[i] = S(1,LIMIT); for(j=0; j<m; j++) B[j] = S(1,LIMIT); } float T = used_time(); nc = 0; for(i=0; i<n; i++){ for(j=0; j<m; j++) if(A[i] == B[j]) break; if(j < m) nc++; } cout << "共通要素の個数 = " << nc << endl; cout << "計算時間 = " << used_time(T) << "sec." << endl; return 0; プログラム 3-2-10.c の実行例 > 3-2-10 共通要素の個数 = 96 計算時間 = 15.82sec. つまり, 10000 個の乱数列を 2 つ生成したとき, 96 個の要素が両方の列に含 まれており, その照合のための計算時間は約 15:82 秒だったということです. for ループは合計 1 億回繰り返されているので, それが 16 秒程度で実行でき ることは驚くべきことですが, こんな簡単なことに 16 秒もかかることも驚き です. 3.2. LEDA の配列 101 最初のプログラムで共通要素を見つけるのが難しかったのは, 要素がばら ばらであるために, ある値の要素があるかどうかを探すことが困難だったか らです. では, 生成した乱数列をソートしてみればどうでしょう. LEDA の array 型では配列要素のソートが 1 行でできたことを思い出しましょう. 最初 のプログラムにソート処理を加えてみましょう. //program 3-2-11.c: 2 つの乱数列の共通部分 (2) #include <LEDA/array.h> #include <LEDA/random_source.h> #define MAXA 15 #define MAXB 15 #define LIMIT 1500 int main() { array<int> A(MAXA), B(MAXB); random_source S; int i, j, n=MAXA, m=MAXB; } S.set_seed(100); for(i=0; i<n; i++) A[i] = S(1,LIMIT); for(j=0; j<m; j++) B[j] = S(1,LIMIT); A.sort(); B.sort(); A.print("配列 A の内容\n"); cout << endl; B.print("配列 B の内容\n"); cout << endl; return 0; 102 第3章 LEDA によるプログラミング プログラム 3-2-11.c の実行例 > 3-2-11 配列 A の内容 6 56 173 264 278 281 346 589 737 758 765 919 1080 1260 1334 配列 B の内容 187 197 252 532 685 813 1011 1066 1068 1072 1170 1221 1326 1401 1460 今度は 2 つの列の照合は簡単です. 人間にとって簡単なのですから, 計算機 にとっても簡単なはずです. 実際, LEDA では昇順にソートされた配列に対 しては, 2 分探索と呼ばれる効率の良い探索を行う関数が用意されています. その関数を用いてプログラム 3-2-11.c を書き換えてみましょう. 上のプログラムのように両方の配列をソートしてもよいのですが, 実際に は配列 B に関してしか探索を行わないので, 配列 A はソートせずに, 配列 B だけをソートします. ソート列 B の上では, x という値が配列 B に含ま れるかどうかは, LEDA で用意された 2 分探索の関数 bin search() を用いて, B.bin_search(x) として効率良く判定することができます. この考え方で 作ったのが次のプログラム 3-2-12.c です. //program 3-2-12.c: 2 つの乱数列の共通部分 (3) #include <LEDA/array.h> #include <LEDA/random_source.h> #define MAXA 10000 #define MAXB 10000 #define LIMIT 1000000 int main() { array<int> A(MAXA), B(MAXB); random_source S; int i, j, n=MAXA, m=MAXB, nc; S.set_seed(100); for(i=0; i<n; i++) A[i] = S(1,LIMIT); for(j=0; j<m; j++) B[j] = S(1,LIMIT); 3.2. LEDA の配列 } 103 float T = used_time(); B.sort(); nc = 0; for(i=0; i<n; i++) if(B.binary_search(A[i]) >= 0) nc++; cout << "共通要素の個数 = " << nc << endl; cout << "計算時間 = " << used_time(T) << "sec." << endl; return 0; プログラム 3-2-12.c の実行例 > 3-2-12 共通要素の個数 = 96 計算時間 = 0.06sec. 今度の計算時間は 0:06 秒です. これは間違いではありません. 最初のプログ ラムでは 2 重ループになっていましたから, A; B の配列のサイズを n; m とす るとき, n m に比例する時間が必要でしたが, サイズ m の配列 B 上での 2 分 探索は log m に比例する時間で実行できますから, ソート処理に m log m に比 例する時間がかかったとしても, 照合処理は n log m 時間でできているのです. 合計で m log m + n log m に比例する時間がかかりますが, これは n m より は圧倒的に小さいのです. 実際, n = m = 10000 のとき, log2 10000 = 13:288 ですから, n m=(2 10000 log 2 10000) = 376:28 ですが, その値は実行時間 の比 15:82=0:06 = 263:67 と近い値になっています. つまり, これだけで約 260 倍ものスピードアップが達成できたわけです. 演習問題 3.2.3 重複のない n 個の乱数を生成するプログラムを作りなさい. ただし, 毎回乱数を生成するたびに今までに生成した乱数との重複を調べ, 重 複があれば乱数を生成しなおすという考え方でプログラムを作りなさい. 演習問題 3.2.3 では, 乱数を生成するたびに, 過去に生成した乱数を蓄える 配列を順に調べることによって重複の有無を確かめることを意図していまし たが, 先に学んだ疎配列を使うとプログラムは簡単になります. 以前と同様 に, 乱数は配列に順次蓄えていきますが, 同時に疎配列にも蓄えておきます. 104 第3章 LEDA によるプログラミング そこで, 新たな乱数を発生したとき, まず疎配列を調べて, それが過去にあっ たかどうかを判断します. 過去になかった場合には, その乱数を配列に蓄え, さらに疎配列にも登録するのです. この考え方でプログラムを作ると次のようになります. //program 3-2-13.c: 同じ値を含まない乱数列の生成 #include <LEDA/array.h> #include <LEDA/d_array.h> #include <LEDA/random_source.h> int main() { int n, r, i; n = read_int("乱数の個数を指定してください "); array<int> A(n); d_array<int, int> ch(0); random_source S; } S.set_seed(100); for(i=0; i<n; i++){ do{ r = S(); }while( ch[r] > 0 ); A[i] = r; ch[r]++; } A.print("得られた乱数列: "); cout << endl; A.sort(); A.print("ソート結果: "); return 0; 演習問題 3.2.4 プログラム 3-2-13.c では d array を用いましたが, h array を用いて計算時間が改善できるかを試しなさい. 演習問題 3.2.5 本文において2つの乱数列の共通部分を求める問題について 考えましたが, 疎配列を用いるとプログラムが簡単になります. このことを 3.3. 基本データタイプ 105 実際にプログラムを作って確かめるとともに, 計算時間を他の方法と比較し なさい. 3.3 基本データタイプ 効率の良いアルゴリズムを設計するには最も適切なデータ構造を用いるこ とが大切です. しかし, 一般にデータ構造は難しい, 面倒だという印象が強い のも事実です. 実際, データ構造に関する教科書はたくさんありますが, 1 冊 の本をすべて理解し, 自分のものにするには非常に忍耐力が必要です. しか し, 本当にすべてのプログラマがデータ構造の詳細まで理解する必要がある のでしょうか. LEDA はこのような要望に応えるために主要なデータ構造は ほとんどすべて一般的な形で提供してくれています. したがって, プログラマ は各データ構造の性質を知っていれば適切なデータ構造を選んで自分のプロ グラムに組み込んで使うことができるのです. ここでは、最も基本的なデータ構造としてスタック, キュー, 優先順位つき キュー, リストを取り上げ, さらにそれらのデータ構造の応用例についても触 れることにします. 3.3.1 スタック スタック (stack) は, データの格納と最も最近格納したデータの取り出しを 行うためのデータ構造です. ちょうど机の上に書類を積み重ねる場合を考え ると, 取り出されるのは常に最も上の書類です. スタックでは, データの格納 操作をプッシュ(push) と呼び, データを取り出す操作をポップ (pop) と呼び ます. スタックではデータの格納と取出しを同じ場所を通して行いますから, 最 後に格納されたものが取り出されることになります. 図 3.2 を参照してくだ さい. スタックで蓄えるべきデータの型は何でもよくて, たとえば string 型のデー タを蓄えるスタック S を宣言する場合には, 106 第3章 push(x) LEDA によるプログラミング pop() x stack 図 3.2: スタックの概念図 stack <string> S; とすればいいのです. プッシュとポップの操作は, それぞれ string s; S.push(s); s = S.pop() として実行することができます. また, スタックが空かどうかは, S.empty() によって知ることができます. スタックを使う場合, 最初は空の状態から始めます. 自分でスタックのデー タ構造を定義する場合には空に初期化することも必要ですが, LEDA で用意 されているスタックは最初は空の状態に初期化されていますので, 特に空に する命令を置かなくても大丈夫です. これは次に説明するキューやリストで も同じです. 処理の途中で別の用途に用いるためにスタックを空にしたい場 合がありますが, その場合には S.clear() とします. 詳しくは LEDA Book を 参照してください. 次のプログラムは, 幾つかの文字列を入力で指定して, それらをスタックに 順に格納したあと, スタックが空になるまでポップ操作を繰り返して, 蓄えて あった文字列を出力するというものです. 3.3. 基本データタイプ 107 //program 3-3-1.c: スタックを用いた例題 #include <LEDA/stack.h> int main() { stack <string> S; string s; } while(cin >> s) S.push(s); cout << "入力文字列" << endl; while( !S.empty() ) cout << S.pop() << endl; return 0; プログラム 3-3-1.c の実行例 // 入力がある限り繰り返し // 入力文字列をスタックに蓄える // スタックが空になるまで // 文字列を取り出し出力 > 3-3-1 tetsuo koji kurt stefan ^z 入力文字列 Æ stefan kurt koji tetsuo 上の実行例でも分かるように, スタックは格納された逆の順序でデータが 取り出されますから, LIFO メモリ (Last-in First-out memory) と呼ばれるこ ともあります. この性質は図 3.3 に示した動作例でも分かるでしょう. 演習問題 3.3.1 1 から 9 までの整数を順にスタックに格納した後, 順に取り 出して出力するプログラムを作りなさい. 108 第3章 A B C LEDA によるプログラミング D D D A E E C C C C B B B B B A A A A A push(A) push(B) push(C) push(D) pop() push(E) 図 3.3: スタックの動作例 3.3.2 キュー キュー (queue) もスタックと同様のデータ構造ですが, 待ち行列とも訳され るように, データの格納口とデータの取り出し口が別なので(図 3.4 参照), 格納されたのと同じ順序でデータが取り出されます. したがって, FIFO メモ リ (First-in First-out memory) とも呼ばれています. queue x pop() append(x) 図 3.4: キューの概念図 キューでもデータの格納と取り出しを行います. LEDA では, データの取 り出しについてはスタックと同じ pop という名前を使いますが, データの格 納にはスタックとは違って, append という名前を使っています. キューが空 かどうかはスタックと同様です. スタックのときと同じ例題をキューについても作ってみましょう. そのプ ログラムを下に示します. //program 3-3-2.c: キューを用いた例題 #include <LEDA/queue.h> 3.3. 基本データタイプ 109 int main() { queue <string> S; string s; } while(cin >> s) S.append(s); cout << "入力文字列" << endl; while( !S.empty() ) cout << S.pop() << endl; return 0; プログラム 3-3-2.c の実行例 // 入力がある限り繰り返し // 入力文字列をキューに蓄える // キューが空になるまで繰り返し // ユーから文字列を取りだし出力 > 3-3-2 tetsuo koji kurt stefan ^z 入力文字列 Æ tetsuo koji kurt stefan プログラム 3-3-2.c の実行例を見ると, 入力されたのと同じ順序で文字列が 出力されていることが分かるでしょう. 図 3.5 の動作例も参考にしてください. 演習問題 3.3.2 1 から 9 までの整数を順にキューにプッシュした後, 順に取 り出して出力するプログラムを作りなさい. 3.3.3 優先順位つきキュー スタックにしてもキューにしても, データが格納された順番だけに基づい てデータを取り出していましたが, 現在格納されているデータの中でキー値 110 第3章 S.append(A) A S.append(B) A B LEDA によるプログラミング S.append(C) A B C S.append(D) A B C D S.pop() B C D S.append(E) B C D E 図 3.5: キューの動作例 が最も小さいものを取り出したいということがあります. そのような場合に は, LEDA の p queue データタイプを使うことができます. PQ.insert(key, inf) key PQ.find_min() inf priority queue PQ 図 3.6: 優先順位つきキューの概念図 LEDA の優先順位つきキュー (priority queue) では, データと, そのデータ に付随するキー値の組 < p; i > を格納します. 優先順位はキー値 p によって 決まるものでなければなりません. 以下では, p をキー部, i をデータ部と呼ぶ 3.3. 基本データタイプ 111 ことにします(図 3.6 参照). たとえば, 世界の主要な都市の名前を東京から の距離とともに蓄えておく場合を考えましょう. 優先順位を東京からの距離 で決めることにします. これらのデータを適当な順序で入力して, 距離の小さ い順にデータを取り出して出力するというプログラムを作ってみましょう. まず, 優先順位つきキューを PQ という名前で宣言します. #include <LEDA/p_queue.h> p_queue <int, string> PQ; このとき, PQ の各要素 < p; i > は pq item というタイプをもちますが, それ らはたとえば, <7547, Vancouver> <10842, NewYork> <9712, Paris> <9561, London> のように, 優先順位を決めるためのキー部と都市名を格納するデータ部を組 にしたものです. 優先順位つきキューに新たな項目 < p; i > を挿入したり, キー値 p が最小 である項目を求めたり, 項目 it を取り除いたりするには次のようにします. p_queue <int, string> PQ; PQ.insert(p, i); pq_item it = PQ.find_min(); PQ.del_item(it); 上のようにしてキー値が最小である項目を it という変数に求めたとき, その 項目のキー部とデータ部を取り出す必要があります. それには次のようにす ればいいのです. pq_item it = PQ.find_min(); p = PQ.prio(it); i = PQ.inf(it); 次のプログラムは, 上記のような優先順位つきデータを順に入力して, 入力 が終れば蓄えられたデータをキー値の小さい順に取り出すというものです. 112 第3章 LEDA によるプログラミング //program 3-3-3.c: 優先順位つきキューを用いた例題 #include <LEDA/p_queue.h> int main() { p_queue <int, string> PQ; pq_item it; int x; string s; } while(cin >> x){ cin >> s; PQ.insert(x, s); } cout << "入力都市" << endl; while( !PQ.empty() ){ it = PQ.find_min(); cout << PQ.inf(it) << ": " << PQ.prio(it) << endl; PQ.del_item(it); } return 0; プログラム 3-3-3.c の実行例 > 3-3-3 7547 Vancouver 10842 NewYork 9712 Paris 9561 London ^z 入力都市 Æ Vancouver 7547 London 9561 Paris 9712 NewYork 10842 演習問題 3.3.3 本文のプログラムではキーの値は数値でしたが, 数値以外でも 比較ができればいいので, たとえば文字列でも大丈夫です. Tetsuo Asano の ようにファーストネームとラストネームの順で名前を入力するとして, これ 3.3. 基本データタイプ 113 をラストネームの辞書式の順序に並び替えて出力するプログラムを本文のプ ログラムにならって作りなさい. 3.3.4 スタック, キューの応用例 上で説明したスタックやキューは非常に基本的なデータ構造で, 常識とし て知っておかなければならないものなのですが, ではスタックやキューを使っ た簡単な例題があるかというと, なかなか思いつかないというのが実情です. 今のところは, とにかく, スタックやキューは大事だということを認識だけし て先に進むのもいいのですが, やはり気になるから例題を示してほしいとい う読者の要望に応えるために, 一つの例題を用意しました. S T 図 3.7: 経路探索問題(SとTを結ぶ経路を求めよ) ここで考えるのは経路探索問題です. 図 3.7 を見てください. 全体がセルと 呼ばれる小さな正方形に区切られていますが, Sと記されたセル(スタート) からTと記されたセル(ゴール)に至る経路を見つけるのが問題です. スター トセルから出発して, 上下左右の隣接セルの中で障害物でないセルに進むこ とができます. 斜め方向には進めません. 障害物のセルには網掛けしてあり ます. 人間にとってはこんな簡単なことでも, プログラムで見つけるとなると 結構難しいものです. ここで紹介するのは迷路法という名前で知られている方法です. 基本的な 114 第3章 LEDA によるプログラミング 考え方は, スタートセルから到達できるセルを順に求め, (同じセルを何度も 調べないように)セルにラベルをつけていきます. ラベルのつけ方には色々 な方法が考えられますが, 最も分かりやすいのは, 1 歩で行けるところには 1 というラベルをつけ, そこからまた 1 歩で行けるところに 2 というラベルを つけるという操作を繰り返すものです. 一般には k というラベルがついてい るセルの上下左右のセルを調べて, そこに障害物でなくまだラベルがついて いないセルがあれば, そこに k + 1 というラベルをつけます. このようにして, ゴールセルにラベルがつけば, 経路が見つかったことになります. 逆に, 到達 できるところにすべてラベルをつけたのに, ゴールセルにラベルがついていな い場合には, 経路が存在しないと断定することができます. 直観的には, 池の 中に小石を投げ込んだときに波紋が広がる様子を思い浮かべればいいでしょ う. 経路が存在するなら, スタートセルからの波は必ずゴールセルにも届きま す. このようにラベルをつけた結果を図 3.8 に示します. 3 2 2 1 1 2 3 4 5 6 S 1 5 2 6 T 3 7 15 3 2 4 3 4 5 4 6 5 6 13 14 15 4 6 7 8 7 10 11 12 8 9 9 10 13 14 15 14 15 10 9 10 11 12 13 14 15 図 3.8: 前進操作(Sからの距離に応じてラベルをつける) さて, スタートセルから到達できるところを順次拡張していく操作(前進 操作)が終ると, 今度は経路を求めます. もちろん, ゴールセルにラベルがつ かない状態で終われば, 経路が存在しないので以下の処理は必要ありません. ゴールセルに m というラベルがついたとしましょう. するとその上下左右 に m 1 というラベルをもつセルが存在するはずです. そこで, そのセルに 経路を延ばし, さらにその隣接セルで m 2 というラベルをもつセルを探し 3.3. 基本データタイプ 115 ます. このようにしてゴールセルから番号の逆順にたどることにより, スター トセルまでの経路を得ることができます. この操作を後進操作と呼んでいま すが, それを図示したのが図 3.9 です. 見つかった経路上のセルは薄い網掛け で示されています. 3 2 2 1 1 2 3 4 5 6 S 1 5 2 6 T 3 7 15 3 2 4 3 4 5 4 6 5 6 13 14 15 4 6 7 8 7 10 11 12 8 9 9 10 13 14 15 14 15 10 9 10 11 12 13 14 15 図 3.9: 後進操作(Tから逆にラベルをたどる) では, 上記の考え方でプログラムを作ってみましょう. 簡単のため, セルは 9 9 に固定します. 各セルの状態としては, 障害物かどうかの区別と, ラベ ルがついているかどうかの区別があります. ラベルがついている所には経路 を延長しないのですから, 障害物もラベルも共に正の整数で表し, 残りのセル (自由セル)には 1 というラベルをつけておくことにしましょう. セルのラ ベルを管理するために 2 次元の配列 status() を宣言し, その値を初期設定し ます. このとき, 外周に障害物のラベルをつけたセルを配置しておきますと, 隣接セルを調べるときに, 外部のセルかどうかの判定をしなくて済みます. 初 期設定の部分は次のようになります. #include <LEDA/array2.h> #define FREE -1 #define OBSTACLE 10000 array2 <int> status(0,10, 0, 10); for(x=0; x<=10; x++) for(y=0; y<=10; y++) if(x%10==0 || y%10==0) status(x,y) = OBSTACLE; else status(x,y) = FREE; 116 第3章 LEDA によるプログラミング 初期設定が終われば, ウィンドウ上にセル配置を表示して, マウスのクリッ クによってスタートセルとゴールセル, さらに障害物のセルを順に入力しま す. この部分のプログラムについては最終のプログラムを参照してください. 準備が整ったところでいよいよ前進操作を行います. ラベルのついたセル を一つ取り出して, その上下左右の隣接セルを調べて経路を延長するという 操作を繰り返すために, ラベルをつけたセルを管理するデータ構造が必要に なります. 今のところは集合 R と呼んでおきましょう. そうすると, 前進操作 は次のように記述できます. スタートセルにラベル 0 をつける; 集合 R をスタートセルだけからなる集合とする; while(R が空でない && ゴールセルのラベルが 0 である){ 集合 R から一つのセル c を取り出す; step = セル c のラベル; セル c のすべての隣接セル c' について if( セル c' の状態が 0 である ){ セル c' にラベル step+1 をつける; セル c' を集合 R に加える; } } これをプログラムとして実現するとき, 集合 R をどのようなデータ構造で 実現するかが問題となります. 集合 R に対して要求される操作は, 集合 R に セルを加えることと, 集合 R から一つの要素を取り出すことです. これ以外 の操作は必要ありませんから, スタックでもキューでも実現できることが分 かります. そこで, まずスタックを用いて集合 R を実現してみましょう. この例題ではセルの情報を記憶しなければなりません. 図からも, セルを x; y 座標で指定するのが最も自然でしょう. ここでは, 非常に大きな数, たと えば 10000 を用いて, (x; y ) という座標を x 10000 + y という一つの整数に 符号化(encode)してスタックに蓄えることにします. そうすると, (x; y ) と いう座標で指定されるセルをスタックに蓄える操作 insert(encode(x,y)) は, 3.3. 基本データタイプ 117 #define encode(x, y) ((x)*10000 + y) S.push(encode(x,y)); として実現できます. また, 集合 R から一つのセルを取り出す操作 PickCell() は, スタックからポップ操作で要素を取り出して, それを復号化 (decode) す ればいいので, #define DecodeX(place) ((place)/10000) #define DecodeY(place) ((place)%10000) place = S.pop(); x = DecodeX(place); y = DecodeY(place); として実現できます. セル c' にラベル step+1 をつける; セル c' を集合 R に加える; の部分は, セル c0 の座標を (x; y ) として, 次の関数 extend() によって実現で きます. void extend(int x, int y, int step, int xt, int yt) { status(x, y) = step+1; insert(encode(x,y)); } したがって, 前進操作に対応するプログラムは次のようになります. insert(encode(xs,ys)); while( !S.empty() && status(xt, yt)==FREE){ place = PickCell(); x = DecodeX(place); y = DecodeY(place); step = status(x,y); if(status(x,y-1)==FREE) extend(x, y-1, step, xt, yt); if(status(x,y+1)==FREE) extend(x, y+1, step, xt, yt); if(status(x-1,y)==FREE) extend(x-1, y, step, xt, yt); if(status(x+1,y)==FREE) extend(x+1, y, step, xt, yt); } 118 第3章 LEDA によるプログラミング 残るのは後進操作だけですが, 同じ考え方で実現できます. 今度は戻るべき 方向が分かれば直ぐに戻ればいいので, スタック操作は関係ありません. 具体 的には次の通りです. int x = xt; int y = yt; int step = do{ if(status(x,y-1)==step-1) else if(status(x,y+1)==step-1) else if(status(x-1,y)==step-1) else if(status(x+1,y)==step-1) step--; }while( x!=xs || y!= ys); status(xt, yt); y=y-1; y=y+1; x=x-1; x=x+1; これらのプログラム部分の他にグラフィック表示部分を付け加えると出来 上がりです. 少し長いですがプログラム全体を示します. //program 3-3-4.c: スタックを用いた経路探索プログラム #include <LEDA/window.h> #include <LEDA/array2.h> #define FREE -1 #define OBSTACLE 10000 #define Lgrid(x) ( 80.0*((x)-1)/11.0 + 10.5 ) #define Rgrid(x) ( 80.0*(x)/11.0 + 9.5 ) #define xcell(p) (((int) ((p.xcoord() - 10.5)*11/80.0+1))) #define ycell(p) (((int) ((p.ycoord() - 10.5)*11/80.0+1))) #define draw_cell(x,y,c) W.draw_box(Lgrid(x),Lgrid(y),Rgrid(x), Rgrid(y),c); array2 <int> status(0,10, 0, 10); window W (400, 400); void void void void initialize(); find_path(int, int, int, int); extend(int, int, int, int, int); backtrack(int, int, int, int); 3.3. 基本データタイプ //--------データ構造に依存する部分-----ここから-----#include <LEDA/stack.h> stack<int> S; #define insert(place) S.push(place) #define encode(x, y) ((x)*10000+(y)) #define DecodeX(place) ((place)/10000) #define DecodeY(place) ((place) % 10000) #define Is_empty() S.empty() int PickCell() { return S.pop(); } //%-------------------------------------ここまで-----int main() { int x, y, xs, ys, xt, yt; point p; W.display(); for(x=0; x<=10; x++) for(y=0; y<=10; y++) if(x%10==0 || y%10==0) status(x,y) = OBSTACLE; else status(x,y) = FREE; for(x=1; x<10; x++) for(y=1; y<10; y++) draw_cell(x, y, green); // 始点の設定 do{ cout << "始点をクリックしてください. " << endl; W >> p; xs = xcell(p); ys = ycell(p); } while( xs*(10-xs)<=0 || ys*(10-ys)<=0 ); status(xs, ys) = 0; draw_cell(xs, ys, blue); // 終点の設定 do{ 119 120 第3章 LEDA によるプログラミング cout << "終点をクリックしてください. " << endl; W >> p; xt = xcell(p); yt = ycell(p); } while( xt*(10-xt)<=0 || yt*(10-yt)<=0 ); draw_cell(xt, yt, red); } // 障害物の設定 do{ cout << "障害物をクリックしてください. " << endl; W >> p; x = xcell(p); y = ycell(p); if( x*(10-x)<=0 || y*(10-y)<=0 ) break; if( status(x, y) == FREE ) { status(x, y) = OBSTACLE; draw_cell(x, y, black); } }while( true ); find_path(xs, ys, xt, yt); backtrack(xs, ys, xt, yt); return 0; void find_path(int xs, int ys, int xt, int yt) { int x, y, step, place; } insert(encode(xs,ys)); while( !Is_empty() && status(xt, yt)==FREE){ place = PickCell(); x = DecodeX(place); y = DecodeY(place); step = status(x,y); if(status(x,y-1)==FREE) extend(x, y-1, step, if(status(x,y+1)==FREE) extend(x, y+1, step, if(status(x-1,y)==FREE) extend(x-1, y, step, if(status(x+1,y)==FREE) extend(x+1, y, step, W.read_mouse(); } void extend(int x, int y, int step, int xt, int yt) { status(x,y) = step+1; insert(encode(x,y)); xt, xt, xt, xt, yt); yt); yt); yt); 3.3. 基本データタイプ } 121 if(x!=xt || y!=yt) draw_cell(x,y,pink); void backtrack(int xs, int ys, int xt, int yt) { int x = xt; int y = yt; int step = status(xt, yt); do{ if(x!=xt || y!=yt) draw_cell(x, y, cyan); if(status(x,y-1)==step-1) y=y-1; else if(status(x,y+1)==step-1) y=y+1; else if(status(x-1,y)==step-1) x=x-1; else if(status(x+1,y)==step-1) x=x+1; step--; W.read_mouse(); }while( x!=xs || y!= ys); } 同じことがスタックの代わりにキューを用いてもできます. 変更すべきと ころはスタックに関する操作の部分だけです. 具体的には, //--------データ構造に依存する部分-----ここから-----#include <LEDA/stack.h> stack<int> S; #define insert(place) S.push(place) #define encode(x, y) ((x)*10000+(y)) #define DecodeX(place) ((place)/10000) #define DecodeY(place) ((place) % 10000) #define Is_empty() S.empty() int PickCell() { return S.pop(); } //%-------------------------------------ここまで-----という部分を, #include <LEDA/queue.h> queue<int> S; 122 #define #define #define #define #define 第3章 LEDA によるプログラミング insert(place) S.append(place) encode(x, y) ((x)*10000+(y)) DecodeX(place) ((place)/10000) DecodeY(place) ((place) % 10000) Is_empty() S.empty() int PickCell() { return S.pop(); } に置き換えればよいだけです. このようにスタックとキューのどちらを用いても経路探索は行えるのですが, 見つかる経路も同じというわけではありません. スタックは Last-In First-Out のメモリでしたから, 最後に見つけたセルの隣接セルを調べることになりま す. 隣接セルを上下左右の順に調べるものとすると, 最後に調べるのが右のセ ルですから, もしスタートセルから右のセルだけをたどってゴールセルの隣 接セルに到達できる状況なら, 非常に効率よく経路を求めることができます. ところが, 一つしか障害物がなくてもゴールセルがスタートセルの 2 つ上に あって, しかもゴールセルの右に障害物がある場合にはなかなか経路が見つ からないことになります. 図 3.10 に示したのはそのような 2 つの例を実行し たときの最終画面です. ただし, 図において S と T は始点と終点を, 濃い網掛 けの部分は障害物を, 薄い網掛けの部分は経路として見つかった部分を, さら に斜線を引いた部分はラベルづけされた部分をそれぞれ表しています. つま り, 斜線の部分は無駄に探索した部分だと言うこともできます. 一方, キューは First-In First-Out のメモリでしたから, 蓄えたのと同じ順 序でセルを取り出すことになります. したがって, まず 1 歩で行けるセルをす べて調べた後で 2 歩で行けるセルをすべて調べる, という風に進んで行きま すから, 必ず最短経路が見つかることになります. その反面, どんな場合でも 同心円状に波を広げて行きますから, ゴールまでの距離の 2 乗に比例する時 間がかかることになります. 図 3.11 にキューを用いて経路探索した場合の状 況を示します. 3.3. 基本データタイプ 123 T S T labeled cells S cells on the path found obstacles 図 3.10: スタックによる経路探索の実行例 演習問題 さい. 3.3.5 3.3.4 様々な状況を仮定してテキストのプログラムを実行してみな 優先順位つきキューによる経路探索 スタックとキューを用いた経路探索ではゴールセルの位置に関係なく探索 を行いましたが, ゴールに向かって探索を行った方が多くの場合には効率が 良くなるものと思われます. つまり, 集合 R からセルを取り出すとき, ゴール までの距離が最小のものを選ぶようにするのです. そのためには, セルを蓄え るとき, その座標だけではなく, ゴールまでの距離(の 2 乗)をキーとして優 先順位つきキューに蓄えればいいでしょう. したがって, セル (x; y ) を蓄える ときには, その座標を符号化したもの以外に, ゴール (xt; yt) までの距離を対 にして蓄えることにします. 具体的には, #define dist(x,y,xt,yt) ( ((x)-xt)*((x)-xt) + ((y)-yt)*((y)-yt)) によってゴールまでの距離を求めることにして, insert(dist(x,y,xt,yt), encode(x,y)); 124 第3章 S LEDA によるプログラミング T 図 3.11: キューによる経路探索の実行例 として優先順位つきキューに蓄えればいいのです. キューを用いた経路探索 についてはスタックの場合と異なる部分だけを記述しましたが, 優先順位つ きのキューを用いる場合は, その部分を次のように置きかえればいいのです. #include <LEDA/p_queue.h> p_queue<int, int> S; #define encode(x, y) ((x)*10000+(y)) #define dist(x,y,xt,yt) (((x)-xt)*((x)-xt)+((y)-yt)*((y)-yt)) #define DecodeX(place) ((place)/10000) #define DecodeY(place) ((place) % 10000) #define insert(d,place) S.insert((d), (place)) #define Is_empty() S.empty() int PickCell() { pq_item it = S.find_min(); S.del_item(it); return S.inf(it); } 図 3.12 は, 優先順位つきキューを用いて経路探索した場合の途中結果を計 算機の画面のままに表示したものです. 実際の画面では色の区別でスタート 点とゴール点が区別できるのですが, 同図では印刷の関係で識別不可能になっ ています. 実際には左上角がスタート点で, 右下角がゴール点です. 黒で示し 3.3. 基本データタイプ 125 たセルが障害物で, 薄い灰色で示したセルが探索されたセルです. ゴールに向 かって波が進んでいる様子が分かるでしょう. 図 3.12: 優先順位つきキューによる経路探索の実行例 演習問題 3.3.5 テキストのプログラムではゴールまでの距離だけに注目しま したが, スタートからの距離とゴールまでの距離の和をキーとするようにプ ログラムを変更しなさい. ただし, スタートからの距離は見つかっている経路 長, ゴールまでの距離は障害物を無視した最短距離とします. 3.3.6 連結リスト構造 最も簡単なデータ構造はデータを配列に順に詰めていくというものです. そ の場合, 配列の k 番目の次のデータは配列の k + 1 番目にあり, 直前のデータ は k 1 番目にあります. 配列の先頭と末尾の要素の位置は配列の宣言文から 知ることができます. これと同じ構造を別の形式で作ることができます. つ まり, 前後の要素がある場所を明示的に指定するのです. 配列の場合には要素 126 第3章 LEDA によるプログラミング を蓄えるだけの場所が確保されていればよかったのですが, この形式の場合 には前後の要素が格納されている場所を蓄えるための領域も必要になります. したがって, メモリ的には得策ではないのですが, 配列に比べて有利な点も多 く, 商用のプログラムでは多用されています. 上で説明したデータ構造はリスト構造と呼ばれるものです. 配列は箱を 1 列 に(2 次元配列は 2 次元的に)並べた形式で図示されるのが一般的ですが, リ スト構造の場合には, 図 3.13 に示すように, 箱をポインタと呼ばれる矢印でつ ないだ形式で図示されることが多いようです. データを蓄える箱と関連するポ インタを蓄える箱を一まとめにしたものを LEDA ではリスト項目 (list item) と呼んでいます. 前後の要素の場所を蓄えるための箱も陽に書いてそこから 前後の要素を蓄えている箱への矢印を書いておくと計算機内部での表現をイ メージしやすいと思います. ポインタは, 実際にはメモリに付けられた番地 (32 ビットの整数)によって表されます. L: head 5 L.front() 3 L.pred(it) 1 it 6 L.succ(it) 8 L.contents(it) or L[it] tail 2 L.tail() 図 3.13: 連結リスト構造 リスト構造の利点は, データの挿入と削除に関する操作に見ることができ ます. 配列にデータを格納している場合, ある要素を削除しようとすると, そ れ以降の要素をすべて一つ前の位置に移動させる必要があります. 特に先頭 の要素を削除する場合には残りの要素をすべて移動しないといけないので, 時 間がかかることになります. データの挿入についても同様のことが言えます. 配列の最後尾に新たな要素を挿入する場合は問題ありませんが, 途中に挿入 しようとすると, やはり多数の要素を移動しなければなりません. これに対し て, リスト構造の場合には, 挿入の場合にも削除の場合にも前後のポインタを 変更するという局所的な操作だけで済みます. 挿入と削除の操作を具体的に 説明すると次のようになります. 3.3. 基本データタイプ 127 新たなデータ x を挿入する場合, まず一つのリスト項目のためのメモリ領 域を確保して, 挿入すべき場所を求めます. 新たなリスト項目を it とすると き, そのデータ部に x を蓄えます. 次にポインタの付け替えを行います. リス ト項目 it1 と it2 の間に挿入すべきことが分かれば, it; it1 ; it2 のポインタを付 け替えて, it1 it it2 かつ it1 it it2 となるようにします (図 3.14 参照). ! ! head 5 tail 3 1 2 L.insert(x, it, LEDA::before) head 5 tail it 3 1 2 x 図 3.14: 連結リストへの新たなリスト項目の挿入 リスト項目 it を削除する場合も同様です. まず, it の前後へのポインタに よって it の前後のリスト項目 it1 ; it2 を求めます. 次に it1 の次が it2 で, it2 の前が it1 になるように it1 と it2 のポインタを付け替えれば it が削除できた ことになります (図 3.15 参照). 不要になったリスト項目はメモリから解放 するようにするとメモリの使用効率が良くなります. このように, データの挿入と削除は, 前後のリスト項目が存在する場合には, 上記のポインタ操作によって実現できます. しかし, リストの先頭や末尾では 前後のリスト項目のどちらかが存在しないことがありますから, 例外処理が 必要になり, 実際のプログラムは結構面倒なものです. LEDA では, リストを 操作する関数がすべて用意されていますから, プログラミングは非常に簡単 です. LEDA で int 型のデータを蓄えるリスト構造を宣言するには, 128 第3章 head L: 5 LEDA によるプログラミング it 3 1 6 8 tail 2 L.erase(it) head L: 5 it 3 6 8 図 3.15: リスト項目の削除 #include <LEDA/list.h> list<int> L; とします. もちろん, int 以外のどのような型でも構いません. このように宣 言すると, リスト L の先頭と末尾のリスト項目は, list_item L.first() list_item L.last() として参照できます. また, リスト項目 it の前後のリスト項目は, list_item L.pred(it) list_item L.succ(it) として参照します. どの場合も対応するリスト項目が存在しない場合には, nil という値を取ります. ここまではデータ部とポインタ部からなるリスト項目に関する操作につい て説明してきましたが, リスト項目 it のデータ部は, int L.contents(it) として参照することになります. あるいは, 簡単に, x = L[it]; または L[it] = x; 3.3. 基本データタイプ 129 のようにアクセスすることもできます. 先頭と末尾のリスト項目については, int L.front() int L.back() または または int L.head() int L.tail() によって直接アクセスすることができます. 挿入に関しては, リストの先頭または最後尾に挿入するのが一般的です. データ x を含むリスト項目を先頭に挿入するには, list_item L.push(x) list_item L.push_front(x) または とします. 最後尾に追加する場合には, list_item L.append(x) または list_item L.push_back(x) とします. リストの途中に挿入することもできます. リスト項目 it の前後に x を含む新たなリスト項目を挿入することができます. その場合, 前後どちら の方向に挿入するかも指定します(図 3.16 参照). 具体的には次の通りです. L.insert(x, it, LEDA::before) または L.insert(x, it, LEDA::after) L: head 5 3 1 6 8 tail 2 y x L.append(y) L.push(x) 図 3.16: リストの先頭と末尾への挿入 削除に関しても同様の操作が可能です. リストの先頭にあるリスト項目を 削除して, そのデータ部の値を返すには int x = L.pop() または int x = L.pop_front() 130 第3章 LEDA によるプログラミング のように使います. リストの最後尾にあるリスト項目を削除して, そのデータ 部の値を返すには int x = L.Pop() または int x = L.pop_back() とします. 特定のリスト項目 it を削除することもできます. 単に削除するだ けなら, L.erase(it) としますが, 削除されるリスト項目のデータ部の値を返したい場合には, int x = L.del_item(it) または int x = L.del(it) とします. その他にも, データ部が x であるリスト項目をすべて削除したり, リストを 2 つのリストに分割したり, リストを逆順にしたりすることもできます. 詳細 については LEDA Book を参照してください. データ部の値が x に等しいリスト項目が存在するかどうかを調べて, もし 存在すれば, そのリスト項目を削除するというのがリスト構造を扱う場合の 基本的な処理です. そのためには, リストを先頭から順にたどって, データ部 が x に等しいリスト項目が見つければ, それを削除すればいいので, 次のよう に実現することができます. list_item it = L.first(); while(it != nil){ cout << L[it] << endl; if(L[it] == x) break; it = L.succ(it); } if(it != nil) L.del_item(it); else cout << "No such element." << endl; このように次へのポインタをたどる操作はどのテキストにも詳しく説明さ れている基本的なものですが, LEDA ではデータ部が x に等しいリスト項目 が存在するかどうかを探索するための関数が用意されています. 3.3. 基本データタイプ 131 L.search(x) によって, データ部が x に等しいリスト項目が存在すれば, そのリスト項目を 返し, 存在しなければ nil を返します. したがって, データ部が x に等しいリ スト項目が存在すればそれを削除するには, 単に L.del_item(L.search(x)) とすればいいのです. さらに, この方法ではデータ部が x に等しい最初のリス ト項目が削除されますが, データ部が x に等しいリスト項目をすべて削除す るという関数 L.remove(x) も用意されています. 次へのポインタをたどってリスト項目を順に調べるというのは非常に基本 的な処理ですが, LEDA ではポインタを意識しなくても, forall_items(it, L){ } とすれば, リスト L のリスト項目を順に調べることができます. また, forall(x, L){ } とすれば, リスト L のリスト項目のデータ部を順に列挙することができます. したがって, リスト L に蓄えられているデータを順に出力するには, forall_items(it, L) cout << " " << L[it]; または, もっと簡単に forall(x, L) cout << " " << x; とすればいいのです. 更に, もっと簡単に同じことが L.print(cout, ' '); 132 第3章 LEDA によるプログラミング としても実現できます. ここで, L.print() では, 最初に出力先を指定し, 次に 区切り記号を文字型で宣言します. 最後に例題として, 0 が入力されるまで整数データを入力して, それらをリ ストの最後尾に挿入することによってリスト L1 を構成し, 次には \end" が入 力されるまで文字列を入力して, 毎回リスト L2 の先頭に挿入していくプログ ラムについて考えてみましょう. //program 3-3-5.c: リスト構造の例題 #include <LEDA/list.h> int main() { list<int> L1; list<string> L2; int x; while( true ){ cin >> x; if(x == 0) break; L1.append(x); } } string s; while( true ){ cin >> s; if(s == "end") break; L2.push_front(s); } cout << endl<< "リスト L1 の内容"; L1.print(cout, '\n'); cout << endl << "リスト L2 の内容"; L2.print(cout, '\n'); return 0; 3.3. 基本データタイプ 133 プログラム 3-3-5.c の実行例 Æ > 3-3-5 1 2 3 0 tetsuo koji stefan kurt end リスト L1 の内容 1 2 3 リスト L2 の内容 kurt stefan koji tetsuo 演習問題 3.3.6 多数の整数を入力し, 同じ数が並んでいれば後のものを削除 して入力と同じ順で出力するプログラムを連結リストを使って作りなさい. 演習問題 3.3.7 名前と電話番号を対にしたデータを連結リストに読み込んで おいて, 名前が入力されると電話番号を答えるプログラムを作りなさい. 3.3.7 一方向連結リスト 上で説明したのは各リスト項目が前後のリスト項目へのポインタをもって いる双方向の連結リストでしたが, メモリを節約したい場合には後へのポイ ンタだけをもったリストを用いることもできます. これを LEDA では一方向 連結リスト slist(singly linked list)と呼んでいます(図 3.17 参照). 宣言の違い #include <LEDA/list.h> list<int> L; 双方向 #include <LEDA/slist.h> slist<int> L; 一方向 134 第3章 LEDA によるプログラミング 一方向のリストであっても双方向リストとほぼ同じ操作が可能ですが, 最 大の違いは要素の削除にあります. 双方向の場合には前後のリスト項目を求 めて, ポインタをつなぎ換えればいいのですが, 一方向の場合には前の項目へ のポインタがないので, 双方向の場合のようにはいきません. もっとも原理的 に削除が不可能だというわけではなく, プログラムの中でリストをたどりな がら常に前の項目へのポインタを管理しておけば削除も可能です. あるいは, 次の項目を削除することは一方向でも可能です. 実際, 次の項目を削除する関 数は用意されています. head 5 L.first() tail 3 1 2 L.last() 図 3.17: 一方向連結リスト 双方向連結リストでも単方向連結リストでも使える関数: L.length() L の長さ L.size() L.length() と同じ L.empty() L が空かどうか L.first() L の先頭のリスト項目 L.last() L の最後尾のリスト項目 L.succ(it) リスト項目 it の次のリスト項目 L.contents(it) リスト項目 it のデータ部の値 L[it] 同上 L.head() L の先頭のリスト項目のデータ部の値 L.tail() L の末尾のリスト項目のデータ部の値 L.push(x) L の先頭にデータ部が x のリスト項目を挿入 L.append(x) L の最後尾にデータ部が x のリスト項目を挿入 L.pop() L の先頭のリスト項目を削除し, そのデータ部の値を返す 双方向連結リストでは使えるが, 単方向連結リストでは使えない関数: L.pred(it) リスト項目 it の直前のリスト項目 L.push_front(x) L の先頭にデータ部が x のリスト項目を挿入 L.push_back(x) L の最後尾にデータ部が x のリスト項目を挿入 L.insert(x,it,dir) リスト項目 it の前後どちらかの方向 dir に x を挿入 3.3. 基本データタイプ L.pop_front() L.pop_back() L.del_item(it) L.erase(it) L.remove(x) L.search(x) L.print() 135 の先頭のリスト項目を削除し, そのデータ部の値を返す の末尾のリスト項目を削除し, そのデータ部の値を返す リスト項目 it を削除し, そのデータ部の値を返す リスト項目 it を削除 データ部の値が x であるリスト項目をすべて削除 データ部が x であるリスト項目を探し, そこへのポインタを返す リストの内容の出力 L L 単方向連結リストだけで使える関数: L.del_succ_item(it) リスト項目 it の次のリスト項目を削除 L.insert(x, it) リスト項目 it の後にデータ部が x のリスト項目を挿入 先に説明しましたスタックとキューでは途中のデータにアクセスすること がありませんから, 単方向連結リストでうまく表現することができます. 実際, LEDA では単方向連結リストを用いてスタックとキューを実現しています. 演習問題 3.3.8 双方向連結リストを用いた例題 program 3-3-5.c を一方向 連結リストを用いて書きなおせるか試しなさい. 137 第 4 章 グラフとデータ構造 4.1 グラフとは アルゴリズムのことを知らない人でも, 平面グラフの 4 色定理については新聞 や本で読んだことがあるのではないでしょうか. これは, 地図の上で隣接する 国を区別するために, 隣接する国には必ず異なる色で塗り分けるとしたとき, 実は 4 色で十分だという定理のことです. この定理は長年未解決でしたが, 計 算機を駆使した証明によって遂に肯定的に解決されて世間を騒がしたことを ご記憶の読者も多いことでしょう. したがって, 理論的には存在証明があるの ですが, 実際に白地図を与えて, 4 色だけで条件を満たすように彩色してほし い, と言われると, なかなか難しいものです. 計算機に上記の問題を解かせようとすると, まず問題を抽象的に定式化す る必要があります. この問題の場合にはグラフを用いるのが最も適切でしょ う. 問題は隣接する国に異なる色を割り当てないといけないのですが, 国の形 がどのようなものであっても関係はないわけです. そこで, 国を 1 つの点で 表し, 2 つの国が隣接しているときには対応する 2 点間を線で結ぶことにしま す. すると一つの図形ができますが, この図形をグラフと言います. 実は, う まく線を引くと, このグラフは必ず平面に描くことができます. つまり, どの 2 つの線も端点以外で交差しないように描くことができるのです. 逆に, うま く線を引けば, 交差しないように描くことができるような隣接関係を表した グラフのことを平面的グラフと言います. 先の彩色問題は, 実は平面的グラフの点に彩色を施す問題だと言うことが できます. 制約は, 線で結ばれている 2 点には必ず違う色を彩色しないといけ ないというものです. 4 色定理では 4 色だけで十分だと保証しているのです が, どのように 4 色を割り当てるべきかを求めるのは難しい問題です. しか 138 第4章 グラフとデータ構造 17 18 2 10 1 3 16 19 9 11 4 15 14 5 8 12 6 7 13 図 4.1: 地図の例 し, 5 色を使ってもよいということになると, 効率の良いアルゴリズムが知ら れています. ただ, そのアルゴリズムを使って彩色しようとすると, そのアル ゴリズムを勉強して, 細部に至るまで動作を理解した上でプログラム化する 必要があります. グラフ理論を勉強したことがない人にとって, これは大変な ことです. 17 18 2 10 1 3 16 19 9 11 4 14 5 15 8 12 6 7 13 図 4.2: 国の隣接関係を表すグラフ もっと難しい問題があります. 上に平面的グラフを定義しましたが, 今度は 任意にグラフを与えて, それが平面的かどうかを問うのです. 分かりやすく言 うと, 点には 1 から n までの番号がついているものとします. グラフを指定す るには, どの点とどの点が隣接するかを指定すればいいわけですから, n n 4.1. グラフとは 139 の行列で隣接関係を表すことができます. つまり, i 番目の点と j 番目の点が 隣接するなら, 行列の (i; j ) および (j; i) 要素を 1 とし, そうでなければ 0 とす るように決めればいいわけです. このような行列が与えられて, その行列が表 すグラフが平面的かどうかを判定しなければならないわけです. 人間なら, 紙 の上に適当に点を並べて, 適当に番号をつけ, 行列の値に従って結ぶべき 2 点 間に線を引いて行きます. 最後まで交差なく線が引けたら, 平面的だと言うこ とができます. でも, 少し隣接関係が複雑になると, 本当は交差なく引けるの に, その方法を見つけるのが非常に難しくなります. 図 4.3 の例を見てくださ い. 適当に点を並べて単純に直線で結んだために, 交差が生じていますが, う まく点の位置を変えると交差をなくすことができます. あなたはできますか? 4 7 12 5 11 9 0 13 6 10 3 8 1 2 図 4.3: このグラフは平面的か? 上に述べた問題は, グラフの平面性判定問題と言って, グラフ理論のテキス トなら必ず載っている重要な問題です. グラフ理論の方では, グラフが平面的 であるための必要十分条件が求められていますが, その条件を確かめるという 形でプログラムを書くことはかなり困難です. そこで, アルゴリズムの分野で は, 如何にして効率良く平面性を判定するかが研究されてきました. 幸いなこ 140 第4章 グラフとデータ構造 とに, 点の個数に比例する時間で平面性を判定するアルゴリズムが Hopcroft と Tarjan によって 1974 年に求められていますので, アルゴリズム理論とし ては解決がついているのですが, 実際にそのアルゴリズムをプログラムの形 に実現するのは, 余程グラフとアルゴリズムの知識を持ち合わせていないと 難しいでしょう. 事実, アルゴリズムの分野では平面性判定に関しては幾つか の方法が提案されていますが, やはりプログラムを組むとなると, 相当の覚悟 が必要になります. LEDA にはグラフの平面性を判定するプログラムが組み込まれているだけ ではなく, 平面的であるグラフを実際に平面上に, すなわち辺が交差しない ように描画する関数が組み込まれています. たとえば, デモ用のプログラム gw plan demo を実行しますと, グラフ作成用のウィンドウが開かれて, そこで グラフを作成・編集することができます. このグラフを平面描画するメニュー を選んで実行すると図 4.4 に示す結果が得られます. このように, LEDA はプ ログラムを作成するツールであると同時に, 論文を書く際の作図の道具とし ても使えるのです. 出力結果を ps ファイルとして出力することも簡単です. 実際図 4.4 もその機能を使ったものです. 8 2 11 13 5 12 7 4 3 10 6 1 0 9 図 4.4: 図 4.3 のグラフの平面描画 4.1. グラフとは 141 0 8 9 5 11 4 4 10 7 6 10 7 6 12 12 3 2 図 4.5: 別のランダムグラフ 1 3 2 1 図 4.6: Kuratowski 部分グラフ では, 図 4.5 に示したようなグラフはどうでしょう. これは, メインメニュー Layout のサブメニューにある Simple Layout の中の circular layout を選択し て, グラフの節点を円周上に配置した後, マウスで節点を適当に移動したもの です. 平面的に描画するメニューを選ぶと, 今度はグラフは平面的ではないと いう出力があり, さらに証明をするかどうかが尋ねられます. ここで証明する 方を選択すると, 図 4.6 のような結果が示され, このグラフに Kuratowski グ ラフ(この場合は, K3;3 )が部分グラフとして含まれていることが陽に示され ます. ここにも LEDA の基本思想が現れています. 求められているのは, グラフ の平面性判定であったとしても, 出力が平面的かどうかだけであれば, 出力の 正しさを証明することは非常に難しいわけですが, 問題を少し変更して, 「与 えられたグラフが平面的かどうかを判定し, もし平面的なら実際に平面上に 描画し, そうでなければ Kuratowski グラフが部分グラフとして含まれること を示せ」とすると, 今度はどちらの結果になっても出力の正しさを検証する のは簡単です. このように, 出力の正しさを検証できる形で問題を設定するこ とは非常に重要です. 142 第4章 グラフとデータ構造 グラフウィンドウ 4.2 4.2.1 グラフを作成 上ではデモプログラムを用いてグラフを作って, それが平面的グラフかど うかを判定しましたが, 同じことが非常に簡単なプログラムでできます. 第 2 章で図形を描くためのウィンドウについて説明しましたが, LEDA にはグラ フを扱うための強力なグラフィック・ユーザ・インタフェースが用意されて います. グラフ用のウィンドウを開くには, 以前と同様に #include <LEDA/graphwin.h> GraphWin gw("LEDA Graph Editor"); gw.display(); とするだけでいいのです. 以前と同様にウィンドウには任意のタイトルをつ けることができますが, ここでは \LEDA Graph Editor" という名前をつけ ています. 日本語のタイトルをつけることは難しいようです. さて, ウィンドウを開いたら, そのウィンドウ上でグラフを新たに作ったり, 様々な編集作業を行うために gw.edit() という関数を呼び出します. これでグ ラフを扱う最も簡単なプログラムができました. 下に示したのがそのプログ ラムです. //program 4-1-1.c: グラフを扱う最も簡単なプログラム #include <LEDA/graphwin.h> int main() { GraphWin gw("LEDA Graph Editor"); gw.display(window::center, window::center); // ウィンドウを画面の中央に表示 gw.edit(); return 0; } ただし, このプログラムではウィンドウを画面の中央に表示するようにし ています. この 3 行だけのプログラムで実に様々なことができます. まず, こ 4.2. グラフウィンドウ 143 のプログラムを実行すると, 図 4.7 に示したようなウィンドウが画面の中央に 現れます. このウィンドウの上部には幾つかの項目からなるパネルがありま す. このパネルの内容はプログラムで変更することも可能ですが, 特に何も指 定しなければ, この通りのパネルが使われます. 図 4.7: グラフウィンドウ パネルの説明は後回しにして, とりあえずウィンドウ上でグラフを作って みましょう. ウィンドウ上の任意の場所でマウスの左ボタンをクリックする と, その場所に小さな円が描かれます. これがグラフの節点です. 節点には入 力順に 0 から始まる番号がつけられます. 節点を入力し終わったら, 今度は辺 を入力します. 節点 3 から節点 5 に辺を引きたいときは, 節点 3 の内部の 1 点 をマウスの左ボタンでクリックします. そうすると円の内部が塗りつぶされ 144 第4章 グラフとデータ構造 図 4.8: Options のサブメニュー 図 4.9: Edge Defaults のサブメニュー ます. その後で節点 5 の円に向けてマウスを移動します. 節点 5 に届いたと ころで再びボタンをクリックすると, 節点 3 から 5 への辺が引かれます. 後は 同じことを繰り返すだけです. デフォールトの状態では辺には向きがついています. すべてを無向辺にした いときには主パネルの Options を選びます. そうすると図 4.8 に示すメニュー が表示されますから, そこで Edge Defaults を選びますと, 図 4.9 のウィンド ウが表示されますから, その上で辺をどのように表示するかを細かく指定す ることができます. 特に説明しなくても大体見当はつくと思いますが, 無向辺 にしたいときは, arrow のところで矢印のないボタンを選んで, 最後に apply のボタンを押せばウィンドウ上の矢印つきの辺がすべて矢印のない無向辺に 置き換わります. ここでは辺の種類だけを変更しましたが, 辺の太さや色など, 多数のオプ ションがあり, その中から適当なものを選ぶことができるようになっていま 4.2. グラフウィンドウ 145 す. 同じ Options の中にある Node Defaults を選びますと, グラフの節点の表 現について様々な選択ができるようになっています. さて, このようにして作ったグラフに名前をつけて蓄えておくことができま す. 主パネルの File を選択すると, メニューが現れますから, その中の Save を 選べばいいのです. LEDA では 3 通りの形式でグラフを記録することができ るようになっています. LEDA で最も一般的に使われるのは gw 形式です. 形 式を選べば, 後はファイル名を指定するだけです. このとき, ファイルの拡張 子は.gw とします. File のサブメニューには Load がありますから, もちろん, このようにしてセーブしたグラフをロードすることもできます. また, Print というメニューがありますが, これは現在のグラフを直接印刷するためのも のです. 直接印刷するのではなく, ポストスクリプトファイルとして蓄えるこ ともできます. Export を選べば, そこで Postscript を選ぶことができます. 上で説明した以外にも便利な機能が色々と備わっています. たとえば, 誤っ て左ボタンをクリックしてしまうと新たな節点ができてしまいますが, その 場合には右ボタンでその節点をクリックすれば, メニューが現れます. その中 から delete を左ボタンで選択すれば節点が消去されます. 節点を画面上で移 動したいときには, 移動すべき節点をドラッグします. そうすると, マウスに 合わせて節点が移動します. このとき, 移動節点に接続している辺もその動 きに合わせて変形されます. さらに, 同じことを SHIFT キーを押した状態で 行いますと, マウスで指定された節点を含むグラフの連結成分全体を移動す ることができます. また, 節点や辺をダブルクリックすると, 指定した節点ま たは辺の属性を変更することができます. たとえば, ある辺を選択して, その shape 属性を直線を意味する poly から spline や bezier に変更すると, 曲線の 辺にすることができます. 辺の属性を変更しただけでは何の変化もありませ んが, もう一度辺を左ボタンでドラッグすると, それにつれて辺の形状が変化 します. 4.2.2 グラフウィンドウのメニュー グラフウィンドウには上で説明した File 以外にも幾つかのメニューがあり ます. それらの機能を簡単に紹介しましょう. 146 第4章 グラフとデータ構造 File グラフの入出力に関連するメニューを含んでいます. グラフに名前をつ けてセーブしたり, 名前を指定してファイルからグラフをロードするこ とができます. また, 現在のグラフを印刷したり, ポストスクリプトの 形式でファイルに格納することもできます. Exit ボタンを押せば処理 が終了します. Edit グラフの節点や辺の属性を設定するためのメニューを含んでいます. 節 点の形や色などを変更したり, 辺に方向をつけるかどうかの選択をした りするためのメニューが含まれています. すべての辺について同じ処理 を施す場合には, Defaults のオプションを選びますが, 一つ一つの節点 や辺について編集作業を行う場合には先にマウスの右ボタンを用いて 対象の節点や辺を選んでおきます. たとえば, 節点を選ぶ場合には, そ の節点の場所にマウスを移動して右ボタンをクリックするのです. そう すると, setup, label, select, delete というメニューが表示されますから, 削除したければ, delete をマウスの左ボタンで選択すればいいわけです. 辺の方向を変えたり, 節点のラベルを変えたりすることもできます. Graph グラフを新たに生成したり, 修正を施したり, あるいは平面性などを 判定したりするためのメニューを含んでいます. Clear のボタンを押す と現在のグラフが消去されてしまいますから注意してください. Create のボタンを押すと, 完全グラフやランダムグラフを生成することができ ます. たとえば, random graph を選択すると, 次に節点数と辺数を指定 するためのパネルが現れますから, インタラクティブにグラフを生成で きます. random planar graph を選択すると, ランダムな平面的グラフ が生成されます. また, Test のメニューを選ぶと, 現在のグラフの連結 性や平面性などを判定することができます. Layout グラフの描画に関連するメニューを含んでいます. たとえば, 一番上 の Layout Tools を選ぶと, 節点の位置をグリッドにフィットさせたり, グラフ全体を拡大・縮小することができます. その他にも, 平面的グラ フを実際に平面に描画するための幾つかのアルゴリズムを呼び出すこ ともできるようになっています. 4.3. 基礎知識 Window 147 主にウィンドウのズームに関連するメニューを含んでいます. Options ウィンドウに関する属性を編集するためのメニューを含んでいま す. ここにもポストスクリプトのメニューがありますが, ここではフォ ントを選んだり, 拡大率を指定するなど, 細かい指定が可能です. もち ろん, 最初に説明した節点や辺の属性の変更もここでできます. Help マウスボタンの役割についての簡単な説明があります. done このボタンを押すと処理を終了します. 4.3 4.3.1 基礎知識 グラフの定義 グラフアルゴリズムはアルゴリズムの分野でも中心的な存在ですが, 理論 と実際が最もかけ離れていると感じている人が多いことも事実です. LEDA では, グラフを簡便に扱えるように, 様々なデータ構造や制御構造を用意して います. 0 1 3 2 図 4.10: グラフ 4-3-1.gw 図 4.10 に 4 つの節点と 4 本の辺からなる簡単なグラフが示されていますが, このグラフを定義してみましょう. まず, 148 第4章 グラフとデータ構造 グラフ G の宣言 (1) graph G; として, G という名前のグラフを構成します. 節点を表すのに v という名前の 変数を用い, 辺を表すのに e という名前の変数を用いるときには, 節点 v の宣言 辺 e の宣言 (2) node v; (3) edge e; と宣言します. 新たな節点や辺を生成するのも簡単です. 単に, (4) node v = G.new node(); (5) edge e = G.new edge(u, v); グラフ G の節点 v を生成 グラフ G の節点 u; v の間の辺 e の 生成 のようにすればいいのです. グラフ G に 4 つの節点を新たに作り, これらの節点の間の辺を同様に定義 すれば終りです. これを LEDA で記述すると次のようになります. graph G; node v0 = G.new_node(); node v1 = G.new_node(); node v2 = G.new_node(); node v3 = G.new_node(); edge e01 = G.new_edge(v0, edge e02 = G.new_edge(v0, edge e12 = G.new_edge(v1, edge e13 = G.new_edge(v1, v1); v2); v2); v3); このようにグラフを定義すれば, 各節点について, そこに接続する辺に関する 情報や, 各辺について, その始点と終点に関する情報も自然に計算されていま す. これらをまとめると以下のようになります. (6) G.outdeg(v) (7) G.indeg(v) (8) G.degree(v) 節点 v から出る辺の本数(出次数) 節点 v に入る辺の本数(入次数) 節点 v に接続する辺の本数(出次 数 + 入次数) 4.3. 基礎知識 149 辺 e の始点 辺 e の終点 辺 e の端点のうち, v でない方 グラフ G の節点数 グラフ G の辺数 グラフ G でランダムに選んだ節点 を返す グラフ G は空か (9) G.source(e) (10) G.target(e) (11) G.opposite(v, e) (12) G.number of nodes() (13) G.number of edges() (14) G.choose node() (15) G.empty() たとえば, グラフを定義してみて, 確かにグラフの節点数と辺数が正しく求 められること, および各節点の出次数が正しく求められることを次のプログ ラムで確かめてみましょう. 節点 v 0 に関しては, v 1 と v 2 への辺が出ていま すから, v 0 の出次数は 2 となります. //program 4-3-1.c: 簡単なグラフの定義 #include <LEDA/graph.h> int main() { graph G; node v0 = G.new_node(); node v1 = G.new_node(); node v2 = G.new_node(); node v3 = G.new_node(); edge e01 = G.new_edge(v0, edge e02 = G.new_edge(v0, edge e12 = G.new_edge(v1, edge e13 = G.new_edge(v1, } cout << "number of cout << "number of cout << "outdegree cout << "outdegree cout << "outdegree cout << "outdegree return 0; nodes edges of v0 of v1 of v2 of v3 = = = = = = v1); v2); v2); v3); " " " " " " << << << << << << G.number_of_nodes() << endl; G.number_of_edges() << endl; G.outdeg(v0) << endl; G.outdeg(v1) << endl; G.outdeg(v2) << endl; G.outdeg(v3) << endl; 150 第4章 プログラム 4-3-1.c の実行例 > 4-3-1 number of number of outdegree outdegree outdegree outdegree nodes edges of v0 of v1 of v2 of v3 = = = = = = 4 4 2 2 0 0 グラフとデータ構造 グラフの節点や辺に関する情報だけでなく, 節点や辺に関する多様な操作 もサポートされています. (16) G.del node(v) (17) (18) (19) (20) G.del edge(e) G.rev edge(e) G.make undirected() G.make directed() 節点 v を削除(v に接続する辺も 含めて) 辺 e を削除 辺 e の方向を逆にする グラフ G を無向グラフにする グラフ G の各辺を双方向にして有 向グラフにする 上では単純な変数を用いてグラフを表現しましたが, 節点と辺の配列を用 いることもできます. 具体的には, node v[SIZE1]; edge e[SIZE2]; とすれば, それぞれ SIZE1 と SIZE2 という大きさをもつ節点の配列 v と辺の 配列 e を構成することができ, 普通の配列と同様にインデックスを用いて任意 の節点と辺を参照することができます. ただし, 注意をしておかないといけな いのは, LEDA の array と違って, この場合には配列のサイズを定数で指定し なければなりません. そのために, 実際のプログラムでは配列として節点や辺 を蓄えるのではなく, サイズを指定しなくてよいリスト構造を用いることが 一般的です. このような配列を用いると, program4-3-1.c を次のように書きかえること ができます. 4.3. 基礎知識 151 //program 4-3-2.c: 簡単なグラフの定義 (2) #include <LEDA/graph.h> int main() { graph G; node v[4]; for(int i=0; i<4; i++) v[i] = G.new_node(); edge e[0] e[1] e[2] e[3] } e[4]; = G.new_edge(v[0], = G.new_edge(v[0], = G.new_edge(v[1], = G.new_edge(v[1], v[1]); v[2]); v[2]); v[3]); cout << "number of nodes = " << G.number_of_nodes() << endl; cout << "number of edges = " << G.number_of_edges() << endl; for(int i=0; i<4; i++) cout << i << "v の出次数 = " << G.outdeg(v[i]) << endl; return 0; この考え方をさらに発展させて, 節点数, 辺数, 辺の始点と終点の情報を格 納したデータファイルからこれらのデータを読み込んでグラフを構成するこ ともできます. たとえば, 4 0 0 1 1 4 1 2 2 3 データファイル graph.dat の内容 としてデータファイル graph.dat を定めておくと, 次のプログラムで上と同じ ことができます. ただし, 先頭の2つの数値はそれぞれ節点数と辺数を表し, 2行目以降の数値は各辺の始点と終点の節点番号を表しています. また節点 と辺の数はデータファイルを読み込むまで分からないので, 配列の代わりに LEDA の array を使っています. 152 第4章 グラフとデータ構造 //program 4-3-3.c: 簡単なグラフの定義 (3) #include <LEDA/array.h> #include <LEDA/graph.h> int main() { ifstream fin ("graph.dat"); int n, m, i, p, q; fin >> n >> m; graph G; array<node> v(n); for(i=0; i<n; i++) v[i] = G.new_node(); array<edge> e(m); for(i=0; i<m; i++){ fin >> p >> q; e[i] = G.new_edge(v[p], v[q]); } } for(i=0; i<n; i++) cout << i << "の出次数 = " << G.outdeg(v[i]) << endl; return 0; 4.3.2 便利な繰り返し構造 グラフを扱うアルゴリズムでは, \すべての節点について以下の操作を繰り 返せ" とか, \すべての辺について繰り返せ" というような制御構造が頻繁に 使われますが, LEDA はそのための多様な制御構造を提供しています. たと えば, すべての節点やすべての辺について一連の処理を繰り返し実行するよ うな場合に便利な for ループが定義されています. (21) forall nodes(v, G) f...g (22) forall edges(e, G) f...g (23) forall adj edges(e, w) f...g グラフ G のすべての節点 v につ いて グラフ G のすべての辺 e について 節点 w に接続するすべての辺 e について 4.3. 基礎知識 153 (24) forall out edges(e, w) f...g 節点 w から出るすべての辺 e に (25) forall in edges(e, w) f...g (26) forall adj nodes(v, w) f...g ついて 節点 w に入るすべての辺 e につ いて 節点 w に隣接するすべての節点 v について このような繰り返し文を用いると, 上のグラフですべての節点についてそ の出次数を出力する部分は次のように書けます. forall_nodes(w, G) cout << G.outdeg(w) << " "; 4.3.3 節点配列と辺配列 グラフの節点や辺にはラベルをつけたり, 数値を付加したりすることがで きます. たとえば, 節点には文字列のラベルをつけ, 辺には整数を割り当てる には, 節点と辺をインデックスとする配列を宣言することによって, 次のよう にします. //program 4-3-4.c: 簡単なグラフの定義 (4) #include <LEDA/graph.h> int main() { graph G; node v0 = G.new_node(); node v1 = G.new_node(); node v2 = G.new_node(); node v3 = G.new_node(); node_array<string> name(G); name[v0] = "Tetsuo"; name[v1] = "Koji"; name[v2] = "Kurt"; name[v3] = "Stefan"; edge e01 = G.new_edge(v0, v1); edge e02 = G.new_edge(v0, v2); edge e12 = G.new_edge(v1, v2); 154 第4章 グラフとデータ構造 edge e13 = G.new_edge(v1, v3); edge_array<int> length(G); length[e01] = 13; length[e02] = 24; length[e12] = 36; length[e13] = 48; } edge e; forall_edges(e, G){ cout << "edge (" << name[G.source(e)] << ", "; cout << name[G.target(e)] << ")"; cout << " length = " << length[e] << endl; } return 0; プログラム 4-3-4.c の実行例 > 4-3-4 edge (Tetsuo, Koji) length edge (Tetsuo, Kurt) length edge (Koji, Kurt) length = edge (Koji, Stefan) length = 13 = 24 36 = 48 このような節点や辺に対する配列を宣言するとき, 宣言部より前に節点集合 や辺集合が確定していなければなりません. 4.3.4 パラメータつきグラフ 同じことが, パラメータつきのグラフ (parameterized graph) の概念を用い て行えます. 上記の場合には, 節点に対しては文字列, 辺に対しては整数を割 り当てましたから, (27) GRAPH<string, int> H; パラメータつきのグラフ H を宣言 と宣言します. そうすると, 節点 v と辺 e に対して, (28) H[v] = "Tetsuo"; グラフ H の節点 v の情報に"Tetsuo" を代入 4.3. 基礎知識 155 グラフ H の辺 e の情報に 48 を代 入 (29) H[e] = 48; のようにアクセスすることができます. この表現を用いて先のプログラムを書きなおしてみましょう. 宣言部がすっ きりして, 幾分読みやすいのではないでしょうか. //program 4-3-5.c: 簡単なグラフの定義 (5) #include <LEDA/graph.h> int main() { GRAPH<string,int> H; node v0 = H.new_node(); node v2 = H.new_node(); H[v0] = "Tetsuo"; H[v1] H[v2] = "Kurt"; H[v3] node v1 = H.new_node(); node v3 = H.new_node(); = "Koji"; = "Stefan"; edge e01 = H.new_edge(v0, v1); edge e02 = H.new_edge(v0, v2); edge e12 = H.new_edge(v1, v2); edge e13 = H.new_edge(v1, v3); H[e01] = 13; H[e02] = 24; H[e12] = 36; H[e13] = 48; } edge e; forall_edges(e, H){ cout << "edge (" << H[H.source(e)] << ", "; cout << H[H.target(e)] << ")"; cout << " H = " << H[e] << endl; } return 0; プログラム 4-3-5.c の実行例 > 4-3-5 edge (Tetsuo, Koji) edge (Tetsuo, Kurt) edge (Koji, Kurt) H edge (Koji, Stefan) H H = H = 13 = 24 36 = 48 このプログラムにおいて指定されているグラフを示したのが図 4.11 です. 156 第4章 Tetsuo グラフとデータ構造 Koji 13 v0 24 v1 36 v2 Kurt 48 v3 Stefan 図 4.11: プログラム 4-3-5.c の中のグラフ 4.3.5 ファイル入力 プログラムの中で構成したグラフをファイルに格納したり, ファイルに格 納されているグラフをロードしたりするのも簡単です. 具体的には, write と read で入出力を行います. (30) G.write(ファイル) (31) G.read(ファイル) 指定したファイルにグラフを出力 指定したファイルからグラフを入 力 最初に定義した簡単なグラフをファイルに出力してみましょう. プログラ ム 4-3-1.c の最後にファイル出力のための 1 行を付け加えただけのプログラム です. //program 4-3-6.c: グラフのセーブ #include <LEDA/graph.h> int main() { graph G; node v0 = G.new_node(); node v1 = G.new_node(); node v2 = G.new_node(); node v3 = G.new_node(); edge e01 = G.new_edge(v0, v1); 4.3. 基礎知識 157 LEDA.GRAPH void void 4 |{}| |{}| |{}| |{}| 4 1 2 0 |{}| 1 3 0 |{}| 2 3 0 |{}| 2 4 0 |{}| 図 4.10 のグラフを表すファイルの 0 1 3 2 図 4.10 のグラフ 内容 図 4.12: ファイルの内容とグラフ } edge e02 = G.new_edge(v0, v2); edge e12 = G.new_edge(v1, v2); edge e13 = G.new_edge(v1, v3); G.write("4-3-6.gw"); return 0; これで図 4.10 に示したグラフがファイル \4-3-6.gw" にセーブされます. そ の内容を書き出してみると図 4.12 のようになっています. では, 節点に文字型のラベルをつけ, 辺には整数の値を割り当てたパラメー タつきグラフではどうでしょう. 下のプログラムは, 2-4-4.c にデータ格納の 1 行を付け加えたものです. //program 4-3-7.c: パラメータつきグラフのセーブ #include <LEDA/graph.h> int main() 158 { } 第4章 GRAPH<string,int> H; node v0 = H.new_node(); node v2 = H.new_node(); H[v0] = "Tetsuo"; H[v1] H[v2] = "Kurt"; H[v3] グラフとデータ構造 node v1 = H.new_node(); node v3 = H.new_node(); = "Koji"; = "Stefan"; edge e01 = H.new_edge(v0, edge e02 = H.new_edge(v0, edge e12 = H.new_edge(v1, edge e13 = H.new_edge(v1, H[e01] = 13; H[e02] = 24; H[e12] = 36; H[e13] = 48; H.write("4-3-7.gw"); return 0; v1); v2); v2); v3); ファイル \4-3-7.gw" の内容を書き出してみると図 4.13 のようになってい ます. 上記の 2 つのファイルの内容を比較すれば, どの行にどの情報が格納され ているか一目瞭然でしょう. このように, パラメータつきグラフの場合には, 節点や辺に付加された情報もファイルに蓄えられますが, プログラム 4-3-5.c のように配列を用いた場合には, それらの情報はファイルには蓄えられない ことに注意してください. 最後に, プログラム 4-1-1.c を実行するとグラフウィンドウが開かれますが, そのメニューから File を選択すればファイルに蓄えられたグラフをロードす ることができます. では, 上で作成したファイル 4-3-6.gw や 4-3-7.gw をロー ドするとどうなるでしょう. まず, グラフウィンドウで作ったグラフでは節点 の座標が決まっているのですが, 4-3-6.gw や 4-3-7.gw では接続関係が決まっ ているだけで, 節点の座標までは決まっていません. プログラム 4-1-1.c を実 行してファイル 4-3-7.gw からグラフをロードしようとすると, LEDA はどの ように節点を配置するか尋ねてきます. ランダムに配置するか, 円周上に配置 するかなどを選択することになります. たとえば, 円周上に配置するように決 めると, 図 4.14 のように配置されます. このとき, プログラム 4-3-7.c では節 点に文字列のラベルをつけ, 辺には整数値を割り当てましたが, ロードされた 4.4. トポロジカルソートの例題 LEDA.GRAPH string int 4 |{Tetsuo}| |{Koji}| |{Kurt}| |{Stefan}| 4 1 2 0 |{13}| 1 3 0 |{24}| 2 3 0 |{36}| 2 4 0 |{48}| 159 Tetsuo Koji 13 v0 24 v2 Kurt v1 36 48 v3 Stefan 図 4.11 のグラフ 図 4.11 のグラフを表すファイルの 内容 図 4.13: ファイルの内容とグラフ グラフでは節点には生成順に整数の番号が割り当てられているだけで, 元の 文字列のラベルは失われてしまいます. また, 辺に割り当てられた整数値もな くなってしまっています. 4.4 トポロジカルソートの例題 最初のグラフアルゴリズムの例題として, グラフのトポロジカルソートに ついて考えてみましょう. トポロジカルソートとは, グラフのどの辺について も, その始点の番号が終点の番号よりも小さくなるように, グラフの節点に整 数の番号をつけるというものです. グラフにサイクルがあると, そのような 番号をつけることは不可能なので, サイクルのない有向グラフが対象となり ます. 基本的な考え方は次の通りです. まず, 入次数が 0 の節点を見つけます. そ のような節点が見つからなければ, そのグラフはサイクルを含んでいますか 160 第4章 グラフとデータ構造 1 2 0 3 図 4.14: 4-3-7.gw を 4-1-1.c のプログラムでロードしたところ ら, トポロジカルソートはできません. 入次数 0 の節点が見つかれば, それを キューに入れて蓄えます. ここまでが前処理です. 前処理が終ったら, 通し番号をつけるための変数 count の値を 1 にして, 繰 り返し処理に入ります. 毎回行うことは, キューから入次数が 0 になった節点 を取り出し, それに現在の番号 count をつけ, count の値を 1 だけ増やします. 次に, その節点に接続するすべての辺について, その終点の入次数を 1 だけ減 らします. これで節点を削除したのと同じ効果が得られます. この操作で隣 接節点の中で入次数が 0 になるものがあれば, それをキューに入れます. この 操作を繰り返して, すべての節点に番号がつけば終りです. すべての節点に番 号をつけ終るまえにキューが空になってしまえば, サイクルがあったことにな りますから, トポロジカルソートはできません. 4.4. トポロジカルソートの例題 161 以上の考え方をプログラムに近い形で表現すると, 次のようになります. グラフ G を宣言; グラフ G を入力; forall_nodes(v, G) v の入り次数 INDEG[v] を求める; INDEG[v] が 0 である節点をすべて求め, それらをキューに加える; count=1; while(キューが空でない){ キューから節点 v を取り出す; v の順序を count+1 の値とし, count を 1 増やす; v から出るすべての辺 e について{ 辺 e の終点を w とする; w の入次数の値 INDEG[w] を 1 だけ減らす; INDEG[w]=0 となれば, w をキューに加える; } } 後はこれをプログラムにすればいいのです. 上では, 節点をインデックスとす る配列として, 入次数を蓄える INDEG[v] と節点の順序を蓄える node ord[v] が必要です. このような節点や辺をインデックスとする配列が使えるのも LEDA の特徴です. この場合には, node_array<int> INDEG(G); node_array<int> node_ord(G); と宣言します. 辺に関する配列を宣言するときには, node array の代わりに edge array とすればいいのです. さらに, 節点を蓄えるキューが必要です. これは, queue<node> Q; のように宣言することができます. これで, キューに節点 v を蓄えたり, キュー から一つの節点を取り出す操作は, それぞれ 162 第4章 グラフとデータ構造 Q.append(v); v = Q.pop(); とします. 同様に, キューが空かどうかは, Q.empty() の値を参照すれば判定できます. 以上を組み合わせると次のプログラムが得 られます. ただし, 節点 v の入次数は, G.indeg(v) として参照できることは先 に説明した通りです. このプログラムにおいて, プログラム 4-1-1.c を実行して作ったグラフ 4-41.gw を入力する場合を考えてみましょう. //program 4-4-1.c: グラフのトポロジカルソート #include <LEDA/graph.h> #include <LEDA/queue.h> int main() { graph G; queue<node> Q; int count = 1; G.read("4-4-1.gw"); node_array<int> node_ord(G); node_array<int> INDEG(G); node v, w; edge e; forall_nodes(v, G) if( (INDEG[v]=G.indeg(v)) == 0) Q.append(v); while( !Q.empty() ){ v = Q.pop(); node_ord[v] = count++; forall_out_edges(e, v){ w = G.target(e); if( --INDEG[w] == 0) Q.append(w); } } 4.5. 最短経路問題 } 163 cout << "Node number "; forall_nodes(v, G) cout << node_ord[v] << " "; return 0; グラフ 4-4-1.gw が図 4.15 のようなものであったとき, トポロジカルソート の結果は次のようになります. プログラム 4-4-1.c の実行例 > 4-4-1 Node number 1 3 4 2 5 この結果は, 節点 0 に 1 という番号を, 節点 1 節点には 3 という番号を, 節 点 2 節点には 4 という番号をつけることを意味しています. 番号 3; 4 の節点 にはそれぞれ 2; 5 という番号をつけることになります. つまり, トポロジカル ソートの結果は, (v 0; v 3; v 1; v 2; v 4) と表すことができます. 0 2 1 3 4 図 4.15: トポロジカルソートの例 上の例題では節点配列として整数値をとるものを考えましたが, 整数値以 外のどんなデータタイプでも大丈夫です. 4.5 最短経路問題 次に各辺に対して長さが指定されたグラフにおいて, 1 つの節点 s を指定し たとき, 残りのすべての節点について s からの最短距離を計算する問題につ 164 第4章 グラフとデータ構造 いて考えてみましょう. トポロジカルソートの場合には節点間の接続関係だ けが問題でしたから, プログラム 4-1-1.c を実行することでデータとなるグラ フを構成することができましたが, この問題では各辺について長さという数 値データを与えないといけません. そこで, 今度はパラメータつきグラフを作 成できるようにプログラム 4-1-1.c を次のように変更します. //program 4-5-1.c: パラメータつきグラフの作成 #include <LEDA/graphwin.h> int main() { GRAPH<int, double> G; GraphWin gw(G, "LEDA Graph Editor"); gw.display(window::center, window::center); gw.edit(); return 0; } このプログラムを実行しますと, プログラム 4-1-1.c と同じ画面が現れます が, 今度は各辺について数値データを入力することができます. 以前と同じ ように節点を作り, 辺を引いた後で辺をダブルクリックすると, 辺に付加すべ き数値データを入力する data 欄がありますから, ここにキーボードから数値 データを入力します. すべての辺について長さを表す数値データを入力でき たら, 適当なファイル名でセーブします. このようにして作成したグラフの一 例を図 4.16 に示します. さて, このようにしてグラフ G を作成し, ファイルとして蓄えておくのも一 つの方法ですが, ここでは作成されたグラフに対して直接処理を施すことに しましょう. その場合, G の各辺について定義された長さを表す情報 cost[e] を用いることになります. 先にも説明しましたように, パラメータつきグラフ では, 辺 e に蓄えられた情報は G[e] として参照できるのですが, 辺に関する 情報を辺配列 cost に送ることもできます. 具体的には, edge_array<double> cost(G); cost = G.edge_data(); 4.5. 最短経路問題 165 0 1 9 8 32 12 5 4 3 2 14 20 25 13 4 19 5 6 図 4.16: 辺が長さをもつグラフ とするだけです. さて, これでグラフの入力に関しては準備が整いましたから, 辺に対して長 さが定義されているグラフ上で, すべての節点について指定された節点から の最短経路の長さを求める方法について考えましょう. この問題に対してはダイクストラのアルゴリズムが有名です. この方法は, グラフのテキストでは次のように擬似言語で説明されることが多いようです. ダイクストラの最短経路アルゴリズム 入力:有向グラフ G = (V; E ), 始点 s, および各辺 e 辺のコスト cost(e); begin dist(s) = 0; dist(v) = 1 for v 6= s; すべての節点に「未到達」のラベルをつける; while(「未到達」の節点が存在する) u を「未到達」の節点の中で dist の値が最小のものとする; u に「到達」のラベルをつける; for u から出るすべての辺 e について 辺 e の他方の端点を v とする f f 166 第4章 g g グラフとデータ構造 if(dist(u) + cost(e) < dist(v)) dist(v) = dist(u) + cost(e); このプログラムでは各節点について, 始点 s からの距離 dist(v ) を蓄えるため の節点配列が必要になります. それは, 距離を double 型で表すことにすると, node_array<double> dist(G); と表すことができます. また, このプログラムでは各節点を「到達」と「未到達」に区別して, 未到 達の節点の中から始点 s からの距離 dist(v ) が最小である節点を選ぶという操 作を繰り返します. これを実現する素朴な方法は, 各節点に「到達」と「未到 達」を区別するためのラベルをつけるというものです. たとえば, node_array<bool> visited(G, false); のように到達したかどうかを表すブール型の節点配列を用意しておきます. 最 初はどの節点も「未到達」の状態にあるので, 初期値を false(偽)としてい ます. そうすると, プログラムの本体は次のようになります. ただし, プログラム 中で MAXDOUBLE は double 型で表現できる最大の数値を表しています. //program 4-5-2.c: 1 節点からの最短経路 #include <LEDA/graphwin.h> int main() { GRAPH<int, int> G; G.read("4-5-1.gw"); node s = G.first_node(); edge_array<int> cost(G); cost = G.edge_data(); node_array<double> dist(G, MAXDOUBLE); 4.5. 最短経路問題 167 node_array<bool> visited(G, false); node u,v; edge e; double dmin; dist[s] = 0; do{ dmin = MAXDOUBLE; forall_nodes(v, G) if( !visited[v] && dist[v] < dmin){ dmin = dist[v]; u = v; } if(dmin == MAXDOUBLE) break; visited[u] = true; forall_out_edges(e, u){ v = target(e); if(dist[u] + cost[e] < dist[v]) dist[v] = dist[u] + cost[e]; } } while( true ); } forall_nodes(v, G) cout <<"node" <<index(v) <<" distance = "<< dist[v] <<endl; return 0; プログラム 4-5-2.c の実行例 > 4-5-2 node 0 distance node 1 distance node 2 distance node 3 distance node 4 distance node 5 distance node 6 distance = = = = = = = 0 9 8 12 39 26 31 このプログラムを実行すると, 実行例に示すように各節点までの距離が計 算されます. 上のプログラムでは各節点にラベルをつけることによって「到達」したか どうかを判定し, 未到達の節点の中で dist の値が最小のものを for 文によって 求めていましたが, 優先順位つきキューを用いて書きなおすこともできます. 168 第4章 グラフとデータ構造 つまり, 「未到達」の節点を距離をキーとする優先順位つきキューで管理す るのです. このような節点に関する優先順位つきキューは #include <LEDA/node_pq.h> node_pq <int> PQ(G); として宣言することができます. そうすると, 値が最小の要素を取り出す命 令は node u = PQ.del_min(); のように記述することができます. このキューに新たな節点 v を挿入するには, PQ.insert(v, dist[v]); とするだけでいいのです. キューが空かどうかは PQ.empty() の値で判定できます. 最後に, 節点 v に関する dist の値が減少すると, キュー の中での順位が変化するので, そのための調整をする必要があります. 節点 v の dist 値が c に減少したとき, 具体的には PQ.decrease_p(v, c); dist[v] = c; とします. プログラムは次のようにシンプルになります. //program 4-5-3.c: 1 節点からの最短経路 (2) #include <LEDA/graphwin.h> #include <LEDA/node_pq.h> int main() { GRAPH<int, int> G; G.read("4-5-1.gw"); node s = G.first_node(); 4.5. 最短経路問題 169 edge_array<int> cost(G); cost = G.edge_data(); node_array<int> dist(G, MAXINT); node_pq<int> PQ(G); node u,v; edge e; dist[s] = 0; PQ.insert(s, 0); forall_nodes(v, G) PQ.insert(v, dist[v]); while( !PQ.empty() ){ u = PQ.del_min(); forall_out_edges(e, u){ v = target(e); int c = dist[u] + cost[e]; if(c < dist[v]){ PQ.decrease_p(v, c); dist[v] = c; } } } } forall_nodes(v, G) cout <<"node" <<index(v) <<" distance = " << dist[v] <<endl; return 0; 始点が与えられたとき, 始点から各節点までの最短経路の長さは求まりま すが, 経路自身を求めるためには更に情報が必要です. 上では各節点について 始点からの距離を dist という配列で管理していますが, 距離だけではなく, 最 短経路における直前の節点を求めておく必要があります. 始点から節点 v へ の最短経路における v の直前の辺を pred という配列で管理することにする と, プログラムは次のようになります. ただし, 節点 v の番号を index(v ) とし て参照しています. //program 4-5-4.c: 1 節点からの最短経路 (3) #include <LEDA/graphwin.h> #include <LEDA/node_pq.h> int main() 170 { 第4章 グラフとデータ構造 GRAPH<int, int> G; G.read("4-5-1.gw"); node s = G.first_node(); edge_array<int> cost(G); cost = G.edge_data(); node_array<int> dist(G, MAXINT); node_array<edge> pred(G, nil); node_pq<int> PQ(G); node u,v; edge e; dist[s] = 0; PQ.insert(s, 0); forall_nodes(v, G) PQ.insert(v, dist[v]); while( !PQ.empty() ){ u = PQ.del_min(); forall_out_edges(e, u){ v = target(e); int c = dist[u] + cost[e]; if(c < dist[v]){ PQ.decrease_p(v, c); dist[v] = c; pred[v] = e; } } } } forall_nodes(v, G){ cout <<"node "<<index(v) <<" distance = " <<dist[v] <<endl; u = v; while(e = pred[u]){ cout << " node " << index(u); u = G.source(e); } cout << endl; } return 0; 4.5. 最短経路問題 プログラム 4-5-4.c の実行例 > 4-5-4 node 0 distance = 0 node 0 node 1 distance = 9 node 1 node 0 node 2 distance = 8 node 2 node 0 node 3 distance = 12 node 3 node 0 node 4 distance = 39 node 4 node 5 node 3 node 0 node 5 distance = 26 node 5 node 3 node 0 node 6 distance = 31 node 6 node 3 node 0 171 このプログラムを実行すると, 実行例に示すように番号 0 の始点までの経 路上の節点が順に出力されます. 上記のプログラムでは \node pq" という実体がよく分からない優先順位つ きキューを用いていますが, 第 3 章で説明したふつうの優先順位つきキュー を用いて書きなおすこともできます. 優先順位つきキューでは, データと, 優 先順位を決めるためのキー値の組を扱います. この場合には, 節点 v と始点か らその節点までの現在の距離 dist[v ] を組にしたもの < dist[v ]; v > になりま す. 各節点についてそのような組を蓄えるために, 次のような節点配列を宣言 しておきます. p_queue<int, node> PQ; node_array<pq_item> I(G); こうしておくと, 節点 v をキューに蓄えるときには, 節点 v 自身ではなく I [v ] ということになります. 後は基本的に同じですが, キューから最小のキー値 をもつ要素として取り出された it は節点と距離値の組ですから, プログラム 4-5-4.c では, node u = PQ.del_min(); 172 第4章 グラフとデータ構造 と簡単にできた部分が pq_item it = PQ.find_min(); node u = PQ.inf(it); PQ.del_item(it); のように面倒になります. また, このプログラムで厄介なのは, 節点 v への近 道が見つかったときにキューに蓄えられている v に関するキー値を c に減ら す処理です. PQ.decrease_p(c, v); と書ければいいのですが, この操作では節点 v が蓄えられている場所に直接 アクセスしないといけないので, 節点 v に関連する pq item である I [v ] を用 いて, PQ.decrease_p(I[v], c); としなければなりません. この操作のために I という節点配列を定義する必 要があったのです. さらに, この節点配列に値を入れておかないといけないの ですが, それはキューにデータを挿入するときに, I[v] = PQ.insert(dist[v], v); のようにすればいいので, 面倒なポインタ操作は必要ありません. このような 点に注意して書きなおしたのが次のプログラムです. //program 4-5-5.c: 1 節点からの最短経路 (4) #include <LEDA/graphwin.h> #include <LEDA/p_queue.h> int main() { GRAPH<int, int> G; G.read("4-5-1.gw"); node s = G.first_node(); 4.5. 最短経路問題 173 edge_array<int> cost(G); cost = G.edge_data(); node_array<int> dist(G, MAXINT); node_array<edge> pred(G, nil); p_queue<int, node> PQ; node_array<pq_item> I(G); node u,v; edge e; dist[s] = 0; I[s] = PQ.insert(0, s); forall_nodes(v, G) I[v] = PQ.insert(dist[v], v); while( !PQ.empty() ){ pq_item it = PQ.find_min(); u = PQ.inf(it); PQ.del_item(it); forall_out_edges(e, u){ v = target(e); int c = dist[u] + cost[e]; if(c < dist[v]){ PQ.decrease_p(I[v], c); dist[v] = c; pred[v] = e; } } } } forall_nodes(v, G){ cout <<"node" <<index(v) <<" distance = "<<dist[v] <<endl; u = v; while(e = pred[u]){ cout << " node " << index(u); u = G.source(e); } cout << endl; } return 0; ここまで LEDA で用意された様々な機能を用いて最短経路を求めるプログ ラムを記述してきましたが, もちろん LEDA では多数の基本的なアルゴリズ 174 第4章 グラフとデータ構造 ムが関数の形で用意されています. したがって, このような基本的な問題で あれば, 用意された関数を呼び出すだけで計算を終えることができます. 実 際, 最短経路問題に対しても幾つかの関数が用意されていますが, そのうちの DIJKSTRA T() という関数が今まで説明してきたプログラムとうまく適合し ます. この関数を使えば, プログラムは次のように簡潔になります. //program 4-5-6.c: 1 節点からの最短経路 (5) #include <LEDA/graphwin.h> #include <LEDA/graph_alg.h> int main() { GRAPH<int, int> G; G.read("4-5-1.gw"); node s = G.first_node(); edge_array<int> cost(G); cost = G.edge_data(); node_array<int> dist(G, MAXINT); node_array<edge> pred(G, nil); DIJKSTRA_T(G, s, cost, dist, pred); } node u, v; forall_nodes(v, G){ cout <<"node" <<index(v) <<" distance = " <<dist[v] <<endl; u = v; while(edge e = pred[u]){ cout << " node " << index(u); u = G.source(e); } cout << endl; } return 0; 上のプログラムでは先にグラフをファイルに蓄えておいて, それを取り込 んで最短経路アルゴリズムを実行しましたが, プログラムの中でグラフを作 4.5. 最短経路問題 175 成して, そのグラフに同じ処理を施すこともできます. そのためにはグラフの 編集作業が終ったら, そのグラフをプログラムの中で取り込む必要がありま す. そのためには, gw.edit(); graph& G = gw.get_graph(); とすればいいのです. あるいは, 編集作業が終るたびにグラフ G を更新して 処理を行うためには, while( gw.edit() ){ graph& G = gw.get_graph(); .... } とすることができます. このようにしてアニメーションのプログラムを作る ことができますが, グラフウィンドウ上で表示しながら処理を進めるために は細々とした設定が必要です. マニュアルだけを眺めてどんな設定が必要か を知ることは非常に難しいのですが, 幸いなことに LEDA には多数のデモプ ログラムが用意されていますから, それらを参考にするのがいいでしょう. 最 後に少し複雑なプログラムを示しましょう. このプログラムは, 上記のプログ ラムからさらに進んで, 新たな節点や辺を追加したり, 辺の長さを変えるたび に最短経路を求める上記の関数を呼び出して, 常に最新の結果を画面上に表 示するというものです. ここでは詳細については説明しませんが, 次のプログ ラムでは最初に作られた節点を始点として, 節点や辺に関する編集作業が行 われるごとに各節点までの最短距離の長さを節点のラベルとして画面上に表 示するようにしたものです. 図 4.17 は処理の途中を示したものです. //program 4-5-7.c: 1 節点からの最短経路 (6) #include <LEDA/graphwin.h> #include <LEDA/graph_alg.h> GRAPH<int,int> G; 176 第4章 グラフとデータ構造 void run_dijkstra(GraphWin& gw) { bool flush = gw.set_flush(false); node s = G.first_node(); if (s == nil) return; //empty graph node_array<edge> pred(G); gw.message("\\bf Computing Shortest Paths"); DIJKSTRA(G,s,G.edge_data(),G.node_data(),pred); gw.set_node_color(blue); gw.set_edge_color(grey2); gw.set_edge_width(1); } node v; forall_nodes(v,G) { edge e = pred[v]; if (e != nil) { gw.set_color(v,red); gw.set_color(e,blue); gw.set_width(e,2); } } gw.set_flush(flush); gw.message(""); gw.redraw(); void init_edge(GraphWin& gw, edge e) { G[e] = rand_int(0,99); gw.set_slider_value(e,G[e]/100.0,0); } void new_edge_handler(GraphWin& gw, edge e) { init_edge(gw,e); run_dijkstra(gw); } void edge_slider_handler(GraphWin& gw,edge e, double f) { G[e] = int(100*f); } 4.6. グラフに関する基本アルゴリズム 177 void end_edge_slider_handler(GraphWin& gw, edge, double) { run_dijkstra(gw); } int main() { GraphWin gw(G,"Dijkstra Demo"); gw.set_node_shape(rectangle_node); gw.set_edge_label_type(data_label); gw.set_node_label_type(data_label); gw.set_del_edge_handler(run_dijkstra); gw.set_del_node_handler(run_dijkstra); gw.set_new_edge_handler(new_edge_handler); gw.set_edge_slider_handler(edge_slider_handler); gw.set_end_edge_slider_handler(end_edge_slider_handler); } gw.display(); gw.edit(); return 0; 4.6 グラフに関する基本アルゴリズム LEDA には多数のアルゴリズムが関数の形式で用意されていますから, そ れらを呼び出すだけで簡単にプログラムが書けます. グラフに関しては基本 的なアルゴリズムから高度なアルゴリズムまで幅広く用意されています. こ こでは基本的な関数として用意されているものを列記しておきます. 詳細は マニュアルを参照してください. 実行時間はどの関数も節点数と辺数の和に 比例する時間 O (n + m) です. ただし, 以下では節点数を n で, 辺数を m で表 すことにします. TOPSORT(): トポロジカルソート 与えられたグラフがサイクルを含まな 178 第4章 グラフとデータ構造 図 4.17: プログラム 4-5-7.c の実行の様子 ければ true を返し, トポロジカルソートの結果を節点配列の形で返しま すが, サイクルを含めば false を返して終ります. DFS() : 深さ優先探索 有向グラフと始点 s を与えて, を行います. s からの深さ優先探索 DFS NUM() : 有向グラフの深さ優先探索 有向グラフの深さ優先探索を行 います. 節点に番号をつけていきますが, 2 通りの方法を選択すること ができます. BFS() : s からの幅優先探索を行 BFS() : 幅優先探索 上と同じですが, 最短路木を構成する点が異なります. 幅優先探索 有向グラフと始点 s を与えて, います. 4.7. グラフに関する高度なアルゴリズム 179 STRONG COMPONENTS() : 強連結成分 有向グラフの強連結成分を 計算します. 各節点について, それが属する強連結成分の番号を求め ます. BICONNECTED COMPONENTS() : 2 連結成分 無向グラフの 2 連 結成分(どの 1 節点を取り除いても連結性を失わない)を求めます. TRANSITIVE CLOSURE() : めます. 推移的閉包 有向グラフの推移的閉包を求 グラフに関する高度なアルゴリズム 4.7 4.7.1 最短経路に関するアルゴリズム 最短経路問題に関連するプログラムだけでも実に様々なアルゴリズムが用 意されています. SHORTEST PATH T() : 単一始点最短経路 グラフ G と始点 s が与えら れたとき, すべての節点について s からの最短経路の長さを求めます. サイクルを含まないグラフの場合は線形時間で実行できます. 辺の長さ がすべて非負であれば, 実行時間は O (m + n log n) です. 負の長さの辺 がある場合にも適用できます. ただし, m は辺数, n は節点数です. ACYCLIC SHORTEST PATH T() : サイクルを含まないグラフでの最 短経路 サイクルを含まないグラフ上で始点となる節点が与えられたと き, 単一始点最短経路問題を解きます. DIJKSTRA T() : ダイクストラ法 ダイクストラのアルゴリズムに基づい て最短経路を求めます. ただし, 辺の長さは非負でなければなりません. BELLMAN FORD B T() : ベルマン-フォードのアルゴリズム 最短経路 問題を解くベルマン-フォードのアルゴリズムを実現したものです. こ のプログラムは入力のグラフに負のサイクルがある場合にも適用でき ます. 負のサイクルがある場合, この関数は false を返します. ALL PAIRS SHORTEST PATHS T() : 全節点対最短経路 すべての節 点対について最短経路を求めます. 実行時間は O (nm + n2 log n) です. 180 第4章 4.7.2 グラフとデータ構造 フローに関するアルゴリズム MAX FLOW T() : 最大フロー グラフ G, 始点 s, 終点 t, および各辺 e に 定義された容量 cap[e] で定義されるネットワーク (G; s; t; cap) が与えら れたとき, s t 間の最大フローを計算し, その値を返します. GoldbergTarjan のプリフロー-プッシュのアルゴリズムに基づいています. CHECK MAX FLOW T() : 最大フローの判定 ネットワーク (G; s; t; cap) と数値 f が与えられたとき, f が最大フローかどうかを判定します. max ow gen rand() : 最大フローの問題生成 最大フローの問題を乱数を 用いて生成します. MIN COST FLOW() : 最小コストフロー 各辺にコストが定義されたネ ットワークが与えられたとき, その最小コストフローを求めます. 実行 時間は O (m log U (m + n log n)) です. 4.7.3 最小カットに関するアルゴリズム MIN CUT() : 最小カット 辺に非負の重みがついたグラフが与えられたと き, このグラフから節点集合 C とその補集合にまたがる辺をすべて取り 除くことによってグラフが空でない 2 つの部分に分かれるとき, その節 点集合をカットと言い, C と C の補集合にまたがる辺の重みの総和を カットの重みと言います. この関数では, 最小重みのカットを求めます. 実行時間は O (nm + n2 log n) ですが高速化のためのヒューリスティック を用いることもできます. CUT VALUE() : カットの値 節点集合を与えて, それで定義されるカット の値を返します. 4.7.4 最大マッチングに関するアルゴリズム MAX CARD BIPARTITE MATCHING() : 2 部グラフの最大マッチ ング 2 部グラフの最大要素数マッチングを計算します. MAX CARD BIPARTITE MATCHING() : 最大マッチングと最小節 点被覆 与えられた 2 部グラフの最大マッチングと最小節点被覆を求め ます. 4.7. グラフに関する高度なアルゴリズム 181 CHECK MCB() : マッチングの判定 グラフ G, 辺のリスト M , 2 値表現 された節点集合 NC を与えて, M が G のマッチングになっているかと, NC が G の節点被覆になっているかどうかを判定します. MAX CARD BIPARTITE MATCHING XX() : 2 部グラフの最大マ ッチング 2 部グラフの最大要素数マッチングを計算します. ただし, こ の関数では, XX の部分を HK, ABMP, FF, RRB のいずれかにするこ とによって, 用いるヒューリスティックを選択できます. それぞれの記 号の意味は次の通りです. また, 最後に実行時間を示しています. HK: Hopcroft and Karp, O(m) AMBP: Alt, Blum, Mehlhorn, and Paul, O(m), FF:Ford and Fulkerson, O(nm), FFB: FF を単純にしたもの. MAX WEIGHT BIPARTITE MATCHING T() : 2 部グラフの最大 重みマッチング 辺に重みが付与された 2 部グラフの重み最大のマッチ ングを求めます. 計算時間は O (n(m + n log n)) です. MAX CARD MATCHING() : 最大マッチング 一般のグラフについて 要素数最大のマッチングを求めます. 基本は Edmonds のアルゴリズム です. 計算時間は O (nm(n; m)) です. MAX WEIGHT MATCHING T() : 最大重みマッチング 一般のグラ フについて重みの和が最大となるマッチングを求めます. 計算時間は O(nm log n) です. MAX WEIGHT PERFECT MATCHING T() : 最大重み完全マッ チング 一般のグラフについて重みの和が最大となる完全マッチングを 求めます. 完全マッチングが存在しない場合には空集合を返します. 計 算時間は O (nm log n) です. MIN WEIGHT PERFECT MATCHING T() : 最小重み完全マッチ ング 一般のグラフについて重みの和が最小となる完全マッチングを求 めます. 完全マッチングが存在しない場合には空集合を返します. 計算 時間は O (nm log n) です. 4.7.5 最小木に関するアルゴリズム SPANNING TREE() : 全域木 グラフ G によって定まる無向グラフの全 域木を求めます. 計算時間は O (n + m) です. 182 第4章 グラフとデータ構造 MIN SPANNING TREE() : 最小全域木 各辺に重みが定義された無向 グラフ G の最小全域木を求めます. 計算時間は O (m log n) です. 4.7.6 グラフの平面性に関するアルゴリズム PLANAR() : グラフの平面性判定 与えられた有向グラフが平面グラフか どうかを判定します. 計算時間は O (n + m) です. TRIANGULATE PLANAR MAP() : 三角形分割された平面地図 平面 地図を表す有向グラフ G が与えられたとき, G のすべての面を弦の挿 入により三角形にします. 計算時間は O (n + m) です. FIVE COLOR() : 平面グラフの 5 彩色 平面グラフ G が与えられたとき, どの辺についても両端の節点の色が異なるように節点に 5 色で彩色し ます. 計算時間は O (n + m) です. INDEPENDENT SET() : 独立点集合 グラフ G の独立点集合 I を求め ます. グラフ G が平面グラフで並列辺をもたないときには I のサイズ は n=6 以上です. 4.7.7 グラフ描画に関するアルゴリズム STRAIGHT LINE EMBED MAP() : 直線描画 平面地図を表すグラ フ G の直線描画を求めます. すなわち, 各節点を整数座標の点に配置 して, 互いに交差しないように各辺を直線で結びます. 計算時間は (n2 ) です. STRAIGHT LINE EMBEDDING() : 直線描画 上と同じですが, 節点 の座標は非負という制約をつけています. 計算時間は (n2 ) です. VISIBILITY REPRESENTATION() : 可視表現 各節点を水平線分ま たは長方形で, 辺を垂直線分で表すグラフの可視表現を求めます. SPRING EMBEDDING() : バネ表現 各辺を弾性のあるバネと見なして 力が釣り合うように節点を配置することによってグラフを表現します. ORTHO EMBEDDING() : 直交表現 各辺の折れ曲がり許容回数を与え て, グラフを折れ線表現します. 制約を満たす表現が存在しない場合に は false を返します. また, 折れ曲がり数を最小にすることもできます. 183 第 5 章 幾何データの取り扱い この章では, 幾何データの処理を効率良く行うために LEDA が何を提供して くれているのかを説明します. 点, 線分, 円, 多角形などの基本的な図形要素 の取り扱いについては第 2 章でも説明しましたが, ここではもう少し高度な 処理について説明することにします. 5.1 基本的な例題 最初に図形に関する基本的な例題について説明しましょう. 第 2 章でも点, 線分, 円, 多角形などの入力や表示について説明しましたが, ここではそれら の図形要素を配列に蓄えて, 何らかの処理を行う場合を考えます. 最初の例題は, 多数の点が入力されている状態で, 新たに入力された点に最 も近いものを求めるというものです. 点間の距離を求めるには x; y 座標値の 差の 2 乗和を求めるのが基本ですが, LEDA では, 面倒な計算をしなくても, 2 点 p; q 間の距離を p.distance(q) として簡単に求めることができます. したがって, プログラムは次のようにな ります. //program 5-1-1.c: 最も近い点を求める #include <LEDA/window.h> #define MAX 1000 int main() { 184 第 5 章 幾何データの取り扱い point pt[MAX]; window W (400, 400, "program5-1-1.c"); W.display(window::center, window::center); point p, q; int n=0; while(W >> p) { W << p; pt[n++] = p; } } W >> q; W << q; // 質問点 q を入力 p=pt[0]; for(int i=1; i<n; i++) if( pt[i].distance(q) < p.distance(q) ) p = pt[i]; W.draw_point(p, red); W.read_mouse(); return 0; このプログラムでは, 最初に点データをウィンドウから入力して配列 pt に 蓄えます. その後, 質問点 q を入力して, 配列に蓄えられた点の中で最も q に 近い点 p を求めます. 最後に, 求めた点 p を別の色(ここでは赤)で表示して 終りです. プログラム 5-1-1.c では配列に点のデータを蓄えましたが, 配列を使う場合 には配列のサイズを指定しないといけないので, 予想より多い点が入力され た場合には問題が生じます. そのような問題を避けるためにも LEDA ではリ ストによる管理を推奨しています. 実際, 点のリストを用いてプログラムを書 きなおすと次のプログラム 5-1-2.c のようになります. //program 5-1-2.c: 最も近い点を求める(点リスト版) #include <LEDA/window.h> int main() { list<point> OBJS; window W (400, 400, "program5-1-2.c"); W.display(window::center, window::center); 5.1. 基本的な例題 185 point obj; while (W >> obj) W << obj; OBJS.append(obj); } point q; W >> q; W << q; // 質問点 q を入力 point closest = OBJS.head(); forall(obj, OBJS) if( obj.distance(q) < closest.distance(q) ) closest = obj; W.draw_point(closest, red); W.read_mouse(); return 0; 演習問題 5.1.1 プログラム 5-1-2.c では質問点に最も近い点を求めるのに, 毎回 2 点間の距離を 2 回求めています. それまでの最小距離と, その最小距 離を実現する点を管理するようにすれば, 距離計算を毎回 1 回に減らすこと ができます. そのようにプログラムを変更しなさい. 演習問題 5.1.2 プログラム 5-1-2.c では 1 回だけ質問点を入力しましたが, 質問点を何度も繰り返し入力して同じ処理が繰り返せるようにプログラムを 拡張しなさい. まったく同じ構造のプログラムを線分や円に対しても書くことができます. 線分の場合, 最初に線分データをリストに入れておいて, 後で指定された点 p に最も近い線分を求めることになります. このとき, 線分と点の距離を求める 必要があります. 点 p から線分 s に垂線を下ろすことができる場合には, その 垂線の長さが距離になります. そうでない場合には, 線分の 2 端点のうち近い 方までの距離が点 p から線分 s までの距離となります. このように, 線分 s か ら点 p までの距離の計算は結構面倒ですが, LEDA では, s.distance(p) が距離を与えてくれるのです. したがって, プログラムは次のようになります. 186 第 5 章 幾何データの取り扱い //program 5-1-3.c: 最も近い線分を求める #include <LEDA/window.h> int main() { list<segment> OBJS; window W (400, 400, "program5-1-3.c"); W.display(window::center, window::center); segment obj; while (W >> obj) { W << obj; OBJS.append(obj); } } point q; W >> q; W << q; // 質問点 q を入力 segment closest = OBJS.head(); forall(obj, OBJS) if( obj.distance(q) < closest.distance(q) ) closest = obj; W.draw_segment(closest, red); W.read_mouse(); return 0; 多数の円の中で与えられた点に最も近い円を求める問題も同様のプログラ ムで記述できます. 単に, 線分として宣言されているところを円の宣言に置き かえればいいだけです. 具体的には次のようになります. //program 5-1-4.c: 最も近い円を求める #include <LEDA/window.h> int main() { list<circle> OBJS; window W (400, 400, "program5-1-4.c"); W.display(window::center, window::center); circle obj; 5.1. 基本的な例題 187 while (W >> obj) { W << obj; OBJS.append(obj); } } point q; W >> q; W << q; // 質問点 q を入力 circle closest = OBJS.head(); forall(obj, OBJS) if( obj.distance(q) < closest.distance(q) ) closest = obj; W.draw_circle(closest, red); W.read_mouse(); return 0; このように LEDA では便利な関数が多数用意されています. すべてをここ で列挙することはできませんが, 便利そうなものを幾つか説明することにし ましょう. 詳細については LEDA のマニュアルを参照してください. 5.1.1 point データタイプ 2 次元の点を表す point データタイプの宣言は次の通りです. これ以外にも ベクトルを用いた方法などがありますが, 詳細はマニュアルを参照してくだ さい. (1) point p: (2) point p(x, y): 点を表す変数 p を宣言. double 型の値 x; y を用いて, 点 p を初期値 (x; y ) で宣言. 変数 p を point 型で宣言すると, p が表す点に関して以下の値が利用できま す. ただし, 引数として使われている p; q; a; b; c; d はすべて point 型の変数で す. 図 5.1 も参考にしてください. (3) p.xcoord(): (4) p.ycoord(): p の x 座標値 (double) p の y 座標値 (double) 188 第 5 章 幾何データの取り扱い q r p.distance(q) p.ycoord() p p.distance(s) center(p, r) segment s p.xcoord() 図 5.1: point データタイプに関連する各種関数 (5) p.xdist(q): (6) p.ydist(q): (7) p.distance(q): (8) p.distance(): (9) center(a, b): (10) midpoint(a, b): (11) orientation(a, b, c): (12) (13) (14) (15) collinear(a, b, c): right turn(a, b, c): left turn(a, b, c): incircle(a, b, c, d): 2 点 p; q 間の水平距離 (double) 2 点 p; q 間の水平距離 (double) 2 点 p; q 間のユークリッド距離 (double) 点 p と原点 (0; 0) 間のユークリッ ド距離 (double) 2 点 a; b のちょうど中間の点 (point) 同上 3 点 a; b; c の方向 (int). すなわち, (a; b; c) が反時計回りのとき +1, 3 点が一直線上にあるとき 0, それ 以外のとき 1 の値をとる. 3 点 a; b; c が一直線上にあるか (bool) 3 点 a; b; c が右回りの順か (bool) 3 点 a; b; c が左回りの順か (bool) 点 d は 3 点 a; b; c を通る円の内部 5.1. 基本的な例題 (16) cocircular(a, b, c, d): 189 にあるか (bool) 点 d は 3 点 a; b; c を通る円の周上 にあるか (bool) 2点のちょうど中間の点を求めたりするのは比較的簡単ですが, 3点が時 計回りの順に並んでいるかどうかを判定するのは自分でプログラムを書くと なると結構面倒なものです. 基本的にはベクトルの外積(三角形の符号付面 積)の計算ですが, 式で書くと長くなるので上の関数は非常に便利です. ま た, 4点を与えて, それらが同一円周上にあるかどうかの判定も便利です. こ れら2つの判定は計算幾何学では基本中の基本ですので, 幾何データを扱う ときにはその有り難さが分かるでしょう. 上の機能を使って簡単なプログラムを作ってみましょう. 多数の点を入力 した後で, マウスで 2 点を指定することによって 1 本の直線を入力し, 直線よ り上にある点を赤で, 下にある点を青で表示するというプログラムです. 残念 ながら点が直線より上にあるかどうかを判断する機能はありませんから, 別 の方法を考えなければなりません. ここでは 3 点の方向(時計回りの順かど うか)を利用することにします. そのために, 直線を定義する 2 点 a; b が入力 されたとき, より左の点が a, より右の点が b となるようにします(もし, 2 点 のx座標を比べて逆の関係になっていれば, leda swap() という値を交換する ための関数を用いて 2 点を交換しておきます. そうすると, 3 点 a; b; p が反時 計回りの順に並んでいれば点 p は a; b を通る直線より上にあり, そうでなけれ ば下にあると判断することができます. 3点 a; b; p が反時計回りの順に並ん でいるかどうかは, orientation(a; b; p) の値が正であるかどうかで判断できま す. 以上の考え方を総合するとプログラム 5-1-5.c が得られます. //program 5-1-5.c: 直線に関する点集合の2分割 #include <LEDA/window.h> int main() { window W (400, 400); W.display(window::center, window::center); list <point> S; point p; 190 第 5 章 幾何データの取り扱い while(W >> p){ W << p; S.append(p); } line l_ab; W >> l_ab; W << l_ab; point a=l_ab.point1(), b=l_ab.point2(); if(a.xcoord() > b.xcoord() ) leda_swap(a, b); } forall(p, S) if( orientation(a, b, p) > 0) W.draw_point(p, red); else W.draw_point(p, blue); W.read_mouse(); W.close(); return 0; 5.1.2 segment データタイプ 線分を表す segment データタイプの宣言には幾つかのバリエーションがあ ります. segment s; のように宣言する最も簡単な形式の他に, 両端点を指定し たり, 両端点の座標値を指定したりする方法があります. その他にもベクトル を使ったりする方法などがありますが, 詳細についてはマニュアルを参照し てください. 下で型が宣言されていない引数は double 型です(以下同様). (1) segment s(point p, point q): 2 点間の線分として線分を定義 (2) segment s(x1, y1, x2, y2): 2 端点の座標値を与えることで線 分を定義 (引数は double 型) (3) segment s(point p, alpha, length): 始点と方向および長さに よって線分を定義 segment 型で宣言された変数 s に関しては以下の値が利用できます. 図 5.2 も参考にしてください. (4) s.start(): 線分 s の始点 (point) 5.1. 基本的な例題 191 q segment(p, q) p s.end() s.ycoord2() s.ycoord1() s.start() segment s s.xcoord1() s.xcoord2() 図 5.2: segment データタイプに関連する各種関数 (5) s.end(): (6) s.xcoord1(): (7) s.ycoord1(): (8) s.xcoord2(): (9) s.ycoord2(): (10) s.length(): (11) s.contains(point p): (12) s.intersection(segment t): (13) s.distance(point p): 線分 s の終点 (point) 線分 s の始点の x 座標 (double) 線分 s の始点の y 座標 (double) 線分 s の終点の x 座標 (double) 線分 s の終点の y 座標 (double) 線分 s の長さ (double) 点 p は線分 s 上にあるか (bool) 線分 s は線分 t と交差するか (bool) 線分 s から点 p までの距離 (dou- ble) 簡単なプログラムを作ってみましょう. まず, 多数の線分を入力して線分の リストに蓄えます. その後で別の線分を入力し, この線分と交差する線分の色 を赤に変えて表示するというものです. //program 5-1-6.c: 質問線分と交差する線分の列挙 #include <LEDA/window.h> 192 第 5 章 幾何データの取り扱い int main() { window W (400, 400); W.display(window::center, window::center); list <segment> S; segment s; while(W >> s){ W << s; S.append(s); } } segment query_s; W >> query_s; W << query_s; forall(s, S) if( s.intersection(query_s) ) W.draw_segment(s, red); W.read_mouse(); W.close(); return 0; 5.1.3 ray データタイプ 1点からある方向に無限に延びる半直線を表すデータタイプも用意されて います. その宣言は次の通りです. (1) ray r: (2) ray r(point p, point q): (3) ray r(segment s): 変数 r を ray 型で宣言 点 p を始点とし, 点 q の方向に延 びる半直線として変数 r を定義 線分 s の始点から s の終点の方向 に延びる半直線として変数 r を定 義 ray 型で宣言された変数 r に関しては以下の値が利用できます. (4) r.source(): (5) r.direction(): (6) r.is vertical(): 半直線 r の始点 (point) 半直線 r の方向 (double) 半直線 r は垂直か (bool) 5.1. 基本的な例題 193 半直線 r は水平か (bool) (7) r.is horizontal(): 半直線 r の傾き (double) (8) r.slope(): (9) r.intersection(ray s, point& p): 半直線 r が半直線 s と交差す る場合には true の値を返し, その 交点が変数 p に代入されます. 交 差しない場合には false を返しま す (bool) (10) r.intersection(segment s, point& p): 半直線 r が線分 s と交 差する場合には true の値を返し, その交点が変数 p に代入されます. 交差しない場合には false を返し ます (bool) 半直線 r は点 p を含むか (bool) (11) r.contains(point p): (12) r.contains(segment s): 半直線 r は線分 s を含むか (bool) ray データタイプに関する簡単なプログラムを作ってみましょう. まず, 多 数の線分を入力します. その後でマウスで半直線を入力します. 具体的には, 最初に左ボタンで指定した点が半直線の始点となり, そこから2番目に指定 した点を通る半直線が画面上に引かれることになります. 2番目の点を指定 して半直線を固定したとき, 始点から見て最も近い線分との交点(最初に線 分と交差する地点)を求め, その交点を赤で示すというものです. ここでは効率のことは考えずに, 優先順位つきのキューを用いてプログラム を作ってみましょう. 当然, キーとなるのは半直線の始点までの距離です. 半 直線と交差する線分をすべて優先順位つきのキューに挿入して, 最後にキー の値が最小の要素を取り出せば, それが始点に最も近い線分だということに なります. また, すべての線分についての処理が終ったときに優先順位つき キューが空であれば, 質問の半直線と交差する線分が存在しなかったことが わかります. //program 5-1-7.c: 半直線と最初に交差する線分 #include <LEDA/window.h> #include <LEDA/p_queue.h> int main() 194 { } 第 5 章 幾何データの取り扱い window W (400, 400); W.display(); list<segment> S; segment s; while(W>>s){ W << s; S.append(s); } ray r; point p; W >> r; W << r; p_queue<double, point> PQ; forall(s, S) if(r.intersection(s, p)) PQ.insert( p.distance(r.source()), p); if( !PQ.empty() ){ pq_item it = PQ.find_min(); W.draw_point(PQ.inf(it), red); } else cout << "No intersection" << endl; W.read_mouse(); W.draw_ray(r, white); W.close(); return 1; 5.1.4 line データタイプ line データタイプは平面上での直線を表すためのものです. 次のように宣 言します. (1) line l: (2) line l(point p, point q): (3) line l(segment s): (4) line l(ray r): 変数 l を line 型で宣言 2 点 p; q を通る直線で, p から q に 方向づけられた直線 線分 s を含み, s と同じ方向をも つ直線 半直線 r を含み, r と同じ方向を もつ直線 5.1. 基本的な例題 195 line 型で宣言された変数 l に関しては以下の値が利用できます. 直線 l の方向 (double) (5) l.direction(): (6) l.angle(line g): 2 直線 l と g がなす角 (double) (7) l.angle(): l.direction() と同じ (double) (8) l.is vertical(): 直線 l は垂直か (bool) (9) l.is horizontal(): 直線 l は水平か (bool) 直線 l と点 p の間の距離 (double) (10) l.distance(point q): (11) l.slope(): 直線 l の傾き (double) (12) l.intersection(line g, point& p): 直線 l が直線 g と交差する とき true の値を返し, その交点が 変数 p に代入されます. 交差しな いときは, false を返します (bool) (13) l.intersection(segment s, point& p): 直線 l が線分 s と交差 するとき true の値を返し, その 交点が変数 p に代入されます. 交 差しないときは, false を返します (bool) (14) l.dual(): (15) l.contains(point p): (16) l.perpendicular(point p): 直線 l と双対な点(ただし, l は垂 直でないこと) (point) 直線 l は点 p を含むか (bool) 点 P から直線 l 向けて下ろした 垂線 5.1.5 circle データタイプ circle データタイプは平面上での円を表すためのものです. 次のように宣言 します. (1) (2) (3) (4) circle circle circle circle 円を表す変数 C を宣言 C: C(point a, point b, point c): 3 点 a; b; c を通る円 C(point a, point b): 点 a を中心とし点 b を通る円 C(point c, double r): 点 c を中心とし, 半径 r の円 196 第 5 章 幾何データの取り扱い (5) circle C(double x, double y, double r): 点 (x; y ) を中心とし, 半径 r の円 circle 型で宣言された変数 C に関しては以下の値が利用できます. (6) C.center(): (7) C.radius(): (8) C.inside(point p): (9) C.contains(point p): (10) C.intersection(circle D): (11) C.intersection(line l): (12) C.intersection(segment s): (13) C.distance(point p): (14) C.distance(line l): (15) C.distance(circle D): 円 C の中心 (point) 円 C の半径 (double) 点 p は円 C の内部にあるか (bool) 円 C は点 p を内部に含むか (bool) 円 C と円 D の交点をリストの形 式で返す (list<point>) 円 C と直線 l の交点をリストの形 式で返す (list<point>) 円 C と線分 s の交点をリストの形 式で返す (list<point>) 円 C と点 p の間の距離 (double) 円 C と直線 l の間の距離 (double) 円 C と円 D の間の距離 (double) 5.1.6 polygon データタイプ 平面上の多角形を表すデータタイプとして polygon がありますが, これは 平面上の点を循環リストでつないだ形式です. 次のように宣言します. (1) polygon P: (2) polygon P(list<point> pl): 多角形を表す変数 P を宣言 点のリスト pl で定まる多角形とし て P を宣言 一般に, 与えられた点が多角形の内部にあるかどうかの判定は自明ではな く, 実際にプログラムを書くとなると計算幾何学のテキストを参照したりす る必要がありますが, こんなことも LEDA はきちんとサポートしてくれてい ます. それ以外にもたくさんありますが, たとえば, 与えられた線分との交点 を点リストとして返してくれる関数も用意されているのです. 5.1. 基本的な例題 197 具体的には, polygon 型で宣言された変数 P に関しては以下の値が利用で きます. (3) P.is simple(): (4) P.inside(point p): 多角形 P は単純か (bool) 点 p が多角形 P の内側にあるか (5) P.on boundary(point p): 点 p が多角形 P の境界上にあるか (6) P.outside(point p): 点 p が多角形 (7) P.intersection(segment s): 線分 s と多角形 P との交点をリス トとして返す 多角形 P は凸か (bool) 点 q の回りに多角形 P を 90 度回 転して得られる多角形 (polygon) (8) P.is convex(): (9) P.rotate90(point q): (bool) (bool) (bool) P の外側にあるか ここで若干の注意が必要です. 平面上で自己交差のない閉じた曲線を描く と平面は2つの部分に分割されます. 一方は曲線に囲まれた有限の領域で, 他 方はその補集合をなす無限の領域です. 多角形を入力したとき, 各辺の左側が 入力の多角形の内部だと見なされます. したがって, 多角形を反時計回りの順 に並んだ点列で指定すると, 有限の面積をもつ閉じた多角形が対応しますが, 逆の順序で点列を指定すると, 無限の領域に対応する多角形を指定したこと になります. 多角形のデータが polygon データタイプの変数 P に蓄えられているとき, その頂点を点のリストとして取り出したり, 多角形の辺を線分のリストとし て取り出したいときがありますが, そのような要求にも応えてくれています. (10) list (11) list <point> P.vertices(): <segment> P.edges(): 多角形 P の頂点のリスト 多角形 P の辺のリスト さらに, 多角形のすべての頂点や辺について調べることが多いので, 次のよ うな繰り返し構造も用意されています. (12) forall vertices(v, P)f...g 多角形 P のすべての頂点 v につ いて ... を繰り返し実行する. f g 198 第 5 章 幾何データの取り扱い (13) forall segments(s, P)f...g 5.2 5.2.1 多角形 P のすべての辺 s につい て ... を繰り返し実行する. f g 凸多角形に関する計算 多角形の入力 幾何データを扱う問題の例として凸多角形に関連する問題について考えて みましょう. 最初に, 凸多角形を正式に定義してみましょう. 一般に多角形 とは, その隣接頂点を直線で結ぶことによって得られる閉図形を指します. もっと形式的には, 点列 p0 ; p1 ; : : : ; pn = p0 が与えられたとき, 隣接する 2 点 pk ; pk+1; k = 0; 1; : : : ; n 1 を線分で結ぶことによって定まる図形ということ になります. 場合によっては自己交差を許す場合もありますが, ここではそれ らの線分は互いに交差しないものと仮定しておきます. ウィンドウ上で多角形を指定する方法は既に説明した通りです. マウスを 用いて頂点を順に指定していき, 最後の頂点を入力した後でマウスの右ボタ ンをクリックすればいいのです. したがって, 多角形を表示するだけなら, 次 のようなプログラムとなります. //program 5-2-1.c: 多角形の入力 #include <LEDA/window.h> int main() { window W (400, 400, "program5-2-1.c"); W.display(window::center, window::center); polygon poly; W >> poly; W << poly; W.read_mouse(); W.close(); return 0; } 5.2. 凸多角形に関する計算 5.2.2 199 凸多角形かどうかの判定 では次に入力した多角形が凸多角形かどうかを判定するプログラムについ て考えてみましょう. 凸多角形の特徴づけには様々なものがありますが, 数学 では次のように定義します. 定義 5.2.1 多角形の内部にある任意の 2 点を結ぶ線分がその多角形の内部に 含まれるとき, その多角形は凸である. この定義に基づいて凸多角形かどうかを判定するプログラムを書こうとし ますと, 多角形内部のすべての点を列挙するひつようがあるのですが, そんな ことは不可能です. したがって, 計算可能な別の特徴づけが必要になります. その一つが次の定理です. 定理 5.2.2 多角形 P が凸多角形であるための必要十分条件は, 対角線が P の内部にあるときである. P のすべての 今度は調べるべき対角線が頂点数の 2 乗に比例する程度しか存在しません から, プログラムが書けるはずです. そこで, 多角形の i 番目の頂点 pi と j 番 目の頂点 pj を結ぶ対角線 sij について考えます. sij と sji は同じ対角線なの で, 議論を簡単にするために, 以後 i < j とします. 隣接頂点を結んでも対角 線とは言わないので, さらに i j 2 と仮定することができます. さて, 対角線 sij が多角形 P の内部にあるとは何を意味するのでしょう. ま ず, この対角線は頂点 pi の付近で多角形 P の内部になければなりません. そ の後, P の他のどの辺とも接触することなく頂点 pj に接続しているなら, 確 かに sij は P の内部にあると言うことができます. 最初に, 対角線 sij が頂点 pi の付近で多角形 P の内部にあるかどうかを判 定する方法について考えてみましょう. 多角形 P を定める点列が多角形の内 部を左に見るように, すなわち反時計回りに方向づけられていると仮定したと き, 三角形 (pi 1 ; pi ; pj ) と (pi ; pi+1 ; pj ) も反時計回りでなければなりません. 図 5.3 を参照すれば明らかだと思いますので, 証明はしません. プログラムではどうでしょう. 3 点 a; b; c が反時計回り(左回り)かどうか を判定するためには, その 3 点で定まる三角形の符号付面積の符号を求めれ 200 第 5 章 幾何データの取り扱い p p i i+1 p i-1 p j p i p p p i-1 i+1 j 図 5.3: 対角線 sij が頂点 pi の内角を通るかどうかの判定 ばいいのですが, LEDA では left turn(point a point b, point c) という関数 が用意されているので簡単です. 後は, 対角線 sij が頂点 pi に接続する辺以外とは P のどの辺とも交差しな いことを確かめればいいのですが, これには線分 s が線分 t と交差するかどう かを判定する関数 s.intersection(segment t) を使うことができます. 最後に, 多角形は反時計回りに方向づけされていると仮定しました. 入力の ときにはそのような制約はないので, 逆向けに入力されないとも限りません. したがって, もし逆向けに入力されたのなら, 内部で点列を逆転しておく必要 があります. すなわち, 点列を (p0 ; p1 ; : : : ; pn = p0 ) とするとき, これを逆に して (p0 = pn ; pn 1 ; : : : ; p1 ; p0 ) に変換するわけです. これは, プログラムで は, i = 1; : : : ; n=2 に対して, pi と pn i を交換すれば達成できます. 値の交換 には, LEDA で用意されている関数 leda swap() を用いると簡単です. 多角形 P の符号付面積 A(P ) は次式で求めます. ただし, xi ; yi は頂点 pi の x; y 座標 です. P A(P ) = 21 =01 x (y +1 y 1 ): ここで, y 1 = y 1 ; y = y0 です. n i i i i このようにして計算した符号付面積が正 なら反時計回りで, 負なら時計回りです. したがって, 符号付面積が負なら, 上 記の点データの交換を行います. 以上に基づいてプログラムを順に作ってみましょう. まず, 多角形の入力部 ですが, 単に図形を描くだけではないので, 点列を配列の形で蓄える必要があ ります. その部分は次のようになります. n n 5.2. 凸多角形に関する計算 201 #define MAX 1000 array<point> p(MAX); while(W >> p[n]) W << p[n++]; p[--n] = p[0]; for(i=0; i<n; i++) W << segment(p[i], p[i+1]); 上のプログラムでは点列を入力して, 順に配列 p に蓄えています. n は頂点 数を管理する変数です. 最後に{n と 1 だけ減らしているのは, 最後に押され たマウスの右ボタンの分を無視するためです. この後, 符号付面積を求めて, もし負ならば点データを leda swap() を用い て逆順にします. double area; area = p[0].xcoord()*(p[1].ycoord() - p[n-1].ycoord()) + p[n-1].xcoord()*(p[0].ycoord() - p[n-2].ycoord()); for(i=1; i<=n-2; i++) area += p[i].xcoord()*(p[i+1].ycoord() - p[i-1].ycoord()); if(area < 0) for(i=0; i<n-i; i++) leda_swap(p[i], p[n-i]); 次に対角線 sij が多角形 P の内部にあるかどうかを判定します. まず, sij が対角線であるためには, 頂点 pi と pj は隣接していては駄目なので, それを チェックします. そのチェックの後で, 頂点 pi にも pj にも接続しないすべて の辺について, 対角線 sij と交差するかどうかを判定します. その部分のプロ グラムは次のようになります. bool inside = true, intersect = false; for(i=0; i<n-2; i++) for(j=i+2; j<n; j++){ if(j==(i-1+n)%n) continue; if( left_turn(p[(i-1+n) % n], p[i], p[j]) && left_turn(p[i], p[i+1], p[j]) ){ s = segment(p[i], p[j]); for(k=0; k<n; k++) if( k!=i && k!=(i-1+n)%n && k!=j && k!=(j-1+n)%n ){ 202 第 5 章 幾何データの取り扱い t = segment(p[k], p[k+1]); if( s.intersection(t) ) intersect = true; } } } else inside = false; 上のプログラムでは, 内角を通らない対角線があったかどうかを監視する ためのブール変数と, 多角形の辺と交差する対角線があったかどうかを監視 するブール変数によって凸多角形かどうかの判定をします. 全体のプログラムは次のようになります. //program 5-2-2.c: 凸多角形かどうかの判定 #include <LEDA/window.h> #define MAX 1000 int main() { window W (400, 400, "program5-2-2.c"); W.display(window::center, window::center); int n = 0, i, j, k; segment s, t; array<point> p(MAX); while(W >> p[n]) W << p[n++]; p[--n] = p[0]; double area = p[0].xcoord()*(p[1].ycoord() - p[n-1].ycoord()) + p[n-1].xcoord()*(p[0].ycoord() - p[n-2].ycoord()); for(i=1; i<=n-2; i++) area += p[i].xcoord()*(p[i+1].ycoord() - p[i-1].ycoord()); if(area < 0) //逆順なら点列を逆順にする for(i=0; i<n-i; i++) leda_swap(p[i], p[n-i]); for(i=0; i<n; i++) //多角形の辺を順に表示 W << segment(p[i], p[i+1]); bool inside = true, intersect = false; for(i=0; i<n-2; i++) for(j=i+2; j<n; j++){ 5.2. 凸多角形に関する計算 } } 203 if(j==(i-1+n)%n) continue; if( left_turn(p[(i-1+n) % n], p[i], p[j]) && left_turn(p[i], p[i+1], p[j]) ){ s = segment(p[i], p[j]); for(k=0; k<n; k++) if( k!=i && k!=(i-1+n)%n && k!=j && k!=(j-1+n)%n ){ t = segment(p[k], p[k+1]); if( s.intersection(t) ) intersect = true; } } else inside = false; if(inside && !intersect) cout << "凸多角形です " << endl; else cout << "凸多角形ではありません " << endl; W.close(); return 0; 5.2.3 効率の良い方法 上で説明した方法により凸多角形かどうかの判定が可能ですが, プログラ ムに 3 重ループが含まれていることからも分かるように, 計算時間は多角形 の頂点数 n の 3 乗に比例します. 多角形の頂点数が多くなければこれも構い ませんが, 凸多角形かどうかを判定するのに O (n3 ) もの時間がかかってしま うのは素人目にも効率が悪いように思われます. 実は, 画期的な方法があります. 上のプログラムでは線分どうしの交差判定 が必要でしたが, 実は線分の交差判定をしなくても凸多角形かどうかの判定 は可能です. ここでは証明を省略しますが, 凸多角形の次の特徴づけに基づい て非常に効率良く凸かどうかの判定ができるのです. 定理 5.2.3 多角形 P を最も左にある y 座標最小の頂点から反時計回りの方向 にたどるとき, どの頂点 pi においても前後の頂点となす三角形 (pi 1 ; pi ; pi+1 ) が反時計回りであり, しかも y 座標値が前後の点より低くなるのは最初の頂 点以外にないとき, P は凸多角形である. 図 5.4 に 3 通りの多角形が示されています. 左端の凸多角形では定義の条 204 第 5 章 幾何データの取り扱い 件が成り立ちますが, 中央の多角形のように凹の部分があると, 凹の頂点では 対応する三角形が時計回りになってしまいますし, 右端の図のように 1 周以 上してしまうと, 必ず局所最小の頂点が複数個存在することになり, 確かに定 義の条件は満たされません. 図 5.4: 辺角の単調増加性による凸多角形の認識 では, 定理 5.2.3 に基づいた判定は具体的にはどのようにすればいいでしょ う. 2 つの条件がありました. 最初の条件である三角形の方向のチェックは簡 単です. left_turn(p[i-1], p[i], p[i+1]) の値を調べるだけです. もう一方の y 座標値に関するチェックは p[i].ycoord() < p[i-1].ycoord() && p[i].ycoord() < p[i+1].ycoord() が成り立つかどうかで行えばいいのです. 実際には y 座標値が最小の頂点から計算を始めて, 多角形を一周しなけれ ばなりません. このとき, 配列のサイズを超えて参照することがないように気 をつける必要があります. j 番目の要素の次の要素を単に j + 1 番目の配列要 素として参照しようとすると, j = n 1 のときに次の要素が正しく指定され ないことになります. n 1 番目の次は 0 番目に戻るようにするには, if 文を 使うか, j 番目の次を (j + 1)%n 番目として計算するかのいずれかですが, こ こでは後者の方法を選んでいます. 同じことが i 番目の前の要素を求めると きにも必要です. この考え方に基づくプログラムは, 下に示すように, 非常に簡単です. 5.2. 凸多角形に関する計算 205 //program 5-2-3.c: 凸多角形かどうかの判定 (2) #include <LEDA/window.h> #define MAX 1000 int main() { int n = 0, i; segment s, t; window W (400, 400, "program5-2-3.c"); W.display(window::center, window::center); array<point> p(MAX); while(W >> p[n++]) W << p[n-1]; p[--n] = p[0]; double area = p[0].xcoord()*(p[1].ycoord() - p[n-1].ycoord()) + p[n-1].xcoord()*(p[0].ycoord() - p[n-2].ycoord()); for(i=1; i<=n-2; i++) area += p[i].xcoord()*(p[i+1].ycoord() - p[i-1].ycoord()); if(area < 0) for(i=0; i<n-i; i++) leda_swap(p[i], p[n-i]); for(i=0; i<n; i++) //多角形の辺を順に表示 W << segment(p[i], p[i+1]); int st = 0; for(i=1; i<n; i++) if( p[i].ycoord() < p[st].ycoord() ) st = i; for(i=1; i<n; i++) if(!left_turn(p[(st+i-1)%n], p[(st+i)%n], p[(st+i+1)%n]) || p[(st+i)%n].ycoord() < p[(st+i-1)%n].ycoord() && p[(st+i)%n].ycoord() < p[(st+i+1)%n].ycoord() ) break; if( i==n ) cout << "凸多角形です " << endl; else cout << "凸多角形ではありません " << endl; return 0; } ここまでは多角形の頂点を点配列に入れて管理していましたが, もちろん リスト構造を用いることもできます. まず, 点列の宣言と入力の部分は次のよ うになります. list<point> S; point p; 206 第 5 章 幾何データの取り扱い while(W >> p){ W << p; S.append(p); } 次に多角形の面積を計算します. 3点ずつ取り出して三角形の面積を求め るのですが, 最初は最後の点と最初の2点で三角形を作って面積を求めるこ とから始めて, 後は1つずつ点をずらしながら三角形を構成して行きます. こ こではポインタの扱いに慣れるために, 3つのポインタを使って順に3点の 組を取り出す素朴な方法を紹介しましょう. 3点を表すポインタを最初はリ ストの最後の要素, リストの最初と2番目の要素を指すようにしておきます. その後, 処理が終わるたびに, それぞれのポインタを一つずつ進めます. 具体 的には次の通りです. double area = 0.0; list_item i1, i2, i3; point p1, p2, p3; i1 = S.last(); i2 = S.first(); i3 = S.succ(i2); while(i3){ p1 = S[i1]; p2 = S[i2]; p3 = S[i3]; area += p2.xcoord()*(p3.ycoord() - p1.ycoord()); i1 = i2; i2 = i3; i3 = S.succ(i2); } 面積が負のときにはリストを反転する必要がありますが, LEDA の関数を 使うと, 単にS.reverse(); とするだけで達成できます. 後は, y座標最小の点を求めて, そこからリストが始まるように調整をしな ければなりません. そのために, リストを順にたどり, y座標最小の点の位置 を求めます. i_min = i = S.first(); do{ if(S[i].ycoord() < S[i_min].ycoord() ) i_min = i; i = S.succ(i); } while(i); y座標最小の点 q が求まったところで, 今度はリストが点 q から始まるよう に順序を変更します. リストではポインタを付けかえるだけで順序の変更が 5.2. 凸多角形に関する計算 207 可能になります. まず, 点リスト S を, 点 q までの部分とそれ以降の部分に分 割します. LEDA では, split() という関数を用いてリストの分割ができるの で, この関数を利用します. 後は, 後半のリスト部分の後に前半のリスト部分 を連接すればいいのです. リストの連接には, conc() という関数を用います. 具体的には次の通りです. list<point> S1; S.split(i_min, S, S1, LEDA::before); S.conc(S1, LEDA::before); これで多角形を標準形に変換できたので, 後は面積の計算と同様にして3 点ずつ見て行き, 凸性に反するものがあるかどうかを調べるだけです. これで プログラムのできあがりです. //program 5-2-4.c: 凸多角形かどうかの判定 (3) #include <LEDA/window.h> int main() { window W (400, 400); W.display(); list<point> S; point p, q; while(W>>p){ W << p; S.append(p); } W << segment(S.tail(), S.head()); q = S.tail(); forall(p, S){ W << segment(q, p); q = p; } list_item i, i_min; point p1, p2, p3; double area = 0.0; p1 = S.tail(); i = S.first(); p2 = S[i]; while(S.succ(i)){ p3 = S[S.succ(i)]; area += p2.xcoord()*(p3.ycoord() - p1.ycoord()); 208 第 5 章 幾何データの取り扱い p1 = p2; p2 = p3; i = S.succ(i); } if(area < 0) S.reverse(); i_min = i = S.first(); do{ if(S[i].ycoord() < S[i_min].ycoord() ) i_min = i; i = S.succ(i); } while(i); list<point> S1; S.split(i_min, S, S1, LEDA::before); S.conc(S1, LEDA::before); S.append(S.head()); p1 = S.head(); i = S.succ(S.first()); p2 = S[i]; while(S.succ(i)){ p3 = S[S.succ(i)]; if(!left_turn(p1, p2, p3) || p2.ycoord() < p1.ycoord() && p2.ycoord() < p3.ycoord()) break; p1 = p2; p2 = p3; i = S.succ(i); } if( !S.succ(i) ) cout << "凸多角形です. " << endl; else cout << "凸多角形ではありません. " << endl; } W.close(); return 1; LEDA の機能をフルに活用するなら, 上のプログラムは必要ありません. 多 角形のデータタイプを用いれば, 入力の多角形が凸かどうかは, P.is convex() と直接的に尋ねることができるからです. 次のプログラムは, この機能を用い て簡潔にプログラムの形にしたものです. ただし, 頂点列は反時計回りを基本 にしていますから, 時計回りで点列が入力されたとき(すなわち, 凸多角形の 面積が負になるとき)には, P = P.complement(); として頂点列を逆にしてお く必要があります. 5.3. 凸包の計算 209 //program 5-2-5.c: 凸多角形かどうかの判定 (4) #include <LEDA/window.h> int main() { window W (400, 400); W.display(); } polygon P; W >> P; W << P; if(P.area() < 0) P = P.complement(); if( P.is_convex() ) cout << "凸多角形です. " << endl; else cout << "凸多角形ではありません. " << endl; W.close(); return 1; 5.3 5.3.1 凸包の計算 基本的な考え方 幾何データの効率的な処理法を研究対象とする計算幾何学は 1970 年代に始 まった学問分野ですが, 最初に研究対象となったのが凸包の構成法です. 凸包 とは, 多数の点が与えられたとき, すべての点を含む最小の凸多角形のことを 言います. 3 次元以上では, すべての点を含む最小の凸多面体となります. こ こでは, 2 次元平面上での凸包の構成法について考えてみましょう. ここで紹介する方法は, x 座標に関するソート順に点を取り出して, 前半で は凸包の上部を求め, 後半で凸包の下部を求めるというものです. 以下では x 座標の昇順にソートされた点を p0 ; p1 ; : : : ; pn 1 とします. 凸包の上部を求めるために, 最初の 2 点 p0 ; p1 から始めて, 上部凸包の頂点 を順に列挙していきます. 凸包の形より, p0 ; p1 ; p2 がこの順に上部凸包の頂点 なら, この 3 点で構成される三角形は時計回りの方向をもつはずです. もし時 計回りでなければ, 中央の点は上部凸包上の点でないことが分かります. 求め たいのは, どの連続する 3 頂点をとっても, それで定義される三角形は時計回 210 第 5 章 幾何データの取り扱い りの方向をもっているような極大な点列です. アルゴリズムでは, (p0 ; p1 ) か ら始めて, 点列に頂点を順に加えていきます. いま, 頂点 pi 頂点に関する処理 を終って, (p00 ; p01 ; : : : ; p0k ) という点列が得られているものとします. p0 は左端 の点であり, pi はそれまでに扱った点の中で最も右にある点ですから, 必ず上 記の点列に含まれています. さて, 次の頂点 pi+1 を加えるとき, 最初に現在の点列の最後の 2 点と頂点 pi+1 で定まる三角形 (p0k 1 ; p0k ; pi+1 ) の符号付面積を計算します. これが負で あれば, 対応する三角形の頂点は時計回りに並んでいますから, そのまま次の 頂点への処理に進むことができます. 一方, 符号付面積が正だった場合には, 凹の部分が見つかったので, 最後の頂点 p0k を現在の点列から削除し, さらに その前の頂点 p0k 2 を取り出して, 三角形 (p0 k 2; p0k 1 ; pi+1 ) を調べます. そ れでも符号付面積が正ならば, 同じことを繰り返します. これを繰り返して, 最後の 2 頂点と現在の頂点 pi+1 で定まる三角形の符号付面積が負になったと ころで頂点 pi+1 に関する処理を終り, 次の頂点に関する処理に移ります. 以上の処理の流れをまとめると次のようになります. 点集合を入力し, x 座標の昇順に並べなおしたものを p0 ; p1 ; : : : ; pn とする. 点列 Q を (p0 ; p1 ) に初期設定する. 1 for(i = 2; i < n; i++)f dof Q の最後の 2 要素を取り出し (q1 ; q2 ) とする. correct turn = (q1 ; q2 ; p ) は右回りか. if( !correct turn ) Q から最後の要素 q2 を削除. g while( !correct turn ); 点 p を Q の最後に挿入. i g i 最初の部分は簡単です. 多角形の場合と同じように点集合を配列に蓄えて ソートします. LEDA では配列のソートが簡単だったことを思い出しましょ う. 配列全体に値が入っているときはp.sort() とすればよかったのですが, 点配列 p のサイズは, たとえばMAX=1000 として大きめに宣言されています. 5.3. 凸包の計算 211 その最初の n 個の場所にだけ点データが蓄えられているので, ソートするとき には配列の一部だけを対象にソートすることを示すためにp.sort(0, n-1) とします. 具体的には次の通りです. array<point> p(MAX); while(W >> p[n++]) W << p[n-1]; n--; p.sort(0, n-1); 次に上部凸包を求めます. 凸包を表す点列を管理するのに, ここでは連結 リストを用います. すなわち, 上部凸包を構成する頂点を順に求め, それらを 連結リストの形式で管理するのです. 最初に2つの点をリストに入れた状態 で処理を始めます. その後, x 座標の昇順に点を取りだし, これとリストの最 後の2点とが正しく右回りの順に並んでいるかどうかを確かめます. 正しい 順ならリストに点を加えて, 次の点の処理に移ります. そうでなければ, リス トのひとつ遡って, 別の2点組を取り出して同じ処理を繰り返します. 連結リ ストから最後の 2 つの要素を取り出すにはポップ操作を用いることになるの で, 取り出した点を連結リストに戻す操作が必要になります. また, 三角形の 符号付面積を求めるためにはリストに 2 点以上含まれていなければなりませ ん. その条件も付加すると, プログラムは次のようになります. list<point> CH_A; point q1, q2; bool correct_turn; CH_A.append(p[0]); CH_A.append(p[1]); for(i=2; i<n; i++){ do{ q2 = CH_A.pop_back(); q1 = CH_A.tail(); correct_turn = right_turn(q1, q2, p[i]); if( correct_turn ) CH_A.append(q2); } while( !correct_turn && CH_A.size() >= 2); CH_A.append(p[i]); } 上部凸包が求まれば, 今度は下部凸包を求めます. 少し考えれば分かるこ 212 第 5 章 幾何データの取り扱い とですが, 右回りを正しい方向と決めていたところを, 逆に左回りを正しい方 向と決めるだけです. 最後に上部と下部の凸包を表すリストを連結して 1 つにまとめます. 全体 を環状リストとするためには, 下部凸包を表すリストを逆順にしてから連結 する必要があります. これをプログラムで書くと次のようになります. CH_B.pop_back(); CH_B.reverse_items(); CH_A.conc(CH_B, LEDA::after); 最後に凸包を表示する部分も付け加えた全体のプログラムを下に示します. //program 5-3-1.c: 凸包の計算 #include <LEDA/window.h> #include <LEDA/list.h> #define MAX 1000 int main() { int n = 0, i; window W (400, 400, "program5-3-1.c"); W.display(window::center, window::center); array<point> p(MAX); while(W >> p[n++]) W << p[n-1]; n--; p.sort(0, n-1); list<point> CH_A; point q1, q2; bool correct_turn; CH_A.append(p[0]); CH_A.append(p[1]); for(i=2; i<n; i++){ do{ q2 = CH_A.pop_back(); q1 = CH_A.tail(); correct_turn = right_turn(q1, q2, p[i]); if( correct_turn ) CH_A.append(q2); } while( !correct_turn && CH_A.size() >= 2); 5.3. 凸包の計算 } 213 CH_A.append(p[i]); list<point> CH_B; CH_B.append(p[0]); CH_B.append(p[1]); for(i=2; i<n; i++){ do{ q2 = CH_B.pop_back(); q1 = CH_B.tail(); correct_turn = left_turn(q1, q2, p[i]); if( correct_turn ) CH_B.append(q2); } while( !correct_turn && CH_B.size() >= 2); CH_B.append(p[i]); } } CH_B.pop_back(); CH_B.reverse_items(); CH_A.conc(CH_B, LEDA::after); q1 = CH_A.pop_back(); while( !CH_A.empty() ){ q2 = CH_A.pop_back(); W.draw_segment(q1, q2, red); q1 = q2; } W.read_mouse(); W.close(); return 0; 5.3.2 点のリストを用いたプログラム 上のプログラムでは配列を用いましたが, 最初から連結リストに点データ を入れておけばプログラムが簡単になるはずです. 連結リストに蓄えられて いるデータも同様にソート可能です. 実際のプログラムでは注目する 3 点を 管理するために多少面倒な操作が必要ですが, 基本的にはすっきりとした形 になっています. 214 第 5 章 幾何データの取り扱い //program 5-3-2.c: 凸包の計算 (2) #include <LEDA/window.h> #include <LEDA/list.h> int main() { point p; window W (400, 400, "program5-3-2.c"); W.display(window::center, window::center); list<point> CH_A, CH_B; while(W >> p) { W << p; CH_A.append(p); CH_B.append(p); } CH_A.sort(); CH_B.sort(); bool correct_turn; list_item q1, q2, q3; q1 = CH_A.first(); q2 = CH_A.succ(q1); while( CH_A.succ(q2) ){ q3 = CH_A.succ(q2); do{ correct_turn = right_turn(CH_A[q1], CH_A[q2], CH_A[q3]); if( !correct_turn ){ CH_A.erase(q2); q2 = q1; q1=CH_A.pred(q1); } } while( !correct_turn && q1 ); q1 = q2; q2 = q3; } q1 = CH_B.first(); q2 = CH_B.succ(q1); while( CH_B.succ(q2) ){ q3 = CH_B.succ(q2); do{ 5.3. 凸包の計算 } 215 correct_turn = left_turn(CH_B[q1], CH_B[q2], CH_B[q3]); if( !correct_turn ){ CH_B.erase(q2); q2 = q1; q1=CH_B.pred(q1); } } while( !correct_turn && q1 ); q1 = q2; q2 = q3; CH_B.pop_back(); CH_B.reverse_items(); CH_A.conc(CH_B, LEDA::after); q1 = CH_A.first(); while( (q2=CH_A.succ(q1)) ){ W << segment(CH_A[q1], CH_A[q2]); q1 = q2; } } W.read_mouse(); W.close(); return 0; ここまでは点のデータをウィンドウから入力していましたが, 点のデータ をファイルから読取ることもできます. 具体的には, 図 5.5 のように座標値を 単に並べただけのファイルを作ります. 図 5.5 には対応する凸包も表示して あります. \chull.dat" という名前のデータファイルファイルから x; y 座標を 読み取って, 点を管理するリスト L に蓄えるには次のようにします. ifstream fin ("chull.dat"); list<point> L; point p; while(fin >> p) { W << p; L.append(p); } 下のプログラムでは, データファイルの名前を入力して, そこからデータを 読取るようになっています. 残りの部分は以前と同じです. ウィンドウからの 入力とファイルからの入力が同じように扱えることを再確認しましょう. 216 第 5 章 幾何データの取り扱い 20.0 21.1 40.2 36.4 67.3 85.0 15.3 14.4 30.3 60.5 80.2 50.3 30.1 68.8 40.3 72.1 66.7 45.1 61.8 50.1 50.8 55.9 27.7 20.2 データファイル"chull.dat"の 内容 対応する凸包 図 5.5: データファイルとプログラム 5-3-3.c の出力 //program 5-3-3.c: 凸包の計算 (3) #include <LEDA/window.h> #include <LEDA/list.h> int main() { string fname; cout << "Enter Data File Name "; cin >> fname; ifstream fin (fname); point p; window W (400, 400, "program5-3-3.c"); W.display(window::center, window::center); list<point> CH_A, CH_B; while(fin >> p) { W << p; 5.3. 凸包の計算 217 CH_A.append(p); CH_B.append(p); } CH_A.sort(); CH_B.sort(); bool correct_turn; list_item q1, q2, q3; q1 = CH_A.first(); q2 = CH_A.succ(q1); while( CH_A.succ(q2) ){ q3 = CH_A.succ(q2); do{ correct_turn = right_turn(CH_A[q1], CH_A[q2], CH_A[q3]); if( !correct_turn ){ CH_A.erase(q2); q2 = q1; q1=CH_A.pred(q1); } } while( !correct_turn && q1 ); q1 = q2; q2 = q3; } q1 = CH_B.first(); q2 = CH_B.succ(q1); while( CH_B.succ(q2) ){ q3 = CH_B.succ(q2); do{ correct_turn = left_turn(CH_B[q1], CH_B[q2], CH_B[q3]); if( !correct_turn ){ CH_B.erase(q2); q2 = q1; q1=CH_B.pred(q1); } } while( !correct_turn && q1 ); q1 = q2; q2 = q3; } CH_B.pop_back(); CH_B.reverse_items(); CH_A.conc(CH_B, LEDA::after); q1 = CH_A.first(); 218 第 5 章 幾何データの取り扱い while( (q2=CH_A.succ(q1)) ){ W << segment(CH_A[q1], CH_A[q2]); q1 = q2; } } W.read_mouse(); W.close(); return 0; 5.3.3 LEDA ライブラリの利用 凸包は計算幾何学における主要なテーマですから, LEDA でももちろん幾 つかのライブラリ関数が用意されています. たとえば, CONVEX HULL() と いう関数は, 引数で指定された点のリスト L に対する凸包を同じく点のリス トとして返します. 引数は同じですが, 凸包を polygon データタイプで返す関 数 CONVEX HULL POLY() も用意されています. たとえば, 後者の関数を 用いた場合, プログラム 5-3-2.c は次のように非常に簡単になります. //program 5-3-4.c: 凸包の計算 (4) #include <LEDA/window.h> #include <LEDA/plane_alg.h> #include <LEDA/list.h> int main() { window W (400, 400, "program5-3-4.c"); W.display(window::center, window::center); list<point> L; point p; while(W >> p) { W << p; L.append(p); } W << CONVEX_HULL_POLY(L); W.read_mouse(); W.close(); return 0; 5.4. 計算誤差対策 219 } 5.4 計算誤差対策 グラフを扱う問題と違って幾何データを扱う問題では計算誤差による誤動 作に気をつけなければなりません. 人間の目で見て間違うはずがないと思わ れる箇所で誤った計算結果が得られることがあります. 単純な計算問題であ れば多少計算結果に誤差があっても大きな問題とはなりませんが, 計算結果 によって異なる処理を行う場合には, 予期しない事態が生じる可能性があり, 最悪の場合にはプログラムが暴走してしまうことが考えられます. 5.4.1 凸包の計算における注意点 ここでは 2 つ例をあげて計算誤差に対する対処法を説明することにします. 最初の例は上で説明した凸包に関するものです. 前節の説明の通り, 点データ を入力した後, x 座標の昇順に点データを走査して, 凹部を見つければ点を現 在の凸包から取り除いて行くというのが基本的な考え方です. 上部凸包を構 成するときには, いま加えようとしている点と, 現在の凸包上の最後の 2 点で 三角形を作り, それが正しく右回り(時計回り)になっているかどうかを判定 しました. もし向きが逆なら, その 3 点で凹部を形成しているわけですから, 中央の点(この場合は現在の凸包の最後の頂点)を取り除きます. この操作 を繰り返していくと上部凸包が得られるはずです. 実際, ウィンドウ上で点を入力してプログラムを実行してみると, ほとんど 問題なく凸包を計算してくれます. しかし, 厳密に考えてみると上の考え方で は問題が発生することがあります. まず, 暗黙のうちに 3 点が同一直線上にあ ることはないと仮定していないでしょうか. デモプログラムのような小規模 のデータを扱う場合には可能性が低いのですが, 実際の大規模なデータでは 3 点以上が同一直線上にある可能性は決して否定できません. では, 3 点が同一 直線上にあった場合にはどうすればいいでしょう. やはり中央の点を削除す るというのが妥当な考え方でしょう. したがって, 中央の点を削除しないとい 220 第 5 章 幾何データの取り扱い けないのは, 注目している 3 点が同一直線上にあるか, または左回りになって いる場合です. つまり, 右回りになっていないとき(正確には, 符号付面積が 負でないとき)です. このような事情を考慮して, プログラム 5-3-2.c では correct_turn = right_turn(CH_A[q1], CH_A[q2], CH_A[q3]); としているのです. これで理論的には問題が片付きました. でも, 何か暗黙のうちに仮定してい ないでしょうか. そうです, 計算誤差のことを考慮していないのです. C言語 では浮動小数点数を固定長のビット列で表しています. つまり, 有効桁数は固 定されているのです. したがって, 有効桁数を超える精度が要求されるような 計算を実行すると, 計算結果は誤差を含んでしまうのです. 具体的に点集合 p0 (10; 70); p1 (10; 10 ); p2 (20; 20 ); : : : ; p7 (70; 70 ) の凸包を求めてみましょう. この点集合は, 左下の p1 から順に p7 まで直線 y = x 上に等間隔に並んだ 7 個の点と左上の点 p0 で構成されています. し たがって, 計算誤差がなければ p0 ; p1 ; p7 の 3 点だけからなる凸包が出力され るはずです. これを確かめるために, LEDA ライブラリの関数を用いたプロ グラム 5-3-4.c の入力部だけ変えた次のプログラムで試してみましょう. f //program 5-4-1.c: 凸包の計算 (5) #include <LEDA/window.h> #include <LEDA/plane_alg.h> #include <LEDA/list.h> int main() { double eps = read_real("Enter a small number "); window W (400, 400, "program5-4-1.c"); W.display(window::center, window::center); list<point> L; for(double x=10.0; x<80.0; x=x+10) L.append( point(x, x-eps) ); L.append( point(10.0, 70.0) ); polygon chull = CONVEX_HULL_POLY(L); W << chull; g 5.4. 計算誤差対策 } 221 cout << "Convex hull: " << chull; W.read_mouse(); W.close(); return 0; このプログラムでは最初に の値を入力するようにしています. プログラ ムを実行してみると, 次のような結果が得られます. プログラム 5-4-1.c の実行例 > 5-4-1 Enter a small number input: 0.1 Convex hull: (20,19.9) (60,59.9) (70,69.9) (10,90) (10,9.9) 凸包は 3 点だけから成るはずですが, ここでは余分に 2 点が出力されてい ます. 途中の点がすべて凸包上の点として出力されるのならともかく, 途中の 5 点のうち 3 点は凸包上になく, 2 点だけが凸包上にあると判断されたわけで す. ただ, ライブラリ関数の中でどのような計算が行われているか分からない ので, 今度はライブラリ関数を使わないで同じ計算をしてみましょう. ここで は, プログラム 5-3-2.c に基づいた次のプログラム 5-4-2.c で試します. //program 5-4-2.c: 凸包の計算 (6) #include <LEDA/window.h> #include <LEDA/plane_alg.h> #include <LEDA/list.h> int main() { double eps = read_real("Enter a small number "); window W (400, 400, "program5-4-2.c"); W.display(window::center, window::center); list<point> CH_A, CH_B; for(double x=10.0; x<80.0; x=x+10){ CH_A.append( point(x, x-eps) ); CH_B.append( point(x, x-eps) ); } CH_A.append( point(10.0, 70.0) ); 222 第 5 章 幾何データの取り扱い CH_B.append( point(10.0, 70.0) ); CH_A.sort(); CH_B.sort(); bool correct_turn; list_item q1 = CH_A.first(); list_item q2 = CH_A.succ(q1); while( CH_A.succ(q2) ){ list_item q3 = CH_A.succ(q2); do{ correct_turn = right_turn(CH_A[q1], CH_A[q2], CH_A[q3]); if( !correct_turn ){ CH_A.erase(q2); q2 = q1; q1=CH_A.pred(q1); } } while( !correct_turn && q1 ); q1 = q2; q2 = q3; } q1 = CH_B.first(); q2 = CH_B.succ(q1); while( CH_B.succ(q2) ){ list_item q3 = CH_B.succ(q2); do{ correct_turn = left_turn(CH_B[q1], CH_B[q2], CH_B[q3]); if( !correct_turn ){ CH_B.erase(q2); q2 = q1; q1=CH_B.pred(q1); } } while( !correct_turn && q1 ); q1 = q2; q2 = q3; } CH_B.pop_back(); CH_B.reverse_items(); CH_A.conc(CH_B, LEDA::after); cout << "Convex hull: " << CH_A << endl; q1 = CH_A.first(); while( (q2=CH_A.succ(q1)) ){ W << segment(CH_A[q1], CH_A[q2]); 5.4. 計算誤差対策 } 223 q1 = q2; } W.read_mouse(); W.close(); return 0; 上のプログラムを同じ の値で試してみると, 下の実行例に示すように, や はり同じ結果が得られます. したがって, 2 つのプログラムは同じ計算を行っ ているようです. プログラム 5-4-2.c の実行例 > 5-4-2 Enter a small number input: 0.1 Convex hull: (20,19.9) (60,59.9) (70,69.9) (10,90) (10,9.9) では, の値をもっと小さいくしてみましょう. たとえば, みましょう. そうすると結果は次のようになります. = 0:001 として プログラム 5-4-2.c の実行例 > 5-4-2 Enter a small number input: 0.001 Convex hull: (10,9.999) (10,70) (70,69.999) (10,9.999) 予想に反して, 今度は正しい結果が得られました. このように計算誤差に よる影響はかなり予想しにくいと言えます. LEDA では誤差を伴わない計算 方法を提供していますが, それについては次の例題の後で説明することにし ます. 5.4.2 交差判定 次の例題として, 2 直線の交点と別の直線との上下関係を調べるプログラム について考えてみましょう. 多数の線分の間の交点を列挙する問題は計算幾 何学における主要な問題の一つで, これまでに多数のアルゴリズムが提案さ れています. 中でも平面走査法に基づいたアルゴリズムが計算幾何学のテキ 224 第 5 章 幾何データの取り扱い ストではよく取り上げられているようです. 平面走査法は, 垂直な走査線を左 無限遠から右方向に走査していきながら, 走査線と交差する線分の上下関係 から交差判定の対象となる線分対を効率良く求めようという方法です. ここ では平面走査法自身は説明しませんが, そこで重要な線分間の上下関係の判 定について考えてみましょう. (x2,y2) (x3,y3) s1 s2 (x1,y1) (x4,y4) 図 5.6: 2本の線分の交差 2 本の線分 s1 (x1 ; y1 ; x2 ; y2 ) と s2(x3 ; y3 ; x4 ; y4 ) (図 5.6 参照)が交わるのは, ! x x (1 ) 1 + 2 y1 y2 を満たす (0 < について解くと, ! ! x x = 3 + (1 ) 4 y3 y4 ! < 1) と (0 < < 1) が存在するときです. これを と (y4 y3 )(x4 x1 ) (x4 x3 )(y4 y1 ) (x2 x1 )(y4 y3 ) (y2 y1 )(x4 x3 ) (y2 y1 )(x4 x1 ) + (x2 x1 )(y4 y1 ) = (x2 x1 )(y4 y3 ) (y2 y1 )(x4 x3 ) = となります. このようにして計算された と がともに 0 と 1 の間の数なら ば, 2 線分は交差すると判定されます. 交差する場合には, その交点 (x; y ) を ! ! x x x = 1 + 2 y y1 y2 ! として求めることができます. この交点が線分 s3 (x5 ; y5 ; x6 ; y6 ) よりも上に 5.4. 計算誤差対策 225 あるかどうかは, y> y6 y5 (x x 5 ) + y 5 x6 x5 を満たすかどうかで判定できます. 計算誤差が特に生じやすいのは 2 本の線分がほぼ同じ傾きをもっている場 合です. そこで, 人為的に 1 点で交差する 3 本の線分を定義してみます. 出力 は, 理想的には, 最初の 2 線分の交点は 3 番目の線分上にある, となるはずで すが, 果たしてどうでしょう. 下のプログラムでは, 最初に交点の座標を入力 した後, 傾きが Æ だけ異なる 3 本の直線によって 3 本の線分を定義しています. Æ の値が大きいときは正しい結果が得られるはずですが, Æ = 0:15 では正しい 結果が得られるのに, Æ = 0:2 とすると間違った結果が得られてしまいます. //program 5-4-3.c: 線分の交点と線分の上下関係 #include <LEDA/window.h> int main() { double p=10, q=90, xc=50, yc=50, delta, m1, m2, m3; delta = read_real("Enter delta value "); m2 = 0.879; m1 = m2 - delta; m3 = m2 + delta; double x1,y1,x2,y2,x3,y3,x4,y4,x5,y5,x6,y6; x1=p; x2=q; x3=p; x4=q; x5=p; x6=q; y1=m1*(p-xc)+yc; y2=m1*(q-xc)+yc; y3=m2*(p-xc)+yc; y4=m2*(q-xc)+yc; y5=m3*(p-xc)+yc; y6=m3*(q-xc)+yc; double a = (y4-y3)*(x4-x1) - (y4-y1)*(x4-x3); double b = -(y2-y1)*(x4-x1) + (y4-y1)*(x2-x1); double c = (y4-y3)*(x2-x1) - (y2-y1)*(x4-x3); window W (400, 400, "program5-4-3.c"); W.display(window::center, window::center); W << segment(x1,y1, x2,y2); W << segment(x3,y3, x4,y4); W << segment(x5,y5, x6,y6); 226 } 第 5 章 幾何データの取り扱い double lambda = a/c; double mu = b/c; if(0 < lambda && lambda < 1 && 0 < mu && mu < 1) { cout << "s1 and s2 intersect "; double x = x1 + lambda*(x2-x1); double y = y1 + lambda*(y2-y1); cout << "at " << point(x, y) << endl; if(x5 == x6){ cout << "no vertical line segment is allowed " << endl; return 0; } else { double d = y - (y6-y5)/(x6-x5)*(x-x5)-y5; if(d > 0) cout << "the intersection is above s3." << endl; else if(d < 0) cout << "the intersection is below s3." << endl; else cout << "the intersection is on s3." << endl; } } else cout << "s1 and s2 do not intersect " << endl; W.read_mouse(); return 0; 実際にプログラム 5-4-3.c を Æ の値を変えて実行した結果を下に示します. 理想的には, どんな Æ の値に対しても「交点は s3 の上にある」という出力が 得られるはずですが, 実際には 3 通りの異なる出力が出てしまいます. 5.4. 計算誤差対策 プログラム 5-4-3.c の実行例 Æ > 5-4-3 Enter delta value 0.1 s1 and s2 intersect at (50,50) the intersection is on s3. > 5-4-3 Enter delta value 0.2 s1 and s2 intersect at (50,50) the intersection is above s3. > 5-4-3 Enter delta value 0.01 s1 and s2 intersect at (50,50) the intersection is below s3. 5.4.3 227 有理数を用いた誤差なし計算 上では計算誤差について説明しましたが, 最初の例題ではプログラムが暴 走してしまうという最悪の事態には繋がらないものの, 2 番目の例題では順序 関係が異なってしまうので, 順序関係に基づいて探索を行う平面走査法など では致命的なエラーとなります. そこで, 計算誤差に対して頑強なアルゴリズ ムが望まれます. 如何にして頑健なアルゴリズムを設計するかは計算幾何学 における主要な話題で, 多くの優れた研究がなされてきました. LEDA が提 供している解決方法は, 計算誤差が生じないように計算を行おう, というもの です. 計算誤差が生じるのは, 数値を固定長で管理するからですから, 計算結 果に応じて数値を表現するためにビット長を増やしてやれば, 計算誤差はな くなることになります. ところが, 浮動小数点数の形式のままでは困難な点が ありますので, LEDA では有理数を浮動小数点数の代わりに用いることにし ています. 有理数を表す rational データタイプについては第 3 章で説明しま したが, このデータタイプを用いて点や線分などの幾何データを表現するこ ともできます. point p; とすると, double 型の x; y 座標をもつ点 p が作られますが, rat_point p; 228 第 5 章 幾何データの取り扱い とすると, rational 型の x; y 座標をもつ点 p が作られます. 線分についても同 様です. 単に rat を前につけるだけなのです. では, 有理数を用いて先の交差判定のプログラムを書きなおしてみましょ う. ウィンドウの扱いが面倒なので省略しますが, 基本的には double と宣言 されていたものを rational の宣言に直しただけです. プログラムは次のよう になります. //program 5-4-4.c: 線分の交点と線分の上下関係 (2) #include <LEDA/rational.h> int main() { rational p=10, q=90, xc=50, yc=50, delta, m1, m2, m3; delta = read_real("Enter delta value "); m2 = 0.879; m1 = m2 - delta; m3 = m2 + delta; rational x1,y1,x2,y2,x3,y3,x4,y4,x5,y5,x6,y6; x1=p; x2=q; x3=p; x4=q; x5=p; x6=q; y1=m1*(p-xc)+yc; y2=m1*(q-xc)+yc; y3=m2*(p-xc)+yc; y4=m2*(q-xc)+yc; y5=m3*(p-xc)+yc; y6=m3*(q-xc)+yc; rational a = (y4-y3)*(x4-x1) - (y4-y1)*(x4-x3); rational b = -(y2-y1)*(x4-x1) + (y4-y1)*(x2-x1); rational c = (y4-y3)*(x2-x1) - (y2-y1)*(x4-x3); rational lambda = a/c; rational mu = b/c; if(0 < lambda && lambda < 1 && 0 < mu && mu < 1) { cout << "s1 and s2 intersect "; rational x = x1 + lambda*(x2-x1); rational y = y1 + lambda*(y2-y1); cout << "at " << "(" << x << ",\n" << y << ")" << endl; if(x5 == x6){ cout << "no vertical line segment is allowed " << endl; return 0; } else { rational d = y - (y6-y5)/(x6-x5)*(x-x5)-y5; 5.4. 計算誤差対策 229 if(d > 0) cout << "the intersection is above s3." << endl; else if(d < 0) cout << "the intersection is below s3." << endl; else cout << "the intersection is on s3." << endl; } } } else cout << "s1 and s2 do not intersect " << endl; return 0; Æ の値を 0:01 としたときのプログラムの出力 5-4-4.c の実行例を下に示しま す. ここでは出力結果を 1 通りしか示しませんが, どんな Æ の値に対しても正 しい結果が得られます. ただ, 正しい結果を得るために, 非常に大きな整数を 分母と分子にもつ有理数が計算されていることも出力結果から分かるでしょ う. 下の出力において \/" は分子と分母の整数の境界を表しています. この ように, 有理数を用いた計算では分母と分子が非常に大きくなって計算の効 率が低下することがあるので, ときどき分母と分子を最大公約数で割る約分 の演算q.normalize() を実行する必要があります. > 5-4-4 Enter delta value 0.01 s1 and s2 intersect at (61565634681866375050778221709020446007 45011631183352918029192185812560223618935627366662649164201070 2537608396800000/123131269363732750101556443418040892014900232 62366705836058384371625120447237871254733325298328402140507521 67936000,10373788922202482612228881797301329252436963949533221 47573460500271863041736853785136747149459917726103444764951501 31161521915880975324041837139577490112942900616831900316388556 800000/2074757784440496522445776359460265850487392789906644295 14692100054372608347370757027349429891983545220688952990300262 32304383176195064808367427915498022588580123366380063277711360 00) the intersection is on s3. プログラム 5-4-4.c の実行例 230 第 5 章 幾何データの取り扱い 計算誤差に関してもう一つ例題をあげましょう. 今度も線分の交点に関す るものです. 2 本の線分の交点はもちろん両方の線分に含まれます. ところが, この当たり前のことが常に成り立つとは限らないのです. 次のプログラムで は, 乱数で作った点対で決まる 2 直線 l1; l2 の交点 p が l1 と l2 に含まれるか どうかを判定しています. //program 5-4-5.c: 2 直線の交点は両方の直線上にあるか? #include <LEDA/point.h> #include <LEDA/line.h> #include <LEDA/random_point.h> int main() { int count=0, max_coord=10000; float T = used_time(); for(int i=0; i<100000; i++){ point a, b, c, d, p; random_point_in_square(a, max_coord); random_point_in_square(b, max_coord); random_point_in_square(c, max_coord); random_point_in_square(d, max_coord); line l1(a, b), l2(c, d); if( !l1.intersection(l2, p) || l1.contains(p) && l2.contains(p) ) count++; } cout << count << endl; cout << "計算時間 = " << used_time(T) << endl; return 0; } プログラム 5-4-5.c において, random point in square() は, 2 番目の引数 で指定された値の逆数を刻み幅として, 単位正方形内の点をランダムに定め て, その座標を 1 番目の引数に代入するという関数です. このようにして 4 点 a; b; c; d を生成し, a; b と通る直線 l1 と c; d と通る直線 l2 を定めます(図 5.7 参照). l1 と l2 が交差するかどうかを判定して, 交差するならその交点を point 型の変数 p に代入するのが l1.intersection(l2, p) です. 生成された 2 直 5.4. 計算誤差対策 231 線が平行なら, もちろん交点は存在しません. プログラムでは, 「交点が存在し ないか, 交点が存在すれば, その交点が両方の直線上にあるかどうか」を判定 しています. プログラムでは, 交点が存在するかどうかを, l1.intersection(l2, p) によって判定しています. また, 交点 p が両方の直線上にあるかどうかは, l1.contains(p) && l2.contains(p) によって判定しています. 数学的には, 2 直線が平行で交差していれば, その交点は必ず元の直線の上 にありますから, プログラムの最後の if 文の条件式は絶対に成り立つはずで す. すべて正解ならば, 100000 という数値が出力されるはずです. では, プロ グラム 5-4-5.c を実行してみましょう. プログラム 5-4-5.c の実行例 > 5-4-5 10793 計算時間 = 1.11 上の実行結果が示しているように, 実に 9 割近くが不正解なのです. そもそ も, 交点の座標は本節の最初で説明したように, 分母と分子が座標値の乗算を 含む式で計算されますから, 正確に求めるには座標を表すビット数の約4倍 のビット数(計算精度)が必要になります. 4倍のビット数を確保せずに同 じビット数の変数を使って計算していますので, むしろ1割強も正解があった ということの方が驚きなのです. 先の例題と同じく, 浮動小数点数を用いているとどうしても計算誤差を免 れることはできません. point 型の値は x; y 座標を表す double 型の数値の組 で定義されていますが, rat point 型では座標値を有理数で表現しますので, 誤 差のない計算を行うことができます. そこで, rat point 型を使ってプログラ ム 5-4-5.c を書きなおしてみましょう. double 型で定義されている箇所をすべ て有理数型のものに変更すればいいのですから, point の宣言を rat point に, line の宣言を rat line の宣言に変更すればいいのです. たったこれだけです. 念のため変更後のプログラムを下に示します. //program 5-4-6.c: 2 直線の交点は両方の直線上にあるか?(2) #include <LEDA/rat_point.h> #include <LEDA/rat_line.h> 232 第 5 章 幾何データの取り扱い c b p a l1 l2 d 図 5.7: 4点で定まる2本の直線とその交点 #include <LEDA/random_rat_point.h> int main() { int count=0, max_coord=10000; float T = used_time(); for(int i=0; i<100000; i++){ rat_point a, b, c, d, p; random_point_in_square(a, max_coord); random_point_in_square(b, max_coord); random_point_in_square(c, max_coord); random_point_in_square(d, max_coord); rat_line l1(a, b), l2(c, d); if( !l1.intersection(l2, p) || l1.contains(p) && l2.contains(p) ) count++; } cout << count << endl; cout << "計算時間 = " << used_time(T) << endl; return 0; } 5.4. 計算誤差対策 233 プログラム 5-4-6.c の実行例 > 5-4-6 100000 計算時間 = 3.9 今度は実行例からも分かるように, すべて正解です. ここでは計算時間も測 定していますが, プログラム 5-4-5.c では 1.15 秒で計算できていたのに, 今度 は 3.9 秒かかっています. このように多少の計算時間は犠牲になりますが, 結 果の信頼性を考えるとこの程度のオーバヘッドは仕方がないところでしょう. 5.4.4 有理数に基づく幾何データタイプ 上の例題からも明らかなように, 幾何データを表現するのに浮動小数点数を 用いる場合と有理数を用いる場合があり, 実行時間は余分にかかるものの, 後 者の場合には計算誤差をなくすことができるという利点があります. 浮動小 数点数の表現に基づいたプログラムを有理数に基づいたプログラムになおす のも簡単です. 大まかに言えば, point p; や segment s; のような浮動小数点数 型の宣言を, rat point p; と rat segment s; のような有理数型の宣言に変更す ればいいのです. LEDA では両者の変換をもっと簡単にするために, point と rat point の両方を表すデータタイプが用意されています. それが大文字で表 記した POINT や SEGMENT のようなタイプです. POINT が point を表す のか rat point を表すのかは, プログラムの先頭で区別できます. circle, ray, line などについても, CIRCLE, RAY, LINE という表現が用意されています. 浮動小数点数型を用いる場合には, #include <LEDA/float_kernel.h> #include <LEDA/float_kernel_types.h> とすればよく, 有理数型を用いる場合には #include <LEDA/rat_kernel.h> #include <LEDA/rat_kernel_types.h> とすればいいのです. たとえば, プログラム 5-4-6.c は次のように書くことが できます. 234 第 5 章 幾何データの取り扱い //program 5-4-7.c: 2 直線の交点は両方の直線上にあるか?(3) #include <LEDA/rat_kernel.h> #include <LEDA/rat_kernel_types.h> #include <LEDA/random_rat_point.h> int main() { int count=0, max_coord=10000; float T = used_time(); for(int i=0; i<100000; i++){ POINT a, b, c, d, p; random_point_in_square(a, max_coord); random_point_in_square(b, max_coord); random_point_in_square(c, max_coord); random_point_in_square(d, max_coord); LINE l1(a, b), l2(c, d); if( l1.intersection(l2, p) && (!l1.contains(p) || !l2.contains(p)) ) count++; } cout << count << endl; cout << "計算時間 = " << used_time(T) << endl; return 0; } 基本的に最初の3行を書きなおすと, 浮動小数点数型のプログラムができ あがります. //program 5-4-8.c: 2 直線の交点は両方の直線上にあるか?(4) #include <LEDA/float_kernel.h> #include <LEDA/float_kernel_types.h> #include <LEDA/random_point.h> int main() { int count=0, max_coord=10000; float T = used_time(); for(int i=0; i<100000; i++){ POINT a, b, c, d, p; 5.5. ボロノイ図とその利用 235 random_point_in_square(a, max_coord); random_point_in_square(b, max_coord); random_point_in_square(c, max_coord); random_point_in_square(d, max_coord); LINE l1(a, b), l2(c, d); if( l1.intersection(l2, p) && (!l1.contains(p) || !l2.contains(p)) ) count++; } } cout << count << endl; cout << "計算時間 = " << used_time(T) << endl; return 0; ここでは有理数を用いて誤差なし計算を実現することで計算誤差による影 響を排除しようという考え方を述べましたが, 解決方法はこれだけではあり ません. たとえば上の問題について考えると, そもそも 2 直線の交点が元の 直線上にあるかどうかを尋ねること自体がナンセンスなのです. それは絶対 的な真実ですから, 数値計算の結果よりもその事実を優先すべきなのです. こ の例は非常に単純ですが, 幾何に関連する問題では幾何的に必ず成り立つ性 質がありますから, その幾何的性質を数値計算の結果より優先してアルゴリ ズムを設計しようという考え方があります. 位相優先法1 と呼ばれる方法で, 多数の優れた研究結果が知られています. 5.5 5.5.1 ボロノイ図とその利用 ボロノイ図を描こう 計算幾何学では様々なアルゴリズムが知られていますが, 中でもボロノイ図 は他の分野にも応用されることが多いので, よく知られています. ボロノイ図 とは, 一言で言うと, 平面上に多数の点が与えられたとき, 平面をどの点に一 1 位相優先法については次の論文が分かりやすいと思います:杉原厚吉: 「数値誤差と幾 何学的整合性 | 暴走の心配のないアルゴリズムをめざして |」, 電子情報通信学会誌, 76 巻 6 号(1993 年 6 月), pp.618-625. 236 第 5 章 幾何データの取り扱い 番近いかで分割したものです. 図 5.8 は, ボロノイ図の例を示したものです. 図 5.8: ボロノイ図の例 2点に関する等距離線はその2点の垂直2等分線ですから, ボロノイ図は基 本的に点対ごとの垂直2等分線から構成されています. 点集合が2点だけか らなる場合には, 2点の垂直2等分線を引けばボロノイ図が完成します. 3点 から成る場合には, それらが1直線上にない限り, 点対ごとの垂直2等分線は 1点で交わることになります. このとき, ボロノイ図はこの交点から無限に延 びる3本の半直線から構成されることになります. 一般に, ボロノイ図は, 両 端点をもつ線分と一方の端点が無限遠点にある半直線から構成されます. ボ ロノイ図を構成する線分をボロノイ辺と呼び, その端点をボロノイ頂点と呼 5.5. ボロノイ図とその利用 237 びます. 図では最初に与えた点(母点と呼びます)も, ボロノイ頂点も小さな 円で表現しています. 3点の場合で見たように, ボロノイ頂点というのは, 実 は3個の母点を通る円の中心になっています. このようにボロノイ図は基本的には点対ごとに垂直2等分線を求めれば構 成できます. では, 点集合を入力で与えてボロノイ図を構成するプログラムを 書くのは簡単でしょうか?アルゴリズムの効率を無視すると, すべての点対に ついて垂直2等分線を求めて, それらをうまく繋ぎ合せればよいわけですが, これが結構面倒です. 新たな垂直2等分線を加えるとき, 既存のボロノイ辺と の交点をすべて求め, 必要なら既存のボロノイ辺を分割したり, 削除したりす ることによってボロノイ図を更新しなければならないのですが, そのために はグラフを動的に管理するデータ構造が必要になります. すべての点対について垂直2等分線を計算しながらボロノイ図を動的に更 新していくという腕力法では母点の個数の2乗に比例する時間がかかってし まいますから, 計算幾何学では効率を改善するために様々なアルゴリズム上 の工夫が考案されています. もちろん, LEDA にもボロノイ図を計算する関 数が用意されています. 母点の集合を点のリスト S として与えると, VORONOI(S, VD); として関数 VORONOI() を呼び出すだけでボロノイ図 VD を(有向)グラフ の形で求めてくれます. これだけでボロノイ図が計算できてしまいます. しかし, この関数を呼び出 しただけではボロノイ図をウィンドウ上に描いてくれるわけではありません. そこで, ここではグラフ構造をもったボロノイ図 VD をウィンドウ上に表示 するプログラムを考えることにします. そのためには, ボロノイ図がどのよう なグラフとして表現されているかを知らなければなりません. LEDA ではボ ロノイ図をパラメータつきの有向グラフとして表わしています. 具体的には, 次の通りです. GRAPH<circle, point> VD; これは, ボロノイ図の各頂点には円の情報を与え, 各辺には点の情報を与える ことを意味しています. 先にも述べましたように, ボロノイ頂点はそこで交差 238 第 5 章 幾何データの取り扱い circle(c,b,b) c b circle(b,c,d) b c c b d c d a circle(a,c,d) c a circle(a,c,c) d d d a a b circle(a,b,d) b a circle(b,a,a) 図 5.9: ボロノイ頂点とボロノイ辺に関連する情報 する3本のボロノイ辺に関連する3個の母点によって決まりますが, それら 3点を通る円が各ボロノイ頂点に関連付けられています. 図 5.9 を参照して ください. ただし, 無限遠にあるボロノイ頂点については3点を通る円を関連 付けることができませんから, その頂点に接続するボロノイ辺を決める2点 を重複して用いて(縮退した, つまり無限大の半径をもつ)円を関連付けて います. したがって, 有限の場所にあるボロノイ頂点の座標は, 関連する円の 中心座標として求めることができます. 具体的には, v をボロノイ図 VD の節 点とすると, VD[v].center() が円の中心を表しています. では, 無限遠にあるボロノイ頂点かどうかはどのようにして判定できるで しょう. ボロノイ図は有向グラフの形で表現されていますから, 有限の場所 にあるボロノイ頂点は3以上の次数をもちますが, 無限遠にあるボロノイ頂 点からは1本の有向辺しか出ていませんから, その出次数は1です. したがっ て, すべてのボロノイ頂点の場所に小さな円を描くには次のようにすればよ いことが分かります. node v; forall_nodes(v, VD) { 5.5. ボロノイ図とその利用 } 239 if(VD.outdeg(v) < 2) continue; // 無限遠の頂点は無視 point p = VD[v].center(); // ボロノイ頂点を特徴づける円の中心 W.draw_circle(p, 1, green); 次にボロノイ辺を描きましょう. ボロノイ辺 e は有向辺ですが, その始点は として求めることができます. 両端点 が共に有限の場所にある場合には, それぞれに対応する点を結ぶ線分を描け ばいいのですが, 無限遠の頂点が含まれる場合には半直線を求める必要があ ります. 終点 v だけが無限遠にある場合を考えてみましょう. 始点 u から終 点 v に向かう半直線を求めるのに, 無限遠にあるボロノイ頂点 v を特徴付け る円を定める3個の母点を用います. この円は無限大の半径をもちますが, こ の円を特徴づける3個の母点のうち, 1番目のものは有向辺 uv の左にある母 点であり, 3番目のものは uv の右にある母点です. これは, 3番目の点から 1番目の点に向かうベクトルを時計回りの方向に 90 度だけ回転すると, ちょ うど半直線の方向に一致することになります. この性質を利用すると, 次のよ うにして半直線を描くことができます. u = source(e);, 終点は v = target(e); if(VD.outdeg(u) ==1 && VD.outdeg(v)>=1){ vector vec = VD[u].point3() - VD[u].point1(); point cv = VD[v].center() + vec.rotate90(); W.draw_ray(VD[v].center(), cv, blue); } 基本的にはボロノイ図のすべての辺について, 両端点の性質によって場合 分けして線分か半直線を描けばいいのですが, ボロノイ図は互いに反対の方 向をもつ有向辺から構成されていましたから, このままでは各辺がちょうど 2度ずつ描画されることになります. 各辺を一度しか描画しないようにする ために, ある有向辺を描画したとき, その逆方向の辺を後で描画しないように, 疎配列 map を用いて対応する辺が描画済みかどうかを管理します. 具体的に は, 有向辺 e に対応する逆方向の辺は VD.reverse(e) として取り出します. 以上を総合すると, ボロノイ図を描くプログラムが完成します. ただし, 下 のプログラムでは母点が2点だけの場合にも対応できるように, ボロノイ辺 が直線となる場合も考慮しています. 240 第 5 章 幾何データの取り扱い //program 5-5-1.c: ボロノイ図を描く #include <LEDA/graph.h> #include <LEDA/map.h> #include <LEDA/float_kernel.h> #include <LEDA/window.h> #include <LEDA/geo_alg.h> window W (500, 500); void Draw_Voronoi(GRAPH<circle, point>& VD) { node v; edge e; point p, q; map<edge, bool> drawn(false); forall_nodes(v, VD) { if(VD.outdeg(v) < 2) continue; point p = VD[v].center(); W.draw_circle(p, 1, green); } // ボロノイ頂点を円で表示 forall_edges(e, VD){ // すべてのボロノイ辺について if( drawn[e] ) continue; drawn[ e ] = drawn[ VD.reverse(e) ] = true; node u = source(e); // u は e の始点 node v = target(e); // v は e の終点 if(VD.outdeg(u) ==1 && VD.outdeg(v)==1){ // u も v も無限遠 line l = p_bisector(VD[u].point1(),VD[u].point3()); W.draw_line(l, blue); } else if(VD.outdeg(u) ==1 && VD.outdeg(v)>=1){ // u だけ無限遠 vector vec = VD[u].point3() - VD[u].point1(); point cv = VD[v].center() + vec.rotate90(); W.draw_ray(VD[v].center(), cv, blue); } else if(VD.outdeg(u) >=1 && VD.outdeg(v)==1){ // v だけ無限遠 vector vec = VD[v].point3() - VD[v].point1(); point cv = VD[u].center() + vec.rotate90(); W.draw_ray(VD[u].center(), cv, blue); } else // 両端点とも有限の場合 5.5. ボロノイ図とその利用 } 241 W.draw_segment( VD[u].center(), VD[v].center(), blue); } W.read_mouse(); int main() { point p; list<point> S; GRAPH<circle, point> VD; W.display(window::center, window::center); } while(W >> p){ S.append(p); W.draw_circle(p, 1, red); } VORONOI(S, VD); Draw_Voronoi(VD); return 0; // 母点の入力 // ボロノイ図の計算 // ボロノイ図の描画 上のプログラムでは浮動小数点数で定義された点や線分を用いていました が, 計算誤差が心配になる場合には, 有理数型のデータタイプを用いることが 考えられます. 下のプログラムはすべて有理数型に変換したものです. 基本 的には, point を rat point に, segment を rat segment に変換すればいいので すが, ウィンドウに点や線分を表示するときには浮動小数点数型に変換しな ければなりません. rat point 型の点 p を浮動小数点数型に変換するには, p.to_point() とすればいいのです. //program 5-5-2.c: ボロノイ図を描く--rat type(2) #include <LEDA/graph.h> #include <LEDA/map.h> #include <LEDA/graphwin.h> #include <LEDA/rat_kernel.h> #include <LEDA/rat_kernel_types.h> 242 第 5 章 幾何データの取り扱い #include <LEDA/window.h> #include <LEDA/geo_alg.h> window W (500, 500); void Draw_Voronoi(GRAPH<rat_circle, rat_point>& VD) { node v; edge e; rat_point p, q; map<edge, bool> drawn(false); rat_line l; forall_nodes(v, VD) { // ボロノイ頂点を円で表示 if(VD.outdeg(v) < 2) continue; rat_point p = VD[v].center(); W.draw_circle(p.to_point(), 1, green); } forall_edges(e, VD){ if( drawn[e] ) continue; drawn[ e ] = drawn[ VD.reverse(e) ] = true; node u = source(e); node v = target(e); if(VD.outdeg(u) ==1 && VD.outdeg(v)==1){ l = p_bisector(VD[u].point1(), VD[u].point3()); W.draw_line(l.to_line(), blue); } else if(VD.outdeg(u) ==1 && VD.outdeg(v)>=1){ rat_vector vec = VD[u].point3() - VD[u].point1(); rat_point cv = VD[v].center() + vec.rotate90(); W.draw_ray(VD[v].center().to_point(), cv.to_point(), blue); } else if(VD.outdeg(u) >=1 && VD.outdeg(v)==1){ rat_vector vec = VD[v].point3() - VD[v].point1(); rat_point cv = VD[u].center() + vec.rotate90(); W.draw_ray(VD[u].center().to_point(), cv.to_point(), blue); } else W.draw_segment(VD[u].center().to_point(), VD[v].center().to_point(),blue); 5.5. ボロノイ図とその利用 } 243 } W.read_mouse(); int main() { point p; list<rat_point> S; GRAPH<rat_circle, rat_point> VD; W.display(window::center, window::center); } while(W >> p){ S.append(rat_point(p)); W.draw_circle(p, 1, red); } VORONOI(S, VD); Draw_Voronoi(VD); return 0; これで有理数型の誤差なしプログラムができあがったわけですが, 浮動小 数点数型と有理数型を統一的に扱うためには, point や rat point のように明 示的に型を示さずに, 両者に共通する POINT などの宣言を用いて書きなおし たものが次のプログラムです. //program 5-5-3.c: ボロノイ図を描く--rat type(3) #include <LEDA/graph.h> #include <LEDA/map.h> #include <LEDA/graphwin.h> #include <LEDA/rat_kernel.h> #include <LEDA/rat_kernel_types.h> #include <LEDA/window.h> #include <LEDA/geo_alg.h> window W (500, 500); void Draw_Voronoi(GRAPH<CIRCLE, POINT>& VD) { node v; edge e; POINT p, q; 244 第 5 章 幾何データの取り扱い map<edge, bool> drawn(false); forall_nodes(v, VD) { // ボロノイ頂点を円で表示 if(VD.outdeg(v) < 2) continue; POINT p = VD[v].center(); W.draw_circle(p.to_point(), 1, green); } } forall_edges(e, VD){ if( drawn[e] ) continue; drawn[ e ] = drawn[ VD.reverse(e) ] = true; node u = source(e); node v = target(e); if(VD.outdeg(u) ==1 && VD.outdeg(v)==1){ LINE l = p_bisector(VD[u].point1(), VD[u].point3()); W.draw_line(l.to_line(), blue); } else if(VD.outdeg(u) ==1 && VD.outdeg(v)>=1){ VECTOR vec = VD[u].point3() - VD[u].point1(); POINT cv = VD[v].center() + vec.rotate90(); W.draw_ray(VD[v].center().to_point(), cv.to_point(), blue); } else if(VD.outdeg(u) >=1 && VD.outdeg(v)==1){ VECTOR vec = VD[v].point3() - VD[v].point1(); POINT cv = VD[u].center() + vec.rotate90(); W.draw_ray(VD[u].center().to_point(), cv.to_point(), blue); } else W.draw_segment(VD[u].center().to_point(), VD[v].center().to_point(),blue); } W.read_mouse(); int main() { point p; list<POINT> S; GRAPH<CIRCLE, POINT> VD; 5.5. ボロノイ図とその利用 245 W.display(window::center, window::center); } while(W >> p){ S.append(POINT(p)); W.draw_circle(p, 1, red); } VORONOI(S, VD); Draw_Voronoi(VD); return 0; 5.5.2 ボロノイ図に基づいた曲線復元法 ボロノイ図を用いたアルゴリズムは今までに多数報告されていますが, こ こでは次のような問題を考えてみましょう. 問題は, 平面上に点集合が与えら れたとき, これらの点を適当な順で結んで, 自然に見える図形を構成せよ, と いうものです. 人間にとっては比較的簡単な仕事ですが, これまでこの問題が 理論計算機科学者の関心を引くことはありませんでしたが, 最近になって, サ ンプル点をどの程度の密度で取れば元の曲線を復元できるかに関して強力な 理論的な結果が幾つか報告されています. それらの研究の先鞭をつけたのが, 現テキサス大学の Nina Amenta 達による結果です. 彼女たちは, この問題に 計算幾何学の道具をもちこんで, 実に華麗なアルゴリズムを考案しました. ど んな方法かと言うと, まず与えられた点集合 S に対してボロノイ図 VD(S ) を 構成します. ここで生じたボロノイ図の頂点の集合 V と元の点集合 S の和集 合 L = S V に対して Delaunay 三角形分割を求めます. 点集合 L の三角形 分割とは, L の凸包(L の点をすべて内に含む最小の凸多角形)の内部を L の 点だけを頂点としてもつ三角形に分割したものですが, 任意の2点を結ぶ線 分を1辺とする三角形分割が考えられますので, 多数の三角形分割が存在す ることになります. その中でも Delaunay 三角形分割とは, 大雑把に言うと, 三角形の内角の最小値が最大になるものです. この三角形分割に含まれる辺 のうち, 元の点集合 S の点どうしを結ぶ辺だけを残して, 残りの辺とボロノイ 図の頂点を消し去ってできる図形が, このアルゴリズムの出力となります. 上に述べた方法は, ボロノイ図と(その双対として与えられる)Delaunay [ 246 第 5 章 幾何データの取り扱い 図 5.10: 入力の点集合 図 5.11: ボロノイ図 三角形分割さえ計算できれば, 残りの部分は余り難しくありません. 上にも述 べたように, LEDA ではボロノイ図と Delaunay 三角形分割を求める関数が用 意されていますので, 実に簡単にプログラムが仕上がってしまいます. プログラムは後で示しますが, そのプログラムの実行例を示すことにしま しょう. 図 5.10 はウィンドウ上で入力の点集合を指定したところを示してい ます. この点集合に対するボロノイ図を構成したのが図 5.11 で, さらにボロ ノイ頂点を加えて Delaunay 三角形分割を行った結果が図 5.12 です. この三 角形分割に含まれる辺のうち, 元の入力点どうしを結ぶものだけを残すと線 分列が得られますが, それが図 5.13 に示した最終結果です. 上に述べたアルゴリズムは 1997 年にドイツのダグシュツールで開かれた計 算幾何学に関するワークショップにおいて Nina Amenta 2 によって報告さ 2 詳細については, 論文:N. Amenta, M. Bern, and D. Eppstein: "The crust and the -skeleton: Combinatorial curve reconstruction," Graphical Model and Image Processing, pp.125-135, 1998 を参照してください. この論文の結果はさらなる理論的発展を生み出し, Mehlhorn らによる結果を含めて幾つかの優れた結果が知られています. 5.5. ボロノイ図とその利用 図 5.12: Delaunay 三角形分割 247 図 5.13: 最終結果 れたものですが, 同じ日の夕方に Kurt Mehlhorn は LEDA に関する講演をす ることになっていました. 彼はこの講演に非常に興味をもち, 何とか夕方の講 演までにアルゴリズムを実装できないかと考え, 午後の休憩を利用して実際 に LEDA でプログラムを書き上げたのです. 彼の講演が拍手喝采のうちに終 わったことは容易に想像できることと思います. これはまさに LEDA が目指 しているところなのです. では, Kurt Mehlhorn が作ったプログラムを見てみましょう. まず, 点集合 S の入力は次のように行います. list<point> S; window W; W.display(); point p; while(W>>p) //マウスで点データを入力し, 点集合 S を構成 S.append(p); 次に, 点集合 S に対するボロノイ図 VD(S ) を構成します. 前節でも説明し 248 第 5 章 幾何データの取り扱い ましたように, 単に VORONOI() という関数を呼び出せばいいのです. 具体 的には以下の通りです. く list<point> L = S; // 点集合 L を元の点集合 S に初期設定してお GRAPH<circle, point> VD; VORONOI(L, VD); // ボロノイ図を表すグラフ VD を宣言 //点集合 S のボロノイ図 VD(S) ボロノイ図ができたら, ボロノイ図の頂点の集合 V と元の点集合 S の和集 合 L = S V に対して Delaunay 三角形分割を求めます. このとき注意を要 するのは, 関数 VORONOI() を実行することによって得られるボロノイ図に は, 頂点として無限遠にあるものも含まれていることです. 曲線復元のアルゴ リズムでは無限遠にあるボロノイ頂点は必要がないので, それらを無視しな ければなりません. ボロノイ頂点が無限遠にあるかどうかは前節で述べた通 りです. ボロノイ図は有向グラフとして表現されていますから, グラフの節点 の出次数を調べれば, 無限遠にあるものかどうかを判定できるのです. すな わち, 無限遠点にある頂点には2本の辺が接続していますが, 方向が異なるの で, 出る辺は1本だけなので, 出次数も1ということになります. 一方, ボロ ノイ辺の交点として与えられるボロノイ頂点からは少なくとも3本の辺が出 ています. また, このプログラムでは, 入力で与えられた点とボロノイ頂点とを区別す るために map の疎配列を用いています. すなわち, 点集合 L = S V を作る ときに, L の各点 p について, それがボロノイ図の頂点だったものかどうかを, map のデータ構造 voronoi vertex[ ] を用いて, [ [ voronoi_vertex[p] == true voronoi_vertex[p] == false 点 p はボロノイ頂点 点 p は入力で与えた点 のようにして区別しています. 伝統的なC言語でのプログラミングスタイルでは複雑な構造体を宣言して 管理することが多いのですが, 構造体を使わずに疎配列をうまく活用する, と いうのが LEDA 流のプログラミングスタイルではないでしょうか. 具体的な手順は次の通りです. 5.5. ボロノイ図とその利用 249 map<point, bool> voronoi_vertex(false); // ボロノイ図の頂点の印 node v; forall_nodes(v, VD){ // ボロノイ図のすべての頂点について if (VD.outdeg(v) < 2) continue; // 出次数が2以上か? point p = VD[v].center( // ボロノイ頂点に対応する点を p とする voronoi_vertex[p] = true; // その頂点に印をつける L.append(p); // その頂点を点集合 L に加える } [ 残された処理は, 新たな点集合 L = S V に対する Delaunay 三角形分割 求めて, そこからボロノイ図の頂点だったものと, それらの頂点に接続するす べてのボロノイ辺を削除することです. LEDA では, グラフの節点を削除す れば, それに接続していた辺も削除されますから, 実際にはボロノイ図の頂点 だった節点を削除するだけでいいことになります. この操作が終了したとき, 元の点集合 S の点を結ぶ辺だけが残っていますが, これが求める曲線の線分 近似になります. Delaunay 三角形分割を求めて, 元のボロノイ図の頂点だっ た節点を削除する部分は次のようになります. DELAUNAY_TRIANG(L, G); forall_nodes(v, G) if (voronoi_vertex[G[v]]) G.del_node(v); 関数 CRUST() の実行が終了すると, その出力としてグラフ G が得られま す. このグラフは, 入力された点をグラフの節点とし, CRUST で結ばれた2 点間にグラフの辺を定義したものです. このグラフを描画したものが最終的 な結果になります. グラフを描画するために, パラメータつきのグラフとして 定義してあります. プログラムの main() において, GRAPH<point, int> G; と宣言して, 入力の点集合 S に対する出力結果をグラフ表現したものとして グラフ G を求めています. 宣言から分かるように, 節点に関する情報として 対応する点の座標を蓄えています. この位置情報を用いてグラフを描画しま す. 具体的には, W.draw_node(G[v]) として各節点 v を描いた後, すべての 250 第 5 章 幾何データの取り扱い 辺 e について, その始点G[G.source(e)] と終点G[G.target(e)] を線分で結 びます. 全体のプログラムは次のようになります. //program 5-5-4.c: 点列から曲線を復元するアルゴリズム CRUST #include <LEDA/graph.h> #include <LEDA/map.h> #include <LEDA/float_kernel.h> #include <LEDA/geo_alg.h> #include <LEDA/window.h> void CRUST(const list<point>& S, GRAPH<point, int>& G){ list<point> L = S; // 点集合 L を元の点集合 S に初期設定 GRAPH<circle, point> VD; // ボロノイ図を表すグラフ VD を宣言 VORONOI(L, VD); //点集合 S のボロノイ図 VD(S) } // ボロノイ図の頂点で2個以上の頂点につながるものに印をつけていく map<point, bool> voronoi_vertex(false);// ボロノイ図の頂点の印 node v; forall_nodes(v, VD){ // すべてのボロノイ頂点について if (VD.outdeg(v) < 2) continue; // 出次数が2以上か? point p = VD[v].center(); voronoi_vertex[p] = true; // その頂点に印をつける L.append(p); // その頂点を点集合 L に加える } // 点集合 L に対する Delaunay 三角形分割を求める DELAUNAY_TRIANG(L, G); forall_nodes(v, G) if (voronoi_vertex[G[v]])// ボロノイ図の頂点だったものを削除 G.del_node(v); int main() { list<point> S; window W; W.display(); point p; while(W>>p) S.append(p); // 点集合 S の入力 5.6. 幾何アルゴリズム 251 GRAPH<point, int> G; // グラフ G の宣言 //手続き CRUST により S の点をつなぐグラフを作成 CRUST(S, G); // グラフを描画したものが出力の図形 node v; edge e; W.clear(); forall_nodes(v, G) // 節点の描画 W.draw_node(G[v]); forall_edges(e, G) // 辺の描画 W.draw_segment( G[G.source(e)], G[G.target(e)] ); } W.screenshot("crust.ps"); leda_wait(2.0); return 0; // psファイルに出力 //wait 2 seconds 幾何アルゴリズム 5.6 5.6.1 凸包の計算 CONVEX HULL() : 点集合に対する凸包 点集合のリストが入力される と, 対応する凸包を表す凸多角形の頂点列を返します. CONVEX HULL POLY() : 点集合に対する凸包 同じく点集合の凸包を 求めますが, 結果を polygon データタイプで返します. UPPER CONVEX HULL() : 上部凸包の計算 点集合の上部凸包を求め LOWER CONVEX HULL() : 下部凸包の計算 点集合の下部凸包を求 ます. めます. CONVEX HULL S() : 点集合に対する凸包 平面走査法に基づいて点集 合の凸包を求めます. 計算時間は最善でも最悪でも O (n log n) です. CONVEX HULL IC() : 点集合に対する凸包 逐次添加法に基づいて点集 合の凸包を求めます. 計算時間は最悪が O (n2 ) で平均的には O (n log n) です. 最善の場合には線形時間です. 252 第 5 章 幾何データの取り扱い CONVEX HULL RIC() : 点集合に対する凸包 入力の点列をランダムに 並び替えた後で逐次添加法に基づいて点集合の凸包を求めます. 5.6.2 三角形分割 TRIANGULATE POINTS() : 点集合の三角形分割 平面上に点集合が リストの形で与えられたとき, この点集合の三角形分割(平面地図)と 外面(凸包)を構成する辺の集合を求めます. DELAUNAY TRIANG() : 点集合の Delaunay 三角形分割 平面上に点 集合がリストの形で与えられたとき, この点集合の三角形分割の中で最 小角を最大にする Delaunay 三角形分割を求めます. F DELAUNAY TRIANG() : 点集合の最遠点 Delaunay 三角形分割 点 集合の最遠点 Delaunay 三角形分割を求めます. 5.6.3 制約つき三角形分割 TRIANGULATE SEGMENTS() : 線分集合の三角形分割 平面上に与 えられた線分の集合を辺として用いた三角形分割を求めます. DELAUNAY TRIANG() : 制約つきの Delaunay 三角形分割 平面上に 与えられた線分の集合を辺として用いた Delaunay 三角形分割を求め ます. TRIANGULATE PLANE MAP() : 平面グラフの三角形分割 平面上に 描画された平面グラフに辺を加えることによって三角形分割を行います. DELAUNAY TRIANG() : 平面グラフの Delaunay 三角形分割 平面上 に描画された平面グラフに辺を加えることによって Delaunay 三角形分 割を行います. TRIANGULATE POLYGON() : 内部と外部を三角形分割します. TRIANGULATE POLYGON() : 多角形の三角形分割 単純な多角形の 多角形領域の三角形分割 外側境界と 内側境界からなる一般の多角形(穴を含んだ多角形)の内部と外部を三 角形分割します. 5.6. 幾何アルゴリズム 253 CONVEX COMPONENTS() : 多角形の凸多角形分割 単純な多角形の 内部と外部を凸多角形に分割します. CONVEX COMPONENTS() : 多角形領域の凸多角形分割 外側境界と 内側境界からなる一般の多角形(穴を含んだ多角形)の内部と外部を凸 多角形に分割します. 5.6.4 ミンコフスキー和 MINKOWSKI SUM() : 多角形のミンコフスキー和 多角形 P と Q のミ ンコフスキー和を求めます. MINKOWSKI DIFF() : 多角形のミンコフスキー差 多角形 P と Q のミ ンコフスキー差を求めます. MINKOWSKI SUM() : 多角形領域のミンコフスキー和 多角形領域 P と 多角形 Q のミンコフスキー和を求めます. MINKOWSKI DIFF() : 多角形領域のミンコフスキー差 多角形領域 P と 多角形 Q のミンコフスキー差を求めます. 5.6.5 ユークリッド最小木 MIN SPANNING TREE() : ユークリッド最小木 与えられた点集合に 対するユークリッド最小木を求めます. 5.6.6 ボロノイ図 VORONOI() : 点集合のボロノイ図 与えられた点集合に対するボロノイ 図を求めます. 計算時間は最悪の場合には O (n2 ) ですが, 高い確率で O(n log n) です. F VORONOI() : 点集合の最遠点ボロノイ図 与えられた点集合に対する 最遠点ボロノイ図を求めます. LARGEST EMPTY CIRCLE() : 点集合の最大空円 与えられた点集合 L の凸包の内部に中心をもち, L の点を内部に含まない半径最大の円を 求めます. 254 第 5 章 幾何データの取り扱い SMALLEST ENCLOSING CIRCLE() : 点集合の最小包含円 与えられ た点をすべて内部に含む半径最小の円を求めます. 5.6.7 環形 MIN AREA ANNULUS() : 面積最小の環形 与えられた点をすべて内 部に含む面積最小の環形(2 つの同心円で囲まれた領域)を求めます. MIN WIDTH ANNULUS() : 幅最小の環形 与えられた点をすべて内部 に含む環形で, 同心円の半径の差(幅)が最小のものを求めます. 5.6.8 線分交差 SEGMENT INTERSECTION() : 与えられた線分集合で定まる平面グ ラフ 線分の集合 L が与えられたとき, それらの交点と端点を節点とす る平面グラフを求めます. SWEEP SEGMENTS() : 与えられた線分集合で定まる平面グラフ 線分 の集合 L が与えられたとき, それらの交点と端点を節点とする平面グラ フを平面走査法により求めます. 計算時間は O ((n + s) log n + m) です. ただし, n は線分数, s はグラフの節点数(交点数と線分の端点の個数の 和), m はグラフの辺数で, m = O (n + s) です. MULMULEY SEGMENTS() : 与えられた線分集合で定まる平面グラフ 線分の集合 L が与えられたとき, それらの交点と端点を節点とする平面 グラフを Mulmuley の逐次添加法に基づいてにより求めます. 計算時間 の期待値は O (m + s + n log n) です. SEGMENT INTERSECTION() : 交差線分列挙 線分の集合 L が与え られたとき, L の線分間の交点をすべて列挙します. 計算時間は O (n log2 n+ k) です. ここで, k は交点数です. SEGMENT INTERSECTION() : 交差線分列挙 線分の集合 L が与え られたとき, L の線分間の交点をすべて列挙します. BentleyOttman の アルゴリズムに基づいているので, 計算時間は O ((n + k ) log n) です. こ こで, k は交点数です. 5.6. 幾何アルゴリズム 5.6.9 255 最近点対 CLOSEST PAIR() : 最近点対 点集合 S が与えられたとき, 最も近い L の 点対を求めます. 計算時間は O (n log n) です. 5.6.10 その他 Bounding Box() : 限界長方形 与えられた点集合を包み込む最小の軸平行 長方形を求めます. Is Simple Polygon() : 点列の単純性 与えられた点列が単純な多角形をな すかどうかを判定します. 計算時間は O (n log n) です. 257 第 6 章 デモとアニメーション LEDA には様々なデモプログラムが用意されています. LEDA でのプログラ ミングの参考になるものから, アルゴリズムの理解に不可欠なアルゴリズム アニメーションに至るまで様々なレベルのものが用意されています. 6.1 アルゴリズムの基礎に関するデモプログラム アルゴリズムの基礎をアニメーションを通じて効果的に教えることができ るように様々なアルゴリズムアニメーションが用意されています. ここでは, 代表的なものについてのみ紹介します. gw quicksort anim.c クイックソートの動作をアニメーションで示します. 非常に良くできたデモプログラムで, 難解なクイックソートもこのデモ プログラムさえあればもっと簡単に理解できたのにと悔やまれるほどで す. 単にクイックソートの動作を示すだけではなく, ソースプログラム のどの部分を実行しているのかも示してくれます. デモプログラムの実 行の様子は図で示してあります. gw heapsort anim.c gw bintree.c ヒープソートの動作をアニメーションで示します. (非平衡)2 分探索木, 2 色木, AVL 木, ランダマイズ木, BB- 木にデータが挿入される様子がアニメーションで示されます. 回転操作 が行われる様子が良く理解できます. デモプログラムの実行の様子を図 で示してあります. 258 6.2 第 6 章 デモとアニメーション グラフに関するデモプログラム グラフに関しては実に様々なデモプログラムが提供されていますが, ここ では代表的なものを幾つか紹介します. gw.c 最も基本的なプログラムです. グラフを自分で編集して作成したり, 節 点数や辺数を指定してランダムグラフや完全グラフなどを生成するこ とができます. ファイルにグラフを格納したり, ファイルからグラフを ロードすることもできます. グラフのレイアウトを自動で変更したり, 平面性の判定やその証明も行うことができます. デモプログラムの実行 の様子を図で示してあります. gw basic alg.c グラフの生成に関しては gw.c と同じですが, 生成したグラ フの連結成分, 2 連結成分, 3 連結成分を動的に求めることができます. gw ve colors.c 平面グラフの節点を 5 色で彩色して表示します. グラフの 作り方は今までと同じですが, これらのデモプログラムでは単なるラン ダムグラフ以外にランダム平面グラフも作れます. gw dfs.c 任意のグラフに対して深さ優先探索の結果を表示します. gw scc anim.c 有向グラフの強連結成分を求めるアルゴリズムの動作をア ニメーションで示します. gw dijkstra.c 任意のグラフに対してダイクストラ法で最短経路木を構成し て表示します. 辺の重みをマウスで変更すると最短経路木も変化しま す. デモプログラムの実行の様子を図で示してあります. gw tammassia.c, gw tutte.c, gw spring.c, gw sugiyama.c グラフ描画 に関する 4 つの代表的なアルゴリズムをプログラム化したものです. ア ルゴリズムを選択して実行できるようになっています. また, 描画結果 を編集して, ファイルにセーブすることもできます. 6.3. 幾何問題に関するデモプログラム 6.3 259 幾何問題に関するデモプログラム 計算幾何学に関連するアルゴリズムについても多数のデモプログラムが提 供されています. ボロノイ図や Delaunay 三角形分割を求めるプログラムを一 から書くのは相当大変なことですが, ここで提供されているプログラムは単 に与えられた点集合に対するボロノイ図などを計算するだけでなく, 点が新 たに加えられるたびに構造を更新するという動的なアルゴリズムになってい ます. delaunay anim.c 画面上にマウスで点を多数指定すると, 点が入力される たびに Delaunay 三角形分割を更新して表示します. マウスでの入力の 他, ランダムに自動生成することもできます. また, 点だけでなく, 線分 や円を入力することもできます. ただし, 線分と円は図形上の点集合と して表現しています. delaunay demo.c 画面上にマウスで点を多数指定すると, 点が入力される たびにその時点の点集合に対する Delaunay 三角形分割, ボロノイ図, 最 小木, 凸包を更新して表示します(表示すべきものをマウスで指定する ことができます). 入力だけでなく, 点の削除も可能です. 点だけでは なく, 線分, 円, 長方形, 三角形も図形上の点集合として扱えます. さら に, 点の追加と削除だけでなく, マウスで指定された質問点に最も近い 点をハイライト表示で答えるなどの機能もあります. デモプログラムの 実行の様子を図で示してあります. voronoi demo.c このデモプログラムは 2 次元の幾何アルゴリズムを多数含 んだものです. マウスや乱数による自動生成で点を多数入力して, それ らに対するボロノイ図, 最遠点ボロノイ図, ユークリッド最小木, 幅最小 環形, 最大空円, 最小包含円, 凸包, Delaunay 三角形分割を動的に求め て表示します. intersection segment.c 画面上の線分集合に対してすべての交点を列挙し ます. 線分はマウスでも入力できますが, 乱数で自動生成することもでき 260 第 6 章 デモとアニメーション ます. 交点列挙のアルゴリズムも, 腕力法の他に平面走査法, Mulmuley 法, Balaban 法などの中から選択できるようになっています. polygon demo.c 多角形に関するデモプログラムです. 2 つの多角形の共通 部分や合併を求めたり, 多角形の内部/外部の判定などもできます. 多 角形はマウスでも入力できますが, ヒルベルト多角形を自動で生成する こともできます. rpoly demo.c polygon demo.c と同じですが, 座標に有理数を用いています. sweep anim.c 平面走査法によって線分の交差列挙を行う様子をアニメー ションで示します. 線分の集合はマウスで入力するか, 乱数で指定本数 だけ自動生成します. デモプログラムの実行の様子を図で示してあり ます. 6.4 3 次元幾何に関するデモプログラム 本文ではまったく触れませんでしたが, LEDA では 3 次元空間を扱うため のデータ構造やアルゴリズムが豊富に提供されています. また, 3 次元物体を 表示するためのウィンドーの威力も以下のアニメーションソフトで体感する ことができます. ここでは中でも代表的な 2 つだけを紹介します. d3 hull demo.c 3 次元空間にランダムに点集合を生成して, その凸包を求 めます. 大体の形を立方体, 球, 放物面などの中から選択することがで きます. マウスの動きに合わせて凸包を自由に回転させたり, 回転を止 めたりできます. また, ズーム機能もあります. d3 hull anim.c このプログラムも同様ですが, さらに多くの機能が備わっ ています. たとえば, 回転の速度や凸包の表示方法を変えたりすること ができます. デモプログラムの実行の様子を図で示してあります. 6.5. アルゴリズムの効率に関するデモプログラム 6.5 261 アルゴリズムの効率に関するデモプログラム 同じ問題を解くのに様々なアルゴリズムが提案されていますが, それらを 利用する上で大切なことは, どのような状況ではどのアルゴリズムが最も効 率的かを知っておくことです. たとえば, グラフを扱う問題では, グラフのサ イズによっても最適なアルゴリズムは異なりますが, 平面グラフや 2 部グラ フなどのグラフの性質によっても最適なアルゴリズムは異なります. そのた めにアルゴリズムの効率を測定するためデモプログラムが用意されています. これらのデモプログラムは LEDA によるプログラミングに馴れるためにも参 考になりますから, 是非ソースプログラムにも目を通して見てください. array sort times.c ソーティングのアルゴリズムの計算時間を比較します. 比較するのは挿入法, マージソート法, クイックソート法の 3 種類です. ソートすべきデータ数の上限を入力すると, 幾つかのデータ数に対する 各方法の実行時間が出力されます. ただし, 挿入法は遅いので, 最大サ イズは 10000 までに制限されています. braided lines.c 浮動小数点数による計算誤差を示すためのプログラムです. LEDA Book でも説明されている例題で, 傾きがほぼ等しい直線上の点 の上限関係を調べて, 誤差により誤った上下関係が報告されることを確 かめます. Euler demo.c オイラー定数を指定された精度で計算します. 入力で整数 m の値を指定すると, オイラー定数 e = 2:71 : : : を 2 m の精度で計算し ます. giant component demo.c ランダムグラフに巨大成分が出現する現象をシ ミュレーションします. 最初は各頂点がばらばらな状態から出発して, ランダムに 2 頂点を選び, それらが異なる成分に属していればマージす るという操作を繰り返して, 最大の成分のサイズが頂点数の半分になっ たところで終ります. max ow large time.c 最大フローのプリフロープッシュアルゴリズムで 使われる各種のヒューリスティックの効果を計算時間で比較します. 262 第 6 章 デモとアニメーション max ow time.c 最大フローのアルゴリズムの実行時間がグラフの種類に よってどのように異なるかを調べます. ランダムグラフの他, Cherkassky { Goldberg のグラフや Ahuja { Orlin { Magnanti のグラフが対象です. mc matching time.c グラフの節点数と辺数を入力して, 要素数最大のマッ チングを求めるアルゴリズムの計算時間を比較します. minspantree time.c グラフの節点数と辺数を入力してランダムグラフを生 成し, その最小木を求めるアルゴリズムの計算時間を求めます. multiplication times.c 巨大な整数どうしの掛け算を計算する時間を求め ます. planarity time.c グラフの平面性を判定するアルゴリズムの計算時間を求 めます. priority queues demo.c ダイクストラのアルゴリズムを様々な優先順位つ きキューでインプリメントして, その計算時間を評価します. 6.5. アルゴリズムの効率に関するデモプログラム 図 6.1: クイックソートのアニメーション 263 264 第 6 章 デモとアニメーション bin tree.c gw.c 図 6.2: 2分探索木とグラフエディタに関するデモ 6.5. アルゴリズムの効率に関するデモプログラム gw dijkstra.c 265 delaunay demo.c 図 6.3: ダイクストラの最短経路アルゴリズムと Delaunay 三角形分割に関す るデモ 266 第 6 章 デモとアニメーション sweep anim.c d3 hull anim.c 図 6.4: 平面走査法による線分交差探索と3次元凸包に関するデモ 267 プログラム一覧 program 2-1-1.c: 最も簡単なプログラム . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 program 2-1-2.c: 最初の LEDA プログラム . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 program 2-2-1.c: 摂氏から華氏への変換 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 program 2-2-2.c: 摂氏から華氏への変換 (2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 program 2-2-3.c: 摂氏から華氏への変換 (3) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 program 2-2-4.c: 摂氏から華氏への変換 (4) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 program 2-2-5.c: 摂氏から華氏への変換 (5) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 program 2-2-6.c: 円錐の体積を求める . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 program 2-3-1.c: ウィンドウ上に点を描く . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 program 2-3-2.c: ウィンドウ上に様々な図形を描く . . . . . . . . . . . . . . . . . . . . . 20 program 2-3-3.c: ウィンドウ上に様々な図形を描く . . . . . . . . . . . . . . . . . . . . . 22 program 2-4-1.c: どちらが右? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 program 2-4-2.c: 閏年かどうかの判定 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 program 2-4-3.c: 長方形の内部・外部の判定 . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 program 2-4-4.c: 変数の値を順に変化させて平方数を出力する . . . . . . . . . . 30 program 2-4-5.c: 直線 y=2x に沿って点列をプロットする . . . . . . . . . . . . . . 31 program 2-4-6.c: 直線 y=2x に沿って点列をプロットする (2) . . . . . . . . . . . 32 program 2-4-7.c: 直線 y=2x に沿って点列をプロットする (3) . . . . . . . . . . . 34 program 2-4-8.c: ウィンドウ上に多数の点を描画 . . . . . . . . . . . . . . . . . . . . . . . 36 program 2-4-9.c: ウィンドウ上に多数の図形を描画 . . . . . . . . . . . . . . . . . . . . . 36 program 2-4-10.c: データの総和を求める . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .39 program 2-4-11.c: データの総和を求める . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .39 program 2-4-12.c: ウィンドウ上に多数の図形を描画 (2) . . . . . . . . . . . . . . . . 40 268 第 6 章 デモとアニメーション program 2-4-13.c: ウィンドウ上に多数の図形を描画 (3) . . . . . . . . . . . . . . . . 41 program 2-4-14.c: ウィンドウ上に多数の図形を描画 (4) . . . . . . . . . . . . . . . . 42 program 2-6-1.c: ウィンドウ上に星の形を描く . . . . . . . . . . . . . . . . . . . . . . . . . 48 program 2-6-2.c: ウィンドウ上に星の形を描く (2) . . . . . . . . . . . . . . . . . . . . . . 50 program 2-6-3.c: ウィンドウ上に星の形を描く (3) . . . . . . . . . . . . . . . . . . . . . . 53 program 2-6-4.c: ウィンドウ上に星の形を描く (4) . . . . . . . . . . . . . . . . . . . . . . 54 program 2-6-5.c: 1 から 6 までの乱数の生成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 program 2-6-6.c: 異なる区間の乱数の生成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 program 2-6-7.c: 異なる区間の乱数の生成 (2) . . . . . . . . . . . . . . . . . . . . . . . . . . 58 program 2-6-8.c: double 型の乱数の生成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 program 2-7-1.c: 入力データの最大値を求める . . . . . . . . . . . . . . . . . . . . . . . . . 59 program 2-7-2.c: 入力データの 2 番目に大きい値を求める . . . . . . . . . . . . . . 62 program 2-7-3.c: 入力データの 3 番目に大きい値を求める . . . . . . . . . . . . . . 63 program 2-7-4.c: 入力データの 3 番目に大きい値を求める (2) . . . . . . . . . . . 65 program 2-7-5.c: 入力データを昇順にソートする . . . . . . . . . . . . . . . . . . . . . . . 66 program 2-7-6.c: 乱数の度数分布 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 program 2-7-7.c: 乱数の度数分布 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 program 2-8-1.c: 文字列の比較 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 program 2-8-2.c: 文字列の初期化と代入 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 program 2-8-3.c: 入力文字列のカウント . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 program 2-9-1.c: ファイル入出力 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 program 2-9-2.c: ファイル入出力 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 program 3-1-1.c: integer 型を用いた多倍長計算 . . . . . . . . . . . . . . . . . . . . . . . . 77 program 3-1-2.c: 有理数の和の計算 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 program 3-1-3.c: 有理数に関連する関数 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80 program 3-1-4.c: 平方根を含む計算 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 program 3-1-5.c: 平方根を含む計算 (2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .83 program 3-2-1.c: 配列への入出力 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 program 3-2-2.c: 配列への入出力 (2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 program 3-2-3.c: 配列への入出力 (3) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 program 3-2-4.c: 配列への入出力 (4) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 6.5. アルゴリズムの効率に関するデモプログラム 269 program 3-2-5.c: 英単語の出現度数を数える . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 program 3-2-6.c: 英語-ドイツ語辞書 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 program 3-2-7.c: 整数の出現度数を数える . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 program 3-2-8.c: 英単語の出現度数を数える (2) . . . . . . . . . . . . . . . . . . . . . . . . 95 program 3-2-9.c: 2 つの乱数列の共通部分 (0) . . . . . . . . . . . . . . . . . . . . . . . . . . .98 program 3-2-10.c: 2 つの乱数列の共通部分 (1) . . . . . . . . . . . . . . . . . . . . . . . . . 99 program 3-2-11.c: 2 つの乱数列の共通部分 (2) . . . . . . . . . . . . . . . . . . . . . . . . 101 program 3-2-12.c: 2 つの乱数列の共通部分 (3) . . . . . . . . . . . . . . . . . . . . . . . . 102 program 3-2-13.c: 同じ値を含まない乱数列の生成 . . . . . . . . . . . . . . . . . . . . . 104 program 3-3-1.c: スタックを用いた例題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .107 program 3-3-2.c: キューを用いた例題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 program 3-3-3.c: 優先順位つきキューを用いた例題 . . . . . . . . . . . . . . . . . . . .112 program 3-3-4.c: スタックを用いた経路探索プログラム . . . . . . . . . . . . . . . 118 program 3-3-5.c: リスト構造の例題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132 program 4-1-1.c: グラフを扱う最も簡単なプログラム . . . . . . . . . . . . . . . . . 142 program 4-3-1.c: 簡単なグラフの定義 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149 program 4-3-2.c: 簡単なグラフの定義 (2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151 program 4-3-3.c: 簡単なグラフの定義 (3) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 program 4-3-4.c: 簡単なグラフの定義 (4) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153 program 4-3-5.c: 簡単なグラフの定義 (5) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 program 4-3-6.c: グラフのセーブ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156 program 4-3-7.c: パラメータつきグラフのセーブ . . . . . . . . . . . . . . . . . . . . . . 157 program 4-4-1.c: グラフのトポロジカルソート . . . . . . . . . . . . . . . . . . . . . . . . 162 program 4-5-1.c: パラメータつきグラフの作成 . . . . . . . . . . . . . . . . . . . . . . . . 164 program 4-5-2.c: 1 節点からの最短経路 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166 program 4-5-3.c: 1 節点からの最短経路 (2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168 program 4-5-4.c: 1 節点からの最短経路 (3) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169 program 4-5-5.c: 1 節点からの最短経路 (4) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172 program 4-5-6.c: 1 節点からの最短経路 (5) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174 program 4-5-7.c: 1 節点からの最短経路 (6) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175 program 5-1-1.c: 最も近い点を求める . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183 270 program 5-1-2.c: program 5-1-3.c: program 5-1-4.c: program 5-1-5.c: program 5-1-6.c: program 5-1-7.c: program 5-2-1.c: program 5-2-2.c: program 5-2-3.c: program 5-2-4.c: program 5-2-5.c: program 5-3-1.c: program 5-3-2.c: program 5-3-3.c: program 5-3-4.c: program 5-4-1.c: program 5-4-2.c: program 5-4-3.c: program 5-4-4.c: program 5-4-5.c: program 5-4-6.c: program 5-4-7.c: program 5-4-8.c: program 5-5-1.c: program 5-5-2.c: program 5-5-3.c: program 5-5-4.c: 第 6 章 デモとアニメーション 最も近い点を求める(点リスト版) . . . . . . . . . . . . . . . . . 184 最も近い線分を求める . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .186 最も近い円を求める . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186 直線に関する点集合の2分割 . . . . . . . . . . . . . . . . . . . . . . . . 189 質問線分と交差する線分の列挙 . . . . . . . . . . . . . . . . . . . . . . 191 半直線と最初に交差する線分 . . . . . . . . . . . . . . . . . . . . . . . . 193 多角形の入力 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198 凸多角形かどうかの判定 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202 凸多角形かどうかの判定 (2) . . . . . . . . . . . . . . . . . . . . . . . . . 205 凸多角形かどうかの判定 (3) . . . . . . . . . . . . . . . . . . . . . . . . . 207 凸多角形かどうかの判定 (4) . . . . . . . . . . . . . . . . . . . . . . . . . 208 凸包の計算 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212 凸包の計算 (2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214 凸包の計算 (3) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216 凸包の計算 (4) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218 凸包の計算 (5) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220 凸包の計算 (6) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221 線分の交点と線分の上下関係 . . . . . . . . . . . . . . . . . . . . . . . . 225 線分の交点と線分の上下関係 (2) . . . . . . . . . . . . . . . . . . . . .228 2 直線の交点は両方の直線上にあるか? . . . . . . . . . . . . . .230 2 直線の交点は両方の直線上にあるか?(2) . . . . . . . . . . . 231 2 直線の交点は両方の直線上にあるか?(3) . . . . . . . . . . . 234 2 直線の交点は両方の直線上にあるか?(4) . . . . . . . . . . . 234 ボロノイ図を描く . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239 ボロノイ図を描く|rat type(2) . . . . . . . . . . . . . . . . . . . . . . 241 ボロノイ図を描く|rat type(3) . . . . . . . . . . . . . . . . . . . . . . 243 点列から曲線を復元するアルゴリズム CRUST . . . . . . . 250 271 索引 数字と欧文 2 次元配列 . . . . . . . . . . . . . . . . . . . . 69 2 色木 . . . . . . . . . . . . . . . . . . . . . . . . 257 2 部グラフ | の最大重みマッチング .181 | の最大マッチング . . . . . 180 2 分探索木 . . . . . . . . . . . . . . . . . . . 257 2 連結成分 . . . . . . . . . . . . . . 179, 258 3 次元幾何 . . . . . . . . . . . . . . . . . . . 260 3 次元幾何ライブラリ . . . . . . . . . . .7 3 次元空間 . . . . . . . . . . . . . . . . . . . 260 3 次元凸包 . . . . . . . . . . . . . . . . . . . 260 3 連結成分 . . . . . . . . . . . . . . . . . . . 258 4 色定理 . . . . . . . . . . . . . . . . . . . . . 137 5 彩色 . . . . . . . . . . . . . . . . . . . 182, 258 平面グラフの |. . . . .182, 258 CIRCLE . . . . . . . . . . . . . . . . . . . . . 233 circle データタイプ . . . . . . . . . . . 195 C言語の配列 . . . . . . . . . . . . . . . . . . 59 d array. . . . . . . . . . . . . . . . . . . . . . . .89 #dene 文 . . . . . . . . . . . . . . . . . . . . 46 Delaunay 三角形分割 . . . . 252, 259 制約つきの |. . . . . . . . . . . .252 点集合の | . . . . . . . . . 252, 259 点集合の最遠点 | . . . . . . . 252 平面グラフの | . . . . . . . . . 252 double 型の乱数 . . . . . . . . . . . . . . . 58 endl. . . . . . . . . . . . . . . . . . . . . . . . . . .10 for ループ . . . . . . . . . . . . . . . . . . . . . 30 h array. . . . . . . . . . . . . . . . . . . . . . . .94 AND. . . . . . . . . . . . . . . . . . . . . . . . . . 26 array タイプ . . . . . . . . . . . . . . . . . . .85 AVL 木 . . . . . . . . . . . . . . . . . . . . . . 257 IEEE 標準の浮動小数点数 . . . . . 81 if 文 . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 integer タイプ . . . . . . . . . . . . . . . . . 77 BB- 木 . . . . . . . . . . . . . . . . . . . . . .257 bigoat タイプ . . . . . . . . . . . . . . . . 81 break 文 . . . . . . . . . . . . . . . . . . . . . . .38 l.contains() . . . . . . . . . . . . . . . . . . 230 l.intersection() . . . . . . . . . . . . . . . 230 leda swap() . . . . . . . . . . . . . . 62, 200 272 leda wait() . . . . . . . . . . . . . . . . 54, 55 LEDA のインストール . . . . . . . 1, 3 LEDA のデータタイプ . . . . . . . . .77 LEDA の配列 . . . . . . . . . . . . . . . . . 85 LEDA プログラムのコンパイル . 1 LINE . . . . . . . . . . . . . . . . . . . . . . . . 233 line データタイプ . . . . . . . . . . . . 194 main . . . . . . . . . . . . . . . . . . . . . . . . . . 10 map . . . . . . . . . . . . . . . . . . . . . . . . . . 95 NOT. . . . . . . . . . . . . . . . . . . . . . . . . . 26 OR . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 POINT . . . . . . . . . . . . . . . . . . . . . . 233 point データタイプ . . . . . . . . . . . 187 polygon データタイプ . . . . . . . . 196 random point in square() . . . . 230 rat line . . . . . . . . . . . . . . . . . . . . . . 231 rat point . . . . . . . . . . . . . . . . . . . . . 231 rat point データタイプ . . . . . . . 228 rational タイプ . . . . . . . . . . . . . . . . 78 RAY . . . . . . . . . . . . . . . . . . . . . . . . . 233 ray データタイプ . . . . . . . . . . . . . 192 read string() . . . . . . . . . . . . . . . . . . 75 read int . . . . . . . . . . . . . . . . . . . . . . . 15 read real() . . . . . . . . . . . . . . . . . . . . 16 real タイプ . . . . . . . . . . . . . . . . . . . . 82 SEGMENT . . . . . . . . . . . . . . . . . . 233 segment データタイプ . . . . . . . . 190 索引 switch case 構文 . . . . . . . . . . . . . . . 41 UNIX 系でのインストール . . . . . . 3 W.choice item() . . . . . . . . . . . . . . . 43 W.close() . . . . . . . . . . . . . . . . . . . . . 19 W.display() . . . . . . . . . . . . . . . . . . . 18 W.draw point() . . . . . . . . . . . . . . . 25 W.draw segment() . . . . . . . . . . . . 49 W.init(). . . . . . . . . . . . . . . . . . . . . . .23 W.read mouse() . . . . . . . . . . . 18, 19 W.screenshot() . . . . . . . . . . . . . . . . 43 W.set point style() . . . . . . . . . . . . 32 while ループ . . . . . . . . . . . . . . . 30, 34 Windows 系でのインストール . . . 4 あ アスキー符号 . . . . . . . . . . . . . . . . . . 71 アニメーション . . . . . . . . . . 257{260 位相優先法 . . . . . . . . . . . . . . . . . . . 235 一方向連結リスト . . . . . . . . . . . . 133 インクリメント演算子 . . . . . . . . . 44 インクルードファイル . . . . . . . . . . 8 インストール . . . . . . . . . . . . . . . . . . . 1 UNIX 系での | . . . . . . . . . . . . 3 Windows 系での |. . . . . . . . .4 ウィンドウ . . . . . . . . . . . . . . . . . . . . 18 | のサイズ . . . . . . . . . . . 21, 23 | の座標系 . . . . . . . . . . . . . . . 21 | の宣言 . . . . . . . . . . . . . . . . . 18 | の縦横比 . . . . . . . . . . . . . . . 23 閏年の判定条件 . . . . . . . . . . . . . . . 26 273 索引 円 . . . . . . . . . . . . . . . . . . . . . . . . . . . 195 オイラー定数 . . . . . . . . . . . . . . . . .261 オブジェクトパッケージ . . . . . . . . 2 か 改行 . . . . . . . . . . . . . . . . . . . . . . . . . . 10 回転操作 . . . . . . . . . . . . . . . . . . . . . 257 可視表現 . . . . . . . . . . . . . . . . . . . . . 182 カットの値 . . . . . . . . . . . . . . . . . . . 180 環形 . . . . . . . . . . . . . . . . . . . . 254, 259 幅最小の | . . . . . . . . . 254, 259 面積最小の |. . . . . . . . . . . .254 幾何アルゴリズム . . . . . . . .251{255 幾何データの取り扱い . . . . . . . . 183 幾何用グラフィックツールライブラ リ ..................... 7 基本データタイプ . . . . . . . . . . . . 105 キャスト . . . . . . . . . . . . . . . . . . . . . . 45 キュー . . . . . . . . . . . . . . 108{125, 161 | の応用例 . . . . . . . . . . . . . . 113 優先順位つき | . . . . . . . . . 109 強連結成分 . . . . . . . . . . . . . . 179, 258 クイックソート . . . . . . . . . .257, 261 グラフ . . . . . . . . . . . . . . . . . . .137{182 | アルゴリズム . . . . . 177{182 | ウィンドウ . . . . . . . 142{147 | のメニュー . . . . . . . . . 145 | に関する基本アルゴリズム 177 | に関する高度なアルゴリズ ム . . . . . . . . . . . . . . . . . . . 179 | の 2 連結成分 . . . . . . . . . 258 | の 3 連結成分 . . . . . . . . . 258 | の格納 . . . . . . . . . . . . . . . . 145 | の可視表現 . . . . . . . . . . . .182 | の作成 . . . . . . . . . . . . . . . . 142 | の生成 . . . . . . . . . . . . . . . . 258 | の節点 . . . . . . . . . . . . . . . . 143 | の直線描画 . . . . . . . . . . . .182 | の直交表現 . . . . . . . . . . . .182 | の定義 . . . . . . . . . . . . . . . . 147 | の入出力 . . . . . . . . . . . . . . 156 | のバネ表現 . . . . . . . . . . . .182 | の平面性に関するアルゴリ ズム . . . . . . . . . . . . . . . . . 182 | の平面性判定 182, 258, 262 | の平面性判定問題 . . . . . 139 | の辺 . . . . . . . . . . . . . . . . . . 143 | のレイアウト . . . . . . . . . 258 グラフ描画 . . . . . . . . . . 182, 258 | に関するアルゴリズム182 | ライブラリ . . . . . . . . . . . . . . 5 パラメータつき | . . . . . . . 154 平面的 | . . . . . . . . . . . . . . . . 137 ランダム | . . . . . . . . . . . . . . 261 グラフィックツールライブラリ . . 6 グラフィックユーザインタフェース 6 繰り返し構造 . . . . . . . . . . . . . . . . . . 30 計算幾何学 . . . . . . . . . . . . . . . . . . . 209 計算誤差 . . . . . . . . . . . . . . . . . . xi, 230 | 対策 . . . . . . . . . . . . . . . . . . 219 274 索引 経路探索 優先順位つきキューによる | 123 経路探索問題 . . . . . . . . . . . . . . . . .113 限界長方形 . . . . . . . . . . . . . . . . . . . 255 交差線分列挙 . . . . . . . . . . . . 254, 260 交差判定 . . . . . . . . . . . . . . . . . . . . . 223 降順ソート . . . . . . . . . . . . . . . . . . . . 87 誤差なし計算 . . . . . . . . . . . . . . . . .230 コンパイル . . . . . . . . . . . . . . . . . . 1, 5 さ 最遠点 Delaunay 三角形分割 . . 252 最遠点ボロノイ図 . . . . . . . 253, 259 最近点対 . . . . . . . . . . . . . . . . . . . . . 254 最小重み完全マッチング . . . . . .181 最小カット . . . . . . . . . . . . . . . . . . . 180 | に関するアルゴリズム .180 最小木 . . . . . . . . . 181, 253, 259, 262 | に関するアルゴリズム .181 ユークリッド | . . . . . . . . . 253 最小コストフロー . . . . . . . . . . . . 180 最小全域木 . . . . . . . . . . . . . . . . . . . 182 最小包含円 . . . . . . . . . . . . . . 253, 259 彩色問題 . . . . . . . . . . . . . . . . . . . . . 137 最大重み完全マッチング . . . . . .181 最大重みマッチング . . . . . . . . . . 181 2 部グラフの | . . . . . . . . . . 181 最大空円 . . . . . . . . . . . . . . . . 253, 259 最大フロー . . . . . . . . . . . . . . 180, 261 | の判定 . . . . . . . . . . . . . . . . 180 | の問題生成 . . . . . . . . . . . .180 最大マッチング . . . . . . . . . .180, 181 2 部グラフの | . . . . . . . . . . 180 | と最小節点被覆 . . . . . . . 180 | に関するアルゴリズム .180 要素数 | . . . . . . . . . . . . . . . . 262 最短経路 . . . . . . . . . . . . . . . . . . . . . 179 全節点対 | . . . . . . . . . . . . . . 179 | に関するアルゴリズム .179 最短経路問題 . . . . . . . . . . . . . . . . .163 三角関数 . . . . . . . . . . . . . . . . . . . . . . 47 三角形の符号付面積 . . . . . . . . . . 199 三角形分割 . . . . . . . . . . . . . . 252, 259 | された平面地図 . . . . . . . 182 制約つき | . . . . . . . . . . . . . . 252 制約つきの Delaunay | . 252 線分集合の |. . . . . . . . . . . .252 多角形の | . . . . . . . . . . . . . . 252 多角形領域の | . . . . . . . . . 252 点集合の | . . . . . . . . . . . . . . 252 平面グラフの | . . . . . . . . . 252 平面グラフの Delaunay |252 算術関数 . . . . . . . . . . . . . . . . . . 47, 48 算術式 . . . . . . . . . . . . . . . . . . . . . . . . 13 辞書式順序 . . . . . . . . . . . . . . . . . . . . 70 指数関数 . . . . . . . . . . . . . . . . . . . . . . 48 自然対数 . . . . . . . . . . . . . . . . . . . . . . 48 条件演算子 . . . . . . . . . . . . . . . . . . . . 46 条件式 . . . . . . . . . . . . . . . . . . . . .24, 26 条件分岐 . . . . . . . . . . . . . . . . . . . . . . 24 常用対数 . . . . . . . . . . . . . . . . . . . . . . 48 索引 推移的閉包 . . . . . . . . . . . . . . . . . . . 179 スタック . . . . . . . . . . . . . . . . . . . . . 105 | の応用例 . . . . . . . . . . . . . . 113 制御構造 . . . . . . . . . . . . . . . . . . . . . . 24 制約つき三角形分割 . . . . . . . . . . 252 節点 . . . . . . . . . . . . . . . . . . . . . . . . . 143 グラフの | . . . . . . . . . . . . . . 143 | 配列 . . . . . . . . . . . . . . . . . . 153 全域木 . . . . . . . . . . . . . . . . . . . . . . . 181 最小 | . . . . . . . . . . . . . . . . . . 182 前後のリスト項目 . . . . . . . . . . . . 128 線分 . . . . . . . . . . . . . . . . . . . . . . . . . 190 線分交差 . . . . . . . . . . . . . . . . . . . . . 254 線分集合 | の三角形分割 . . . . . . . . . 252 挿入ソート . . . . . . . . . . . . . . . . . . . 261 ソースコードパッケージ . . . . . . . . 2 ソーティング . . . . . . . . . . . . . . . . .261 ソート . . . . . . . . . . . . . . . . . . . . . . . . 86 クイック | . . . . . . . . . 257, 261 降順 | . . . . . . . . . . . . . . . . . . . 87 挿入 | . . . . . . . . . . . . . . . . . . 261 マージ | . . . . . . . . . . . . . . . . 261 ヒープ | . . . . . . . . . . . . . . . . 257 部分 | . . . . . . . . . . . . . . . . . . . 86 疎配列 . . . . . . . . . . . . . . . . . . . . . . . . 88 た ダイクストラのアルゴリズム . 165 ダイクストラの最短経路法 . . . 165 ダイクストラ法 . . . . . 179, 258, 262 275 代入文の値 . . . . . . . . . . . . . . . . . . . . 44 代入命令 . . . . . . . . . . . . . . . . . . . . . . 13 多角形 . . . . . . . . . . . . . . . . . . 196, 198 | の合併 . . . . . . . . . . . . . . . . 260 | の共通部分 . . . . . . . . . . . .260 ヒルベルト |. . . . . . . . . . . .260 単純な | . . . . . . . . . . . . . . . . 255 | の三角形分割 . . . . . . . . . 252 | の凸多角形分割 . . . . . . . 252 | のミンコフスキー差 . . . 253 | のミンコフスキー和 . . . 253 多角形領域 . . . . . . . . . . . . . . . . . . . 252 | の三角形分割 . . . . . . . . . 252 | の凸多角形分割 . . . . . . . 253 | のミンコフスキー差 . . . 253 | のミンコフスキー和 . . . 253 多次元配列 . . . . . . . . . . . . . . . . . . . . 69 単一始点最短経路 . . . . . . . . . . . . 179 単純な多角形 . . . . . . . . . . . . . . . . .255 逐次添加法 . . . . . . . . . . . . . . . . . . . 251 注釈文 . . . . . . . . . . . . . . . . . . . . . . . . 11 直線 . . . . . . . . . . . . . . . . . . . . . . . . . 194 直線描画 . . . . . . . . . . . . . . . . . . . . . 182 直交表現 . . . . . . . . . . . . . . . . . . . . . 182 定数の定義 . . . . . . . . . . . . . . . . . . . . 46 データの削除 . . . . . . . . . . . . . . . . .127 データの挿入 . . . . . . . . . . . . . . . . .126 デクリメント演算子 . . . . . . . . . . . 44 デモ . . . . . . . . . . . . . . . . . . . . . . . . . 257 デモプログラム . . . . . . . 3, 257{261 点 . . . . . . . . . . . . . . . . . . . . . . . . . . . 187 276 点集合 | に対する凸包 . . . . . . . . . 251 | の Delaunay 三角形分割252, 259 | の最遠点 Delaunay 三角形 分割 . . . . . . . . . . . . . . . . . 252 | の最遠点ボロノイ図 . . 253, 259 | の最小包含円 . . . . .253, 259 | の最大空円 . . . . . . . 253, 259 | の三角形分割 . . . . . . . . . 252 | のボロノイ図 . . . . .253, 259 点列の単純性 . . . . . . . . . . . . . . . . .255 独立点集合 . . . . . . . . . . . . . . . . . . . 182 凸多角形 . . . . . . . . . . . . . . . . . . . . . 198 | かどうかの判定 . . . . . . . 199 | の定義 . . . . . . . . . . . . . . . . 198 | の特徴づけ . . . . . . . . . . . .199 凸多角形分割 . . . . . . . . . . . . . . . . .252 多角形の | . . . . . . . . . . . . . . 252 多角形領域の | . . . . . . . . . 253 凸包 . . . . . . . . . . . . . . . . . . . . 209, 259 3 次元 {. . . . . . . . . . . . . . . . . .260 下部 | . . . . . . . . . . . . . 211, 251 下部 | の計算 . . . . . . . . . . . 251 上部 | . . . . . . . . . . . . . 211, 251 上部 | の計算 . . . . . . . . . . . 251 点集合に対する | . . . . . . . 251 | の計算 . . . . . . . . . . . 209, 251 トポロジカルソート . . . . . 159, 177 索引 は 場合分け . . . . . . . . . . . . . . . . . . . . . . 40 配列 . . . . . . . . . . . . . . . . . . . . . . . 59{69 2 次元 |. . . . . . . . . . . . . . . . . .69 C言語の | . . . . . . . . . . . . . . . 59 LEDA の |. . . . . . . . . . . . . . .85 多次元 | . . . . . . . . . . . . . . . . . 69 | の初期化 . . . . . . . . . . . . . . . 68 | 要素 . . . . . . . . . . . . . . . . . . . 61 ハッシュ関数 . . . . . . . . . . . . . . . . . . 94 バネ表現 . . . . . . . . . . . . . . . . . . . . . 182 幅優先探索 . . . . . . . . . . . . . . . . . . . 178 パラメータつきグラフ . . . . . . . . 154 半直線 . . . . . . . . . . . . . . . . . . . . . . . 192 ヒープソート . . . . . . . . . . . . . . . . .257 引数つきのマクロ . . . . . . . . . . . . . 47 描画色 . . . . . . . . . . . . . . . . . . . . . . . . 25 ヒルベルト多角形 . . . . . . . . . . . . 260 ファイル入出力 . . . . . . . . . . . . . . . 74 深さ優先探索 . . . . . . . . . . . . 178, 258 有向グラフの |. . . . .178, 258 複合代入文 . . . . . . . . . . . . . . . . . . . . 44 浮動小数点数 . . . . . . . . . . . . . . . . . . 81 IEEE 標準の | . . . . . . . . . . . 81 部分ソート . . . . . . . . . . . . . . . . . . . . 86 プリフロープッシュ . . . . . . . . . . 261 フロー . . . . . . . . . . . . . . . . . . . . . . . 180 最小コスト |. . . . . . . . . . . .180 最大 | . . . . . . . . . . . . . . . . . . 180 最大 | の判定 . . . . . . . . . . . 180 277 索引 最大 | の問題生成 . . . . . . 180 | に関するアルゴリズム .180 ブロック構造 . . . . . . . . . . . . . . . . . . 28 平方根 . . . . . . . . . . . . . . . . . . . . . . . . 48 平面幾何ライブラリ . . . . . . . . . . . . 6 平面グラフ . . . . . . . . . . . . . . . . . . . 254 与えられた線分集合で定まる | . . . . . . . . . . . . . . . . . . . 254 | の 4 色定理 . . . . . . . . . . . .137 | の 5 彩色 . . . . . . . . . 182, 258 | の三角形分割 . . . . . . . . . 252 平面性判定 . . . . . . . . . . . . . . . . . . . 262 グラフの { . . . . . . . . . . . . . . . 182 平面性判定問題 . . . . . . . . . . . . . . 139 平面走査法 . . . . . . . . . 223, 254, 260 平面地図 三角形分割された | . . . . . 182 平面的グラフ . . . . . . . . . . . . . . . . .137 べき乗 . . . . . . . . . . . . . . . . . . . . . . . . 48 ベルマン-フォードのアルゴリズム 179 辺 . . . . . . . . . . . . . . . . . . . . . . . . . . . 143 グラフの | . . . . . . . . . . . . . . 143 | 配列 . . . . . . . . . . . . . . . . . . 153 ポインタ . . . . . . . . . . . . . . . . . . . . . 126 ボロノイ図 . . . . . . . . . 235, 253, 259 点集合の | . . . . . . . . . 253, 259 点集合の最遠点 | . . 253, 259 ま マージソート . . . . . . . . . . . . . . . . .261 マクロ . . . . . . . . . . . . . . . . . . . . . . . . 47 引数つきの |. . . . . . . . . . . . . 47 |定義 . . . . . . . . . . . . . . . . . . . . 47 マッチング . . . . . . . . . . . . . . . . . . . 180 最小重み完全 | . . . . . . . . . 181 最大重み完全 | . . . . . . . . . 181 最大重みマッチング . . . . . . 181 2 部グラフの | . . . . . . . . 181 最大マッチング . . . . . . . . . . 180 2 部グラフの | . . . . . . . . 180 | と最小節点被覆 . . . . . 180 | に関するアルゴリズム180 要素数 | . . . . . . . . . . . . . . 262 | の判定 . . . . . . . . . . . . . . . . 181 ミンコフスキー差 . . . . . . . . . . . . 253 多角形の | . . . . . . . . . . . . . . 253 多角形領域の | . . . . . . . . . 253 ミンコフスキー和 . . . . . . . . . . . . 253 多角形の | . . . . . . . . . . . . . . 253 多角形領域の | . . . . . . . . . 253 迷路法 . . . . . . . . . . . . . . . . . . . . . . . 113 メインライブラリ . . . . . . . . . . . . . . 5 文字列 . . . . . . . . . . . . . . . . . . . . . . . . 69 | の比較 . . . . . . . . . . . . . . . . . 70 | 変数の初期化 . . . . . . . . . . 71 モデュロ演算 . . . . . . . . . . . . . . . . . . 26 や ユークリッド最小木 . . . . . 253, 259 ユーザ定義関数 . . . . . . . . . . . . . . . 50 優先順位つきキュー . . . . . 109, 167 278 | による経路探索 . . . . . . . 123 有理数 . . . . . . . . . . . . . . . . . . . . . . . . 79 | を用いた誤差なし計算 .227 要素数最大マッチング . . . . . . . . 262 ら ライセンス . . . . . . . . . . . . . . . . . . . . . 1 ライブラリ 3 次元幾何| . . . . . . . . . . . . . . . 7 幾何用グラフィックツール|7 グラフ| . . . . . . . . . . . . . . . . . . . 5 グラフィックツール| . . . . . . 6 平面幾何| . . . . . . . . . . . . . . . . . 6 メイン| . . . . . . . . . . . . . . . . . . . 5 ライブラリファイル . . . . . . . . . . . . 8 乱数 . . . . . . . . . . . . . . . . . . . . . . . . . . 56 double 型の | . . . . . . . . . . . . 58 | の生成 . . . . . . . . . . . . . . . . . 56 | の種 . . . . . . . . . . . . . . . . . . . 57 乱数列 . . . . . . . . . . . . . . . . . . . . . . . . 58 ランダマイズ木 . . . . . . . . . . . . . . 257 ランダムグラフ . . . . . 258, 261, 262 | の生成 . . . . . . . . . . . . . . . . 258 リスト . . . . . . . . . . . . . . . . . . .125{135 前後の | 項目 . . . . . . . . . . . 128 | からの削除 . . . . . . . . . . . .129 | 構造 . . . . . . . . . . . . . . . . . . 126 | 項目 . . . . . . . . . . . . . . . . . . 126 | における検索 . . . . . . . . . 130 | の先頭 . . . . . . . . . . . . . . . . 128 | の末尾 . . . . . . . . . . . . . . . . 128 索引 連結 | 構造 . . . . . . . . . . . . . 125 リンク . . . . . . . . . . . . . . . . . . . . . . . . . 5 ループからの脱出 . . . . . . . . . . . . . 38 連結リスト構造 . . . . . . . . . . . . . . 125 連想配列 . . . . . . . . . . . . . . . . . . . . . . 97
© Copyright 2025 Paperzz