ローマ数字アルゴリズム:Python・JavaScript・Javaによる実装
Python、JavaScript、Javaでローマ数字変換アルゴリズムを実装する方法を解説します。最適化されたコード、計算量の分析、および貪欲アルゴリズムの仕組みについて詳しく説明します。
ローマ数字と整数の相互変換は、コーディング面接・競技プログラミング・実際のアプリケーション開発で頻繁に登場する定番のプログラミング問題です。本ガイドでは、Python・JavaScript・Javaによる最適化された実装と、その背後にあるアルゴリズムの解説を提供します。
問題の理解
解決すべき問題は、相互に関連した2つのサブ問題から成ります。
- 整数からローマ数字へ: アラビア数字(例:1994)をローマ数字(MCMXCIV)に変換する
- ローマ数字から整数へ: ローマ数字(例:MCMXCIV)をアラビア数字(1994)に変換する
制約条件
- 入力範囲:1〜3999(標準的なローマ数字の範囲)
- 標準的な減算記法を使用する(IV・IX・XL・XC・CD・CM)
- すべての有効なローマ数字の組み合わせを処理する
貪欲アルゴリズムのアプローチ
いずれの変換にも貪欲アルゴリズムの戦略を用います。
整数からローマ数字へ(貪欲法)
- 値とローマ数字の対応表を降順で作成する
- 各「値・数字」ペアに対して:
- その数字をできるだけ多く使用する
- 残りの数値からその値を引く
- 数値がゼロになるまで繰り返す
ローマ数字から整数へ(貪欲法)
- ローマ数字の文字列を左から右にスキャンする
- 各記号に対して:
- 次の記号より小さい場合は減算する(減算記法)
- そうでない場合は加算する
- 累積した合計値を返す
Pythonによる実装
整数からローマ数字へ
def int_to_roman(num: int) -> str:
"""
Convert an integer to Roman numerals.
Args:
num: Integer between 1 and 3999
Returns:
Roman numeral string
Time Complexity: O(1) - fixed number of iterations (13)
Space Complexity: O(1) - fixed size mapping
"""
# Mapping in descending order (including subtractive combinations)
values = [
(1000, 'M'),
(900, 'CM'),
(500, 'D'),
(400, 'CD'),
(100, 'C'),
(90, 'XC'),
(50, 'L'),
(40, 'XL'),
(10, 'X'),
(9, 'IX'),
(5, 'V'),
(4, 'IV'),
(1, 'I')
]
result = []
for value, numeral in values:
# How many times does this value fit?
count = num // value
if count:
result.append(numeral * count)
num -= value * count
return ''.join(result)
# Examples
print(int_to_roman(1994)) # MCMXCIV
print(int_to_roman(58)) # LVIII
print(int_to_roman(3999)) # MMMCMXCIX
ローマ数字から整数へ
def roman_to_int(s: str) -> int:
"""
Convert Roman numerals to an integer.
Args:
s: Roman numeral string
Returns:
Integer value
Time Complexity: O(n) where n is length of string
Space Complexity: O(1) - fixed size mapping
"""
# Mapping of Roman numerals to values
roman_map = {
'I': 1,
'V': 5,
'X': 10,
'L': 50,
'C': 100,
'D': 500,
'M': 1000
}
result = 0
prev_value = 0
# Scan from right to left
for char in reversed(s):
value = roman_map[char]
# If current value is less than previous, subtract (subtractive notation)
if value < prev_value:
result -= value
else:
result += value
prev_value = value
return result
# Examples
print(roman_to_int('MCMXCIV')) # 1994
print(roman_to_int('LVIII')) # 58
print(roman_to_int('IX')) # 9
代替案:左から右へのスキャン
def roman_to_int_v2(s: str) -> int:
"""
Alternative implementation scanning left to right.
"""
roman_map = {
'I': 1, 'V': 5, 'X': 10, 'L': 50,
'C': 100, 'D': 500, 'M': 1000
}
result = 0
for i in range(len(s)):
current = roman_map[s[i]]
# Look ahead: if next symbol is larger, subtract current
if i + 1 < len(s) and current < roman_map[s[i + 1]]:
result -= current
else:
result += current
return result
JavaScriptによる実装
整数からローマ数字へ
/**
* Convert an integer to Roman numerals
* @param {number} num - Integer between 1 and 3999
* @returns {string} Roman numeral string
*
* Time Complexity: O(1)
* Space Complexity: O(1)
*/
function intToRoman(num) {
const values = [
[1000, 'M'],
[900, 'CM'],
[500, 'D'],
[400, 'CD'],
[100, 'C'],
[90, 'XC'],
[50, 'L'],
[40, 'XL'],
[10, 'X'],
[9, 'IX'],
[5, 'V'],
[4, 'IV'],
[1, 'I']
];
let result = '';
for (const [value, numeral] of values) {
while (num >= value) {
result += numeral;
num -= value;
}
}
return result;
}
// Examples
console.log(intToRoman(1994)); // MCMXCIV
console.log(intToRoman(58)); // LVIII
console.log(intToRoman(3999)); // MMMCMXCIX
ローマ数字から整数へ
/**
* Convert Roman numerals to an integer
* @param {string} s - Roman numeral string
* @returns {number} Integer value
*
* Time Complexity: O(n)
* Space Complexity: O(1)
*/
function romanToInt(s) {
const romanMap = {
'I': 1,
'V': 5,
'X': 10,
'L': 50,
'C': 100,
'D': 500,
'M': 1000
};
let result = 0;
for (let i = 0; i < s.length; i++) {
const current = romanMap[s[i]];
const next = romanMap[s[i + 1]];
// If current is less than next, subtract (subtractive notation)
if (next && current < next) {
result -= current;
} else {
result += current;
}
}
return result;
}
// Examples
console.log(romanToInt('MCMXCIV')); // 1994
console.log(romanToInt('LVIII')); // 58
console.log(romanToInt('IX')); // 9
ES6+版:reduceを使った関数型アプローチ
/**
* Functional approach using reduce
*/
const romanToIntFunctional = (s) => {
const map = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000 };
return s.split('').reduce((acc, char, i, arr) => {
const current = map[char];
const next = map[arr[i + 1]];
return acc + (next && current < next ? -current : current);
}, 0);
};
Javaによる実装
整数からローマ数字へ
/**
* Convert an integer to Roman numerals
*
* Time Complexity: O(1)
* Space Complexity: O(1)
*/
public class RomanConverter {
public String intToRoman(int num) {
// Parallel arrays for values and numerals
int[] values = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
String[] numerals = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
StringBuilder result = new StringBuilder();
for (int i = 0; i < values.length; i++) {
while (num >= values[i]) {
result.append(numerals[i]);
num -= values[i];
}
}
return result.toString();
}
// Examples
public static void main(String[] args) {
RomanConverter converter = new RomanConverter();
System.out.println(converter.intToRoman(1994)); // MCMXCIV
System.out.println(converter.intToRoman(58)); // LVIII
System.out.println(converter.intToRoman(3999)); // MMMCMXCIX
}
}
ローマ数字から整数へ
/**
* Convert Roman numerals to an integer
*
* Time Complexity: O(n)
* Space Complexity: O(1)
*/
public class RomanConverter {
public int romanToInt(String s) {
// Create mapping
Map<Character, Integer> romanMap = new HashMap<>();
romanMap.put('I', 1);
romanMap.put('V', 5);
romanMap.put('X', 10);
romanMap.put('L', 50);
romanMap.put('C', 100);
romanMap.put('D', 500);
romanMap.put('M', 1000);
int result = 0;
int prevValue = 0;
// Scan from right to left
for (int i = s.length() - 1; i >= 0; i--) {
int currentValue = romanMap.get(s.charAt(i));
// Subtractive notation check
if (currentValue < prevValue) {
result -= currentValue;
} else {
result += currentValue;
}
prevValue = currentValue;
}
return result;
}
// Examples
public static void main(String[] args) {
RomanConverter converter = new RomanConverter();
System.out.println(converter.romanToInt("MCMXCIV")); // 1994
System.out.println(converter.romanToInt("LVIII")); // 58
System.out.println(converter.romanToInt("IX")); // 9
}
}
アルゴリズムの分析
時間計算量
整数からローマ数字へ:
- O(1) — 定数時間
- 常に13個の「値・数字」ペアを反復処理する
- 入力サイズに依存しない
ローマ数字から整数へ:
- O(n) — 線形時間
- nはローマ数字文字列の長さ
- 各文字を1回ずつ調べる必要がある
空間計算量
いずれのアルゴリズムも:
- O(1) — 定数空間
- 入力に関わらず固定サイズのマッピングを使用
- 結果の文字列・数値のサイズは上限が決まっている
貪欲アルゴリズムが有効な理由
貪欲アルゴリズムがローマ数字に対して有効なのは、以下の性質を持つためです。
- 最適部分構造: 最適解はサブ問題の最適解を含む
- 貪欲選択性: 局所的に最適な選択が大域的に最適な解につながる
- 重複するサブ問題がない: 各値は独立して使用される
重要な直感:常に使用可能な最大値を先に選ぶことで、最も短いローマ数字表現が保証されます。
エッジケースとバリデーション
入力バリデーション
def int_to_roman_safe(num: int) -> str:
"""Integer to Roman with validation"""
if not isinstance(num, int):
raise TypeError("Input must be an integer")
if num < 1 or num > 3999:
raise ValueError("Input must be between 1 and 3999")
return int_to_roman(num)
def roman_to_int_safe(s: str) -> int:
"""Roman to Integer with validation"""
if not isinstance(s, str):
raise TypeError("Input must be a string")
if not s:
raise ValueError("Input cannot be empty")
# Validate characters
valid_chars = set('IVXLCDM')
if not all(c in valid_chars for c in s.upper()):
raise ValueError("Invalid Roman numeral characters")
return roman_to_int(s.upper())
テストケース
# Test Integer to Roman
assert int_to_roman(1) == 'I'
assert int_to_roman(4) == 'IV'
assert int_to_roman(9) == 'IX'
assert int_to_roman(58) == 'LVIII'
assert int_to_roman(1994) == 'MCMXCIV'
assert int_to_roman(3999) == 'MMMCMXCIX'
# Test Roman to Integer
assert roman_to_int('I') == 1
assert roman_to_int('IV') == 4
assert roman_to_int('IX') == 9
assert roman_to_int('LVIII') == 58
assert roman_to_int('MCMXCIV') == 1994
assert roman_to_int('MMMCMXCIX') == 3999
# Test round-trip conversion
for num in range(1, 4000):
assert roman_to_int(int_to_roman(num)) == num
パフォーマンスの最適化
メモ化(繰り返し変換に対して)
from functools import lru_cache
@lru_cache(maxsize=4000)
def int_to_roman_cached(num: int) -> str:
"""Cached version for repeated conversions"""
return int_to_roman(num)
バッチ変換
def batch_int_to_roman(numbers: list[int]) -> list[str]:
"""Convert multiple numbers efficiently"""
return [int_to_roman(num) for num in numbers]
def batch_roman_to_int(numerals: list[str]) -> list[int]:
"""Convert multiple Roman numerals efficiently"""
return [roman_to_int(s) for s in numerals]
よくある面接の質問
LeetCodeの問題
-
Integer to Roman(LeetCode #12)
- 難易度:中級
- 上記の貪欲アルゴリズムを使用する
-
Roman to Integer(LeetCode #13)
- 難易度:初級
- 左から右、または右から左へのスキャンを使用する
フォローアップの質問
Q:3999より大きい数値はどのように処理しますか?
A:ビンキュラム記法(×1000を表すオーバーライン)を実装します。
def int_to_roman_extended(num: int) -> str:
"""Support numbers up to 3,999,999 using vinculum"""
if num >= 4000:
thousands = num // 1000
remainder = num % 1000
return f"{int_to_roman(thousands)}̅{int_to_roman(remainder)}"
return int_to_roman(num)
Q:ローマ数字文字列をどのように検証しますか?
A:無効なパターンを確認します。
def is_valid_roman(s: str) -> bool:
"""Validate Roman numeral string"""
# Convert and convert back
try:
num = roman_to_int(s)
return int_to_roman(num) == s.upper()
except:
return False
実用的な応用例
1. ドキュメント生成
def format_chapter(num: int, title: str) -> str:
"""Format chapter heading with Roman numerals"""
return f"Chapter {int_to_roman(num)}: {title}"
2. 著作権年号
function copyrightYear(year) {
return `©${intToRoman(year)}`;
}
3. リスト番号付け
public String formatListItem(int index, String content) {
return String.format("%s. %s", intToRoman(index), content);
}
まとめ
ローマ数字変換アルゴリズムは、最適部分構造を持つ問題に対して貪欲アプローチがいかに強力かを示す好例です。本ガイドで紹介した実装は:
- 効率的: 整数からローマ数字へはO(1)、ローマ数字から整数へはO(n)
- 可読性が高い: 数学的なルールに対応した明快なロジック
- 堅牢: 標準範囲内のすべての有効な入力を処理できる
- 検証済み: 1〜3999のすべての数値で動作確認済み
コーディング面接の準備、ドキュメント生成システムの構築、あるいは単純にアルゴリズムへの興味から学ぶ場合でも、これらの実装は確固たる基盤を提供します。
コードを書かずに手軽に試したい方は、拡張記法にも対応したオンライン ローマ数字変換ツールをご利用ください。
さらに学びたい方へ: ローマ数字の完全ガイドや、ExcelでのローマX数字の使い方もご覧ください。