M.Hiroi's Home Page

お気楽C#プログラミング超入門

数で遊ぼう


Copyright (C) 2016-2022 Makoto Hiroi
All rights reserved.

●多倍長整数

C# はライブラリ System.Numerics に定義されている構造体 BigInteger を使うと、任意の桁の整数 (多倍長整数) を扱うことができます。

リスト : 多倍長整数 (bigint.csx)

using System.Numerics;

// 階乗
BigInteger Fact(int n) {
  if (n == 0)
    return 1;
  else
    return n * Fact(n - 1);
}

// フィボナッチ数
BigInteger Fibo(int n) {
  BigInteger a = 0, b = 1;
  while (n-- > 0) {
    BigInteger c = a;
    a = b;
    b += c;
  }
  return a;
}

// 累乗
BigInteger Power(BigInteger n, int m) {
  if (m == 0) {
    return 1;
  } else if (m % 2 == 1) {
    return n * Power(n, m - 1);
  } else {
    BigInteger a = Power(n, m / 2);
    return a * a;
  }
}

void test() {
  for (int i = 0; i <= 20; i++)
    Console.WriteLine("{0}! = {1}", i, Fact(i));
  for (int i = 40; i <= 60; i++)
    Console.WriteLine("Fibo({0}) = {1}", i, Fibo(i));
  Console.WriteLine("Power(2, 100) = {0}", Power(2, 100));
  Console.WriteLine("Power(3, 100) = {0}", Power(3, 100));
  Console.WriteLine("Power(5, 100) = {0}", Power(5, 100));
}

// 実行
test();
$ dotnet script bigint.csx
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
10! = 3628800
11! = 39916800
12! = 479001600
13! = 6227020800
14! = 87178291200
15! = 1307674368000
16! = 20922789888000
17! = 355687428096000
18! = 6402373705728000
19! = 121645100408832000
20! = 2432902008176640000
Fibo(40) = 102334155
Fibo(41) = 165580141
Fibo(42) = 267914296
Fibo(43) = 433494437
Fibo(44) = 701408733
Fibo(45) = 1134903170
Fibo(46) = 1836311903
Fibo(47) = 2971215073
Fibo(48) = 4807526976
Fibo(49) = 7778742049
Fibo(50) = 12586269025
Fibo(51) = 20365011074
Fibo(52) = 32951280099
Fibo(53) = 53316291173
Fibo(54) = 86267571272
Fibo(55) = 139583862445
Fibo(56) = 225851433717
Fibo(57) = 365435296162
Fibo(58) = 591286729879
Fibo(59) = 956722026041
Fibo(60) = 1548008755920
Power(2, 100) = 1267650600228229401496703205376
Power(3, 100) = 515377520732011331036461129765621272702107522001
Power(5, 100) = 
7888609052210118054117285652827862296732064351090230047702789306640625

●分割数

整数 n を 1 以上の自然数の和で表すことを考えます。これを「整数の分割」といいます。整数を分割するとき、同じ自然数を何回使ってもかまいませんが、並べる順序が違うだけのものは同じ分割とします。簡単な例を示しましょう。次の図を見てください。

─┬─ 6                           : 6
  │
  ├─ 5 ─ 1                      : 5 + 1
  │
  ├─ 4 ┬ 2                      : 4 + 2
  │     │
  │     └ 1 ─ 1                 : 4 + 1 + 1
  │
  ├─ 3 ┬ 3                      : 3 + 3
  │     │
  │     ├ 2 ─ 1                 : 3 + 2 + 1
  │     │
  │     └ 1 ─ 1 ─ 1            : 3 + 1 + 1 + 1
  │
  ├─ 2 ┬ 2 ┬ 2                 : 2 + 2 + 2
  │     │   │
  │     │   └ 1 ─ 1            : 2 + 2 + 1 + 1
  │     │
  │     └ 1 ─ 1 ─ 1 ─ 1       : 2 + 1 + 1 + 1 + 1
  │
  └─ 1 ─ 1 ─ 1 ─ 1 ─ 1 ─ 1  : 1 + 1 + 1 + 1 + 1 + 1

                    図 : 整数 6 の分割

