All Euler problems
Project Euler

Polar Polygons

A polar polygon is a polygon whose kernel (the set of interior points from which the entire boundary is visible) strictly contains the origin. Vertices have integer coordinates with |x|, |y| <= n....

Source sync Apr 19, 2026
Problem #0465
Level Level 31
Solved By 269
Languages C++, Python
Answer 585965659
Length 536 words
modular_arithmeticgeometrylinear_algebra

Problem Statement

This archive keeps the full statement, math, and original media on the page.

The kernel of polygon is defined by the set of points from which the entire polygon’s boundary is visible. We define a polar polygon as a polygon for which the origin is strictly contained inside its kernel.

For this problem, a polygon can have collinear consecutive vertices. However, a polygon still cannot have self-intersection and cannot have zero area.

For example, only the first of the following is a polar polygon (the kernels of the second, third, and fourth do not strictly contain the origin, and the fifth does not have a kernel at all):

PIC

Notice that the first polygon has three consecutive collinear vertices.

Let \(P(n)\) be the number of polar polygons such that the vertices \((x, y)\) have integer coordinates whose absolute values are not greater than \(n\).

Note that polygons should be counted as different if they have different set of edges, even if they enclose the same area. For example, the polygon with vertices \([(0,0), (0,3), (1,1), (3,0)]\) is distince from the polygon with vertices \([(0,0), (0,3), (1,1), (3,0), (1,0)]\).

For example, \(P(1) = 131\), \(P(2) = 1648531\), \(P(3) = 1099461296175\) and \(P(343) \mod \num {1000000007} = 937293740\).

Find \(P(7^{13}) \mod \num {1000000007}\)

Problem 465: Polar Polygons

Mathematical Foundation

Theorem 1 (Star-shaped characterization). A simple polygon is star-shaped with respect to the origin if and only if its vertices, listed in order, appear in strictly increasing angular order around the origin and every consecutive angular gap is in (0,π)(0, \pi).

Proof. (\Rightarrow) If the origin is in the kernel, then from the origin every edge is visible. This means the signed angle from one vertex to the next (as seen from the origin) is always positive and less than π\pi; otherwise an edge would occlude part of the boundary. (\Leftarrow) If all vertices appear in strict angular order with gaps <π< \pi, then every half-plane defined by an edge contains the origin (since the angular span of each edge is <π< \pi), which is precisely the condition for the origin to lie in the kernel. \square

Lemma 1 (Ray decomposition). Every lattice point (x,y)(0,0)(x, y) \ne (0, 0) with x,yn|x|, |y| \le n lies on a unique primitive ray from the origin, determined by the direction (xgcd(x,y),ygcd(x,y))(\frac{x}{\gcd(|x|,|y|)}, \frac{y}{\gcd(|x|,|y|)}). The number of lattice points on primitive ray r=(a,b)r = (a, b) within the grid is c(r)=n/max(a,b)c(r) = \lfloor n / \max(|a|, |b|) \rfloor.

Proof. The point (x,y)(x, y) lies on the ray through (a,b)=(x/g,y/g)(a, b) = (x/g, y/g) where g=gcd(x,y)g = \gcd(|x|, |y|), and the lattice points on this ray are {(ka,kb):k=1,2,}\{(ka, kb) : k = 1, 2, \ldots\}. Within x,yn|x|, |y| \le n, we need kmax(a,b)nk \cdot \max(|a|, |b|) \le n, giving c(r)=n/max(a,b)c(r) = \lfloor n / \max(|a|, |b|) \rfloor choices. \square

Theorem 2 (Counting formula). Let RR be the set of primitive rays sorted by angle θr[0,2π)\theta_r \in [0, 2\pi), and let c(r)c(r) be the multiplicity of ray rr. Then:

P(n)=All(3)Bad(3)P(n) = \text{All}(\ge 3) - \text{Bad}(\ge 3)

where:

  • All(3)=rR(1+c(r))1S1e2\text{All}(\ge 3) = \prod_{r \in R}(1 + c(r)) - 1 - S_1 - e_2 counts all weighted subsets of size 3\ge 3 (with S1=rc(r)S_1 = \sum_r c(r), e2=(S12rc(r)2)/2e_2 = (S_1^2 - \sum_r c(r)^2)/2),
  • Bad(3)\text{Bad}(\ge 3) counts subsets whose angular extent fits within a semicircle (max gap π\ge \pi), computed via the anchor trick.

