M.Hiroi's Home Page

Linux Programming

Yet Another Perl Problems

[ PrevPage | Perl | NextPage ]

●問題1

次のような九九表を表示する関数 ninety_nine() を定義してください。

   |  1  2  3  4  5  6  7  8  9
---+---------------------------
 1 |  1  2  3  4  5  6  7  8  9 
 2 |  2  4  6  8 10 12 14 16 18 
 3 |  3  6  9 12 15 18 21 24 27 
 4 |  4  8 12 16 20 24 28 32 36 
 5 |  5 10 15 20 25 30 35 40 45 
 6 |  6 12 18 24 30 36 42 48 54 
 7 |  7 14 21 28 35 42 49 56 63 
 8 |  8 16 24 32 40 48 56 64 72 
 9 |  9 18 27 36 45 54 63 72 81 

解答

●問題2

整数 n から m までの和、二乗の和、三乗の和を求める関数 sum_of_int(n. m), sum_of_square(n, m), sum_of_cube(n, m) を次に示す公式を使って定義してください。

解答

●問題3

x の y 乗を求める関数 power(x, y) を定義してください。ここでは、x を浮動小数点数, y を整数とします。

解答

●問題4

下図に示すフィボナッチ関数 fibo(n) を定義してください。

fibo(0) = 0
fibo(1) = 1
fibo(n) = fibo(n - 1) + fibo(n - 2)

0, 1, 1, 2, 3, 5, 8, 13 .... という直前の 2 項を足していく数列


    図 : フィボナッチ関数の定義

解答

●問題5

整数 n を b 進数 (2 <= b <= 16) で画面 (標準出力) に表示する関数 print_int(n, b) を定義してください。

解答

●問題6

n 個の中から r 個を選ぶ組み合わせの数 nr を整数で求める関数 combination(n, r) を定義してください。

解答

●問題7

1 から n までの数字から m 個を選ぶ順列を画面に表示する関数 permutations(n, m) を定義してください。

解答

●問題8

1 から n までの数字から重複を許して m 個を選ぶ順列を画面に表示する関数 repeat_perm(n, m) を定義してください。

解答

●問題9

1 から n までの数字から r 個を選ぶ組み合わせを画面に表示する関数 combinations(n, r) を定義してください。

解答

●問題10

1 から n までの数字から重複を許して r 個を選ぶ組み合わせを画面に表示する関数 repeat_comb(n, r) を定義してください。

解答


●解答1

リスト : 九九表 (yapp01.pl)

use strict;
use warnings;

sub ninety_nine {
    print "   |  1  2  3  4  5  6  7  8  9\n";
    print "---+---------------------------\n";
    for (my $i = 1; $i <= 9; $i++) {
        printf("%2d | ", $i);
        for (my $j = 1; $j <= 9; $j++) {
            printf("%2d ", $i * $j);
        }
        print "\n";
    }
}

ninety_nine();

九九表は二重の for ループで簡単に作ることができます。関数 printf はC言語の printf とほぼ同じです。printf の書式 %2d の 2 はフィールド幅を指定します。%nd は表示する整数が n 桁未満の場合、上位の桁には空白文字が埋められます。

●解答2

リスト : 整数の和、二乗の和、三乗の和 (yapp02.pl)

use strict;
use warnings;

# 1 から  n までの合計値を求める
sub sum_of_int_1 {
    my $n = shift;
    $n * ($n + 1) / 2;
}

sub sum_of_square_1 {
    my $n = shift;
    $n * ($n + 1) * (2 * $n + 1) / 6;
}

sub sum_of_cube_1 {
    my $n = shift;
    return $n * $n * ($n + 1) * ($n + 1) / 4;
}

# n から m までの合計値を求める
sub sum_of_int {
    my ($n, $m) = @_;
    sum_of_int_1($m) - sum_of_int_1($n - 1);
}

sub sum_of_square {
    my ($n, $m) = @_;
    sum_of_square_1($m) - sum_of_square_1($n - 1);
}

sub sum_of_cube {
    my ($n, $m) = @_;
    sum_of_cube_1($m) - sum_of_cube_1($n - 1);
}

printf("%.0f\n", sum_of_int(1, 10000));
printf("%.0f\n", sum_of_int(100, 10000));
printf("%.0f\n", sum_of_square(1, 10000));
printf("%.0f\n", sum_of_square(100, 10000));
printf("%.0f\n", sum_of_cube(1, 10000));
printf("%.0f\n", sum_of_cube(100, 10000));
$ perl yapp02.pl
50005000
50000050
333383335000
333383006650
2500500025000000
2500500000497500

