Problem 408: Admissible Paths Through a Grid

View on Project Euler

Project Euler Problem 408 Solution

EulerSolve provides an optimized solution for Project Euler Problem 408, Admissible Paths Through a Grid, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary We count monotone lattice paths from \((0,0)\) to \((n,n)\), where each step is either one unit right or one unit up. A path is admissible if it never visits any forbidden grid point. In the implementations, the forbidden points are exactly the interior square-coordinate points $$F_n=\{(a^2,b^2): 1 \le a,b \le \lfloor \sqrt{n}\rfloor,\ \exists c \in \mathbb{Z}_{>0}: a^2+b^2=c^2\}.$$ The final answer is the number of admissible paths modulo \(10^9+7\). Mathematical Approach Step 1: Count paths between two comparable points If \((x_1,y_1)\) and \((x_2,y_2)\) satisfy \(x_1 \le x_2\) and \(y_1 \le y_2\), then any monotone path from the first point to the second point must use exactly \(x_2-x_1\) right moves and \(y_2-y_1\) up moves. Hence the number of such paths is $$\binom{(x_2-x_1)+(y_2-y_1)}{x_2-x_1}.$$ In particular, without any forbidden points, the total number of paths from \((0,0)\) to \((n,n)\) is $$\binom{2n}{n}.$$ Step 2: Describe the forbidden set Only points with both coordinates at most \(n\) can matter, so it is enough to enumerate \(a,b \le \lfloor \sqrt{n}\rfloor\). The condition \(a^2+b^2=c^2\) means that \((a,b,c)\) forms a Pythagorean triple, and each such pair produces a forbidden point \((a^2,b^2)\)....

Detailed mathematical approach

Problem Summary

We count monotone lattice paths from \((0,0)\) to \((n,n)\), where each step is either one unit right or one unit up. A path is admissible if it never visits any forbidden grid point.

In the implementations, the forbidden points are exactly the interior square-coordinate points

$$F_n=\{(a^2,b^2): 1 \le a,b \le \lfloor \sqrt{n}\rfloor,\ \exists c \in \mathbb{Z}_{>0}: a^2+b^2=c^2\}.$$

The final answer is the number of admissible paths modulo \(10^9+7\).

Mathematical Approach

Step 1: Count paths between two comparable points

If \((x_1,y_1)\) and \((x_2,y_2)\) satisfy \(x_1 \le x_2\) and \(y_1 \le y_2\), then any monotone path from the first point to the second point must use exactly \(x_2-x_1\) right moves and \(y_2-y_1\) up moves. Hence the number of such paths is

$$\binom{(x_2-x_1)+(y_2-y_1)}{x_2-x_1}.$$

In particular, without any forbidden points, the total number of paths from \((0,0)\) to \((n,n)\) is

$$\binom{2n}{n}.$$

Step 2: Describe the forbidden set

Only points with both coordinates at most \(n\) can matter, so it is enough to enumerate \(a,b \le \lfloor \sqrt{n}\rfloor\). The condition \(a^2+b^2=c^2\) means that \((a,b,c)\) forms a Pythagorean triple, and each such pair produces a forbidden point \((a^2,b^2)\).

After sorting the forbidden points lexicographically, write them as

$$P_1=(x_1,y_1),\ P_2=(x_2,y_2),\ \dots,\ P_m=(x_m,y_m).$$

This ordering is compatible with monotone movement: if a path can go from \(P_j\) to \(P_i\), then necessarily \(x_j \le x_i\) and \(y_j \le y_i\), which implies \(j \lt i\) after lexicographic sorting.

Step 3: Inclusion-exclusion by the first forbidden point

For each forbidden point \(P_i\), let \(B_i\) be the number of monotone paths from \((0,0)\) to \(P_i\) whose first forbidden point is exactly \(P_i\). Start from the unrestricted count

$$\binom{x_i+y_i}{x_i}.$$

