【十二届蓝桥杯】试题 D. 货物摆放 再谈时间复杂度的现实测度

问题描述

小蓝有一个超大的仓库,可以摆放很多货物。 现在,小蓝有 n n n 箱货物要摆放在仓库,每箱货物都是规则的正方体。小蓝 规定了长、宽、高三个互相垂直的方向,每箱货物的边都必须严格平行于长、 宽、高。 小蓝希望所有的货物最终摆成一个大的长方体。即在长、宽、高的方向上 分别堆 L 、 W 、 H L、W、H LWH 的货物,满足 n = L × W × H n = L × W × H n=L×W×H。 给定 n n n,请问有多少种堆放货物的方案满足要求。 例如,当 n = 4 n = 4 n=4 时,有以下 6 6 6 种方案: 1 × 1 × 4 1×1×4 1×1×4 1 × 2 × 2 1×2×2 1×2×2 1 × 4 × 1 1×4×1 1×4×1 2 × 1 × 2 2×1×2 2×1×2 2 × 2 × 1 2 × 2 × 1 2×2×1 4 × 1 × 1 4 × 1 × 1 4×1×1。 请问,当 n = 2021041820210418 n = 2021041820210418 n=2021041820210418 (注意有 16 16 16 位数字)时,总共有多少种方案? 提示:建议使用计算机编程解决问题。

问题抽象

给定一个大整数 N N N N > 1 e 16 N>1e16 N>1e16),求它可以由三个因数(这里记为 a , b , c a,b,c a,b,c)相乘的式子所表示的不同方案数量,相同因数的不同次序记作不同方案。

思路解析
方法一:三重循环暴力

考虑最朴素的暴力枚举方法,枚举 N N N 的三个因数 a , b , c a,b,c a,b,c,每个因数范围为 [ 1 , N ] [1, N] [1,N]

for (int a=1; a<N; ++a)
    for (int b=1; b<N; ++b)
        for (int c=1; c<N; ++c)
            if (a * b * c == N)
                ++ans;

时间复杂度: O ( n 3 ) O(n^3) O(n3)

回忆起当时赛后下场,的确听到不少同学说,比赛开始就在运行,比完赛,还是没有出结果。这些同学很可能就是用这样最朴素的逻辑,期待着计算机的算力能够解决问题。其实,如果你也有这样的期望,就说明了对与算法时间复杂度认识的不足。程序能否在比赛时间内运行结束,可以通过检查算法的时间复杂度大致估计

以上面的代码为例,多重 for 循环的时间复杂度与最内层代码的执行次数有最直接的关系,常常可以直观地看做循环次数相乘。此例中, n ≈ 1 e 16 n\approx 1e16 n1e16 n 3 ≈ 1 e 48 n^3\approx 1e48 n31e48。而按照比赛时间 4 4 4 个小时计算,计算机保持每秒 1 0 8 10^8 108 次计算的速度,你能够达到的计算次数上限大约是 1.44 e 12 1.44e12 1.44e12。也就是说,你需要约 7 e 35 7e35 7e35 场比赛的时间才能完成运行。因此,这是极其悬殊,根本不可能完成的计算量。其实,出题人对此也一定有所计划。

方法二: 两重循环暴力

第三重不必循环,枚举两个因子,最后的因子有唯一值 c = N a b c = \dfrac{N}{ab} c=abN,可直接计算。

for (int a=1; a<N; ++a)
    for (int b=1; b<N; ++b)
        if (N % (a * b) == 0)
            ++ans;

时间复杂度: O ( N 2 ) O(N^2) O(N2)

虽然去掉一重循环,较大改善了时间复杂度,其理论计算次数 1 e 32 1e32 1e32 仍然不能接受。而余下的两重循环,我们再也没有办法避免——在我们当前的策略下,我们必须枚举出两个因数。要想继续优化复杂度,必须改进这一策略。

方法三: 预处理因数

对于数 N N N ,其因数总是成对出现,(完全平方数有一个单独的因数 N \sqrt N N ),因此我们只需要枚举一次 [ 1 , N ] [1, \sqrt N] [1,N ] 的区间即可记下 N N N 的所有因数。