1 から n までの合計値を関数 sum_of_xxx_1 で求めます。あとは sum_of_xxx_1($m) - sum_of_xxx_1($n - 1) を計算するだけです。printf の書式 %.0f の .0 は精度の指定です。f 指定子の場合、精度は小数点以下の桁数を表します。.0 と指定することで、小数点以下を非表示にすることができます。

●解答3

リスト : 累乗 (yapp03.pl)

use strict;
use warnings;

# 単純な繰り返し
sub power {
    my ($x, $y) = @_;
    my $a = 1.0;
    while ($y-- > 0) {
        $a *= $x;
    }
    $a;
}

# 再帰定義
sub power_rec {
    my ($x, $y) = @_;
    return 1 if ($y == 0);
    my $z = power_rec($x, int($y / 2));
    if ($y % 2 == 0) {
        $z * $z;
    } else {
        $x * $z * $z;
    }
}

printf("%.0f\n", power(2, 16));
printf("%.0f\n", power(2, 32));
printf("%.0f\n", power(2, 64));
printf("%.0f\n", power_rec(2, 16));
printf("%.0f\n", power_rec(2, 32));
printf("%.0f\n", power_rec(2, 64));
$ perl yapp03.pl
65536
4294967296
18446744073709551616
65536
4294967296
18446744073709551616

累乗は x の n 乗という x を n 回掛ける計算です。累乗は x の右上に小さく n を書くことで表されますが、ここでは x ** n と書くことにします。

pow (x, y) = x ** y

x ** 3 = x * x * x;
x ** 4 = x * x * x * x;
x ** 5 = x * x * x * x * x;


  図 : 累乗の計算

これをそのままプログラムしたものが関数 power です。プログラムは簡単なので説明は割愛します。この場合、n 回の乗算が必要になります。ところが、式を変形するともっと少ない回数で求めることができます。

x ** 4  = (x ** 2) ** 2 -> 2 回
x ** 8  = (x ** 4) ** 2 -> 3 回
x ** 16 = (x ** 8) ** 2 -> 4 回

一般化すると

x ** y = (x ** (y / 2)) ** 2       (n は偶数)
x ** y = ((x ** (y / 2)) ** 2) * x (n は奇数)


        図 : 累乗の高速化

階乗の計算では n を n - 1 の計算に置き換えていきますが、累乗の場合は y を y / 2 に置き換えていくことができます。y が半分になっていくので減少の度合いが大きくなり、計算回数は少なくて済みます。x ** (y / 2) を計算する部分は、再帰を使えば簡単です。

この考え方でプログラムしたものが関数 power_rec です。局所変数 $z を定義します。$z の値は $x ** ($y / 2) で、power_rec を再帰呼び出しすれば簡単に求めることができます。関数 int は小数点数を切り捨てて整数を返します。あとは、$y が偶数であれば $z * $z を返し、奇数であれば $x * $z * $z を返します。このように、再帰呼び出しを使って累乗を効率的に計算することができます。

●解答4

リスト : フィボナッチ関数 (yapp04.pl)

use strict;
use warnings;

# 二重再帰
sub fibo {
    my $n = shift;
    if ($n == 0 || $n == 1) {
        $n;
    } else {
        fibo($n - 1) + fibo($n - 2);
    }
}

# 末尾再帰
sub fibo_sub {
    my ($n, $a, $b) = @_;
    if ($n == 0) {
        $a;
    } else {
        fibo_sub($n - 1, $a + $b, $a);
    }
}

sub fibo_rec {
    my $n = shift;
    fibo_sub($n, 0, 1);
}

# 繰り返し
sub fiboi {
    my $n = shift;
    my $a = 0, $b = 1;
    while ($n-- > 0) {
        my $c = $a;
        $a += $b;
        $b = $c;
    }
    return $a;
}

printf("%d\n", fibo(30));
printf("%d\n", fibo_rec(30));
printf("%d\n", fiboi(30));
$ perl yapp04.pl
832040
832040
832040

フィボナッチ関数は再帰呼び出しを使えば簡単にプログラムできます。関数 fibo は自分自身を 2 回呼び出しています。これを「二重再帰」といいます。fibo の呼び出しをトレースすると下図のようになります。


            図 : 関数 fibo のトレース

