多重ループを避ける方法(深さ優先探索)【Rubyで覆面算】

2023/03/04(土)修整:Problem#solve_dfsProblem#dfsを高速化

2023/03/06(月)修整:Questionクラスをリファクタリング

目次

多重ループを避ける方法

Rubyで多重ループを避ける方法には、Array#productを使う方法があります。 しかし、この方法では不要な計算をスキップすることができません。

そこでおすすめするのが深さ優先探索(バックトラック法, DFS)です。 この方法では、ループをネストする代わりに再帰関数を呼び出すことで多重ループを避けることができます。

この記事では、覆面算を解くプログラムを例に長大な10重ループを小さな再帰関数に書き換える様子をお見せします。

覆面算とは

覆面算とは数字の代わりにアルファベットが書かれた筆算の式が与えられ、そのアルファベットに当てはまる数字を当てるというものです。 有名な例は、以下になります。

   SEND
+) MORE
--------
  MONEY

ちなみに、答えは以下のようになります。

   9567
+) 1085
--------
  10652

覆面算の解き方

覆面算では、問題がアルファベットの式で与えられます。 それぞれのアルファベットには、数字の0~9が当てはまります。 これを総当たりで解こうとすると、各アルファベットごとに0~9の数字をそれぞれ試すことになります。 そのため、使われているアルファベットの数だけのループ処理が必要になります。 例題のSEND+MORE=MONEYでは8つのアルファベットが使われているので0~9の繰り返しが8重必要になります。

実際には、異なるアルファベット同士で同じ数字になることはないため、そのときには繰り返しをスキップ(next)できます。 また、最大10重ループ必要ですが8重ループで済むときには、9重ループ目以降はループを飛ばす(break)ことができます。

10重ループと深さ優先探索の比較

まず、覆面算を解くプログラム中の10重ループを抜き出したコードが以下になります。 各ループ中では、特定の条件に合致したらループを飛ばしたり(break)、次の繰り返しにスキップ(next)したりして、ループ数を減らしています。

  def solve
    (0...10).each do |x0|
      next if @table[0][:head?] and x0 == 0
      @table[0][:num] = x0
      (0...10).each do |x1|
        next if x0 == x1
        next if @table[1][:head?] and x1 == 0
        @table[1][:num] = x1
        (0...10).each do |x2|
          next if [x0, x1].include?(x2)
          next if @table[2][:head?] and x2 == 0
          @table[2][:num] = x2
          answer if @table.size == 3
          (0...10).each do |x3|
            break if @table.size == 3
            next if [x0, x1, x2].include?(x3)
            next if @table[3][:head?] and x3 == 0
            @table[3][:num] = x3
            answer if @table.size == 4
            (0...10).each do |x4|
              break if @table.size == 4
              next if [x0, x1, x2, x3].include?(x4)
              next if @table[4][:head?] and x4 == 0
              @table[4][:num] = x4
              answer if @table.size == 5
              (0...10).each do |x5|
                break if @table.size == 5
                next if [x0, x1, x2, x3, x4].include?(x5)
                next if @table[5][:head?] and x5 == 0
                @table[5][:num] = x5
                answer if @table.size == 6
                (0...10).each do |x6|
                  break if @table.size == 6
                  next if [x0, x1, x2, x3, x4, x5].include?(x6)
                  next if @table[6][:head?] and x6 == 0
                  @table[6][:num] = x6
                  answer if @table.size == 7
                  (0...10).each do |x7|
                    break if @table.size == 7
                    next if [x0, x1, x2, x3, x4, x5, x6].include?(x7)
                    next if @table[7][:head?] and x7 == 0
                    @table[7][:num] = x7
                    answer if @table.size == 8
                    (0...10).each do |x8|
                      break if @table.size == 8
                      next if [x0, x1, x2, x3, x4, x5, x6, x7].include?(x8)
                      next if @table[8][:head?] and x8 == 0
                      @table[8][:num] = x8
                      answer if @table.size == 9
                      (0...10).each do |x9|
                        break if @table.size == 9
                        next if [x0, x1, x2, x3, x4, x5, x6, x7, x8].include?(x9)
                        next if @table[9][:head?] and x9 == 0
                        @table[9][:num] = x9
                        answer
                      end
                    end
                  end
                end
              end
            end
          end
        end
      end
    end
  end

