午後のひとときに、プログラムの問題を作ったので解いてみる。
ヤマザキ春のパンまつり
点数シールは、
2.5点、2点、1.5点、1点、0.5点
の5種類。
台紙に28点分を貼って、
白いお皿と交換出来る。
台紙をよく見ると、シールを30枚貼れるように目印のワクがある。
因みに上記写真では、1段目を飛ばして2段目から貼っており、右下のQRコードの上までの、5列6段となっています。
注意書きには、点数シールのワクに28点分貼れなくなりましたら、台紙をもう1枚追加してください。とあります。
これを踏まえて、以下の問いの答えを求めるプログラムを書け。
問題1
点数シールを30枚以下に限定せず、合計が丁度28点になるように、
点数シールを左上から点数の高い順に詰めて貼ったとすると、
貼り方は何通り?
問題2
点数シールを30枚以下に限定して、合計が丁度28点になるように、
点数シールを左上から点数の高い順に詰めて貼ったとすると、
貼り方は何通り?
問題3
30枚以下の合計が丁度28点分の点数シールがあるとき、
30箇所の目印のワクに対して無作為に点数シールを貼るとして、
最小の組み合わせは何通り?
問題4
30枚以下の合計が丁度28点分の点数シールがあるとき、
30箇所の目印のワクに対して無作為に点数シールを貼るとして、
最大の組み合わせは何通り?
問題5
30枚以下の合計が丁度28点分の点数シールがあるとき、
30箇所の目印のワクに対して無作為に点数シールを貼るとして、
組み合わせの合計は何通り?
シンキングタ~イム
問題1は、
2.5点、2点、1.5点、1点の点数シールの枚数で、4重ネストにして、28点から先の点数を引いて0.5点の枚数で、常に丁度28点になるように調整して、すべてのパターンを数える。
問題2は、
各点数の点数シールの枚数が30枚以下だけを数える。
問題3、4は、
各点数の点数シールの枚数が30枚以下の場合、30箇所の目印に対して、組み合わせを掛け合わせ、最小と最大を保持する。
問題5は、
先の組み合わせを掛け合わせた値を合計する。
と、単体のプログラムですべての問題の答えを導き出すものとする。
単純に考えて、問題1、2、3はオーバーフローしないと思うが、問題4、5はオーバーフローを考える必要があるだろう。
例えば、問題4の可能性として、nCrのrを、5種類の点数シールと空白の6つを均等に考え、r=30/6=5として、
30C5×25C5×20C5×15C5×10C5×5C5
= 142506×53130×15504×3003×252×1
= 88832646059788350720 > 18446744073709551615
と十分に64ビットを超えてくる可能性があることが解る。
但し、この計算では丁度28点にはならないので、あくまでも可能性の話しです。
個々の値での最大は、
30C15 = 155117520
であるので、組み合わせの関数を作ったとしても、64ビットで事足りることが解る。
但し、分母、分子をそれぞれ計算してから、最後に除算するようなことをすると、分子が、
30! = 265252859812191058636308480000000 > 18446744073709551615
と64ビットを優に超えるので、分母、分子、ともに階乗の掛け算をする度に約分していく必要がある。
こういった事柄を念頭において、C言語で書いてみる。
yamazaki.c
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
unsigned long long gcd(unsigned long long i, unsigned long long j)
{
unsigned long long k;
while ( j > 0 ) {
k = i%j;
i = j;
j = k;
}
return i;
}
unsigned long long combination(int n, int r)
{
unsigned long long u, l, g;
u = l = 1;
while ( r > 0 ) {
u *= n;
l *= r;
g = gcd(u, l);
u /= g;
l /= g;
n--;
r--;
}
return u/l;
}
int main()
{
int a, b, c, d, e, f, g, h;
unsigned long long i, j, k, l, m, n, o, p, q, r;
g = h = 0;
m = o = p = q = r = 0LL;
n = ULONG_LONG_MAX;
for (a=0; a*25<=280; a++)
for (b=0; a*25+b*20<=280; b++)
for (c=0; a*25+b*20+c*15<=280; c++)
for (d=0; a*25+b*20+c*15+d*10<=280; d++) {
e = (280-a*25-b*20-c*15-d*10)/5;
fprintf(stderr,"%d\t%d\t%d\t%d\t%d\t%d",a+b+c+d+e,a,b,c,d,e);
f = 30-a-b-c-d-e;
g++;
if ( f >= 0 ) {
h++ ;
i = combination(30,a);
j = i*combination(30-a,b);
k = j*combination(30-a-b,c);
l = k*combination(30-a-b-c,d);
m = l*combination(30-a-b-c-d,e);
if ( i > j || j > k || k > l || l > m ) {
printf("*** overflow ***\n");
} else {
fprintf(stderr,"\t%llu\n",m);
if ( n > m ) {
n = m;
}
if ( o < m ) {
o = m;
}
r += m;
q += r/100000000;
p += q/100000000;
r %= 100000000;
q %= 100000000;
}
} else {
fprintf(stderr,"\n");
}
}
printf("Ans1. %d\n",g);
printf("Ans2. %d\n",h);
printf("Ans3. %llu\n",n);
printf("Ans4. %llu\n",o);
printf("Ans5. %llu%08llu%08llu\n",p,q,r);
return EXIT_SUCCESS;
}
4重ネストでは、終了条件を、点数シールの点数を小数として扱わず、10倍して整数にして、合計280点を超えないようにしました。
バカ正直に、2.5とか1.5とか小数を使うと、丸め誤差などが出てしまったら答えに影響が出るので、いずれも整数となるように2倍なり、10倍なりして考えるのが妥当ですね。
まぁ、10倍の方が解りやすいですよね。
答えには直接関係しないが、データとして出力したいものがあったので、それはエラー出力に出力しています。
逆にオーバーフロー検知においては、標準出力への出力としました。
サブルーチンの、combinationは組み合わせ、gcdは最小公倍数を、それぞれ求めます。
手計算でやるならば、nCrの、rが小さい方からやり、nが小さくなってから大きいrを計算するほうが簡単で楽ですが、プログラミングではそこに差異はないので、大小比較などせずに順番にこなしています。
それぞれの組み合わせを掛け合わせるので、毎回オーバーフローしていないかの検査が必要でした。
正と正を掛けたのに元より小さくなったら、オーバーフローしているということです。
求まった組み合わせの合計は、64ビットを優に超えることは解っていたので、加算の多倍長演算をしています。
プログラムの解説はこんなところで、標準出力だけを表示するように実行してみます。
> yamazaki 2>nul
Ans1. 5608
Ans2. 4131
Ans3. 435
Ans4. 12589660722759120000
Ans5. 1208583142623882872205
問題1の答え 5608通り
問題2の答え 4131通り
問題3の答え 435通り
問題4の答え 12589660722759120000通り
問題5の答え 1208583142623882872205通り
という結果を出力しました。
問題4はunsigned long long型に入ってますね。
12589660722759120000
18446744073709551615
long long型には入らないので、long longで組むとオーバーフローします。
問題5は64ビットを超えているので、加算だけですが10進8桁ずつに区切って、多倍長演算をしました。
問題3の最小435通りは、
1点シールを28枚、空白が2箇所ということで、
30C28 = 435
ということは、容易に想像出来ますね。
この問いだけであれば、机上で求めることも可能だと思います。
問題4の最大12589660722759120000通りは、どういう枚数での組み合わせなのか、簡単には思い浮かばないので、エラー出力に出力したデータから抽出してみましょう。
yamazaki 2>&1 | find "12589660722759120000"
22 3 3 4 5 7 12589660722759120000
Ans4. 12589660722759120000
合計22枚で、2.5点が3枚、2点が3枚、1.5点が4枚、1点が5枚、0.5点が7枚という内訳で、
3×2.5+3×2+4×1.5+5×1+7×0.5 = 7.5+6+6+5+3.5 = 28
と合計28点となっていますね。
30C3×27C3×24C4×20C5×15C7
= 4060×2925×10626×15504×6435
= 12589660722759120000
これが64ビットを超えていたとすると、
*** overflow ***
の文字が出ていたでしょう。
もしそうなったら、乗算の多倍長演算も組み込む必要が出ていまいたね。
さて、もしも、これらの問題を机上の数学だけで解くとしたら、どうやって解くのでしょうか。
プログラミング出来ても、数学で解けるかというと、かなり面倒で時間が掛かるということは解ります。
もっと効率的に求める方法があるかもしれませんが、今どきのコンピュータの性能やコンパイラの性能を考えると、これくらいの計算ならば、それほど時間を掛けずにゴリゴリと力技で書いてしまっても、問題は無いかとは思います。
プログラミングの授業での提出物や、速さを競うようなプログラムならば、もっとちゃんと考えて、変数名もちゃんとして、書いてくださいね。
それにしても、このヤマザキ春のパンまつりを題材にしたプログラミングの問題。
プログラマが考えなければいけないことが、いろいろと詰まっていて、面白い問題になったと自画自賛しております。
ではでは
PS:
DOSのfindコマンド、いつからエラー出力を拾わなくなったのだろうか。
30年前くらいに、IBM DOSやMS-DOSの全コマンドのテストを仕事でやっていたので、findコマンドもテストしたはずなのだが、こんな仕様だったのかを、今更ながら思い返している。
まぁ、拾わないならば、エラー出力を標準出力にリダイレクトすれば問題は解決するのだから、特に問題はない。
PS:
2021年度のヤマザキ春のパンまつりは、写真の通りお皿2枚分となりました。
家族3人ですので、本来ならば3枚欲しいところでしたが、まぁ仕方のないことでしょう。
因みに、2.5点はすべて6枚入りのダブルソフトです。
19枚あるので19袋、パンの枚数は19×6=114枚分ですね。
結構食べているなぁ。
写真左の台紙は、2.5点を11枚、0.5点を1枚とかなり几帳面に計算して貼っていますね。
右の台紙も、最初は几帳面に貼っていたのですが、終了間近になって使っていなかったシールで埋めたので、点数を確認する人には面倒を掛けたかと思います。