This is my second attempt to solve this problem, my first attempt is here: UVA 100: "The 3n + 1 problem"
This time I tried to take lessons from the failures and mistakes You pointed out in my first attempt.
- I resigned from packing multiple statements in one line. (This resulted in the code length balooning, though :( )
- I removed constructs that I had placed to “optimize” the code, but that were shown to be slowing it down instead, that is
std::pow
,std::log2
and most importantly,std::unordered_map
. - I did some actual benchmarking, and I think the code is reasonably fast now.
- I added comments whenever I thought my intent wasn’t obvious.
- However I fail to understand why, but OK, I’ve put away the
using namespace std
- I still tried to make the code error proof, for example by checking input correctness.
- I’ve put
assert
macros.
Any comments for this code?
// This program solves UVA Online Judge Problem 100: "The 3n + 1 Problem"
// Problem specification:
// https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=36
#ifdef ONLINE_JUDGE
#define NDEBUG
#endif
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <cmath>
#include <limits>
#include <cassert>
// aoo = At Or Operator[]
template<class Container>
typename Container::reference
aoo(Container &cont, typename Container::size_type index) {
#ifndef NDEBUG
return cont.operator[](index);
#else
return cont.at(index);
#endif
}
template<class Container>
typename Container::value_type::reference
aoo(Container &cont, typename Container::size_type i1,
typename Container::value_type::size_type i2) {
#ifndef NDEBUG
return cont.operator[](i1).operator[](i2);
#else
return cont.at(i1).at(i2);
#endif
}
using seqlen = std::uint_fast16_t;
using seqval = std::uint_fast32_t;
// As of now, the problem specification incorrectly states that all integers
// in the input will be less than 10.000. The correct boundaries, stating that
// integers will be less than 1.000.000, can be found in the archived version:
// web.archive.org/web/20161225044321/https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=36
constexpr seqval input_max = 999999;
using cache_t = std::vector<std::vector<seqlen>>;
static_assert(std::numeric_limits<cache_t::size_type>::max() >= input_max,
"This implementation cannot hold the cache.");
// cache[0] stores lengths of all values from 1 to input_max
// cache[1] stores maximums of lengths of 2-3, 4-5, etc
// cache[2] stores maximums of lengths of 4-7, 8-12, etc
// etc
cache_t cache = []()
{
//Log2 and pow are slow, but hopefully it doesn't matter in the initialization
cache_t ret(std::log2(input_max), cache_t::value_type{});
// 0 means unknown
for(cache_t::size_type i = 0; i < ret.size(); i++)
aoo(ret, i) = cache_t::value_type(input_max / std::pow(2,i), 0);
aoo(ret, 0, 0) = 1;
return ret;
}();
seqlen calculate_collatz_length(seqval n)
{
auto collatz_next = [](seqval n)
{
assert(n >= 1);
if(n%2 == 0)
return n/2;
else {
// Problem specification guarantees us that
// no operation overflows a 32bit integer.
assert(n <= (UINT32_MAX-1)/3);
return 3*n+1;
}
};
seqlen excessive_length = 0;
while(n > input_max)
{
n = collatz_next(n);
++excessive_length;
}
if(aoo(cache, 0, n-1)==0)
aoo(cache, 0, n-1) = calculate_collatz_length(collatz_next(n)) + 1;
return aoo(cache, 0, n-1) + excessive_length;
}
seqlen get_length_from_cache(seqval i, seqval j)
{
assert(1 <= i && i <= j && j <= input_max+1);
if(i == j)
return 0;
cache_t::size_type exponent;
seqval adji, adjj, interval;
auto adjust_bounds_to_multiplications_of_a_power_of_two = [&, i, j]()
{
assert(1 <= i && i < j && j <= input_max+1);
exponent = 0;
interval = 1;
auto adjusted_j = [j](seqval interval)
{return interval*(j/interval);};
auto adjusted_i = [i,j,adjusted_j](seqval interval)
{return adjusted_j(interval) - interval;};
auto broadened_i = [i](seqval interval)
{return i+interval;};
while(j >= broadened_i(4*interval) ||
adjusted_j(2*interval) >= broadened_i(2*interval))
{
interval *= 2;
exponent++;
}
assert(interval <= j-i);
assert(std::pow(2,exponent) == interval);
adjj = adjusted_j(interval);
adji = adjusted_i(interval);
assert(
adjj <= j &&
adji >= i &&
adjj % interval == 0 &&
adji % interval == 0 &&
adji + interval == adjj &&
adjusted_j(interval*2) < broadened_i(interval*2));
};
adjust_bounds_to_multiplications_of_a_power_of_two();
cache_t::value_type::size_type ind = adji/interval - 1;
if(aoo(cache, exponent, ind) == 0) {
if(exponent == 0)
aoo(cache, exponent, ind) = calculate_collatz_length(adji);
else {
seqval mid = adji + interval/2;
assert(mid+interval/2 == adjj);
aoo(cache, exponent, ind) =
std::max(get_length_from_cache(adji, mid),
get_length_from_cache(mid, adjj));
}
}
assert(
aoo(cache, exponent, ind) ==
*std::max_element(
aoo(cache, 0).begin()+adji-1,
aoo(cache, 0).begin()+adjj-1));
return std::max({
get_length_from_cache(i, adji),
aoo(cache, exponent, ind),
get_length_from_cache(adjj, j)
});
}
struct test_case
{
// long unsigned not seqval to avoid this:
// https://codereview.stackexchange.com/questions/146669/enforcing-correct-input-output-of-integers
long unsigned int i, j;
// Sole purpose of this operator: Check input correctness
friend std::istream &operator >> (std::istream &is, test_case &tc) try
{
std::string inpstr;
std::getline(is, inpstr);
if(!is.good()) {
if(is.eof()) {
// Either no characteres were read, in which case
// failbit is set already, or the input didn't end with '\n',
// which is invalid and we need to set failbit
is.setstate(std::ios_base::failbit);
}
return is;
}
std::stringstream inp(inpstr+'\n');
inp.exceptions(std::ios_base::badbit);
inp >> tc.i >> tc.j;
if(!inp.good()) {
is.setstate(inp.rdstate());
return is;
}
if(tc.i < 1 || tc.i > input_max || tc.j < 1 || tc.j > input_max) {
is.setstate(std::ios_base::failbit);
return is;
}
// No errors were detected, input is correct
return is;
} catch(...) {
is.setstate(std::ios_base::badbit);
return is;
}
};
int main()
{
#ifdef NDEBUG
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
#endif
std::cin.exceptions(std::ios_base::failbit | std::ios_base::badbit);
std::cout.exceptions
(std::ios_base::eofbit | std::ios_base::failbit | std::ios_base::badbit);
while(std::cin.peek() != decltype(std::cin)::traits_type::eof())
{
test_case tc;
std::cin >> tc;
// Unary + to avoid this:
// https://codereview.stackexchange.com/questions/146669/enforcing-correct-input-output-of-integers
std::cout << tc.i << ' ' << tc.j << ' ' <<
+get_length_from_cache(std::min(tc.i, tc.j),
std::max(tc.i, tc.j)+1) << '\n';
}
// On any error an exception should've been thrown somewhere above, so here
// we can announce SUCCESS
return EXIT_SUCCESS;
}
Edit: The file I used for benchmarking: http://www.filedropper.com/100_3