同じ値を何回も求めているため、関数 fibo の効率はとても悪いのです。この場合、二重再帰を「末尾再帰」に変換すると高速化することができます。累算変数を使って二重再帰を末尾再帰へ変換したものが関数 fibo_rec と fibo_sub です。

関数 fibo_sub の累算変数 a と b の使い方がポイントです。現在のフィボナッチ数を変数 $a に、ひとつ先の値を変数 $b に格納しておきます。あとは $a と $b を足し算して、新しいフィボナッチ数を計算すればいいわけです。fibo_sub の呼び出しを下図に示します。

fibo_sub(5, 0, 1)
  fibo_sub(4, 1, 1)
    fibo_sub(3, 1, 2)
      fibo_sub(2, 2, 3)
        fibo_sub(1, 3, 5)
          fibo_sub(0, 5, 8)
          => a の値 5 を返す
        => 5
      => 5
    => 5
  => 5
=> 5


  図 : 関数 fibo_sub の呼び出し

二重再帰では、同じ値を何回も求めていたため効率がとても悪かったのですが、このプログラムでは無駄な計算を行っていないので、値を高速に求めることができます。もちろん、末尾再帰になっているので、末尾再帰最適化を行う処理系では、プログラムをより高速に実行することができます。

Perl の場合、規格では末尾再帰最適化をサポートしてませんが、末尾再帰を繰り返しに変換することは簡単です。関数 fibo を繰り返しに変換すると関数 fiboi のようになります。このように、末尾再帰は簡単に繰り返しに変換することができます。

●解答5

リスト ; 整数の n 進数表示 (yapp05.pl)

use strict;
use warnings;

our @table = qw/0 1 2 3 4 5 6 7 8 9 A B C D E F/;

sub print_int_sub {
    my ($n, $b) = @_;
    if ($n >= $b) {
        print_int_sub(int($n / $b), $b);
    }
    print $table[$n % $b];
}

sub print_int {
    my ($n, $b) = @_;
    if ($b > 1 || $b <= 16) {
        if ($n < 0) {
            print "-";
            print_int_sub(-$n, $b);
        } else {
            print_int_sub($n, $b);
        }
    } else {
        print "基数は 2 以上 16 以下の数を指定してください";
    }
    print "\n";
}

print_int(123456789, 2);
print_int(-123456789, 8);
print_int(123456789, 10);
print_int(-123456789, 16);
print_int(0xabcdef, 10);
print_int(0xabcdef, 16);
print_int(0x7fffffff, 10);
print_int(0x7fffffff, 16);
$ perl yapp05.pl
111010110111100110100010101
-726746425
123456789
-75BCD15
11259375
ABCDEF
2147483647
7FFFFFFF

実際の印字処理は関数 print_int_sub で行います。上位の桁から表示するため再帰呼び出しを使っています。$n < $b が再帰呼び出しの停止条件です。$n が $b 以上であれば、$n を $b で割り算して print_int_sub を再帰呼び出しします。戻ってきたら、n % b の印字コードを文字列 table から求めて、それを print で出力します。print_int は引数の条件をチェックして print_int_sub を呼び出すだけです。

●解答6

組み合わせの数 nr を (n, r) と表記します。(n, r) を求めるには、次の公式を使えば簡単です。

(n, r) = n * (n - 1) * (n - 2) * ... * (n - r + 1) / (1 * 2 * 3 * ... * r)

皆さんお馴染みの公式ですね。ところが、整数値の範囲が限られているプログラミング言語では、この公式を使うと乗算で「桁あふれ」を起こす恐れがあります。Perl で use integer を指定すると、M.Hiroi が使用している処理系では 64 bit 整数として計算されるので、公式をそのままプログラムするわけにはいきません。そこで、次の公式を使うことにします。

(n, 0) = (n, n) = 1
(n, r) = (n, r - 1) * (n - r + 1) / r

この式は (n, r) と (n, r - 1) の関係を表しています。あとは階乗やフィボナッチ関数と同じように、再帰定義を使って簡単にプログラムできます。

リスト : 組み合わせの数を求める (yapp06.pl)

use strict;
use warnings;
use integer;

sub combination {
    my ($n, $r) = @_;
    if ($n == $r || $r == 0) {
        1;
    } else {
        combination($n, $r - 1) * ($n - $r + 1) / $r;
    }
}

for (my $i = 8; $i < 32; $i++) {
    my $j = $i * 2;
    print "($j, $i) => ";
    print combination($j, $i), "\n";
}

とても簡単ですが、このプログラムでも桁あふれする場合があります。どこまで計算できるか試してみましょう。

