M.Hiroi's Home Page

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

Yet Another C++ Problems (6)


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

●問題44

ファイルの文字数 (ファイルサイズ) と行数をカウントするプログラムを作ってください。コマンドラインでファイル名が省略された場合は標準入力を使うものとします。なお、Unix 系 OS には同等の機能を持つコマンド wc があります。

prog [input_file]

●問題45

ファイルから文字列を探索し、見つけたらその行を出力するプログラムを作ってください。コマンドラインでファイル名が省略された場合は標準入力を使うものとします。なお、Unix 系 OS には正規表現を使って文字列を検索するコマンド grep があります。

prog key [input_file]

●問題46

ファイルを読み込んで、ある特定の文字を他の文字で置換するプログラムを作ってください。コマンドラインでファイル名が省略された場合は標準入出力を使うものとします。

コマンドラインの第 1 引数が置換対象となる文字、第 2 引数が置き換える文字とします。これらの引数は文字列で指定することができ、たとえば prog abc ABC とすると、a -> A, b -> B, c -> C と置換します。引数の長さが異なる場合はエラー終了するものとします。なお、Unix 系 OS にはもっと多くの機能を持つコマンド tr があります。

prog char_set1 char_set2 [input_file [output_file]]

●問題47

ファイルを読み込んで、第 1 引数で指定した文字列を、第 2 引数で指定した文字列に置換するプログラムを作ってください。コマンドラインでファイル名が省略された場合は標準入出力を使うものとします。なお、Unix 系 OS ではコマンド sed で文字列の置換を行うことができます。もちろん、正規表現も利用することができます。

prog key_str replace_str [input_file [output_file]]

●問題48

ファイルの単語をカウントするプログラムを作ってください。単語は空白文字で区切られた文字列とします。コマンドラインでファイル名が省略された場合は標準入力を使うものとします。なお、Unix 系 OS には同等の機能を持つコマンド wc があります。

prog [input_file]

●問題49

タブを空白文字に展開するプログラムを作ってください。タブの値は 8 個とします。コマンドラインでファイル名が省略された場合は標準入出力を使うものとします。なお、Unix 系 OS には同等の機能を持つコマンド expand があります。

prog [input_file [output_file]]

●問題50

空白文字をタブに置き換えるプログラムを作ってください。タブの値は 8 個とします。コマンドラインでファイル名が省略された場合は標準入出力を使うものとします。なお、Unix 系 OS には同等の機能を持つコマンド unexpand があります。

prog [input_file [output_file]]

●問題51

自然数 n (long long int) を素因数分解する関数 factorization を定義してください。返り値は vector<Pair> で、Pair(p, q) は pq を表します。

リスト : Pair の定義

typedef tuple<long long, long long> Pair;
factorization(24)         => (2, 3) (3, 1)
factorization(12345678)   => (2, 1) (3, 2) (47, 1) (14593, 1)
factorization(123456789)  => (3, 2) (3607, 1) (3803, 1)
factorization(1234567890) => (2, 1) (3, 2) (5, 1) (3607, 1) (3803, 1)
factorization(1111111111) => (11, 1) (41, 1) (271, 1) (9091, 1)

●問題52

自然数 n の約数の個数を求める関数 divisor_num を定義してください。

divisor_num(24)         => 8
divisor_num(12345678)   => 24
divisor_num(123456789)  => 12
divisor_num(1234567890) => 48
divisor_num(1111111111) => 16

●問題53

自然数 n の約数の合計値を求める関数 divisor_sum を定義してください。

divisor_sum(24)         => 60
divisor_sum(12345678)   => 27319968
divisor_sum(123456789)  => 178422816
divisor_sum(1234567890) => 3211610688
divisor_sum(1111111111) => 1246404096

●問題54

自然数 n の約数を vector に格納して返す関数 divisor を定義してください。

divisor(24) => 1 2 3 4 6 8 12 24
divisor(12345678) =>
1 2 3 6 9 18 47 94 141 282 423 846 14593 29186 43779 87558 131337 262674 685871
 1371742 2057613 4115226 6172839 12345678
divisor(123456789) => 1 3 9 3607 3803 10821 11409 32463 34227 13717421 41152263 123456789
divisor(1234567890) =>
1 2 3 5 6 9 10 15 18 30 45 90 3607 3803 7214 7606 10821 11409 18035 19015 21642
 22818 32463 34227 36070 38030 54105 57045 64926 68454 108210 114090 162315 171135
 324630 342270 13717421 27434842 41152263 68587105 82304526 123456789 137174210
 205761315 246913578 411522630 617283945 1234567890
