[LeetCode] 學會轉羅馬數字,你也會轉古埃及數字 #12 Integer to Roman

古埃及數字轉法

我們先來看古埃及數字轉法,3942 我們就一位數一位數慢慢轉:


每次遇到 9 大概就是最麻煩的時候,竟然要寫 9 次!

因為古埃及數字系統裡面就沒有用一個符號代表 9 的圖案,只好乖乖寫囉。

中文數字轉法

我們再來看一個應該非常熟悉的語言:


至少這比古埃及來的簡單: "九百" 代表 900, "八百" 代表  800... 這樣至少寫的字就少一點了。

羅馬數字轉法

那我們再來看看羅馬數字系統,就會發現它其實混合了上面兩種方式:


查表法

如果我們去看一下羅馬數字的 Wikipedia 網頁,我們會看到這樣的 table:

Individual decimal places
ThousandsHundredsTensUnits
1MCXI
2MMCCXXII
3MMMCCCXXXIII
4CDXLIV
5DLV
6DCLXVI
7DCCLXXVII
8DCCCLXXXVIII
9CMXCIX


所以我們就先找出每個十進位的數字: 3492 = 3, 4, 9, 2 再來轉成羅馬數字的 string:


class Solution {
 public:
  std::string intToRoman(int num) {
    const int digit1000 = ((num % 10000) / 1000);
    const int digit100 = ((num % 1000) / 100);
    const int digit10 = ((num % 100) / 10);
    const int digit1 = ((num % 10) / 1);
    return digitToRoman1000(digit1000) + digitToRoman100(digit100) +
           digitToRoman10(digit10) + digitToRoman1(digit1);
  }

  std::string digitToRoman1000(const int digit) {
    static const char *TABLE[] = {"", "M", "MM", "MMM", "",
                                  "", "",  "",   "",    ""};
    return TABLE[digit];
  }

  std::string digitToRoman100(const int digit) {
    static const char *TABLE[] = {"",  "C",  "CC",  "CCC",  "CD",
                                  "D", "DC", "DCC", "DCCC", "CM"};
    return TABLE[digit];
  }

  std::string digitToRoman10(const int digit) {
    static const char *TABLE[] = {"",  "X",  "XX",  "XXX",  "XL",
                                  "L", "LX", "LXX", "LXXX", "XC"};
    return TABLE[digit];
  }

  std::string digitToRoman1(const int digit) {
    static const char *TABLE[] = {"",  "I",  "II",  "III",  "IV",
                                  "V", "VI", "VII", "VIII", "IX"};
    return TABLE[digit];
  }
};


重複字串的做法

羅馬數字有時候可以重複數字,像是 3 = III,有時候又不行,像是 9 = IV 而不是 IIIIIIIII。

所以如果你又想要用 for loop 重複數字,又想要查表的話,舉棋不定的結果通常就是寫出有 bug 的程式。

我們設計演算法就是要越簡單越好,概念最好也只有一個,不要兩三個。

如果我們要重複,我們就要更細的把它拆開:

3678 = 3000 + 600 + 70 + 8
     = 3000 + (500 + 100) + (50 + 20) + (5 + 3)
     = 3*1000 + (500 + 1*100) + (50 + 2*10) + (5 + 3*1)
     = 3*M + (D + 1*C) + (L + 2*X) + (V + 3*I)
     = MMM.DC.LXX.VIII

這樣的過程我們就可以寫出很蠢但是很直覺的程式:


class Solution {
 public:
  std::string intToRoman(int num) {
    std::string roman;
    while (num >= 1000) {
      roman += "M";
      num -= 1000;
    }
    while (num >= 900) {
      roman += "CM";
      num -= 900;
    }
    while (num >= 500) {
      roman += "D";
      num -= 500;
    }
    while (num >= 400) {
      roman += "CD";
      num -= 400;
    }
    while (num >= 100) {
      roman += "C";
      num -= 100;
    }
    while (num >= 90) {
      roman += "XC";
      num -= 90;
    }
    while (num >= 50) {
      roman += "L";
      num -= 50;
    }
    while (num >= 40) {
      roman += "XL";
      num -= 40;
    }
    while (num >= 10) {
      roman += "X";
      num -= 10;
    }
    while (num >= 9) {
      roman += "IX";
      num -= 9;
    }
    while (num >= 5) {
      roman += "V";
      num -= 5;
    }
    while (num >= 4) {
      roman += "IV";
      num -= 4;
    }
    while (num >= 1) {
      roman += "I";
      num -= 1;
    }
    return roman;
  }
};


然後我們再把上面這段程式碼 refactor 一下就可以變成這樣:


struct RomanAndValue {
  std::string romanStr;
  int value = 0;
};

class Solution {
 public:
  std::string intToRoman(int num) {
    static const RomanAndValue TABLE[] = {
        {"M", 1000}, {"CM", 900}, {"D", 500}, {"CD", 400}, {"C", 100},
        {"XC", 90},  {"L", 50},   {"XL", 40}, {"X", 10},   {"IX", 9},
        {"V", 5},    {"IV", 4},   {"I", 1},
    };

    std::string roman;
    for (const auto &item : TABLE) {
      while (num >= item.value) {
        roman += item.romanStr;
        num -= item.value;
      }
    }
    return roman;
  }
};


當然你也可以用 2 個 arrays、vector + pair 等方式來達成,只是我這邊用 struct 就是講清楚說明白我想要存取哪一個 member。

Submission 結果

這題就沒什麼好爭速度的:


你甚至可以寫 3999 行的程式碼直接回傳 1~3999 對應的羅馬數字,O(1) 鐵定爆快。

結語

看了 Wikipedia 才知道羅馬數字在中世紀的用法是很混亂的,甚至還有 4 = "IIII" 這種變形存在,所以拿來當題目大概就是要考驗你能不能在混亂中找出一些規律?

小心了如果面試問了這題,進公司後是不是就要在混亂中求生存?

下一篇我們用超懶惰的方法把羅馬數字轉回阿拉伯數字。

留言

此網誌的熱門文章

[試算表] 追蹤台股 Google Spreadsheet (未實現損益/已實現損益)

[Side Project] 互動式教學神經網路反向傳播 Interactive Computational Graph

[插件] 在 Chrome 網頁做區分大小寫的搜尋