そして、上のコードを深さ優先探索で書き換えたものが以下になります。 66行のコードがわずか17行にまで減りました。

  def solve_dfs
    xs = (0...10).to_a
    xs.each do |x|
      dfs(0, x, xs)
    end
  end

  def dfs(i, x, xs)
    return if @table[i][:head?] and x == 0
    @table[i][:num] = x
    return answer if i + 1 == @table.size
    ys = xs - [x]
    ys.each do |y|
      dfs(i + 1, y, ys)
    end
  end

※覆面算を解くプログラムのソースコード全体が知りたい方は、下にソースコードを貼っておくので参考にしてみてください。

dfsでは繰り返さない場合には、早期にreturnしています。 dfsの引数xsには配列を渡しています。配列の中の各値xs[i]は、@table[i][:num]に代入しています。 xsには各繰り返しで試しているアルファベットの候補の数字が入っています。

dfsの最後では、0~9の値でループしてdfs自身を再起的に呼び出しています。 dfs再帰の様子を下の図にしました。各dfsでは10回dfsを呼び出しており、その呼び出されたdfsの中でも10回呼び出しています。

dfs関数の呼び出しグラフ
深さ優先探索

覆面算を解くプログラムのソースコード全文

class Object
  def deep_clone
    Marshal.load(Marshal.dump(self))
  end
end

class Question
  REG = /([a-z]+)([+*])([a-z]+)=([a-z]+)/

  def initialize(question)
    m = REG.match(question)
    @expr0 = m[0]
    @expr1 = m[1]
    @op = m[2]
    @expr2 = m[3]
    @expr3 = m[4]
    @heads = [@expr1[0], @expr2[0], @expr3[0]].uniq
  end

  def to_s
    @expr0
  end

  def head?(c)
    @heads.include?(c)
  end

  def answer?(table)
    expr1 = expr_str_to_num(@expr1, table)
    expr2 = expr_str_to_num(@expr2, table)
    expr3 = expr_str_to_num(@expr3, table)
    op_lambda.call(expr1, expr2) == expr3
  end

  def display(table)
    printf "\n%15d\n", expr_str_to_num(@expr1, table)
    print "#{@op})"
    printf "%13d\n", expr_str_to_num(@expr2, table)
    puts "-----------------"
    printf "%15d\n", expr_str_to_num(@expr3, table)
  end

  private

  def expr_str_to_num(expr_str, table)
    expr_str
      .each_char
      .map { |c|
        table
          .find { |letter| letter[:name] == c }
          .fetch(:num)
      }
      .join("")
      .to_i
  end

  def op_lambda
    case @op
    when "+" then ->(x, y) { x + y }
    when "*" then ->(x, y) { x * y }
    end
  end

  public

  class << self
    def valid?(question)
      max_ten_char = question
                      .each_char
                      .select {|c| /[a-z]/.match?(c) }
                      .uniq
                      .size <= 10
      REG.match?(question) and max_ten_char
    end
  end
end