If a path reaches an earlier forbidden point \(P_j\) first and later continues to \(P_i\), then the number of such paths is

$$B_j \binom{(x_i-x_j)+(y_i-y_j)}{x_i-x_j},$$

provided \(x_j \le x_i\) and \(y_j \le y_i\). Therefore

$$B_i=\binom{x_i+y_i}{x_i}-\sum_{\substack{j \lt i \\ x_j \le x_i \\ y_j \le y_i}} B_j \binom{(x_i-x_j)+(y_i-y_j)}{x_i-x_j} \pmod{10^9+7}.$$

This dynamic program is a topological inclusion-exclusion on the partial order induced by coordinate-wise comparison.

Every inadmissible path from \((0,0)\) to \((n,n)\) has a unique first forbidden point, so after all \(B_i\) are known we subtract the bad paths by that first hit:

$$A(n)=\binom{2n}{n}-\sum_{i=1}^{m} B_i \binom{(n-x_i)+(n-y_i)}{n-x_i} \pmod{10^9+7}.$$

This is exactly the recurrence used in the implementations.

Step 4: Worked examples from the checkpoints

For \(n=5\), there are no forbidden points because the only possible square coordinates are \(1\) and \(4\), and none of the sums \(1+1\), \(1+4\), \(4+1\), \(4+4\) is a square. Hence

$$A(5)=\binom{10}{5}=252.$$

For \(n=16\), the forbidden points are \((9,16)\) and \((16,9)\), coming from \(3^2+4^2=5^2\) and \(4^2+3^2=5^2\). The unrestricted count is

$$\binom{32}{16}=601080390.$$

Each forbidden point receives

$$\binom{9+16}{9}=\binom{25}{9}=2042975$$

paths from the origin. Neither forbidden point dominates the other, so no path can pass through both. Therefore

$$A(16)=\binom{32}{16}-2\binom{25}{9}=596994440,$$

which matches the checkpoint value used by the implementations.

Step 5: Fast modular binomial coefficients

All binomial coefficients are evaluated modulo the prime

$$M=10^9+7.$$

The implementations precompute factorials and inverse factorials up to \(2n\), so every combination query becomes

$$\binom{N}{R}\equiv N! \cdot (R!)^{-1} \cdot ((N-R)!)^{-1} \pmod{M}.$$

The inverse factorials are derived using Fermat's little theorem:

$$a^{-1}\equiv a^{M-2}\pmod{M}, \qquad a \not\equiv 0 \pmod{M}.$$

After this preprocessing, each path-count query is \(O(1)\).

How the Code Works

The C++, Python, and Java implementations follow the same structure. They first build a fast square-membership structure up to \(2n\), enumerate every forbidden point \((a^2,b^2)\) inside the \(n \times n\) grid, sort the resulting list, and remove duplicates. Next they precompute factorials and inverse factorials so that any binomial coefficient needed by the path formulas can be answered in constant time modulo \(10^9+7\).

Once those tables are ready, the implementation scans the forbidden points in sorted order and computes the number of paths whose first forbidden point is each obstacle. The final admissible count is obtained by subtracting all bad-path contributions from the unrestricted total \(\binom{2n}{n}\).

Complexity Analysis

Let \(m\) be the number of forbidden points. Enumerating candidate square pairs takes \(O(n)\) time because both \(a\) and \(b\) range up to \(\lfloor \sqrt{n}\rfloor\). Precomputing factorials and inverse factorials up to \(2n\) also takes \(O(n)\) time and \(O(n)\) memory. The dynamic program compares each forbidden point with all earlier ones, so it costs \(O(m^2)\) time and \(O(m)\) additional memory. Overall, the algorithm runs in \(O(n+m^2)\) time and uses \(O(n+m)\) memory.

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=408
  2. Lattice path counting: Wikipedia — Lattice path
  3. Binomial coefficients: Wikipedia — Binomial coefficient
  4. Fermat's little theorem: Wikipedia — Fermat's little theorem
  5. Pythagorean triples: Wikipedia — Pythagorean triple

