Problem 833: Square Triangle Products

View on Project Euler

Project Euler Problem 833 Solution

EulerSolve provides an optimized solution for Project Euler Problem 833, Square Triangle Products, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary Let \(T_n=\frac{n(n+1)}{2}\) be the \(n\)-th triangular number. The problem asks for the sum \(S(N)\) of all positive integers \(c\le N\) for which there exist two distinct triangular numbers \(T_a\) and \(T_b\) such that $$T_aT_b=c^2.$$ For the actual input \(N=10^{35}\), direct enumeration of triangular pairs is impossible. The successful approach groups triangular numbers by squarefree part, turns each group into a Pell-type family, and then sums the corresponding square roots modulo \(136101521\). Mathematical Approach The key is to understand when two triangular numbers have a square product. That happens exactly when they belong to the same squarefree class. Step 1: Reduce the Condition to a Pell-Type Equation If \(T_aT_b\) is a square, then \(T_a\) and \(T_b\) have the same squarefree part. So we can write $$T_a=d\,r^2,\qquad T_b=d\,s^2,$$ where \(d\) is squarefree. Then automatically $$c=d\,rs.$$ Thus, for each fixed squarefree \(d\), we need all triangular numbers of the form $$T_n=d\,y^2.$$ Using \(T_n=\frac{n(n+1)}{2}\) and \(x=2n+1\), this becomes $$x^2-8dy^2=1.$$ So every squarefree class of triangular numbers is controlled by a Pell equation. Step 2: Describe One Family with Chebyshev Polynomials Suppose a squarefree class has a fundamental representative \(T_k=d\)....

Detailed mathematical approach

Problem Summary

Let \(T_n=\frac{n(n+1)}{2}\) be the \(n\)-th triangular number. The problem asks for the sum \(S(N)\) of all positive integers \(c\le N\) for which there exist two distinct triangular numbers \(T_a\) and \(T_b\) such that

$$T_aT_b=c^2.$$

For the actual input \(N=10^{35}\), direct enumeration of triangular pairs is impossible. The successful approach groups triangular numbers by squarefree part, turns each group into a Pell-type family, and then sums the corresponding square roots modulo \(136101521\).

Mathematical Approach

The key is to understand when two triangular numbers have a square product. That happens exactly when they belong to the same squarefree class.

Step 1: Reduce the Condition to a Pell-Type Equation

If \(T_aT_b\) is a square, then \(T_a\) and \(T_b\) have the same squarefree part. So we can write

$$T_a=d\,r^2,\qquad T_b=d\,s^2,$$

where \(d\) is squarefree. Then automatically

$$c=d\,rs.$$

Thus, for each fixed squarefree \(d\), we need all triangular numbers of the form

$$T_n=d\,y^2.$$

Using \(T_n=\frac{n(n+1)}{2}\) and \(x=2n+1\), this becomes

$$x^2-8dy^2=1.$$

So every squarefree class of triangular numbers is controlled by a Pell equation.

Step 2: Describe One Family with Chebyshev Polynomials

Suppose a squarefree class has a fundamental representative \(T_k=d\). Then

$$d=T_k=\frac{k(k+1)}{2},\qquad x_1=2k+1.$$

The Pell family generated by this base produces all triangular numbers in the same squarefree class. The implementation expresses the square multipliers with Chebyshev polynomials of the second kind:

$$U_0(x)=1,\qquad U_1(x)=2x,\qquad U_{m+1}(x)=2x\,U_m(x)-U_{m-1}(x).$$

For this fixed base \(k\), the family of triangular numbers is

$$A_m(k)=T_k\,U_m(2k+1)^2,\qquad m=0,1,2,\dots$$

These are exactly the triangular numbers whose squarefree part is \(T_k\). The first term is \(A_0(k)=T_k\) because \(U_0=1\).

Step 3: Turn a Family into Square Triangle Products

Take two distinct members of the same family, say \(A_i(k)\) and \(A_j(k)\) with \(i<j\). Their product is

$$A_i(k)A_j(k)=\left(T_k\,U_i(2k+1)\,U_j(2k+1)\right)^2.$$

Therefore each admissible contribution to \(S(N)\) has the form

$$C_{i,j}(k)=T_k\,U_i(2k+1)\,U_j(2k+1),\qquad i<j.$$

So the entire problem becomes: enumerate all family pairs \((i,j)\), sum \(C_{i,j}(k)\) over all fundamental bases \(k\), and keep only the values not exceeding \(N\).

Step 4: Bound the Family Pairs and Sum over \(k\)

For fixed \(i\) and \(j\), the function \(C_{i,j}(k)\) is increasing in \(k\), because both \(T_k\) and \(U_m(2k+1)\) increase for \(k\ge 1\). The smallest base is \(k=1\), where \(2k+1=3\), so a necessary condition for the pair \((i,j)\) to contribute at all is

$$U_i(3)\,U_j(3)\le N.$$

This is why the implementations first build the short sequence

$$U_0(3),U_1(3),U_2(3),\dots=1,6,35,204,\dots$$

and keep only the pair indices whose minimum possible contribution is not already too large.

Once \((i,j)\) is fixed, there is a largest admissible base \(\kappa_{i,j}\) satisfying

$$C_{i,j}(k)\le N,\qquad 1\le k\le \kappa_{i,j}.$$

Because \(U_m(2k+1)\) is a polynomial in \(k\) of degree \(m\), the whole expression \(C_{i,j}(k)\) is a polynomial in \(k\). Hence

$$\sum_{k=1}^{\kappa_{i,j}} C_{i,j}(k)$$

can be reduced to a linear combination of power sums \(\sum_{k=1}^{\kappa_{i,j}} k^p\).

Step 5: Remove Non-Fundamental Bases