class Problem
  def initialize(question)
    @question = Question.new(question)
    @table = Problem.make_table(@question)
    @answers = []
  end

  def solve_dfs
    xs = (0...10).to_a
    xs.each do |x|
      dfs(0, x, xs)
    end
  end

  def dfs(i, x, xs)
    return if @table[i][:head?] and x == 0
    @table[i][:num] = x
    return answer if i + 1 == @table.size
    ys = xs - [x]
    ys.each do |y|
      dfs(i + 1, y, ys)
    end
  end

  def solve
    (0...10).each do |x0|
      next if @table[0][:head?] and x0 == 0
      @table[0][:num] = x0
      (0...10).each do |x1|
        next if x0 == x1
        next if @table[1][:head?] and x1 == 0
        @table[1][:num] = x1
        (0...10).each do |x2|
          next if [x0, x1].include?(x2)
          next if @table[2][:head?] and x2 == 0
          @table[2][:num] = x2
          answer if @table.size == 3
          (0...10).each do |x3|
            break if @table.size == 3
            next if [x0, x1, x2].include?(x3)
            next if @table[3][:head?] and x3 == 0
            @table[3][:num] = x3
            answer if @table.size == 4
            (0...10).each do |x4|
              break if @table.size == 4
              next if [x0, x1, x2, x3].include?(x4)
              next if @table[4][:head?] and x4 == 0
              @table[4][:num] = x4
              answer if @table.size == 5
              (0...10).each do |x5|
                break if @table.size == 5
                next if [x0, x1, x2, x3, x4].include?(x5)
                next if @table[5][:head?] and x5 == 0
                @table[5][:num] = x5
                answer if @table.size == 6
                (0...10).each do |x6|
                  break if @table.size == 6
                  next if [x0, x1, x2, x3, x4, x5].include?(x6)
                  next if @table[6][:head?] and x6 == 0
                  @table[6][:num] = x6
                  answer if @table.size == 7
                  (0...10).each do |x7|
                    break if @table.size == 7
                    next if [x0, x1, x2, x3, x4, x5, x6].include?(x7)
                    next if @table[7][:head?] and x7 == 0
                    @table[7][:num] = x7
                    answer if @table.size == 8
                    (0...10).each do |x8|
                      break if @table.size == 8
                      next if [x0, x1, x2, x3, x4, x5, x6, x7].include?(x8)
                      next if @table[8][:head?] and x8 == 0
                      @table[8][:num] = x8
                      answer if @table.size == 9
                      (0...10).each do |x9|
                        break if @table.size == 9
                        next if [x0, x1, x2, x3, x4, x5, x6, x7, x8].include?(x9)
                        next if @table[9][:head?] and x9 == 0
                        @table[9][:num] = x9
                        answer
                      end
                    end
                  end
                end
              end
            end
          end
        end
      end
    end
  end

  def display_answers
    @answers.each_with_index do |table, i|
      printf "\n正解【%2d】\n", i + 1

      table.each_with_index do |letter, j|
        printf "%10c = %d", letter[:name], letter[:num]
        print "\n" if j % 2 == 1
      end

      @question.display(table)
    end
  end

  private
  
  def answer
    @answers << @table.deep_clone if @question.answer?(@table)
  end

  class << self
    def make_problem
      loop do
        puts "↓の例のように文字を入力して下さい。"
        puts "例:send+more=money"
        puts "例:send*sp=money"
        print "> "
        question = gets.chomp.downcase

        return Problem.new(question) if Question.valid?(question)
      end
    end

    def make_table(question)
      question
        .to_s
        .each_char
        .select {|c| /[a-z]/.match?(c) }
        .uniq
        .sort
        .map {|c| {name: c, num: 0, head?: question.head?(c)} }
    end
  end
end

def main
  loop do
    p = Problem.make_problem
    
    start_time = Time.now
    # p.solve
    p.solve_dfs
    end_time = Time.now
    p.display_answers
    
    printf "\n計測時間:%.1f秒\n", (end_time - start_time)

    print "もう一度繰り返しますか?はい…(0)/いいえ…(1):"
    break if gets.to_i == 1
  end
end

if __FILE__ == $PROGRAM_NAME then
  main
end

おわりに

どうでしたか。深さ優先探索を用いることで多重ループを避ける方法を紹介しました。 ソースコードは、短くなりますが基本的には全探索に近いことをやっているので多重ループの時と計算量は変わりません。 SEND+MORE=MONEYの場合 {10}^{8} 回の繰り返しが必要になります。

計算量を抑えることはできませんが、ソースコードを短くできるので皆さんもぜひ多重ループを避けるときには、深さ優先探索を試してみてください。

ちなみに、この覆面算を解くプログラムは昔C言語で書いたことがあって、それをRubyに移植したものになります。 最後にgcc -O3コンパイルしたものとの実行速度の比較を下に載せておきます。

言語 実行時間
C言語 0.2秒
Ruby 25.9秒

C言語Rubyより実に129.5倍高速でした。

参考

cutでは列の入れ替えはできないので代替のRubyスクリプトを紹介

cutコマンドには-fオプションがあり-d区切りでスプリットした列の中から特定の列だけ抜き出すことができる。 しかし、列を入れ替えることはできない。

そこでRubyで入れ替えOKで好きな列を抜き出すスクリプトを書いたので紹介する。

require 'optparse'
require 'ostruct'

# コマンド使用例
# $ cat input.txt | ruby cut.rb -d'|' -f2,1,3

options = OpenStruct.new
OptionParser.new do |opt|
  opt.on('-d', '--delimiter DELIMITER') { |o| options.delimiter = o }
  opt.on('-f', '--fields FIELD_STRING') { |o| options.field = o }