6 の場合、分割の仕方は上図のように 11 通りあります。この数を「分割数」といいます。分割の仕方を列挙する場合、整数 n から k 以下の整数を選んでいくと考えてください。まず、6 から 6 を選びます。すると、残りは 0 になるので、これ以上整数を分割することはできません。次に、6 から 5 を選びます。残りは 1 になるので、1 を選ぶしか方法はありません。

次に、4 を選びます。残りは 2 になるので、2 から 2 以下の整数を分割する方法になります。2 から 2 を選ぶと残りは 0 になるので 2 が得られます。1 を選ぶと残りは 1 になるので、1 + 1 が得られます。したがって、4 + 2, 4 + 1 + 1 となります。同様に、6 から 3 を選ぶと、残りは 3 から 3 以下の整数を選ぶ方法になります。

6 から 2 以下の整数を選ぶ方法は、残り 4 から 2 以下の整数を選ぶ方法になり、そこで 2 を選ぶと 2 から 2 以下の整数を選ぶ方法になります。1 を選ぶと 4 から 1 以下の整数を選ぶ方法になりますが、これは 1 通りしかありません。最後に 6 から 1 を選びますが、これも 1 通りしかありません。これらをすべて足し合わせると 11 通りになります。

それでは問題です。

  1. 分割数を求めるプログラムを作ってください。
  2. 分割の仕方をすべて求めるプログラムを作ってください。

●解答1

整数 n を k 以下の整数で分割する総数を求める関数を p(n, k) とすると、p(n, k) は次のように定義することができます。

\( p(n, k) = \begin{cases} 1 & \mathrm{if} \ n = 0 \ \mathrm{or} \ k = 1 \\ 0 & \mathrm{if} \ n \lt 0 \ \mathrm{or} \ k \lt 1 \\ p(n - k, k) + p(n, k - 1) & \mathrm{others} \end{cases} \)

たとえば、p(6, 6) は次のように計算することができます。

p(6, 6) => p(0, 6) + p(6, 5)
        => 1 + p(1, 5) + p(6, 4)
        => 1 +    1    + p(2, 4) + p(6, 3)
        => 1 + 1 + 2 + 7
        => 11

p(2, 4) => p(-2, 4) + p(2, 3)
        =>    0     + p(-1, 3) + p(2, 2)
        =>    0     +    0     + p(0, 2) + p(2, 1)
        => 0 + 0 + 1 + 1
        => 2

p(6, 3) => p(3, 3) + p(6, 2)
        => p(0, 3) + p(3, 2) + p(4, 2) + p(6, 1)
        =>    1    + p(1, 2) + p(3, 1) + p(2, 2) + p(4, 1) + 1
        =>    1    +    1    +    1    + p(0, 2) + p(2, 1) + 1 + 1
        => 1 + 1 + 1 + 1 + 1 + 1 + 1
        => 7

分割数を求める関数を PartitionNumber() とすると、関数 p(n, k) を使って次のようにプログラムすることができます。

リスト : 分割数 (partnum1.csx)

int PartNum(int n, int k) {
  if (n == 1 || k == 1)
    return 1;
  else if (n < 0 || k < 1)
    return 0;
  else
    return PartNum(n - k, k) + PartNum(n, k - 1);
}

int PartitionNumber(int n) {
  return PartNum(n, n);
}

void test() {
  for (int n = 1; n <= 20; n++)
    Console.Write("{0} ", PartitionNumber(n));
  Console.WriteLine("");
}

// 実行
test();
$ dotnet script partnum1.csx
1 2 3 5 7 11 15 22 30 42 56 77 101 135 176 231 297 385 490 627

関数 PartNum() は p(n, k) の定義をそのままプログラムしただけです。ただし、このプログラムは二重再帰で何度も同じ値を求めているため実行速度はとても遅くなります。

●動的計画法による高速化

動的計画法を使うと、大きな値でも高速に計算することができます。次の図を見てください。

k 
1 : [1,  1,  1,  1,  1,  1,  1] 

2 : [1,  1,  1+1=2, 1+1=2, 2+1=3, 2+1=3, 3+1=4]
 => [1,  1,  2,  2,  3,  3,  4]