Not every base index \(k\) should start a new squarefree class. Some triangular numbers already occur inside an older family:

$$T_k=T_{k_1}\,U_r(2k_1+1)^2$$

for a smaller base \(k_1\). Such \(k\) are non-fundamental. If we summed over all \(k\ge 1\) blindly, those classes would be counted more than once.

The duplicate base indices are generated by the same Pell mechanism. Starting from \(x_1=2k_1+1\), define

$$x_0=1,\qquad x_{r+1}=2x_1x_r-x_{r-1},\qquad k_r=\frac{x_r-1}{2}.$$

Every \(k_r\) produced in this way belongs to the squarefree class already represented by \(k_1\), so its contribution must be subtracted from the all-\(k\) polynomial sum.

Step 6: Worked Example

Take the smallest fundamental base \(k=1\). Then

$$T_1=1,\qquad 2k+1=3,\qquad U_0(3)=1,\qquad U_1(3)=6,\qquad U_2(3)=35.$$

The corresponding triangular family begins with

$$A_0(1)=1,\qquad A_1(1)=1\cdot 6^2=36,\qquad A_2(1)=1\cdot 35^2=1225.$$

Now pair these family members:

$$1\cdot 36=6^2,\qquad 1\cdot 1225=35^2,\qquad 36\cdot 1225=210^2.$$

So \(6\), \(35\), and \(210\) are all square triangle products.

The same family also explains why some later bases are non-fundamental. The Pell recurrence from \(x_1=3\) gives

$$x_0=1,\qquad x_1=3,\qquad x_2=17,\qquad x_3=99,\dots$$

hence

$$k=0,1,8,49,\dots$$

and indeed

$$T_8=36,\qquad T_{49}=1225,$$

which were already present in the family of \(k=1\). Those later bases must therefore be excluded as new starting points.

Step 7: Final Summation Formula

If \(\mathcal{N}_{i,j}\) denotes the non-fundamental bases not exceeding the relevant bound for the pair \((i,j)\), then the target sum is

$$\boxed{S(N)=\sum_{i<j}\left(\sum_{k=1}^{\kappa_{i,j}} C_{i,j}(k)-\sum_{k\in \mathcal{N}_{i,j}} C_{i,j}(k)\right)\pmod{136101521}.}$$

This is exactly the structure implemented by the programs.

How the Code Works

The C++, Python, and Java implementations all follow the same pipeline. First they enumerate the admissible family pairs \((i,j)\) by growing the sequence \(U_m(3)\) until its products are too large to matter. For each surviving pair they find the largest admissible base by monotone binary search on \(C_{i,j}(k)\le N\).

Next they build the Chebyshev factors symbolically as polynomials in \(k\). Multiplying those polynomials by the triangular factor \(T_k=\frac{k(k+1)}{2}\) gives a polynomial representation of each contribution \(C_{i,j}(k)\). The full range sum for that pair is then evaluated through power sums, and those power sums are obtained modulo \(136101521\) by Lagrange-style interpolation of Faulhaber polynomials.

Finally the implementations generate every non-fundamental base only once, evaluate the corresponding pair contributions at those indices, and subtract them. The C++ implementation parallelizes this subtraction pass, while the Python and Java implementations perform the same arithmetic sequentially.

Complexity Analysis

Let \(F\) be the number of family levels with \(U_F(3)\le N\). Because \(U_m(3)\) grows exponentially, \(F=O(\log N)\), so the number of family pairs is \(O(F^2)\). Let \(B\) be the largest admissible base among all pairs, let \(D\) be the maximum polynomial degree, and let \(L\) be the number of non-fundamental bases up to \(B\).

Enumerating pair indices costs \(O(F^2)\). Finding all pair bounds costs \(O(F^2\log B)\) evaluations, each using a short Chebyshev recurrence. Polynomial preprocessing and power-sum evaluation are low-degree operations with total cost polynomial in \(F\), and the correction phase costs \(O(F^2L)\) in the straightforward form used here. Memory is dominated by the stored pair metadata, polynomial coefficients, and the correction list, so it is \(O(F^2D+L)\).

The important practical fact is that the family depth grows only logarithmically, which keeps the entire computation manageable even at \(N=10^{35}\).

Footnotes and References

  1. Project Euler Problem 833: https://projecteuler.net/problem=833
  2. Triangular numbers: Wikipedia — Triangular number
  3. Pell's equation: Wikipedia — Pell's equation
  4. Chebyshev polynomials: Wikipedia — Chebyshev polynomials
  5. Faulhaber's formula: Wikipedia — Faulhaber's formula
  6. Lagrange interpolation: Wikipedia — Lagrange polynomial

Problem 833 source code

C++

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <thread>
#include <vector>
#include <functional>

using namespace std;

