プログラムをトレースする問題をトレースせずに解く方法について

頭の中に計算機を持たない学生へ。

プログラムをトレースする問題で困ってない? あまりにつらすぎて「計算機に計算させる方法を学んでいるはずなのに、どうして自分が計算機になって計算しないといけないのか」と不満に思っているかもしれない。

そんなあなたのために、プログラムをトレースをせずに問題を解く方法を伝授します! もしかするとこの方法にも向き不向きがあるかもしれないけど、トレースができなくても何とかなるかもしれないよ!

はじめに

私は脳内に計算機を持っていない。レジスタが不足しているのだ。 頭の中で同時に記憶できる数字はせいぜい二つが限界であり三桁の足し算すら暗算できない。 小学校の算数ドリルは数字で埋め尽くされていた。

高校生になってプログラミングに興味を持ってC言語を学び、 特に問題なく簡単なプログラムを書いたり読んだりしていた。 その後、運よくプログラミングやアルゴリズムの授業を受ける機会を得た。

授業の範囲内でプログラムを読み書きするのは簡単だった。 しかし、どうも簡単にはできないことがあった。 それがプログラムやフローチャートのトレースである。 頭の中で複数の数が変化していくのを追えないので脳内でトレースをするには限界がある。 単純な作業を正確に行うのも苦手で、紙とペンを用意して数字を書き出してトレースをしても時間がかかる上によく間違えてしまう。 これを訓練してもトレースが上手くなる希望がないことは、 膨大な時間を書けて算数の訓練を受けてもいまだに三桁の足し算すら暗算できないことが証明している。 プログラミングにはそこそこ自信があったのにトレースしろと言われたらできないのだから大きな挫折を感じた。 「私にはプログラミングは向いていないのだろうか」そう心から思った。

しかし、あるときトレースを訓練させるための問題はトレースせずに解けると気づいた。 しかも実際にデバッグなどでトレースする場合には、 トレーサーが使えるので脳内でトレースできなくてもそれほど困らない。 それを必要としている者にとってはプログラムを理解するのに必要だということに過ぎないのである。

別の方法があってそれで楽しくプログラミングできる。 トレースが苦手だからといってプログラミングを諦めるにはまだ早い。 プログラミングに向いていないと言われるかもしれないが、 それは周囲の人々が固定観念に囚われているだけかもしれない。 とはいえ、トレースの問題が解けないと試験を通過できないかもしれないし、 基本情報技術者試験を突破するのも難しくなってしまうかもしれない。

そこで、トレースの問題をハックしてトレースせずに回答するための技術を伝えよう。

トレースせずに理解して式を導け!

最初にひとつ断わっておくと、この方法には確実性がない。 出題のされ方によっても難易度が変わってくる。 うまくいけば圧倒的なスピードでトレースの問題が解けてその上正確な答えを得られるが、 うまくいかない場合には自力でトレースした方が早く解ける可能性もある。 ただ、うまくいくとトレースの問題だけでなくその周辺の問題も解けるようになる場合もあるので、引き際を考えるのは結構難しい。 少し考えてみてどうも無理だと感じたなら諦めた方がよい。

私が提案するトレースの問題の解法は、トレースせずにプログラムやフローチャートを読み、 作者が何をしようとしているのかを考えて式を導出するという方法である。 初学者がトレースをするのはプログラムを理解するためだとされているが、これはちょうどその逆の手法と言える。 つまり、プログラムの意図を理解しトレースをしたらどのような答えが得られるかを導くのだ!

実際に問題を解きながらこの方法について伝える。 一度自力で問題で考えてから、回答を確認して欲しい。

まだレベル1の問題しかないがこの回答を読めば何を言いたいのかだいたい分かると思う。 気力が湧いてきたら複雑なトレースの問題も書くけど、たぶんこれの応用で解けると思う。

レベル1: 単純な繰り返し

問題: 下記のプログラムが出力する値を求めよ。

#include <stdio.h>

int main(int argc, char **argv) {
  int acc = 1;
  int n = 5;
  int i;

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

  return 0;
}

iacc という二つの変数の遷移を追っていかないと解けないようになっている。 この二つの変数を追いかけずに問題を解くにはどうしたら良いだろうか?

答え

まず、出力している acc の初期値を確認しておこう。 トレースしたい変数の最初の値がなんであったかは重要だ。 「 acc の初期値は 1 」であることを覚えておこう。

int acc = 1

下記の for 文に注目してみよう。 これは、「 i1 から n になるまで ... を繰り返す」を意味している。 for 文の中で i が変更される場合にはこの前提が崩れることに注意しよう。

for (i = 1; i <= n; i++) {
  ...
}

その後に、for 文の中身に注目しよう。 「 acci * acc を代入」しているのが分かる。

acc = acc * i;

さて、 これで「 acc の初期値は 1 」「 i1 から n になるまで ... を繰り返す」「 acci * acc を代入」まで分かった。 まずは、繰り返し文「 i1 から 5 になるまで ... を繰り返す」 を展開してみると良い。 もしも for 文が書けなかったとしたらこのプログラムは下記のようになっていたはずだ。

acc = acc * 1;
acc = acc * 2;
.
.
.
acc = acc * n;

実際のプログラムでは当然 1...n のように列挙できない。 それができないから for 文が使用されているわけだが、 これはあくまでもあなたが問題を解くために仮に考えているだけなので列挙してよい。 上記のように並べてみるともっと単純にできることに気づく。

acc = acc * 1 * 2 * ... n

acc の初期値は 1 であり 1 には掛けても変化しないという性質があるので、 上の右辺の acc は無視できる。

acc = 1 * 2 * ... n

随分とシンプルな式になった。 これは数学的には階乗と呼ばれている。 accn の階乗、すなわち n! である。 このプログラムの作者は階乗を計算したかったのだ。 よって、 5! = 1 * 2 * 3 * 4 * 5 を慎重に計算して答えればよい。 もしも n10 だったとしても簡単にこの問題を解くことができるだろう。

もしもあなたが同じプログラムを書く場合には、このような不毛な努力をしなくても済むように階乗の部分を関数に分割して名前を付けることをおすすめする。 トレースの問題は意図して理解しにくいようにプログラムが書かれているのでマネしないように注意しよう。

#include <stdio.h>

int factorial(int n) {
  int i;
  int acc = 1;
  for (i = 1; i <= n ; i++) {
    acc = i * acc;
  }
  return acc;
}

int main(int argc, char **argv) {
  printf("%d\n", factorial(5));
  return 0;
}

おめでとう!トレースせずに答えが得られた!

まとめ

まだ例が少ないけどトレースの問題をトレースせずに解けると分かったと思う。 プログラミングの上達だってこの方法でトレースの問題を解くようにした方が却って早くなるかもしれない。 トレースがうまくできなくて困っているならレッツチャレンジ!

発想を逆転して素早く解いて、トレース信者どもを見返してやろう!