CodeIQ:スムーズな電車の乗り降り

私自身が表題の問題を解いた時のプログラムについて解説します。
問題の詳細は「スムーズな電車の乗り降り」(CodeIQ)を参照してください。

問題の概要

問題を引用します。
電車には進行方向の左右にドアがあり、駅のホームの配置によってどちらかのドアが開きます。
降りる駅で、乗った側のドアと同じドアが開くのであれば、乗った側のドアのそばに立っておくと便利です。
しかし、乗った側のドアが開いたのに降りないと、他の乗客の邪魔になります。

そこで、乗ったときはそのドアのそばに立ち、以下の行動を取ることにします。
・乗った側のドアが開いた場合は、その駅で降りる
・反対側のドアが開き続けた場合は、乗った側のドアが開くまで乗り続ける

電車が片道を走行する間に、二人の乗客がそれぞれ、別々の駅から乗って別々の駅で降ります。
このとき、この二人がそれぞれ反対側のドアで上記の行動を取ることにします。

全部で n 個の駅があったとき、このような行動ができる「ホームの配置」と、「乗客の動き」の組み合わせが何通りあるかを求めてください。
(ただし、1人目は進行方向の左側のドアから、2人目は右側のドアから乗降するものとします。)

例えば、n = 4 でA駅からD駅に電車が動くとき、以下の6通りがあります。
ホームの配置(開くドア) 乗客の動き
A駅 B駅 C駅 D駅 1人目 2人目
A→B C→D
A→C B→D
A→D B→C
B→C A→D
B→D A→C
C→D A→B

しかし、以下のようなホームの配置の場合は、上記の行動ができません。
ホームの配置(開くドア) 乗客の動き
A駅 B駅 C駅 D駅 1人目 2人目
A→B C→?
B→D C→?

なお、路線は環状にはなっておらず、乗客も折り返すことなく一方向で乗降するものとします。
そのほかの条件は以下になります。
※降りた駅でそのまま再度乗ることはありません
※一度降りてから別の駅で再度乗ることはありません
※乗った駅でそのまま降りることはありません
※必ず二人とも電車に乗る(一方でも電車に乗らないことはない)こととします
※駅では両側のドアが同時に開くことはありません

標準入力から n が与えられたとき、「ホームの配置」と「乗客の動き」の組み合わせを求め、その数を標準出力に出力してください。
なお、与えられる n は14以下の整数とします。

【入出力サンプル】
標準入力
4

標準出力
6

私のプログラム

Rubyで解答しています。

#!/usr/bin/ruby

def count(arr)
	ret = 0

	for i in 0..(arr.size-4)
		l = arr[i..-1].count("L")
		r = arr[i..-1].count("R")
		if l >= 2 && r >= 2 then
			if arr[i] == "L" then ret += r-1
			else ret += l-1
			end
		end
	end

	return ret
end

def solve(n)
	cnt = 0
	["L", "R"].repeated_permutation(n){|str|
		l = str.count("L")
		r = str.count("R")
		if l >= 2 && r >= 2 then
			cnt += count(str)
		end
	}
	return cnt
end

# main
while line = gets
	line.strip!
	if line.empty? then next end

	p solve(line.to_i)
end

解説

問題の意味が読み取りづらいです。n=5の場合を例にしてくれれば分かり易かったと思うのですが。
なので補足しておきます。
例えばn=5の時の各駅に列車が停車した時、開く扉のパターンのうち1つに「左、左、右、左、右」というのがあります。この様な駅のパターンに対して乗客の乗り降りは次の2パターンがあります。

パターン1A↑A↓B↑B↓
パターン2A↑B↑A↓B↓

問題はこういう組み合わせの数を全て数えなさい、ということです。

考え方

私のプログラムは各駅に停車した列車の開く扉のパターンを総当たりで求め、それに対してAとBが乗り降りできるパターンを数え上げるという単純なものです。

扉のパターンに対して乗り降りのパターンの数え方です。
駅数が6のうち1つを例に説明します。

123456パターン数
パターン1A↑A↓B↑B↑2
パターン2A↑B↑A↓B↑2

Aの乗り降りを基準に考えます。
乗ってきた駅の次に同じ側の扉が開いたときに降りるので、ある駅からAが乗車したときその先に1以上左側の扉が開く駅がなければその駅からは乗車できません。
Aがある駅から乗車できたとするとAが降車する駅は一意に決まります。Aが乗り降りする駅が決まったときにBが乗車できる駅のパターンは(右側の扉が開く駅の数-1)です。例えば上表でAが駅1から乗ってきたときにBは駅3と5から乗車可能なので2パターン、Aが駅2から乗ってきたときもBは駅3と5から乗車可能なので2パターン、合計4パターンの乗降パターンがあります。

扉のパターンは["右", "左"]からn個選んだ時の重複順列を列挙すればOKですので、そのパターン全てに対して乗降パターンを計算して足し合わせれば良いわけです。

main

入力値を数値に変換し、solve()に渡します。
solve()が結果を返すのでそれを印字して終わります。

solve(n)

扉のパターンを列挙し、count()でそれぞれのパターンに対する乗降パターン数を数え、足し合わせます。左右どちらかの扉が1以下の場合、問題の条件を満たすことがないので無視します。
合計値を結果として返却します。

count(arr)

arrの駅パターンに対して乗降パターンが何通りあるかを数えます。
arrの先頭から順番にチェックして"L"(左扉)ならAが乗車してきたものとして、Bの乗降パターンを加算足ます。考え方に書いた通り、"R"(右扉)の数-1がBの乗降パターンになります。
ある駅以降の左側の扉の数が1以下になったら問題の条件を満たせないので無視します(プログラムでは右扉も同様にチェックしていますが無意味です)。
合計値を結果として返します。

雑感

あまり頭良さそうなプログラムではありません。少なくともcount()の処理はarrに対してループする必要はなく、計算だけで求められます。
この問題は題意がよくわからなくて、「こういう意味なのかなぁ」と思いながらテストケースを通していたらできた(1回リジェクトされています)という状態でした。このプログラムは問題の意味を確認するために遅くてもどういう理屈かわかりやすいプログラムを作っていたら時間的にも間に合ってしまったのでそのまま投稿してしまったという代物、というのが言い訳です。