==================================== ナンバーライン改造プログラムについて ==================================== 1.最初に (1) NumberLine.java はもともと藤原博文さんのプログラムです.それを私(亀井)が勝手に改造を加えて自動的に解けるような(無粋な)ものにしました.とはいえ,解くことだけを至上とはせず,解いていて行き詰まったときに機械にちょっとヒントをもらうという形にはしたつもりです.機械が使用するアルゴリズムも人間が解く場合の発想と同じです.解の枝を片端からつぶしていくという方法は取っていません.だから機械が教えてくれるヒントも,よく考えればなぜそうなるのかがわかるはずです. (2) 私は java programming は全くの初めてであり,テクニックのすべては藤原さんのオリジナルプログラムから学びました.プログラムのおかしな部分,へたくそな部分は亀井の理解不足のためです.(解法のアルゴリズム以外の部分はほとんど理解しておらず,オリジナルが動いているのをそのまま利用しています.) ---------------------------------------------------- オリジナル作者の藤原博文さんに敬意を表し感謝します. ---------------------------------------------------- 2.オリジナルからの変更点(お詫び) (1) Hint を Trial に変えさせていただきました.(追加した機能の方がヒントという感じなので.) (2) Trial mode に入る際,edgeinfo[][].draw を丸ごと保存用の配列 int trial_temp[][] に保存しておき,Trial mode から抜ける際に書き戻すようにしました.持っている情報が複雑になってきたので,書いた線を消すというような操作では対応できなくなったためです.(内部情報は少し失われますが trivial_check で復元できます.) (3) area size が xn*yn だったのを,画面に表示しない area で取り囲んで (xn+2)*(yn+2) にしました.(その方が例外処理が少なくて楽だからです.)vertex, edge も同様です. (4) HTML file の width を 200 増やしています.( panel を表示するスペースが必要なため.) (5) "Copyright(C)1997 Hirofumi Fujiwara." の後ろに勝手に " Arranged 1999 Masato Kamei" などと付けてしまいました.(公開していませんので,単なる自己満足です), (6) 友人に試してもらったところ,もっと易しい問題が解きたいというので,おかゆさんの問題が扱えるようにしました.(コンパイル時に FUJIWARA=false;OKAYU=true; で切り替える.) 3.操作法 以下では「辺」は場所を表す用語とし,そこに実際に引かれるものを「線」と呼びます.つまり辺には線のある辺と線のない辺の2種類が存在するわけです.(「辺」は NumberLine.java のコメント行では「稜線」と呼ばれていますが,線のある稜線,線のない稜線というのも紛らわしいので,辺と呼ぶことにしました.) (1) HTML file を立ち上げると問題を読み込み,上部にコントロールボタン類,右側に情報表示部が表れます.情報表示部は textfield になってはいますが,入力に使われることはありません.出力のみです.(開発段階では入力にも使っていました.そのままにしています.) (2) Trial mode は「お試し」モードで,「仮にここにあるとして見よう」という試行を取り消し可能な状態で進めることができます.(オリジナルの Hint mode です.) Trial ボタンを押すと trial mode の on, off が切り替わります. Trial mode が on, off いずれでも,Solve,Trivial,Triv_rep,Parity,Loop,Dual ボタンを受け付けます. Trial mode が off の場合は,辺上の直接クリックにせよ Solve,Trivial,Triv_rep,Parity,Loop,Dual ボタンによる機械判断にせよ,実線または×が黒色で表示されます. Trial mode が on の場合は,実線または×が緑色で表示されます. Trial mode を on から off に切り替えた場合は,緑色の実線及び×がすべて消え,状態が Trial mode on に入る直前の状態に戻ります. (3) Trivial ボタンを押すとプログラムが trivial_check を実行し,パズル盤面の状態が少し進行します.何度も押すと,そのたびごとに少しずつ進行していきます.(易しい問題ならこれだけで解決してしまいます.) trivial_check とは,隣接する面と頂点,面と辺,頂点と辺の状況の可能性の摺り合わせから,不可能手を殺していくという消去法のことです.ただし,このボタンを押して内部的に手がかりが増えても,見た目に線が表れないこともあります.(対角線に 3,2,2,...,2,3 と並んでいる場合など,数回押すして初めて線が表れます.) 本当に手がかりが増えたかどうかを知るには,情報表示部の changed * の欄を見てください.ボタンを押した後ここが 0 でなければ,何らかの進歩があったということです.すべて 0 ならこのチェックではこれ以上先に進めないということを意味します. (4) Triv_rep とは Trivial の繰り返しです.( Trivial ボタンを,それ以上手が進まなくなるまで押し続ける操作に等しい.) 巨大な盤面などで,単純な作業を省略したいときに使うと便利です. (5) Parity ボタンを押すと parity_check をかけて不可能手を消します. parity_check とは,2つのジョルダン閉曲線が交わる場合,交点は偶数個であるという事実に基づいたチェック法です.trivial_check で行き詰まっている場合,このチェックで新たな攻略点を見つけられることがあります. (6) Loop ボタンを押すと loop_check をかけて不可能手を消します. loop_check は,小さい loop ができてしまわないようにするチェックです.trivial_check で行き詰まっている場合,このチェックで新たな攻略点を見つけられることがあります. (7) 以上の操作で解ききれないときに使用するのが dual_check です.Dual ボタンを押すと(Trial mode on, off のいずれでも) Dual mode に入ります.もう一度 Dual ボタンを押すと Dual mode から抜けます. Dual mode にはいると,プログラムは Dual ボタンと辺の指定以外を受け付けなくなります. Dual mode on の状態で未決定の辺の上をマウスでクリックすると,プログラムはその辺に (i) 線があると仮定してさらに手を進めていった最終状態 (ii) 線がないと仮定してさらに手を進めていった最終状態 の2つを作り出します.最終状態とは,仮定を取り込んだうえで trivial_check, parity_check, loop_check を変化がなくなるまでかけ続けて得られる状態のことです.作業の様子は,クリックした辺が赤色,そこから伸びる手が青色で表示されることにより確認できます. この場合 (i), (ii) のチェックの一方が矛盾すれば他方が正解だとわかります.(両方が矛盾すれば,それまでの着手が間違っているか,問題自体に解がないかのどちらかです.) 両方とも矛盾しない場合,それぞれの最終状態を比較して,線のあるなしの一致するところはその状態に確定します.(Solve mode では,線のあるなしの一致しないところにしばらくチェックの必要無しという情報を持たせています.) Trivial, Parity, Loop のすべてで行き詰まった場合,怪しそうなところを Dual mode でチェックしてみると,新たな攻略点を見つけられることがあります. (8) Solve ボタンを押すと(Trial mode on, off のいずれでも) Solve mode に入ります.trivial_check, parity_check, loop_check で確定するところをすべて確定させてから,未確定の辺について片っ端から dual_check をかけていきます.(ある辺について dual_check で何の情報も得られなかった場合,深追いはしません.攻撃目標を他の辺に変えます.) これを繰り返して,何をどうしようともそれ以上変化のないところまで解を追い続けます. この解法は原理的にいって,未確定の部分の解を仮定するのは 1 回だけであり,仮定の上に仮定を重ねていくという方法は取っていません.従って,この解法では完全な解が求められない問題も存在するかもしれません.(存在するでしょう.) しかし,今までそのような問題には出会っていませんし,そのような問題は人間には不可能なほど難しいのではないでしょうか. もしも本当にそのような問題が出てきたら,2世代の仮定を許す改造を施して見るつもりです.それでもだめなら無限世代に作り替えます.(誰かそんな難問を作ってくれることを希望しています.) (9) Clear, Check ボタンはオリジナルと同じです. 4.このプログラムが解を見つける方法 (trivial_check) について (1) 概要 各面は,自分自身の周囲の4つの辺について,線のあるなしの組み合わせ(自明な場合を除いて 15 通り)の状態に関する可能性を保持しています. つまり,全く線がない,左にだけ線がある,左と右と上に線があり下には線がない などという線のあるなしの組み合わせの状態が 2^4=16 通りあり,そのうち左右上下すべてに線のある状態があり得ないとすると,考えられる状態が 15 通り残ります.その 15 通りの状態を表す 15 個の変数(並びにその 15 通りを 15 個の bit に持つ 1 つの integer)を作り,その面に関してそういう状態が 確定している あり得ない 未確定だがあり得る の 3 通りの可能性を 1,-1,0 の値で(もしくは,あり得るかあり得ないか を 15 個の bit の on, off で)情報保持するのです.(最終解が4本の線分からできているような問題を作られたら,この解法はアウトです.そういう解を持つまともな問題は存在するでしょうか?) 同様に各頂点は,自分自身の上下左右の4つの辺について,線のあるなしの組み合わせ(7通り,ただし処理の合理化のため面と同様に 15 通りとして扱う)に関する可能性を保持しています. また各辺は,そこに線があるかないか確定できないかの可能性を保持しています.(これは edgeinfo[][].draw のこと) ここで,ある面の(周囲の辺に関する)可能性と,隣接する頂点の(接触する辺に関する)可能性の両方を比較すると,それぞれの可能性が制限される状況が起こり得ます. 例えばある面に書かれた数字が 0 であれば,その面の左上の頂点について,その頂点に接触する右や下の辺に線が存在する可能性はありません. trivial_check では,近接する面と辺,頂点と辺,面と頂点の可能性の摺り合わせによって不可能手を消していきます. trivial_check で行き詰まった場合,parity_check や loop_check という,別の観点からの不可能手消去手段を使います. それでもだめなら dual_check という,線の存在,非存在の両方の仮定から導かれる情報を利用します.これにより,ほぼすべての問題(私の知る既存の問題はすべて)が解決できます. このプログラムでは実際にそのような比較によって可能性を殺していくのですが,面の可能性が周囲の頂点の可能性を積極的に殺しに行くという方法は採用していません.ある面のある状態の可能性を殺すのはその面の責任とし,周囲の頂点はその面に対し「こういう状況はこの頂点の可能性からいってありえないよ」という情報を,参照されるために持っておくという形を取っています. 以下で可能性の消し方の詳細を説明します. (2) 面,頂点,辺が,自分自身の可能性について持っている情報について まず,面が自分自身の可能性について持っている情報について説明します. 左右上下を _L=1=0001(2), _R=2=0010(2), _U=4=0100(2), _D=8=1000(2) で定義します.その上で,面の周囲の4つの辺について,線のあるなしの組み合わせの 15 通りの状態を,status[0] から status[14] で表現しました.例えば次のようになります. status[0] は周囲のどの辺にも線がないという状態 status[1] は左の辺にだけ線があり,右,上,下の辺には線がないという状態 status[3] は左の辺と右の辺に線があり,上,下の辺には線がないという状態(3=0011(2)) 要するに status[i] は,i を4桁の2進数に展開し,各辺に対応する bit が 1 ならばその辺がある,0 ならばその辺はないという状態を表します.(5=0101(2) -> _L,_U がある.) 表にすると次の通りです. i : 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 i の2進数展開 : 1110 1101 1100 1011 1010 1001 1000 0111 0110 0101 0100 0011 0010 0001 0000 status[i] の表す状態: RUD LUD UD LRD RD LD D LRU RU LU U LR R L NON 各 status[i] は,その面にとって そういう状態が起こり得なければ -1, 起こるか起こらないかわからなければ 0, そういう状態に確定していれば 1 という値をとります.例えば次のような具合です. status[0]= 1 -> 周囲の4辺に線が全くない状態であると決定している. status[0]=-1 -> 周囲の4辺に線が全くない状態の可能性はない. status[0]= 0 -> 周囲の4辺に線が全くない状態の可能性はあるが,確定はしていない. status[9]= 1 -> 左と下の辺に線があり右と上の辺に線がない状態であると決定している. status[9]=-1 -> 左と下の辺に線があり右と上の辺に線がない状態の可能性はない. status[9]= 0 -> 左と下の辺に線があり右と上の辺に線がない状態の可能性はあるが,確定はしていない. 解きはじめの未確定要素の多い段階では,各面における status[0] 〜 status[14] の多くは 0 です.しかしこれらは,周囲の辺や頂点の状態と比較していくことにより -1 が増えていきます.( Trivial ボタンが押されて for loop で自分の順番が回ってきた際に実際に比較を実行します.) -1 が 14 個になり 0 が 1 個だけになった時点で,その 0 を 1 に変えて確定とします. 比較に先立って 初期データが読み込まれた際に,書かれた数字からわかる情報を持たせておきます.(init_check の仕事です.) 面に書かれた数字が 2 の場合,周囲の線は 2 本と決まっているわけですから,次のように決めてやるわけです. i : 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 i の2進数展開 : 1110 1101 1100 1011 1010 1001 1000 0111 0110 0101 0100 0011 0010 0001 0000 status[i] の表す状態: RUD LUD UD LRD RD LD D LRU RU LU U LR R L NON status[i] の値 : -1 -1 0 -1 0 0 -1 -1 0 0 -1 0 -1 -1 -1 頂点も同様にして,接触する左右上下の辺に線があるかないかの組み合わせの状態に関する変数 status[0] 〜 status[14] を持っています.ただし,頂点に集まる線の数は 0 か 2 と決まっているので,7 つ以外はすべて最初から -1 です. 辺もそこに線があるかないかの変数を持っていますが,これは edgeinfo[][].draw のことです.配列にする必要はありません. (3) 統合変数 this_possibility の構造 処理の簡便さのため,面及び頂点に status[0] から status[14] を統合した変数 this_possibility を導入します. status[0] =-1 なら this_possibility の第 0 bit=0, (あり得ない場合 0 にする) status[0] = 0 なら this_possibility の第 0 bit=1, (あり得る場合 1 にする) status[14]=-1 なら this_possibility の第 14 bit=0, status[14]= 0 なら this_possibility の第 14 bit=1, などとします.(右端の bit を第 0 bit と考えます.) 一般には status[i]=-1 なら this_possibility の第 i bit=0, status[i]= 0 なら this_possibility の第 i bit=1 です. (2) で例示した,面に書かれた数字が2である場合,this_possibility=001011001101000 となります.表で示すと this_possibility = 0 0 1 0 1 1 0 0 1 1 0 1 0 0 0 bit 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 RUD LUD UD LRD RD LD D LRU RU LU U LR R L NON もちろん status[0] 〜 status[14] と this_possibility は同じ情報を別の形で表現したものですから,同期には注意する必要があります.(プログラムを改造して status[] をいっさい使わなくする方がよいのかもしれません.) status[0] 〜 status[14] の情報を改めて this_possibility という別の形で表現した理由は,次の通りです.このような形で情報を持っておくと,周囲の辺や頂点からの情報が同様の構造で与えられた場合, & で bit 演算をしてやるだけで処理が完了します.もしも配列どうしの比較で処理しようとすると, for loop の記述と処理時間が数十倍程度に膨れ上がると思います. 各面は自分自身の可能性 this_possibility の他に (その面の状態から帰結できる)左上の頂点の可能性 lu_possibility などのデータを持っており,各頂点も自分自身の可能性 this_possibility の他に (その頂点の状態から帰結できる)右下の面の可能性 rd_possibility などのデータを持っています. ある面について this_possibility = (this_possibility &(左上の頂点の持つ rd_possibility) ); とすると,左上の頂点と両立するデータだけが残ります. trivial_check では,こういう具合に可能性の摺り合わせを行っていきます (4) 面,頂点,辺が,周囲の面,頂点,辺の可能性について持っている情報について 面の状態から結論できる左の辺の可能性は l_possibility で表現します.この可能性はその面が保持しており,左の辺から参照されます. l_possibility= 1; 左の辺に線は必ずある. l_possibility=-1; 左の辺に線は必ずない. l_possibility= 0; 左の辺に線があるかないかわからない. ある面の状態から結論できる左上の頂点の可能性は lu_possibility で表現します.この可能性はその面が保持しており,左上の頂点から参照されます.lu_possibility などの構造は頂点の this_possibility と同様で,15 個の bit が 15 通りの状態の可能性を表すようになっています. 同様にして各辺は自分自身の状態から判断できる周囲の面,頂点の可能性を保持し,各頂点は自分自身の状態から判断できる周囲の面,辺の可能性を保持しています. この可能性の作成方法は (5) で述べます. (5) class Data の意味,周囲の可能性の作成方法. 面,頂点に関する *_possibility をうまく扱うための参照用のデータとして data.L,data.l,dataLU,dataLu などを作りました.これらは class Data で計算されます.各 areainfo[][], edgeinfo[][], vertexinfo[][] は data = new Data() を参照用に持っています.(メモリーの浪費?) これらのデータは this_possibility と同様の構造を持っています.例えば data.L は,左を含む status[] に対応するビットを 1 にしたものです.具体的には次の通りです. data.L= 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 bit: 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 RUD LUD UD LRD RD LD D LRU RU LU U LR R L NON ある面の左側の辺に線があるとわかった場合,その面について this_possibility = (this_possibility & data.L); としてやれば,左に線のある状態が反映されます. また,ある面の左側の辺に線があるという状況があり得るかどうかは, if((this_possibility & data.L) !=0); // これは可能性がある場合 で調べることができます. 同様に data.l は *_possibility の各ビットのうち左を含まないものに対応するところを 1 にしたものです. (大文字は線がある,小文字は線がないという約束) 具体的には次の通りです. data.l= 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 bit: 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 RUD LUD UD LRD RD LD D LRU RU LU U LR R L NON ( data.l は l=0x07ff-L で作ります.) ある面の左側の辺に線がないとわかった場合,その面について this_possibility = (this_possibility & data.l); としてやれば,左に線のない状態が反映されます. また,ある面の左側の辺に線がないという状況があり得るかどうかは, if((this_possibility & data.l) !=0); // これは可能性がある場合 で調べることができます. 2つの辺を同時に考慮するための変数も作りました.例えば data.LU は *_possibility の各ビットのうち左と上の両方を含むものに対応するところを 1 にしたものです. 具体的には次の通りです. data.LU= 0 1 0 0 0 0 0 1 0 1 0 0 0 0 0 bit: 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 RUD LUD UD LRD RD LD D LRU RU LU U LR R L NON ( data.LU は LU=L&U で作ります.) また data.Lu は *_possibility の各ビットのうち左とは含むが上は含まないものに対応するところを 1 にしたものです. 具体的には次の通りです. data.Lu= 0 0 0 1 0 1 0 0 0 0 0 1 0 1 0 bit: 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 RUD LUD UD LRD RD LD D LRU RU LU U LR R L NON ( data.Lu は Lu=L&u で作ります.) これらの変数を使うことによって,*_possibility の操作がかなりすっきりします. ある面に関して this_possibility & data.LU == 0 だったとします.つまり,この面に関して status[5]=status[7]=status[13]=-1 であり,面の左と上の辺の両方が線有りという可能性がないとします. this_possibility = * 0 * * * * * 0 * 0 * * * * * data.LU = 0 1 0 0 0 0 0 1 0 1 0 0 0 0 0 bit: 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 RUD LUD UD LRD RD LD D LRU RU LU U LR R L NON この場合,この面の左上の頂点については,右と下の辺の両方が線有りという可能性のないことがわかります.その情報を lu_possibility = (lu_possibility & (0x07ff-data.RD)) として,左上の頂点から参照されるために保持しておきます. 頂点の情報から近接する面や辺の可能性の lu_possibility, l_possibility などを作る方法,辺の情報から近接する面や頂点の可能性の u_possibility, l_possibility などを作る方法,も同様です. (辺の持つ周囲の情報は4つあり,l_possibility, r_possibility と u_possibility, d_possibility の一方が面の情報で他方が頂点の情報です.これは考えている辺が水平か垂直かによります.) (6) parity_check について ある程度ナンバーラインを解き慣れている人なら,「この面はジョルダン閉曲線の内側で,この面はジョルダン閉曲線の外側だ」というような発想をしていると思います.盤面の上下から状態が決まってきて中間あたりでその状態が接触した場合,「この面はジョルダン閉曲線の内側で,この面はジョルダン閉曲線の外側だから間に線が必要だ」というような発想で線のあるなしを決めることができます. 各面に対し,最終的にできる閉曲線の内か外かに関する絶対的な情報がなくても,「この面と隣の面は内外が一致しているはずだから,間に線はない」と判断することもあるでしょう.これは盤面に任意に閉曲線を書いた場合,解答の閉曲線との交点が偶数個であるという事実と同値です.人間的にはこの方が考えやすいかもしれませんが,プログラムで閉曲線を考えるのは難しいので,領域の内外という捉え方で処理します. 全く線の引かれていない状態で,各面に対し region_id と region_parity の2つの情報を持たせます.私が新たに作った外枠の面は region_id = 0, region parity=1 です.これはジョルダン閉曲線の外側ということを意味します.(より正確にはジョルダン閉曲線の外側領域に関する内側) 残りの面は 1 〜 xn*yn のすべて異なる region_id を持っており,region_parity はすべて 1 です. これは例えば「 areainfo[1][1] は region_id 1 で表される領域の内側である」ということを意味します. 辺に実線または×が描かれると,その両側の面の region_id を結合し,大きい方の id を持つすべての面の id を小さい方の id に変更します.その際,間の辺に実線が引かれるなら大きい方 id を持っていたすべての面の region_parity を反転させ,×が描かれるなら region_parity を一致させます.その後欠番となった region_id をなくすように refion_id を詰め直し,max_region_id を 1 減らします.要するに,一番小さい id を持っていた面を代表としてその面と同じ側か反対側かを考えるわけです. 結局 parity_check を施すと 実線または×で連結している面はすべて同じ region_id を持つ. 実線をまたぐと region_parity は反転し,×をまたぐと region_parity は反転しない. そうすると,同じ region_id を持つ面が接触してその間の辺の状態が未決定の場合 region_parity が不一致( 1 と -1 )の場合,間に線がある. region_parity が一致の場合,間に×がある. と結論できます. 面 A と面 B が面 C を挟んで接触している場合,位相的には非連結である A, B の region_id が一致することがあります. |B| - - |A|C| or |A|C|B| のようになっていて,A,C および B,C 間の2つの辺の状態は未確定だとします.このとき A,C および B,C 間の2つの辺に引かれる線の数の和が偶数本 -> A,C は同一 region_id で 同一 region_parity A,C および B,C 間の2つの辺に引かれる線の数の和が奇数本 -> A,C は同一 region_id で 異なる region_parity としてよろしい. このようにして trivial_check では得られなかった情報が得られます. (7) loop_check について これは「小さいループができたら矛盾」というだけの判定です.判定法は次の通りです. どこかの辺に新たに線が引かれる場合,その線に新しい id を与えます. その線の両端の頂点にも同じ id を与えます. 接触している線,頂点が同一の id を持つように id に変更をかけます. 未確定辺の両端点の id が同じで max_loop_id >1 の場合,そこに線はないことがわかります. 5.その他 (1) area size を増やした理由は,見当がつくと思います. 面,辺,頂点は自分の周囲の面,辺,頂点の情報を読みに行きます.このとき配列外になってしまわないようにしたのです.新たに付け足した面,辺,頂点は参照されるだけで,周囲の情報を読みに行くことはありません. (2) 本当にチェックが必要な面,辺,頂点のみチェックするように,boolean determined と boolean no_need_check を作りました.状態変化した面,辺,頂点は周りの面,辺,頂点の no_need_check をリセットします.チェックが済めば no_need_check が true になります. (3) java の仕様が新しくなったので,コンパイルするたびに古い書き方をしていると警告が出ますが,無視しました. 6.発展性並びにやっていないこと (1) 自分が手作業で解く際には,線に向きを付けていました.最終的なジョルダン閉曲線が反時計回りの向きを持つとして線分に矢印を付けるのです.すると「上矢印と上矢印の間には必ず下矢印がある」というような事実が簡単に分かるのですが,これは parity_check と同値な判定基準と思われたので,採用しませんでした. (2) ×により領域が2分されてしまう場合,一方の領域には線が存在できません.これはある種の問題では結構有効な判断基準だと思われますが,今まで見てきた問題では Dual check によって小さい loop が簡単にできてしまって排除されるので,あえて取り上げませんでした.(このプログラムで解けない問題が出てくるとすると,まずはこのあたりからでしょうか.) (3) 私の気づいていない人間的な判定基準があるかと思います.その場合その check を付け足すことで,よりエレガントなチェックができます.藤原さんの未発表の解法には何かすごいテクニックがあるのでしょうか. 7.最後に 今回ほんの気まぐれから java programing に深入りしてしまいましたが,いかに java が簡単かということがわかりました.(難しいことをしていないだけなのですが.) 今後何かの問題をプログラムで解く際には,おそらく java で書くことになるだろうと思います.それもこれもすばらしいお手本があってのことです.藤原さん,ありがとうございました. 1999. Masato Kamei.