divisor(1111111111) =>
1 11 41 271 451 2981 9091 11111 100001 122221 372731 2463661 4100041 27100271
 101010101 1111111111

●問題55

完全数 - Wikipedia によると、『完全数 (かんぜんすう,perfect number) とは、その数自身を除く約数の和が、その数自身と等しい自然数のことである。』 とのことです。自然数 n 以下の完全数を求める関数 perfect_number を定義してください。

perfect_number(10000) => (画面に出力)
6
28
496
8128

●問題56

友愛数 - Wikipedia によると、『友愛数(ゆうあいすう)とは、異なる2つの自然数の組で、自分自身を除いた約数の和が、互いに他方と等しくなるような数をいう。』 とのことです。自然数 n 以下の友愛数を求める関数 yuuai_number を定義してください。

yuuaiNumber(100000) => (画面に出力)
220 284
1184 1210
2620 2924
5020 5564
6232 6368
10744 10856
12285 14595
17296 18416
63020 76084
66928 66992
67095 71145
69615 87633
79750 88730

●問題57

m 個の整数 1, 2, ..., m の順列を考えます。このとき、i 番目 (先頭要素が 1 番目) の要素が整数 i ではない順列を「完全順列」といいます。簡単な例を示しましょう。

1 - 3 の完全順列
2 3 1
3 1 2
1 - 4 の完全順列
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

1 から m までの整数値で完全順列を生成するプログラムを作ってください。

●問題58

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

\(\begin{array}{ll} A_1 = 0 & \\ A_2 = 1 & \\ A_n = (n - 1) \times (A_{n-1} + A_{n-2}) & \mathrm{if} \ n \geqq 3 \end{array}\)

モンモール数を求める関数 montmort_number を定義してください。

montmort_number(1) => 0
montmort_number(2) => 1
montmort_number(3) => 2
montmort_number(4) => 9
montmort_number(5) => 44
montmort_number(6) => 265
montmort_number(7) => 1854
montmort_number(10) => 1334961
montmort_number(20) => 895014631192902121

●問題59

