【问题标题】:C++ Collatz Conjecture OptimizationC++ Collat​​z 猜想优化
【发布时间】:2016-06-30 04:51:51
【问题描述】:

在 ProjectEuler 问题 #14 中,需要找到最长的 Collat​​z 链,最多可达 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;
}

【问题讨论】:

  • 记住您已经调查过的起点并知道其长度。从新起点开始追踪时,一旦达到您之前调查过的数字,就立即停止。
  • 你确定它工作正常吗?

标签: c++ optimization collatz


【解决方案1】:

你遇到了一些小问题:

  1. 我很确定您正在溢出int 数据类型。使用uint64_t 可以大大降低这种可能性。
  2. 您应该只在 while 循环之外更新 superIsuperN。这应该无关紧要,但会损害性能。
  3. 在每次迭代中,您应该只修改一次x。您目前可能会对其进行两次修改,这可能会导致您陷入无限循环。您对n 的计算也将关闭。
  4. 使用 memoization 通过缓存旧结果来提高性能。

应用这个,你可以想出一些像这样的代码:

#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.

【讨论】:

  • 真正的错误是整数溢出。顺序 if 仅提供不正确的计数。此外,排除偶数会带来更大的加速——所有偶数在迭代后总是会变小,并在某些时候变成奇数。 pastebin.com/XgvCgUCa 这是一个演示奇数解的例子——运行时间为 0.18 秒。
  • @p4plus2:考虑一下我们正在寻找以
  • 也使用x &amp; 1L 而不是x % 2 == 0 然后反转 if 逻辑会更快。和x &gt;&gt;= 1 而不是x = x / 2。按位运算符通常比算术更快。
  • @BrainStone:你的两个例子都会产生相同的汇编代码。
  • @BillLynch 19 也有 21 个术语。但我们可以解决这个问题:while(i*2 &lt;= MAX){ n++; i*=2; } 所以在你的情况下 9 将产生 20 个步骤 18 小于最大值,这将显示 18 的 21 个周期。这仍然是平均所需操作的一半。
【解决方案2】:

我建议不要使用 memoization,因为它运行速度较慢;在我的情况下(最多 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;
}

【讨论】:

    【解决方案3】:

    如果您可以使用编译器内在函数,尤其是计数和删除尾随零,您将认识到您不需要在主循环中分支,您将始终交替使用奇数和偶数。之前介绍的记忆技术很少会绕过你正在做的数学,因为我们正在处理冰雹数字 - 此外,大多数数字只有一个入口点,所以如果你看到它们一次,你再也见不到他们了。

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

    【讨论】:

      【解决方案4】:

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

      #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 << " milliseconds\n";
      
          return EXIT_SUCCESS;
      }
      

      【讨论】:

      • 我想知道 1 mil 的 uint32 的向量初始化是否会比简单地运行算法慢...
      猜你喜欢
      • 2012-06-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-03-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多