☰
P1151子数整数:用进制位权与整除取模拆分五位数
2026/10/19 8:56:48 网站建设 项目流程

第一次在题库里看到“P1151 子数整数【进制】”这个标签时,我也没太当回事,觉得不就是把一个五位数拆成三段再除一除吗?等真正动手写代码、又帮好几个同学调过这题之后我才意识到,这道题真正想训练的东西是两件特别基础的事:第一,把一个整数拆成“可以操作的一段段数字”;第二,理解十进制里位权(10的幂)是怎么在“整除”和“取模”里发挥作用的。这篇文章就把这道题从头到尾掰开讲一遍,包括题目本身的含义、进制视角的推导、三种实现方式对比、新手最容易踩的坑,以及这道题背后能延伸出去的通用思路。适合刚学完循环和数组、准备开始刷基础题的新手,也适合想在讲题的时候把“位权”这个概念讲得更明白的人。

1. 题目到底在问什么

1.1 先把题面翻译成人话

题目的主角是一个五位数,写成a1a2a3a4a5,这里的每个字母代表一位数字。比如12345,就是a1=1, a2=2, a3=3, a4=4, a5=5。所谓“子数”,就是从这五个连续数字里,截取连续的三位片段,一共可以得到三个:

  • 第一个子数是前三位:a1a2a3;
  • 第二个子数是中间连续三位:a2a3a4;
  • 第三个子数是后三位:a3a4a5。

注意,第二个子数是从第二位开始取的,不是跳过一位再取。子数是“连续滑动”的:起点依次是第1位、第2位、第3位,每次往后挪一格,长度固定为3。这个“滑动窗口”的概念后面会在字符串、数组题目里反复出现,在这里先用最简单的五位数感受一下。

题目会给你一个正整数K,然后要求在某个区间内,找出所有满足条件的五位数。条件是:这个五位数的三个子数都能被K整除。如果找到了,就从小到大、每行一个输出;如果整个区间里一个都没有,就输出NO。

区间范围是题面里明确给出的,常见的版本是10000到30000。为什么只搜这一段而不是全部五位数?因为五位数总共有99999 - 10000 + 1 = 90000个,全部搜也不是不行,但很多题目会故意把搜索范围缩小,一方面让输出结果不要太多,另一方面方便手工验证。不同OJ上这一题的区间描述可能略有差异,所以你的第一步永远是先把题面里的区间看清楚,代码里的循环边界跟着题面走。

1.2 手算一遍,把整个流程走通

我习惯拿到这类题先拿具体数字手算,算明白了再写代码。用12345和K=3来演示。

先拆子数:

  • 前三位:12345的前三位是123;
  • 中间三位:第2到第4位是2、3、4,拼出来是234;
  • 后三位:第3到第5位是3、4、5,拼出来是345。

再逐个判断整除性:

  • 123 % 3 = 0,因为123 = 3 × 41;
  • 234 % 3 = 0,因为234 = 3 × 78;
  • 345 % 3 = 0,因为345 = 3 × 115。

三个子数都能被3整除,所以12345是满足条件的数,应该被输出。

换一个数12346,同样K=3:

  • 前三位123,能被3整除;
  • 中间三位234,能被3整除;
  • 后三位346,数字和为3+4+6=13,13 % 3 != 0,所以12346不满足条件。

这里有个新手特别容易绕晕的点:判断“能否被整除”用的是取模运算,也就是求余数是否为0。代码里写成sub % K == 0,意思是“子数除以K的余数为0”。很多人会条件反射写成K % sub == 0,这俩在数学含义上完全不同,后面我会专门列出来讲。

2. 为什么这题的标签是“进制”

2.1 十进制位权就是一堆10的幂

题目标签里的【进制】两个字,不是吓唬人,而是点出了这道题最底层的解法来源。我们在小学就学过“个十百千万”的位置关系。数字12345拆开写其实是:

1×10000 + 2×1000 + 3×100 + 4×10 + 5×1

也就是:

1×10^4 + 2×10^3 + 3×10^2 + 4×10^1 + 5×10^0