Problem 408 source code

C++

#include <algorithm>
#include <cstdint>
#include <iostream>
#include <string>
#include <utility>
#include <vector>
#include <cmath>
#include <functional>

namespace {

using i64 = long long;
using u64 = std::uint64_t;
constexpr int MOD = 1000000007;

struct Options {
    int n = 10000000;
    bool run_checkpoints = true;
};

bool parse_int_after_prefix(const std::string& arg, const std::string& prefix, int& value) {
    if (arg.rfind(prefix, 0U) != 0U) {
        return false;
    }
    const std::string tail = arg.substr(prefix.size());
    if (tail.empty()) {
        return false;
    }
    try {
        value = std::stoi(tail);
    } catch (...) {
        return false;
    }
    return true;
}

bool parse_arguments(int argc, char** argv, Options& options) {
    for (int i = 1; i < argc; ++i) {
        const std::string arg(argv[i]);
        if (arg == "--skip-checkpoints") {
            options.run_checkpoints = false;
            continue;
        }
        if (parse_int_after_prefix(arg, "--n=", options.n)) {
            continue;
        }
        std::cerr << "Unknown argument: " << arg << '\n';
        return false;
    }
    return options.n >= 1;
}

int mod_pow(int base, i64 exp) {
    i64 result = 1;
    i64 cur = base % MOD;
    i64 e = exp;
    while (e > 0) {
        if (e & 1LL) {
            result = (result * cur) % MOD;
        }
        cur = (cur * cur) % MOD;
        e >>= 1LL;
    }
    return static_cast<int>(result);
}

std::vector<std::pair<int, int>> inadmissible_points(int n) {
    const int lim = static_cast<int>(std::sqrt(static_cast<long double>(n)));
    const int lim2 = static_cast<int>(std::sqrt(static_cast<long double>(2LL * n)));

    std::vector<std::uint8_t> is_square(static_cast<std::size_t>(2 * n + 1), 0U);
    for (int c = 1; c <= lim2; ++c) {
        is_square[static_cast<std::size_t>(c * c)] = 1U;
    }

    std::vector<std::pair<int, int>> pts;
    pts.reserve(8000);
    for (int a = 1; a <= lim; ++a) {
        const int a2 = a * a;
        for (int b = 1; b <= lim; ++b) {
            const int b2 = b * b;
            if (is_square[static_cast<std::size_t>(a2 + b2)] != 0U) {
                pts.emplace_back(a2, b2);
            }
        }
    }

    std::sort(pts.begin(), pts.end());
    pts.erase(std::unique(pts.begin(), pts.end()), pts.end());
    return pts;
}

int P_mod(const int n) {
    const int maxv = 2 * n;
    std::vector<int> fact(static_cast<std::size_t>(maxv + 1), 1);
    for (int i = 1; i <= maxv; ++i) {
        fact[static_cast<std::size_t>(i)] =
            static_cast<int>((1LL * fact[static_cast<std::size_t>(i - 1)] * i) % MOD);
    }

    std::vector<int> inv_fact(static_cast<std::size_t>(maxv + 1), 1);
    inv_fact[static_cast<std::size_t>(maxv)] = mod_pow(fact[static_cast<std::size_t>(maxv)], MOD - 2);
    for (int i = maxv; i >= 1; --i) {
        inv_fact[static_cast<std::size_t>(i - 1)] =
            static_cast<int>((1LL * inv_fact[static_cast<std::size_t>(i)] * i) % MOD);
    }

    auto nCr = [&](int nn, int rr) -> int {
        if (rr < 0 || rr > nn) {
            return 0;
        }
        return static_cast<int>(
            1LL * fact[static_cast<std::size_t>(nn)] * inv_fact[static_cast<std::size_t>(rr)] % MOD *
            inv_fact[static_cast<std::size_t>(nn - rr)] % MOD);
    };

    std::vector<std::pair<int, int>> pts = inadmissible_points(n);
    const int m = static_cast<int>(pts.size());
    std::vector<int> ways(static_cast<std::size_t>(m), 0);

    for (int i = 0; i < m; ++i) {
        const int xi = pts[static_cast<std::size_t>(i)].first;
        const int yi = pts[static_cast<std::size_t>(i)].second;
        i64 w = nCr(xi + yi, xi);

        for (int j = 0; j < i; ++j) {
            const int xj = pts[static_cast<std::size_t>(j)].first;
            const int yj = pts[static_cast<std::size_t>(j)].second;
            if (xj <= xi && yj <= yi) {
                const int paths = nCr((xi - xj) + (yi - yj), xi - xj);
                w -= 1LL * ways[static_cast<std::size_t>(j)] * paths % MOD;
                if (w < 0) {
                    w += MOD;
                }
            }
        }

        ways[static_cast<std::size_t>(i)] = static_cast<int>(w % MOD);
    }

    i64 ans = nCr(2 * n, n);
    for (int i = 0; i < m; ++i) {
        const int x = pts[static_cast<std::size_t>(i)].first;
        const int y = pts[static_cast<std::size_t>(i)].second;
        const int tail_paths = nCr((n - x) + (n - y), n - x);
        ans -= 1LL * ways[static_cast<std::size_t>(i)] * tail_paths % MOD;
        if (ans < 0) {
            ans += MOD;
        }
    }

    return static_cast<int>(ans % MOD);
}

bool run_checkpoints() {
    if (P_mod(5) != 252) {
        std::cerr << "Checkpoint failed: P(5)\n";
        return false;
    }
    if (P_mod(16) != 596994440) {
        std::cerr << "Checkpoint failed: P(16)\n";
        return false;
    }
    if (P_mod(1000) != 341920854) {
        std::cerr << "Checkpoint failed: P(1000)\n";
        return false;
    }
    return true;
}

}  // namespace

