题解 ZJUT 3121/2 二进制小数的乘积(简单版、困难版)
题目大意
有 次询问,每次询问给定一个整数 ,若 可以分解为若干个所有数位都是 或 的正整数的乘积,则输出 YES,否则输出 NO。
解题思路
根据题面描述,「二进制小数」定义为所有数位都是 或 的正整数,类比枚举二进制整数的方法,可以手动枚举出所有 以内的二进制小数 ,也可以编写代码,使用 BFS 枚举二进制小数序列。BFS 枚举的具体做法为:队列初始为 ,此后不断将队首元素 出栈,若 不超过 就将 存入 vector 并将 入栈,直到队列为空。易知 vector 中的元素是按升序存入的。
枚举出所有不大于 的二进制小数后,思考该以何种顺序试除每个二进制小数,此时考虑贪心算法,按照从大到小的顺序用每个二进制小数试除 ,直至 变成 或者所有二进制小数因子都被除尽。如果 最终为 (包括 本身为 或者 除尽所有二进制小数因子后变成 两种情况),则输出 YES,否则输出 NO。需要注意的是,虽然 也是一个二进制小数,但不能用它作为因子试除,因为 ,会造成死循环而 TLE,因此只能除到 (即 )为止。
此外还能做一个剪枝:注意到二进制小数的个位均为 或 ,那么倘若正整数 可以分解为若干个二进制小数的乘积,那么 的个位必定是 或 。对于个位不是 或 的 ,它一定无法分解为若干个二进制小数的乘积,可以直接输出 NO。
贪心正确性证明(数学归纳法)
设 表示正整数 能分解为若干个二进制小数的乘积。
如果 , 是二进制小数,显然 为真;
如果 ,
如果 本身为二进制小数,显然 为真;
如果 不是二进制小数但可以分解为若干个二进制小数的乘积,设 ( 是二进制小数,,,),则根据贪心过程可得
因此 为真;
- 如果 不是二进制小数也不可以分解为若干个二进制小数的乘积,设 ( 是二进制小数, 不是二进制小数,,,),则根据贪心过程可得
因为 不是二进制小数,所以 为假。
综上所述,本题使用的贪心算法正确。
关于不能采用从小到大的顺序试除二进制小数因子的错误贪心策略,是因为从小因子开始试除,可能会将某些大因子破坏,可能使这些因子由二进制小数被破坏为非二进制小数。例如,当 时, 本身就是一个二进制小数,应当输出 YES,但如果从小因子开始除, 不是 的因子,但 是 的因子,,而 无法分解为若干个二进制小数的乘积,最终将输出 NO,此时在错误的贪心策略下, 原有的二进制小数大因子 在除以 被破坏为非二进制小数因子。故采用从小到大的顺序试除二进制小数因子的贪心策略错误。
复杂度分析
时间复杂度
由于最多生成 个二进制小数,生成二进制小数序列的时间复杂度是可以忽略的常数。有 轮询问,在每轮询问中,共有 个二进制小数因子可能被试除, 除以每个因子的次数不超过 次,故每轮询问的时间复杂度为 , 轮询问的时间复杂度为 。总时间复杂度为 。
空间复杂度
vector 中最多只存储了 个二进制小数,总空间复杂度为 。
代码
#include <cstdio>
#include <queue>
#include <vector>
int t, n;
std::vector<int> nums;
const int MAX_N = 100000; // 简单版改为 MAX_N = 1000
void build_nums() {
std::queue<int> q;
q.push(1);
while (!q.empty()) {
int x = q.front();
q.pop();
if (x > MAX_N) continue;
nums.push_back(x);
q.push(x * 10);
q.push(x * 10 + 1);
}
}
void work(int n) {
if (n % 10 != 0 && n % 10 != 1) {
printf("NO\n");
return;
}
for (int i = nums.size() - 1; i >= 1; i--) {
if (n == 1) break;
while (n % nums[i] == 0)
n /= nums[i];
}
printf("%s\n", n == 1 ? "YES" : "NO");
}
int main() {
scanf("%d", &t);
build_nums();
while (t--) {
scanf("%d", &n);
work(n);
}
return 0;
}