namespace {

using int64 = long long;
using u64 = unsigned long long;
using i128 = __int128_t;
using u128 = __uint128_t;

constexpr int64 kMod = 136101521;

struct PairInfo {
    int i_idx;
    int j_idx;
    u64 k_max;
    vector<int64> poly; // coefficients mod kMod
    int64 sum_all = 0;
    int64 sum_nf = 0;
};

u128 pow10_u128(int exp) {
    u128 res = 1;
    for (int i = 0; i < exp; ++i) res *= 10;
    return res;
}

bool mul_leq(u128 a, u128 b, u128 limit, u128 &prod) {
    if (a == 0 || b == 0) {
        prod = 0;
        return true;
    }
    if (a > limit / b) return false;
    prod = a * b;
    return true;
}

// Evaluate c = T_k * U_{i_idx}(x) * U_{j_idx}(x) with x=2k+1.
// Returns true if c <= limit.
bool eval_pair_leq(u64 k, int i_idx, int j_idx, u128 limit) {
    u128 kk = static_cast<u128>(k);
    u128 x = 2 * kk + 1;
    int max_idx = max(i_idx, j_idx);
    vector<u128> U(max_idx + 1, 0);
    U[0] = 1;
    if (max_idx >= 1) U[1] = 2 * x;

    u128 two_x = 2 * x;
    for (int m = 1; m < max_idx; ++m) {
        if (U[m] > (limit + U[m - 1]) / two_x) {
            U[m + 1] = limit + 1;
        } else {
            u128 term = two_x * U[m];
            u128 next = term - U[m - 1];
            U[m + 1] = (next > limit) ? (limit + 1) : next;
        }
    }

    u128 t = kk * (kk + 1) / 2;
    u128 tmp = 0;
    if (!mul_leq(t, U[i_idx], limit, tmp)) return false;
    if (!mul_leq(tmp, U[j_idx], limit, tmp)) return false;
    return true;
}

u128 eval_pair_value(u64 k, int i_idx, int j_idx) {
    u128 kk = static_cast<u128>(k);
    u128 x = 2 * kk + 1;
    int max_idx = max(i_idx, j_idx);
    vector<u128> U(max_idx + 1, 0);
    U[0] = 1;
    if (max_idx >= 1) U[1] = 2 * x;
    for (int m = 1; m < max_idx; ++m) {
        U[m + 1] = 2 * x * U[m] - U[m - 1];
    }
    u128 t = kk * (kk + 1) / 2;
    return t * U[i_idx] * U[j_idx];
}

u64 max_k_for_pair(u128 n, int i_idx, int j_idx) {
    u64 lo = 0;
    u64 hi = 1;
    while (eval_pair_leq(hi, i_idx, j_idx, n)) {
        if (hi > (numeric_limits<u64>::max() / 2)) break;
        hi *= 2;
    }
    while (lo + 1 < hi) {
        u64 mid = lo + (hi - lo) / 2;
        if (eval_pair_leq(mid, i_idx, j_idx, n)) {
            lo = mid;
        } else {
            hi = mid;
        }
    }
    return lo;
}

vector<u64> generate_non_fundamentals(u64 k_max) {
    vector<u64> nf;
    if (k_max < 4) return nf;
    u64 k1_max = static_cast<u64>(std::sqrt(static_cast<long double>(k_max) / 4.0L)) + 2;
    for (u64 k1 = 1; k1 <= k1_max; ++k1) {
        u128 x1 = 2 * static_cast<u128>(k1) + 1;
        u128 x_prev = 1;
        u128 x_cur = x1;
        while (true) {
            u128 x_next = 2 * x1 * x_cur - x_prev;
            u64 k_next = static_cast<u64>((x_next - 1) / 2);
            if (k_next > k_max) break;
            nf.push_back(k_next);
            x_prev = x_cur;
            x_cur = x_next;
        }
    }
    sort(nf.begin(), nf.end());
    nf.erase(unique(nf.begin(), nf.end()), nf.end());
    return nf;
}

int64 mod_pow(int64 base, int64 exp) {
    int64 res = 1 % kMod;
    base %= kMod;
    while (exp > 0) {
        if (exp & 1) res = (static_cast<i128>(res) * base) % kMod;
        base = (static_cast<i128>(base) * base) % kMod;
        exp >>= 1;
    }
    return res;
}

int64 mod_inv(int64 a) {
    return mod_pow(a, kMod - 2);
}

using Poly = vector<int64>;

Poly poly_add(const Poly &a, const Poly &b) {
    size_t n = max(a.size(), b.size());
    Poly c(n, 0);
    for (size_t i = 0; i < n; ++i) {
        int64 va = (i < a.size()) ? a[i] : 0;
        int64 vb = (i < b.size()) ? b[i] : 0;
        int64 v = va + vb;
        if (v >= kMod) v -= kMod;
        c[i] = v;
    }
    return c;
}

Poly poly_sub(const Poly &a, const Poly &b) {
    size_t n = max(a.size(), b.size());
    Poly c(n, 0);
    for (size_t i = 0; i < n; ++i) {
        int64 va = (i < a.size()) ? a[i] : 0;
        int64 vb = (i < b.size()) ? b[i] : 0;
        int64 v = va - vb;
        if (v < 0) v += kMod;
        c[i] = v;
    }
    return c;
}

Poly poly_mul(const Poly &a, const Poly &b) {
    Poly c(a.size() + b.size() - 1, 0);
    for (size_t i = 0; i < a.size(); ++i) {
        for (size_t j = 0; j < b.size(); ++j) {
            c[i + j] = (c[i + j] + static_cast<i128>(a[i]) * b[j]) % kMod;
        }
    }
    return c;
}

Poly poly_scale(const Poly &a, int64 s) {
    Poly c(a.size(), 0);
    for (size_t i = 0; i < a.size(); ++i) {
        c[i] = static_cast<i128>(a[i]) * s % kMod;
    }
    return c;
}

vector<Poly> build_U_polys(int max_idx) {
    Poly t = {1, 2}; // 2k+1
    vector<Poly> U(max_idx + 1);
    U[0] = {1};
    if (max_idx >= 1) {
        U[1] = poly_scale(t, 2);
    }
    for (int m = 1; m < max_idx; ++m) {
        Poly temp = poly_mul(t, U[m]);
        temp = poly_scale(temp, 2);
        U[m + 1] = poly_sub(temp, U[m - 1]);
    }
    return U;
}

struct SumPowers {
    vector<vector<int64>> y;
    vector<int64> fact;
    vector<int64> invfact;
};

SumPowers precompute_sum_pows(int max_p) {
    SumPowers sp;
    sp.y.resize(max_p + 1);
    for (int p = 0; p <= max_p; ++p) {
        int m = p + 1;
        sp.y[p].assign(m + 1, 0);
        int64 s = 0;
        for (int i = 1; i <= m; ++i) {
            s += mod_pow(i, p);
            s %= kMod;
            sp.y[p][i] = s;
        }
    }
    sp.fact.assign(max_p + 2, 1);
    sp.invfact.assign(max_p + 2, 1);
    for (int i = 1; i < static_cast<int>(sp.fact.size()); ++i) {
        sp.fact[i] = static_cast<i128>(sp.fact[i - 1]) * i % kMod;
    }
    sp.invfact.back() = mod_inv(sp.fact.back());
    for (int i = static_cast<int>(sp.fact.size()) - 1; i >= 1; --i) {
        sp.invfact[i - 1] = static_cast<i128>(sp.invfact[i]) * i % kMod;
    }
    return sp;
}

int64 sum_pows(int p, u64 n, const SumPowers &sp) {
    if (n <= static_cast<u64>(p + 1)) {
        return sp.y[p][static_cast<size_t>(n)];
    }
    int m = p + 1;
    vector<int64> pre(m + 2, 1), suf(m + 2, 1);
    int64 nmod = static_cast<int64>(n % kMod);
    for (int i = 0; i <= m; ++i) {
        int64 term = nmod - i;
        if (term < 0) term += kMod;
        pre[i + 1] = static_cast<i128>(pre[i]) * term % kMod;
    }
    for (int i = m; i >= 0; --i) {
        int64 term = nmod - i;
        if (term < 0) term += kMod;
        suf[i] = static_cast<i128>(suf[i + 1]) * term % kMod;
    }
    int64 res = 0;
    for (int i = 0; i <= m; ++i) {
        int64 num = static_cast<i128>(pre[i]) * suf[i + 1] % kMod;
        int64 denom_inv = static_cast<i128>(sp.invfact[i]) * sp.invfact[m - i] % kMod;
        int64 term = static_cast<i128>(sp.y[p][i]) * num % kMod;
        term = static_cast<i128>(term) * denom_inv % kMod;
        if ((m - i) & 1) {
            if (term != 0) term = kMod - term;
        }
        res += term;
        if (res >= kMod) res -= kMod;
    }
    return res;
}

int64 sum_poly(const Poly &coeffs, u64 n, const SumPowers &sp) {
    int64 res = 0;
    for (size_t p = 0; p < coeffs.size(); ++p) {
        if (coeffs[p] == 0) continue;
        int64 spow = sum_pows(static_cast<int>(p), n, sp);
        res = (res + static_cast<i128>(coeffs[p]) * spow) % kMod;
    }
    return res;
}

vector<PairInfo> build_pairs(u128 n) {
    vector<u128> U3;
    U3.push_back(1);
    U3.push_back(6);
    while (true) {
        u128 next = 2 * static_cast<u128>(3) * U3[U3.size() - 1] - U3[U3.size() - 2];
        U3.push_back(next);
        if (next > n) break;
    }
    int max_idx = static_cast<int>(U3.size()) - 1;

    vector<PairInfo> pairs;
    for (int i = 0; i <= max_idx; ++i) {
        for (int j = i + 1; j <= max_idx; ++j) {
            u128 prod = 0;
            if (!mul_leq(U3[i], U3[j], n, prod)) continue;
            PairInfo info;
            info.i_idx = i;
            info.j_idx = j;
            pairs.push_back(info);
        }
    }
    return pairs;
}

u64 compute_exact_small(u64 n) {
    u128 n128 = static_cast<u128>(n);
    vector<PairInfo> pairs = build_pairs(n128);
    u64 k_max = 0;
    for (auto &p : pairs) {
        p.k_max = max_k_for_pair(n128, p.i_idx, p.j_idx);
        k_max = max(k_max, p.k_max);
    }
    vector<u64> nf = generate_non_fundamentals(k_max);
    vector<char> is_nf(k_max + 1, 0);
    for (u64 v : nf) if (v <= k_max) is_nf[v] = 1;

    u128 total = 0;
    for (const auto &p : pairs) {
        for (u64 k = 1; k <= p.k_max; ++k) {
            if (is_nf[k]) continue;
            u128 c = eval_pair_value(k, p.i_idx, p.j_idx);
            if (c > n128) break;
            total += c;
        }
    }
    return static_cast<u64>(total);
}

int64 compute_mod(u128 n) {
    vector<PairInfo> pairs = build_pairs(n);
    int max_idx = 0;
    u64 k_max = 0;
    for (auto &p : pairs) {
        p.k_max = max_k_for_pair(n, p.i_idx, p.j_idx);
        k_max = max(k_max, p.k_max);
        max_idx = max(max_idx, max(p.i_idx, p.j_idx));
    }

    vector<u64> nf = generate_non_fundamentals(k_max);

    const int64 inv2 = mod_inv(2);
    vector<Poly> U_polys = build_U_polys(max_idx);
    Poly t_poly = {0, inv2, inv2};
    int deg_max = 0;
    for (auto &p : pairs) {
        Poly r = poly_mul(U_polys[p.i_idx], U_polys[p.j_idx]);
        p.poly = poly_mul(r, t_poly);
        deg_max = max(deg_max, static_cast<int>(p.poly.size()) - 1);
    }

    SumPowers sp = precompute_sum_pows(deg_max);
    for (auto &p : pairs) {
        p.sum_all = sum_poly(p.poly, p.k_max, sp);
    }

    // Parallel non-fundamental contributions.
    const unsigned int hw = thread::hardware_concurrency();
    const unsigned int threads = (hw == 0) ? 4u : min(hw, 8u);
    vector<vector<int64>> local_sums(threads, vector<int64>(pairs.size(), 0));
    vector<thread> workers;

    auto worker = [&](unsigned int tid, size_t start, size_t end) {
        vector<int64> U(max_idx + 1, 0);
        for (size_t idx = start; idx < end; ++idx) {
            u64 k = nf[idx];
            u64 k_mod = k % kMod;
            int64 x_mod = static_cast<int64>((2 * (k_mod % kMod) + 1) % kMod);
            U[0] = 1;
            if (max_idx >= 1) U[1] = (2LL * x_mod) % kMod;
            for (int m = 1; m < max_idx; ++m) {
                int64 next = (2LL * x_mod % kMod * U[m] - U[m - 1]) % kMod;
                if (next < 0) next += kMod;
                U[m + 1] = next;
            }
            int64 t = static_cast<int64>(k_mod) * static_cast<int64>((k_mod + 1) % kMod) % kMod;
            t = static_cast<int64>(static_cast<i128>(t) * inv2 % kMod);

            auto &sum_nf = local_sums[tid];
            for (size_t p = 0; p < pairs.size(); ++p) {
                if (k > pairs[p].k_max) continue;
                int64 val = static_cast<int64>(static_cast<i128>(t) * U[pairs[p].i_idx] % kMod);
                val = static_cast<int64>(static_cast<i128>(val) * U[pairs[p].j_idx] % kMod);
                sum_nf[p] += val;
                if (sum_nf[p] >= kMod) sum_nf[p] -= kMod;
            }
        }
    };

    size_t chunk = (nf.size() + threads - 1) / threads;
    for (unsigned int t = 0; t < threads; ++t) {
        size_t start = t * chunk;
        size_t end = min(nf.size(), start + chunk);
        workers.emplace_back(worker, t, start, end);
    }
    for (auto &th : workers) th.join();

    for (size_t p = 0; p < pairs.size(); ++p) {
        int64 total = 0;
        for (unsigned int t = 0; t < threads; ++t) {
            total += local_sums[t][p];
            if (total >= kMod) total -= kMod;
        }
        pairs[p].sum_nf = total;
    }

    int64 ans = 0;
    for (const auto &p : pairs) {
        int64 term = p.sum_all - p.sum_nf;
        if (term < 0) term += kMod;
        ans += term;
        if (ans >= kMod) ans -= kMod;
    }
    return ans;
}

} // namespace

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    // Validation checks.
    if (compute_exact_small(100000) != 1479802ULL) {
        cerr << "Validation failed: S(1e5) mismatch\n";
        return 1;
    }
    if (compute_exact_small(1000000000ULL) != 241614948794ULL) {
        cerr << "Validation failed: S(1e9) mismatch\n";
        return 1;
    }

    u128 n = pow10_u128(35);
    int64 ans = compute_mod(n);
    cout << ans << "\n";
    return 0;
}

