午後のひとときに、虫食い算、プログラミングの問題を出題してみる。
2□3□5□…□P
2から素数Pまで続く素数列の間に、
「=」を1つと、残りを「+」「-」「×」「÷」「」のいずれかで埋め、
等式が成り立つものを見つけるものとする。
例
P=5のとき、
2+3=5
1組
例
P=7のとき、
0組
例
P=11のとき、
2=3-5-7+11
23=5+7+11
23-5=7+11
23-5-7=11
2-3+5+7=11
5組
問題1
P=13の解と解の個数を求めよ。
問題2
P=17の解と解の個数を求めよ。
問題2
P=19の解と解の個数を求めよ。
問題4
2から始まる素数の個数(3以上)を与えると、
解を導き出すプログラムを組め。
シンキングタ~イム
解答1
2+3*5+7=11+13
2+3*5+7-11=13
2組
解答2
2+3=5+7-11-13+17
2+3=5-7+11+13-17
2+35/7=11+13-17
2+35/7-11=13-17
2+3-5=7-11-13+17
2+3-5+7=11+13-17
2+3-5+7-11=13-17
2+3-5-7+11+13=17
2*3+5*7=11+13+17
2*3+5*7-11=13+17
2*3+5*7-11-13=17
2*3*5*7+11=13*17
12組
解答3
2=35-7-11-13+17-19
2=3-5*7+11-13+17+19
23=57-11+13-17-19
23=5*7+11+13-17-19
23+5=7*11-13-17-19
23-57+11=13-17-19
23-5*7=11+13-17-19
23-5*7-11=13-17-19
2+3*5*7=11*13-17-19
2-35+7+11+13=17-19
2-3+5*7=11-13+17+19
2-3+5*7-11+13=17+19
2-3+5*7-11+13-17=19
2*3+5=7-11+13-17+19
2*3+5-7+11=13-17+19
2*3+5-7+11-13+17=19
2/3*57+11=13+17+19
2/3*57+11-13=17+19
2/3*57+11-13-17=19
19組
机上で求めるのは結構大変だったのではなかろうか。
解答4
primepuzzle.c
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int NUM;
char str[1024],bak[1024],tmp[1024],clc[1024];
char o[6][5] = {"=","","+","-","*","/"};
int p[25] = {2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97};
int a[24];
int count=0;
int md,pm;
long long gcd(long long a, long long b)
{
long long c;
if ( a < 0LL ) a*=-1;
if ( b < 0LL ) b*=-1;
while (b > 0) {
c = a%b;
a = b;
b = c;
}
return a;
}
void reduction(long long *a, long long *b)
{
long long g ;
g = gcd(*a,*b);
*a /= g;
*b /= g;
}
int eval()
{
int i,j,k;
long long p,n,s,d,g;
long long pu,pl,nu,nl,au,al;
if ( md > 0 ) {
for (i=0,s=0; str[i]!='\0'; i++) {
if ( str[i] == '(' ) s++;
if ( str[i] == ')' ) s--;
if ( s == 0 && ( str[i] == '*' || str[i] == '/' ) ) break;
}
md--;
} else if ( pm > 0 ) {
for (i=0,s=0; str[i]!='\0'; i++) {
if ( str[i] == '(' ) s++;
if ( str[i] == ')' ) s--;
if ( s == 0 && ( str[i] == '+' || str[i] == '-' ) ) break;
}
pm--;
} else if ( str[0] != '(' ){
sprintf(tmp,"(%s/1)",str);
sprintf(str,"%s",tmp);
return 0;
} else {
return 0;
}
p = 1;
if ( str[i-p] == ')' ) {
pu = pl = 0;
p++;
d = 1;
do {
pl += d*(str[i-p]-'0');
p++;
d*=10;
} while ( str[i-p] != '/' );
p++;
d = 1;
do {
pu += d*(str[i-p]-'0');
p++;
d*=10;
} while ( str[i-p] != '(' && str[i-p] != '-' );
if ( str[i-p] == '-' ) {
p++;
pu*=-1;
}
} else {
pu = 0;
pl = 1;
d = 1;
do {
pu += d*(str[i-p]-'0');
p++;
d*=10;
} while ( str[i-p] != '+' && str[i-p] != '-' && i-p >= 0 );
if ( str[i-p] == '+' || str[i-p] == '-' || i-p < 0 ) {
p--;
}
}
n = 1;
if ( str[i+n] == '(' ) {
nu = nl = 0;
n++;
if ( str[i+n] == '-' ) {
n++;
do {
nu *= 10;
nu += str[i+n]-'0';
n++;
} while ( str[i+n] != '/' );
n++;
do {
nl*=10;
nl += str[i+n]-'0';
n++;
} while ( str[i+n] != ')' );
nu *= -1;
n++;
} else {
do {
nu *= 10;
nu += str[i+n]-'0';
n++;
} while ( str[i+n] != '/' );
n++;
do {
nl *= 10;
nl += str[i+n]-'0';
n++;
} while ( str[i+n] != ')' );
n++;
}
} else {
nu = 0;
nl = 1;
if ( str[i+n] == '-' ) {
n++;
do {
nu *= 10;
nu += str[i+n]-'0';
n++;
} while ( str[i+n] != '*' && str[i+n] != '/' && str[i+n] != '+' && str[i+n] != '-' && str[i+n] != '\0' );
nu *= -1;
} else {
do {
nu *= 10;
nu += str[i+n]-'0';
n++;
} while ( str[i+n] != '*' && str[i+n] != '/' && str[i+n] != '+' && str[i+n] != '-' && str[i+n] != '\0' );
}
}
switch ( str[i] ) {
case '*':
reduction(&pu,&nl);
reduction(&pl,&nu);
au = pu*nu;
al = pl*nl;
break;
case '/':
reduction(&pu,&nu);
reduction(&pl,&nl);
au = pu*nl;
al = pl*nu;
break;
case '+':
g = gcd(pl,nl);
al = pl/g*nl;
au = nl/g*pu + pl/g*nu;
reduction(&au,&al);
break;
case '-':
g = gcd(pl,nl);
al = pl/g*nl;
au = nl/g*pu - pl/g*nu;
reduction(&au,&al);
break;
}
if ( al < 0 ) {
au *= -1;
al *= -1;
}
for (j=0; j<i-p; j++) {
tmp[j] = str[j];
}
k = sprintf(clc,"(%lld/%lld)",au,al);
for (j=0; j<k; j++) {
tmp[i-p+j] = clc[j];
}
for (j=0; str[i+n+j] != '\0'; j++) {
tmp[i-p+k+j] = str[i+n+j];
}
tmp[i-p+k+j] = '\0';
sprintf(str,"%s",tmp);
eval();
return 0;
}
int nest(int n, int e)
{
int i, j, k, l;
if ( n < NUM-1 ) {
if ( e == 0 ) {
a[n] = 0;
nest(n+1,1);
for (a[n]=1; a[n]<6; a[n]++) {
nest(n+1,0);
}
} else {
for (a[n]=1; a[n]<6; a[n]++) {
nest(n+1,1);
}
}
} else if ( n == NUM-1 ) {
if ( e == 0 ) {
a[n] = 0;
nest(n+1,1);
} else {
for (a[n]=1; a[n]<6; a[n]++) {
nest(n+1,1);
}
}
} else {
i = 0;
k = sprintf(str,"%d",p[i]);
while ( a[i] != 0 ) {
l = sprintf(tmp,"%s%d",o[a[i]],p[i+1]);
for (j=k; j<=k+l; j++) {
str[j] = tmp[j-k];
}
k+=l;
i++;
}
str[k] = '\0';
md = pm = 0;
for (j=0; str[j] != '\0'; j++) {
if ( str[j] == '*' || str[j] == '/' ) md++;
if ( str[j] == '+' || str[j] == '-' ) pm++;
}
eval();
sprintf(bak,"%s",str);
i++;
k = sprintf(str,"%d",p[i]);
while ( i < NUM ) {
l = sprintf(tmp,"%s%d",o[a[i]],p[i+1]);
for (j=k; j<=k+l; j++) {
str[j] = tmp[j-k];
}
k+=l;
i++;
}
str[k] = '\0';
md = pm = 0;
for (j=0; str[j] != '\0'; j++) {
if ( str[j] == '*' || str[j] == '/' ) md++;
if ( str[j] == '+' || str[j] == '-' ) pm++;
}
eval();
i = 0;
while ( str[i] == bak[i] ) {
i++;
if ( str[i] == '\0' && bak[i] == '\0' ) break;
}
if ( str[i] == '\0' && bak[i] == '\0' ) {
for (i=0; i<NUM; i++) printf("%d%s",p[i],o[a[i]]);
printf("%d\t%s\t%s\n",p[NUM],bak,str);
count++;
}
}
return 0;
}
int main(int argc, char *argv[])
{
NUM = atoi(argv[1])-1;
nest(0,0);
printf("%d組\n",count);
return EXIT_SUCCESS;
}
いつもの通りC言語です。
C言語にはeval関数が存在しないので、今回の問題を耐えうる程度のeval風関数をゴリゴリと自作しました。
例えば、LISP、Perl、PHP、Javascript、Python、Rubyなどでは、eval関数やそれに準ずる関数は標準であります。
eval関数とは、簡単に言えば、文字列として与えた数式を解読して、答えを出してくれる評価関数です。
解答1、2、3は、このプログラムによる出力から切り出したものです。
> primepuzzle 9
2=35/7-11-13+17-19+23 (2/1) (2/1)
2=3+5-7-11-13-17+19+23 (2/1) (2/1)
2=3-5+7-11-13+17-19+23 (2/1) (2/1)
2=3-5-7+11+13-17-19+23 (2/1) (2/1)
2=3-5-7+11-13+17+19-23 (2/1) (2/1)
23=5+7+11+13-17-19+23 (23/1) (23/1)
23=5+7+11-13+17+19-23 (23/1) (23/1)
235=7+11+13*17+19-23 (235/1) (235/1)
235-7=11+13*17+19-23 (228/1) (228/1)
235-7-11=13*17+19-23 (217/1) (217/1)
235-7-11-13*17=19-23 (-4/1) (-4/1)
23+5+7+11+13=17+19+23 (59/1) (59/1)
23+5+7+11+13-17=19+23 (42/1) (42/1)
23+5+7+11+13-17-19=23 (23/1) (23/1)
23-5=7+11+13-17-19+23 (18/1) (18/1)
23-5=7+11-13+17+19-23 (18/1) (18/1)
23-5-7=11+13-17-19+23 (11/1) (11/1)
23-5-7=11-13+17+19-23 (11/1) (11/1)
23-5-7-11=13-17-19+23 (0/1) (0/1)
23-5-7-11+13=17+19-23 (13/1) (13/1)
23-5-7-11+13-17=19-23 (-4/1) (-4/1)
23-5-7-11-13+17+19=23 (23/1) (23/1)
23*5=7*11+13-17+19+23 (115/1) (115/1)
23*5+7=11*13-17+19-23 (122/1) (122/1)
23*5+7-11*13+17=19-23 (-4/1) (-4/1)
23*5-7*11=13-17+19+23 (38/1) (38/1)
23*5-7*11-13+17=19+23 (42/1) (42/1)
23*5-7*11-13+17-19=23 (23/1) (23/1)
2+3+5=7+11+13-17+19-23 (10/1) (10/1)
2+3+5-7=11+13-17+19-23 (3/1) (3/1)
2+3+5-7-11=13-17+19-23 (-8/1) (-8/1)
2+3+5-7-11-13+17=19-23 (-4/1) (-4/1)
2-3=5-7-11-13-17+19+23 (-1/1) (-1/1)
2-35/7+11+13=17-19+23 (21/1) (21/1)
2-35/7+11+13-17+19=23 (23/1) (23/1)
2-3+5=7-11-13+17-19+23 (4/1) (4/1)
2-3+5+7=11+13-17-19+23 (11/1) (11/1)
2-3+5+7=11-13+17+19-23 (11/1) (11/1)
2-3+5+7-11=13-17-19+23 (0/1) (0/1)
2-3+5+7-11+13=17+19-23 (13/1) (13/1)
2-3+5+7-11+13-17=19-23 (-4/1) (-4/1)
2-3+5+7-11-13+17+19=23 (23/1) (23/1)
2-3+5-7+11+13=17-19+23 (21/1) (21/1)
2-3+5-7+11+13-17+19=23 (23/1) (23/1)
2-3-5+7+11+13+17=19+23 (42/1) (42/1)
2-3-5+7+11+13+17-19=23 (23/1) (23/1)
2*3*5=7+11-13-17+19+23 (30/1) (30/1)
2*3*5=7-11+13+17-19+23 (30/1) (30/1)
2*3*5+7=11+13+17+19-23 (37/1) (37/1)
2*3*5+7-11=13+17+19-23 (26/1) (26/1)
2*3*5+7-11-13=17+19-23 (13/1) (13/1)
2*3*5+7-11-13-17=19-23 (-4/1) (-4/1)
2*3*5-7=11-13-17+19+23 (23/1) (23/1)
2*3*5-7+11=13+17-19+23 (34/1) (34/1)
2*3*5-7+11-13=17-19+23 (21/1) (21/1)
2*3*5-7+11-13-17+19=23 (23/1) (23/1)
2*3*5-7-11+13+17=19+23 (42/1) (42/1)
2*3*5-7-11+13+17-19=23 (23/1) (23/1)
2*3/57=11+13-17/19-23 (2/19) (2/19)
2*3/57-11=13-17/19-23 (-207/19) (-207/19)
60組
プログラム内部では有理数として四則演算を行っており、2列目が左辺、3列目が右辺の結果となっております。
素数を9個、つまり、2、3、5、7、11、13、17、19、23までで間に演算子を入れるパターンから、有理数となる等号が現れます。
2*3/57=11+13-17/19-23 (2/19) (2/19)
2*3/57-11=13-17/19-23 (-207/19) (-207/19)
これは、机上で見つけるのは難しいかと思います。
「÷」を使うのはハードルが高いと感じてしまいますよね。
また、「」を使って数字を連なる場合、どれくらいの大きな値が出てきたかというと、
2*3=57*1113-171923+2931*37+41 (6/1) (6/1)
2*3-57*1113+171923=2931*37+41 (108488/1) (108488/1)
2*3-57*1113+171923-2931*37=41 (41/1) (41/1)
23*5*7+11=131719-2329-3137*41+43 (816/1) (816/1)
23*5*7+11-131719+2329+3137*41=43 (43/1) (43/1)
の131719や171923などがあります。
さて、素数の個数による組数の変化を見てみましょう。
個数 | 素数 | 組数 | 「+」 | 「-」 | 「×」 | 「÷」 | 「」 |
3 | 2,3,5 | 1 | 1 | 0 | 0 | 0 | 0 |
4 | 7 | 0 | 0 | 0 | 0 | 0 | 0 |
5 | 11 | 5 | 6 | 6 | 0 | 0 | 3 |
6 | 13 | 2 | 5 | 1 | 2 | 0 | 0 |
7 | 17 | 12 | 26 | 20 | 10 | 2 | 2 |
8 | 19 | 19 | 37 | 44 | 17 | 3 | 13 |
9 | 23 | 60 | 167 | 172 | 42 | 7 | 32 |
10 | 29 | 113 | 353 | 380 | 110 | 3 | 58 |
11 | 31 | 313 | 1084 | 1072 | 345 | 14 | 302 |
12 | 39 | 706 | 2575 | 2699 | 969 | 62 | 755 |
13 | 41 | 1781 | 7075 | 7046 | 2998 | 154 | 2318 |
14 | 43 | 6214 | 26102 | 25814 | 11638 | 991 | 10023 |
あくまでもプログラムが出力した個数を拾っただけです。
オーバーフローによって本来は等しいのに出力されないものも、個数が多くなるにつれて出てくることも想定されますので、鵜呑みにしないように。
単純計算、演算子は6種類で、個数が1増えるのに対して、計算量は6倍に増える、つまり計算時間も6倍に増えるということと考えて差し支えないだろう。
連結と四則演算以外にも冪乗を入れることも考えたが、容易に大きくなってオーバーフローして、拾い上げるのが大変だと思ったので、入れませんでした。
もし冪乗を採用したとしても、64ビットの符号付き整数で表現出来るのは、
2^3= | 8 |
3^5= | 243 |
5^7= | 78125 |
23^5= | 6436343 |
7^11= | 1977326743 |
2^35= | 34359738368 |
35^7= | 64339296875 |
11^13= | 34522712143931 |
235^7= | 39579931286171875 |
13^17= | 8650415919381337933 |
57^11= | 20635899893042801193 |
17^19= | 239072435685151324847153 |
3^57= | 1570042899082081611640534563 |
の10通りしかない。
これ以上は、基数側も指数側も増やすことは出来ない。
例えば、
2^3+5-7=11-13-17+19-23+29 (6) (6)
といったように解は見つかるだろうが、
冪乗で使った素数よりも大きな素数を使って、
冪乗の大きな値との差を埋める必要があるため、
素数の個数は多く必要となるだろう。
さて、今回のプログラミングの肝であるeval関数だが、
あくまでも今回の問題に特化したところもなくはない。
例えば、符号演算子、例えば負数を使いたいとなると、
(-5/1)
のように分数の形で括弧で括って与えれば、正しく認識出来るようにはなっている。
あくまでも内部的に有理数同士の四則演算という形を取っているので、無理数や複素数には対応していない。
作った後に気がついたが、有理数の括弧を別の括弧記号にすればよかった。
例えば、{}とかにすれば、()は括弧演算子として組み込むことも可能ですね。
また、オーバーフローを検知していないので、ちゃんとしたものを作るにはその辺りも組み込む必要は出てくるであろう。
eval関数や同様の評価関数を持つプログラミング言語は、インタプリタ言語には多く、コンパイラ言語には少ないように思う。
また、簡単に実行できてしまうことから、悪意のあるプログラムを実行しかねないというセキュリティの問題もあるため、安易に知らないコードをeval関数に放り込むようなことは辞めたほうが良い。
ではでは
knifeのmy Pick