end.parse!

class Array
  def fields(indices)
    indices.map {|i| self[i] }
  end
end

indices = options.field.split(",").map(&:to_i)
while gets
  puts $_.split(options.delimiter).fields(indices).join(options.delimiter)
end

下の記事の中の表データを整形するときに上のコードを書いた。

add20.hatenablog.jp

初めの状態の表データは、以下。 |区切りで「カード・セット名」、「発売日」、「スタンダード期限」の3つの列がある。

|ファイレクシア:完全なる統一(PHYREXIA ALL WILL BE ONE)|2023/2/3|2024年秋まで|
|兄弟戦争(THE BROTHERS WAR)|2022/11/18|2024年秋まで|
|団結のドミナリア(DOMINARIA UNITED)|2022/9/9|2024年秋まで|
|ニューカペナの街角(STREETS OF NEW CAPENNA)|2022/4/29|2023年秋まで|
|神河:輝ける世界(KAMiGAWA NEON DYNASTY)|2022/2/18|2023年秋まで|
|イニストラード:真紅の契り|2021/11/19|2023年秋まで|
|イニストラード:真夜中の狩り|2021/9/24|2023年秋まで|
|フォーゴトン・レルム探訪|2021/7/23|2022年秋まで|
|ストリクスヘイヴン:魔法学院|2021/4/23|2022年秋まで|
|カルドハイム|2021/2/5|2022年秋まで|
|ゼンディカーの夜明け|2020/9/25|2022年秋まで|
|基本セット2021|2020/7/3|2021年秋まで|
|イコリア:巨獣の棲処|2020/4/17|2021年秋まで|
|テーロス還魂記|2020/1/24|2021年秋まで|
|エルドレインの王権|2019/10/4|2021年秋まで|
|基本セット2020|2019/7/12|2020年秋まで|
|灯争大戦|2019/5/3|2020年秋まで|
|ラヴニカの献身|2019/1/25|2020年秋まで|
|ラヴニカのギルド|2018/10/5|2020年秋まで|

この表データの「発売日」の列を1列目に持ってくるようにするのに、もう一つスクリプトを書いてそれらを組み合わせた。

そのスクリプトの内容は、各行の先頭と末尾に文字列を追加するだけのコードです。 cut.rbを実行すると先頭と末尾の|は取り除かれてしまうので、それを追加するだけのコードです。

require 'optparse'
require 'ostruct'

options = OpenStruct.new
OptionParser.new do |opt|
  opt.on('-q', '--quote QUOTE_STRING') { |o| options.quote = o }
  opt.on('-o', '--open OPEN_STRING') { |o| options.open = o }
  opt.on('-c', '--close CLOSE_STRING') { |o| options.close = o }
end.parse!

while gets
  line = (options.open || options.quote) + $_.chomp + (options.close || options.quote)
  puts line
end

実行結果

src/mtg % cat matrix2.txt | ruby cut.rb -d'|' -f2,1,3 | ruby quote.rb -q'|'
|2023/2/3|ファイレクシア:完全なる統一(PHYREXIA ALL WILL BE ONE)|2024年秋まで|
|2022/11/18|兄弟戦争(THE BROTHERS WAR)|2024年秋まで|
|2022/9/9|団結のドミナリア(DOMINARIA UNITED)|2024年秋まで|
|2022/4/29|ニューカペナの街角(STREETS OF NEW CAPENNA)|2023年秋まで|
|2022/2/18|神河:輝ける世界(KAMiGAWA NEON DYNASTY)|2023年秋まで|
|2021/11/19|イニストラード:真紅の契り|2023年秋まで|
|2021/9/24|イニストラード:真夜中の狩り|2023年秋まで|
|2021/7/23|フォーゴトン・レルム探訪|2022年秋まで|
|2021/4/23|ストリクスヘイヴン:魔法学院|2022年秋まで|
|2021/2/5|カルドハイム|2022年秋まで|
|2020/9/25|ゼンディカーの夜明け|2022年秋まで|
|2020/7/3|基本セット2021|2021年秋まで|
|2020/4/17|イコリア:巨獣の棲処|2021年秋まで|
|2020/1/24|テーロス還魂記|2021年秋まで|
|2019/10/4|エルドレインの王権|2021年秋まで|
|2019/7/12|基本セット2020|2020年秋まで|
|2019/5/3|灯争大戦|2020年秋まで|
|2019/1/25|ラヴニカの献身|2020年秋まで|
|2018/10/5|ラヴニカのギルド|2020年秋まで|

