Roman numerals (Check iO > Home) 数字をローマ数字に変換する
いよいよ、あと2問で Home Station(最初の島)が終わる。いつの間にか、周辺の島がアンロックされていて、Homeの次はどこに行こうか楽しみだ。
どんな問題?
Roman numerals
http://www.checkio.org/mission/roman-numerals/
数字をローマ数字(I,II,III,IV...)に変換するという問題。
ローマ数字って腕時計に付いてる1から12ぐらいまでしか知らないんだよなー。
問題文でも詳しい説明はなく、詳細は Wikipedia を見ろって書いてあるだけ。幸い、日本語版があったので、そっちを読んで簡単にまとめてみた。
3分でわかるローマ数字
使う文字は7種類
1 | 5 | 10 | 50 | 100 | 500 | 1000 |
I | V | X | L | C | D | M |
文字を並べるルール
- できるだけ使う文字数が少なくなるようにする。IIIII→V
- 左から桁の大きい順に並べる。100の桁→10の桁→1の桁
- 同じ文字を4つ並べてはいけない。IIII→IV や VIIII→IX など。これを減算則という。
- 減算則で認められるのは、IV(4), IX(9), XL(40), XC(90), CD(400), CM(900) の6通りだけ。
- V(5),L(50),D(500)は1回しか使えない。
- 0は無い。
- 1から3999までしか表現できない。
どうやって解く?
思っていたより簡単そうだ。いろいろルールはあるものの、結局は、数字を各桁ごとにローマ数字に変換して、結合すればいいだけだ。
Wikipediaに、各桁の一覧が載っていたので、これを参考にして作ってみる。
1の桁
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
I | II | III | IV | V | VI | VII | VIII | IX |
10の桁
10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 |
X | XX | XXX | XL | L | LX | LXX | LXXX | XC |
100の桁
100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 |
C | CC | CCC | CD | D | DC | DCC | DCCC | CM |
1000の桁
1000 | 2000 | 3000 |
M | MM | MMM |
コードを書いてみた。
def checkio(number): def make_roman_nums(i, v, x): return ['', i, i * 2, i * 3, i + v, v, v + i, v + i * 2, v + i * 3, i + x] # 変換表を作成 roman_nums = [make_roman_nums('I', 'V', 'X'), # ones make_roman_nums('X', 'L', 'C'), # tens make_roman_nums('C', 'D', 'M'), # hundreds make_roman_nums('M', '', '')[:4]] # thousands # 数字を各桁に分解する digits = [int(c) for c in reversed(str(number))] # 123 -> [3, 2, 1] # ローマ数字に直して結合 return ''.join(reversed([roman_nums[i][digits[i]] for i in range(len(digits))]))
roman_nums が変換表だ。内容は下記の通り。直接、下のように書いてもよかったが、さすがに芸がないので上のようにしておいた。
roman_nums = [['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'], ['', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC'], ['', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM'], ['', 'M', 'MM', 'MMM']]
あとは、この変換表を使って各桁の数字をローマ数字に変換する。
roman_nums[0] が 1の桁
roman_nums[1] が 10の桁
roman_nums[2] が 100の桁
roman_nums[3] が 1000の桁
となっているので、
2014なら、
roman_nums[3][2] + roman_nums[2][0] + roman_nums[1][1] + roman_nums[0][4]
となる。
最後の2行では、数字を分解して、ローマ数字に変換して、結合する。
digits = [int(c) for c in reversed(str(number))] # 123 -> [3, 2, 1] return ''.join(reversed([roman_nums[i][digits[i]] for i in range(len(digits))]))
数字を各桁に分解するところは、数字を文字列にして反転させ、またintに変換している。
digits[0] が 1の桁
digits[1] が 10の桁
digits[2] が 100の桁
digits[3] が 1000の桁
となる。
次の行で、1の桁から順番に変換表にあてはめ、反転してから、結合する。
ただ、読み直してみると、やっぱりこの2行がわかりにくいかなと思う。reversedが2回出てくるのも、あまり綺麗じゃない。
digitsを (桁,数字) のリストにしたらどうだろう?
# 852 -> [(3, 0), (2, 8), (1, 5), (0, 2)] digits = [(place, number // (10 ** place) % 10) for place in range(3, -1, -1)] return ''.join(roman_nums[place][value] for place, value in digits)
この方がいいかも。
あとは、checkio()が呼ばれるたびに、変換表を作り直すのも無駄だなぁ、ということで、roman_nums は関数の外に出した。
def _make_roman_nums(i, v, x): return ['', i, i * 2, i * 3, i + v, v, v + i, v + i * 2, v + i * 3, i + x] roman_nums = [_make_roman_nums('I', 'V', 'X'), # ones _make_roman_nums('X', 'L', 'C'), # tens _make_roman_nums('C', 'D', 'M'), # hundreds _make_roman_nums('M', '', '')[:4]] # thousands def checkio(number): # 852 -> (3, 0), (2, 8), (1, 5), (0, 2) digits = [(place, number // (10 ** place) % 10) for place in range(3, -1, -1)] return ''.join(roman_nums[place][value] for place, value in digits)
よし、これで公開!