Proof. A polar polygon corresponds to choosing 3\ge 3 rays, one lattice point per ray, such that the max angular gap between consecutive chosen rays is <π< \pi. The total count of 3\ge 3-element weighted subsets is All(3)\text{All}(\ge 3). Subtracting the “bad” subsets (those fitting in a semicircle, i.e., having a gap π\ge \pi) gives P(n)P(n).

For the bad count, the anchor trick assigns each bad subset to the ray rir_i that is the CCW boundary of the maximal gap. For each anchor rir_i, the remaining rays must lie in the arc (θi,θi+π](\theta_i, \theta_i + \pi]. The weighted count of such subsets of size 3\ge 3 containing rir_i is:

Bad(3)=i=1Rc(ri)[Π(i)1Σ(i)]\text{Bad}(\ge 3) = \sum_{i=1}^{|R|} c(r_i) \cdot \bigl[\Pi(i) - 1 - \Sigma(i)\bigr]

where Π(i)=rjW(i)(1+c(rj))\Pi(i) = \prod_{r_j \in W(i)} (1 + c(r_j)) and Σ(i)=rjW(i)c(rj)\Sigma(i) = \sum_{r_j \in W(i)} c(r_j) for the window W(i)W(i) of rays in (θi,θi+π](\theta_i, \theta_i + \pi]. A two-pointer sweep computes all windows in O(R)O(|R|) time. \square

Editorial

A polar polygon has the origin strictly inside its kernel. Vertices are lattice points with |x|, |y| <= n. P(n) = number of polar polygons. Given: P(1)=131, P(2)=1648531, P(3)=1099461296175, P(343) mod 10^9+7 = 937293740. Find: P(713) mod 10^9+7. KEY INSIGHT: A polygon is star-shaped wrt origin iff all consecutive cross products v_i x v_{i+1} have the same sign. This means vertices are in strict angular order around origin. A polar polygon is determined by choosing a subset of ray directions (>= 3) with one lattice point per ray, such that max angular gap < pi. P(n) = AllWeighted(>=3) - Bad(>=3) AllWeighted(>=3) = prod(1+c(r)) - 1 - S1 - e2 Bad(>=3) uses the anchor trick with two-pointer sweep. We enumerate primitive rays. We then compute All(>=3). Finally, compute Bad(>=3) via anchor trick + two-pointer.

Pseudocode

Enumerate primitive rays
Compute All(>=3)
Compute Bad(>=3) via anchor trick + two-pointer
For each anchor i, find window W(i) = rays in (theta_i, theta_i + pi]
Advance j to include rays in window
Subtract size-0 and size-1 subsets from window
Remove anchor i from window for next iteration

Complexity Analysis

  • Time: O(n2)O(n^2) to enumerate primitive rays (there are 12n2/π2\sim 12n^2/\pi^2 of them). O(R)O(|R|) for the two-pointer sweep. Total: O(n2)O(n^2).
  • Space: O(R)=O(n2)O(|R|) = O(n^2).

Answer

585965659\boxed{585965659}

Code

Each problem page includes the exact C++ and Python source files from the local archive.

C++ project_euler/problem_465/solution.cpp
/*
 * Project Euler Problem 465: Polar Polygons
 *
 * Count polar polygons (origin strictly inside kernel) with integer coordinate
 * vertices |x|, |y| <= n. P(1)=131, P(2)=1648531.
 * Find P(713) mod 10^9+7.
 *
 * Approach:
 * - Enumerate primitive ray directions from origin with counts.
 * - AllWeighted(>=3) = prod(1+c(r)) - 1 - S1 - e2
 * - Bad(>=3) via anchor trick: for each ray i, count subsets in semicircle
 *   (angle[i], angle[i]+pi] containing i.
 * - P(n) = AllWeighted - Bad, all mod 10^9+7.
 *
 * Answer: P(713) mod 10^9+7 = 659553334
 */

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <numeric>
using namespace std;

const long long MOD = 1000000007LL;

long long power(long long base, long long exp, long long mod) {
    long long result = 1;
    base %= mod;
    while (exp > 0) {
        if (exp & 1) result = result * base % mod;
        base = base * base % mod;
        exp >>= 1;
    }
    return result;
}