使用试除法分解因数:

int fac[M], cnt;   // 栈
for (int i=1; i<=n/i; ++i) {
    if (n % i == 0) {
        fac[cnt++] = i;
        if (i != n / i) fac[cnt++] = n / i;
    }
}
cout << cnt << endl;
// cnt = 128

由于因数个数很少,取得所有因数后,我们用「方法一」的思路,在因数数组上三重 for 枚举即可。我们写出完整代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N = 2021041820210418, M = 1e6 + 4;
ll fac[M], cnt;
ll n = N;
int main() {
    for (ll i=1; i<=n/i; ++i) {
        if (n % i == 0) {
            fac[cnt++] = i;
            if (i != n / i) fac[cnt++] = n / i;
        }
    }
    ll ans = 0;
    for (int i=0; i<cnt; ++i)
        for (int j=0; j<cnt; ++j)
            for (int k=0; k<cnt; ++k)
                if (fac[i] * fac[j] * fac[k] == n) ++ans;
    cout << ans << endl;
    return 0;
}

时间复杂度: O ( N + m 3 ) O(\sqrt N + m^3) O(N +m3),只考虑此题时, m = 128 m=128 m=128

程序在约 4 秒内可以算出结果。

方法四: 暴力 + 剪枝

事实上,我也认真地以为,预处理因数就是此题最该出现的标准答案,而暴力的方法应该无法完成任务,直到我在 CSDN 找到了这种解法。它在优化暴力方法上大下功夫,使得暴力算法整体框架得到保留。

这种思路的核心是:令要找的因子 a ≤ b ≤ c a\le b\le c abc,再通过排列 a , b , c a,b,c a,b,c 计算方案数。从答案来看,若有 a < b < c a<b<c a<b<c ,则 a , b , c a,b,c a,b,c 6 6 6 种可行方案,而我们现在只搜索其中的 1 1 1 种。另外,只在第一重循环找到一个因数的情况下,才进入第二重循环,由此实现了很好的剪枝效果。剪枝在理论时间复杂度上常常体现较少,但在程序实际运行速度上有显著效果。

最令人惊异的莫过于,其显然的两重 for 循环,给人以极其「暴力」的直观感受,竟然能获得只稍逊于预处理因数方法的运行速度(大约在 5 5 5 秒以内算出答案)。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N = 2021041820210418;
int main() {
    int ans = 0;
    // a <= b <= c
    for (ll a=1; a*a*a<=N; ++a) {
        if (N % a != 0) continue;
        ll t = N / a;
        for (ll b=a; b*b<=t; ++b) {
            if (t % b != 0) continue;
            ll c = t / b;
            // 计算方案数
            if (a == b && b == c) ++ans;
            else if (a == b || b == c || a == c) ans += 3;
            else ans += 6;
        }
    }
    cout << ans << endl;
    return 0;
} 

时间复杂度: O ( m N ) O(m\sqrt{N}) O(mN ) m m m 为因子个数,由上可知 m = 128 m=128 m=128。进入内层循环 m m m 次,并循环试除 b b b 不超过 N \sqrt{N} N 次。

实现参考:第十二届蓝桥杯 试题 D: 货物摆放

后记

许多人戏称蓝桥杯为「暴力杯」,诚然,赛事考察基础暴力算法较多,但尤其是最近的赛题,很多情况简单地写出暴力算法并不能很好的解决问题。在我看来,暴力算法是设计程序解决问题的基础和本质,从暴力算法转变为更有策略的算法就是算法思想的很好展现。

我们在学习算法时,常常分析算法的理论时间复杂度,或许不自觉地会形成一种认识——认为暴力算法的多重 for 循环的每一层一定是 O ( n ) O(n) O(n) 复杂度相乘,一定会运行超时。在实际的解决问题中,理论时间复杂度形式相同的算法尚且会有天差地别的表现,现场制造规模递增的数据,测试运行时间,才能获知算法的实际运行效率。

算法不是一定的套路。算法有技巧,但技巧的运用离不开清楚的思路。即使是暴力算法,也可以是充分理解算法和计算机,构思巧妙的优雅算法。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值