参考

MTGをしばらくお休みしようと思う(ファイレクシア:完全なる統一)

MTGを始めたきっかけ

MTGを始めたのはイコリア:巨獣の棲処が発売される少し前の2020年4月頃。

その頃、ふとMTGがどんなゲームか気になり公式ホームページのスタートガイドを見てゲームのルールを覚え始めた。 そこでウェルカムデッキの存在を知り、マジック公認店を調べたりしていた。 イコリアが発売された頃、イコリアのカードリストを眺めてどんなデッキを作るかを考えてnumbersにまとめていた。 また、PCゲームのMTG ARENAがあるのを知り早速始めてみた。当時は、Mac版はまだ開発されていなかったので、Boot CampMacWindowsを入れて遊んでいた。

その後、ウェルカムデッキの入手とデッキパーツを集めに秋葉原に買いに行った。 ウェルカムデッキは一人で対戦をシミュレートできるように2つ手に入れた。 カードショップを何店舗か回って最後に晴れる屋に行きそこで初心者講習を受けて「チャレンジャーデッキ2020(行軍の猛攻)」とサプライを購入して帰ってきた。 しかし、紙のカードは集め始めると置く場所がなくなるだろうなと思ったのでその後買い足すことはなかった。じゃあ、なんで買ったんだ。

MTG ARENAへの課金

始めた頃は、とにかく知らないカードだらけでよく現行のスタンダードのカードリストを眺めてどんなカードが存在するのか確認していた。 (スタンダードのカード・セットの一覧は以下の表にまとめた) また、YouTubeMTG関連の動画もよく見ていた。

MTG ARENAは最初は無課金で配布デッキを使って遊んでいたが、一回だけ課金してみた。 初めは、試しにウェルカム・セットと単価の安い20,000ジェムを購入してみた。それが2020年4月29日のこと。 課金履歴はnumbersに記録しているのだが、最初の課金後しばらくMTGから離れていたことがある。 それから、確かカルドハイムの頃には復帰していて次に課金したのは、ストリクスヘイヴン:魔法学院の頃数日のうちに20,000ジェムを2回購入している。 この辺りから課金するのが当たり前になってきている。 その後、フォーゴトン・レルム探訪が発売される直前にプレオーダーセットを初めて購入した。

発売日 カード・セット名 スタンダード期限
2023/2/3 ファイレクシア:完全なる統一(PHYREXIA ALL WILL BE ONE) 2024年秋まで
2022/11/18 兄弟戦争(THE BROTHERS WAR) 2024年秋まで
2022/9/9 団結のドミナリア(DOMINARIA UNITED) 2024年秋まで
2022/4/29 ニューカペナの街角(STREETS OF NEW CAPENNA) 2023年秋まで
2022/2/18 神河:輝ける世界(KAMiGAWA NEON DYNASTY) 2023年秋まで
2021/11/19 イニストラード:真紅の契り 2023年秋まで
2021/9/24 イニストラード:真夜中の狩り 2023年秋まで
2021/7/23 フォーゴトン・レルム探訪 2022年秋まで
2021/4/23 ストリクスヘイヴン:魔法学院 2022年秋まで
2021/2/5 カルドハイム 2022年秋まで
2020/9/25 ゼンディカーの夜明け 2022年秋まで
2020/7/3 基本セット2021 2021年秋まで
2020/4/17 イコリア:巨獣の棲処 2021年秋まで
2020/1/24 テーロス還魂記 2021年秋まで
2019/10/4 エルドレインの王権 2021年秋まで
2019/7/12 基本セット2020 2020年秋まで
2019/5/3 灯争大戦 2020年秋まで
2019/1/25 ラヴニカの献身 2020年秋まで
2018/10/5 ラヴニカのギルド 2020年秋まで

MTGを止めることに

新しいカード・セットのプレビュー期間が始まると、どんなカードが発表されるかワクワクしながらYouTubeTwitterなどで新情報をチェックしていた。 そして、毎日YouTubeでデッキ紹介動画を見漁った。デッキビルダー達のコンボを見るのが楽しかった。

