先日、互いに素の再帰処理とループ処理の違いを書いた。
可読性とか、高速性とか、いろいろな角度から良し悪しを論ずることが出来る。
再帰処理ではスタックオーバーフローということがあるので、それを回避するためには、リンカーオプションでスタックを充分に取るとかできればよいが、メモリーは有限であるので、限界がある。
そういうことを回避するためにか、再帰処理をループ処理に落とし込むようなことを考えなければならない。
実は、これはかなり面倒な作業である。
再帰処理は、数学的にいうところの漸化式や木構造などの処理に使われやすい。
私としては、互いに素の構造を木構造であらわすことに成功したので、どうしても再帰処理でプログラムを書きたいというのはあり、ユークリッドの互除法による判定が無い分、かなり高速に処理出来ることも判明している。
さて、前回のプログラムでは、unsignedを使ったのだが、今回は再帰処理をループ処理に落とし込むために、どうしても符号が必要なため、signedにします。
また、プログラムの処理速度を測ることも考えて、ループ処理のfalseなどは数えないこととする。
更に、個数だけが同じでも中身が伴わなければ意味がないので、m、nをリストして中身を精査することも考える。
【ループ処理ver】
coprime_loop_signed.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int gcd(unsigned int i, unsigned int j)
{
int k;
while (j > 0) {
k = i%j;
i = j;
j = k;
}
return i;
}
int main(int argc, char *argv[])
{
int m, n, true=0, fin, prn=0;
clock_t s, e;
switch ( argc ) {
case 3:
prn = atoi(argv[2]);
case 2:
fin = atoi(argv[1]);
break;
default:
printf("Usage: coprime_loop_signed fin [list]\n\n");
printf("\tfin:\tfin > m > n for coprime m, n\n");
printf("\tlist:\t0: no print\n\t\t1: print m[tab]n\n");
printf("\n\tSample:\n\t\tcoprime 100 1 >list.txt\n");
return EXIT_SUCCESS;
}
s = clock();
for (m=2; m<fin; m++) {
for (n=1; n<m; n++) {
if ( gcd(m,n) == 1 ){
if ( prn ) printf("%d\t%d\n", m, n);
true++;
}
}
}
e = clock();
printf("TRUE=%u\nCLOCK=%f\n", true, (double)(e-s)/CLOCKS_PER_SEC);
return EXIT_SUCCESS;
}
【再帰処理ver】
coprime_recursive_signed.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int true=0, fin, prn;
void recursive(int m, int n)
{
if ( m >= fin ) return;
if ( prn ) printf("%d\t%d\n", m, n);
true++;
recursive(-n+m*2, m);
recursive(+m+n*2, n);
recursive(+n+m*2, m);
}
int main(int argc, char *argv[])
{
clock_t s, e;
switch ( argc ) {
case 3:
prn = atoi(argv[2]);
case 2:
fin = atoi(argv[1]);
break;
default:
printf("Usage: coprime_recursive_signed fin [list]\n\n");
printf("\tfin:\tfin > m > n for coprime m, n\n");
printf("\tlist:\t0: no print\n\t\t1: print m[tab]n\n");
printf("\n\tSample:\n\t\tcoprime 100 1 >list.txt\n");
return EXIT_SUCCESS;
}
s = clock();
recursive(2, 1);
recursive(3, 1);
e = clock();
printf("TRUE=%u\nCLOCK=%f\n", true, (double)(e-s)/CLOCKS_PER_SEC);
return EXIT_SUCCESS;
}
【再帰処理からループ処理化ver】
coprime_recursive_2_loop.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(int argc, char *argv[])
{
int l, m=2, n=1, fin, true=0, prn=0, up=1;
clock_t s, e;
switch ( argc ) {
case 3:
prn = atoi(argv[2]);
case 2:
fin = atoi(argv[1]);
break;
default:
printf("Usage: coprime_recursive_to_loop fin [list]\n\n");
printf("\tfin:\tfin > m > n for coprime m, n\n");
printf("\tlist:\t0: no print\n\t\t1: print m[tab]n\n");
printf("\n\tSample:\n\t\tcoprime 100 1 >list.txt\n");
return EXIT_SUCCESS;
}
s = clock();
while ( !( up == 0 && m == 3 && n == 1 ) ) {
if ( up ) {
if ( prn ) printf("%d\t%d\n", m, n);
true++;
if ( m == 2 && n == 1) {
m = 3;
n = 2;
} else if ( m == 3 && n == 1 ) {
m = 5;
n = 3;
} else if ( m > n ) {
l = m;
m = -n+2*m;
n = l;
} else {
m = -m+2*n;
if ( m > 0 ) {
l = m;
m = n+2*m;
n = l;
} else if ( -m > n ) {
l = m;
m = n-2*m;
n = -l;
} else if ( -m < n ) {
l = m;
m = n;
n = -l;
up = 0;
}
}
} else {
up = 1;
if ( m == 2 && n == 1 ) {
m = 3;
} else if ( m > n ) {
m = -m+2*n;
if ( m > 0 ) {
l = m;
m = n+2*m;
n = l;
} else if ( -m > n ) {
l = m;
m = n-2*m;
n = -l;
} else if ( -m < n ) {
l = m;
m = n;
n = -l;
up = 0;
}
}
}
if ( m >= fin ) up = 0;
}
e = clock();
printf("TRUE=%u\nCLOCK=%f\n", true, (double)(e-s)/CLOCKS_PER_SEC);
return EXIT_SUCCESS;
}
こんな感じで、一気に可読性は低くなってしまうし、コードも長くなる。
clock関数でクロック時刻を収集し、時差を求めてありますので、それぞれのプログラムの速度を数値的に感じられるかと思います。
finの値が大きくなれば、
ループ処理ver > 再帰処理からループ処理ver > 再帰処理ver
の順位に変動はないかと思います。
再帰処理がスタックオーバーフローするfinの値は、環境に依存するかと思います。
今回の様にm、nのリストを表示出来るようにしてあるので、画面上であればいくらでも羅列することは出来るでしょう。
リダイレクトでlist.txtなどへ出力したとしても、HDDの空き容量次第でしょう。
しかし、Excelで比較しようものならば、最大行は2^20=1048576行です。
(昔は2^16=65536行でしたね。)
fin=1857でTRUE=1047437となり、これがExcelのm, nの2列で比較できる最大値となります。
最近の環境であれば、1857程度でスタックオーバーフローするとは思えません。
複数行に渡ったとして、2^14=16384列
1ページのセル数は2^20x2^14=2^(20+14)=2^34=17179869184セル
TRUEのカウントで言えば、、2^32=8589934592
と32ビットの正整数型でもカウント出来ませんので、64ビットにしなければなりませんね。
そもそも、ASCIIテキストで容量的に保存出来るのか、そういった問題になってきますね。
因みに、再帰処理とループ処理では(m, n)の出力される順番が異なりますので、mやnでソートする必要があるでしょう。
再帰処理をループ処理化してみたが、結構頭を使い、たいへんでした。
もっと綺麗な方法があるのかもしれませんが、再起するものの性質によって変わってくるのだろうと思います。
また、枝刈りが出来る部分がいくつか残っていることも不満ではあります。
1st Childのmがfinより大きかったら、2nd Child、3rd Childへ行く必要がない。
2nd Childのmがfinより大きかったら、3rd Childへ行く必要がない。
こういった部分や、親へ帰らなければ自分が第何子なのか解らないので、処理が面倒になる。とか…。
そもそも、折角数学的、プログラム的に綺麗な形に落とし込むことに成功したのに、なぜ可読性が低くなるような改悪をしなければならないんだという思いが強くなりますね。
ではでは