[语言月赛 202409] 数字 的题解
在解题之前提交题解的代码会导致封禁。
作者:
概述
本题要求求出满足「各位数字之和对 取模的余数最小」、且在该前提下数值最小的
位数。核心解法是按
与
的大小关系确定目标位和,再从左到右逐位贪心构造出最小的
位数。
分析
核心观察
一个 位数的各位数字之和可以取遍
中的每一个值,位和越小、取模后得到的余数就越小。能否让余数取到最小值
,取决于最大位和
与
的大小关系:
时任何位和都小于
,余数等于位和本身,最小只能取
;
时位和取
就能让余数为
,这也是可能的最小余数。
思路
由核心观察,最小余数只有两种可能,对应的目标位和也随之确定: 时目标位和为
,否则目标位和为
。目标位和确定后,问题化归为「给定
与
,求位和恰为
的最小
位数」。
先说明 时为什么取位和
而不是更大的
的倍数。记
为位和恰为
的最小
位数。对
,把
中最靠右的非零位减
:若最高位是唯一的非零位,位和只能是
,与位和
矛盾,因此被减的这一位不是最高位,最高位不会被减成
。于是得到的仍是一个位和为
的合法
位数,且比
小,说明
。即
关于
严格递增,位和越小的候选数一定越小,所以在可以取到余数
的位和中取最小的那个,即
。同理,
时余数等于位和,最小余数
对应位和
,此时最小的数就是
。
再逐位构造 。从左到右处理第
位(
),设已经用掉的位和为
,剩余位和
,当前位后面还剩
位,它们至多贡献
的位和。当前位要尽量小,但不能小到让后面的位塞不下剩余位和,于是
下界 保证最高位不为
;若
更小说明后面装得下,当前位可以取到下界
。取完当前位后剩余位和变为
,恰好能被后面的
位装满,因此构造过程不会中途失败。每一位都取了「保证后面还能补齐」前提下的最小值,逐步比较即知结果正是
。
朴素做法是枚举全部 位数,计算位和后打擂台,复杂度
;本题
时该做法也能通过,但上面的贪心只需
,且不依赖
的具体范围。
具体示例
以样例 #1(,
)为例。
,目标位和取
。第
位后面还剩
位,至多贡献
,故
,剩余位和变为
;第
位后面还剩
位,至多贡献
,故
,剩余位和仍为
;第
位之后没有位了,
。得到
,位和
,余数
,与样例一致。
样例 #3(,
)同理:
,目标位和取
,第
位只能取
(最高位不为
),此后剩余位和
小于后面各位置的上界,中间各位都取
,最后一位取
,得到
。
样例 #4(,
):
,目标位和取
,第
位取
,第
位取
,得到
。
另一分支的例子:、
时
,任何
位数的位和都小于
,余数等于位和,最小余数
由位和最小的
取到。
算法步骤
- 读入
n与p。 - 若
n小于p,最小余数只能取,答案固定为
1后面跟n - 1个0,即,输出后结束。
- 否则令剩余位和
rest等于p,并准备一个空字符串存放答案。 - 从左到右枚举第
i位():令后面剩余位数
remaining为n - 1 - i,令下界low在时取
、其余情况取
,再令当前位
d取low与rest - 9 * remaining中的较大者。 - 把
d追加到答案字符串,并令rest减去d。 - 输出答案字符串。
复杂度分析
- 时间:只需从左到右构造
位数字,为
。
- 空间:答案字符串占
,不计输入输出。
实现注意事项
- 最高位不能为
:构造第
位时下界是
,其余位的下界是
。样例 #2(
,
)的答案是
而不是
,因为
不是
位数。
- 分类的判据是
与
的大小关系:
时余数不可能为
,目标位和是
而非
;
时位和
一定可达,目标位和才是
。样例 #2 的
也落在后一分支,目标位和
与
恰好相等。
- 目标位和不能取更小的值:
时位和
不可能出现(
位数的位和至少为
),而
是不超过
的最小正倍数。
- 逐位取值时
剩余位数的上界不可缺少:没有它就会构造出后半段装不下的数字串,位和达不到目标值。
- 边界
落在
分支,目标位和
,每一位都取
(例如
、
时答案为
)。
- 答案用字符串逐位拼接,不必计算
这样的幂,也就避免了整型溢出问题;
n = 1时分支一的答案就是1。 - 输入只有一行两个整数,Python 中直接
split()读取即可。
源代码
#include <iostream>
#include <string>
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n = 0, p = 0;
if (!(std::cin >> n >> p)) return 0;
if (9 * n < p) {
// 各位数字之和最大为 9n,永远达不到 p,最小余数由最小位和 1 取得
std::cout << '1' << std::string(n - 1, '0') << '\n';
return 0;
}
// 余数 0 可以达到,位和取最小的正倍数 p,再逐位构造最小的那个数
int rest = p;
std::string answer;
for (int i = 0; i < n; ++i) {
const int remaining = n - 1 - i; // 当前位之后还剩几位
const int low = (i == 0) ? 1 : 0; // 最高位不能为 0
int d = rest - 9 * remaining; // 后面塞不下的部分必须由当前位承担
if (d < low) d = low;
answer += static_cast<char>('0' + d);
rest -= d;
}
std::cout << answer << '\n';
return 0;
}
import sys
def main():
n, p = map(int, sys.stdin.read().split()[:2])
if 9 * n < p:
# 各位数字之和最大为 9n,永远达不到 p,最小余数由最小位和 1 取得
sys.stdout.write("1" + "0" * (n - 1) + "\n")
return
# 余数 0 可以达到,位和取最小的正倍数 p,再逐位构造最小的那个数
rest = p
digits = []
for i in range(n):
remaining = n - 1 - i # 当前位之后还剩几位
low = 1 if i == 0 else 0 # 最高位不能为 0
d = max(low, rest - 9 * remaining) # 后面塞不下的部分必须由当前位承担
digits.append(str(d))
rest -= d
sys.stdout.write("".join(digits) + "\n")
if __name__ == "__main__":
main()
评论