惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threatpost
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Security Affairs
N
News and Events Feed by Topic
T
Tenable Blog
P
Proofpoint News Feed
W
WeLiveSecurity
Simon Willison's Weblog
Simon Willison's Weblog
Google DeepMind News
Google DeepMind News
C
CERT Recently Published Vulnerability Notes
Help Net Security
Help Net Security
I
Intezer
T
Threat Research - Cisco Blogs
S
Secure Thoughts
C
Cyber Attacks, Cyber Crime and Cyber Security
L
Lohrmann on Cybersecurity
AWS News Blog
AWS News Blog
Google Online Security Blog
Google Online Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Know Your Adversary
Know Your Adversary
Project Zero
Project Zero
The Hacker News
The Hacker News
Security Archives - TechRepublic
Security Archives - TechRepublic
T
Tor Project blog
N
News | PayPal Newsroom
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Hacker News - Newest:
Hacker News - Newest: "LLM"
A
Arctic Wolf
Forbes - Security
Forbes - Security
O
OpenAI News
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Security Latest
Security Latest
P
Palo Alto Networks Blog
S
Schneier on Security
S
Securelist
C
Cybersecurity and Infrastructure Security Agency CISA
H
Heimdal Security Blog
V
Vulnerabilities – Threatpost
www.infosecurity-magazine.com
www.infosecurity-magazine.com
博客园_首页
T
Troy Hunt's Blog
Latest news
Latest news
Recent Announcements
Recent Announcements
MyScale Blog
MyScale Blog
人人都是产品经理
人人都是产品经理
L
LINUX DO - 热门话题
M
MIT News - Artificial intelligence
N
Netflix TechBlog - Medium
V
Visual Studio Blog
H
Hacker News: Front Page

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角
罗马数字与阿拉伯数字的相互转换 - 元视角
飞鸿踏雪 · 2018-04-30 · via 元视角

最近遇到一道非常有趣的题目,题目大意如下:有一个富翁在银河系里做生意,而银河系使用的是罗马数字,所以他需要一个精明能干的助手,帮助他完成罗马数字与阿拉伯数字的相互转换,题目在这个背景下衍生出交易场景,我们需要帮助他计算出相关商品的价格。对于这道题目,如果剥离开这个题目本身的交易场景,这道题目本质上就是一个纯粹的算法问题。说来惭愧,博主当时并未能快速地解决这个问题,事后通过研读别人的文章始能有所领悟。所以,今天想在这篇文章里,同大家一起来讨论下这个问题。今天,全世界都在使用 0 到 9 这 10 个阿拉伯数字,比阿拉伯数字早 2000 年的罗马数字。为什么没有流传下来为后世所用呢?我觉得这是一个非常有意思的问题,数学同计算机学科间那种千丝万缕的联系、技术演进过程中若有若无的某种必然性……这些都是令我觉得非常有意思的地方。那么,一起来看看这个问题可好?

罗马数字起源

罗马数字,顾名思义,就是古罗马人使用的数字系统。在罗马数字中,共有 7 个基本数字,即 I、V、X、L、C、D、M,它们分别表示 1、5、10、50、100、500、1000。可以注意到,在这套数字系统中,0 不被视作是一个整数。据说,曾经有一位罗马学者不顾教皇的反对,执意将与 0 相关的知识以及 0 在运算中的作用向民众传播,因此被教皇囚禁并投入监狱,理由是 0 是一个邪物,破坏了神圣的数。同样罗马数字无法表示小数(注:罗马数字有分数的表示方法,可仅仅能表示 1/12 的整数倍),因此罗马数字常常用来表示纪年,在欧洲国家的古书籍、建筑和钟表中,我们都可以见到罗马数字的身影。我们熟悉的元素周期表,同样采用了罗马数字来表示元素所在的"族"。需要说明的是,罗马数字是一种计数规则,而非计算规则,这意味者罗马数字是没有进位和权重的概念的,所以一般罗马数字只用以计数而不用以演算。

