2009 年度「計算機基礎論3B」 2009-11-06

§26 二重ループ

(26.1) 次のプログラムは九九の表を出力する。

(26.2) 二重ループについて説明する。 まず外側のループから始まる。 最初は i が 1 で、 外側のループの中カッコにはいる。 するとそこに内側のループがあり、 j が 1 から 9 までこの内側のループを回る。 それが済んで改行を出力すると外側のループの中カッコの中がおわる。 そこで外側のループの再初期化にゆき、 i が 1 だけ増えて 2 になる。 これを i が 9 になるまでくり返す。

#include <stdio.h>

main() {
    int i, j;

    for (i = 1; i <= 9; i++) {
        for (j = 1; j <= 9; j++) {
            printf(" %d", i*j);
        }
        printf("\n");
    }
}

§27 ここまでの知識で書けるいくつかのプログラム

(27.1) アラレ(霰)数とは、初項を自然数とし、次の漸化式で定義される数列である。

この数列は、初項がいかなる自然数であってもそのうち 1, 4, 2, 1, 4, 2, ... というループに落ち着くであろうと予想されているそうだ。 (「霰」とは、空から降るあの「あられ」である。)

(27.2) 次のプログラムは、ユーザが入力した数から始めて 1 になるまで、 この数列を出力する。 for の小カッコの中は二つのセミコロンで三つに区切られるが、 何もない部分は空白にしておくことができる。 下の例では継続条件しかない。

#include <stdio.h>  /* アラレ数 */

main() {
    int n;

    printf("自然数を入れてください.\n");
    scanf("%d", &n);
    for (      ; n > 1;      ) {
        if (n % 2 == 0) {       /* n が偶数のとき */
            n = n / 2;
        } else {                /* n が奇数のとき */
            n = 3 * n + 1;
        }
        printf("%d ", n);
    }
    printf("\n");
}
(こういった場合、while を知っていればそちらを使うほうがはるかに自然である。)

(27.3) 自然対数の底 e を e = 1/0! + 1/1! + 1/2! + 1/3! + ... として計算するプログラムの、配列を使わない書き方。

#include <stdio.h>  /* 自然対数の底 e の計算 */

main() {
    int n;
    double sum, factorial;

    sum = 1;
    factorial = 1;

    for (n = 1; n < 23; n++) {
        factorial = factorial * n;
        sum = sum + 1 / factorial;
        printf("第 %2d 項までの和は %.53f です.\n", n, sum);
    }
}

(27.4) int 型と double 型をいっしょに使うときはこのように宣言する。 順序は逆でも構わない。 %2d は「二ケタの幅で表示せよ」の意味、 %.53f は「小数点以下 53 ケタで表示せよ」の意味である。 このプログラムでは double 型と int 型との間で四則演算を行なっている。 その場合、結果は double 型になる。 くわしくは K&R2 §2.7, A6 を参照のこと。

(27.5) ※ 計算は第 22 項までで打ち切っているが、これは試行錯誤をくり返しながら決めたもの。 「収束したら打ち切る」とすることもできなくはないが。

(27.6) 次は、素因数分解のプログラム。 三重ループになっている。 素因数が見つかるまで 2, 3, 4, ... と順に割ってみる素朴なやり方なので、 素因数分解される数が一億ぐらいになるとちょっと時間がかかる場合がある。

(ここまでの知識だけを使って“強引に”書いてみたプログラムなので、 あまりうまくないと思う。 読んで理解できなくても心配する必要はないし、 まねするのがいいかどうかもわからない。)

#include <stdio.h>

#define FROM    1   /* この数から */
#define TO    100   /* この数まで */

main() {
    int n, m, p, i;

    for (n = FROM; n <= TO; n++) {      /* n を素因数分解する */
        printf("%d =", n);
        for (m = n; m != 1; m = m/p) {  /* m を割る最小の素数を p に入れる */
            p = 0;                      /* 0 は「まだはいっていない」の印 */
            for (i = 2; p == 0; i++) {  /* 2, 3, 4, ... と順に割ってゆく */
                if (m % i == 0) {       /* 割り切れたら */
                    p = i;              /* p に格納 */
                }
            }
            printf(" %d", p);
        }
        printf("\n");
    }
}

