减少下面代码的时间复杂度

时间:2017-03-26 12:58:50

标签: c++ time-complexity

以下代码是比赛问题陈述的解决方案。 给出的时间限制是1秒。该代码适用于5/7测试用例。对于其他情况,超出了时间限制。 如何降低以下代码的时间复杂度?

编辑: 问题陈述定义为返回数字n的值或n / 2,n / 3,n / 4之和,以最大值为准。 例如,如果输入为24 它可以减少或交换 12 + 8 + 6 = 26 此外,12可以减少到6 + 4 + 3 = 13。 不应减少8和6,因为它可能会降低该值。 所以最终答案是13 + 8 + 6 = 27

#include <cmath>
#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
#define lli long long int
using namespace std;

lli exchange(lli n){
    if(n<12)
        return n;

    else{

        lli sum=0;
        sum+=max(n/2,exchange(n/2));
        sum+=max(n/3,exchange(n/3));
        sum+=max(n/4,exchange(n/4));
        return sum;
    }
}

int main() {  
    lli t;
    cin>>t;
    while(t--){
        lli n;
        cin>>n;
        lli ans;
        ans=max(n,exchange(n));
        cout<<ans<<endl;
    }
    return 0;
}

2 个答案:

答案 0 :(得分:0)

只是尝试一些想法。首先,&#34; true&#34; if语句的分支是编译器预加载指令的分支。通过将高n分支设为默认值,它的速度会快一些。

编辑:其他一些想法没有成功(没有更快)。然而,有希望的是展开递归的两个级别。

exchange(n) = exchange(n/2) + exchange(n/3) + exchange(n/4)

exchange(n/2) = exchange(n/2/2) + exchange(n/3/2) + exchange(n/4/2) 
exchange(n/3) = exchange(n/2/3) + exchange(n/3/3) + exchange(n/4/3) 
exchange(n/4) = exchange(n/2/4) + exchange(n/3/4) + exchange(n/4/4)

然而,如果n / 4 <1,则这些变得不真实。 12.因此,我们可以展开一个完整的递归级别,并且只使用n&lt;这是&#34; opt&#34;交换版本:

lli exchange_opt(lli n) 
{
    if (n >= 12 * 4) // for n=48+ none of the terms would trigger n < 12
    {
        lli sum = 0;
        sum += exchange_opt(n / 4);
        sum += exchange_opt(n / 6) * 2;
        sum += exchange_opt(n / 8) * 2;
        sum += exchange_opt(n / 9);
        sum += exchange_opt(n / 12) * 2;
        sum += exchange_opt(n / 16);
        return sum;
    }
    if (n > 11)
    {
        lli sum = 0;
        sum += exchange_opt(n / 2);
        sum += exchange_opt(n / 3);
        sum += exchange_opt(n / 4);
        return sum;
    }
    return n;
}

这在我的机器上比默认实现快4倍,并且这个想法是可扩展的,例如你可以展开三个级别的递归,但增加它适用的n计数。这是一个从基本案例中展开三级递归的版本,并结合类似术语来减少函数调用。它现在快了8倍:

lli exchange_opt(lli n) 
{
    if (n >= 12 * 4 * 4)
        // for n=48+ none of the core terms would trigger n < 12
    {
        lli sum = 0;
        sum += exchange_opt(n / 8);
        sum += exchange_opt(n / 12) * 3;
        sum += exchange_opt(n / 16) * 3;
        sum += exchange_opt(n / 18) * 3;
        sum += exchange_opt(n / 24) * 6;
        sum += exchange_opt(n / 27);
        sum += exchange_opt(n / 32) * 3;
        sum += exchange_opt(n / 36) * 3;
        sum += exchange_opt(n / 48) * 3;
        sum += exchange_opt(n / 64);
        return sum;
    }
    if (n >= 12 * 4)
    // for n=48+ none of the core terms would trigger n < 12
    {
        lli sum = 0;
        sum += exchange_opt(n / 4);
        sum += exchange_opt(n / 6) * 2;
        sum += exchange_opt(n / 8) * 2;
        sum += exchange_opt(n / 9);
        sum += exchange_opt(n / 12) * 2;
        sum += exchange_opt(n / 16);
        return sum;
    }
    if (n >= 12)
    {
        lli sum = 0;
        sum += exchange_opt(n / 2);
        sum += exchange_opt(n / 3);
        sum += exchange_opt(n / 4);
        return sum;
    }
    return n;
}

