题解 ZJUT 3121/2 二进制小数的乘积(简单版、困难版)


题目大意

tt 次询问,每次询问给定一个整数 nn,若 nn 可以分解为若干个所有数位都是 0011 的正整数的乘积,则输出 YES,否则输出 NO

解题思路

根据题面描述,「二进制小数」定义为所有数位都是 0011 的正整数,类比枚举二进制整数的方法,可以手动枚举出所有 10510^5 以内的二进制小数 1,10,11,100,101,1, 10, 11, 100, 101, \ldots,也可以编写代码,使用 BFS 枚举二进制小数序列。BFS 枚举的具体做法为:队列初始为 11,此后不断将队首元素 xx 出栈,若 xx 不超过 maxn\max{n} 就将 xx 存入 vector 并将 10x,10x+110x, 10x + 1 入栈,直到队列为空。易知 vector 中的元素是按升序存入的。

枚举出所有不大于 nn 的二进制小数后,思考该以何种顺序试除每个二进制小数,此时考虑贪心算法,按照从大到小的顺序用每个二进制小数试除 nn,直至 nn 变成 11 或者所有二进制小数因子都被除尽。如果 nn 最终为 11(包括 nn 本身为 11 或者 nn 除尽所有二进制小数因子后变成 11 两种情况),则输出 YES,否则输出 NO。需要注意的是,虽然 11 也是一个二进制小数,但不能用它作为因子试除,因为 1mod1=11 \bmod 1 = 1,会造成死循环而 TLE,因此只能除到 1010(即 nums1\mathrm{nums _ 1})为止。

此外还能做一个剪枝:注意到二进制小数的个位均为 0011,那么倘若正整数 nn 可以分解为若干个二进制小数的乘积,那么 nn 的个位必定是 0011。对于个位不是 0011nn,它一定无法分解为若干个二进制小数的乘积,可以直接输出 NO

贪心正确性证明(数学归纳法)

P(n)P(n) 表示正整数 nn 能分解为若干个二进制小数的乘积。

如果 n=1n = 111 是二进制小数,显然 P(n)P(n) 为真;

如果 n2n \ge 2

  • 如果 nn 本身为二进制小数,显然 P(n)P(n) 为真;

  • 如果 nn 不是二进制小数但可以分解为若干个二进制小数的乘积,设 n=a1k1a2k2amkmn = a _ 1 ^ {k _ 1} a _ 2 ^ {k _ 2} \ldots a _ m ^ {k _ m}aia _ i 是二进制小数,kiN+k _ i \in \mathbb{N _ +}a1>a2>>ama _ 1 > a _ 2 > \ldots > a _ mi=1,2,,mi = 1, 2, \ldots, m),则根据贪心过程可得

    P(n)=P(a1k1a2k2amkm)=P(a2k2a3k3amkm)==P(amkm)=P(1)\begin{aligned} P(n) &= P(a _ 1 ^ {k _ 1} a _ 2 ^ {k _ 2} \ldots a _ m ^ {k _ m}) \\ &= P(a _ 2 ^ {k _ 2} a _ 3 ^ {k _ 3} \ldots a _ m ^ {k _ m}) \\ &= \cdots \\ &= P(a _ m ^ {k _ m}) \\ &= P(1) \end{aligned}

因此 P(n)P(n) 为真;

  • 如果 nn 不是二进制小数也不可以分解为若干个二进制小数的乘积,设 n=a1k1a2k2amkmbn = a _ 1 ^ {k _ 1} a _ 2 ^ {k _ 2} \ldots a _ m ^ {k _ m} baia _ i 是二进制小数,bb 不是二进制小数,kiN+k _ i \in \mathbb{N _ +}a1>a2>>ama _ 1 > a _ 2 > \ldots > a _ mi=1,2,,mi = 1, 2, \ldots, m),则根据贪心过程可得

P(n)=P(a1k1a2k2amkmb)=P(a2k2a3k3amkmb)==P(amkmb)=P(b) \begin{aligned} P(n) &= P(a _ 1 ^ {k _ 1} a _ 2 ^ {k _ 2} \ldots a _ m ^ {k _ m} b) \\ &= P(a _ 2 ^ {k _ 2} a _ 3 ^ {k _ 3} \ldots a _ m ^ {k _ m} b) \\ &= \cdots \\ &= P(a _ m ^ {k _ m} b) \\ &= P(b) \end{aligned}

因为 bb 不是二进制小数,所以 P(n)=P(b)P(n) = P(b) 为假。

综上所述,本题使用的贪心算法正确。

关于不能采用从小到大的顺序试除二进制小数因子的错误贪心策略,是因为从小因子开始试除,可能会将某些大因子破坏,可能使这些因子由二进制小数被破坏为非二进制小数。例如,当 n=1001n = 1001 时,10011001 本身就是一个二进制小数,应当输出 YES,但如果从小因子开始除,1010 不是 10011001 的因子,但 111110011001 的因子,1001÷11=911001 \div 11 = 91,而 9191 无法分解为若干个二进制小数的乘积,最终将输出 NO,此时在错误的贪心策略下,nn 原有的二进制小数大因子 10011001 在除以 1111 被破坏为非二进制小数因子。故采用从小到大的顺序试除二进制小数因子的贪心策略错误。

复杂度分析

时间复杂度

由于最多生成 3232 个二进制小数,生成二进制小数序列的时间复杂度是可以忽略的常数。有 tt 轮询问,在每轮询问中,共有 3131 个二进制小数因子可能被试除,nn 除以每个因子的次数不超过 55 次,故每轮询问的时间复杂度为 O(1)\mathcal{O}(1)tt 轮询问的时间复杂度为 O(t)\mathcal{O}(t)。总时间复杂度为 O(t)\mathcal{O}(t)

空间复杂度

vector 中最多只存储了 3232 个二进制小数,总空间复杂度为 O(1)\mathcal{O}(1)

代码

#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;
}