わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

プログラミング課題を読み解く〜三角形の面積

昨日の続きです.二つあるうちの後者,「三角形の面積」について解説します.
この問題は,各学生に15分ほど与えて読み解く(プログラミングに必要な要素を取り出す)作業をしてもらいました.車座になってみんなの結果をつき合わせてから,それぞれコーディングに取りかかってもらいました.
課題文は次の通り.出典は『C言語10課 (ポインタ編)』です.

3つの二次元座標を入力し,その3点で囲まれる三角形の面積を出力しなさい.ただし,二次元座標を管理する適当な構造体を定義して使うこと.

昨日と同じやり方で進めていきます.まず,抜き出し.

  • 3つ(の二次元座標)
  • 入力
  • 三角形の面積
  • 出力
  • 二次元座標を管理する適当な構造体

次にそれぞれについて検討.ただし,検討の順序を入れ替えます.

  • 二次元座標を管理する適当な構造体 : これは,2次元座標に関する次の「基本形」をメモしておきます.intでいいかどうかは,また後で考えます.
struct point {
  int x;
  int y;
};
  • 入力 : キーボードだとか,ファイルだとかいった指定がないので,ここはプログラムを作りやすい方法を,自分で選ぶことにします.動作確認のしやすさから,“コマンドライン引数”を採用します.「コマンド」と「入力」を同時に与えることができますし,動作確認をするとき,入力の一部を変更するのが簡単だからです.関連語として“argc,argv”を書いておきます.
  • 出力 : ここも特に指定がありません.標準出力に出力することとします.“printf”ですね.
  • 3つ(の二次元座標) : “(x1,y1),(x2,y2),(x3,y3)”の3点です.入力としては“6つの数値”を獲得することになります.そして“→A,B,C”と書いておきましょう.3つの構造体の変数,という意味です.
  • 三角形の面積 : ここがこの問題の山場です.面積の求め方はいろいろあります.ヘロンの公式でしょうか.でもそれだと,3辺の長さを求めておかないといけません.3点の座標をもとに,3辺の長さを求められますが,2乗して足し算して,さらに平方根を求めて,それを3つのペア(点の)に対して求めるのはかなり手間です.何かいい方法がないか…インターネットで検索してみましょう.Googleで「三角形 面積」をキーワードに調べてみると,http://www004.upp.so-net.ne.jp/s_honma/heron/heron.htmというのが見つかりました.この中の“S=(1/2)| ad−bc |”が簡単そうです.ページを後ろまで見ると,これで求められる理由まで書いてありました.すばらしい*1.式をメモして,細かいことは次のステップで考えましょう.このページはまだ消さないように.

そして,もう一歩深い詳細化と,疑問の確認をしていきます.

  • 数値を扱うので,その型を決めます.具体的には,座標と面積です.
    • 座標は,整数値のみだと作れる三角形の種類が小さそうなので,実数を採用します*2.型は“double”.コマンドライン引数から実数型の値の変換には,“atof”を使います.文字列からintだとatoi,文字列からdoubleだとatofです.
    • 面積も,doubleです.座標の型より大きくも小さくもする理由がありませんので.
  • ということで,pointの構造体も,メンバの型はdoubleにしておきます.メモでは,2箇所の「int」に線を引いて,そばに「double」を書くことにしましょう.消しゴムで消すのは,消し跡が汚くなってしまいます.
struct point {
  double x;
  double y;
};
  • 3つの構造体を,先ほどA,B,Cと書きましたが,ここは配列を使うべきでしょうか? いや,点(二次元座標)の数が増えるとか可変だとかいうわけではないので,ここはstruct point型の3つの変数を使うことにしましょう.
  • そこでどう格納するかですが,ループを使うことなく,以下を書いておくことにします*3
    • “A.x = atof(argv[1]);”
    • “A.y = atof(argv[2]);”
    • “B.x = atof(argv[3]);”
    • “B.y = atof(argv[4]);”
    • “C.x = atof(argv[5]);”
    • “C.y = atof(argv[6]);”
  • 残すは,三角形の面積の再検討です.“S=(1/2)| ad−bc |”とメモしましたが,Sは面積として,a,b,c,d,は何でしょうか? 先ほどのページを見直すと,『A( a , b )、B( c , d )、O( 0 ,0 )』とあります.今回の問題とは一見異なる形ですが,我々の検討で使用しているA,B,Cを“平行移動”することで,この公式を適用するためのA,B,Oに変換することができます.以下をメモしておきます.
    • “A = A - C”
    • “B = B - C”
    • “O = O - C”
  • ただし,C言語では構造体どうしの引き算はできません.上の3つの引き算で,マイナスのそばに“関数を定義”と書いておきましょう.
  • こうしてA,B,Oが得られたら,次のようにして面積を求めることができます.「1/2」は「0.5」に置き換えておきます.一般に割り算よりも掛け算のほうが効率がいいし,何らかの理由で座標を整数としたときに,「整数割る整数は整数」の問題に悩まされなくてすみます.
    • “a = A.x;”
    • “b = A.y;”
    • “c = B.x;”
    • “d = B.y;”
    • “s = 0.5 * | a * d - b * c |;”
  • これでプログラムを作ろう! と思ったけど,おかしい.Cで「|」はビット単位ORです.ここの「|…|」は,「…の“絶対値”」です.絶対値を求めるには,ライブラリ関数*4ではなく,“0以上ならそのまま,負なら符号を反転”を使うことにします.Cのコードとして,“if (x < 0) x = -x;”も添えておきましょう.

コーディングの技法については別のトピックとして書きますが,上記について一点だけ.コマンドライン引数をとるときは,引数の数をチェックし,足りなかったらエラーで終了とする必要があります.数のチェックをせずに,argv[なんとか]で取り出そうしても,配列の領域外アクセスで「セグメントエラー」になることがあります.ここは,C.yへ代入する式のそばに,“if (argc < 7) エラー処理”と書いておきます.argcやargvの値を変更しなければ,argv[argc]は必ずNULLになります.なので,コマンドライン引数をちょうど6個とるのなら,argcの値は7です.

*1:このページの例4を見て,A0(1,0),B0(0,1),O(0,0)を一次変換によりA(a,b),B(c,d),O(0,0)に移したら,⊿A0B0Oの面積は1/2,⊿ABCの面積は,行列式の絶対値を掛ければ求められる,と考えて,学生には「線形代数の知識でこの式になるよ」と言ったのでした.

*2:一昨日は,実は整数型を採用してからプログラムを作り,あとで実数に変更していました.手元のメモには“座標:int”,“面積:double”と書いています.

*3:紙に書くときは,最初の行だけ全部書き,あとは違うところ(例えば2行目なら「 .y = [2]」)だけでいいでしょう.テキストエディタでこの6行を書くとき,すべてタイプするのは効率が悪い上にタイプミスの可能性もあります.ここは「A.x = atof(argv[1]);<改行>」をタイプし,この行を丸々コピー・ペーストして,一部だけ修正して「A.y = atof(argv[2]);<改行>」に書き換えます.これでAへの代入文ができますが,その2行をコピーして,2度,ペーストします.これで6行になります.あとは,AをBまたはCに変換し,argvの添字を書き換えます

*4:ライブラリ関数にabsというのがありますが,manpageを見ると,引数も戻り値もintなので,今回は利用できません.