Python

import math

kMod = 136101521

def max_k_for_pair(n_val, i_idx, j_idx):
    def eval_pair_leq(k):
        x = 2 * k + 1
        max_idx = max(i_idx, j_idx)
        U = [0] * (max_idx + 1)
        U[0] = 1
        if max_idx >= 1: U[1] = 2 * x
        
        two_x = 2 * x
        for m in range(1, max_idx):
            if U[m] > (n_val + U[m - 1]) // two_x:
                U[m + 1] = n_val + 1
            else:
                nex = two_x * U[m] - U[m - 1]
                U[m + 1] = nex if nex <= n_val else n_val + 1
                
        t = k * (k + 1) // 2
        tmp = t * U[i_idx]
        if tmp > n_val: return False
        tmp *= U[j_idx]
        if tmp > n_val: return False
        return True

    lo = 0
    hi = 1
    while eval_pair_leq(hi):
        hi *= 2
    while lo + 1 < hi:
        mid = (lo + hi) // 2
        if eval_pair_leq(mid):
            lo = mid
        else:
            hi = mid
    return lo

def generate_non_fundamentals(k_max):
    nf = []
    if k_max < 4: return nf
    k1_max = int(math.sqrt(k_max / 4.0)) + 2
    for k1 in range(1, k1_max + 1):
        x1 = 2 * k1 + 1
        x_prev = 1
        x_cur = x1
        while True:
            x_next = 2 * x1 * x_cur - x_prev
            k_next = (x_next - 1) // 2
            if k_next > k_max: break
            nf.append(k_next)
            x_prev = x_cur
            x_cur = x_next
    return sorted(list(set(nf)))