int main(int argc, char** argv) {
    Options options;
    if (!parse_arguments(argc, argv, options)) {
        return 1;
    }
    if (options.run_checkpoints && !run_checkpoints()) {
        return 2;
    }

    std::cout << P_mod(options.n) << '\n';
    return 0;
}

Python

import math

def solve():
    N = 10_000_000
    MOD = 1_000_000_007

    def mod_pow(base, exp, mod):
        result = 1
        cur = base % mod
        e = exp
        while e > 0:
            if e & 1: result = result * cur % mod
            cur = cur * cur % mod
            e >>= 1
        return result

    maxv = 2 * N
    fact = [1] * (maxv + 1)
    for i in range(1, maxv + 1):
        fact[i] = fact[i-1] * i % MOD
    inv_fact = [1] * (maxv + 1)
    inv_fact[maxv] = mod_pow(fact[maxv], MOD - 2, MOD)
    for i in range(maxv, 0, -1):
        inv_fact[i-1] = inv_fact[i] * i % MOD

    def nCr(n, r):
        if r < 0 or r > n: return 0
        return fact[n] * inv_fact[r] % MOD * inv_fact[n-r] % MOD

    # Find inadmissible points (a², b²) where a²+b² is a perfect square
    lim = int(math.isqrt(N))
    lim2 = int(math.isqrt(2 * N))
    is_sq = set(c*c for c in range(1, lim2+1))

    pts = set()
    for a in range(1, lim+1):
        a2 = a * a
        for b in range(1, lim+1):
            b2 = b * b
            if a2 + b2 in is_sq:
                pts.add((a2, b2))
    pts = sorted(pts)

    m = len(pts)
    ways = [0] * m
    for i in range(m):
        xi, yi = pts[i]
        w = nCr(xi + yi, xi)
        for j in range(i):
            xj, yj = pts[j]
            if xj <= xi and yj <= yi:
                paths = nCr((xi-xj)+(yi-yj), xi-xj)
                w = (w - ways[j] * paths) % MOD
        ways[i] = w % MOD

    ans = nCr(2*N, N)
    for i in range(m):
        x, y = pts[i]
        tail = nCr((N-x)+(N-y), N-x)
        ans = (ans - ways[i] * tail) % MOD

    return str(ans % MOD)

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