从右往左数,第0位(个位)的权是10^0,第1位(十位)的权是10^1,第2位(百位)的权是10^2,依此类推。这个“权重是10的幂”的体系,就是十进制。

为什么要强调这个?因为从一个大数字里“切出”连续的一段,本质上就是在操作这些10^k。一个整数n对10^k做整除,等价于把整个数字往右移动k位,把低k位丢掉;一个整数n对10^k取模,等价于只保留低k位,把高位丢掉。

看具体操作:

  • 12345 / 100 = 123,这一步把低两位45丢掉了;
  • 12345 % 100 = 45,这一步只保留了低两位;
  • (12345 / 10) % 1000 = 1234 % 1000 = 234,先丢掉个位,再保留低三位,正好是中间三位。

所以三个子数的计算公式可以写成这样:

  • 前三位:n / 100;
  • 中间三位:(n / 10) % 1000;
  • 后三位:n % 1000。

每一行都在和10的幂打交道,这就是“进制”标签的由来。你不需要背公式,只需要记住一个原则:除以10的幂是砍低位,模10的幂是留低位,两个动作组合起来就能取任意连续片段。

2.2 “整除”和“取模”其实就是一对搭档

很多人刚接触除法时只关注“商”,忽略了“余数”。但在算法题里,a / b和a % b是形影不离的一对:

  • a / 10^k:砍掉低k位;
  • a % 10^k:留下低k位。

想从一个数里取出任意连续片段,通用的思路是:先通过除以10^L把比目标片段更低的位全部扔掉,再通过模10^(R-L+1)把目标片段之外更高的位也扔掉。举个例子,想从123456里取第2位到第4位(从高位往低位数),也就是234,做法是:

(123456 / 10^2) % 10^3 = 1234 % 1000 = 234

这里的10^2是为了丢掉低两位56,10^3是为了只保留三位结果234。这个通用公式比死记硬背有用得多,因为题目换一个位数、换一种截取方式,你都能自己推出来。

还有一个细节容易被忽略:中间子数可能带前导零。比如五位数20207,中间三位是020,这个子数的数值就是20。在做能否被K整除的判断时,20和020在数值上完全一样,取模结果当然也一样。所以计算过程中完全不用管前导零的问题,直接当普通整数算就行。如果你用字符串截取的方式拿到的是"020",也记得转换成整数20再做取模,不要拿字符串直接判断。

3. 实现方案与代码落地

3.1 最直接的写法:三行公式解决

如果你理解了上面的位权推导,代码其实就没什么悬念了。枚举区间内所有五位数,算出三个子数,依次判断即可。C++ 参考写法:

#include <bits/stdc++.h> using namespace std; int main() { int K; cin >> K; bool hasAns = false; for (int n = 10000; n <= 30000; n++) { int s1 = n / 100; // 前三位 int s2 = (n / 10) % 1000; // 中间三位 int s3 = n % 1000; // 后三位 if (s1 % K == 0 && s2 % K == 0 && s3 % K == 0) { cout << n << '\n'; hasAns = true; } } if (!hasAns) { cout << "NO" << '\n'; } return 0; }

Python 参考写法:

K = int(input()) has_ans = False for n in range(10000, 30001): # range 左闭右开,所以要写成 30001 s1 = n // 100 s2 = (n // 10) % 1000 s3 = n % 1000 if s1 % K == 0 and s2 % K == 0 and s3 % K == 0: print(n) has_ans = True if not has_ans: print("NO")

这里最核心的就是那三行取子数的代码。s1 = n / 100用的是整除,C++里注意n和100都是整数,结果是整数;Python里要用//而不是/,否则会得到浮点数。s2那行的运算顺序是从左到右,先除以10再对1000取模,可以加括号让意图更明确。

循环边界要特别小心。C++ 的<= 30000对应 Python 的range(10000, 30001),这个30001是初学者最常写错的地方——写range(10000, 30000)会漏掉30000本身。如果题面给的区间是10000到99999,也要对应把范围扩大。

3.2 逐位拆解写法:把位权写得更明白

