Consistent Collatz: Difference between revisions

From BusyBeaverWiki
Jump to navigation Jump to search
(a class of sequences, including that used by Hydra/Antihydra, that can be calculated in quasilinear time)
(No difference)

Revision as of 00:25, 4 August 2024

A consistent Collatz sequence is a sequence of integers which can be defined by a recurrence relation of the following form:

where the sequence itself is , and are positive integers, and is a map from integers modulo to (possibly negative) integers (with the values chosen to ensure that all are integers). The sequence is entirely defined by , , , and the value of its first element .

Many small cryptids work by calculating the elements of a consistent Collatz sequence modulo , and deciding whether or not to halt based on the remainders. For example, Hydra calculates the consistent Collatz sequence with , , , and , halting only if the sequence has contained more than twice as many even elements as odd elements. seems to be particularly common among small cryptids, due to creating the simplest consistent Collatz sequences that have nontrivial behaviour; however, other values have been observed, such as Bigfoot's .

Efficiently calculating consistent Collatz sequences

For any given Collatz sequence, it is possible to calculate its sequence of remainders in amortized quasilinear time. This can be accomplished via the use of two helper sequences:

, except
, with undefined (the algorithm never uses it)

where is the largest power of 2 that divides into .

Time complexity

The algorithm works by calculating then for each in turn, with the only operations used being additions, subtractions, and multiplications of numbers whose number of digits is proportional to ; division and modulo by powers of ; and calculation of with . Because the values of can be memoized, and it is possible to trivialise the division and modulus operations by storing the numbers in base , this means that the only slow operations are additions and subtractions taking time, and multiplications taking time (and there are such operations performed), so the calculation of each pair takes time quasilinear in . : as such, the calculation of the entire sequences and takes time quasilinear in , thus quasilinear in .

Details of the algorithm

The algorithm itself is:


(i.e. all the calculations are done modulo , saving time in cases where happens to be much larger than )
(which should always be an integer).

(If is odd, then , , and the sum in the calculation of is a degenerate sum with no elements.)

Sketch proof of correctness

The proof that the algorithm is correct starts with the definition , adds a degenerate sum (that sums to zero) to produce the following expression:

(with each element of the sum being of the form and thus 0)

and then rebrackets by grouping the term before the sum with the first half of the first element of the sum, the second half of the first element of the sum with the first half of the second element of the sum, etc., and finally the second half of the last element of the sum with the term after the sum. From there, the proof is mostly just a matter of expanding definitions.

Possible tricks to optimize the implementation

Once the algorithm produces a value of or , it happens that it will never again read a value or where . As such, it is possible to save memory by storing only one value of and one value of for each .

It is probably faster to switch to an alternative implementation (e.g. simple repeated multiplication) when the values are small, because at that point the numbers are small enough to fit into a machine register; this does not improve the asymptotic behaviour of the implementation but is likely to speed it up by a reasonably high constant factor.

Every is necessarily a multiple of . It seems like that might provide some sort of shortcut to calculate it faster, although the details are currently unclear.

Proof-of-concept implementation

Here's a proof-of-concept implementation, using Python 3 and gmpy2 (configured to calculate Hydra, but it could easily be adapted for other consistent Collatz sequences with ):

import gmpy2
import time

# The Rules of Hydra:
# if x_n = 2y + 0, then x_{n+1} = 3y + 0, i.e. 2x_{n+1} = 3x_n + 0
# if x_n = 2y + 1, then x_{n+1} = 3y + 1, i.e. 2x_{n+1} = 3x_n - 1
m = 2        # modulus; denominator of the ratio between successive elements
r = 3        # numerator of the ratio between successive elements
J = [0, -1]  # J[x_n % m] is the difference between m*x_{n+1} and r*x_n
x_0 = 3      # first term of the sequence

def f(n):
    """The largest power of 2 that divides into the argument"""
    return n & ~(n - 1)

global mod_m_exp
if m == 2:
    mod_m_exp = lambda v,e: gmpy2.f_mod_2exp(v, e)
else:
    mod_m_exp = lambda v,e: gmpy2.f_mod(v, gmpy2.pow(m, e))

global div_m_exp
if m == 2:
    div_m_exp = lambda v,e: gmpy2.f_div_2exp(v, e)
else:
    div_m_exp = lambda v,e: gmpy2.f_div(v, gmpy2.pow(m, e))

global mul_m_exp
if m == 2:
    mul_m_exp = lambda v,e: gmpy2.mpz(v) << e
else:
    mul_m_exp = lambda v,e: gmpy2.mul(v, gmpy2.pow(m, e))

r_exp_cache = {1: gmpy2.mpz(r)}
def r_exp(e):
    """Returns r raised to the power of e. e must be a power of 2."""
    global r_exp_cache
    if not e in r_exp_cache:
        r_exp_cache[e] = gmpy2.square(r_exp(e/2))
    return r_exp_cache[e]

# The bulk of the calculation is to calculate w_n and j_n for each n.
# The definitions are:
# w_n = x_n mod m**f(n)
# j_n = m**f(n) * x_n - r**f(n) * x_{n-f(n)}
# The output from the program is the sequence of x_n mod m.
# This can be calculated by taking the values of w_n mod m.
w = {0: x_0}  # most recently seen w for each f value
j = {}        # most recently seen j for each f value

modulus_count = {0: 0, 1: 0}

n = 0
perf_counter_timestamp = time.perf_counter_ns()
while True:
    last_f = f(n)
    last_x_mod_m = mod_m_exp(w[last_f], 1)
    # print(last_x_mod_m, end="")

    modulus_count[last_x_mod_m] += 1
    if n % 1000000 == 0:
        last_timestamp = perf_counter_timestamp
        perf_counter_timestamp = time.perf_counter_ns()
        print("Reached n = ", n, "; modulus counts: ", modulus_count,
              "; time for last 1000000 elements = ",
              (perf_counter_timestamp - last_timestamp) // 1000000,
              " ms", sep="")

    n = n + 1
    cur_f = f(n)

    # j can be a sum of multiple terms; J[last_x_mod_m] is always present
    # but if f > 1 there are other terms too
    m_shift = cur_f - 1
    r_shift = 1
    new_j = mul_m_exp(J[last_x_mod_m], m_shift)
    while m_shift >= r_shift:
        m_shift -= r_shift
        new_j += mul_m_exp(j[r_shift] * r_exp(r_shift), m_shift)
        r_shift *= 2
    j[cur_f] = new_j

    # w can be calculated directly from the new j and the appropriate past w
    wrap = lambda v: mod_m_exp(v, cur_f * 2)
    past_w = w[f(n - f(n))]
    new_w = wrap(wrap(new_j) + wrap(wrap(r_exp(cur_f)) * wrap(past_w)))
    assert(mod_m_exp(new_w, cur_f) == 0)
    new_w = div_m_exp(new_w, cur_f)
    w[cur_f] = new_w

This implementation is probably not suitable for serious use due to having poor constant factors: a faster implementation would use an alternative algorithm for low values. It is also intened only for , because the complexity result is dependent on modulus by powers of being fast regardless of the size of the dividend, but gmpy2 is only able to store numbers in binary and that operation is quick only if the modulus is a power of the base. As such, a full implementation of efficient consistent Collatz calculation would probably involve writing a new arbitrarily-large-integers library which is able to store numbers in arbitrary bases.