3:  [1,  1,  2,  1+2=3, 1+3=4, 2+3=5, 3+4=7]
 => [1,  1,  2,  3,  4,  5,  7]

4:  [1,  1,  2,  3,  1+4=4, 1+5=6, 2+7=9]
 => [1,  1,  2,  3,  5,  6,  9

5:  [1,  1,  2,  3,  5,  1+6=7, 1+9=10]
 => [1,  1,  2,  3,  5,  7,  10]

6:  [1,  1,  2,  3,  5,  7,  10+1=11]
 => [1,  1,  2,  3,  5,  7,  11]

大きさ n + 1 の配列を用意します。配列の添字が n を表していて、p(n, 1) から順番に値を求めていきます。p(n, 1) の値は 1 ですから、配列の要素は 1 に初期化します。次に、p(n, 2) の値を求めます。定義により p(n, 2) = p(n - 2, 2) + p(n, 1) なので、2 番目以降の要素に n - 2 番目の要素を加算すれば求めることができます。あとは、k の値をひとつずつ増やして同様の計算を行えば p(n, n) の値を求めることができます。

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

リスト : 分割数 (動的計画法, partnum2.csx)

using System.Numerics;

BigInteger PartitionNumber2(int n) {
  var table = new BigInteger [n + 1];
  for (int i = 0; i <= n; i++)
    table[i] = 1;
  for (int i = 2; i <= n; i++) {
    for (int j = i; j <= n; j++) {
      table[j] += table[j - i];
    }
  }
  return table[n];
}

void test() {
  DateTime s = DateTime.Now;
  Console.WriteLine("{0}", PartitionNumber2(1000));
  DateTime e = DateTime.Now;
  Console.WriteLine("{0}", e - s);
  s = DateTime.Now;
  Console.WriteLine("{0}", PartitionNumber2(2000));
  e = DateTime.Now;
  Console.WriteLine("{0}", e - s);
  s = DateTime.Now;
  Console.WriteLine("{0}", PartitionNumber2(4000));
  e = DateTime.Now;
  Console.WriteLine("{0}", e - s);
}

// 実行
test();

説明をそのままプログラムしただけなので、とくに難しいところはないと思います。

それでは実際に試してみましょう。1000, 2000, 4000 の分割数を求めてみました。

$ dotnet script partnum2.csx
24061467864032622473692149727991
00:00:00.3146545
4720819175619413888601432406799959512200344166
00:00:00.1798506
1024150064776551375119256307915896842122498030313150910234889093895
00:00:00.4350317

実行環境 : Ubunts 22.04 (WSL), Intel Core i5-6200U 2.30GHz

動的計画法の効果はとても高いですね。

●さらなる高速化

ところで、数がもっと大きくなると動的計画法を使ったプログラムでも遅くなります。実際に 5000, 6000, 7000 の分割数を求めてみましょう。

$ dotnet script partnum2.csx
169820168825442121851975101689306431361757683049829233322203824652329144349
00:00:01.0632405
467172753197020909297102464397369064336462915327003703385660552892507240534
9246129
00:00:00.9890304
328569308034406157862809256359241668619501515745322406596990321574322363943
74450791229199
00:00:01.3793465

分割数 - Wikipedia によると、次の漸化式を使うと分割数を高速に求めることができるそうです。

\( p(k) = p(k - 1) + p(k - 2) - p(k - 5) - p(k - 7) + p(k - 12) + p(k - 15) - p(k - 22) - \cdots \)

漸化式の説明を 分割数 - Wikipedia より引用します。

『ここで p(0) = 1 および負の整数 k に対して p(k) = 0 とし、和は (1/2)n(3n - 1) の形(ただし n は正または負の整数全体を走る)の一般五角数全体にわたってとるものとする(順に n = 1, -1, 2, -2, 3, -3, 4, -4 ..., とすると、値として 1, 2, 5, 7, 12, 15, 22, 26, 35, 40, 51, ... が得られる)。和における符号は交互に +, +, -, -, +, +, ... と続く。』

分割数 p(k) は k - 1 以下の分割数がわかれば求めることができます。この漸化式も動的計画法を使えば簡単にプログラムできます。次のリストを見てください。

リスト : 分割数 (オイラーの五角数定理, partnum3.csx)

using System.Numerics;

// 五角数
int Pentagon(int n) {
  return n * (3 * n - 1) / 2;
}

BigInteger PartitionNumber3(int n) {
  var p = new BigInteger [n + 1];
  p[0] = 1;
  for (int i = 1; i <= n; i++) {
    int j = 1, s = 1;
    while (true) {
      int k = Pentagon(j);
      if (i < k) break;
      p[i] += p[i - k] * s;
      k = Pentagon(-j);
      if (i < k) break;
      p[i] += p[i - k] * s;
      j++;
      s *= -1;
    }
  }
  return p[n];
}

void test() {
  DateTime s = DateTime.Now;
  Console.WriteLine("{0}", PartitionNumber3(10000));
  DateTime e = DateTime.Now;
  Console.WriteLine("{0}", e - s);
  s = DateTime.Now;
  Console.WriteLine("{0}", PartitionNumber3(15000));
  e = DateTime.Now;
  Console.WriteLine("{0}", e - s);
  s = DateTime.Now;
  Console.WriteLine("{0}", PartitionNumber3(20000));
  e = DateTime.Now;
  Console.WriteLine("{0}", e - s);
}

// 実行
test();

配列 p は分割数 p(k) を記憶するために使います。p[0] を 1 に初期化したあと、for ループで 1 から n までの分割数を順番に求めていきます。あとは、漸化式をそのままプログラムするだけです。変数 s は符号 (+. -) を表していて、j が奇数のとき s は 1 になり、j が偶数のときは -1 になります。

それでは実際に 10000, 15000, 20000 の分割数を求めてみましょう。

$ dotnet script partnum3.csx
3616725132563629398882047189095369549501603033931565042208186860588795256875406
6420592310556052906916435144
00:00:00.5131060
2626337936403790841371023191659066988029320559654372494065885879713751200817910
56718639088570913175942816125969709246029351672130266
00:00:00.2803968
2521148138125296979166195332304704522813289496018115934368503141080342844238015
64956623970731689824369192324789351994903016411826230578166735959242113097
00:00:00.4392155

このように、20000 の分割数でも 1 秒かからずに求めることができます。


●解答2

リスト : 整数の分割 (parint.csx)

void PrintPartInt(List<int> a, int n) {
  foreach(int x in a) {
    Console.Write("{0} ", x);
  }
  while (n-- > 0) Console.Write("1 ");
  Console.WriteLine("");
}
  
void PartIntSub(int n, int k, List<int> a) {
  if (n == 0) {
    PrintPartInt(a, 0);
  } else if (n == 1) {
    PrintPartInt(a, 1);
  } else if (k == 1) {
    PrintPartInt(a, n);
  } else {
    if (n >= k) {
      a.Add(k);
      PartIntSub(n - k, k, a);
      a.RemoveAt(a.Count - 1);
    }
    PartIntSub(n, k - 1, a);
  }
}

void PartitionOfInt(int n) {
  PartIntSub(n, n, new List<int>());
}

// 実行
PartitionOfInt(5);
PartitionOfInt(6);
PartitionOfInt(7);

基本的な考え方は PartitionNumber() と同じです。関数 PartIntSub() は選んだ数値を累積変数 a の配列に格納していくだけです。n が 0 の場合は a を出力し、n が 1 の場合は a と 1 をひとつ出力します。k が 1 の場合は a と 1 を n 個出力します。

5, 6, 7 の分割の仕方は次のようになります。

$ dotnet script partint.csx
5
4 1
3 2
3 1 1
2 2 1
2 1 1 1
1 1 1 1 1
6
5 1
4 2
4 1 1
3 3
3 2 1
3 1 1 1
2 2 2
2 2 1 1
2 1 1 1 1
1 1 1 1 1 1
7
6 1
5 2
5 1 1
4 3
4 2 1
4 1 1 1
3 3 1
3 2 2
3 2 1 1
3 1 1 1 1
2 2 2 1
2 2 1 1 1
2 1 1 1 1 1
1 1 1 1 1 1 1

●参考 URL

  1. 分割数 - Wikipedia
  2. 自然数の分割 - Wikipedia
  3. 分割数 - 桃の天然水, (inamori さん)

●完全順列

N 個の整数 1, 2, ..., N の順列を考えます。先頭要素を 1 番目としたとき、i 番目の要素が整数 i ではない順列を完全順列 (derangement) といいます。今回は 1 から N までの整数値で完全順列を生成する関数 Derangement() を作ってみましょう。

リスト : 完全順列 (derange.csx)

using System.Collections.Generic;

void PrintList(List<int> a) {
  foreach(int x in a) {
    Console.Write("{0} ", x);
  }
  Console.WriteLine("");
}
  
void DerangeSub(int n, List<int> a) {
  if (a.Count == n) {
    PrintList(a);
  } else {
    for (int i = 1; i <= n; i++) {
      if (i == a.Count + 1 || a.Contains(i)) continue;
      a.Add(i);
      DerangeSub(n, a);
      a.RemoveAt(a.Count - 1);
    }
  }
}

void Derangement(int n) {
  DerangeSub(n, new List<int>());
}

void test() {
  for (int i = 3; i <= 5; i++)
    Derangement(i);
}

// 実行
test();

プログラムは簡単です。DerangeSub() の変数 a に完全順列を格納します。数字 i を a に追懐するとき、i が a.Count または i が a に含まれているならば、数字 i を a には追加しません。あとは、メソッド Add() で i を a の末尾に追加して、DerangeSub() を再帰呼び出しします。戻ってきたら末尾の数字をメソッド RemoveAt() で削除するだけです。

実行結果を示します。

$ dotnet script derange.csx
2 3 1
3 1 2
2 1 4 3
2 3 4 1
2 4 1 3
3 1 4 2
3 4 1 2
3 4 2 1
4 1 2 3
4 3 1 2
4 3 2 1
2 1 4 5 3
2 1 5 3 4
2 3 1 5 4

・・・略・・・

5 4 1 3 2
5 4 2 1 3
5 4 2 3 1

●モンモール数

完全順列の総数を「モンモール数 (Montmort number)」といいます。モンモール数は次の漸化式で求めることができます。

\( A(n) = \begin{cases} 0 & \mathrm{if} \ n = 1 \\ 1 & \mathrm{if} \ n = 2 \\ (n - 1) \times (A(n-1) + A(n-2)) & \mathrm{if} \ n \geqq 3 \end{cases} \)

BigInteger でモンモール数を求める関数は次のようになります。

リスト : モンモール数 (montmort.csx)

using System.Numerics;

BigInteger MontmortNumber(int n) {
  if (n == 1)
    return 0;
  else if (n == 2)
    return 1;
  else
    return (n - 1) * (MontmortNumber(n - 1) + MontmortNumber(n - 2));
}
  
// 別解
BigInteger MontmortNumber2(int n) {
  BigInteger a = 0, b = 1;
  for (int i = 1; i < n; i++) {
    BigInteger c = a;
    a = b;
    b = (i + 1) * (b + c);
  }
  return a;
}

void test() {
  for (int i = 1; i < 10; i++) {
    Console.WriteLine("{0}", MontmortNumber(i));
  }
  for (int i = 1; i <= 5; i++)
    Console.WriteLine("{0}", MontmortNumber2(i* 10));
}

// 実行
test();
$ dotnet script montmort.csx
0
1
2
9
44
265
1854
14833
133496
1334961
895014631192902121
97581073836835777732377428235481
300158458444475693321518926221316715906770469041
11188719610782480504630258070757734324011354208865721592720336801

関数 MontmortNumber() は公式をそのままプログラムしただけです。二重再帰になっているので、実行速度はとても遅くなります。これを繰り返しに変換すると別解のようになります。考え方はフィボナッチ数列と同じです。変数 a に i 番目の値を、b に i + 1 番目の値を保存しておきます。すると、i + 2 番目の値は (i + 1) * (a + b) で計算することができます。あとは、b の値を a に、新しい値を b にセットして処理を繰り返すだけです。


●エラトステネスの篩

素数を求める簡単で高速な方法を紹介しましょう。最初に、2 から N までの整数列を生成します。先頭の 2 は素数なので、この整数列から 2 で割り切れる整数を取り除き除きます。2 で割り切れる整数が取り除かれたので、残った要素の先頭が素数になります。先頭要素は 3 になるので、今度は 3 で割り切れる整数を取り除けばいいのです。このように、素数を見つけたらそれで割り切れる整数を取り除いていくアルゴリズムを「エラトステネスの篩 (ふるい)」といいます。

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

リスト : エラトステネスの篩 (sieve.csx)

void Sieve(int n) {
  var primes = new bool [n / 2];   // false で初期化される
  int x = 3;
  Console.Write("2 ");
  while (x * x <= n) {
    int y = (x - 3) / 2;
    if (!primes[y]) {
      Console.Write("{0} ", x);
      y += x;
      while (y < primes.Length) {
        primes[y] = true;
        y += x;
      }
    }
    x += 2;
  }
  while (x <= n) {
    int y = (x - 3) / 2;
    if (!primes[y]) Console.Write("{0} ", x);
    x += 2;
  }
  Console.WriteLine("");
}

// 実行
Sieve(1000);

bool の配列 primes で奇数列 (3, 5, 7, ... ) を表します。false で素数を表し、素数でない場合は true に書き換えます。primes は false で初期化されるので、最初はすべての数が素数ということになります。

奇数を変数 x とし、それに対応する primes の添字を変数 y とすると、変数 x は 3, 5, 7, 9, ... に、それに対応する変数 y は 0, 1, 2, 3, ... になります。この場合、x の倍数に対応する y の値は y + x, y + x * 2, y + x * 3, ... になります。たとえば、3, 5, 7 の倍数は次のようになります。

x |  3  5  7  9 11 13 15 17 19 21 23 25
y |  0  1  2  3  4  5  6  7  8  9 10 11
--+-------------------------------------
3 |  O        0        O        0
5 |     0              0              0
7 |        0                    0

プログラムは簡単です。最初の while ループで、x を √n まで +2 ずつ増やして素数かチェックします。primes の添字 y は (x - 3) / 2 で求めることができます。primes[y] が false ならば x は素数です。x の倍数を primes から削除します。それから、次の while ループで √n よりも大きい素数を求めます。

$ dotnet script sieve.csx
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 
107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 
223 227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331 
337 347 349 353 359 367 373 379 383 389 397 401 409 419 421 431 433 439 443 449 
457 461 463 467 479 487 491 499 503 509 521 523 541 547 557 563 569 571 577 587 
593 599 601 607 613 617 619 631 641 643 647 653 659 661 673 677 683 691 701 709 
719 727 733 739 743 751 757 761 769 773 787 797 809 811 821 823 827 829 839 853 
857 859 863 877 881 883 887 907 911 919 929 937 941 947 953 967 971 977 983 991 
997

●ハミング数

7 以上の素数で割り切れない正の整数を「ハミング数 (Hamming Numbers)」といいます。ハミング数は素因子が 2, 3, 5 しかない自然数のことで、素因数分解したとき 2i * 3j * 5k (i, j, k >= 0) の形式になります。たとえば、100 以下のハミング数は次のようになります。

1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, 16, 18, 20, 24, 25, 27, 30, 32, 36, 40, 45, 48, 50, 
54, 60, 64, 72, 75, 80, 81, 90, 96, 100

今回はハミング数を求めるプログラムを作りましょう。一番簡単な方法は、1 から n までの整数列を生成して、そこからハミング数を取り出していくことです。これを C# でプログラムすると次のようになります。

リスト : ハミング数を求める (hamming.csx)

using System.Linq;

bool check(int n) {
  while (n % 2 == 0) n /= 2;
  while (n % 3 == 0) n /= 3;
  while (n % 5 == 0) n /= 5;
  return n == 1;
}

void hamming() {
  foreach(int x in Enumerable.Range(1, 100).Where(check)) {
    Console.Write("{0} ", x);
  }
  Console.WriteLine("");
  for (int n = 100; n <= 100000000; n *= 10) {
    int x = Enumerable.Range(1, n).Where(check).Count();
    Console.WriteLine("{0}, {1}", n, x);
  }
}

// 実行
hamming();
$ dotnet script hamming.csx
1 2 3 4 5 6 8 9 10 12 15 16 18 20 24 25 27 30 32 36 40 45 48 50 54 60 64 72 75 80 81 90 96 100
100, 34
1000, 86
10000, 175
100000, 313
1000000, 507
10000000, 768
100000000, 1105

関数 check(n) は n がハミング数かチェックします。これは 2, 3, 5 だけで割り切れるか試しているだけです。プログラムはとても簡単ですが、引数 n の値が大きくなると時間がかかるようになります。n に比べてハミング数の個数は少ないようなので、式 2i * 3j * 5k (i, j, k >= 0) を使ってハミング数を生成したほうがよさそうです。引数 n に対して i, j, k の上限値は log2 n, log3 n, log5 n で求めることができます。たとえば、100000000 の場合は次のようになります。

i : 0 - 26
j : 0 - 16
k : 0 - 11

全体で 27 * 17 * 12 = 5508 個しかありません。この中から 100000000 以下の数を選べばいいわけです。プログラムは次のようになります。

リスト : ハミング数 (hamming2.csx)

using System.Numerics;

// べき乗
BigInteger Power(BigInteger x, int y) {
  if (y == 0) {
    return 1;
  } else if (y % 2 == 1) {
    return x * Power(x, y - 1);
  } else {
    BigInteger z = Power(x, y / 2);
    return z * z;
  }
}
  
// べき乗の集合を生成する
List<BigInteger> MakePowerList(BigInteger n, int m) {
  var xs = new List<BigInteger>();
  for (int i = 0; i <= m; i++)
    xs.Add(Power(n, i));
  return xs;
}

List<BigInteger> hamming(long n) {
  var zs = new List<BigInteger>();
  foreach(BigInteger x in MakePowerList(2, (int)Math.Log(n, 2))) {
    foreach(BigInteger y in MakePowerList(3, (int)Math.Log(n, 3))) {
      foreach(BigInteger z in MakePowerList(5, (int)Math.Log(n, 5))) {
        BigInteger m = x * y * z;
        if (m <= n) {
          zs.Add(m);
        }
      }
    }
  }
  return zs;
}

// ハミング数の個数を求める
for (long n = 100000000; n <= 1000000000000; n *= 10) {
  Console.WriteLine("{0}, {1}", n, hamming(n).Count());
}

2, 3, 5 のべき乗の集合を MakePowerList() で生成し、その要素を掛け合わせて、条件を満たす数値を選択していくだけです。実行結果は次のようになりました。

$ dotnet script hamming2.csx
100000000, 1105
1000000000, 1530
10000000000, 2053
100000000000, 2683
1000000000000, 3429

この方法だと短時間で答えを求めることができます。


●カッコ列

カッコ列は ( と ) からなる列のことで、バランスが取れているカッコ列は、右カッコで閉じることができる、つまり右カッコに対応する左カッコがある状態のことをいいます。たとえば n = 1 の場合、( ) はバランスの取れたカッコ列ですが、) ( はバランスが取れていません。今回はカッコ列を生成する関数 Kakko() を作ります。

リスト : カッコ列 (kakko.csx)

void Kakko(int n, int x = 0, int y= 0, string a = "") {
  if (x == n && y == n) {
    Console.WriteLine(a);
  } else {
    if (x < n) Kakko(n, x + 1, y, a + "(");
    if (y < x) Kakko(n, x, y + 1, a + ")");
  }
}

// 実行
Kakko(3);
Kakko(4);

カッコ列の生成は簡単です。関数 Kakko() の引数 x が左カッコの個数、引数 y が右カッコの個数を表します。引数 a は累積変数でカッコ列を表す文字列です。

バランスの取れたカッコ列の場合、x, y, n には y <= x <= n の関係が成り立ちます。x == y == n の場合、カッコ列が一つ完成しました。WriteLine() でカッコ列 a を表示します。。そうでなければ、Kakko() を再帰呼び出しします。x < n であれば左カッコを追加し、y < x であれば右カッコを追加します。これでカッコ列を生成することができます。

$ dotnet script kakko.csx
((()))
(()())
(())()
()(())
()()()
(((())))
((()()))
((())())
((()))()
(()(()))
(()()())
(()())()
(())(())
(())()()
()((()))
()(()())
()(())()
()()(())
()()()()

●カッコ列の総数 (カタラン数)

カタラン数 - Wikipedia によると、カッコ列の総数は「カタラン数 (Catalan number)」になるとのことです。カタラン数は次に示す公式で求めることができます。

\( \mathrm{C}_n = \dfrac {(2n)!}{(n+1)!n!} \)

これをそのままプログラムしてもいいのですが、それではちょっと面白くないので別な方法でプログラムを作ってみましょう。カタラン数は次に示す経路図において、A から B までの最短距離の道順を求めるとき、対角線を超えないものの総数に一致します。

                    B                      B  
  ┌─┬─┬─┬─┐      ┌─┬─┬─0─14    
  │  │  │  │  │      │  │  │  │  │    
  ├─┼─┼─┼─┤      ├─┼─0─5─14    
  │  │  │  │  │      │  │  │  │  │    
  ├─┼─┼─┼─┤      ├─0─2─5─9    
  │  │  │  │  │      │  │  │  │  │    
  ├─┼─┼─┼─┤      0─1─2─3─4    
  │  │  │  │  │      │  │  │  │  │    
  └─┴─┴─┴─┘      1─1─1─1─1    
A                      A                      

            図 : 道順の総数の求め方

A からある地点にいたる最短距離の道順の総数は、左隣と真下の地点の値を足したものになります。一番下の地点は 1 で、対角線を越えた地点は 0 になります。あとは下から順番に足し算していけば、A から B までの道順の総数を求めることができます。上図の場合はカラタン数 \(\mathrm{C}_4\) に相当し、その値は 14 となります。

プログラムは配列を使うと簡単です。次の図を見てください。

0 : [1, 1, 1, 1, 1]

1 : [1, 1, 1, 1, 1,]

2 : [1, 1, 1+1=2, 2+1=3, 3+1=4]
 => [1, 1, 2, 3, 4]

3 : [1, 1, 2, 3+2=5, 5+4=9]
 => [1, 1, 2, 5, 9]

4 : [1, 1, 2, 5, 5+9=14]
 => [1, 1, 2, 5, 14]

上図は \(\mathrm{C}_n\) (n = 4) を求める場合です。大きさが n + 1, 要素の値が 1 のベクタを用意します。n = 0, 1 の場合は n 番目の要素をそのまま返します。n が 2 よりも大きい場合、変数 i を 2 に初期化して、i - 1 番目以降の要素の累積和を求めます。

たとえば i = 2 の場合、2 番目の要素は 1 番目の要素と自分自身を加算した値 2 になります。3 番目の要素は 2 番目の要素と自分自身を足した値 3 になり、4 番目の要素は 3 + 1 = 4 になります。次に i を +1 して同じことを繰り返します。3 番目の要素は 2 + 3 = 5 になり、4 番目の要素は 5 + 4 = 9 になります。i = 4 のとき、4 番目の要素は 5 + 9 = 14 となり、C4 の値を求めることができました。

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

リスト : カッコ列の総数 (kakkonum.csx)

using System.Numerics;

BigInteger KakkoNum(int n) {
  var table = new BigInteger [n + 1];
  for (int i = 0; i <= n; i++) table[i] = 1;
  for (int i = 2; i <= n; i++) {
    for (int j = i; j <= n; j++) {
      table[j] += table[j - 1];
    }
  }
  return table[n];
}

void test() {
  for (int i = 1; i <= 10; i++)
    Console.WriteLine("{0}", KakkoNum(i));
  Console.WriteLine("{0}", KakkoNum(50));
  Console.WriteLine("{0}", KakkoNum(100));
}

// 実行
test();
$ dotnet script kakkonum.csx
1
2
5
14
42
132
429
1430
4862
16796
1978261657756160653623774456
896519947090131496687170070074100632420837521538745909320

初版 2016 年 10 月 15 日
改訂 2022 年 2 月 19 日