Цілочислові точки всередині нецілочислового многокутника
Для цілочислових многокутників існує формула Піка, яка дозволяє підрахувати кількість цілочислових точок усередині многокутника. А що робити з многокутниками, вершини яких довільні?
- Потрібно підрахувати цілочислові точки всередині многокутника з нецілочисловими (довільними) вершинами?
- Вершини многокутника цілочислові? Тоді задача розв'язується простіше. (якщо так → Теорема Піка)
- Готовий рахувати суму під кожним ребром (підхід у дусі алгоритму Евкліда) для кожного ребра окремо?
Опрацюймо кожне ребро многокутника окремо, а після цього зможемо просумувати кількості цілочислових точок під кожним ребром, враховуючи його орієнтацію для вибору знака (так само, як при обчисленні площі многокутника через трапеції).
Насамперед зауважимо, що якщо поточне ребро має кінці в точках і , то його можна подати як лінійну функцію:
Тепер виконаємо заміну , так що . Це дозволяє нам працювати з та . Позначимо .
Ми не сумуватимемо точки при і на задля цілісності алгоритму. Їх можна додати вручну згодом. Отже, нам треба просумувати . Також припускаємо, що і . Інакше слід зробити заміну і додати до .
Обговорімо, як можна обчислити суму . Маємо два випадки:
-
або .
Тоді нам слід почати із сумування точок під . Їхня кількість дорівнює
Тепер нас цікавлять лише точки такі, що . Ця кількість збігається з кількістю точок таких, що . Отже, ми звели нашу задачу до , , де тепер і , і менші за . Ось малюнок: ми щойно просумували сині точки і відняли синю лінійну функцію від чорної, щоб звести задачу до менших значень і :

-
і .
Якщо дорівнює , ми можемо безпечно повернути . Якщо ж це не так, то можна стверджувати, що немає цілочислових точок таких, що і . Це означає, що ми отримаємо ту саму відповідь, якщо розглянемо нову систему відліку, у якій , вісь напрямлена вниз, а вісь напрямлена ліворуч. Для цієї системи відліку нас цікавлять цілочислові точки на множині
що повертає нас назад до випадку . Нову точку відліку та осі і можна побачити на малюнку нижче:

Як бачите, у новій системі відліку лінійна функція матиме коефіцієнт , а її нуль буде в точці , що робить наведену вище формулу правильною.
Аналіз складності
Нам треба підрахувати щонайбільше точок. Серед них на найпершому кроці ми підрахуємо . Можемо вважати, що є знехтувано малим, оскільки на початку ми можемо зробити його меншим за . У такому разі можна сказати, що ми підраховуємо приблизно усіх точок. Отже, ми завершимо за кроків.
Реалізація
Ось проста функція, яка обчислює кількість цілочислових точок таких, що та :
- C++
- Python
- TypeScript
- Go
int count_lattices(Fraction k, Fraction b, long long n) {
auto fk = k.floor();
auto fb = b.floor();
auto cnt = 0LL;
if (k >= 1 || b >= 1) {
cnt += (fk * (n - 1) + 2 * fb) * n / 2;
k -= fk;
b -= fb;
}
auto t = k * n + b;
auto ft = t.floor();
if (ft >= 1) {
cnt += count_lattices(1 / k, (t - t.floor()) / k, t.floor());
}
return cnt;
}
from fractions import Fraction
from math import floor
# Кількість цілих точок (x; y): 0 <= x < n та 0 < y <= floor(k*x + b)
def count_lattices(k: Fraction, b: Fraction, n: int) -> int:
fk = floor(k)
fb = floor(b)
cnt = 0
if k >= 1 or b >= 1:
cnt += (fk * (n - 1) + 2 * fb) * n // 2
k -= fk
b -= fb
t = k * n + b
ft = floor(t)
if ft >= 1:
# Дзеркалимо систему координат: коефіцієнт стає 1/k
cnt += count_lattices(1 / k, (t - floor(t)) / k, ft)
return cnt
// Раціональний дріб на BigInt: знаменник завжди додатний.
class Fraction {
num: bigint;
den: bigint;
constructor(num: bigint, den: bigint = 1n) {
if (den < 0n) {
num = -num;
den = -den;
}
const a = num < 0n ? -num : num;
let g = Fraction.gcd(a, den);
if (g === 0n) g = 1n;
this.num = num / g;
this.den = den / g;
}
static gcd(a: bigint, b: bigint): bigint {
while (b) {
[a, b] = [b, a % b];
}
return a;
}
// Округлення вниз до цілого
floor(): bigint {
let q = this.num / this.den;
if (this.num % this.den !== 0n && this.num < 0n) q -= 1n;
return q;
}
add(o: Fraction): Fraction {
return new Fraction(this.num * o.den + o.num * this.den, this.den * o.den);
}
subBigInt(v: bigint): Fraction {
return new Fraction(this.num - v * this.den, this.den);
}
mulBigInt(v: bigint): Fraction {
return new Fraction(this.num * v, this.den);
}
inv(): Fraction {
return new Fraction(this.den, this.num);
}
div(o: Fraction): Fraction {
return new Fraction(this.num * o.den, this.den * o.num);
}
geBigInt(v: bigint): boolean {
return this.num >= v * this.den;
}
}
// Кількість цілих точок (x; y): 0 <= x < n та 0 < y <= floor(k*x + b)
function countLattices(k: Fraction, b: Fraction, n: bigint): bigint {
const fk = k.floor();
const fb = b.floor();
let cnt = 0n;
if (k.geBigInt(1n) || b.geBigInt(1n)) {
cnt += ((fk * (n - 1n) + 2n * fb) * n) / 2n;
k = k.subBigInt(fk);
b = b.subBigInt(fb);
}
const t = k.mulBigInt(n).add(b);
const ft = t.floor();
if (ft >= 1n) {
// Дзеркалимо систему координат: коефіцієнт стає 1/k
cnt += countLattices(k.inv(), t.subBigInt(ft).div(k), ft);
}
return cnt;
}
// floorRat повертає floor(x) для раціонального x як int64.
func floorRat(x *big.Rat) int64 {
q := new(big.Int)
q.Div(x.Num(), x.Denom()) // big.Int.Div округлює до нижнього (євклідове ділення)
return q.Int64()
}
// countLattices рахує цілі точки (x; y): 0 <= x < n та 0 < y <= floor(k*x + b).
func countLattices(k, b *big.Rat, n int64) int64 {
fk := floorRat(k)
fb := floorRat(b)
var cnt int64
if k.Cmp(big.NewRat(1, 1)) >= 0 || b.Cmp(big.NewRat(1, 1)) >= 0 {
cnt += (fk*(n-1) + 2*fb) * n / 2
k = new(big.Rat).Sub(k, big.NewRat(fk, 1))
b = new(big.Rat).Sub(b, big.NewRat(fb, 1))
}
t := new(big.Rat).Add(new(big.Rat).Mul(k, big.NewRat(n, 1)), b)
ft := floorRat(t)
if ft >= 1 {
// Дзеркалимо систему координат: коефіцієнт стає 1/k
invK := new(big.Rat).Inv(k)
tFrac := new(big.Rat).Sub(t, big.NewRat(ft, 1))
newB := new(big.Rat).Quo(tFrac, k)
cnt += countLattices(invK, newB, ft)
}
return cnt
}
Тут Fraction — це деякий клас для роботи з раціональними числами.
На практиці виявляється, що якщо всі знаменники й чисельники за абсолютною величиною не перевищують , то в рекурсивних викликах вони будуть не більшими за , якщо ви постійно ділите чисельники й знаменники на їхній найбільший спільний дільник.
За цього припущення можна сказати, що допустимо використовувати double і вимагати точності , де — це точність, з якою задано і .
Це означає, що під час заокруглення вниз числа слід вважати цілими, якщо вони відрізняються від цілого не більш ніж на .