(27.7) エラトステネスのふるいで 10000 以下の素数を全て求めるプログラム。

#include <stdio.h>  /* エラトステネスのふるいで 10000 以下の素数を全て求める */

#define KESITENAI 0                     /* 消してない */
#define KESITA 1                        /* 消した */

int a[10000+1];                         /* 配列 a[0], ... , a[10000] を宣言 */

main() {
    int n, i;

    for (n = 2; n <= 10000; n++) {      /* 配列の初期化 */
        a[n] = KESITENAI;
    }

    for (n = 2; n <= 100; n++) {        /* ふるいにかける */
        for (i = 2; n * i <= 10000; i++) {  /* n の倍数のうち */
            a[n*i] = KESITA;                /* 10000 以下のものを消す */
        }
    }

    for (n = 2; n <= 10000; n++) {      /* 結果の出力 */
        if (a[n] == KESITENAI) {
            printf("%d は素数.\n", n);
        } else {
            printf("%d は合成数.\n", n);
        }
    }
}

(27.8) ※ 出力が大量になり、最初の部分がよく見えない場合などは、 「./a.out」の代わりに「./a.out | more」とする。 一画面分だけ出力されると「--続ける--」が出て止まるので、 スペースバーを押せば次の一画面分に進む。 Enter なら一行分だけ進む。 次に進まないでプロンプトに戻りたい場合は「q」または Ctrl+C を押す。

(27.9) ※ 上のプログラムは、第二のループで合成数の倍数も消しているので、 まだまだ効率をあげる余地がある。 しかし、出力が始まったときには第二のループは終わっているわけで、 このくらいの計算はコンピュータにとってはあっという間である。

(27.10) 自然数 a, b の最大公約数 d = gcd(a, b) の求め方や、 ax + by = d を満たす整数 x, y の求め方は代数学で習ったと思う。 a が 19, b が 5 の場合、次のように上から表を埋めてゆく。

 i      a[i]     b[i]     r[i]     q[i]     x[i]     y[i]
 0:      19        5        4        3
 1:       5        4        1        1
 2:       4        1        0        4
 3:       1        0        0        0        1        0
ここで r は余り、q は商である。 x, y は、一番下はすぐわかるので、埋めておいた。 ここを、下から上へと埋めてゆきたい。

ai+1 = bi, bi+1 = ri = ai - biqi を ai+1xi+1 + bi+1yi+1 = d に代入すると bixi+1 + (ai - biqi)yi+1 = d となる。 これを ai , bi について整理して aiyi+1 + bi(xi+1 - qiyi+1) = d を得る。 よって xi = yi+1, yi = xi+1 - qiyi+1 でよい。 (ほかの選び方もある。)

これに従って、x[i], y[i] を下から埋めてゆく。

 i      a[i]     b[i]     r[i]     q[i]     x[i]     y[i]
 0:      19        5        4        3       -1        4
 1:       5        4        1        1        1       -1
 2:       4        1        0        4        0        1
 3:       1        0        0        0        1        0
のようになる。x = -1, y = 4 が答えである。

#include <stdio.h>

#define N 50

int a[N];
int b[N];
int q[N];
int r[N];
int x[N];
int y[N];

main() {
    int i;

    scanf("%d", &a[0]);
    scanf("%d", &b[0]);

    for (i = 0; b[i] != 0; i++) {
        q[i] = a[i] / b[i];
        r[i] = a[i] % b[i];
        a[i+1] = b[i];
        b[i+1] = r[i];
    }
    a[i+1] = 0;

    x[i] = 1; y[i] = 0;
    for (i-- ; i >= 0; i--) {
        x[i] = y[i+1];
        y[i] = x[i+1] - q[i] * y[i+1];
    }

    printf(" i      a[i]     b[i]     r[i]     q[i]     x[i]     y[i]\n");
    for (i = 0; a[i] != 0; i++) {
        printf("%2d:%8d %8d ", i, a[i], b[i]);
        printf("%8d %8d ", r[i], q[i]);
        printf("%8d %8d\n", x[i], y[i]);
    }
}

