c++ Collatz猜想优化

C++ Collatz Conjecture Optimization

本文关键字:优化 猜想 Collatz c++      更新时间:2023-10-16

在ProjectEuler问题#14中,需要找到最长的Collatz链,最多100万。我找到了一种半体面的方法,但是,感觉我太愚蠢了,因为我找不到让这段代码更高效的方法(代码应该只在测试1到100万次后打印出解决方案,但在10分钟后没有打印出任何东西)。我是否以错误的方式处理这个问题,或者是否有一种方法可以优化我现有的代码?

#include <iostream>
using namespace std;
int main()
{
    int i;
    int x;
    int n;
    int superN;
    int superI;
    superN = 0;
    superI = 0;
    for (i = 1; i <= 1000000; i++) {
        x = i;
        n = 1;
        do {
            if (x % 2 == 0) {
                x = x / 2;
            }
            if (x % 2 == 1 && x != 1) {
                x = 3 * x + 1;
            }
            n++;
            if (n > superN) {
                superN = n;
                superI = i;
            }
        } while (x != 1);
    }
    cout << "The number " << superI << " ran for " << superN << " terms.";
    system("pause");
    return 0;
}

你有一些小问题:

  1. 我很确定您正在溢出int数据类型。使用uint64_t使这种情况更不可能发生。
  2. 你应该只在while循环之外更新superIsuperN。这应该无关紧要,但它会影响性能。
  3. 在每次迭代中,您应该只修改x一次。当前可能会修改它两次,这可能会导致您陷入无限循环。你的n的计算也会关闭。
  4. 使用记忆来缓存旧的结果来提高性能。
应用这个,你可以得到这样的代码:
#include <cstdint>
#include <iostream>
#include <map>
using namespace std;
int main()
{
    uint64_t i;
    uint64_t x;
    uint64_t n;
    uint64_t superN;
    uint64_t superI;
    std::map<uint64_t, uint64_t> memory;
    superN = 0;
    superI = 0;
    for (i = 1; i <= 1000000; i++) {
        x = i;
        n = 1;
        do {
            if (memory.find(x) != memory.end()) {
                n += memory[x];
                break;
            }
            if (x % 2 == 0) {
                x = x / 2;
            } else {
                x = 3 * x + 1;
            }
            n++;
        } while (x != 1);
        if (n > superN) {
            superN = n;
            superI = i;
        }
        memory[i] = n;
    }
    cout << "The number " << superI << " ran for " << superN << " terms.n";
    system("pause");
    return 0;
}

需要4秒输出:

The number 837799 ran for 556 terms.

我建议不要使用记忆,因为对我来说它运行速度较慢;在我的例子中(最多10,000,000),下面的代码在没有记忆的情况下更快。主要变化如下:

  1. 只测试当前数字是否为偶数(不需要else-if)。
  2. 使用位操作而不是模操作(稍微快一点)

除此之外,我不知道为什么你的代码这么长(我的代码低于200毫秒),也许你编译为调试?

bool isEven(uint64_t value)
{
    return (!(value & 1));
}
uint64_t solveCollatz(uint64_t start)
{
    uint64_t counter = 0;
    while (start != 1)
    {
        if(isEven(start))
        { 
            start /= 2;
        }
        else
        {
            start = (3 * start) + 1;
        }
        counter++;
    }
    return counter;
}

如果你可以使用编译器的内在特性,特别是计数和删除末尾的零,你就会意识到你不需要在主循环中进行分支,你总是可以交替使用奇数和偶数。前面介绍的记忆技巧很少会在你所做的数学中短路,因为我们处理的是冰雹数字——此外,大多数数字只有一个入口点,所以如果你看到它们一次,你就再也见不到它们了。

在GCC中看起来像这样:

#include <cstdint>
#include <iostream>
#include <unordered_map>
#include <map>
using namespace std;
using n_iPair = std::pair<uint32_t, uint64_t>;
auto custComp = [](n_iPair a, n_iPair b){
  return a.first < b.first;
};
int main()
{
    uint64_t x;
    uint64_t n;
    n_iPair Super = {0,0};
    for (auto i = 1; i <= 1000000; i++){
        x = i;
        n = 0;
        if (x % 2 == 0) {
          n += __builtin_ctz(x); // account for all evens
          x >>= __builtin_ctz(x); // always returns an odd
        }
         do{ //when we enter we're always working on an odd number
          x = 3 * x + 1; // always increases an odd to an even
          n += __builtin_ctz(x)+1; // account for both odd and even transfer
          x >>= __builtin_ctz(x); // always returns odd
        }while (x != 1);
        Super = max(Super, {n,i}, custComp);
    }
    cout << "The number " << Super.second << " ran for " << Super.first << " terms.n";
    return 0;
}

如果性能很关键,但内存不是,您可以使用缓存来提高速度。

#include <iostream>
#include <chrono>
#include <vector>
#include <sstream>
std::pair<uint32_t, uint32_t> longestCollatz(std::vector<uint64_t> &cache)
{
    uint64_t length = 0;
    uint64_t number = 0;
    for (uint64_t current = 2; current < cache.size(); current++)
    {
        uint64_t collatz = current;
        uint64_t steps = 0;
        while (collatz != 1 && collatz >= current)
        {
            if (collatz % 2)
            {
                // if a number is odd, then ((collatz * 3) + 1) would result in
                // even number, but even number can have even or odd result,  so
                // we can combine two steps for even number, and increment twice.
                collatz = ((collatz * 3) + 1) / 2;
                steps += 2;
            }
            else
            {
                collatz = collatz / 2;
                steps++;
            }
        }
        cache[current] = steps + cache[collatz];
        if (cache[current] > length)
        {
            length = cache[current];
            number = current;
        }
    }
    return std::make_pair(number, length);
}
int main()
{
    auto start = std::chrono::high_resolution_clock::now();;
    uint64_t input = 1000000;
    std::vector<uint64_t> cache(input + 1);
    auto longest = longestCollatz(cache);
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
    std::cout << "Longest Collatz (index : value) --> " << longest.first << " : " << longest.second;
    std::cout << "nExecution time: " << duration << " millisecondsn";
    return EXIT_SUCCESS;
}