Java

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Euler408 {
    private static final int MOD = 1000000007;

    private static int modPow(int base, long exp) {
        long result = 1;
        long cur = base % MOD;
        long e = exp;
        while (e > 0) {
            if ((e & 1L) != 0) {
                result = (result * cur) % MOD;
            }
            cur = (cur * cur) % MOD;
            e >>= 1L;
        }
        return (int) result;
    }

    private static class Point implements Comparable<Point> {
        int x, y;

        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }

        @Override
        public int compareTo(Point o) {
            if (this.x != o.x)
                return Integer.compare(this.x, o.x);
            return Integer.compare(this.y, o.y);
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof Point))
                return false;
            Point o = (Point) obj;
            return this.x == o.x && this.y == o.y;
        }
    }

    public static String solve() {
        int n = 10000000;
        int lim = (int) Math.sqrt(n);
        int lim2 = (int) Math.sqrt(2L * n);

        boolean[] isSquare = new boolean[2 * n + 1];
        for (int c = 1; c <= lim2; ++c) {
            isSquare[c * c] = true;
        }

        List<Point> pts = new ArrayList<>();
        for (int a = 1; a <= lim; ++a) {
            int a2 = a * a;
            for (int b = 1; b <= lim; ++b) {
                int b2 = b * b;
                if (isSquare[a2 + b2]) {
                    pts.add(new Point(a2, b2));
                }
            }
        }

        Collections.sort(pts);
        List<Point> uniquePts = new ArrayList<>();
        for (Point p : pts) {
            if (uniquePts.isEmpty() || !uniquePts.get(uniquePts.size() - 1).equals(p)) {
                uniquePts.add(p);
            }
        }
        pts = uniquePts;

        int maxv = 2 * n;
        int[] fact = new int[maxv + 1];
        fact[0] = 1;
        for (int i = 1; i <= maxv; ++i) {
            fact[i] = (int) ((1L * fact[i - 1] * i) % MOD);
        }

        int[] invFact = new int[maxv + 1];
        invFact[maxv] = modPow(fact[maxv], MOD - 2);
        for (int i = maxv; i >= 1; --i) {
            invFact[i - 1] = (int) ((1L * invFact[i] * i) % MOD);
        }

        int m = pts.size();
        int[] ways = new int[m];

        for (int i = 0; i < m; ++i) {
            int xi = pts.get(i).x;
            int yi = pts.get(i).y;
            long w = nCr(xi + yi, xi, fact, invFact);

            for (int j = 0; j < i; ++j) {
                int xj = pts.get(j).x;
                int yj = pts.get(j).y;
                if (xj <= xi && yj <= yi) {
                    long paths = nCr((xi - xj) + (yi - yj), xi - xj, fact, invFact);
                    w = (w - 1L * ways[j] * paths) % MOD;
                    if (w < 0)
                        w += MOD;
                }
            }
            ways[i] = (int) w;
        }

        long ans = nCr(2 * n, n, fact, invFact);
        for (int i = 0; i < m; ++i) {
            int x = pts.get(i).x;
            int y = pts.get(i).y;
            long tailPaths = nCr((n - x) + (n - y), n - x, fact, invFact);
            ans = (ans - 1L * ways[i] * tailPaths) % MOD;
            if (ans < 0)
                ans += MOD;
        }

        return String.valueOf(ans);
    }

    private static long nCr(int nn, int rr, int[] fact, int[] invFact) {
        if (rr < 0 || rr > nn)
            return 0;
        return 1L * fact[nn] * invFact[rr] % MOD * invFact[nn - rr] % MOD;
    }

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