BTW进行测试,我通过系统运行了从0到9999的所有数字,然后为OP的原始功能和我的功能添加了时间,同时测试结果是否相等。由于这是针对大数字进行优化的,因此对于非常大的数字,结果可能会更好。

我猜测展开的每个级别的递归都会大致加倍这个算法的速度。我没有像我一样手工计算展开,实际上可能编写一个程序,输出所需展开水平的正确方程式。基本上,在您想要展开到最接近的递归水平的最短时间内计算交换(n)&#34; k&#34;其中n> = 12 * 4 ^ k

但足以手动展开循环。这是一个递归函数,可以生成所需展开级别的递归函数。它使用std :: vector,std :: map,因此你需要包含正确的标题:

std::vector<std::map<lli, lli>> map1;
map1.push_back(std::map<lli, lli>());
map1[0][2] = 1;
map1[0][3] = 1;
map1[0][4] = 1;
const int unrolled_levels = 20;
for (int level = 1; level < unrolled_levels; ++level)
{
    map1.push_back(std::map<lli, lli>());
    for (auto i = map1[level - 1].begin(); i != map1[level - 1].end(); ++i)
    {
        map1[level][(*i).first * 2] += map1[level - 1][(*i).first];
        map1[level][(*i).first * 3] += map1[level - 1][(*i).first];
        map1[level][(*i).first * 4] += map1[level - 1][(*i).first];
    }
}

int level = unrolled_levels - 1;
std::cout << "\tlli exchange_opt(lli n) // unroll" << level << "\n\t{\n\n";
for (int inner_level = level; inner_level >= 0; --inner_level)
{
    lli mult = 12;
    std::cout << "\t\tif (n >= 12LL ";
    for (auto i = 0; i < inner_level; ++i)
    {
        std::cout << " * 4LL";
        mult *= 4LL;
    }
    std::cout << ") // " << (mult) << "\n\t\t{\n";
    std::cout << "\t\t\tlli sum = 0;\n";

    for (auto i = map1[inner_level].begin(); i != map1[inner_level].end(); ++i)
    {
        std::cout << "\t\t\tsum += exchange_opt(n/" << (*i).first << "LL)";
        if ((*i).second > 1) std::cout << " * " << (*i).second;
        std::cout <<"; \n";
    }
    std::cout << "\t\t\treturn sum;\n";

    std::cout << "\t\t}\n";
}
std::cout << "\t\treturn n;\n";
std::cout << "\n\t}\n\n";

基本上,您将unrolled_levels设置为您想要的任何内容。每个级别展开等式4次更大的数字。请注意输出功能将是巨大的,它测试n的数字范围,然后尽可能地使子级别短路。对于一些更高的数字,它可以计算出部分值,数千或数百万,可以有效地短路数百万次函数调用。

复制并粘贴此代码的输出,并将其用作计算exchange(n)的函数。对于大约100万的数字,它比原始公式(0.5%的运行时间)快200倍。对于大约1亿的数字,它占原方程1%的1/70,快7000倍。

顺便说一句,这可能会更快。我没有通过并收集在同一分支中乘以相似常数的术语。

答案 1 :(得分:0)

任何算法的标准权衡之一是时间与空间;您拥有的内存或磁盘空间越多,您可以节省的时间就越多,反之亦然。在这种情况下,您需要在特定时间内运行,但您似乎可以使用机器的完整内存。因此,注意到此特定算法经常请求已经计算的值,应该值得保存它们 all 以便快速查找。

实际上,Python通常不被认为特别快,可以在大约一秒钟内计算10 295 ,但是如果运行10 300 会遇到最大递归深度误差空结果缓存:

exchanged = {}
def exchange(n):
    if n in exchanged:
        value = exchanged[n]
    elif n < 12:
        exchanged[n] = value = n
    else:
        exchanged[n] = value = exchange(n//2) + exchange(n//3) + exchange(n//4)
    return value
exchange(10**295)

对于C ++,静态std::map<lli, lli>应该可以代替exchanged dict。但是,不要尝试使用数组,因为您不需要与计算出的最大值一样多的值;例如,10 295 使用的结果少于300,000。

是的,max部分可以省略,因为它是由n < 12检查处理的。我们可以通过注意只有(n-1)/2 + (n-2)/3 + (n-3)/4 < n(考虑整数除法抛弃其余部分的方式),抛出23以上的所有情况才能证明这一点;剩下的情况很容易手工检查,最大的异常恰好是11.但与将算法从O(n * log(n))更改为O(log(n))相比,这是一个小优化。