C++ Гипотеза Коллатца Оптимизация

В задаче ProjectEuler #14 нужно найти самую длинную цепочку Коллатца, до 1 миллиона. Я нашел наполовину приличный способ сделать это, однако мне кажется, что я просто глуп, потому что я не могу найти способ сделать этот код более эффективным (предполагается, что код только распечатывает решение после того, как он проверит от 1 до 1 миллиона, но ничего не распечатал через 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;
}

person Rocky    schedule 30.06.2016    source источник
comment
Запомните отправные точки, которые вы уже исследовали, и знайте длину. При трассировке с новой начальной точки остановитесь, как только вы нажмете число, которое исследовали ранее.   -  person Igor Tandetnik    schedule 30.06.2016
comment
Вы уверены, что он работает правильно?   -  person user253751    schedule 30.06.2016


Ответы (4)


У вас есть несколько небольших проблем:

  1. Я совершенно уверен, что вы переполняете тип данных int. Используйте uint64_t, чтобы сделать это менее вероятным.
  2. Вы должны обновлять только superI и superN вне цикла while. Это не должно иметь значения, но это вредит производительности.
  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.
person Bill Lynch    schedule 30.06.2016
comment
Настоящая ошибка — целочисленное переполнение. Последовательный, если только обеспечивает неправильный счет. Кроме того, гораздо большее ускорение может быть получено за счет исключения четных чисел — все четные числа всегда будут уменьшаться после их итерации и в какой-то момент станут нечетными числами. pastebin.com/XgvCgUCa Это пример, демонстрирующий суть решений с нечетными номерами — выполняется за 0,18 секунды. - person p4plus2; 30.06.2016
comment
@ p4plus2: Рассмотрим случай, когда мы ищем самую большую последовательность, начинающуюся с числа ‹= 20. В этом случае правильный ответ — 18, которого ваше решение не найдет. - person Bill Lynch; 30.06.2016
comment
Также использование x & 1L вместо x % 2 == 0, а затем инвертирование логики if будет быстрее. И x >>= 1 вместо x = x / 2. Побитовые операторы обычно быстрее, чем арифметические. - person BrainStone; 30.06.2016
comment
@BrainStone: оба ваших примера будут создавать идентичный ассемблерный код. - person Bill Lynch; 30.06.2016
comment
@BillLynch 19 также имеет 21 термин. Но мы можем решить эту проблему: while(i*2 <= MAX){ n++; i*=2; } Таким образом, в вашем случае 9 будет производить 20 шагов, 18 меньше, чем максимум, который покажет 21 цикл для 18. Это все еще половина необходимых операций в среднем. - person p4plus2; 30.06.2016
comment
Это можно улучшить, используя стек. Например, когда i=3, стек будет: 3, 10, 5, 16 и на этом он остановится. Мы знаем, что 16 -> 4. Вместо того, чтобы устанавливать коллац от 3 до 7, мы можем установить 10 to 6, 5 to 5. Для больших значений это становится более полезным. - person smttsp; 23.01.2020
comment
Эта программа работала в 10 раз быстрее без запоминания на компьютере: g++ -std=gnu++11 3xplus1.cpp && time ./a.out . Также n=1 следует изменить на n=0, чтобы мемоизация выдавала правильный результат: 524, а не 556. - person Kjetil S.; 06.08.2021

Я бы посоветовал не использовать мемоизацию, так как для меня она работает медленнее; в моем случае (до 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;
}
person call me Steve    schedule 11.09.2016

Если вы можете использовать встроенные функции компилятора, особенно с подсчетом и удалением завершающих нулей, вы поймете, что вам не нужно ветвление в основном цикле, вы всегда будете чередовать нечетные и четные числа. Методы запоминания, которые были представлены ранее, редко будут замыкать накоротко вокруг математики, которую вы делаете, поскольку мы имеем дело с числами градины — кроме того, у большинства чисел есть только одна точка входа, поэтому, если вы видите их один раз, вы больше никогда их не увижу.

В 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;
}
person Alex Shirley    schedule 17.05.2018

Если производительность критична, а память — нет, вы можете использовать кэширование для повышения скорости.

#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;
}
person iM71    schedule 26.03.2019