小町算の解法

小町算とは 123456789 の数字の間に幾つか四則演算子を挿入して計算しその結果を 100 に するというものです. 例えば 123-45-67+89=1001*2+34-56/7+8*9=100 のようなものです.
考え方は如何にして全ての計算パターンを尽くすかと言うことにあります. ここでは 8個ある全ての文字の間に演算子を入れることにするが、四則演算子に加えて数字を連なったものと 見なす演算子として # を用いることにする. これは四則演算子記号以外なら何でもよい. つまり 1#2#3 は数 123 を表すことにするのである. このように考えれば全ての計算パターンは8重の繰り返しループで表すことができる. ここではC++言語を使用するが、ネストしたループの数には制限があるので心配でしたが Borland の BCC コンパイラ、MinGW の g++ コンパイラどちらでも 8重ループは使用できました. 今の場合 5^8=390625 通りありますがPCにとってはたいした問題にはなりません(8重のループなど はじめて使用しました). また使用した環境では整数の範囲が10桁(-2,147,483,648〜2,147,483,647)ですから9桁の数の 計算途中で桁あふれの心配はありません.
例えば 1+2×34-56+78+91+2*3#4-5#6+7#8+9 で表されます. 計算はまず # 文字が 現れた順に両側の文字を結合し、次に演算の優先順位から現れた順に *, / の計算を行い 最後に +,− を現れた順に計算すればよいことになります.

次の例では上から下に順次処理を行っています.

1+2*3 #4-5 #6+7 #8+9
1+2*34-56+78+9
1+68-56+78+9
69-56+78+9
13+78+9
91+9
100
この場合には目的の数である 100 になりました.

コンピューターなら計算結果を 1/17 にすることも問題なく求めることができます. この場合 3 パターンで可能でした.

1 2/34 5 6 7 8 9
1×2/34 5 67 8×9
1×2/34 5 67 8×9

double でも計算できますがこの場合目的の数 1/17 になったかどうかの判断は結構めんどくさいです. もし等号で判断すると1つも検出されない場合もある. 0.00001以下を等しいと見なすと3個であるが、0.0001とすると 5個検出され間違いがある. 最後の二つは正しくない.

   1 +  2 / 34 − 5 −   6 − 7  + 8 + 9
   1 ×  2 / 34 + 5 +  67 − 8  × 9
   1 ×  2 / 34 − 5 −  67 + 8  × 9
   1 /  2 ×  3 / 4 / 567 × 89      = 1/17 + 1/25704
   1 / 23 +  4 / 5 /   6 / 78 × 9 = 1/17 + 1/10465

この問題、多分江戸時代に名付けられたのでしょうが, 教え子から質問されて考えてみました. 解法はすぐに思いつきましたが、プログラムを整理整頓するのに時間が掛かりました. 特に両終端キューの使用方法は少し調べる必要がありました. 使い慣れたスタックでまず制作しましたが、その場合 スタックを逆順に並べ替える処理が必要となりコーディングが汚くなるので両終端キューで制作し直しました.
C++ で私が制作したソースは以下のようなものです. 101 のパターンで100にすることができました.
12-94行目までは有理数クラスの宣言部分でこの問題では実数を用いても結果は同じになるようです.

ここでは演算子を渡して計算していますが、計算式そのものを渡す関数で記述した方がきれいかも知れません. いずれ書き直そうかとも.

