ALGOBIT

2010/10/04

ハノイとバベルの塔 第五階 – 端末アニメ in C

Filed under: 離散的な気まぐれ — タグ: , — Kohyama @ 13:59

スタック* を少々拡張し, 任意の位置のデータが見れるようにしたデータ構造で一つの杭に積んである円盤の番号を保存することにし, これを三つ用意します.

以下の操作を提供する stack 型のオブジェクトがあるとします. すなわち

  • init(int n) で n 個の int 値を格納できるよう初期化
  • push(int data) で data をスタックにプッシュ
  • int pop() でデータをスタックからポップ
  • int depth() で現在のスタックの深さを参照
  • int data(int position) で指定した位置の値を参照
  • finalize() で終了処理を行う

とします.

仮のコードは以下のようになります.

#include <stdio.h>
#include <stdlib.h>

stack pegs[3];
int N, w;

static void
print()
{
	int i, j, k;

	w ミリ秒停止.
	端末をクリアして 2行1桁目に移動.
	for (i = 0; i < N; i++) {
		for (j = 0; j < 3; j++) {
			if (pegs[j].depth < N - i)
				1 個の ' ', N個の ' ', 1個の '|', N個の ' ' および 1個の ' ' を出力.
			else
				1 個の ' ',
				(N - pegs[j].data[N - i - 1]) 個の ' ',
				pegs[j].data[N - i - 1] 個の '-',
				1 個の '|',
				pegs[j].data[N - i - 1] 個の '-',
				(N - pegs[j].data[N - i - 1]) 個の ' ' および
				1 個の ' ' を出力.
		}
		改行を出力.
	}
	(6*N + 9) 個の '=' と改行を出力.
}

static void
move(stack *src, stack *tmp, stack *dst, int n)
{
	if (n == 0)
		return;
	move(src, dst, tmp, n - 1);
	dst->push(src->pop());
	print();
	move(tmp, src, dst, n - 1);
}

int
main(int argc, char *argv[])
{
	/*
	 * init
	 */
	N = (argc < 2)?3:atoi(argv[1]);
	w = (argc < 3)?500:atoi(argv[2]);
	pegs[0].init();
	pegs[1].init();
	pegs[2].init();
	for (i = 0; i < N; i++)
		pegs[0].push(N - i);

	/*
	 * do
	 */
	print();
	move(&pegs[0], &pegs[1], &pegs[2], N);

	/*
	 * fainalize
	 */
	free(pegs[0].data);
	free(pegs[1].data);
	free(pegs[2].data);

	return 0;
}

stack の実装.

  • データ自体は構造体で定義します.
    typedef _struct {
    	int depth;
    	int *data;
    } struct;
    

    stack s; に対して

  • s.init(int n) は
    s.data = (int *)malloc(n*sizeof(int));
    s.depth = 0;
  • s.push(int data) は
    s.data[s.depth++] = data;
  • s.pop() は
     s.data[--s.depth];
  • s.depth() は
    s.depth
  • s.data(int position) は
    s.data[position]
  • s.finalize() は
    free(s.data);

のように書けます.
また, 利用される可能性のある最大の長さで, ‘ ‘, ‘-’, ‘=’ の連なった文字列を用意しておき, (用意した長さ – 出力したい長さ)番目の文字から出力することで任意の長さの ‘ ‘ や ‘-’ の連なった文字列を出力します.

それぞれのコード量が少ないので展開してしまいます. コードは以下のようになります.

#include <stdio.h>
#include <stdlib.h>

typedef struct _stack {
	int depth;
	int *data;
} stack;
stack pegs[3];
int N, w;
char *bar, *ws, *grnd;
char clsmv21[] = "\x1b[2J\x1b[2;1H";

static void
print()
{
	int i, j, k;

	usleep(w*1000);
	fputs(clsmv21, stdout);
	for (i = 0; i < N; i++) {
		for (j = 0; j < 3; j++) {
			if (pegs[j].depth < N - i)
				printf(" %s|%s ", ws, ws);
			else
				printf(" %s%s|%s%s ",
				  ws + (pegs[j].data[N - i - 1]),
				  bar + (N - pegs[j].data[N - i - 1]),
				  bar + (N - pegs[j].data[N - i - 1]),
				  ws + (pegs[j].data[N - i - 1]));
		}
		putchar('\n');
	}
	fputs(grnd, stdout);
}

static void
move(stack *src, stack *tmp, stack *dst, int n)
{
	if (n == 0)
		return;
	move(src, dst, tmp, n - 1);
	dst->data[dst->depth++] = src->data[--src->depth];
	print();
	move(tmp, src, dst, n - 1);
}