既然罗马数字是一种计数规则,那么我们就不得不说一说它的组合规则,因为 4000 以内的数字,都可以用这 7 个基本数字组合表示。具体来讲,罗马数字的基本规则有以下 4 条:

  • 重复次数:**一个数字重复多少次,所表示的数字就是这个罗马数字的多少倍;一个罗马数字最多重复三次。**这条规则该怎么理解呢?第一点,I、II、III 分别表示 1、2、3;第二点,4 必须被表示为 IV,而不是 IIII。关于 4 的表示方法,在历史上一直存在争议,一种观点认为 IIII 这种写法占用书写空间,IV 可以达到简化书写的作用;而一种观点则认为 IV 有亵渎神灵朱庇特、含不敬侮辱之意。
  • 左减原则:当一个较小的数字被放在一个较大数字的左边时,所表示的数字等于这个大数减去这个小数,且左边最多只能放一个较小的数字。联系第一条原则,IV 表示的实际上是 V-I,所以这个数值表示 4;同理,9 为了满足第一条原则,必须被表示成 IX。
  • 右加原则:当一个较小的数字被放在一个较大数字的右边时,所表示的数字等于这个大数加上这个小数,且右边最多只能放一个较小的数字。这一条原则和第二条原则相对应,例如 11 会被表示成 XI、21 会被表示为 XXI,以此类推。
  • 搭配原则:I 只能被放在 V 和 X 的左边;X 只能被放在 L 和 C 的左边;C 只能被放在 D 和 M 的左边;V、L、D 不能被放在左边。这一条可以看作对是第二条的总结,所以没有什么可说的。

好了,通过这个这些规则我们就可以组合出不同的数字,我们可以注意到这些数字呈现出 1、4、5、9 的规律。什么是 1、4、5、9 的规律呢?我们可以注意到 4 和 9 是两个特殊的数字,4 必须通过 5 左减来得到,9 必须通过 10 左减来得到,这是因为罗马数字要满足最多重复三次的原则,而 4 和 9 相对 1 和 5 的偏移量恰好是 4,所以它们的表示方法和其他数字不同。因为罗马数字没有进位和权重的概念,所以除了左减和右增这两种特殊情况以外,它的基本数字应该从左至右依次递减,即使在左减的情况下,左边的数字应该和右边的数字处在同一序列。这句话怎么理解呢?例如,90 必须用 100-10 来表示;而 99 必须拆解为 90 和 9,然后分别用 100-10 和 10-1 来表示,唯独不能通过 100-1 来表示,因为 100 和 1 分属两个不同的序列。

数字转换实现

了解完罗马数字的历史渊源,我们就对罗马数字有了一定的了解。现在来考虑一个问题,即罗马数字和阿拉伯数字间的相互转换。罗马数字的确是古罗马人发明的,可阿拉伯数字实际上却是古印度人发明的。今天全世界人都在使用阿拉伯数字,因此这两者间需要一个转换器,这正是我们一开始所讨论的问题:假如银河系里的人们都使用罗马数字来计数,当一个地球上的富翁来到银河系以后,他要如何去和这里的人们进行交易。显然,这种转换应该是双向的,我们下面分别来看如何实现相应的转换。

阿拉伯转罗马

首先来考虑阿拉伯数字转罗马数字,因为一个罗马数字必然是从左到右依次递减,所以我们只需要将这 7 个基本数字从大到小排列,找到第一个不小于指定数字的数位即可。例如 1024 显然超过了 1000,而罗马数字中的 1000 对应 M,因此 1024 的第一位应该是 M。接下来 24,显然超过 10,因此 1024 的第二位数字应该是 X。接下来 14,显然超过 10,因此 1024 的第三位数字同样是 X。接下来 4,这是一个特殊的数字,需要被表示为 IV,这是 1024 的第四位数字。我们将整个过程串联起来,就可以得到 1024 的罗马数字形式 MXXIV。我们注意的一点是,这里需要 4 和 9 这两个数字作为辅助数字,因为 1 到 3、6 到 8 的数字,我们总可以通过不断地重复 1 来得到,就像辗转相除法一样。如果没有这两个辅助数字会怎样呢?4 会变成 IIII,而 9 会变成 VIIII,显然这是不符合我们预期的。整理下我们的思路,这段代码实现如下:

public static string ConvertToRoman(int number)
{
    var output = new StringBuilder();
    var digitMap = new Dictionary<int,string>()
    {
      {1,"I"},{4,"IV"},{5,"V"},{9,"IX"},
      {10,"X"},{40,"XL"},{50,"L"},{90,"XC"},
      {100,"C"},{400,"CD"},{500,"D"},{900,"CM"},
      {1000,"M"}
    };
    var digits = digitMap.OrderByDescending(e => e.Key).ToList();

    for (int i = 0; i < digits.Count && number > 0; i++)
    {
        if (number < digits[i].Key) continue;
        while (number >= digits[i].Key)
        {
            number -= digits[i].Key;
            output.Append(digits[i].Value);
        }
    }

    return output.ToString();
}

罗马转阿拉伯

接下来考虑罗马数字如何转换为阿拉伯数字,我们可以明确的一点是,罗马数字基本上是从左到右依次递减排列的,每一个数字的左侧和右侧出现的数字一定处于当前数字的同一序列。比如,I 只能被放在 V 和 X 的左边;X 只能被放在 L 和 C 的左边;C 只能被放在 D 和 M 的左边。因此,我们从左到右依次遍历整个字符串,将每个字符转化为对应的阿拉伯数字然后累加即可,需要注意的是,当当前元素小于下一元素时,表示当前元素为负数;当当前元素大于下一元素时,表示当前元素为正数。显然,这里最后一位应该是正数,因为它没有下一个元素可以比较。至此,我们梳理出整个思路:从第一位到第 n-1 位依次循环,判断当前元素的正负然后累加,再加上最后一位元素的值即可。下面是代码实现:

public static int ConvertToNumber(string romanNumber)
{
    var number = 0;
    var length = romanNumber.Length;
    var digits = new Dictionary<string,int>()
    {
      {"I",1},{"V",5},{"X",10},{"L",50},{"C",100},{"D",500},{"M",1000}
    };

    for (int i = 0; i < length - 1; i++)
    {
        //前面 n-1 位数字通过左右比较决定正负 & 第 n 位数字必然为正
        if ((digits[romanNumber[i].ToString()] >= digits[romanNumber[i + 1].ToString()]) || i + 1 >= length)
        {
            number += digits[romanNumber[i].ToString()];
        }
        else
        {
            number -= digits[romanNumber[i].ToString()];
        }
    }
    return number;
}

为什么会溢出

相信上面这两段代码,大家都已然把玩过了。可我们仔细想想,就会觉得这事儿不靠谱。前段时间网络上一直流传着,我们这些佛系青年正在被同龄人抛弃。这个题目里我们所面对的,可是一个来自地球的的富翁啊!富翁的钱不都是按亿来计数的吗?我们没有一个亿这样的小目标,我们的目标是月入 5 万啊,这是一个社会上流行的说法。好了,回到这个题目中来,如果我们输入 50000 这个阿拉伯数字,它会输出什么呢?答案是 50 个 M,这很罗马数字啊,当然更神奇的事情是什么呢?当我们尝试把这由 50 个 M 组成的罗马数字转换为阿拉伯数字时,会发现它不能像我们期望地输出 50000,而会变成是一个负数。为什么这里是负数呢?答案是溢出啦!

过去,我们常常听到”溢出“这个词儿,最常见的是数据溢出。为什么会发生数据溢出呢?因为我们定义的数据超过了计算机所使用的数据的表示范围。这一点我们可能无法理解,一个相对粗浅的认识是,现代计算机的内存已经大到非常客观,甚至我们的硬盘都已经使用 TB 这样的容量单位,为什么还是会发生数据溢出呢?回到罗马数字这个问题,我们发现一个残酷的事实是,古罗马人并没有定义 1000 以上的数字表示,这或许和古罗马人发明数字的过程有关。古人最早都是使用手指、绳结、竹筹这样的工具来计数,在人们没有接触到相当大的数字以前,人们认为这些数的表示是足够的。同样的,我们的计算机经历了从 8 位、16 位、32 位到 64 位的发展。所以,这个世界上没有任何东西是一成不变的,一个技术方案势必要随着业务演化而扩展。