Microsoft Visual C では 574 重にネストしたループが可能のようです. BCCでも 8重ループなど問題なくコンパイルできるのは当然かも知れません. 杞憂でした.


  1. // 小町算
  2. // 2020/06/18-2020/06/20
  3. #include <iostream>
  4. #include <sstream> // string stream を用いるので
  5. #include <fstream> // ファイルを用いるので
  6. #include <queue> // que 使用
  7. #ifdef __GNUC__
  8. #include <climits> //mingw で必要 INT_MAX
  9. #endif
  10. using namespace std;
  11. // 有理数(分数)クラスの定義
  12. class RATIONAL{
  13. int xi, yi; // private
  14. public:
  15. RATIONAL(int a=0,int b=1){xi=a;yi=b;}; // コンストラクタ 分母yiの既定値は1
  16. int numerator(){ return xi; }; // 分子
  17. int denominator(){ return yi; }; // 分母
  18. RATIONAL operator +(RATIONAL a);
  19. RATIONAL operator -(RATIONAL a);
  20. RATIONAL operator *(RATIONAL a);
  21. RATIONAL operator /(RATIONAL a);
  22. int operator ==(RATIONAL a);
  23. int operator !=(RATIONAL a);
  24. int operator >(RATIONAL a);
  25. int operator >=(RATIONAL a);
  26. int operator <(RATIONAL a);
  27. int operator <=(RATIONAL a);
  28. friend ostream& operator << ( ostream &os, RATIONAL &a );
  29. friend istream& operator >> ( istream &os, RATIONAL &a );
  30. };
  31. const RATIONAL RATIONAL_ZERO(0,1);
  32. const RATIONAL RATIONAL_INF=INT_MAX;
  33. // 有理数演算の実装
  34. RATIONAL RATIONAL :: operator +(RATIONAL a){
  35. RATIONAL b(xi * a.yi + yi * a.xi, yi * a.yi );
  36. return b;
  37. }
  38. RATIONAL RATIONAL :: operator -(RATIONAL a){
  39. RATIONAL b(xi * a.yi - yi * a.xi, yi * a.yi );
  40. return b;
  41. }
  42. RATIONAL RATIONAL :: operator *(RATIONAL a){
  43. RATIONAL b(xi * a.xi, yi * a.yi );
  44. return b;
  45. }
  46. RATIONAL RATIONAL :: operator /(RATIONAL a){
  47. RATIONAL b(xi * a.yi, yi * a.xi );
  48. return b;
  49. }
  50. int RATIONAL :: operator ==(RATIONAL a){
  51. if( (xi*a.yi) == (yi*a.xi) ) return 1;
  52. return 0;
  53. }
  54. int RATIONAL :: operator !=(RATIONAL a){
  55. return !((xi*a.yi) == (yi*a.xi));
  56. }
  57. int RATIONAL :: operator >(RATIONAL a){
  58. return ( (xi*a.yi-yi*a.xi) * (yi*a.yi) > 0 );
  59. }
  60. int RATIONAL :: operator <(RATIONAL a){
  61. return ( (xi*a.yi-yi*a.xi) * (yi*a.yi) < 0 );// 分母分子が同符号
  62. }
  63. int RATIONAL :: operator >=(RATIONAL a){
  64. return !(RATIONAL(xi,yi) < a);
  65. }
  66. int RATIONAL :: operator <=(RATIONAL a){
  67. return !(RATIONAL(xi,yi) > a);
  68. }
  69. // 入出力
  70. ostream& operator << (ostream &os, RATIONAL &a){
  71. if( a.yi == 1 ) os << a.xi;
  72. else os << a.xi << "/" << a.yi;
  73. return os;
  74. }
  75. istream& operator >> (istream &is, RATIONAL &a){
  76. is >> a.xi >> a.yi;
  77. return is;
  78. }
  79. // ユークリッドの互除法による有理数の既約化
  80. RATIONAL Euclidean_Algorithm( RATIONAL ra ){
  81. int a = ra.numerator(), b = ra.denominator();
  82. int a0 = a, b0 = b;
  83. int mod = a % b;
  84. while( mod ){
  85. a = b;
  86. b = mod;
  87. mod = a%b;
  88. }
  89. RATIONAL rra( a0/b, b0/b );// lcd 最大公約数 b で約分
  90. return rra;
  91. }
  92. //##### 有理数クラス宣言はここまで #####
  93. RATIONAL g_TARGETr = RATIONAL(100,1);// (1,17); 演算結果として目指す数
  94. RATIONAL g_nr[9] = {1,2,3,4,5,6,7,8,9};// default 123456789
  95. ofstream ofs("小町算解答.txt",ios::app);// ファイル追加モードで開く
  96. void GetNumbers();
  97. // 式の計算を行い100になるかどうかをチェック
  98. int Check100( char op[8] );
  99. // -----------------------------------
  100. // ##### main #####
  101. // -----------------------------------
  102. int main( void )
  103. {
  104. // TARGETr と 9桁の数字を入力させる
  105. GetNumbers();
  106. // 計算式が格納される 例えば 1+2#3+4+5+6+7#8*9
  107. char op[5] = {'+','-','*','/','#'};// # は空白無し つまり 1#2 は 12 を表す
  108. char Operator[8];
  109. int i=0; // 全処理候補数=390625=5^8
  110. int noi=0; // 答えの数
  111. // 可能な計算式を全て求める
  112. for(int i0=0;i0<5;i0++){
  113. Operator[0]=op[i0]; // 1 と 2 の間に入る記号 +-*/#
  114. for(int i1=0;i1<5;i1++){
  115. Operator[1]=op[i1]; // 2 3 間 +-*/#
  116. for(int i2=0;i2<5;i2++){
  117. Operator[2]=op[i2]; // 3 4 間 +-*/#
  118. for(int i3=0;i3<5;i3++){
  119. Operator[3]=op[i3]; // 4 5 間 +-*/#
  120. for(int i4=0;i4<5;i4++){
  121. Operator[4]=op[i4]; // 5 6 間 +-*/#
  122. for(int i5=0;i5<5;i5++){
  123. Operator[5]=op[i5]; // 6 7 間 +-*/#
  124. for(int i6=0;i6<5;i6++){
  125. Operator[6]=op[i6]; // 7 8 間 +-*/#
  126. for(int i7=0;i7<5;i7++){
  127. Operator[7]=op[i7]; // 8 9 間 +-*/#
  128. i ++;
  129. if( Check100( Operator ) ){// 検算 100?
  130. noi++;// 候補数を増やす
  131. }
  132. }
  133. }
  134. }
  135. }
  136. }
  137. }
  138. }
  139. }
  140. ofs << "\t\t" << g_TARGETr << endl;
  141. ofs << "\t\t(" << noi << ")/(" << i << ")" << endl;
  142. ofs.close(); // ファイルを閉じる
  143. cout << g_TARGETr << endl;
  144. cout << "(" << noi << ")/(" << i << ")" << endl;
  145. return noi;
  146. }
  147. // ---------------------------------------------
  148. // Check100
  149. // ---------------------------------------------
  150. int Check100( char op[8] ){
  151. ostringstream osstrm;
  152. deque<RATIONAL> Rdq0; // 両終端キュー double-ended queue
  153. for(int i=0;i<9;i++){
  154. Rdq0.push_back( g_nr[i] ); // 順にqueに格納
  155. }
  156. deque<RATIONAL> Rdq; // 両終端キュー double-ended queue
  157. deque<char> Opdq; // 両終端キュー double-ended queue
  158. RATIONAL Rtmp0, Rtmp1, Rtmp2;
  159. // 前処理 # 演算子の場合
  160. for(int i=0; i<8; i++){
  161. if(op[i]=='#'){ // 前後の数字を纏める preprocess
  162. Rtmp1 = Rdq0.front(); Rdq0.pop_front();
  163. Rtmp2 = Rdq0.front(); Rdq0.pop_front();
  164. Rtmp0 = Rtmp1*10 + Rtmp2; // 例えば 1#2#3 -> 12#3 -> 123 とする
  165. Rdq0.push_front( Rtmp0 ); // 元の場所に戻す
  166. }else{ // 加減乗除はそのままpush
  167. Rtmp1 = Rdq0.front();
  168. Rdq.push_back( Rtmp1 ); // 登録
  169. Opdq.push_back( op[i] ); // 登録
  170. Rdq0.pop_front(); // 取り去る
  171. }
  172. }
  173. Rdq.push_back(Rdq0.front()); // 登録
  174. Rdq0.pop_front(); // 取り去る
  175. // ここまででque セット
  176. deque<RATIONAL> Rdq1; // 両終端キュー double-ended queue
  177. deque<char> Opdq1; // 両終端キュー double-ended queue
  178. RATIONAL R1,R2,R3;
  179. while( !Opdq.empty() ){ // */の処理
  180. switch(Opdq.front()){
  181. case '*' :
  182. R1 = Rdq.front(); Rdq.pop_front();
  183. R2 = Rdq.front(); Rdq.pop_front();
  184. R3 = R1*R2;
  185. Rdq.push_front(R3); //登録
  186. break;
  187. case '/' :
  188. R1 = Rdq.front(); Rdq.pop_front();
  189. R2 = Rdq.front(); Rdq.pop_front();
  190. R3 = R1/R2;
  191. Rdq.push_front( R3 ); //登録
  192. break;
  193. default :
  194. Rdq1.push_back(Rdq.front());Rdq.pop_front();
  195. Opdq1.push_back(Opdq.front()); //登録
  196. break;
  197. }
  198. Opdq.pop_front();
  199. }
  200. Rdq1.push_back( Rdq.front() ); Rdq.pop_front();
  201. while( !Opdq1.empty() ){// +−の処理
  202. switch( Opdq1.front() ){
  203. case '+' :
  204. R1 = Rdq1.front(); Rdq1.pop_front();
  205. R2 = Rdq1.front(); Rdq1.pop_front();
  206. R3 = R1+R2;
  207. Rdq1.push_front( R3 ); // 登録
  208. break;
  209. case '-' :
  210. R1 = Rdq1.front(); Rdq1.pop_front();
  211. R2 = Rdq1.front(); Rdq1.pop_front();
  212. R3 = R1-R2;
  213. Rdq1.push_front( R3 ); // 登録
  214. break;
  215. default : break;
  216. }
  217. Opdq1.pop_front();
  218. }
  219. R1 = Euclidean_Algorithm(Rdq1.front());Rdq1.pop_front();// 既約分数へ
  220. if( R1 == g_TARGETr ){
  221. for( int j=0;j<8;j++){
  222. osstrm << g_nr[j];
  223. if( op[j] != '#' )osstrm<<op[j];// #文字は数字を連結
  224. }osstrm<<g_nr[8]<<endl;
  225. ofs << osstrm.str(); // ファイルに書き出す
  226. osstrm.str(""); // buffa clear
  227. return 1;
  228. }
  229. return 0;
  230. }
  231. //---------------
  232. void GetNumbers()
  233. {
  234. char na[16];
  235. cout << "標的の数(有理数)は(100)? 100 1 ";
  236. cin >> g_TARGETr; cin.ignore();
  237. cout << "9桁の数は? 123456789(Enter) ";
  238. cin.getline(na,sizeof(na));
  239. if( na[0] ){// 数字の入力があった場合
  240. for(int i=0;i<9;i++){
  241. g_nr[i]=int(na[i])-48; //int('0')=48 etc
  242. }
  243. }
  244. }
inserted by FC2 system