int
main(int argc, char *argv[])
{
	int i;

	/*
	 * init
	 */
	N = (argc < 2)?3:atoi(argv[1]);
	w = (argc < 3)?500:atoi(argv[2]);
	pegs[0].data = (int *)malloc(N*sizeof(int));
	pegs[0].depth = 0;
	pegs[1].data = (int *)malloc(N*sizeof(int));
	pegs[1].depth = 0;
	pegs[2].data = (int *)malloc(N*sizeof(int));
	pegs[2].depth = 0;
	for (i = 0; i < N; i++)
		pegs[0].data[pegs[0].depth++] = N - i;

	/*
	 * for display
	 */
	bar = (char *)malloc((N + 1)*sizeof(char));
	for (i = 0; i < N; i++)
		bar[i] = '-';
	bar[N] = '\0';
	ws = (char *)malloc((N + 1)*sizeof(char));
	for (i = 0; i < N; i++)
		ws[i] = ' ';
	ws[N] = '\0';
	grnd = (char *)malloc((6*N + 11)*sizeof(char));
	for (i = 0; i < 6*N + 9; i++)
		grnd[i] = '=';
	grnd[6*N + 9] = '\n';
	grnd[6*N + 10] = '\0';

	/*
	 * do
	 */
	print();
	move(&pegs[0], &pegs[1], &pegs[2], N);

	/*
	 * fainalize
	 */
	free(bar);
	free(ws);
	free(grnd);
	free(pegs[0].data);
	free(pegs[1].data);
	free(pegs[2].data);

	return 0;
}

ハノイとバベルの塔 第三階 – C におけるモジュールの分離

Filed under: 離散的な気まぐれ — タグ: , , , — Kohyama @ 13:32

C言語で, 第一階 の Haskell の記述と同様な,
データ生成部分と出力部分を分離する方法について考えます.
いろいろなやり方があると思いますが, 以下に二例ほど記述したいと思います.

手順を出力する関数を用意し, パズルを解く関数への引数として, 用意しておいた関数への参照を渡すことができます.

#include <stdio.h>
#include <stdlib.h>

static void
printop(int i, char src, char dst)
{
    printf("move disc #%d from %c to %c\n", i, src, dst);
}

static void
hanoi(char src, char tmp, char dst, int n, void (*pp)(int, char, char))
{
    if (n == 0)
        return;
    hanoi(src, dst, tmp, n - 1, pp);
    pp(n, src, dst);
    hanoi(tmp, src, dst, n - 1, pp);
}

int
main(int argc, char *argv[])
{
    int n = (argc < 2)?3:atoi(argv[1]);
    hanoi('A', 'B', 'C', n, &printop);
}

グラフィカルなプログラムではユーザからのアクション(イベント)を検知する部分がライブラリとして用意されており, ライブラリ関数にそのイベントの処理方法を渡すような場合に良く使われる方法で「コールバック関数」または「デリゲート」などと呼ばる方法です.

もうひとつの例は, パズルを解く部分とデータを表示する部分で異なるプロセスもしくはスレッドに分かれて片方から片方へデータを引き渡す方法です.
以下の実装では UNIX系の OS でプロセスをフォークして, データをパイプ渡しにします.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

struct _op {
    int i;
    char dst;
    char src;
};

static void
printop()
{
    struct _op op;

    while (read(0, &op, sizeof(op)) > 0)
        printf("move disc #%d from %c to %c\n", op.i, op.src, op.dst);
}

static void
hanoi(char src, char tmp, char dst, int n)
{
    struct _op op = {n, src, dst};

    if (n == 0)
        return;
    hanoi(src, dst, tmp, n - 1);
    write(1, &op, sizeof(op));
    hanoi(tmp, src, dst, n - 1);
}

int
main(int argc, char *argv[])
{
    int n = (argc < 2)?3:atoi(argv[1]);
    int fildes[2];
    pid_t pid;

    if (pipe(fildes) < 0) {
        perror("pipe");
        exit(1);
    }
    if ((pid = fork()) < 0) {
        perror("fork");
        exit(1);
    }
    if (pid == 0) {
        close(fildes[0]);
        dup2(fildes[1], 1);
        close(fildes[1]);
        hanoi('A', 'B', 'C', n);
        close(1);
    } else {
        close(fildes[1]);
        dup2(fildes[0], 0);
        close(fildes[0]);
        printop();
        close(0);
        _exit(0);
    }
    wait(0);
    return 0;
}

引き渡すデータが struct _op 型であるという約束の元, hanoi 関数は, 手順を標準出力に次々と出力し, printop 関数は標準入力から手順が入力されてくる限りそれを読み取って出力します. hanoi 関数の実装者は, 処理したデータがどう扱われるかについては関知しません.
もちろん, 他のプラットフォームでも本質的に同様な実装が可能です.