(27.11) ※ N を 50 にしたのは、 このコンパイラが扱える int 型の範囲ではこれで(たぶん)十分であることを別に確かめたからである。

(27.12) ※ C言語の規格では、負の数を含む割り算の余りは一通りに決まってはいない。 (-10) % 3 は -1 かも知れないし、2 かも知れないのである。 よって、上のプログラムを負の数も扱えるように改良することは (ここでは)やめておいた。

§28 練習問題(その1)

全部やらなくても構いません。 むずかしさはまちまちです。

  1. (19.3) のプログラムを改変し、 「いくつまで足しますか?」に 10 を入力すると
    1 から 1 まで足すと 1 です.
    1 から 2 まで足すと 3 です.
        ....
    1 から 10 まで足すと 55 です.
    
    と出力されるようにせよ。
  2. 鶴亀算は知らないものとし、 全ての可能性を調べることで答えを見つけるプログラムを書け。 次が出力例。ただし「5」と「16」はユーザが入力したものである。
    ツルとカメを合わせて何匹いますか?
    5
    足は何本ですか?
    16
    ツル 0 匹、カメ 5 匹のとき足は 20 本で、これは答えではありません。
    ツル 1 匹、カメ 4 匹のとき足は 18 本で、これは答えではありません。
    ツル 2 匹、カメ 3 匹のとき足は 16 本で、これが答えです。
    ツル 3 匹、カメ 2 匹のとき足は 14 本で、これは答えではありません。
    ツル 4 匹、カメ 1 匹のとき足は 12 本で、これは答えではありません。
    ツル 5 匹、カメ 0 匹のとき足は 10 本で、これは答えではありません。
    
  3. (26.2) の九九のプログラムを、桁をそろえてきれいに表示するように改良せよ。次に、
       |  1  2  ...  9
    ---+--------...----
     1 |
     2 |
     : :
     9 |
    
    のように被乗数・乗数を左と上に出力するようにしてみよ。
  4. 2 以上の自然数 n を一つ固定する。 “数”としては 0, 1, 2, ..., n-1 だけを考え、 足し算、掛け算は結果が n 以上になったら n で割った余りをもって答えとする。 この掛け算の表を出力するプログラムを書け。 n はプログラム中では「#define N 12」のようにし、 容易に変更できるようにせよ。 そして、いろいろな n に対しての出力結果を見ながら、 「一番上の行・一番左の列を除く各行・各列に 0, 1, 2, ..., n-1 がすべて現れるのは n がどのような数の場合か?」という数学の問題を考えてみよ。 この“数”を数学では Z/nZ, Z/n, Zn などと書き表す。
  5. 0 は偶数です.
    1 が奇数です.
    2 は偶数です.
      ...
    9 は奇数です.
    10 は偶数です.
    
    のように出力されるプログラムを書け。
  6. ユーザが入力した数から始めて、 次々と平方根を計算して出力するプログラムを書け。
  7. ユーザが入力した数の約数を全て出力するプログラムを書け。
  8. ユーザが入力した二つの自然数の最大公約数を出力するプログラムを書け。
  9. ユーザが入力した二つの自然数の最小公倍数を出力するプログラムを書け。
  10. プログラムの実行にかかった時間を調べるには 「date; ./a.out; date」 として開始前後の時刻を出力させるのが最も簡単であろう。 一億回まわるループ
        for (i = 0; i < 10000; i++) {
            for (j = 0; j < 10000; j++) {
                sum = sum + (i + j);    /* 意味のない適当な計算 */
            }
        }
    
    はどのくらい時間がかかるか? 回数を変えていろいろ調べてみよ。
  11. (27.3) のような、無限級数の和(の近似値)を求めるプログラムを書け。 等比級数でもよい。
  12. 1/1 + 1/2 + 1/3 + ... + 1/n + ... は無限大に発散するが、 これを実際に計算してみるプログラムを書いてみよ。 また、第 n 項までの和と log(n) との差はオイラー定数と呼ばれる数に収束することが知られている。 それも計算してみよ。 (nint 型変数として 1/n と書くと整数どうしの割り算なので n が 2 以上のときは 0 になってしまう。 1.0/n と書けば結果は double 型になり、 小数点以下まで計算される。)

