信奥赛NOI编程入门例题拓展精讲2
学号分组问题
前言
本文目的:
借用一些常见的入门练习题,复习和拓展《初等数论和算法入门》的知识
顺带,熟悉 NOI 的“阉割+嫁接版 C/C++”
同时,对比掌握真正的C/C++,兼容NOI
本文背景
2024年寒假,为帮助孩子和几位同学自学并更好掌握 Python,龙爸编写了《初等数论和算法入门》的辅导材料。让孩子们在自学的基础上,通过一些精讲的题目来掌握初等数论的知识。
——龙爸给孩子们找了很多书籍、视频、教材,但都不满意。专业的不适合孩子,适合孩子的又太业余。说真的,NOI/CSP行业,包括洛谷等很多教材、题目问题太多。甚至NOI的某些比赛题里的毛病,经常让人哭笑不得——草台班子。
——龙爸认为:教、学编程,应该融入《初等数论》的基础知识和算法思维,不该分割开。
2025年寒假,学校请的培训机构时不时在群里发了一些入门练习,
刚好我们可以用来熟悉 NOI 的“阉割+嫁接版 C/C++”,复习和拓展《初等数论和算法入门》的知识。
龙爸强调:我们学编程不是为了信奥赛,不是等级考试和CSP,而是:
锻炼“思维”,分析和解决问题的方法
提高学习效率——必须在高考前学会提高学习效率,越早越好。
龙爸家的规则:如果学习效率高,有兴趣可以参赛,否则学习为主。
不考虑特招、降分——舍本逐末、因果颠倒。
而且,参加奥数才是本事,信奥可以考虑顺手。
所以,既然意外进了信奥队、不得不分一部分精力 熟悉 C/C++,兼容NOI 的“阉割+嫁接版 C/C++”——要学就学正宗的C/C++,但要能读懂不正宗的代码,以及必要时兼容之。
(我们硬件编程基本不用C,Micro Python、.NET/C# 都很好,以后考虑Rust)
这个系列中,龙爸会坚持一贯的原则:
——设法多引导孩子从不同角度去思考,鼓励孩子实践
——自己提出问题,然后查阅资料,去设计实验来验证自己的疑问。
一些入门级的例题
接下来用跟着学校培训机构打卡遇到的一些入门题作为例题,进行拓展:
(机构按进度教学很正常,没有贬低的意思,只是刚好遇到了就不浪费)
2.学号分组问题
🧠
全班49名学生,学号为1~49,按以下规则分成3组。学号尾号为1、4、7的为第1组、尾号为2、5、8的为第2组,其余学生为第3组。输入学号,输出是第几组。
老师:这道题不仅要用到if条件分支,还要用到取数位的知识点。
先看看同学的标准答案:
🧠
龙爸补充说明:
这道题的重点:初等数论中,关于数位的数学含义,以及如何取某一位上的数字的数学方法。
比如一个十进制的数字 123,取个位数:用 123 对 10 求模,余数就是个位数。
123 % 10 = 3
所以,需要进一步引导:
🧠
龙爸的拓展要求:
仔细思考,这个答案是否避免 if-else 多分支条件判断,比如简化后仅用一个三目运算符;
进一步,考虑如何用一个表达式代替三目运算符?
再进一步,如何用纯数学表达式解决问题,避免使用条件表达式?
下面是引导孩子最终得到的答案:
第12-14行代码是三种不同的实现方式,简单分析:
🧠
首先,题目的提示有错误:
老师:这道题不仅要用到if条件分支,还要用到取数位的知识点。
既然学了求模,就应该引入初等数论中关于余数的基本概念。
取出个位数后,怎么分组——这里不需要条件判断,而是对3求模。
简单分析一下,0-9十个数字对3求模的情况:
个位数字为 1、4、7,对3求模,余数为1,分到第一组;
个位数字为2、5、8,对3求模,余数为2,分到第二组;
个位数字为3、6、9、0,对3求模,余数为0,分到第三组
也就是说,只有余数为0的情况,需要条件判断归为第三组。
接下来,可以用简单的 if-else,或者三目运算符搞定。
——见代码第12行。
完成第一个拓展要求。继续:
🧠
如何用一个表达式代替三目运算符?
基于上面的分析,问题主要出在第三种情况的结果为0而不是期待的3。
如何把结果0,转化为3?
可以利用 bool 值的“隐式转换”——虽然不推荐。
逻辑表达式 ( lastDigit % 3 == 0 ) 的值为 false 或 true,对应着 0 和 1;
所以,如果符合上面分析的第三种情况,其值为 1,乘以 3 达到目的;
否则为0,乘以3运算结果为0,不影响结果。
——这是一种典型的局部修正技巧,摆脱了三目运算符。
——见代码第13行。
完成第二个拓展要求。继续,上难度:
🧠
如何用纯数学表达式解决问题,避免使用条件表达式?
前面分析的三种情况里,第三种情况结果为0,希望变为3,不得不用到条件表达式。
避免条件表达式,重点在如何将余数为0的情况变为3而不需要用条件判断。
这里用到基于同余定理的移位技巧:
显然,任意整数 lastDigit 都可以表示成 lastDigit = 3x + y 的形式,
其中 x, y 是整数,y 是 lastDigit 对 3 求模的结果,也就是余数。即:
lastDigit % 3 = y
lastDigit / 3 = x
(注意:因为lastDigit 是整型变量, / 的运算结果是整数,相当于整除
——不同编程语言有所区别)
可见,对应 lastDigit = {0-9},余数 y 的取值范围在 0, 1, 2 之间不会 >= 3。
如果对 lastDigit 分别+ 1, 2, 3,y 的值分别 +1, 2, 0,范围不变,但映射顺序发生了变化。
这个技巧可以帮助我们完成下面的变化:
对 lastDigit + (3-1),对3求模的结果会增加 (3-1),偏移而错开了0
——思考:为什么不是3?因为 + 3,余数不变,没有变化;
所以 {0, 3, 6, 9} + 2 对3求模余数为2,不再为0,避开了原来模3为0的问题。
然后对结果再+1——修正,回到之前的映射顺序:
(lastDigit + 2) % 3 + 1
验证一下:
1, 4, 7 和 2, 5, 8 分别 + 2 后对 3 求模,结果 + 1——与之前相等;
0, 3, 6, 9 分别 + 2 后对 3 求模,结果 + 1——为3
至此,答案出来了:
group = (lastDigit + 2) % 3 + 1
——见代码第14行
(抱歉,这款编辑软件对 MarkDown 的公式支持不好,回头换一个更清晰的)
🧠
龙爸补充:——初等数论很难么?
对于古希腊的哲学家、思想家们,可能比较难。
但对于现代人,甚至高年级小学生而言,一点儿也不难。
因为,大部分内容,他们在跟着课本学习、做数学作业和习题的过程中,都接触过了。
少数没接触过的,也很容易理解——基于整数的基本性质,进行逻辑推理。
——学编程不需要『天赋』,是喜欢说『需要天赋』的老师不行。