バランスの取れた n 対のカッコ列を画面に表示する関数 kakko を定義してください。カッコ列は ( と ) からなる列のことで、バランスが取れているカッコ列は、右カッコで閉じることができる、つまり右カッコに対応する左カッコがある状態のことをいいます。たとえば n = 1 の場合、( ) はバランスの取れたカッコ列ですが、) ( はバランスが取れていません。

kakko(3) => (画面に表示)
((()))
(()())
(())()
()(())
()()()

kakko(4) => (画面に表示)
(((())))
((()()))
((())())
((()))()
(()(()))
(()()())
(()())()
(())(())
(())()()
()((()))
()(()())
()(())()
()()(())
()()()()

●問題60

バランスの取れた n 対のカッコ列の総数を求める関数 kakko_num を定義してください。

kakko_num(1)   => 1
kakko_num(2)   => 2
kakko_num(3)   => 5
kakko_num(4)   => 14
kakko_num(5)   => 42
kakko_num(6)   => 132
kakko_num(7)   => 429
kakko_num(8)   => 1430
kakko_num(9)   => 4862
kakko_num(10)  => 16796
kakko_num(30)  => 3814986502092304

●解答44

リスト : ファイルの文字数と行数

#include <iostream>
#include <fstream>
using namespace std;

void read_file(istream& fin)
{
  int line = 0;
  int size = 0;
  int c;
  while ((c = fin.get()) != EOF) {
    if (c == '\n') line++;
    size++;
  }
  cout << "line : " << line << ", size = " << size << endl;
}

int main(int argc, char* argv[])
{
  if (argc < 2) {
    read_file(cin);
  } else {
    ifstream fin(argv[1]);
    if (fin) {
      read_file(fin);
    } else {
      cerr << argv[1] << " が見つかりません" << endl;
      return 1;
    }
  }
}

行数と文字数は関数 read_file でカウントします。行数を変数 line, 文字数を変数 size でカウントします。どちらの変数も 0 で初期化します。あとは、get で 1 バイトずつ読み込み、それが改行文字ならば line を +1 して、size を +1 します。最後に、size と line を演算子 << で表示するだけです。

●解答45

リスト : 文字列の探索

#include <iostream>
#include <fstream>
#include <string>
using namespace std;

void search_key(const char* key, istream& fin)
{
  string buff;
  while (getline(fin, buff)) {
    if (buff.find(key, 0) != string::npos) {
      cout << buff << endl;
    }
  }
}

int main(int argc, char* argv[])
{
  if (argc < 2) {
    cerr << "引数が足りません\n";
    return 1;
  }
  if (argc == 2) {
    search_key(argv[1], cin);
  } else {
    ifstream fin(argv[2]);
    if (fin) {
      search_key(argv[1], fin);
    } else {
      cerr << argv[2] << " が見つかりません\n" << endl;
    }
  }
}

文字列の検索は関数 search_key で行います。search_key は getline で fin から 1 行ずつバッファ buff に読み込み、メンバ関数 find で buff から key を探します。見つかった場合は演算子 << で buff を出力します。

●解答46

リスト : 文字の置換

#include <iostream>
#include <fstream>
#include <cstring>
using namespace std;

int position(const char* buff, int c)
{
  for (int i = 0; buff[i] != '\0'; i++)
    if (buff[i] == c) return i;
  return -1;
}

void replace_char(const char* src, const char* dst, istream& fin, ostream& fout)
{
  int c;
  while ((c = fin.get()) != EOF) {
    int n = position(src, c);
    if (n >= 0) {
      fout.put(dst[n]);
    } else {
      fout.put(c);
    }
  }
}

int main(int argc, char* argv[])
{
  if (argc < 3) {
    cerr << "引数が足りません" << endl;
    return 1;
  }
  if (strlen(argv[1]) != strlen(argv[2])) {
    cerr << "引数 (文字列) の長さが異なります" << endl;
    return 1;
  }
  if (argc == 3) {
    replace_char(argv[1], argv[2], cin, cout);
  } else if (argc >= 4) {
    ifstream fin(argv[3]);
    if (!fin) {
      cerr << argv[3] << " が見つかりません" << endl;
      return 1;
    }
    if (argc == 4) {
      replace_char(argv[1], argv[2], fin, cout);
    } else {
      ofstream fout(argv[4]);
      if (!fout) {
        cerr <<  argv[4] << " をライトオープンできません" << endl;
        return 1;
      }
      replace_char(argv[1], argv[2], fin, fout);
    }
  }
}

最初に引数をチェックして、argv[1] と argv[2] の長さが異なればエラー終了します。実際の処理は関数 replace_char で行います。fin から get で 1 バイト読み込み、それが文字列 src にあるか関数 position で検索します。position は単純な線形探索です。見つかった場合は dst[n] を put で出力し、そうでなければ文字 c を put で出力します。

●解答47

リスト : 文字列の置換

#include <iostream>
#include <fstream>
#include <cstring>
using namespace std;

void replace_string(const char* s, const char* d, istream& fin, ostream& fout)
{
  string buff;
  int k = strlen(s);
  while (getline(fin, buff)) {
    int n = 0;
    while (true) {
      n = buff.find(s, n);
      if (n == string::npos) break;
      buff.replace(n, k, d);
      n += k;
    }
    fout << buff << endl;
  }
}

int main(int argc, char* argv[])
{
  if (argc < 3) {
    cerr << "引数が足りません" << endl;
    return 1;
  }
  if (argc == 3) {
    replace_string(argv[1], argv[2], cin, cout);
  } else if (argc >= 4) {
    ifstream fin(argv[3]);
    if (!fin) {
      cerr << argv[3] << " が見つかりません" << endl;
      return 1;
    }
    if (argc == 5) {
      ofstream fout(argv[4]);
      if (!fout) {
        cerr << argv[4] << " をライトオープンできません" << endl;
        return 1;
      }
      replace_string(argv[1], argv[2], fin, fout);
    } else {
      replace_string(argv[1], argv[2], fin, cout);
    }
  }
}

文字列の置換は関数 replace_string で行います。replace_string は getline で fin から 1 行ずつバッファ buff に読み込みます。変数 k は文字列 key の長さをセットします。一致する文字列をすべて置換するため、find の返り値が npos になるまで while ループで処理を繰り返します。変数 n が検索開始位置を表します。最初は buff の先頭 (0) に初期化します。

n が npos でなければ、key が見つかりました。メンバ関数 replace で、n 番目から k 個の文字を引数の文字列 d に置き換えます。そして、検索開始位置 n を n + k に更新します。key が見つからない場合は buff を fout に出力して、次の行を読み込みます。これをファイルの終端に到達するまで繰り返します。

●解答48

リスト : 単語のカウント

#include <iostream>
#include <fstream>
#include <cctype>
using namespace std;


void wc1(istream& fin)
{
  bool inword = false;
  int c, n = 0;
  while ((c = fin.get()) != EOF) {
    if (isspace(c))
      inword = false;
    else {
      if (!inword) {
        inword = true;
        n++;
      }
    }
  }
  cout << n << endl;
}

void wc(istream& fin)
{
  int count = 0;
  string buff;
  while (fin >> buff) count++;
  cout << count << endl;
}

int main(int argc, char *argv[])
{
  if (argc >= 2) {
    ifstream fin(argv[1]);
    if (!fin) {
      cerr << argv[1] << " が見つかりません" << endl;
      return 1;
    }
    wc1(fin);
  } else {
    wc1(cin);
  }
}

単語のカウントは関数 wc で行います。入力演算子 >> は空白文字を読み飛ばすので、fin >> buff で単語を読み込むことができます。あとは、ファイルの終端に到達するまで単語を読み込んで count を +1 するだけです。

関数 wc1 は別解で、入力演算子を使わずに 1 文字ずつ入力する場合です。単語を読んでいるときは、変数 inword を true にし、空白文字が現れたら inword を false にします。そして、inword を false から true に変更するとき、単語の個数 n をインクリメントします。空白文字のチェックはライブラリ (cctype) の関数 isspace で簡単に行うことができます。

int isspace(int c);

isspace は文字 c が空白文字であれば真を返します。ASCII コードの場合、isspace が真を返す空白文字には次の種類があります。

' '  : 空白     (0x20)
'\t' : 水平タブ (0x09)
'\n' : 改行     (0x0a)
'\v' : 垂直タブ (0x0b)
'\f' : 書式送り (0x0c)
'\r' : 復帰     (0x0d)

●解答49

リスト : タブの展開

#include <iostream>
#include <fstream>
using namespace std;

void detab(istream& fin, ostream& fout)
{
  int col = 0;
  int c;
  while ((c = fin.get()) != EOF) {
    if (c == '\t') {
      do {
        fout.put(' ');
        col++;
      } while (col % 8 != 0);
    } else {
      if (c == '\n')
        col = 0;
      else
        col++;
      fout.put(c);
    }
  }
}

int main(int argc, char *argv[])
{
  if (argc < 2) {
    detab(cin, cout);
  } else if (argc >= 2) {
    ifstream fin(argv[1]);
    if (!fin) {
      cerr << argv[1] << " が見つかりません";
      return 1;
    }
    if (argc >= 3) {
      ofstream fout(argv[2]);
      if (!fout) {
        cerr << argv[2] << " をライトオープンできません" << endl;
        return 1;
      }
      detab(fin, fout);
    } else {
      detab(fin, cout);
    }
  }
}

実際の処理は関数 detab で行います。変数 col が欄の位置を表します。get で読み込んだ文字 c がタブ (\t) の場合、col % 8 が 0 になるまで put で空白文字を出力します。これでタブを空白に展開することができます。c が改行文字ならば col を 0 にリセットします。そうでなければ、col をインクリメントして、文字 c を put で出力します。

●解答50

リスト : 空白をタブに置換

#include <iostream>
#include <fstream>
using namespace std;
void entab(istream& fin, ostream& fout)
{
  int col = 0;
  int c;
  while ((c = fin.get()) != EOF) {
    int sc = 0;
    // 空白を集める
    if (c == ' ') {
      do {
        sc++;
        col++;
        if (col % 8 == 0) {
          fout.put('\t');
          sc = 0;
        }
      } while ((c = fin.get()) == ' ');
      if (sc > 0) while (sc-- > 0) fout.put(' ');
    }
    if (c == '\n') {
      col = 0;
    } else {
      col++;
    }
    fout.put(c);
  }
}

int main(int argc, char *argv[])
{
  if (argc < 2) {
    entab(cin, cout);
  } else if (argc >= 2) {
    ifstream fin(argv[1]);
    if (!fin) {
      cerr << argv[1] << " が見つかりません";
      return 1;
    }
    if (argc >= 3) {
      ofstream fout(argv[2]);
      if (!fout) {
        cerr << argv[2] << " をライトオープンできません" << endl;
        return 1;
      }
      entab(fin, fout);
    } else {
      entab(fin, cout);
    }
  }
}

実際の処理は関数 entab で行います。変数 col が欄の位置を表します。get で文字を読み込み、文字 c が空白であれば、連続している空白の個数を変数 sc に求めます。このとき、col % 8 が 0 になったならば、連続している空白をタブに置換することができます。put でタブを出力して、sc の値を 0 にリセットします。

do - while ループが終了して、sc が 0 よりも大きい場合、その空白はタブに置換することができません。put で空白を sc 個だけ出力します。文字 c が空白文字でなければ、文字 c が改行文字かチェックします。そうであれば、col を 0 にリセットします。あとは、文字 c をそのまま put で出力するだけです。

●解答51

リスト : 素因数分解

typedef tuple<long long, long long> Pair;

Pair factor_sub(long long n, long long m)
{
  long long i = 0;
  while (n % m == 0) {
    i++;
    n /= m;
  }
  return make_tuple(i, n);
}

vector<Pair> factorization(long long n)
{
  vector<Pair> a;
  long long c, m;
  tie(c, m) = factor_sub(n, 2);
  if (c > 0) a.push_back(make_tuple(2, c));
  for (long long i = 3; m >= i * i; i += 2) {
    tie(c, m) = factor_sub(m, i);
    if (c > 0) a.push_back(make_tuple(i, c));
  }
  if (m > 1) a.push_back(make_tuple(m, 1));
  return a;
}

素因数分解は素数 2, 3, 5, ... で順番に割り算していけばいいのですが、いちいち素数を求めるのは大変なので、2 と 3 以上の奇数列で割り算していきます。関数 factor_sub は n を m で割り算します。このとき、m で割り切れる回数を求めます。factor_sub は m で割った回数と商を Pair に格納して返します。

次に、factor_sub を呼び出して n を 2 で割り算します。それから、for ループで奇数列を生成します。変数 i は 3 で初期化します。変数 a は結果を格納する vector です。√m < i になったら for ループを終了します。そうでなければ、factor_sub を呼び出して m を i で割り算します。奇数列には素数ではないものがありますが、その前に小さな素数で素因数分解されているので、n がその値で割り切れることはありません。

●解答52

n の素因数分解ができると、約数の個数を求めるのは簡単です。n = pa * qb * rc とすると、約数の個数は (a + 1) * (b + 1) * (c + 1) になります。たとえば、12 は 22 * 31 になるので、約数の個数は 3 * 2 = 6 になります。実際、12 の約数は 1, 2, 3, 4, 6, 12 の 6 個です。

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

リスト : 約数の個数

long long divisor_num(long long n)
{
  long long a = 1;
  for (auto& x : factorization(n)) {
    a *= get<1>(x) + 1;
  }
  return a;
}

divisor_num は範囲 for 文で vector の要素を順番に取り出し、get<1>(x) + 1 を a に掛け算していくだけです。

●解答53

n の素因数分解ができると、約数の合計値を求めるのは簡単です。n の素因数分解が pa だった場合、その約数の合計値は次の式で求めることができます。

σ(p, a) = pa + pa-1 + ... + p2 + p + 1

たとえば、8 の素因数分解は 23 になり、素数の合計値は 8 + 4 + 2 + 1 = 15 になります。

pa の約数の合計値を σ(p, a) で表すことにします。n = pa * qb * rc の場合、n の約数の合計値は σ(p, a) * σ(q, b) * σ(r, c) になります。たとえば、12 は 22 * 3 に素因数分解できますが、その合計値は (4 + 2 + 1) * (3 + 1) = 28 となります。12 の約数は 1, 2, 3, 4, 6, 12 なので、その合計値は確かに 28 になります。

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

リスト : 約数の合計値

// 累乗の計算
long long pow(long long x, long long y)
{
  if (y == 0)
    return 1;
  else if (y == 0)
    return x;
  else {
    long long z = pow(x, y / 2);
    return y % 2 == 0 ? z * z : z * z * x;
  }
}

// σ(p, n) の計算
long long div_sum_sub(long long p, long long n)
{
  long long a = 0;
  for (; n > 0; n--) a += pow(p, n);
  return a + 1;
}

// 約数の和
long long divisor_sum(long long n)
{
  long long a = 1;
  for (auto& x : factorization(n))
    a *= div_sum_sub(get<0>(x), get<1>(x));
  return a;
}

関数 div_sum_sub は σ(p, n) を計算します。あとは for ループで div_sum_sub の返り値を累積変数 a に掛け算していくだけです。

●解答54

p が素数の場合、pa の約数は次のように簡単に求めることができます。

pa, pa-1, ... p2, p, 1

n の素因数分解が pa * qb だったとすると、その約数は次のようになります。

(pa, pa-1, ... p2, p, 1) * qb,
(pa, pa-1, ... p2, p, 1) * qb-1,
        .....
(pa, pa-1, ... p2, p, 1) * q2,
(pa, pa-1, ... p2, p, 1) * q,
(pa, pa-1, ... p2, p, 1) * 1

たとえば、12 の約数は 24 = (1, 2, 4) と 3 = (1, 3) から、(1, 2, 4) * 1 と (1, 2, 4) * 3 のすべての要素 (1, 2, 4, 3, 6, 12) になります。

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

リスト : 約数をすべて求める

// p ^ q の約数を求める
vector<long long> divisor_sub(long long p, long long q)
{
  vector<long long> a;
  for (long long i = 0; i <= q; i++)
    a.push_back(pow(p, i));
  return a;
}	

// 互いの要素を掛け算する
vector<long long> product(const vector<long long>& xs,
			  const vector<long long>& ys)
{
  vector<long long> a;
  for (auto x : xs) {
    for (auto y : ys) {
      a.push_back(x * y);
    }
  }
  return a;
}

// n の約数を求める
vector<long long> divisor(long long n)
{
  auto xs = factorization(n);
  auto ys = divisor_sub(get<0>(xs[0]), get<1>(xs[0]));
  for (int i = 1; i < xs.size(); i++)
    ys = product(divisor_sub(get<0>(xs[i]), get<1>(xs[i])), ys);
  sort(ys.begin(), ys.end());
  return ys;
}

関数 divisor_sub は pn の約数を vector に格納して返します。関数 product は 2 つの vector (xs、ys) の要素を掛け合わせたものを vector に格納して返します。あとは for ループで素因数分解した結果を順番に取り出し、pq を divisor_sub で vector に変換して、それを product で累積変数 ys のスライスと掛け合わせていくだけです。

●解答55

リスト : 完全数

void perfect_number(int n)
{
  for (int x = 2; x <= n; x++) {
    if (divisor_sum(x) - x == x) 
      cout << x << endl;
  }
}

完全数を求める perfect_number は簡単です。x の約数の合計値を divisor_sum で求め、その値から x を引いた値が x と等しければ完全数です。出力演算子 << で x を表示します。

●解答56

リスト : 友愛数

void yuuai_number(int n)
{
  for (int x = 2; x <= n; x++) {
    long long m = divisor_sum(x) - x;
    if (x < m && x == divisor_sum(m) - m)
      cout << x << " " << m << endl;
  }
}

友愛数を求める yuuai_number も簡単です。divisor_sum で x の約数の合計値を求め、その値から x を引いた値を変数 m にセットします。m の約数の合計値から m を引いた値が x と等しければ、x と m は友愛数です。出力演算子 <<で x と m を表示します。同じ組を表示しないようにするため、x < m を条件に入れています。

●解答57

リスト : 完全順列

void derangement_sub(vector<int>& buff, int n = 0)
{
  if (n == buff.size()) {
    for (int x : buff) cout << x << " ";
    cout << endl;
  } else {
    int temp = buff[n];
    for (int i = n; i < buff.size(); i++) {
      if (buff[i] == n + 1) continue;
      buff[n] = buff[i];
      buff[i] = temp;
      derangement_sub(buff, n + 1);
      buff[i] = buff[n];
      buff[n] = temp;
    }
  }
}

void derangement(int m)
{
  vector<int> buff(m);
  iota(buff.begin(), buff.end(), 1);
  derangement_sub(buff);
}

実際の処理は関数 derangement_sub で行います。基本的には 1 から m までの数字を m 個選ぶ順列を生成する処理と同じです。n 番目の数字を選ぶとき、数字 i が n + 1 と等しい場合は i を選択しません。n が m と等しい場合は m 個の数字を選んだので buff の内容を表示します。これで完全順列を生成することができます。

●解答58

リスト : 完全順列の総数

long long montmort_number(int n)
{
  if (n == 1)
    return 0;
  else if (n == 2)
    return 1;
  else
    return (n - 1) * (montmort_number(n - 1) + montmort_number(n - 2));
}

// 別解
long long montmort_number1(int n)
{
  long long a = 0, b = 1;
  for (int i = 1; i < n; i++) {
    long long c = (i + 1) * (a + b);
    a = b;
    b = c;
  }
  return a;
}

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

●解答59

リスト : カッコ列の生成

void kakko_sub(int x, int y, int m, string s)
{
  if (x == y && x == m) {
    cout << s << endl;
  } else {
    if (x < m) {
      kakko_sub(x + 1, y, m, s + "(");
    }
    if (y < x) {
      kakko_sub(x, y + 1, m, s + ")");
    }
  }
}

void kakko(int m)
{
  kakko_sub(0, 0, m, "");
}

カッコ列の生成は簡単です。関数 kakko_sub の引数 x が左カッコの個数、引数 y が右カッコの個数を表します。引数 s はカッコ列を表す文字列です。

バランスの取れたカッコ列の場合、x, y, m には y <= x <= m の関係が成り立ちます。x == y && y == m の場合、カッコ列がひとつ完成しました。文字列 s を出力演算子 << で表示します。そうでなければ、kakko_sub を再帰呼び出しします。x < m であれば左カッコを追加し、y < x であれば右カッコを追加します。これでカッコ列を生成することができます。

●解答60

カタラン数 - 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 までの道順の総数を求めることができます。上図の場合はカラタン数 C4 に相当し、その値は 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]

