C++ Collatz 猜想优化
C++ Collatz Conjecture Optimization
ProjectEuler problem #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;
}
你有几个小问题:
- 我很确定您溢出了
int
数据类型。使用 uint64_t
来降低这种可能性。
- 您应该只在 while 循环之外更新
superI
和 superN
。这应该无关紧要,但会影响性能。
- 在每次迭代中,您只应修改
x
一次。您目前可能会修改它两次,这可能会导致您陷入无限循环。你的 n
计算也将关闭。
- 使用记忆通过缓存旧结果来提高性能。
应用这个,你可以想出一些这样的代码:
#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)下面的代码在没有记忆的情况下速度更快。
主要变化是:
- 仅测试当前数字是否为偶数一次(不需要 else-if)。
- 使用按位运算而不是模运算(稍微快一些)
除此之外我不知道为什么你的代码这么长(我的不到 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 << " milliseconds\n";
return EXIT_SUCCESS;
}
ProjectEuler problem #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;
}
你有几个小问题:
- 我很确定您溢出了
int
数据类型。使用uint64_t
来降低这种可能性。 - 您应该只在 while 循环之外更新
superI
和superN
。这应该无关紧要,但会影响性能。 - 在每次迭代中,您只应修改
x
一次。您目前可能会修改它两次,这可能会导致您陷入无限循环。你的n
计算也将关闭。 - 使用记忆通过缓存旧结果来提高性能。
应用这个,你可以想出一些这样的代码:
#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)下面的代码在没有记忆的情况下速度更快。 主要变化是:
- 仅测试当前数字是否为偶数一次(不需要 else-if)。
- 使用按位运算而不是模运算(稍微快一些)
除此之外我不知道为什么你的代码这么长(我的不到 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 << " milliseconds\n";
return EXIT_SUCCESS;
}