有些时候,直接三行公式看起来有点“黑魔法”。你还可以先把五位数的每一位拆到数组里,再显式地用位权拼回子数。这种方法代码会略长,但逻辑更透明,也更容易扩展到任意位数的场景。

C++ 示例:

#include <bits/stdc++.h> using namespace std; int main() { int K; cin >> K; bool hasAns = false; for (int n = 10000; n <= 30000; n++) { int tmp = n; int d[5]; for (int i = 4; i >= 0; i--) { d[i] = tmp % 10; tmp /= 10; } int s1 = d[0] * 100 + d[1] * 10 + d[2]; int s2 = d[1] * 100 + d[2] * 10 + d[3]; int s3 = d[2] * 100 + d[3] * 10 + d[4]; if (s1 % K == 0 && s2 % K == 0 && s3 % K == 0) { cout << n << '\n'; hasAns = true; } } if (!hasAns) { cout << "NO" << '\n'; } return 0; }

这段代码里,for循环把每一位存进了数组d[0]到d[4],然后拼接子数的时候,每一位都乘上了对应的位权:百位乘100、十位乘10、个位乘1。这其实就是把上一节的位权公式用最朴素的方式重新写了一遍。

为什么说它扩展性更好?因为如果题目改成“六位数里取任意连续四位”,你只需要把d数组扩到6位,把拼数的长度改成4,逻辑完全不用变。而三行公式版本虽然简洁,遇到位数变化就得重新推导。两种写法没有绝对好坏,我的建议是:比赛求快用三行公式,学习求懂用逐位拆解。

3.3 字符串切分写法:直观但别依赖

还有一种初学者特别容易想到的思路:把数字转成字符串,直接按下标截取子串,再转回整数。

K = int(input()) has_ans = False for n in range(10000, 30001): s = str(n) s1 = int(s[0:3]) s2 = int(s[1:4]) s3 = int(s[2:5]) if s1 % K == 0 and s2 % K == 0 and s3 % K == 0: print(n) has_ans = True if not has_ans: print("NO")

这个写法在功能上完全正确,也能通过题目,但我不推荐作为首选思路。原因有三个:第一,数字转字符串、字符串切片、字符串再转整数,每一步都有额外开销,虽然这题数据量小无所谓,但会掩盖“数字处理”的本质;第二,如果把问题推广到大整数或者要求高性能的场景,字符串方式很可能成为瓶颈;第三,也是最关键的,这题的核心是训练进制和位权思维,用字符串等于把底层的数学过程完全绕开了,做了题却丢了收获。

当然,如果你的目标只是把这道题AC掉,字符串写法完全可以用。我的建议是:先理解位权公式,再用字符串方法验证一遍,你会发现两种方法得到完全相同的结果,这个“殊途同归”的过程本身就是对十进制体系的一次很好理解。

4. 新手最容易踩的坑与调试方法

4.1 常见错误速查表

我在帮人调试这题时,遇到的高频问题其实就集中在下面几个地方。

错误现象可能原因解决方法
输出结果少了很多数循环范围写错,比如从1000开始、到9999结束核对题面区间,五位数一定是10000起步
输出结果一个都没有整除条件写反,写成K % s == 0明确“子数能被K整除”是s % K == 0
该输出NO的时候没输出忘记记录“是否有答案”用一个布尔变量标记,循环结束后判断
中间子数明显算错计算s2时没有先除以10中间三位要先丢掉个位,再取低三位
Python里结果出现小数用了/而不是//整数除法用//
输出格式不对每行多了空格、多了空行按样例格式输出,一行一个数,末尾正常换行

其中“整除方向反了”是我见过最多次的错误。这里有一个记忆口诀:“A能被B整除”的意思是 A 除以 B 的余数是0,所以代码写A % B == 0。反过来,B % A == 0说的是 B 能被 A 整除,完全不是一回事。做题之前可以先在草稿纸上用一个小数字验证,比如6能被3整除,写出来是6 % 3 == 0,如果你写成了3 % 6 == 0,虽然这里碰巧余数也是3不等于0,但换个例子就很容易露馅。用这个方向感去检查,能少走很多弯路。