このコード規模では分離のための記述が全体に占める割合が大きくなってしまいますが, もう少し大きなプログラムであれば, 実用されている方式と思います.


デビッド・A. クリ
アスキー
発売日:1991-07

実際に役に立つプログラムは直接なり間接なり OS と対話しなければならない.
とくに UNIX オペレーティングシステムを記述するために生まれた C 言語は, より上位のプログラミング環境の下位で OS と直接対話する基盤である.
システムコールプログラミングへようこそ.
Steve Oualline
オライリー・ジャパン
発売日:1998-06-15


2010/09/20

ハノイとバベルの塔 第二階 – 端末出力 C および Haskell

Filed under: 離散的な気まぐれ — タグ: , , — Kohyama @ 23:35

移動手順を文字列で出力するプログラムを考えます.

左の杭を A, 真ん中の杭を B, 右の杭を C とします. 小さい順に 1番から n番まで円盤には番号が付いているとします.
A の杭にある全ての円盤を C の杭に移します.

円盤の個数をコマンドライン引数として与えると

move disc #1 from A to C
move disc #2 from A to B
...

のように, 移動するべき円盤の番号と, 移動元および移動先の杭の名称を出力するようにします. 引数が無い場合は円盤の数を 3 とすることにします.

円盤の数を 3 としたときの実行結果は以下のようになります.

move disc #1 from A to C
move disc #2 from A to B
move disc #1 from C to B
move disc #3 from A to C
move disc #1 from B to A
move disc #2 from B to C
move disc #1 from A to C

以下のように考えて実装してみます.
n番以下の全ての円盤を杭srcから杭dstに動かすには, もうひとつの杭を tmp として,

  • n – 1 番以下の全ての円盤を杭src から, 杭tmp に動かす.
  • n番の円盤を杭src から杭dst に動かす.
  • n – 1 番以下の全ての円盤を杭tmp から, 杭dst に動かす.

n – 1番以下の全ての円盤を杭src’ から, 杭dst’ に動かすには, src’ を新しい src として, dst’ を新しい dst として, n – 1 を新しい n として, 上記と同じことをします.
ただし, 新しく n として設定した値が 0であれば, 目的達成です.

C 言語で記述したものです.

#include <stdio.h>
#include <stdlib.h>

static void
hanoi(char src, char tmp, char dst, int n)
{
	if (n == 0)
		return;
	hanoi(src, dst, tmp, n - 1);
	printf("move disc #%d from %c to %c\n", n, src, dst);
	hanoi(tmp, src, dst, n - 1);
}

int
main(int argc, char *argv[])
{
	int n = (argc < 2)?3:atoi(argv[1]);
	hanoi('A', 'B', 'C', n);
	return 0;
}

以下は Haskell で記述したものです.

import System

format :: (Int, Char, Char) -> String
format (n,s,d) = "move disc #" ++ (show n) ++ " from " ++ [s] ++ " to " ++ [d]

hanoi :: Char -> Char -> Char -> Int -> [(Int, Char, Char)]
hanoi _ _ _ 0 = []
hanoi s t d n = hanoi s d t (n - 1) ++
                [(n, s, d)] ++
                hanoi t s d (n - 1)

main = do args <- getArgs
          let n | (not . null) args = read (head args)::Int
                | otherwise = 3
          putStr $ unlines $ map format $ hanoi 'A' 'B' 'C' n

6〜10行目が, 主要な関数の定義です. C の場合と違い, この関数の中で出力を行うわけではありません.
15行目が, 動作の本体であり, $ を挟んで, 右側から左側に処理されたデータが引き渡されていきます.
hanoi ‘A’ ‘B’ ‘C’ n は [(1, 'A', 'C'), (2, 'A', 'B'), ...] のような, 円盤を動かす手を表すタプルのリストを返します.
map format で ["move disc #1 from A to C", "move disc #2 from A to B", ...] のように, 各タプルを整形,
unlines が “move disc #1 from A to C\nmove disc #2 from A to B\n…” (\n は改行です) のように行の集合に変換,
putStr が出力します.
UNIX のパイプに似ていると思う方もいらっしゃるかもしれません.

このような小さなプログラムでは, 大きな違いとはなりませんが, 出力の方法が, パズルを解く部分に組み込まれていないところが, 分離性が良いですね.


Graham Hutton
オーム社
発売日:2009-11-11

Bryan O’Sullivan,John Goerzen,Don Stewart
オライリージャパン
発売日:2009-10-26


Copyright © 2010 Yoshinori Kohyama All Rights Reserved.