昨日の記事の続きです。
n桁の自然数が、各桁のn乗の和と等しいような自然数を求めよ。
という問題。
タイトルでは3乗限定みたいに書いているけど、プログラムで行けるとこまで探してみる。
さて、どのようなプログラムを書けば良いのだろうか。
例えば、左辺の値を100から最大正整数までインクリメント(値を1ずつ増加)させて、
左辺の値を1桁ずつに分解して、それをn乗して、
うんちゃら、かんちゃら、…
なんて考えてしまった貴方。
これは途方もなく時間が掛かってしまいます。
なぜ、時間が掛かってしまうのかというと、無駄な計算が多いからです。
例えば、左辺において123と321は別ですが、右辺では項の順番は違えど同じですよね。
今回のような問題であれば、右辺から考えるのが得策でしょうね。
右辺は多項式ですから、項の順番に左右されず、、0n、1n、2n、…、9n、がそれぞれ何個使われているかで、右辺の値は決まります。
また、何度も同じ1桁の数のn乗を計算するのも無駄ですので、これは予め必要なところまで求めておけば良いということも解ります。
さて、gccを使ってプログラミングしているのですが、unsigned long longで、
最大18446744073709551615で20桁ですので、n=19までならオーバーフローせずに計算出来ますね。
さて、どんな変数や配列が必要だろうか。
とりあえず、指数の配列を19乗まで作る必要がありますね。
指数なのでpowerのpとでもしましょうか。
unsigned long long p[10][20];
あとは、64ビット正整数は右辺の合計と各桁の検査用のテンポラリで、
unsigned long long sum, tmp;
他の変数は64ビットも要らないですね。
右辺の項の個数をdigitのdで、テンポラリから数えるのをcountのcとでもして、
int d[10], c[10];
あとは、ループで使う変数として、n桁やn乗のn、汎用でiとでもして、
int n, i;
とりあえずこんな感じですかね。
まずは、指数用の配列を計算しておきます。
for (i=0; i<10; i++) p[i][0] = 1;
for (n=1; n<20; n++) for (i=0; i<10; i++) p[i][n] = p[i][n-1]*i;
0乗は1ですから、0から9まで1で埋めて、1乗以降はi・(n-1)乗で計算サせれば良いでしょう。
まぁ、0乗は使わないから、
for (i=0; i<10; i++) p[i][1] = i;
for (n=2; n<20; n++) for (i=0; i<10; i++) p[i][n] = p[i][n-1]*i;
としても良いかもしれませんね。
さて、この問題はかなりの多重ループになります。
for (n=3; n<20; n++)
for (d[9]=0; d[9]<=n; d[9]++)
for (d[8]=0; d[9]+d[8]<=n; d[8]++)
for (d[7]=0; d[9]+d[8]+d[7]<=n; d[7]++)
for (d[6]=0; d[9]+d[8]+d[7]+d[6]<=n; d[6]++)
for (d[5]=0; d[9]+d[8]+d[7]+d[6]+d[5]<=n; d[5]++)
for (d[4]=0; d[9]+d[8]+d[7]+d[6]+d[5]+d[4]<=n; d[4]++)
for (d[3]=0; d[9]+d[8]+d[7]+d[6]+d[5]+d[4]+d[3]<=n; d[3]++)
for (d[2]=0; d[9]+d[8]+d[7]+d[6]+d[5]+d[4]+d[3]+d[2]<=n; d[2]++)
for (d[1]=0; d[9]+d[8]+d[7]+d[6]+d[5]+d[4]+d[3]+d[2]+d[1]<=n; d[1]++) {
// 処理
}
大外でnで回して、d[9]からd[1]までのネストです。
しかも、終了条件はどんどん長くなっています。
大昔のプログラムならば、終了条件でこんな書き方をしたら減点ものですが、今はコンパイラの性能が良いので、力技で書いてしまいます。
ネストの中での処理です。
さて、d[0]は、nからd[1]からd[9]までを引いて求めてもよいのですが、必要がなくなりましたので、除外。
右辺の合計を求めます。
sum = 0;
for (i=1; i<10; i++) sum += p[i][n]*d[i];
tmp = sum;
0は求めてないので、1からですね。
まぁ、高々9個なので、
sum = temp = p[1][n]*d[1]+p[2][n]*d[2]+p[3][n]*d[3]+p[4][n]*d[4]+p[5][n]*d[5]+p[6][n]*d[6]+p[7][n]*d[7]+p[8][n]*d[8]+p[9][n]*d[9];
としても良いかもしれません。
合計から、各桁の値の個数を数えます。
for (i=0; i<10; i++) c[i] = 0;
while ( tmp > 0 ) c[tmp%10]++, tmp /= 10;
最後に判定ですね。
if ( sum > 1 && c[1]==d[1] && c[2]==d[2] && c[3]==d[3] && c[4]==d[4] && c[5]==d[5] && c[6]==d[6] && c[7]==d[7] && c[8]==d[8] && c[9]==d[9] ) {
// 処理
}
さて、書き忘れてましたが、どのように表示させましょうかね。
19桁の右辺は、かなり長くなるので要らないですよね。
nと左辺の値だけで良いのであれば、
printf("%d\t%llu\n", n, sum);
こんな感じでシンプルでいいかも知れませんね。
もし、0パッディングするならば、桁数nが変動するので、
文字列の変数を用意して、
char s[30];
0パッディング用のformat文を作って表示。
sprintf(s, "%%0%dllu\n", n);
printf(s, sum);
という感じでしょうか。
使った関数から、必要なヘッダーは、
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
の3つですね。
gccのコンパイルオプションは、-O3をつけて見ました。
0パッティングの出力で、
153
370
371
407
1634
8208
9474
04150
04151
54748
92727
93084
548834
1741725
4210818
9800817
9926315
24678050
24678051
88593477
146511208
472335975
534494836
912985153
4679307774
32164049650
32164049651
40028394225
42678290603
44708635679
49388550606
82693916578
94204591914
0564240140138
28116440335967
4338281769391370
4338281769391371
00233411150132317
21897142587612075
35641594208964132
35875699062250035
1517841543307505039
3289582984443187032
4498128791164624869
4929273885928088826
のような結果を秒殺でした。
さて、Facebookの数学コミュに上げた人は、どうやら私とは考えが違うようで、
194979=15+95+45+95+75+95
14459929=17+47+47+57+97+97+27+97
も上げてました。
つまり、左辺の桁数を右辺の指数が超えているのも有りということですね。
うーん、こっちのパターンを入れるとなると、またプログラムを書き直しですね。
ではでは