ショートな課題でジャムプログラミングの問題を解いてみました

第22回 Ruby/Rails勉強会@関西のセッションでやった問題を僕も解いてみました。問題はこちらです。

課題1 配列をシャッフルする

sort_byを使うのは簡単なのですが、先頭にくる要素がどれになるか、選ばれる確率がそれぞれの要素について公平ではないと思いますので、ナイーブなアルゴリズムで実装しています。mapラブ。

def shuffle(ary)
  tmpary = ary.dup
  ary.map { tmpary.slice!(rand(tmpary.size)) }
end

ary = ['A', 'B', 'C', 'D', 'E']
p shuffle(ary) # => ["D", "A", "E", "C", "B"]
p ary          # => ["A", "B", "C", "D", "E"]

selfを省略しています。少しシンプルになりました。

class Array
  def shuffle
    tmpary = dup
    map { tmpary.slice!(rand(tmpary.size)) }
  end
end

ary = ['A', 'B', 'C', 'D', 'E']
p ary.shuffle # => ["D", "A", "E", "C", "B"]
p ary         # => ["A", "B", "C", "D", "E"]

課題2 グラフを実装する

matrixライブラリを使おうか迷った末に、普通に配列で実装しました。この問題では入力ファイルを全部読み込むまで配列のサイズは決定できません。いびつな配列にしながら計算を進めて最後に0で埋める方法もありますが、ここでは以下では2パスで配列のサイズを先に求めています。
入力ファイル

1 - 2 - 3 - 4 - 5 - 1
1 - 6 - 3
5 - 6
2 - 7	
require 'pp'

def parse(lines)
  lines.inject([]) do |a, line|
    ns = line.strip.split(/\s*-\s*/).map {|s| s.to_i }
    a += (0 .. ns.size - 2).map {|i| [ns[i], ns[i + 1]] }
  end
end

def mkgraph(conns)
  size = conns.map {|ary| ary.max }.max + 1
  Array.new(size) { Array.new(size, 0) }
end

def setconns(graph, conns)
  conns.each {|from, to| graph[from][to] = graph[to][from] = 1 }
end

conns = parse(ARGF.readlines)
graph = mkgraph(conns)
setconns(graph, conns)

pp graph
# => [[0, 0, 0, 0, 0, 0, 0, 0],
#     [0, 0, 1, 0, 0, 1, 1, 0],
#     [0, 1, 0, 1, 0, 0, 0, 1],
#     [0, 0, 1, 0, 1, 0, 1, 0],
#     [0, 0, 0, 1, 0, 1, 0, 0],
#     [0, 1, 0, 0, 1, 0, 1, 0],
#     [0, 1, 0, 1, 0, 1, 0, 0],
#     [0, 0, 1, 0, 0, 0, 0, 0]]

課題3 アナグラム作り支援プログラム

アナグラムの文字をArrayにするかStringにするか悩みましたが、以下ではArrayで実装しています。それ以外に特筆すべきとことはあまりないです。副作用ばりばりでバグりそうな怖いプログラムです。

require 'readline'

class Array
  def shuffle
    tmpary = dup
    map { tmpary.slice!(rand(tmpary.size)) }
  end
end

def prompt(rests, fixed)
  puts "rests:`#{rests.join}' -> fixed:`#{fixed.join}'"
end

rests = 'NFLNUOEOIBYR'.split(//)
fixed = []
prompt(rests, fixed)
while buf = Readline.readline('> ', false)
  break if buf == 'BYE'
  buf.gsub(/\s/, '').split(//).each do |s|
    pos = rests.index(s) 
    if pos
      fixed += rests.slice!(pos, 1) 
      if rests.empty?
        puts 'おめでとう!'
        break 
      end
    else
      puts "不正な文字です。`#{s}'"
    end
  end
  prompt(rests, fixed)
end
puts fixed.join + rests.shuffle.join

課題4 しりとりゲーム

関数型言語っぽいデータの変換を繰り返すアプローチで解いてみました。Enumerableの高階関数ラブ。
入力ファイル

ねずみ,うし,とら,うさぎ,りゅう,へび,うま
ひつじ,さる,とり,いぬ,いのしし,すずめ
はと,からす,うぐいす,めじろ,つばめ,かっこう
ほととぎす,にわとり,ちゃぼ,かるがも,きじ
だちょう,くじゃく,しちめんちょう,がちょう
はくちょう,わし,たか,とんび,かもめ,ふくろう
みみずく,きつつき,うずら,しじゅうから,つぐみ
ひよどり,きつね,たぬき,ねこ,ろば
かえる,かっぱ,とかげ,やもり,いもり,しか
りす,いたち,もぐら,こうもり,かもしか
むささび,ももんが,こあら,らっこ,ぞう,かば
らくだ,さい,おおかみ,くじら,いるか,しゃち
おっとせい,せいうち,あざらし
require 'csv'

def read_words(input)
  CSV::IOReader.new(ARGF).inject([]) do |ws, row|
    ws + row.reject {|w| w.nil? }.map {|w| w.split(//) }
  end
end

def shiritori(ret, acc, words)
  nexts = words.select {|word| word.first == acc.last.last }
  if nexts.empty?
    ret << acc
  else
    nexts.each {|word| shiritori(ret, acc + [word], words - [word]) }
  end
  ret
end

words = read_words(ARGF)

seqs = words.inject([]) do |ss, start|
  ss + shiritori([], [start], words - [start])
end

max, ans = seqs.inject([0, []]) do |(max, ans), seq|
  case seq.size <=> max
  when 0 then [max, ans + [seq]]
  when 1 then [seq.size, [seq]]
  else        [max, ans]
  end
end

ans.each do |seq|
  puts seq.map{|word| word.join }.join(' - ')
end
# => たぬき - きつつき - きつね - ねずみ - みみずく - くじゃく - くじら - らっこ - こうもり - りゅう - うし - しか - かもしか - かるがも - ももんが - がちょう - うずら - らくだ - だちょう - うぐいす - すずめ - めじろ - ろば
#    たぬき - きつつき - きつね - ねずみ - みみずく - くじゃく - くじら - らっこ - こうもり - りゅう - うずら - らくだ - だちょう - うし - しか - かもしか - かるがも - ももんが - がちょう - うぐいす - すずめ - めじろ - ろば
#    たぬき - きつつき - きつね - ねずみ - みみずく - くじゃく - くじら - らくだ - だちょう - うし - しか - かもしか - かるがも - ももんが - がちょう - うずら - らっこ - こうもり - りゅう - うぐいす - すずめ - めじろ - ろば
#    たぬき - きつつき - きつね - ねずみ - みみずく - くじゃく - くじら - らくだ - だちょう - うずら - らっこ - こうもり - りゅう - うし - しか - かもしか - かるがも - ももんが - がちょう - うぐいす - すずめ - めじろ - ろば

参照: 日本Rubyの会 公式Wiki - KansaiWorkshop22