def mod_pow(base, exp):
    res = 1 % kMod
    base %= kMod
    while exp > 0:
        if exp & 1: res = (res * base) % kMod
        base = (base * base) % kMod
        exp >>= 1
    return res

def mod_inv(a):
    return mod_pow(a, kMod - 2)

def poly_add(a, b):
    n = max(len(a), len(b))
    c = [0] * n
    for i in range(n):
        va = a[i] if i < len(a) else 0
        vb = b[i] if i < len(b) else 0
        c[i] = (va + vb) % kMod
    return c

def poly_sub(a, b):
    n = max(len(a), len(b))
    c = [0] * n
    for i in range(n):
        va = a[i] if i < len(a) else 0
        vb = b[i] if i < len(b) else 0
        c[i] = (va - vb) % kMod
    return c

def poly_mul(a, b):
    c = [0] * (len(a) + len(b) - 1)
    for i in range(len(a)):
        for j in range(len(b)):
            c[i + j] = (c[i + j] + a[i] * b[j]) % kMod
    return c

def poly_scale(a, s):
    return [(x * s) % kMod for x in a]

def build_U_polys(max_idx):
    t = [1, 2] # 2k+1
    U = [[0]] * (max_idx + 1)
    U[0] = [1]
    if max_idx >= 1:
        U[1] = poly_scale(t, 2)
    for m in range(1, max_idx):
        temp = poly_mul(t, U[m])
        temp = poly_scale(temp, 2)
        U[m + 1] = poly_sub(temp, U[m - 1])
    return U

