Yの再帰–コンビネーター
test(dumb)(1)を呼び出してみましょう。 インタプリタは引数0でdumbを呼び出すことができないため、例外が発生します。トリックを実行して、呼び出しテスト(test(dumb))(1)を行うことができます。 このようにして、このような呼び出しテスト(test(test(test(test(test(test(test(test(test(test(dumb)))))))に到達することもできます。
このような呼び出しを行うことは可能ですか: test(test(. ))(x) 、ここで. はテスト関数への無限の呼び出しです
それは可能であり、 Yコンビネータがこれを支援します。
そのような呼び出し構造が明確に見える形式の1つを次に示します。
ウィキペディアの定義の一部として、内部ラムダ関数( ラムダn:. )へのリンクがあり、このリンクでtestを呼び出したと言うことができます。
もちろん、そのような階乗の実装には実用的な価値はなく、通常の再帰呼び出しよりもさらに遅くなります。 キャッシュを使用すると、メモリを犠牲にして計算を高速化できます。 これを行うには、Yコンビネータをわずかに変更する必要があります。
def Ycache ( f、data ) :
def _fn ( x ) :
データにx がある 場合:
戻り データ [ x フィボナッチ数列は再帰関数の題材として適切なのか ]
その他 :
temp = ( f ( lambda x:Ycache ( f、data ) ( x ) )
) ( x )
データ [ x ] = temp
一時 帰り
_fnを返す
qsort = lambda h: lambda lst: ( フィボナッチ数列は再帰関数の題材として適切なのか フィボナッチ数列は再帰関数の題材として適切なのか lst if len ( lst ) < = 1 else (
h ( [ item < lst [ 0 ] ]の 場合 、 lstのアイテムのアイテム ) + \
[ フィボナッチ数列は再帰関数の題材として適切なのか lst [ 0 ] ] * lst。 count ( lst [ 0 フィボナッチ数列は再帰関数の題材として適切なのか ] ) + \
h ( [ item > lst [ 0 フィボナッチ数列は再帰関数の題材として適切なのか ] ]の 場合、 [ lstのアイテムのアイテム ) ) ))
print ( Y ( qsort ) ( [ 1、13、2、12、3、11、4、10、5、9、6、8、7 ] ) ))
_________
テキストはHabraで準備されます
[1] これは、何らかのクラスの関数f(f(f(f(f(f(f(f(f(f(f(. f(x))))) )))))はyの値に収束し、 f(y)= yとなります。
[2] ウィキペディアは議論の中で、Y-コンビネーターはアルゴリズム的に不溶性の問題を解決する必要はなく、単に特定のクラスの関数での彼の振る舞いを提供するだけだと書いています。
情報処理Ⅱ 2006年12月8日(金).
Presentation on theme: "情報処理Ⅱ 2006年12月8日(金)."— Presentation transcript:
2 本日学ぶこと 関数の補足 既存のプログラムから関数を「抽出する」方法 再帰(recursion) クイックソート
3 関数の抽出 反復語判定プログラム(第7回)を例として 関数を抽出することで一般に 関数抽出の要点
コード量(プログラムのサイズ,ステップ数)は増える. しかし,それぞれの関数のコード量は減り,関数ごとに役割が明確になるため,読みやすい. 関数抽出の要点 ①関数名,②戻り値の型,③仮引数 の順に決める. 変数の有効範囲に注意する. 変数の除去や複製が必要なこともある. 保守性向上
4 再帰呼び出しとは 関数の内部で自分自身を呼び出すことを,再帰呼び出し(リカーシブコール,recursive call)という. 用途 注意点
再帰的(帰納的,循環的)な定義 バックトラック(backtrack) 分割統治法(divide-and-conquer) 注意点 無限ループにならないようにする. 再帰を使わないほうがよいこともある.
6 カウントアップするプログラム 出力の位置を変えると,カウントアップになる.
カウントアップ,カウントダウンともに,再帰呼び出しなしの プログラムのほうが,効率はよい. なぜ再帰呼び出しは効率が悪いか? …関数呼び出しのコスト(オーバーヘッド)があるから. 業務としてのプログラミングで,再帰呼び出しを使用してはならないと(コーディング規約で)定めていることも 再帰呼び出しはしてはいけないと書いている例: 『組み込みソフトウェア開発向けコーディング作法ガイド』p.49 『ずっと受けたかったソフトウェアエンジニアリングの授業2』pp
7 なぜ再帰呼び出しが可能なのか? 同一の変数名x に対して複数の このようなデータ 構造を,「スタック」 オブジェクトが という.
生成される. このようなデータ 構造を,「スタック」 という. countdown x = 0 countdown x = 1 countdownの再帰呼び出しを終えたときの戻り先 countdownの呼び出し(関数処理)を終えたときの戻り先 countdown関数処理時に 生成されるオブジェクト main x = 2 Cの規格として「スタック」が必須ということではない. しかし,関数呼び出しの構造(mainから関数Aを呼び出し,関数Aの中で関数Bを呼び出し,関数Bの処理を終えたら, 制御は,mainではなく,「関数Aの中で関数Bを呼び出したところ」に戻らなければならない(そして,関数Aの処理を 終えたら,制御は,mainの中で関数Aを呼び出したところに戻る))を実行時に保持するには,スタックが最も合理的 フィボナッチ数列は再帰関数の題材として適切なのか ということである. main関数処理時に 生成されるオブジェクト num = 2
8 再帰的な定義の例(1) 階乗 フィボナッチ数列 再帰呼び出しを用いれば,シンプルに書ける.
0! = 1, 1! = 1 n! = n×(n-1)! (n≧2) フィボナッチ数列 a1 = 1, a2 = 1 an = an-1 + an-2 (n≧3) 再帰呼び出しを用いれば,シンプルに書ける. しかしこれらも,whileループのほうが効率がよい.
9 再帰的な定義の例(2) 木構造 再帰呼び出しは必要? トーナメント表の決勝戦から順に見て, 優勝チームの戦績を知る
100万個の記録から,欲しいものを 瞬時に獲得する ホームディレクトリ以下のすべての ファイル名を出力する 再帰呼び出しは必要? するのが自然な場合と,してはいけない 場合がある 呼び出し方よりもデータ構造のほうが重要 「ホームディレクトリ以下のすべてのファイル名を出力する」には,いちいちプログラムを作らなくても, 「find ~」を実行するだけでいい.
10 分割統治法 対象領域を細分化(分割)して求め(統治),全体として正しい解になるようにする. 例: クイックソート 4 7 3 8 6 1 2
5 2 8 5 7 6 1 3 4 「4より小」と 「4より大」に 分ける. 1 8 7 6 5 4 3 2 1 2 3 4 5 6 7 8
11 クイックソートの関数呼び出し 配列領域,開始位置,終了位置を引数とする. 4 7 3 8 6 1 2 5 2 3 1 4 6 7 8 5 1
詳細は, のpp.21~23を参照のこと. 1 2 3 4 フィボナッチ数列は再帰関数の題材として適切なのか 5 6 7 8
12 関数の呼び出し関係 (quicksort.c)
main フィボナッチ数列は再帰関数の題材として適切なのか swap get_array_length quicksort print_score 再帰!
13 まとめ 再帰呼び出しを用いて関数を定義すると,プログラムがすっきり書けることがある. 一般に,再帰を用いないほうが効率的である.
再帰的に定義された問題に適用すると,効果的. 一般に,再帰を用いないほうが効率的である. 関数内のauto変数は,関数呼び出しごとに生成される.
15 ここで学ぶこと ライブラリ関数の活用 (参考)関数の分類 先人の知恵を活用する.
勉強・授業のためでなければ, ライブラリ関数と同じ名前や機能の関数は自作しない. (参考)関数の分類 自作関数: 自分で定義する.⇒ 12月1日のテーマ ライブラリ関数: 出来合いのもの.printfなど.⇒ ここでのテーマ
16 ライブラリ関数とヘッダファイル 既に定義されている関数や定数を利用するには,あらかじめ,適切なファイル名(ヘッダファイル)を記載しておく.
printf なら #include CHAR_BIT なら #include ライブラリ関数と,記載すべきヘッダファイル名は,manpage で知ることができる. man 3 printf jman 3 printf JM Project ( ヘッダファイルはコンパイル環境が保有しているが,その所在は,環境による. Vine Linuxのコマンド
17 ヘッダファイルとインクルード ヘッダファイルには,定数,構造体や特殊な型,関数プロトタイプなどが宣言・定義されている.
#include とすると,/usr/include/stdlib.h を取り込む(インクルードする). 慣例として,ファイル名は「.h」で終わらせる. ヘッダファイルの中で,他のヘッダファイルをインクルードすることもよく行われる. ヘッダファイルを自作することもある.そのファイルをインクルードするときは,#include "headerfile.h" のように書く. 「.c」や「.h」などを,Windowsユーザは「拡張子」と呼んでいるが, フィボナッチ数列は再帰関数の題材として適切なのか Unix文化では「サフィックス(suffix)」と呼ばれている.
18 有用なライブラリ関数(1) #include を必要とするもの
int printf(const char *format, . ); int scanf(const char *format, . ); int putchar(int c); … 1文字出力 int getchar(void); … 1文字入力 int sprintf(char *str, const char *format, . ); … printfと同様に文字列を構成し,結果をstr(の指し示す配列領域)に格納する 可変引数 と呼ばれる 可変引数 と呼ばれる
19 有用なライブラリ関数(2) #include を必要とするもの
int atoi(char *s); … 文字列から整数値への変換 void exit(int status); フィボナッチ数列は再帰関数の題材として適切なのか … プログラムの終了 int rand(void); … 乱数生成 #include を必要とするもの size_t strlen(char *s); … 文字列の長さ int strcmp(char *s1, char *s2); … 文字列比較 char *strstr(char *s1, char *s2); … 文字列検索 char *strcat(char フィボナッチ数列は再帰関数の題材として適切なのか *dest, char *src); …文字列連結 #include を必要とするものについて, strcatのdestを除き,char *型の仮引数には,constがつく.
20 有用なライブラリ関数(3) #include を必要とするもの
int isdigit(int c); … 文字が数字であるか判定 isalphaは英字判定,isalnumは英数字判定 int tolower(int c); … 大文字を小文字に変換 大文字以外の文字はそのまま 逆はtoupper #include を必要とするもの double exp(double x); … eのx乗 double floor(double x); … x以下で最小の整数 最大はceil,四捨五入(丸め)はround コンパイル時,「cc -lm program.フィボナッチ数列は再帰関数の題材として適切なのか フィボナッチ数列は再帰関数の題材として適切なのか c」のようにリンカオプション(linker option)が必要
21 関数名の表記 printf関数,関数printf printf()関数,関数printf() printf(3) この授業で採用している.
関数名であることを明確にする表記法.よく見かける. printf(3) 「printfというライブラリ関数があって,詳細は, man 3 printfを実行すれば得られる」の意味. 「(1)」ならコマンド,「(2)」はシステムコール man printfを実行すると,ライブラリ関数ではなく,コマンドとしてのprintfの使い方が出てくる. 「printfの実引数に3と書く」ではない.
「 情報構造論 」カテゴリーアーカイブ
基本的には、確保したメモリ領域を使い終わった後 free() を実行しないと、再利用できないメモリ領域が残ってしまう。こういう処理を繰り返すと、次第にメモリを食いつぶし、仮想メモリ機能によりハードディスクの読み書きで性能が低下したり、最終的にOSが正しく動けなくなる可能性もある。こういった free() フィボナッチ数列は再帰関数の題材として適切なのか 忘れはメモリーリークと呼ばれ、malloc(),free()に慣れない初心者プログラマーによく見られる。
ただし、ヒープメモリ全体は、プロセスの起動と共に確保され(不足すればOSから追加でメモリを分けてもらうこともできる)、プログラムの終了と同時にOSに返却される。このため、malloc()と処理のあと すぐにプロセスが終了するようなプログラムであれば、free() を忘れても問題はない 。授業では、メモリーリークによる重大な問題を理解してもらうため、原則 free() は明記する。
文字列を保存する場合
文字列を保存する場合には、上記の names[i] への代入のような malloc() と strcpy() を組み合わせて使うことが多い。しかし、この一連の処理の関数として、strdup() がある。基本的には、以下のような機能である。
また、入力した文字列をポインタで保存する場合、以下のようなプログラムを書いてしまいがちであるが、図に示すような状態になることから、別領域にコピーする必要がある。
配列に保存する場合
構造体の配列
同じように、任意サイズの構造体(ここではstruct Complex)の配列を作りたいのであれば、mallocの引数のサイズに「sizeof( struct Complex ) * データ件数」を指定すればいい。
後半の array2[] では、ポインタの配列を使った例を示す。この例では、1つの構造体毎に1つのmallocでメモリを確保している。
(おまけ)C++の場合
C言語における malloc() + free () でのプログラミングは、mallocの結果を型キャストしたりするので、間違ったコーディングの可能性がある。このため、C++ では、new 演算子, delete 演算子というものが導入されている。
注意すべき点は、malloc+freeとの違いは、mallocがメモリ確保に失敗した時の処理の書き方。返り値のNULLをチェックする方法は、呼び出し側ですべてでNULLの場合を想定した書き方が必要になり、処理が煩雑となる。C++の new 演算子は、メモリ確保に失敗すると、例外 bad_alloc を投げてくるので、try-catch 文で処理を書く。(フィボナッチ数列は再帰関数の題材として適切なのか 上記例はtry-catchは省略)
悪趣味なプログラム
ポインタ処理
値渡しとポインタ渡し
値渡し(call by value)
言い方を変えるなら、呼び出し側main() では、関数の foo() の処理の影響を受けない。このように、関数には仮引数の値を渡すことを、値渡し(call by value)フィボナッチ数列は再帰関数の題材として適切なのか フィボナッチ数列は再帰関数の題材として適切なのか と言う。実引数の値は、仮引数の変数に copy し代入される。
でも、プログラムによっては、124,125 と変化して欲しい場合もある。
どのように記述すべきだろうか?
しかし、このプログラムは大域変数を使うために、間違いを引き起こしやすい。
ポインタ渡し(call by pointer)
C言語で引数を通して、呼び出し側の値を変化して欲しい場合は、(引数を経由して関数の副作用を受け取るには)、変更して欲しい変数のアドレスを渡し、関数側では、ポインタ変数を使って受け取った変数のアドレスの示す場所の値を操作する。このような値の受け渡し方法は、ポインタ渡し(call by pointer)と呼ぶ。
変数の寿命とスコープ
変数の管理では、 変数の寿命 と スコープ の理解が重要。
静的変数:変数は、プログラムの起動時に初期化、プログラムの終了時に廃棄。
動的変数:変数は、関数に入るときに初期化、関数を抜けるときに廃棄。
もしくは、ブロックに入るときに初期化、ブロックを抜けるときに廃棄。
大域変数:大域変数は、プログラム全体で参照できる。
局所変数:関数の中 フィボナッチ数列は再帰関数の題材として適切なのか or そのブロックの中でのみ参照できる。
ポインタの加算と配列アドレス
ポインタに整数値を加える ことは、アクセスする場所が、 指定された分だけ後ろにずれる ことを意味する。
*(p + 整数式) と フィボナッチ数列は再帰関数の題材として適切なのか p[ 整数式 ] は同じ意味 (参照”悪趣味なプログラム”)
特に配列 a[] の a だけを記述すると、配列の先頭を意味することに注意。
ポインタインクリメントと式
しかし、この strcpy は、ポインタを使って書くと以下のように書ける。
構造体とポインタ
構造体へのポインタの中の要素を参照する時には、アロー演算子 -> を使う。
練習問題(2018年度中間試験問題より)
再帰呼び出しと再帰方程式
ハノイの塔
ハノイの塔は、3本の塔にN枚のディスクを積み、(1)1回の移動では ディスクを1枚しか動かせない 、(2)ディスクの上に フィボナッチ数列は再帰関数の題材として適切なのか より大きいディスクを積まない …という条件で、山積みのディスクを目的の山に移動させるパズル。
一般解の予想
ハノイの塔の移動回数を とした場合、 少ない枚数での回数の考察から、 以下の一般式で表せることが予想できる。
再帰方程式
ということが言える。(これがハノイの塔の移動回数の再帰方程式)
ディスクが枚の時、予想が正しいのは明らか①,②。
ディスクが フィボナッチ数列は再帰関数の題材として適切なのか フィボナッチ数列は再帰関数の題材として適切なのか 枚で、予想が正しいと仮定 すると、 枚では、
となり、 枚でも、予想が正しいことが証明された。 よって 数学的帰納法 により、1枚以上で予想が常に成り立つことが証明できた。
理解度確認
-
の「ピラミッドの体積」pyra() を、ループにより計算するプログラムを記述せよ。 での2分探索法のプログラムを、再帰によって記述せよ。(以下のプログラムを参考に)。また、このプログラムの処理時間にふさわしい再帰方程式を示せ。
再帰を使ったソートアルゴリズムの分析
- 参考:ソートアルゴリズム12種を可視化してみた
この中で、高速なソートアルゴリズムは、クイックソート(最速のアルゴリズム)とマージソート(オーダでは同程度だが若干効率が悪い)であるが、ここでは、再帰方程式で処理時間をイメージしやすい、マージソートにて説明を行う。
マージソートの分析
マージソートは、与えられたデータを2分割し、 その2つの山をそれぞれマージソートを行う。 この結果の2つの山の頂上から、大きい方を取り出す…という処理を繰り返すことで、 ソートを行う。
この再帰方程式を、N=1,2,4,フィボナッチ数列は再帰関数の題材として適切なのか 8…と代入を繰り返していくと、 最終的に処理時間のオーダが となる。
選択法とクイックソートの処理時間の比較
データ数 N = 20 件でソート処理の時間を計測したら、選択法で 10msec 、クイックソートで 20msec であった。
-
フィボナッチ数列は再帰関数の題材として適切なのか
- データ件数 N = 100 件では、選択法,クイックソートは、それぞれどの程度の時間がかかるか答えよ。
- データ件数何件以上なら、クイックソートの方が高速になるか答えよ。
再帰呼び出しの処理時間の見積もり
前回の授業の復習と練習問題
前回の授業では、for ループによる繰り返し処理のプログラムについて、処理時間を T(N) の一般式で表現することを説明し、それを用いたオーダー記法について説明を行った。理解を確認するための練習問題を以下に示す。
- ある処理のデータ数Nに対する処理時間が、であった場合、オーダー記法で書くとどうなるか?
- の処理時間を要するアルゴリズムを、オーダー記法で書くとどうなるか?また、このような処理時間となるアルゴリズムの例を答えよ。
- の処理時間を要するアルゴリズムを、オーダー記法で書くとどうなるか?
(ヒント: ロピタルの定理)
- 1は、N→∞において、N 2 ≪ 2 N なので、O(2 N ) 。厳密に回答するなら、練習問題3と同様の証明を行うべき。
- 2は、O(1)。誤答の例:O(0)と書いちゃうと、T(N)=Tα×0=0になってしまう。事例は、電話番号を、巨大配列の”電話番号”番目の場所に記憶するといった方法。(これはハッシュ法で改めて講義予定)
再帰呼び出しの基本
再帰関数は、自分自身の処理の中に 「問題を小さくした」自分自身の呼び出し を含む関数。プログラムには 問題が最小となった時の処理 があることで、再帰の繰り返しが止まる。
階乗 fact(N) を求める処理は、以下の様に再帰が進む。
また、フィボナッチ数列 fib(N) を求める処理は以下の様に再帰が進む。
再帰呼び出しの処理時間
> 再帰方程式
このような、式の定義自体を再帰を使って表した式は再帰方程式と呼ばれる。これを以下のような代入の繰り返しによって解けば、一般式 が得られる。
一般的に、再帰呼び出しプログラムは(考え方に慣れれば) 分かりやすくプログラムが書ける が、プログラムを実行する時には、局所変数や関数の戻り先を覚える必要があり、 深い再帰ではメモリ使用量が多くなる 。
ただし、fact() や pyra() のような関数は、プログラムの末端で再帰が行われている。(fib()は、再帰の一方が末尾ではない) フィボナッチ数列は再帰関数の題材として適切なのか
このような再帰は、末尾再帰(tail recursion) と呼ばれ、関数呼び出しの return を、再帰処理の先頭への goto 文に書き換えるといった最適化が可能である。言い換えるならば、 末尾再帰の処理は繰り返し処理に書き換えが可能 である。このため、末尾再帰の処理をループにすれば再帰のメモリ使用量の問題を克服できる。
再帰を含む一般的なプログラム例
このプログラムでは、配列の合計を計算しているが、引数の L,フィボナッチ数列は再帰関数の題材として適切なのか R は、合計範囲の 左端(左端のデータのある場所)・右端( 右端のデータのある場所+1 )を表している。そして、再帰のたびに2つに分割して解いている。
このプログラムでは、対象となるデータ件数(R-L)をNとおいた場合、実行される命令からsum()の処理時間Ts(N)は次の 再帰方程式 で表せる。
これを 代入の繰り返し で解いていくと、
繰り返し処理と処理時間の見積もり
単純サーチの処理時間
例えばこの 単純サーチをフローチャートで表せば、以下のように表せるだろう。フローチャートの各部の実行回数は、途中で見つかる場合があるので、最小の場合・最大の場合を考え平均をとってみる。また、その1つ1つの処理は、コンピュータで機械語で動くわけだから、処理時間を要する。この時間を . とする。
ここで例題
ここで一番のポイントは、データ処理では N が小さな値の場合(データ件数が少ない状態)はあまり考えない。N が巨大な値であれば、Tαは、1000Tβに比べれば微々たる値という点である。よって
で考えれば良い。これであれば、T(1000)=5μ秒=Tβ×1000 よって、Tβ=5n秒となる。この結果、T(10000)=Tβ×10000=50μ秒 となる。
2分探索法と処理時間
ここで、本来なら log の底は2であるが、後の見積もりの例では、問題に応じて底変換の公式で係数が出てくるが、これはTβに含めて考えればいい。
単純なソート(選択法)の処理時間
オーダー記法
単純サーチ |
2分探索法 |
最大選択法 |
そこで、 アルゴリズムの優劣を議論する場合 フィボナッチ数列は再帰関数の題材として適切なのか は、この処理時間の見積もりに最も影響する項で、コンピュータの性能によって決まる係数を除いた部分を抽出した式で表現する。これをオーダー記法と言う。
単純サーチ | オーダーNのアルゴリズム |
2分探索法 | オーダー log N のアルゴリズム | フィボナッチ数列は再帰関数の題材として適切なのか
最大選択法 | オーダー N 2 のアルゴリズム |
- ある処理のデータ数Nに対する処理時間が、であった場合、オーダー記法で書くとどうなるか?
- コンピュータで2分探索法で、データ100件で10[μsec]かかったとする。 フィボナッチ数列は再帰関数の題材として適切なのか
データ10000件なら何[sec]かかるか?
(ヒント: 底変換の公式) - の処理時間を要するアルゴリズムを、オーダー記法で書くとどうなるか?また、このような処理時間となるアルゴリズムの例を答えよ。
- の処理時間を要するアルゴリズムを、オーダー記法で書くとどうなるか?
(ヒント: ロピタルの定理)
再帰呼び出しの予習
次の講義への導入問題
-
フィボナッチ数列は再帰関数の題材として適切なのか
- fact(N)の処理時間を、Tfact(N) = … のような式で表現し、処理時間をオーダ記法で答えよ。
- 以下のプログラムの実行結果を答えよ。また、関数sum()の処理時間を対象となるデータ件数N=R–Lを用いて Tsum(N) = …のような式で表現せよ。
情報構造論ガイダンス2022
基本的なガイダンス
情報構造論のシラバスを、ここに示す。プログラムを作成する上で、どのような考え方で作れば処理速度が速いのかを議論する。基本的に、4回のテストのたびに、レポート課題を実施する。各テスト毎の評価は、テスト素点と、「テスト素点×60%+レポート評価×40%」の良い方とする。テストに自信のない人は、レポート課題をきちんと提出すること。
プログラムを評価する3つのポイント
- あなたが”良い”プログラムを作るために何を考えて作りますか? ※1
- ここまでの段階で 3つの要点を考えメモ してください。
メモリの使用量の影響
メモリを大量に使用すると、どういった影響がでるのか? OSの機能を知らないと、メモリ(主記憶)を使い果たしたら、プログラムが動かないと思うかもしれないけど、最近のOSは仮想メモリ機能があるため、主記憶がメモリが足りなければ待機状態のプロセスのメモリを補助記憶に保存することで、プログラムを動かすことはできる。(仮想記憶)
int 型のメモリ使用量
int 型は、プログラムで扱う一般的な整数を扱うのに十分なデータ型。
32bit の0/1情報の組み合わせで、2 32 通りの情報が表現でき、負の数も扱いたいことから2の補数表現を用いることで、-2 31 ~0~2 31 -1 の範囲を扱うことができる。2 31 = 2×2 10 ×2 10 ×2 10 ≒ 2×1000 3
32bit = 4byte
ソフトウェアとアルゴリズムとプログラム
- アルゴリズム – 計算手順の考え方。
- プログラム – アルゴリズムを特定のプログラム言語によって記述したもの。
- ソフトウェア – プログラムと、その処理に必要なデータ。
(日本語を変換するプログラムは、日本語の辞書データが無いと動かない/役に立たない) - パラダイム – プログラムをどう表現すると分かりやすいか?
トレードオフ関係をプログラムで確認
良いプログラムを作るとは
実際には、限られた予算からメモリやCPUが決まり、その会社の人員やら経験やらでプログラム開発に使える時間がきまる。プログラムをデザインするとは、 限られた条件の中で 、適切な速度のコンピュータ、適切な量のメモリでコンピュータを用意し、限られた納期の中でシステムを完成させることである。
2021年度授業アンケート
情報制御基礎(3年学際科目)フィボナッチ数列は再帰関数の題材として適切なのか
情報構造論(4EI)
若干ポイントも昨年度より上がっているけど、アンケート回答数をみると未回答の人もある程度いるようで、まじめに授業を受けた人がまじめにアンケートに回答してくれたことの影響が多いと思う。マニアックなネタを興味もってくれたり、既に習っている内容がアルゴリズムとしてどういう意味を持っているのかの理解になったとの積極的な意見があってよかった。
データベース(5EI)
こちらもポイントは若干上がっているけど、アンケート回答数からすれば、まじめな授業参加者のおかげかな。他の授業も含め、全講義資料のWeb公開、テスト過去問題の公開に加え、SQLの演習環境をWebで学内向け公開なども実施しているし、好印象を持ってもらえたと思う。
オブジェクト指向プログラミング(専攻科生産システム2年)
情報構造論2021全講義録
情報構造論とオブジェクト指向
データ構造を扱うプログラムの書き方を説明してきたが、その考え方をプログラムにするためには手間もかかる。こういった手間を少しでも減らすために、プログラム言語が支援してくれる。その代表格がオブジェクト指向プログラミング(Object Oriented Programming:略称OOP)であり、以下にその基本を説明する。
データ指向のプログラム記述
このプログラムの書き方では、saitohというデータにset_NameAge() , print_NameAge() を呼び出していて、データに対して処理を加えるという雰囲気がでている。(C言語なのでデータに処理を施す関数には、必ずどのデータに対する処理なのかを与えるポインタがある。) このようにプログラムを書くと、saitoh というデータに対して命令するイメージとなり、擬人化したデータに向かってset,printしろ…って命令しているように見える。
このプログラムでは、例えば、データに誕生日も覚えたいという改良を加えるとしても、main の前のデータ構造と関数の部分は色々と書き換えることになるだろうけど、main の内部はあまり変わらないだろう。こういう書き方をすればプログラムを作成するときには、 データ構造とそれを扱う関数を記述する人 と、 データ構造を使う人 (main内部を書く人)と、分業ができるようになる。
このような記述では、 データ構造の中身を知らなくても、main で、setしてprintして…という処理の雰囲気は分かる 。 さらに、set_NameAge()とか、print_NameAge() の処理の中身を知らなくても、設定するとか表示するとか…は予想できる 。
これは、NameAge というデータをブラックボックス化(隠蔽化)して捉えていると見れる。データ構造の中身を知らなくてもプログラムを理解できることは、データ構造の隠蔽化という。また、関数の中身を知らなくても理解できることは、手続きの隠蔽化という。
オブジェクト指向プログラミング
前述のように、プログラムを書く時には、データ構造とそのデータを扱う関数を一緒に開発する方が分かり易い。そこで、プログラム言語の文法自体を、 データ構造 と その関数 (メソッドと呼ぶ)をまとめてクラスとして扱うプログラムスタイルが、オブジェクト指向プログラミングの基本である。
このプログラムでは、saitoh というデータ( 具体的なデータ が割り当てられたものはオブジェクトと呼ぶ)フィボナッチ数列は再帰関数の題材として適切なのか に対して、set() , print() のメソッドを呼び出している。
オブジェクト指向では、データに対して private を指定すると、クラス以外でその要素やメソッドを扱うことができなくなる。一方 public が指定されたものは、クラス外で使っていい。これにより、クラスを設計する人と、クラスを使う人を明確に分けることができ、クラスを使う人が、クラス内部の変数を勝手に触ることを禁止できる。
プログラムを記述する時には、データ件数を数える時に、カウンタの初期化を忘れて動かないといった、初期化忘れも問題となる。オブジェクト指向のプログラム言語では、こういうミスを減らすために、 データ初期化専用の関数 (コンストラクタ)を定義することで、初期化忘れを防ぐことができる。
プログラムにオブジェクト指向を取り入れると、 クラスを利用する人 と クラスを記述する人 で分業ができ、クラスを記述する人は、クラスを利用するプログラマーに迷惑をかけずにプログラムを修正できる。
この結果、 クラスを記述する人はプログラムを常により良い状態に書き換えることができるようになる 。このように、よりよく改善を常に行うことは リファクタリング と呼ばれ、オブジェクト指向を取り入れる大きな原動力となる。。
最近のC++なら
最近のオブジェクト指向プログラミングは、テンプレート機能と組み合わせると、単純リスト処理が以下のように書けてしまう。struct 宣言やmalloc()なんて出てこない。(^_^;
フィボナッチ数列は再帰関数の題材として適切なのか
ところで、すごいHaskell本は説明順序も適切ですし、例も豊富にある良い本なのですが、勉強会を進めて不満に思うことが出てきました。
それは、自分で考えて手を動かすための演習問題が一切ないという点です。 フィボナッチ数列は再帰関数の題材として適切なのか
例が豊富にあるとはいえ、プログラミングの醍醐味は「どうやって解くかを考えて、それをプログラムに落としこむ」ところにあると自分は思っています。
また、そうやって問題を解くことで記憶も定着しますし、必要な知識が身についているかの理解度チェックにもなります。そういうわけで、社内勉強会では章が終わるごとに演習問題を解いて理解を確認してもらう、ということを行っています。(宿題というわけではなく、任意で解きたい人だけ解いてもらうような感じです)
この演習問題、もともとは社内Githubにまとめていたのですが、社外から見れないと不便であることと、特に社外秘の情報というわけでもないことから、Githubに公開することにしました。
現在勉強会が5章までしか進んでいないので、5章分までの公開です。すごいHaskell本を自習している方や勉強会をしている方、もしくは単にHaskellの問題を解いてみたい方など、ご自由に利用していただければと思います。
なお、問題作成にあたり自分で解けることは確認していますが、記述に間違いがあったり分かりづらい部分があるかもしれません。
そういった場合はメールかTwitterで@kokuyouwind宛てにリプライをいただければと思います。第1章 はじめの第一歩
第1章は、解説されているのが基本的な演算と、リストやタプルなどの基本的な派生型、そしてリスト内包表記くらいだったため、この時点では複雑な計算は行えません。
そこで第1章の演習問題では、幾何学を題材にすることにしました。
幾何学なら座標点を2-Tupleとして、図形を座標点のリストとして、自然に扱えるだろうという目論見です。しかしながら、ユークリッド幾何学では当たり前の計算結果しか得られず面白みに欠けます。
また、点間の距離に実数演算が混ざってしまうため、この時点であまり意識させたくない数値型クラスの問題などにハマってしまう可能性があります。
整数演算だけに絞ればそのリスクを抑えられますし、整数だけを扱うほうがリスト内包表記との相性も良いでしょう。そのあたりを勘案し、あえて馴染みの薄いであろうマンハッタン距離を採用したタクシー幾何学をテーマに据えました。
これなら整数座標点間の距離が必ず整数になるため扱いやすく、また一般的には知らない概念のため面白みもあるでしょう。
問題の構成としては、- 1問目で距離感数を定義(基本的な関数定義とタプルの利用)
- 2問目で点の集合の作成(リスト内包表記のジェネレータ)
- 3問目で円の作成(リスト内包表記のフィルタ)
第2章 型を信じろ!
第2章は、解説されていることが主に「型クラスとはどんなものか」であり、しかも自分で定義するのは第7章までお預けのため、勉強会でもちょっともやっとしたまま終わってしまいました。
このため演習問題を出すにしても、何を理解してもらうことを狙うかが微妙で、readやshowの使い方に関する問題を出しても仕方ないだろうと思い、第2章については演習問題を省略することにしました。
今にして思えば、数値型どうしの変換などについては少し問題を出しても良かったかもしれません。第3章 関数の構文
第3章は基本的な再帰とパターンマッチなどの構文について触れられており、ようやくまともに関数を書けるようになってきます。
第3章の演習問題では、自然とパターンマッチやガードを用いることのできる題材としてフィボナッチ数列を考えましたが、定番すぎるため少し変化させたトリボナッチ数列を大問1に据えました。
大問2では、タプルのパターンマッチを利用してもらうため、2-タプルで表した有理数をテーマに、やや複雑な計算を問題としました。
特に2-3ではgcd関数を使って既約な形を作る必要があるため、ここでletやwhereなどの構文を使ってもらえるだろう、という狙いです。第4章 Hello再帰!
大問1では基本的な再帰関数の問題を出していますが、1-3はリスト末尾まで読まず再帰の途中で打ち切る可能性のある関数、1-4では2つのリストのペアを返す関数を扱います。
大問2では挿入ソートをテーマに、「全体をソートする」という大きな問題から「ソート済みのものに1つの要素を挿入する」という小さな問題に分割する、という方針で問題を解いていきます。
大問3は少しチャレンジングですが、分割数をテーマに漸化式を導き出すよう誘導するヒントを出しています。これらの問題を通じて、再帰関数を設計する上で必要となる「簡単な例で考える」「パターンを見つける」「小さな問題に分割する」といった定石を体感してもらうことを狙っています。
この辺りはポリアの「いかにして問題をとくか」にも通じるものがあるでしょう。
また、急激に値が大きくなるテトレーションや、簡単な法則なのに一般式が非常に複雑になる分割数など、題材としても面白いと感じてもらえるだろうものを選んでいます。第5章 高階関数
- 高階関数の定義
- 基本的な高階関数であるmap、filterなどの使い方
- 畳み込みの使い方
- 関数適用演算子と関数合成演算子の使い方
などが解説されています。
個人的には第5章がこの本の1つ目の山場だと考えており、ここで高階関数の扱い方に馴染めるかが今後の章の理解度に影響しそうな気がしています。
そういったことと、ちょうど年末年始を挟んで3週間ほど空いてしまうため、第5章の演習問題は少し多めに作ることにしました。大問2では畳み込みの操作に慣れてもらうことを狙っていますが、畳み込みでできることは再帰でも書けてしまうため注釈をつけています。
また、単純にsumやproductを問題にしてしまうといちいち「既存の関数を使わず」という注釈が必要になってしまうため、既存の関数を利用しては難しいだろう問題を設定しています。
特に2-2と2-3は少し頭をひねる必要があるものにしており、畳み込みの醍醐味を味わってもらえるのではないかと思います。大問3では趣向を変えて、適用演算子と合成演算子に慣れてもらうことを目的にポイントフリー変換を出題しました。
パズル的な面白さを味わってもらうことを念頭に、だんだん複雑になるように問題を設定しています。
特に3-5は難解ですが、3-3と3-4を解くことで段階的に材料が揃うように配慮しています。- 4.1 チャーチ数
- 4.2チャーチ真理値
- 4-3 ラムダ計算と型付け
正直な所、大問4はテキストの内容を確認するというよりも、テキストの内容を使ってラムダ計算を学ぶことを目的にしています。
ラムダ計算は関数型言語の基盤となる理論のため、知っておくに越したことはないのですが、大学などでないと体系的に学ぶ機会がなく、また理論をなぞるだけでは退屈な気がするため、こうして演習に取り入れるのが一番定着させやすいのではないかと思います。
なお問題の補足にも書いたとおり、Haskellのラムダ式を使って型なしラムダ計算の体系を構築すると型の制約に引っかかってしまうため、第7章でデータ型を扱えるようになってから改めてラムダ計算の項をデータ型を使って表し、それを用いて不動点演算子と再帰を扱う予定です。以上、今回公開した問題を作る上で意図した部分などです。
問題を解く際には知らなくても良いことなのですが、知っておくと理解のヒントになるかもしれませんし、もし同じように問題を作っている人がいたら参考になれば、ということでまとめてみました。
今後、章が進んで問題を追加した際も同様に記事にまとめていこうかと思います。なぜdp「やるだけ」なのか ~動的計画法について考える その1~
さて,ここで少し自問してみて欲しいのですが,指名された生徒の一人一人は前後左右の生徒と自分の点数を比べて(時には自分の点数すら忘れて)その中で一番大きい数字を覚えるようにしているだけです.それを何らかのルールに則って順番に実行させることで,あなたはクラス全体の最高点を知ることができています.さらに言えば,解答例1の薄緑の線を省略しても大丈夫そうと考えることもできるかもしれません.しかし大切なことは,生徒たちに許された動作を考慮したときにどう順番に実行すれば,余さず必要な情報を手元に届けられるか,ということです.今回の問題の場合,クラスの誰が最高得点を得ているか分からないので,全員を経由していることと,それが最終的に21番太郎君の所まで運ばれてくること,です.極端なやり方を挙げれば,1から40までの順に機械的に指名するというのを40回繰り返してから21番太郎君に訊くのでもいい.
" 何"を," どう" 伝えるか!? ~「状態」と「遷移」 ~
本記事のテーマが「なぜdpは「やるだけ」なのか」という事だったので,いかにも簡単すぎる題材を選んでみたのですが,競プロにおける多くのdpの問題はこのような" 何"を," どう" 伝えるかという部分を考察することを必要とします.この”何”にあたる部分を「状態」,"どう"にあたる部分を「遷移」と呼びます.(上の例だと,40人の脳内に何の数字があるかという情報が「状態」,とが「遷移」にあたります)
おそらく一般的には「状態」を適切に設定することが難しいです.「遷移」は自分の行動にあたるので,(効率的かはともかく)何かを挙げることは可能なのだけれど,遷移ドリブンで考えると適切に状態を定義できなかったり広くなりすぎたりしてしまいます.かといって必要な情報は網羅していないといけない.逆に言えば,適切に状態を設定できるということに正しく遷移させられるという要求が含まれます. 上の40人学級の例で,どう遷移を選ぶかという問題に対して割といろいろ考えたり工夫することができるのも,すでに状態が設定できているからとも言える.
- (相対的に)実装する段階で解の正当性の考察が完了している.
そして更に,上に書いたようにdpの難しさは「状態」の設定にあるので,いいかえれば場面場面に応じたテクニカルな「状態」の設定方法が先人の知恵,典型的なアルゴリズムとしていろいろ名前がついています.テクニカルなので,最初は思いつかなくて当然です.しかしテクニックなので,一度習得してしまえば応用が利き,かつ遷移後の状態が求めるものかどうかがすぐに分かるようになります.(ちなみに私が最初に感銘を受けた状態設定をもつdpはナップサック問題でした.そのあたり次章で書きたいと思います).なので,見知ったdpの問題に帰着できた場合にも,実績あるアルゴリズムだからというよりは,「どう推移するかが頭の中で完全に把握できる→正当性が保証される」が引き金となって,ああやるだけだなぁと思ってしまうのではないでしょうか.
ということで,この記事の結論としては敢えて,
dpは「状態」と「遷移」を正しく決められれば,正当性が保証されるので,やるだけ
としたいと思います.出来るとは言ってないしこれトートロジーですよね関連記事
コメント