
1. 题目考点
这道题主要考:
- 字符串判断
- 正则表达式
re - 罗马数字规则
- 整体匹配:从开头到结尾都必须合法
这题不是让你把罗马数字转换成整数,而是判断一个字符串是不是一个合法的罗马数字。
2. 审题
题目给你一个字符串,比如:
CDXXI
你要判断它是不是 1 到 3999 之间的合法罗马数字。
如果合法,输出:
True
否则输出:
False
关键点是:题目要求用 regular expression 正则表达式 来判断。
3. 思路提示:不要一开始想完整正则
这道题如果直接想完整表达式,会很乱。正确思路是把罗马数字按位拆开。
罗马数字可以分成四个部分:
千位 + 百位 + 十位 + 个位
例如:
CDXXI
可以看成:
CD + XX + I
也就是:
400 + 20 + 1 = 421
所以我们不是一次性判断整个字符串,而是分别判断:
千位是否合法
百位是否合法
十位是否合法
个位是否合法
最后把四部分拼成一个完整正则表达式。
4. 完整设计思路
第一步:处理千位
题目范围是 1 到 3999。
千位只能是:
空
M
MM
MMM
也就是最多 3 个 M。
正则可以写成:
M{0,3}
意思是:
M 出现 0 到 3 次
第二步:处理百位
百位可能是:
100 = C
200 = CC
300 = CCC
400 = CD
500 = D
600 = DC
700 = DCC
800 = DCCC
900 = CM
观察一下,百位合法情况可以分成三类:
CM 表示 900
CD 表示 400
D?C{0,3} 表示 0、100、200、300、500、600、700、800
所以百位正则是:
(CM|CD|D?C{0,3})
这里:
|
表示“或者”。
D? 表示 D 可以出现 0 次或 1 次。
C{0,3} 表示 C 可以出现 0 到 3 次。
第三步:处理十位
十位和百位规则完全类似,只是字母换了。
十位可能是:
10 = X
20 = XX
30 = XXX
40 = XL
50 = L
60 = LX
70 = LXX
80 = LXXX
90 = XC
所以十位正则是:
(XC|XL|L?X{0,3})
第四步:处理个位
个位可能是:
1 = I
2 = II
3 = III
4 = IV
5 = V
6 = VI
7 = VII
8 = VIII
9 = IX
所以个位正则是:
(IX|IV|V?I{0,3})
第五步:拼成完整表达式
四部分合起来:
M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})
但是还不够。
因为我们要判断整个字符串都符合规则,所以要加:
^
表示从字符串开头开始匹配。
还要加:
$
表示一直匹配到字符串结尾。
最终正则:
^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$
5. 代码实现
import re
regex_pattern = r"^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$"
s = input()
print(str(bool(re.match(regex_pattern, s))))
如果想更严格一点,避免空字符串也被匹配,可以写成:
import re
regex_pattern = r"^(?=.)M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$"
s = input()
print(str(bool(re.match(regex_pattern, s))))
这里的:
(?=.)
意思是要求字符串至少有一个字符。HackerRank 这题一般输入本身就是一行罗马字符,所以第一种写法通常已经够用。
6. 运行演示
以样例:
CDXXI
程序会这样判断:
千位:没有 M,合法
百位:CD,表示 400,合法
十位:XX,表示 20,合法
个位:I,表示 1,合法
所以整个字符串合法,输出:
True
再看一个非法例子:
IIV
它想表达 3 或 5 附近的数字,但罗马数字里不能这样写。
个位部分允许:
I
II
III
IV
V
VI
VII
VIII
IX
没有 IIV 这种写法,所以输出:
False
再看:
MMMM
因为 3999 最大是:
MMMCMXCIX
千位最多只能有 3 个 M,所以 MMMM 不合法,输出:
False
7. 方法总结
这类题的关键不是马上写代码,而是先把规则拆小。
遇到“判断某种格式是否合法”的题,可以按这个顺序想:
- 这个字符串可以拆成哪几部分?
- 每一部分有哪些合法情况?
- 每一部分能不能用正则表达式表示?
- 最后用
^和$限制必须整体匹配。
本题的核心拆法就是:
罗马数字 = 千位 + 百位 + 十位 + 个位
所以正则也按这四部分写。
8. 练习
请判断下面这些罗马数字是否合法,并说明原因:
XIX
IL
MMMDCCCLXXXVIII
VX
CMXCIX
提示:
先不要急着查答案,按照四部分拆:
千位 + 百位 + 十位 + 个位
特别注意:
4、9、40、90、400、900
这些数有特殊写法。
还是不太理解,处理千位和处理百位这2块内容
先抓住一个核心:罗马数字是按“位”拆的
我们平时写阿拉伯数字,比如:
1421
可以拆成:
1000 + 400 + 20 + 1
罗马数字也可以按这个思路拆:
MCDXXI
拆成:
M + CD + XX + I
也就是:
1000 + 400 + 20 + 1
所以这道题的正则不是乱写的,而是按下面四块拼起来的:
千位 + 百位 + 十位 + 个位
你现在主要卡在:
M{0,3}
和:
(CM|CD|D?C{0,3})
我们单独讲这两块。
一、处理千位:为什么是 M{0,3}
罗马数字里:
M = 1000
题目限制数字范围是:
1 到 3999
所以千位最多只能到 3000。
也就是说,千位部分只可能有这几种情况:
| 数值 | 罗马写法 |
|---|---|
| 0 | 空 |
| 1000 | M |
| 2000 | MM |
| 3000 | MMM |
注意,0 的意思不是整个数字是 0,而是这个数字可能没有千位。
例如:
421 = CDXXI
它没有千位,所以千位部分就是“空”。
所以千位部分可以写成:
M{0,3}
它的意思是:
M 出现 0 次到 3 次
具体对应关系是:
M 出现 0 次:空
M 出现 1 次:M
M 出现 2 次:MM
M 出现 3 次:MMM
所以:
M{0,3}
能匹配:
空
M
MM
MMM
但是不能匹配:
MMMM
因为 MMMM 是 4000,超过题目范围 3999。
二、处理百位:先列出所有合法百位
百位用到这几个字母:
C = 100
D = 500
M = 1000
百位部分可能是 0 到 900。
完整列出来:
| 百位数值 | 罗马写法 |
|---|---|
| 0 | 空 |
| 100 | C |
| 200 | CC |
| 300 | CCC |
| 400 | CD |
| 500 | D |
| 600 | DC |
| 700 | DCC |
| 800 | DCCC |
| 900 | CM |
这一张表非常关键。
我们现在要用正则表达式把这 10 种情况表示出来。
三、百位为什么分成三类
百位的合法写法可以分成三类:
900:CM
400:CD
普通情况:空、C、CC、CCC、D、DC、DCC、DCCC
所以正则写成:
(CM|CD|D?C{0,3})
这个表达式中:
|
表示“或者”。
所以它的意思是:
CM
或者 CD
或者 D?C{0,3}
也就是:
900 的情况
或者 400 的情况
或者普通情况
四、重点理解 D?C{0,3}
我们单独看:
D?C{0,3}
它由两部分组成:
D?
和:
C{0,3}
1. D? 是什么意思
D?
表示:
D 可以出现 0 次或 1 次
也就是说,可以没有 D,也可以有一个 D。
不能有两个 D,因为百位里面不会出现 DD。
2. C{0,3} 是什么意思
C{0,3}
表示:
C 可以出现 0 次到 3 次
也就是:
空
C
CC
CCC
3. 合起来看 D?C{0,3}
现在把它们组合起来:
D?C{0,3}
意思是:
前面可以有一个 D,也可以没有 D;
后面可以跟 0 到 3 个 C。
所以它可以匹配下面这些:
D? 部分 | C{0,3} 部分 | 合起来 | 表示 |
|---|---|---|---|
| 空 | 空 | 空 | 0 |
| 空 | C | C | 100 |
| 空 | CC | CC | 200 |
| 空 | CCC | CCC | 300 |
| D | 空 | D | 500 |
| D | C | DC | 600 |
| D | CC | DCC | 700 |
| D | CCC | DCCC | 800 |
所以:
D?C{0,3}
刚好负责这些情况:
空、C、CC、CCC、D、DC、DCC、DCCC
也就是:
0、100、200、300、500、600、700、800
五、那 400 和 900 为什么要单独写?
因为 400 和 900 是特殊写法。
罗马数字里有一种“左小右大表示减法”的规则。
CD = 500 - 100 = 400
CM = 1000 - 100 = 900
所以:
400 不能写成 CCCC
900 不能写成 DCCCC
它们必须写成:
400 = CD
900 = CM
所以百位正则必须把这两个特殊情况单独列出来:
CM|CD
再加上普通情况:
D?C{0,3}
于是百位完整写法就是:
(CM|CD|D?C{0,3})
六、用例子看一下
例子 1:CDXXI
我们只看前两块:
CDXXI
千位部分:
没有 M
所以匹配:
M{0,3}
里面的“0 个 M”。
百位部分:
CD
它匹配:
(CM|CD|D?C{0,3})
里面的:
CD
所以前两块合法。
例子 2:MCMXCIX
这是 1999。
拆开看:
M + CM + XC + IX
千位:
M
匹配:
M{0,3}
百位:
CM
匹配:
(CM|CD|D?C{0,3})
里面的:
CM
所以前两块合法。
例子 3:DCCC
这是 800。
千位:
没有 M
匹配 M{0,3} 里的 0 个 M。
百位:
DCCC
可以拆成:
D + CCC
对应:
D?C{0,3}
其中:
D? 匹配 D
C{0,3} 匹配 CCC
所以 DCCC 合法。
例子 4:CCCC
这是非法的。
为什么?
百位部分如果是 400,必须写成:
CD
不能写成:
CCCC
而:
D?C{0,3}
最多只允许 3 个 C。
所以:
CCC
可以,但:
CCCC
不可以。
七、把千位和百位连起来理解
现在只看前半部分正则:
M{0,3}(CM|CD|D?C{0,3})
它表示:
先匹配 0 到 3 个 M
再匹配合法的百位
比如:
MCD
拆成:
M + CD
含义是:
1000 + 400 = 1400
再比如:
MMCM
拆成:
MM + CM
含义是:
2000 + 900 = 2900
再比如:
MMMDCCC
拆成:
MMM + DCCC
含义是:
3000 + 800 = 3800
八、你可以这样记
千位很好记:
M{0,3}
因为千位只有:
空、M、MM、MMM
百位这样记:
(CM|CD|D?C{0,3})
其中:
CM:900
CD:400
D?C{0,3}:普通百位,负责 0、100、200、300、500、600、700、800
也就是说,百位不是随便写的,而是:
特殊的 900 + 特殊的 400 + 普通情况
练习
先只练千位和百位,不看十位个位。
判断下面这些“千位 + 百位”部分是否合法:
MMMCM
MMCD
MCCCC
DCCC
MMMM
CM
提示:
先问自己两个问题:
1. 前面的 M 有没有超过 3 个?
2. 百位部分是不是 CM、CD,或者 D?C{0,3} 能表示的情况?
尤其注意:
CCCC 不合法
MMMM 不合法
CM 合法,表示 900



