补集计数:全部三点组减去共线三点组;斜向用「两点间内点数 = gcd(dx,dy)−1」,再借 Σφ(t)=g 交换求和,把 O(mn) 的 gcd 求和降到 O(min(m,n))。
OJ: roj
题目 ID: 1655
难度:普及+/提高-
标签:数学组合计数数论欧拉函数gcdpython
创建: 2026-10-01 00:22
更新: 2026-10-07 13:50
形式化题目
网格是一个
计算任务:在全部格点中任取 3 个,求三点不共线的取法数。
例如 2 2 的答案是 76。
数据范围:
正解
思路
第一步:把"数三角形"改成"数共线三点组"。
三点要么共线、要么不共线,两类互斥且穷尽,所以
减法的好处是:共线这个条件比"不共线"好数得多,因为它能按直线的方向分类。
共线三点组按方向分成三类(
第三类斜向需要用到
第二步:斜向的关键——两点之间夹了几个格点?
任取两个格点
这
把
当作某条直线的两个端点时,它们正好贡献 个斜向共线三点组。
为什么强调"端点"?因为一个共线三点组里,最左和最右的两个点是唯一确定的
(三点不同、
注意位移为
乘 2 是因为
第三步:这个双重求和是
gcd,虽然勉强能跑,但我们要的是与正解同阶的
把它代进双重求和,再交换求和次序(原来是"先枚举位移
定义
两种写法是同一个量:固定距离
第四步:整理成最终公式。
用样例
| 类别 | 代入公式 | 数量 |
|---|---|---|
| 水平 | 3 | |
| 垂直 | 3 | |
| 斜向 | 只有 |
2 |
| 共线合计 | 8 | |
| 答案 | 76 |
其中两条斜向共线三点组就是主对角线
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 00:25
# update_at: 2026-10-01 00:25
import sys
from math import comb
MAX_SIDE = 1000 # 题面 m、n 的上界,欧拉函数表按它建一次
def totients(limit: int) -> list[int]:
"""phi[1..limit]:phi[t] 是不超过 t 且与 t 互质的正整数个数(埃氏筛)。"""
phi = list(range(limit + 1))
for p in range(2, limit + 1):
if phi[p] == p: # p 没被更小的质数除过,说明 p 是质数
for k in range(p, limit + 1, p):
phi[k] -= phi[k] // p
return phi
def pairs_divisible_by(total: int, step: int) -> int:
"""[0, total] 上的有序点对 (x1, x2),满足 x1 < x2 且 x2 − x1 能被 step 整除。
距离取 k·step(k ≥ 1)时左端点有 total − k·step + 1 种,对 k 求和即得。
"""
cnt = total // step # 距离最大到 cnt·step,k 只遍历 1..cnt
return cnt * (total + 1) - step * cnt * (cnt + 1) // 2
PHI = totients(MAX_SIDE) # 主流程只查表,不在求和里现算欧拉函数
def count_triangles(m: int, n: int) -> int:
"""(m+1)×(n+1) 个格点中,三点不共线的取法数 = 全部三点组 − 共线三点组。"""
rows, cols = m + 1, n + 1 # 两个方向的格点数
limit = min(m, n) # 位移分量超过它,另一个方向就放不下第二个点
# step_x[t] 是横坐标差能被 t 整除的点对数(下标即步长,开头的 0 占位,步长从 1 起)。
# 横纵独立,两者相乘正是"横纵位移都被 t 整除、且以这对点为端点"的线段条数。
step_x = [0] + [pairs_divisible_by(m, step) for step in range(1, limit + 1)]
step_y = [0] + [pairs_divisible_by(n, step) for step in range(1, limit + 1)]
# 位移 (dx, dy) 的最大公约数为 g 时,线段中间恰好夹着 g − 1 个格点,也就是以这两点
# 为端点的共线三点组数。由 Σ_{t | g} φ(t) = g 得 g − 1 = Σ_{t | g, t ≥ 2} φ(t):
# 交换求和次序后,第 t 层要数的正是"dx、dy 都能被 t 整除"的线段,于是斜线段总数
# 写成按 φ(t) 的加权和。g 最大只到 min(m, n),t 也只需枚举到这里。
weighted = sum(PHI[t] * step_x[t] * step_y[t] for t in range(1, limit + 1))
slanted = 2 * (weighted - step_x[1] * step_y[1]) # 扣掉 t = 1 层,权重才变成 g − 1
# 水平共线:每条水平格线(cols 个点)任取 3 个;垂直同理
axis_aligned = rows * comb(cols, 3) + cols * comb(rows, 3)
return comb(rows * cols, 3) - axis_aligned - slanted
def solve() -> None:
m, n = map(int, sys.stdin.buffer.read().split())
print(count_triangles(m, n))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 00:48
* update_at: 2026-10-06 00:48
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX_SIDE = 1000; // 题面 m、n 的上界,欧拉函数表按它建一次
ll m, n;
ll phi[MAX_SIDE + 1]; // phi[t] 为欧拉函数,表示不超过 t 且与 t 互质的正整数个数
// 埃氏筛法预处理 phi[1..MAX_SIDE]。
void build_phi() {
for (int i = 1; i <= MAX_SIDE; i++) {
phi[i] = i;
}
for (int p = 2; p <= MAX_SIDE; p++) {
if (phi[p] == p) { // 还没被更小的质数除过,说明 p 是质数
for (int k = p; k <= MAX_SIDE; k += p) {
phi[k] -= phi[k] / p;
}
}
}
}
// 组合数 C(x, 3):从 x 个点里任取 3 个。
ll comb3(ll x) {
return x * (x - 1) * (x - 2) / 6;
}
// [0, total] 上的点对 (x1, x2),满足 x1 < x2 且 x2 - x1 能被 step 整除。
// 距离取 k * step(k >= 1)时左端点有 total + 1 - k * step 种选法,对 k 求和即得。
ll count_pairs(ll total, ll step) {
ll cnt = total / step; // 距离最大能取到 cnt * step,于是 k 只遍历 1..cnt
return cnt * (total + 1) - step * cnt * (cnt + 1) / 2;
}
void solve() {
ll rows = m + 1; // 每个方向上的格点数
ll cols = n + 1;
ll limit = min(m, n); // 位移分量超过它,另一个方向就放不下第二个点
// 斜向共线:位移 (dx, dy) 的最大公约数为 g 时,线段中间夹着 g - 1 个格点,
// 也就是以这两点为端点的共线三点组数。由 Σ_{t | g} φ(t) = g 得
// g - 1 = Σ_{t | g, t >= 2} φ(t),交换求和次序后第 t 层只要数横纵坐标差
// 都被 t 整除的点对数,两者相乘再按 φ(t) 加权即可。
ll slanted = 0;
for (ll t = 2; t <= limit; t++) {
ll pairs_x = count_pairs(m, t);
ll pairs_y = count_pairs(n, t);
slanted += phi[t] * pairs_x * pairs_y;
}
slanted *= 2; // dy 可正可负,右下与右上两种方向对称
// 水平共线:每条水平格线上任取 3 点;垂直同理。
ll axis_aligned = cols * comb3(rows) + rows * comb3(cols);
// 补集计数:全部三点组减去共线三点组。
ll total = comb3(rows * cols);
cout << total - axis_aligned - slanted << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> m >> n;
build_phi();
solve();
return 0;
}复杂度
设
- 时间复杂度:欧拉函数埃氏筛固定到
, (只跑一次); 两条 数组各 ,加权求和 ,其中 。 整体与标准 C++ 正解同阶,比"直接双重枚举 算 "的 低一个数量级。 - 空间复杂度:
表(长度 )加上两条 数组, 。 实测最坏 时 0.018 s / 约 15 MB,远低于 1000 ms / 128 MB 的限制, 无 TLE / MLE 风险。答案是 Python 大整数,也不存在 C++ 里要用 long long的溢出问题。
总结
本题的推导链是补集 → 方向分类 → gcd 分层 → 欧拉函数降维:
- 数三角形不好数,就数共线三点组,用总数去减;
- 共线组按方向分为水平、垂直、斜向,前两类是每行/每列的
; - 斜向的关键观察是"两点之间的内格点数
", 而共线三点组的两端点是唯一的,所以按位移枚举不会重复; - 借
把 改写成 , 交换求和次序后,每个 只要数"横纵差都被 整除"的点对数,复杂度降到 。
最终公式
图示解析
下面的流程图给出整个计数过程。注意看中间两个分叉:水平/垂直走的是"每条线取 3 点"的
组合数,斜向走的是"
flowchart TD
In["读入 m, n"] --> Total["总数 C((m+1)(n+1), 3)"]
Total --> Axis["水平 + 垂直共线"]
Total --> Slant["斜向共线"]
Axis --> HA["(n+1)·C(m+1,3)"]
Axis --> VA["(m+1)·C(n+1,3)"]
Slant --> Phi["筛出 phi[1..min(m,n)]"]
Phi --> Sx["Sx(t):横坐标差被 t 整除的点对数"]
Phi --> Sy["Sy(t):纵坐标差被 t 整除的点对数"]
Sx --> W["累加 phi(t)·Sx(t)·Sy(t)"]
Sy --> W
W --> Minus["从 t=2 起累加(等价于扣掉 t=1 层),再乘 2"]
HA --> Sub["总数 - 水平 - 垂直 - 斜向"]
VA --> Sub
Minus --> Sub
Sub --> Out["输出答案"]
“两端点唯一"与”
x=0 x=1 x=2
y=2 · · B
y=1 · o · A=(0,0) B=(2,2) o=(1,1)
y=0 A · ·这组对比说明:贡献只看
