アルゴリズム概要

💡 この問題を一言で言うと:「ローマ数字の文字列を読み取り、対応する整数に変換する問題」です。

ローマ数字は基本的に大きい記号から小さい記号へ左から右に並べますが、IV(4)や IX(9)のように、小さい記号を大きい記号の前に置いて引き算を表す例外パターンが6つあります。 この問題では、その例外パターンを見落とさずに正しく数値へ変換することがポイントになります。

⚠️ なぜ単純な方法では解けないのか

  • すべての文字の値をそのまま足し算するだけでは、IV が「1+5=6」になってしまい、正しい答え「4」にならない。
  • 「今見ている文字」だけでは、足すべきか引くべきかを判断できず、「次に来る文字」との大小比較が必要になる。
O(n)
時間計算量
O(1)
空間計算量
1 <= n <= 15
入力文字列長
dict
主に使うデータ構造

入力: s = "III"  出力: 3

同じ記号Iが3つ並んでいるだけなので、単純にI+I+I=3となる。

入力: s = "LVIII"  出力: 58

L=50, V=5, III=3で、いずれも減算パターンを含まないため単純な足し算で58になる。

入力: s = "MCMXCIV"  出力: 1994

M=1000, CM=900, XC=90, IV=4で、CM・XC・IVの3つの減算パターンをすべて含む代表的な例。

Python実装

📋 このコードの構造(先に全体像を把握しよう)

  1. 入力が文字列であり、空でなく、有効な文字のみで構成されているかを検証する
  2. ループの外で対応表への参照をローカル変数に束縛し、合計値totalを0で初期化する
  3. 文字列を先頭から1文字ずつ走査し、次の文字と比較しながらtotalを更新する
  4. 走査が終わったらtotalをそのまま返す
from __future__ import annotations

from typing import Dict, Final, Optional


class Solution:
    """
    Roman to Integer (LeetCode 13) を解決するクラス。

    ローマ数字を表す文字列を受け取り、対応する整数値を返す。
    """

    # クラス変数として文字→数値の対応表を保持する。
    # インスタンスごとに作り直す必要がないデータなので、
    # __init__の中ではなくクラス変数として持たせることでメモリを節約できる。
    _ROMAN_VALUES: Final[Dict[str, int]] = {
        "I": 1,
        "V": 5,
        "X": 10,
        "L": 50,
        "C": 100,
        "D": 500,
        "M": 1000,
    }

    def romanToInt(self, s: str) -> int:
        """
        ローマ数字の文字列を整数に変換する。

        Args:
            s: 変換対象のローマ数字文字列(例: "MCMXCIV")。

        Returns:
            変換後の整数値(例: 1994)。

        Raises:
            TypeError: 入力が文字列でない場合。
            ValueError: 入力が空、または未知の文字を含む場合。
        """
        # 入力検証(防御的プログラミング:制約上は保証されているが、
        # 誤った呼び出しをされても安全に動作するようにする)
        self._validate(s)

        # ループの外で対応表への参照をローカル変数に束縛しておく。
        # self._ROMAN_VALUES というアクセスはPythonの内部で属性検索を
        # 伴うため、ループの外で1回だけ済ませておくと検索コストを省ける。
        values = self._ROMAN_VALUES
        total = 0
        length = len(s)

        # 左から右へ1文字ずつ走査する
        for i in range(length):
            current = values[s[i]]

            # 次の文字が存在する場合のみその値を取得し、
            # 存在しない場合はNoneとして扱う。
            next_value: Optional[int] = values[s[i + 1]] if i + 1 < length else None

            # 現在値が次の値より小さい場合は「引き算ルール」
            # (IV, IX, XL, XC, CD, CM の6パターン)に該当する。
            if next_value is not None and current < next_value:
                total -= current
            else:
                total += current

        return total

    def _validate(self, data: object) -> None:
        """型安全な入力検証を行う"""
        if not isinstance(data, str):
            raise TypeError("Input must be a string")

        if len(data) == 0:
            raise ValueError("Input cannot be empty")

        # all() + ジェネレータ式で全文字の妥当性チェックを1行で行う。
        if not all(char in self._ROMAN_VALUES for char in data):
            raise ValueError("Input contains invalid roman numeral characters")

▶ 入力例 "MCMXCIV" での動作トレース

i=0: current='M'(1000), next='C'(100)  → 1000 < 100 は偽 → 加算 → total=1000
i=1: current='C'(100),  next='M'(1000) → 100 < 1000 は真  → 減算 → total=900
i=2: current='M'(1000), next='X'(10)   → 1000 < 10 は偽   → 加算 → total=1900
i=3: current='X'(10),   next='C'(100)  → 10 < 100 は真    → 減算 → total=1890
i=4: current='C'(100),  next='I'(1)    → 100 < 1 は偽     → 加算 → total=1990
i=5: current='I'(1),    next='V'(5)    → 1 < 5 は真       → 減算 → total=1989
i=6: current='V'(5),    next=なし      → 無条件で加算       → total=1994
出力: 1994 ✅

計算量分析

📖 Big-O 記法の読み方

O(1)
入力サイズに関わらず
常に一定の速さ
O(n)
入力が2倍になると
処理も約2倍
O(n log n)
入力が2倍になると
処理は約2倍強
O(n²)
入力が2倍になると
処理は約4倍
アプローチ 時間計算量 空間計算量 備考
ハッシュテーブル1パス走査(採用) O(n) O(1) 文字列を1回だけ走査。対応表は固定サイズ(7種類)でメモリは入力サイズに依存しない
2文字パターンの事前置換 O(n) O(n) CM・XC・IV等を先に置換してから加算。新しい文字列を都度生成するため余分なメモリを消費する

🔍 なぜこの計算量になるのか

時間計算量が O(n) になるのは、文字列の各文字をちょうど1回ずつしか見ないためです。次の文字を先読みする処理も、 ループの中でインデックスを1つずらして参照するだけなので、追加のループを必要としません。 空間計算量が O(1) になるのは、文字→数値の対応表が常に7種類固定であり、入力文字列がどれだけ長くなっても (制約上は最大15文字ですが)対応表のサイズや追加変数の数は変わらないためです。

📖 用語集

このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください(五十音順)。

インデックス
配列や文字列の中で、要素が何番目にあるかを表す番号のこと。多くのプログラミング言語では0から数え始めるため、先頭の文字はインデックス0、2番目の文字はインデックス1となる。
型ヒント
関数の引数や戻り値に、想定している型を注釈として書く仕組み。def f(x: int) -> str:のように書く。pylanceのような型チェッカーが、実行前に型の誤りを検出できるようになる。
計算量
入力の大きさに対して、処理にかかる手間(時間計算量)やメモリ量(空間計算量)がどう増えるかを表す目安。Big-O記法(O(n)など)で表現されることが多い。
走査
配列や文字列などのデータを、順番に1つずつ調べていくこと。今回の実装では、文字列を左から右へ1回だけ走査することで答えを求めている。
ハッシュテーブル
キーから値をO(1)(一定時間)で取り出せるデータ構造。図書館の索引カードに例えると、タイトル(キー)から棚番号(値)を即座に引けるイメージ。Pythonではdictがこれに相当する。
不変条件
アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件のこと。今回の実装では「ループの各時点でtotalには、それまでに処理した文字列の先頭部分を正しく変換した値が入っている」という不変条件が成り立っている。