§29 練習問題(その2)

(29.1) 思いつくままに書いたので、難易度はまちまちである。 いままでの知識で書けるかどうかよく確かめていないものもある。 なお、配列が出てくるものは、 大きさを N とし、N#define を利用して簡単に変更できるようにするとよい。

(29.2)

  1. 配列を適当な数列で初期化し、 次にユーザに数を入力させる。 すると、それが「何番目の項に一致する」 「どれにも一致しない」などの結果を出力するプログラムを書け。
  2. 配列を適当な狭義単調増加数列で初期化し、 次にユーザに数を入力させる。 すると、それが「何番目の項に一致する」 「何番目の項と何番目の項の間にある」 などの比較結果を出力するプログラムを書け。
  3. 配列にユーザに数値を入力させるプログラムを書け。 正しく入力されたかどうか、最後に全て出力してみること。
  4. 配列にユーザに数値を入力させ、 それを別の配列にコピーするプログラムを書け。 正しくコピーされたかどうか、最後に全て出力してみること。
  5. 配列にユーザに数値を入力させ、 それを別の配列に逆順にコピーする (i.e. a[0]b[N-1] に、 a[1]b[N-2] に、…… a[N-1]b[0] に、 のようにコピーする) プログラムを書け。 正しくコピーされたかどうか、最後に全て出力してみること。
  6. ユーザに何個かの数値を入力させ、その階差数列を出力するプログラムを書け。
  7. ユーザに何個かの数値を入力させ、その最大値、最小値を出力するプログラムを書け。 それらが何番目に入力されたものかも出力できるとなおよい。 最大値・最小値が複数個あってもそのうちのどれか一つについて何番目かを出力するだけなら、 配列を使わずにも書ける。
  8. 上のエラトステネスのふるいのプログラムで、 間違って int a[10000]; と書いたらどうなるか試せ。 (たぶん何ともならないが、間違ったプログラムであることに変わりはない。)
  9. 上のエラトステネスのふるいのプログラムでは上限の「10000」 が固定されている。 #define を利用し、簡単に変えられるようにしてみよ。 そして、変えてみよ。 (上限は平方数に限るとしてその平方根を N とし、 N*N までの自然数について調べるプログラムにすればよいだろう。)
  10. 上のエラトステネスのふるいのプログラムで上限を変えたときの実行時間を調べよ。 実行時間は N あるいは上限のどんな関数になるだろうか? (上限が大きくなると出力にかなりの時間を食うし、 「date; ./a.out; date」としても開始時刻が画面から消えてしまうので、 結果の出力をしないように改変し、 ふるいにかけるところまでの時間をはかるとよいだろう。)
  11. 上のエラトステネスのふるいのプログラムを改変し、 素数だけを出力するようにせよ。
  12. 上のエラトステネスのふるいのプログラムを改変し、 合成数については最小の素因数を出力するようにせよ。 (配列に収める値が最小の素因数になるようにすればよい。)
  13. 上のエラトステネスのふるいのプログラムを改良し、 合成数の倍数を消している無駄を省け。 その上で上限を変更し、実行時間との関連を調べよ。 実行時間は N あるいは上限のどんな関数になるだろうか?
  14. 二つの自然数 a, b を入力すると ba で割った商を(たとえば)小数点以下 100 桁まで正確に出力するプログラムを書け。 ba より大きい場合のみに限ってもよい。 (double 型変数に代入して割るだけだと、こんな精度は出ない。 int 型に代入して、小学校で習った筆算のように割り算せよ。)


岩瀬順一