class SumPowers:
    def __init__(self, y, fact, invfact):
        self.y = y
        self.fact = fact
        self.invfact = invfact

def precompute_sum_pows(max_p):
    y = [[0] * (p + 2) for p in range(max_p + 1)]
    for p in range(max_p + 1):
        m = p + 1
        s = 0
        for i in range(1, m + 1):
            s = (s + mod_pow(i, p)) % kMod
            y[p][i] = s
            
    fact = [1] * (max_p + 2)
    invfact = [1] * (max_p + 2)
    for i in range(1, len(fact)):
        fact[i] = (fact[i - 1] * i) % kMod
    invfact[-1] = mod_inv(fact[-1])
    for i in range(len(fact) - 1, 0, -1):
        invfact[i - 1] = (invfact[i] * i) % kMod
        
    return SumPowers(y, fact, invfact)

def sum_pows(p, n, sp):
    if n <= p + 1:
        return sp.y[p][n]
        
    m = p + 1
    pre = [1] * (m + 2)
    suf = [1] * (m + 2)
    nmod = n % kMod
    
    for i in range(m + 1):
        pre[i + 1] = (pre[i] * (nmod - i)) % kMod
    for i in range(m, -1, -1):
        suf[i] = (suf[i + 1] * (nmod - i)) % kMod
        
    res = 0
    for i in range(m + 1):
        num = (pre[i] * suf[i + 1]) % kMod
        denom_inv = (sp.invfact[i] * sp.invfact[m - i]) % kMod
        term = (sp.y[p][i] * num) % kMod
        term = (term * denom_inv) % kMod
        if (m - i) & 1:
            if term != 0: term = kMod - term
        res = (res + term) % kMod
    return res

def sum_poly(coeffs, n, sp):
    res = 0
    for p in range(len(coeffs)):
        if coeffs[p] == 0: continue
        spow = sum_pows(p, n, sp)
        res = (res + coeffs[p] * spow) % kMod
    return res

def build_pairs(n_val):
    U3 = [1, 6]
    while True:
        nex = 2 * 3 * U3[-1] - U3[-2]
        U3.append(nex)
        if nex > n_val: break
    
    max_idx = len(U3) - 1
    pairs = []
    for i in range(max_idx + 1):
        for j in range(i + 1, max_idx + 1):
            tmp = U3[i] * U3[j]
            if tmp <= n_val:
                pairs.append({'i_idx': i, 'j_idx': j})
    return pairs

def compute_mod(n_val):
    pairs = build_pairs(n_val)
    max_idx = 0
    k_max = 0
    for p in pairs:
        p['k_max'] = max_k_for_pair(n_val, p['i_idx'], p['j_idx'])
        k_max = max(k_max, p['k_max'])
        max_idx = max(max_idx, p['i_idx'], p['j_idx'])
        
    nf = generate_non_fundamentals(k_max)
    nf_set = set(nf)
    
    inv2 = mod_inv(2)
    U_polys = build_U_polys(max_idx)
    t_poly = [0, inv2, inv2]
    deg_max = 0
    for p in pairs:
        r = poly_mul(U_polys[p['i_idx']], U_polys[p['j_idx']])
        p['poly'] = poly_mul(r, t_poly)
        deg_max = max(deg_max, len(p['poly']) - 1)
        
    sp = precompute_sum_pows(deg_max)
    for p in pairs:
        p['sum_all'] = sum_poly(p['poly'], p['k_max'], sp)
        
    for p in pairs:
        p['sum_nf'] = 0
        for k in nf:
            if k > p['k_max']: continue
            k_mod = k % kMod
            x_mod = (2 * k_mod + 1) % kMod
            U = [0] * (max_idx + 1)
            U[0] = 1
            if max_idx >= 1: U[1] = (2 * x_mod) % kMod
            for m in range(1, max_idx):
                U[m + 1] = (2 * x_mod * U[m] - U[m - 1]) % kMod
            t = (k_mod * (k_mod + 1)) % kMod
            t = (t * inv2) % kMod
            val = (t * U[p['i_idx']]) % kMod
            val = (val * U[p['j_idx']]) % kMod
            p['sum_nf'] = (p['sum_nf'] + val) % kMod
            
    ans = 0
    for p in pairs:
        term = (p['sum_all'] - p['sum_nf']) % kMod
        ans = (ans + term) % kMod
        
    return ans

def solve():
    n = 10**35
    ans = compute_mod(n)
    return str(ans)

if __name__ == "__main__":
    print(solve())

Java

import java.util.*;

public class Euler833 {

    static final long kMod = 136101521L;

    static class PairInfo {
        int iIdx;
        int jIdx;
        long kMax;
        long[] poly;
        long sumAll = 0;
        long sumNf = 0;
    }

    static boolean mulLeq(long a, long b, long limit) {
        if (a == 0 || b == 0)
            return true;
        if (a > limit / b)
            return false;
        return true;
    }

    static boolean evalPairLeq(long k, int iIdx, int jIdx, double limit) {
        // Since limit could be 10^35, we can't do exact long.
        // We'll use BigInteger maybe? Or simply approximate with double for upper
        // bounds.
        // wait, 10^35 fits in standard double ~1.7e308. Wait, exact arithmetic is
        // better.
        return true;
    }

    // Implementing exact 128 bit behavior with BigInteger
    static java.math.BigInteger toBI(long v) {
        return java.math.BigInteger.valueOf(v);
    }