(16, 8) => 12870
(18, 9) => 48620
(20, 10) => 184756
(22, 11) => 705432
(24, 12) => 2704156
(26, 13) => 10400600
(28, 14) => 40116600
(30, 15) => 155117520
(32, 16) => 601080390
(34, 17) => 2333606220
(36, 18) => 9075135300
(38, 19) => 35345263800
(40, 20) => 137846528820
(42, 21) => 538257874440
(44, 22) => 2104098963720
(46, 23) => 8233430727600
(48, 24) => 32247603683100
(50, 25) => 126410606437752
(52, 26) => 495918532948104
(54, 27) => 1946939425648112
(56, 28) => 7648690600760440
(58, 29) => 30067266499541040
(60, 30) => 118264581564861424
(62, 31) => -284401161134521734

Perl の場合、桁あふれが発生してもランタイムエラーは発生しません。なお、Perl は use integer のかわりに use bigint を指定すると多倍長整数で計算することができます。実行結果は次のようになります。

(16, 8) => 12870

・・・省略・・・

(50, 25) => 126410606437752
(52, 26) => 495918532948104
(54, 27) => 1946939425648112
(56, 28) => 7648690600760440
(58, 29) => 30067266499541040
(60, 30) => 118264581564861424
(62, 31) => 465428353255261088

●解答7

リスト : 順列 (yapp07.pl)

use strict;
use warnings;

our @buffer = (0, 0, 0, 0, 0, 0, 0, 0, 0);
our @used = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0);

sub perm_sub {
    my ($n, $m) = @_;
    if ($n == $m) {
        print @buffer[0 .. $n - 1], "\n";
    } else {
        for (my $i = 1; $i <= $n; $i++) {
            next if $used[$i];
            $buffer[$m] = $i;
            $used[$i] = 1;
            perm_sub($n, $m + 1);
            $used[$i] = 0;
        }
    }
}

sub permutations {
    my $n = shift;
    if ($n > 0 || $n <= @buffer) {
        perm_sub($n, 0);
    }
}

permutations(4);
permutations(5);

実際の処理は関数 perm_sub で行います。第 2 引数 $m が選んだ数字の個数を表します。$n と $m が等しいときが再帰呼び出しの停止条件になります。print で順列を表示します。選んだ数字 $i を配列 $buffer[$m] に格納し、配列 $used[$i] に印をつけてから perm_sub を再帰呼び出しします。再帰呼び出しから戻ってきたら @used を元に戻します。これで順列を表示することができます。

実際に実行すると次のように表示されます。

$ perl yapp07.pl
1234
1243
1324
1342
1423
1432
2134
2143
2314
2341
2413
2431
3124
3142
3214
3241
3412
3421
4123
4132
4213
4231
4312
4321
12345
12354
12435

・・・省略・・・

54231
54312
54321

●解答8

リスト : 重複順列 (yapp08.pl)

use strict;
use warnings;

our @buffer = (0, 0, 0, 0, 0, 0, 0, 0, 0);

sub repeat_perm_sub {
    my ($n, $m) = @_;
    if ($n == $m) {
        print @buffer[0 .. $n - 1], "\n";
    } else {
        for (my $i = 1; $i <= $n; $i++) {
            $buffer[$m] = $i;
            repeat_perm_sub($n, $m + 1);
        }
    }
}

sub repeat_perm {
    my $n = shift;
    if ($n > 0 || $n <= @buffer) {
        repeat_perm_sub($n, 0);
    }
}

repeat_perm(3);
repeat_perm(4);

重複順列は簡単です。数字は重複してもよいので、配列 @used で数字をチェックする必要はありません。実行結果は次のようになります。

$ perl yapp08.pl
111
112
113
121
122
123
131
132
133
211
212
213
221
222
223
231
232
233
311
312
313
321
322
323
331
332
333
1111
1112
1113

・・・省略・・・

4442
4443
4444 

●解答9

1 から 5 までの数字の中から 3 個を選ぶ組み合わせは次のようになります。

[1, 2, 3], [1, 2, 4], [1, 2, 5], [1, 3, 4], [1, 3, 5], [1, 4, 5],
[2, 3, 4], [2, 3, 5], [2, 4, 5],
[3, 4, 5],