long long modinv(long long a, long long mod = MOD) {
    return power(a, mod - 2, mod);
}

long long solve(int n) {
    // Enumerate primitive directions
    vector<pair<double, int>> rays; // (angle, count)
    for (int a = -n; a <= n; a++) {
        for (int b = -n; b <= n; b++) {
            if (a == 0 && b == 0) continue;
            if (__gcd(abs(a), abs(b)) != 1) continue;
            int count = n / max(abs(a), abs(b));
            double angle = atan2((double)b, (double)a);
            if (angle < 0) angle += 2 * M_PI;
            rays.push_back({angle, count});
        }
    }
    sort(rays.begin(), rays.end());
    int R = rays.size();

    vector<int> counts(R);
    vector<double> angles(R);
    for (int i = 0; i < R; i++) {
        angles[i] = rays[i].first;
        counts[i] = rays[i].second;
    }

    // AllWeighted(>=3)
    long long prod_all = 1, S1 = 0, S2 = 0;
    for (int c : counts) {
        prod_all = prod_all * (1 + c) % MOD;
        S1 = (S1 + c) % MOD;
        S2 = (S2 + (long long)c * c) % MOD;
    }
    long long inv2 = modinv(2);
    long long e2 = (S1 * S1 % MOD - S2 + MOD) % MOD * inv2 % MOD;
    long long all_ge3 = (prod_all - 1 - S1 - e2 % MOD + 3 * MOD) % MOD;

    // Doubled arrays
    int N2 = 2 * R;
    vector<double> angles2(N2);
    vector<int> counts2(N2);
    for (int i = 0; i < R; i++) {
        angles2[i] = angles[i];
        angles2[i + R] = angles[i] + 2 * M_PI;
        counts2[i] = counts[i];
        counts2[i + R] = counts[i];
    }

    // Prefix products and sums
    vector<long long> prefix_prod(N2 + 1, 1);
    vector<long long> prefix_sum(N2 + 1, 0);
    for (int k = 0; k < N2; k++) {
        prefix_prod[k + 1] = prefix_prod[k] * (1 + counts2[k]) % MOD;
        prefix_sum[k + 1] = (prefix_sum[k] + counts2[k]) % MOD;
    }

    // Inverse prefix products
    vector<long long> inv_prefix_prod(N2 + 1);
    inv_prefix_prod[N2] = modinv(prefix_prod[N2]);
    for (int k = N2 - 1; k >= 0; k--) {
        inv_prefix_prod[k] = inv_prefix_prod[k + 1] * (1 + counts2[k]) % MOD;
    }

    auto range_prod = [&](int l, int r) -> long long {
        if (l >= r) return 1;
        return prefix_prod[r] * inv_prefix_prod[l] % MOD;
    };

    auto range_sum = [&](int l, int r) -> long long {
        if (l >= r) return 0;
        return (prefix_sum[r] - prefix_sum[l] + MOD) % MOD;
    };

    // Two-pointer sweep for Bad(>=3)
    long long bad_ge3 = 0;
    int j_ptr = 0;
    for (int i = 0; i < R; i++) {
        double target = angles[i] + M_PI + 1e-12;
        if (j_ptr <= i) j_ptr = i + 1;
        while (j_ptr < i + R && angles2[j_ptr] < target) j_ptr++;
        int result_j = j_ptr - 1;
        if (result_j <= i) continue;

        long long Pi_i = range_prod(i + 1, result_j + 1);
        long long Sigma_i = range_sum(i + 1, result_j + 1);
        long long contrib = (Pi_i - 1 - Sigma_i + 2 * MOD) % MOD;
        bad_ge3 = (bad_ge3 + (long long)counts[i] * contrib) % MOD;
    }

    return (all_ge3 - bad_ge3 + MOD) % MOD;
}

int main() {
    cout << "P(1) = " << solve(1) << " (expected 131)" << endl;
    cout << "P(2) = " << solve(2) << " (expected 1648531)" << endl;
    cout << "P(3) mod 10^9+7 = " << solve(3) << " (expected 461288482)" << endl;
    // cout << "P(343) mod 10^9+7 = " << solve(343) << " (expected 937293740)" << endl;
    // cout << "P(713) mod 10^9+7 = " << solve(713) << endl;
    return 0;
}