Yahoo!知恵袋に質問された、大学の授業課題について、よりイケてる?回答案を公開

法政大学の児玉教授が授業で学生に出した課題が、Yahoo!知恵袋で質問されていたことが最近明るみになりました。質問されたのは、2014年の秋ですが、最近になって児玉先生自身も気付かれたようです。

いろんな意味で面白い時代になったものです。

知恵袋で公開された回答をコピペして提出した学生の単位がどうなるか興味深いところではあります。質問者は、課題を解くための知識はおろか、公開質問したらどうなるのかすら理解が及んでいないにもかかわらず、このような手段に及んでいることでもあり、よほど追い込まれていたのだろうと思います。だからといって、奨励されるようなことでは全くありません。

その課題は、以下のような出力をするC言語プログラムを作る課題でした。

     0
    111
   22222
  3333333
 444444444
55555555555
 666666666
  7777777
   88888
    999
     0
     0
    111
   22222
  3333333
 444444444
55555555555
 666666666
  7777777
   88888
    999
     0

(課題の説明はこちらのリンク先にあります。http://www.kodama-lab.com/seminar/clang/summer/ex003.html
参考になるプログラムが最初から与えられていて、これを改良しなさいということです。

Yahoo!知恵袋では、元のプログラムのつくりを維持したまま、最小限の改造ですむやり方を教えてくれた人の回答が、ベストアンサーでした。(そもそも回答は1件しかなかったようです)ある程度C言語が理解できる人であれば、順当に思いつく回答です。

 つ・ま・ら・な・い!

だれが書いても同じであるプログラムにはなんの価値もかんじません!

商売ならまだしも、趣味で回答をしているのなら、技術の無駄遣い!

この課題では、元のプログラムの改良範囲に指定はありません。回答者は、元のプログラムを意識することなく、自由にプログラムを考えてよいのです。そこにはいろいろ工夫を入れられる余地があります。

課題の提出期限はとうに過ぎたはなしでもあるので、僕が考えた回答案を、ここに公開します。

その1:内側のループに含まれる条件分岐をなくす。

#include <stdio.h>

void pdegits(int n) {
  int m = n % 10;
  int p = m * ((m / 5) ^ 1) + (10 - m) * (m / 5);
  for (int i = 0; i < 5 - p; i++) {
    printf(" ");
  }
  for (int i = 0; i < p * 2 + 1; i++) {
    printf("%1d", m);
  }
  printf("\n");
}

int main(void) {
  for (int i = 0; i <= 10; i++) {
    pdegits(i);
  }
  for (int i = 0; i <= 10; i++) {
    pdegits(i);
  }
}

児玉先生の元のプログラムは、初心者に説明しやすいアルゴリズムになっているのですが、多重ループの内側に条件分岐が存在することが、性格的にどうも気になってしょうがありません。これは、除去したい。バージョニングによって、条件分岐をループの外側に括り出すというのが、ひとつのやりかたで、パフォーマンスも良好と思われるのですが、あえて、1, 3, 5, 7, 9, 11, 9, 7, … という数列をつくりだす関数を考えるというアプローチをとりました。

その2:ループでfor文を使わない。

#include <stdio.h>

void pdegits(int n, int x) {
  if (n == 0) {
    printf("%1d", x);
    return;
  }
  printf("%1d", x);
  pdegits(n - 1, x);
  printf("%1d", x);
}

void foo() {
  for (int i = 0; i < 11; i++) {
    int j = i % 10;
    int k = j * ((j / 5) ^ 1) + (10 - j) * (j / 5);
    if (k < 5) {
      for (int s = 0; s < 5 - k; s++) {
        printf(" ");
      }
    }
    pdegits(k, j);
    printf("\n");
  }
}

int main(void) {
  foo();
  foo();
}

一部のループをfor文ではなく、再帰をつかって表現してみました。関数型言語でのプログラミングでは一般的な書き方だと思います。C言語において、こういう書き方をしても、あんまり得をすることはないんじゃないかと思ってます。最近のコンパイラだと、このコードをちゃんと理解して、オプティマイズしてくれるのかもしれません。forループの多用は、プログラムの見通しを悪くすることが多いので、こういう手法も個人的にはありだと思っています。

その3:printfのフォーマット文字列を動的に生成する


#include <stdio.h>

void foo() {
  long n = 1;
  char fmt[8];
  for (int i = 0; i <= 10; i++) {
    int j = i % 10;
    int p = (j / 5) ^ 1;
    int q = j / 5;
    int k = j * p + (10 - j) * q;
    sprintf(fmt, "%%%dld\n", 6 + k);
    printf(fmt, n * j);
    n = n * p * 100 + n * q / 100 + 11 * p;
  }
}

int main(void) {
  foo();
  foo();
}

これはさらにアクロバティックな回答案です。うっかりこれを提出しようものなら、なんのために、このアプローチをとったのか、小一時間問い詰められることでしょう。なにをやるプログラムなのかわかりにくいし、パフォーマンス的にも得をしていません。ひとついえるメリットは、コード量が少ないということでしょう。これはメモリが少ないなどの事情でプログラムを短くしたいときに使えるテクニックです。しかし、メモリ単価が安い今の時代に、こういうリスキーなコーディングをして、ちょっとぐらい短くすることは全く奨励されません。でも、僕はこういうのが好きなんです。ロマンを感じるんです!

さて、以上、自分の3つの案を公開させていただきました。解いてみるとわかるのですが、解く前に思っていたよりかは複雑で面白い課題です。元のプログラムにこだわらないとすると、このようにいろんなやり方が考えられます。俺だったらもっとセンスのあるコードをかけるぜという方の回答案をお待ちしております。お問い合わせページからご投稿いただければ、ここに追記したいと思います。