最初に 1 を選択した場合、次は [2, 3, 4, 5] の中から 2 個を選べばいいですね。2 番目に 2 を選択したら、次は [3, 4, 5] の中から 1 個を選べばいいわけです。これで、[1, 2, 3], [1, 2, 4], [1, 2, 5] が生成されます。[2, 3, 4, 5] の中から 2 個選ぶとき、2 を選ばない場合があります。この場合は [3, 4, 5] の中から 2 個を選べばいいわけです。ここで 3 を選ぶと [1, 3, 4], [1, 3, 5] が生成できます。同様に、3 を除いた [4, 5] の中から 2 個を選ぶと [1, 4, 5] を生成することができます。

これで 1 を含む組み合わせを生成したので、次は 1 を含まない組み合わせ、つまり [2, 3, 4, 5] から 3 個を選ぶ組み合わせを生成すればいいわけです。けっきょく、この処理の考え方は組み合わせの公式と同じです。

nC0 = nCn = 1
nCr = n-1Cr-1 + n-1Cr

プログラムは次のようになります。

リスト : 組み合わせ (yapp09.pl)

use strict;
use warnings;

our @buffer = (0, 0, 0, 0, 0, 0, 0, 0, 0);

sub comb_sub {
    my ($n, $r, $m) = @_;
    if ($r == 0) {
        print @buffer[0 .. $m - 1], "\n";
    } elsif ($n == $r) {
        for (my $i = $r; $i > 0; $i--) {
            $buffer[$m++] = $i;
        }
        print @buffer[0 .. $m - 1], "\n";
    } else {
        comb_sub($n - 1, $r, $m);
        $buffer[$m] = $n;
        comb_sub($n - 1, $r - 1, $m + 1);
    }
}

sub combinations {
    my ($n, $r) = @_;
    if ($r > 0 && $r <= @buffer) {
        comb_sub($n, $r, 0);
    }
}

combinations(5, 3);
combinations(6, 4);

実際の処理は関数 comb_sub で行います。comb_sub は、1 から $n までの数字の中から $r 個を選ぶ組み合わせを生成します。引数 $m は選んだ数字の個数を表します。選んだ要素は配列 @buffer に格納します。$r が 0 になったら組み合わせを一つ生成できたので、print で組み合わせを表示します。$n が $r と等しくなったならば、残りの数字 (1 から $r まで) を全て選択します。for ループで 1 から $r までの数字を @buffer に追加してから print で表示します。

この 2 つの条件が再帰呼び出しの停止条件になります。あとは comb_sub を再帰呼び出しするだけです。最初の呼び出しは数字 $n を選ばない場合です。残りの数字の中から $r 個の数字を選びます。最後の呼び出しが数字 $n を選択する場合です。数字 $n を @buffer に追加して、残りの数字の中から $r - 1 個を選びます。

実際に実行すると次のように表示されます。

$ perl yapp09.pl
321
421
431
432
521
531
532
541
542
543
4321
5321
5421
5431
5432
6321
6421
6431
6432
6521
6531
6532
6541
6542
6543

要素の順番が逆になっていますが、正常に動作しています。

●解答10

リスト : 重複組み合わせ (yapp10.pl)

use strict;
use warnings;

our @buffer = (0, 0, 0, 0, 0, 0, 0, 0, 0);

sub repeat_comb_sub {
    my ($n, $r, $m) = @_;
    if ($r == 0) {
        print @buffer[0 .. $m - 1], "\n";
    } elsif ($n == 1) {
        for (my $i = $r; $i > 0; $i--) {
            $buffer[$m++] = 1;
        }
        print @buffer[0 .. $m - 1], "\n";
    } else {
        repeat_comb_sub($n - 1, $r, $m);
        $buffer[$m] = $n;
        repeat_comb_sub($n, $r - 1, $m + 1);
    }
}

sub repeat_comb {
    my ($n, $r) = @_;
    if ($r > 0 && $r <= @buffer) {
        repeat_comb_sub($n, $r, 0);
    }
}

repeat_comb(3, 2);
repeat_comb(4, 3);

重複組み合わせを求める repeat_comb も簡単です。実際の処理は関数 repeat_comb_sub で行います。2 番目の elsif 節で、$n が 1 の場合は 1 を $r 個選びます。最後の else 節では、$n を選んだあとそれを取り除かないで $r - 1 個の要素を選びます。実行結果は次のようになります。

$ perl yapp10.pl
11
21
22
31
32
33
111
211
221
222
311
321
322
331
332
333
411
421
422
431
432
433
441
442
443
444

初版 2015 年 6 月 21 日
改訂 2023 年 3 月 25 日

Copyright (C) 2015-2023 Makoto Hiroi
All rights reserved.

[ PrevPage | Perl | NextPage ]