另外一个隐藏较深的坑是:如果你把区间枚举起始值写成了0或者9999,可能不会报错,但结果会与标准答案完全对不上。因为题目说的是“五位数”,第一位就不能是0。10000是最小的五位数,这是常识,也是边界条件。

4.2 几个自查小技巧

写完代码之后,别急着提交,先用手算能验证的小数据自测。

第一个技巧是拿K=1做测试。任何正整数都能被1整除,所以如果K=1,区间内所有五位数都应该满足条件,程序应该输出从10000到30000的所有数,一共20001个。如果你发现输出明显少了,说明子数截取的公式有问题。这个测试特别适合用来快速定位是不是s2算错了。

第二个技巧是拿一个具体的数验证,比如我们前面手算过的12345。如果你把区间缩小到包含12345,并且输入K=3,那么输出里应该能看到12345。看不到就说明整除判断逻辑有问题,不要急着去调后面的代码,先把最基础的三步走通。

第三个技巧是留意“没有答案”的情况。你可以故意把K设成一个很大的数,比如K=999999,那么区间内几乎不可能有子数能被它整除,程序应该输出NO。这个用例能帮你确认NO的分支有没有写对、hasAns这个标记有没有在正确的位置更新。

我一般调这类题还会在代码里临时加一行打印,把当前的s1, s2, s3打印出来,和手算结果核对。比如在n=12345时打印出来应该是123 234 345,如果打印出来是123 123 345或者别的,基本一眼就能发现问题出在取子数方式上。实际调试时,用最小数据逐行走一遍比盯着代码发呆高效得多。

5. 延伸思考:进制思维能带你走多远

5.1 从十进制到二进制:切位变成了掩码

理解了十进制里“除以10的幂砍低位、模10的幂留低位”,再看二进制就会觉得非常顺畅。二进制里,位权不是10^k,而是2^k。对应的操作变成了:

  • n >> k等价于n / 2^k,也就是砍掉低k位;
  • n & ((1 << k) - 1)等价于n % 2^k,也就是保留低k位。

比如一个二进制数110110,想取中间四位,可以先把低两位右移掉,再和一个四位掩码做与运算。这套操作和(n / 10) % 1000的中间子数取法在思路上完全一致,只是进制不同,工具从除法和取模换成了移位和位与。很多人在学位运算时觉得抽象,其实就是没在十进制里先把“位权切分”这件事想透。

5.2 位权思想在实际算法里的几个常见位置

“截取连续片段”这个操作远不止这一道题用得到。我随便就能举出三个常见的场景。

第一个是进制转换。十进制转二进制、八进制、十六进制,本质就是不断对基数取模得到每一位的数字,再不断除以基数缩小数字。这个过程里,模和整除的角色与本题一模一样。

第二个是哈希与散列。很多哈希函数会把一个字符串看成一个大整数,然后按位乘以对应的权值再求和。你看到的那些魔幻的31 * hash + c,底层就是位权思想的延续,只不过这里的“进制”可能不是10,而是31或者其他素数。

第三个是字符串与子串问题。虽然字符串可以通过切片直接取片段,但涉及到高性能匹配或者需要把子串数值化比较时,你会再次回到“用一个整数表示某段连续字符”的思路,本质上和把三位子数拼成一个整数没有区别。

这题虽然看起来简单,但它把“位权”“整除”“取模”这三件事一次性揉进了同一个小程序里。把这三点吃透,后面学进制转换、位运算、前缀哈希的时候,你会发现自己比没做过这题的人少一层理解障碍。

我个人在实际带新人的过程中有一个明显感受:能靠自己想明白s2 = (n / 10) % 1000而不是照着抄的人,后来理解二进制位运算都特别快。所以如果你只是抄了一版代码过题,我还是建议回头再推一遍这三个式子:为什么前三位要除以100?为什么中间三位要又除又模?为什么后三位只模不除?想明白这三个为什么,这道题的收获就真的到手了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询