
碎碎念
今天翻旧文件,看到一段 2022 年刚接触编程时写的代码。
题目是蓝桥杯的《卡片》:0 到 9 每个数字各有 2021 张卡片,从 1 开始依次拼数字,拼过的数字不能拆,问最多能拼到多少。
这题本质上就是统计数字消耗。现在看,写个循环,把当前数字每一位拆出来扣库存,扣到不够为止就行。
但当年的我选择了另一条路:手搓。
原代码
#include <iostream>
using namespace std;
int main()
{
int a[10]={2021,2021,2021,2021,2021,2021,2021,2021,2021,2021};
for(int i=1;;i++)
{
int t=1;
for(int j=0;j<10;j++)
{
if(a[j]==0)
{
t=0;
}
}
if(t==0)
{
cout<<i-1<<endl;
break;
}
else if (i<10)
{
a[i]--;
}
else if(i<100)
{
a[(i/10)%10]--;
a[i%10]--;
}
else if(i<1000)
{
a[(i/100)%10]--;
a[(i/10)%10]--;
a[i%10]--;
}
else if(i<10000)
{
a[(i/1000)%10]--;
a[(i/100)%10]--;
a[(i/10)%10]--;
a[i%10]--;
}
else if(i<100000)
{
a[(i/10000)%10]--;
a[(i/1000)%10]--;
a[(i/100)%10]--;
a[(i/10)%10]--;
a[i%10]--;
}
}
return 0;
}
现在看哪里好笑
最有节目效果的是这几段:
else if(i<100)
{
a[(i/10)%10]--;
a[i%10]--;
}
else if(i<1000)
{
a[(i/100)%10]--;
a[(i/10)%10]--;
a[i%10]--;
}
当时的思路非常朴素:一位数拆一位,两位数拆两位,三位数拆三位。
要是题目答案超过十万,那就继续复制粘贴:
a[(i/100000)%10]--;
a[(i/10000)%10]--;
a[(i/1000)%10]--;
a[(i/100)%10]--;
a[(i/10)%10]--;
a[i%10]--;
主打一个“位数增加了,但我的耐心也增加了”。
这段代码还有一个很神奇的判断:
for(int j=0;j<10;j++)
{
if(a[j]==0)
{
t=0;
}
}
if(t==0)
{
cout<<i-1<<endl;
break;
}
它不是判断当前数字 i 需要的卡片够不够,而是只要发现某种数字卡片刚好为 0,就直接停。
这在逻辑上其实不严谨。比如某个数字用完了,不代表下一个数一定需要这个数字;某个数字还剩 1 张,也不代表下一个数不会一次用 2 张。
但这段代码最后能跑出正确答案:3181。
所以它属于那种“理论上有隐患,实践中刚好过了”的代码。竞赛填空题里,这种代码有时候反而很常见:不追求通用,不追求优雅,只要把答案跑出来就行。
如果现在写
现在写的话,我大概会直接拆数字:
#include <iostream>
using namespace std;
int main() {
int cnt[10];
for (int i = 0; i < 10; i++) cnt[i] = 2021;
for (int x = 1; ; x++) {
int need[10] = {0};
int t = x;
while (t > 0) {
need[t % 10]++;
t /= 10;
}
for (int d = 0; d < 10; d++) {
if (need[d] > cnt[d]) {
cout << x - 1 << endl;
return 0;
}
}
for (int d = 0; d < 10; d++) {
cnt[d] -= need[d];
}
}
}
这版的区别很简单:
- 不用关心数字是几位数;
- 先判断够不够,再真正扣卡片;
- 不会因为某个数字库存刚好为 0 就提前停;
- 题目稍微改大一点,也不用继续复制粘贴。
结论
这段 2022 年的代码看起来确实很幽默。
它像是刚学会数组、循环、取模之后,把能用的东西全往题上招呼了一遍。代码不够通用,判断也有点冒险,但目标很明确:把答案算出来。
而且它真的算出来了。
答案:3181。
从现在的角度看,它最大的问题不是“写得丑”,而是“刚好能过”。这种代码最容易给人一种错觉:我好像完全理解了。
其实只是题目比较给面子。