上図は Cn (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 の値を求めることができました。

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

リスト : カッコ列の総数

long long kakko_num(int n)
{
  vector<long long> table(n + 1, 1);
  for (int i = 2; i <= n; i++) {
    for (int j = i; j <= n; j++) {
      table[j] += table[j - 1];
    }
  }
  return table[n];
}

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


●プログラムリスト

//
// yacp6.cpp : Yet Another C++ Problems (6)
//
//             Copyright (C) 2015-2023 Makoto Hiroi
//
#include <iostream>
#include <tuple>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;

// Q51
typedef tuple<long long, long long> Pair;

Pair factor_sub(long long n, long long m)
{
  long long i = 0;
  while (n % m == 0) {
    i++;
    n /= m;
  }
  return make_tuple(i, n);
}

vector<Pair> factorization(long long n)
{
  vector<Pair> a;
  long long c, m;
  tie(c, m) = factor_sub(n, 2);
  if (c > 0) a.push_back(make_tuple(2, c));
  for (long long i = 3; m >= i * i; i += 2) {
    tie(c, m) = factor_sub(m, i);
    if (c > 0) a.push_back(make_tuple(i, c));
  }
  if (m > 1) a.push_back(make_tuple(m, 1));
  return a;
}

void print_factor(const vector<Pair>& v)
{
  for (auto& x : v) {
    cout << "(" << get<0>(x) << "," << get<1>(x) << ")";
  }
  cout << endl;
}

// Q52
long long divisor_num(long long n)
{
  long long a = 1;
  for (auto& x : factorization(n)) {
    a *= get<1>(x) + 1;
  }
  return a;
}

// Q53
long long pow(long long x, long long y)
{
  if (y == 0)
    return 1;
  else if (y == 0)
    return x;
  else {
    long long z = pow(x, y / 2);
    return y % 2 == 0 ? z * z : z * z * x;
  }
}

// σ(p, n) の計算
long long div_sum_sub(long long p, long long n)
{
  long long a = 0;
  for (; n > 0; n--) a += pow(p, n);
  return a + 1;
}

// 約数の和
long long divisor_sum(long long n)
{
  long long a = 1;
  for (auto& x : factorization(n))
    a *= div_sum_sub(get<0>(x), get<1>(x));
  return a;
}

// Q54

// p ^ q の約数を求める
vector<long long> divisor_sub(long long p, long long q)
{
  vector<long long> a;
  for (long long i = 0; i <= q; i++)
    a.push_back(pow(p, i));
  return a;
}	

// 互いの要素を掛け算する
vector<long long> product(const vector<long long>& xs,
			  const vector<long long>& ys)
{
  vector<long long> a;
  for (auto x : xs) {
    for (auto y : ys) {
      a.push_back(x * y);
    }
  }
  return a;
}

//
vector<long long> divisor(long long n)
{
  auto xs = factorization(n);
  auto ys = divisor_sub(get<0>(xs[0]), get<1>(xs[0]));
  for (int i = 1; i < xs.size(); i++)
    ys = product(divisor_sub(get<0>(xs[i]), get<1>(xs[i])), ys);
  sort(ys.begin(), ys.end());
  return ys;
}

template<class T>
void print_vector(const vector<T>& v)
{
  for (auto& x : v) cout << x << " ";
  cout << endl;
}

// Q55
void perfect_number(int n)
{
  for (int x = 2; x <= n; x++) {
    if (divisor_sum(x) - x == x) 
      cout << x << endl;
  }
}

// Q56
void yuuai_number(int n)
{
  for (int x = 2; x <= n; x++) {
    long long m = divisor_sum(x) - x;
    if (x < m && x == divisor_sum(m) - m)
      cout << x << " " << m << endl;
  }
}

// Q57
void derangement_sub(vector<int>& buff, int n = 0)
{
  if (n == buff.size()) {
    for (int x : buff) cout << x << " ";
    cout << endl;
  } else {
    int temp = buff[n];
    for (int i = n; i < buff.size(); i++) {
      if (buff[i] == n + 1) continue;
      buff[n] = buff[i];
      buff[i] = temp;
      derangement_sub(buff, n + 1);
      buff[i] = buff[n];
      buff[n] = temp;
    }
  }
}

void derangement(int m)
{
  vector<int> buff(m);
  iota(buff.begin(), buff.end(), 1);
  derangement_sub(buff);
}

// Q58
long long montmort_number(int n)
{
  if (n == 1)
    return 0;
  else if (n == 2)
    return 1;
  else
    return (n - 1) * (montmort_number(n - 1) + montmort_number(n - 2));
}

// 別解
long long montmort_number1(int n)
{
  long long a = 0, b = 1;
  for (int i = 1; i < n; i++) {
    long long c = (i + 1) * (a + b);
    a = b;
    b = c;
  }
  return a;
}

// Q59
void kakko_sub(int x, int y, int m, string s)
{
  if (x == y && x == m) {
    cout << s << endl;
  } else {
    if (x < m) {
      kakko_sub(x + 1, y, m, s + "(");
    }
    if (y < x) {
      kakko_sub(x, y + 1, m, s + ")");
    }
  }
}

void kakko(int m)
{
  kakko_sub(0, 0, m, "");
}

// Q60
long long kakko_num(int n)
{
  vector<long long> table(n + 1, 1);
  for (int i = 2; i <= n; i++) {
    for (int j = i; j <= n; j++) {
      table[j] += table[j - 1];
    }
  }
  return table[n];
}

// 簡単なテスト
int main()
{
  // Q51
  print_factor(factorization(24));
  print_factor(factorization(12345678));
  print_factor(factorization(123456789));
  print_factor(factorization(1234567890));
  print_factor(factorization(1111111111));
  // Q52
  cout << divisor_num(24) << endl;
  cout << divisor_num(12345678) << endl;
  cout << divisor_num(123456789) << endl;
  cout << divisor_num(1234567890) << endl;
  cout << divisor_num(1111111111) << endl;
  // Q53
  cout << divisor_sum(24) << endl;
  cout << divisor_sum(12345678) << endl;
  cout << divisor_sum(123456789) << endl;
  cout << divisor_sum(1234567890) << endl;
  cout << divisor_sum(1111111111) << endl;
  // Q54
  print_vector(divisor(24));
  print_vector(divisor(12345678));
  print_vector(divisor(123456789));
  print_vector(divisor(1234567890));
  print_vector(divisor(1111111111));
  // Q55
  perfect_number(10000);
  // Q56
  yuuai_number(100000);
  // Q57
  derangement(3);
  derangement(4);
  derangement(5);
  // Q58
  for (int i = 1; i <= 10; i++) {
    cout << montmort_number(i) << endl;
    cout << montmort_number1(i) << endl;
  }
  cout << montmort_number(20) << endl;
  cout << montmort_number(20) << endl;
  // Q59
  kakko(3);
  kakko(4);
  // Q60
  for (int i = 1; i <= 10; i++)
    cout << kakko_num(i) << endl;
  cout << kakko_num(30) << endl;
}

初版 2015 年 11 月 7, 28 日
改訂 2023 年 4 月 15 日