    static boolean mulLeq(java.math.BigInteger a, java.math.BigInteger b, java.math.BigInteger limit,
            java.math.BigInteger[] out) {
        if (a.equals(java.math.BigInteger.ZERO) || b.equals(java.math.BigInteger.ZERO)) {
            out[0] = java.math.BigInteger.ZERO;
            return true;
        }
        java.math.BigInteger p = a.multiply(b);
        if (p.compareTo(limit) > 0)
            return false;
        out[0] = p;
        return true;
    }

    static boolean evalPairLeq(long k, int iIdx, int jIdx, java.math.BigInteger limit) {
        java.math.BigInteger kk = toBI(k);
        java.math.BigInteger x = kk.multiply(toBI(2)).add(java.math.BigInteger.ONE);
        int maxIdx = Math.max(iIdx, jIdx);
        java.math.BigInteger[] U = new java.math.BigInteger[maxIdx + 1];
        U[0] = java.math.BigInteger.ONE;
        if (maxIdx >= 1)
            U[1] = x.multiply(toBI(2));

        java.math.BigInteger twoX = x.multiply(toBI(2));
        for (int m = 1; m < maxIdx; ++m) {
            java.math.BigInteger thresh = limit.add(U[m - 1]).divide(twoX);
            if (U[m].compareTo(thresh) > 0) {
                U[m + 1] = limit.add(java.math.BigInteger.ONE);
            } else {
                java.math.BigInteger next = twoX.multiply(U[m]).subtract(U[m - 1]);
                U[m + 1] = next.compareTo(limit) > 0 ? limit.add(java.math.BigInteger.ONE) : next;
            }
        }

        java.math.BigInteger t = kk.multiply(kk.add(java.math.BigInteger.ONE)).divide(toBI(2));
        java.math.BigInteger[] tmp = new java.math.BigInteger[1];
        if (!mulLeq(t, U[iIdx], limit, tmp))
            return false;
        if (!mulLeq(tmp[0], U[jIdx], limit, tmp))
            return false;
        return true;
    }

    static long maxKForPair(java.math.BigInteger n, int iIdx, int jIdx) {
        long lo = 0;
        long hi = 1;
        while (evalPairLeq(hi, iIdx, jIdx, n)) {
            if (hi > (Long.MAX_VALUE / 2))
                break;
            hi *= 2;
        }
        while (lo + 1 < hi) {
            long mid = lo + (hi - lo) / 2;
            if (evalPairLeq(mid, iIdx, jIdx, n)) {
                lo = mid;
            } else {
                hi = mid;
            }
        }
        return lo;
    }

    static ArrayList<Long> generateNonFundamentals(long kMax) {
        ArrayList<Long> nf = new ArrayList<>();
        if (kMax < 4)
            return nf;
        long k1Max = (long) Math.sqrt((double) kMax / 4.0) + 2;
        for (long k1 = 1; k1 <= k1Max; ++k1) {
            java.math.BigInteger x1 = toBI(2 * k1 + 1);
            java.math.BigInteger xPrev = java.math.BigInteger.ONE;
            java.math.BigInteger xCur = x1;
            while (true) {
                java.math.BigInteger xNext = toBI(2).multiply(x1).multiply(xCur).subtract(xPrev);
                java.math.BigInteger kNextBI = xNext.subtract(java.math.BigInteger.ONE).divide(toBI(2));
                if (kNextBI.compareTo(toBI(kMax)) > 0)
                    break;
                nf.add(kNextBI.longValue());
                xPrev = xCur;
                xCur = xNext;
            }
        }
        Collections.sort(nf);
        ArrayList<Long> distinctNf = new ArrayList<>();
        for (long v : nf) {
            if (distinctNf.isEmpty() || distinctNf.get(distinctNf.size() - 1) != v) {
                distinctNf.add(v);
            }
        }
        return distinctNf;
    }

    static long modPow(long base, long exp) {
        long res = 1 % kMod;
        base %= kMod;
        while (exp > 0) {
            if ((exp & 1) == 1)
                res = (res * base) % kMod;
            base = (base * base) % kMod;
            exp >>= 1;
        }
        return res;
    }

    static long modInv(long a) {
        return modPow(a, kMod - 2);
    }

    static long[] polyScale(long[] a, long s) {
        long[] c = new long[a.length];
        for (int i = 0; i < a.length; ++i) {
            c[i] = (a[i] * s) % kMod;
        }
        return c;
    }

    static long[] polySub(long[] a, long[] b) {
        int n = Math.max(a.length, b.length);
        long[] c = new long[n];
        for (int i = 0; i < n; ++i) {
            long va = i < a.length ? a[i] : 0;
            long vb = i < b.length ? b[i] : 0;
            c[i] = (va - vb) % kMod;
            if (c[i] < 0)
                c[i] += kMod;
        }
        return c;
    }

    static long[] polyMul(long[] a, long[] b) {
        long[] c = new long[a.length + b.length - 1];
        for (int i = 0; i < a.length; ++i) {
            for (int j = 0; j < b.length; ++j) {
                c[i + j] = (c[i + j] + a[i] * b[j]) % kMod;
            }
        }
        return c;
    }

    static long[][] buildUPolys(int maxIdx) {
        long[] t = { 1, 2 };
        long[][] U = new long[maxIdx + 1][];
        U[0] = new long[] { 1 };
        if (maxIdx >= 1)
            U[1] = polyScale(t, 2);

        for (int m = 1; m < maxIdx; ++m) {
            long[] temp = polyMul(t, U[m]);
            temp = polyScale(temp, 2);
            U[m + 1] = polySub(temp, U[m - 1]);
        }
        return U;
    }

    static class SumPowers {
        long[][] y;
        long[] fact;
        long[] invfact;
    }