また、新しいカード・セットが発売されるとUntapped.ggで新デッキのレシピをコピーしてはランク戦で試したりしていた。 オリジナルのデッキを考えるのも楽しかったが、強いデッキを作れることは稀だった。

集めたコインは土地のフルアート版カード・スタイルに消えることが多かったが、後にヒストリックアンソロジーエクスプローラーアンソロジーのために25,000ゴールドを貯めておく知恵を知り、そのために貯めておくようになる。

ウィークリークエストを取りこぼさないように少なくとも数日に一回MTG ARENAをプレイする日々だったが、そのうちウィークリークエストを取りこぼすことが増えていった。 最終的には、1ヶ月以上遊ばないことも増えプレイしないならお金の無駄だからと思い今回ファイレクシア:完全なる統一は見送り、一旦課金を止めようと思う。

ただ、前回課金分の19,270ジェムがまだ残っているのでそのうち気になるカード・セットが出た時に一時的に復帰することがあるかもしれない。 また、MTGをプレイすることは一旦やめることにしたが、情報は時々追って行こうと思っている。

MTGの思い出

「ネスロイ」がカッコ良くてネスロイデッキを作ってた。

死の頂点、ネスロイ

あとは、「ヴォリンクレックス」も強くて使いやすくてよく使ってた。それにカッコ良い。「激情の共感者」で「ヴォリンクレックス」をサーチして出すだけでもデッキが安定した。

巨怪な略奪者、ヴォリンクレックス

激情の共感者

「ブロコス」も使い勝手が良くてよく使ってた。

永遠の頂点、ブロコス

コンボデッキ

勝率は良くないがコンボが決まると爽快なので一応紹介しておく。

本当は何百点もライフを削った時があったんだけど、その時のはスクショ撮り忘れててない。 デッキレシピを下に載せておく。

女王スズメバチと孔蹄のビヒモスデッキ
女王スズメバチと孔蹄のビヒモスデッキ

キーカードを下に載せておく。

女王スズメバチ

孔蹄のビヒモス

深海住まいのタッサ

ユダヤの法則(78 : 22の法則)のHaskellコード

ユダヤの法則とは

ユダヤの法則は、世の中はすべて78:22の割合で成り立っているという考え方です。

また、「78 : 22の法則」とも言います。

例えば、以下のような事実があります。

①地球の海と陸地の割合は、海の78%に対して陸地が22% ②空気中の成分の割合も窒素が78%に対して酸素が22% 『78:22の法則』から学ぼう | 今週の朝礼

ユダヤの法則で考える

大切なことは、人間にとって初めてやることは、勉強にしろ、スポーツにしろ、なんであれ「どんなに頑張っても78%しか達成できない」ということを自覚し、この不完全の22%を次にどう活かすかです。そして、次にやるときは、この22%を100と考えて、最大限の努力をしていく。そうすると1回目では最大限78%の達成度が、2回目では78%+22%×0.78=95%へとレベルアップしたことになり、これを何回も何回も繰り返すことで、限りなく100%に近づいていくことになります。どんな分野でも一流と呼ばれる人は、自分の最大限の努力を何回も何回も繰り返しているのです。『78:22の法則』から学ぼう | 今週の朝礼

Haskellコード

上の考えをHaskellのコードで表現すると以下のようになる。

import           Data.Tuple  (uncurry)
import           Text.Printf (printf)

judeas :: [Double]
judeas = let xs = iterate (* 0.22) (100 * 0.78)
         in scanl1 (+) xs

main :: IO ()
main = do
    mapM_ (uncurry $ printf "%d\t%.2f\n") $
        zip [1::Int ..] $ take 5 judeas

実行結果は以下のようになる。

src/haskell % stack runghc judea.hs
1  78.00
2  95.16
3  98.94
4  99.77
5  99.95

ソースコード解説

judeasのコードを解説する。

1回目では78% = (100 * 0.78)

2回目では95% = (100 * 0.78) + (100 * 0.78 * 0.22)

3回目では98% = (100 * 0.78) + (100 * 0.78 * 0.22) + (100 * 0.78 * 0.22 * 0.22)

...

となっている。ここでiterate (* 0.22) (100 * 0.78)について考える。

なお、iterate f x[x, f x, f (f x), ...]というリストを生成する。したがって、iterate (* 0.22) (100 * 0.78)の結果は以下のようなリストになる。