我们前面曾提到,这 7 个基本数字可以表示 4000 以内的数字,为什么是 4000 以内呢?因为根据罗马数字最多重复三次的规则,我们应该用 5000-1000 来表示 4000,可问题是这 7 个基本数字中并没有 5000 的定义,这和计算机中的数据溢出是非常相似的,因为我们都无法通过现有的构造去描述一个新的东西。这和数学上的那些”扩充“有着极其相似的地方,当我们意识到所有的数不都是整数的时候,我们引入了分数/小数;当我们意识到所有的数不都是有理数的时候,我们引入了无理数; 当我们意识到所有的数不都是实数的时候,我们引入了虚数。在数学上,这叫做数的扩充;在计算机里,这叫做数据溢出。数学作为一本学科,可以通过完善理论来自圆其说;而编程语言里数据结构,是在一开始就定义好的一套规范,它无法更不应该经常去修改,关于如何去解决程序中数据溢出的问题,这已然是一个新的问题了,不过我们可以看看古罗马人是怎么做的。

聪明的罗马人自然想到了这个问题,他们提出的解决方案是这样的:在一个数字的上面加一条横线,表示这个数增值 1000 倍。所以,按照这个定义,4000 应该由 IV 变化而来,9000 应该由 10000 变化而来,而 10000 则可以看作是 10 的 1000 倍,即 10000 应该由 X 变化而来。我们在最初的规则中为什么没有说这一条呢?因为在数字上面增加一条横线,这更接近一个书写的行为,它增加了我们程序解析的难度,当一个数字的上面出现横线以后,我们就不能再按照原来的方式去转换。所以,考虑这个因素,实际上还是为了简化问题本身,这道题目中同样回避了这个问题。罗马人这个想法的确很好,可以解决眼下我们所面临的问题,可时间久了以后,罗马人发现这套计数规则书写了繁琐复杂,因而这套规则渐渐地就被人们放弃了。在 2015 年意大利官方宣布,国内街道编码、文件编码等全部废弃原有的罗马数字,改为使用阿拉伯数字。

选择阿拉伯数字

历史最终选择了阿拉伯数字,而不是罗马数字,这并不是一个巧合,尽管罗马数字要比阿拉伯数字早 2000 年。罗马数字的缺陷不仅仅在于其书写的繁杂,一个更重要的原因是,它不能更好地推动数学学科的发展。罗马人发明罗马数字的目的是为了计数,可一旦产生了数,就势必会产生计算。可我们发现罗马数字并不适合计算,因为它对数字的构造并不是正交的。一个最为直观的例子是,数字可能会用一个字母、两个字母或者三个字母来表示,如果两个数字要进行加减法,我们会发现它的数字是无法”对齐“的,你必须非常小心地分清楚不同的数位,而罗马数字恰好是没有数位的概念的。同样,当数字加减时会产生进位或者借位,罗马数字的构造会导致牵一发而动全身,因为任何一个中间步骤,我们都必须将其记录下来,记录的代价是将整个结果重写。反观阿拉伯数字,0 到 9 共 10 个数字可以表示一切,形式上的统一让计算更加便捷,书写更为简洁,这套定义可以扩展到无限大的数上面去,可以扩展到小数、分数甚至无理数、虚数。这是否意味着,一个统一化的定义或者构造,更适合去做相关的运算流程或者逻辑流程呢?

本文小结

本文从一道有趣的题目作为引子,引出这篇文章的主题:罗马数字。我们首先为大家回顾了罗马数字的历史渊源。罗马数字是一种由古罗马人创造的数字系统,这套数字系统主要的用途是进行计数。罗马数字由 I、V、X、L、C、D、M 共 7 个基本数字组成,其基本规则是最多重复三次、左减右增。接下来,我们分析了罗马数字与阿拉伯数字相互转换的规律,并提供相关代码实现。在当前方案的基础上,我们引出了罗马数字中的”4000“问题,联系计算机中的数据溢出的相关概念,我们分析了为什么当罗马数字超过 4000 时会发生”溢出“,以及罗马人是如何解决这个问题的。虽然罗马数字比阿拉伯数字早 2000 年,可历史最终选择了阿拉伯数字,这里我们简要地分析了原因,因为罗马数字并不适合计算,而数字作为数学的基本要素,一个不能被运用到计算出的数字系统,最终免除不了被人们抛弃的命运。好了,这篇五一节前的文章 就是这样啦,4 月再见!