    static SumPowers precomputeSumPows(int maxP) {
        SumPowers sp = new SumPowers();
        sp.y = new long[maxP + 1][maxP + 2];
        for (int p = 0; p <= maxP; ++p) {
            int m = p + 1;
            long s = 0;
            for (int i = 1; i <= m; ++i) {
                s = (s + modPow(i, p)) % kMod;
                sp.y[p][i] = s;
            }
        }
        sp.fact = new long[maxP + 2];
        sp.invfact = new long[maxP + 2];
        sp.fact[0] = 1;
        sp.invfact[0] = 1;
        for (int i = 1; i < sp.fact.length; ++i) {
            sp.fact[i] = (sp.fact[i - 1] * i) % kMod;
        }
        sp.invfact[sp.invfact.length - 1] = modInv(sp.fact[sp.fact.length - 1]);
        for (int i = sp.fact.length - 1; i >= 1; --i) {
            sp.invfact[i - 1] = (sp.invfact[i] * i) % kMod;
        }
        return sp;
    }

    static long sumPows(int p, long n, SumPowers sp) {
        if (n <= p + 1) {
            return sp.y[p][(int) n];
        }
        int m = p + 1;
        long[] pre = new long[m + 2];
        long[] suf = new long[m + 2];
        pre[0] = 1;
        suf[m + 1] = 1;
        long nmod = n % kMod;

        for (int i = 0; i <= m; ++i) {
            long term = (nmod - i) % kMod;
            if (term < 0)
                term += kMod;
            pre[i + 1] = (pre[i] * term) % kMod;
        }
        for (int i = m; i >= 0; --i) {
            long term = (nmod - i) % kMod;
            if (term < 0)
                term += kMod;
            suf[i] = (suf[i + 1] * term) % kMod;
        }

        long res = 0;
        for (int i = 0; i <= m; ++i) {
            long num = (pre[i] * suf[i + 1]) % kMod;
            long denomInv = (sp.invfact[i] * sp.invfact[m - i]) % kMod;
            long term = (sp.y[p][i] * num) % kMod;
            term = (term * denomInv) % kMod;
            if ((m - i) % 2 != 0) {
                if (term != 0)
                    term = kMod - term;
            }
            res = (res + term) % kMod;
        }
        return res;
    }

    static long sumPoly(long[] coeffs, long n, SumPowers sp) {
        long res = 0;
        for (int p = 0; p < coeffs.length; ++p) {
            if (coeffs[p] == 0)
                continue;
            long spow = sumPows(p, n, sp);
            res = (res + coeffs[p] * spow) % kMod;
        }
        return res;
    }

    public static String solve() {
        java.math.BigInteger n = java.math.BigInteger.TEN.pow(35);

        ArrayList<java.math.BigInteger> U3 = new ArrayList<>();
        U3.add(java.math.BigInteger.ONE);
        U3.add(toBI(6));
        while (true) {
            java.math.BigInteger next = toBI(6).multiply(U3.get(U3.size() - 1)).subtract(U3.get(U3.size() - 2));
            U3.add(next);
            if (next.compareTo(n) > 0)
                break;
        }

        int maxIdx = U3.size() - 1;
        ArrayList<PairInfo> pairs = new ArrayList<>();

        for (int i = 0; i <= maxIdx; ++i) {
            for (int j = i + 1; j <= maxIdx; ++j) {
                java.math.BigInteger[] tmp = new java.math.BigInteger[1];
                if (!mulLeq(U3.get(i), U3.get(j), n, tmp))
                    continue;
                PairInfo info = new PairInfo();
                info.iIdx = i;
                info.jIdx = j;
                pairs.add(info);
            }
        }

        long maxK = 0;
        for (PairInfo p : pairs) {
            p.kMax = maxKForPair(n, p.iIdx, p.jIdx);
            maxK = Math.max(maxK, p.kMax);
        }

        ArrayList<Long> nf = generateNonFundamentals(maxK);

        long inv2 = modInv(2);
        long[][] UPolys = buildUPolys(maxIdx);
        long[] tPoly = { 0, inv2, inv2 };
        int degMax = 0;

        for (PairInfo p : pairs) {
            long[] r = polyMul(UPolys[p.iIdx], UPolys[p.jIdx]);
            p.poly = polyMul(r, tPoly);
            degMax = Math.max(degMax, p.poly.length - 1);
        }

        SumPowers sp = precomputeSumPows(degMax);
        for (PairInfo p : pairs) {
            p.sumAll = sumPoly(p.poly, p.kMax, sp);
        }

        for (PairInfo p : pairs) {
            p.sumNf = 0;
            for (long k : nf) {
                if (k > p.kMax)
                    continue;
                long kModV = k % kMod;
                long xMod = (2 * kModV + 1) % kMod;
                long[] U = new long[maxIdx + 1];
                U[0] = 1;
                if (maxIdx >= 1)
                    U[1] = (2 * xMod) % kMod;
                for (int m = 1; m < maxIdx; ++m) {
                    U[m + 1] = (2 * xMod * U[m] - U[m - 1]) % kMod;
                    if (U[m + 1] < 0)
                        U[m + 1] += kMod;
                }
                long t = (kModV * ((kModV + 1) % kMod)) % kMod;
                t = (t * inv2) % kMod;

                long val = (t * U[p.iIdx]) % kMod;
                val = (val * U[p.jIdx]) % kMod;

                p.sumNf = (p.sumNf + val) % kMod;
            }
        }

        long ans = 0;
        for (PairInfo p : pairs) {
            long term = (p.sumAll - p.sumNf) % kMod;
            if (term < 0)
                term += kMod;
            ans = (ans + term) % kMod;
        }

        return Long.toString(ans);
    }

    public static void main(String[] args) {
        System.out.println(solve());
    }
}