[ 100 * 0.78
, 100 * 0.78 * 0.22
, 100 * 0.78 * 0.22 * 0.22
, ...
]

したがって、上で作った各リストをscanl1 (+) xsで畳み込んだ結果が求めるリストになる。

なお、scanl1foldl1 f xsで畳み込んだ時の各繰り返しごとの結果の値をリストにして返す関数です。

参考

帰納法は健全な証明じゃない

帰納法とは

帰納法とは、実験結果などの事実から一般的な法則を推論することです。

例えば、カラスAは黒い。カラスBも黒い。...カラスZも黒い。という事実から「全てのカラスは黒い」と結論づける推論は、帰納法になります。

帰納法の問題点

しかし、A~Zまでのカラスを調べただけで、全てのカラスを調べたわけじゃないので、黒くないカラスが現実にはいるかもしれないし、今いなくても過去にいたかもしれず、未来に現れるかもしれません。その場合、A~Zのカラスが黒いことから「全てのカラスは黒い」と結論づけるのは、間違っていることになります。

このように、帰納法は常に正しい真理を見つけるわけではないので不健全であると言えます。そもそも帰納法は証明などではなく、推論の一種に過ぎず数学のように厳密に正しい真理を見つけるわけではありません。

「論理的推論」というWikipediaのページがあり、各推論が誰に利用されているかが書いてありました。

ja.wikipedia.org

演繹 数学者
帰納 科学者
アブダクション 歴史科学者や診断専門医や探偵

帰納法を肯定することわざ

「一事が万事」

「一事が万事」の意味は、「一つの物事から他の全てのことを推し量ることができること」です。

「一事が万事」の類語

これの類語として、「一を以て万を知る(いちをもってばんをしる)」=「一を聞いて十を知る」がある。その意味は、「物事の一部を聞いただけで全部を理解できる」です。

帰納法を戒めることわざ・四字熟語

「一事が万事」とは反対の意味のことわざとして「一斑を見て全豹を卜す(いっぱんをみてぜんぴょうをぼくす)」があります。その意味は、以下に引用します。

豹の毛皮は、黄色と黒のまだら模様で全身が覆われている。その毛皮の一部を見て、豹は全身が黄ないし黒色であると断定することをいう。物事の一部分から、全体を推し量ることの愚かさを戒める言葉。 一斑を見て全豹を卜す | 会話で使えることわざ辞典 | 情報・知識&オピニオン imidas - イミダス

「卜する」の意味は、「うらなって、よしあしを判断する」ことです。(リンク:卜する(ぼくする)の意味・使い方をわかりやすく解説 - goo国語辞書

同じ意味の熟語に「全豹一斑(ぜんぴょういっぱん)」があります。その意味は、以下に引用します。

もののごく一部を見て、全体を推測したり批評したりすることのたとえ。見識がきわめて狭いことのたとえ。▽「一斑」は豹ひょうの斑点の一つ。「全豹」は豹全体。転じて、物事の全容のこと。狭い管から豹をのぞき、見えた一つの斑点から豹全体を類推するという意。「一斑全豹いっぱんぜんぴょう」ともいう。 全豹一斑(ぜんぴょういっぱん)の意味・使い方 - 四字熟語一覧 - goo辞書

数学ジョーク

帰納法とも関連するこんな数学ジョークがある。

天文学者、物理学者、そして数学者がスコットランドを走る列車に乗っている。天文学者は窓の外を眺め、一頭の黒い羊が牧場に立っているのを見て、「なんと奇妙な。スコットランドの羊はみんな黒いのか」と言った。すると物理学者はそれに答えて「だから君たち天文学者はいいかげんだと馬鹿にされるんだ。正しくは『スコットランドには黒い羊が少なくとも一頭いる』だろう」と言う。しかし最後に数学者は「だから君たち物理学者はいいかげんだと馬鹿にされるんだ。正しくは『スコットランドに少なくとも一頭、少なくとも片側が黒く見える羊がいる』だ」と言った。 数学的なジョーク - Wikipedia

終わりに

帰納法で推論しているのにそれが正しい前提で話をする人がいるけど、すぐに反例を思いつくし「厳密には正しくないよな」と内心思うことがままある。そういう人には帰納法は蓋然的だということで「全豹一斑」という言葉をぜひ知ってほしいと思う。