AcWing 201. 可见的点
\(AcWing\) \(201\). 可见的点
一、题目描述
在一个平面直角坐标系的第一象限内,如果一个点 \((x,y)\) 与原点 \((0,0)\) 的连线中没有通过其他任何点,则称该点在原点处是可见的。
例如,点 \((4,2)\) 就是不可见的,因为它与原点的连线会通过点 \((2,1)\)。
部分可见点与原点的连线如下图所示:
编写一个程序,计算给定整数 \(N\) 的情况下,满足 \(0≤x,y≤N\) 的可见点 \((x,y)\) 的数量(可见点不包括原点)。
输入格式
第一行包含整数 \(C\),表示共有 \(C\) 组测试数据。
每组测试数据占一行,包含一个整数 \(N\)。
输出格式
每组测试数据的输出占据一行。
应包括:测试数据的编号(从 \(1\) 开始),该组测试数据对应的 \(N\) 以及可见点的数量。
同行数据之间用空格隔开。
数据范围
\(1≤N,C≤1000\)
输入样例:
4
2
4
5
231
输出样例:
1 2 5
2 4 13
3 5 21
4 231 32549
二、算法分析
设\((x_0,y_0)\)是某一直线射到的 第一个点,则该方程为\(y=kx\),其中\(k=\frac{y_0}{x_0}\),该直线有以下性质:
-
直线方程上的其他点均是\((x_0,y_0)\)的倍数,即\((m\cdot x_0,m\cdot y_0)\),因为斜率相同嘛
-
\(\frac{x_0}{y_0}=1\):
- 此情况 特殊,只有一个点满足,就是\((1,1)\)
-
\(\frac{x_0}{y_0} \neq 1\)时,两者必然互质:
- 证明:如果不是互质的,并且斜率一样,那么它前面的互质点对,必然把它挡上。
求\((x,y)\)互质的点对数量,就是本题的题意。
\(gcd(x,y)=1\),联想到 欧拉函数 。
欧拉函数求的是\(1 \sim n\) 中与 \(n\) 互质的数的个数,若需要使用欧拉函数,与\(n\)互质的数需要小于等于它本身,因此求\(0 <= x ,y <= n\)中,\((x,y)\)互质的个数需要进行分类,使得某一边小于等于另一边,如图所示,对\(y = x\)直线进行切割,使得两边的点对称,只需求右下方区域即可,右下区域的点满足\(x > y\)的性质,且\(y\) 属于\(1\)到\(x - 1\)的区间,因此对于每个\(x\),求出\(1\)到\(x - 1\)中与\(x\)互质的数的个数,即相当于求\(x\)的欧拉函数,因此 用筛法求出\(1\)到\(x\)的所有欧拉函数。
由于\(2\)到\(n\)中的欧拉函数的个数需要算两次(对称),而\(x=y=1\)时只需要算一次,因此\(\displaystyle res=1 +\sum_{i=2}^{n}phi(i)\)
三、时间复杂度
线性筛法求欧拉函数,所以是\(O(n)\)的。
四、暴力实现
因为本题数据量并不大,也可以不用筛法,直接暴力\(O(n^2)\)进行计算每个数字的欧拉函数,也是可以的,但考虑到通用性,还是 建议尽量用筛法求欧拉函数。
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
//快读
char buf[1 << 23], *p1 = buf, *p2 = buf;
#define getchar() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
const int N = 1010;
// 功能:欧拉函数
int get_euler(int x) {
int res = x;
for (int i = 2; i <= x / i; i++)
if (x % i == 0) {
res = res / i * (i - 1);
while (x % i == 0) x /= i;
}
if (x > 1) res = res / x * (x - 1);
return res;
}
int main() {
int n, m = read();
for (int i = 1; i <= m; i++) {
n = read();
int res = 1; // x=y 记1个
for (int j = 1; j <= n; j++) res += get_euler(j) * 2; //叠加两倍的欧拉函数值
printf("%d %d %d\n", i, n, res);
}
return 0;
}
五、筛法实现
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
typedef long long LL;
//快读
char buf[1 << 23], *p1 = buf, *p2 = buf;
#define getchar() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
const int N = 1010;
//筛法求欧拉函数
int primes[N], cnt;
bool st[N];
int phi[N];
void euler(int n) {
phi[1] = 1;
for (int i = 2; i <= n; i++) {
if (!st[i]) {
primes[cnt++] = i;
phi[i] = i - 1;
}
for (int j = 0; primes[j] * i <= n; j++) {
st[i * primes[j]] = true;
if (i % primes[j] == 0) {
phi[i * primes[j]] = phi[i] * primes[j];
break;
}
phi[i * primes[j]] = phi[i] * (primes[j] - 1);
}
}
}
int main() {
//利用筛法,预处理出欧拉函数值数组
euler(N - 1); //这里注意一下是N-1,防止数组越界
int n, m = read();
for (int i = 1; i <= m; i++) {
n = read();
int res = 1; // x=y 记1个
for (int j = 1; j <= n; j++) res += phi[j] * 2; //叠加两倍的欧拉函数值
printf("%d %d %d\n", i, n, res);
}
return 0;
}
六、利用前缀和优化
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
typedef long long LL;
//快读
char buf[1 << 23], *p1 = buf, *p2 = buf;
#define getchar() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
const int N = 1010;
//筛法求欧拉函数
int primes[N], cnt;
bool st[N];
int phi[N];
LL s[N];
void euler(int n) {
phi[1] = 1;
for (int i = 2; i <= n; i++) {
if (!st[i]) {
primes[cnt++] = i;
phi[i] = i - 1;
}
for (int j = 0; primes[j] * i <= n; j++) {
st[i * primes[j]] = true;
if (i % primes[j] == 0) {
phi[i * primes[j]] = phi[i] * primes[j];
break;
}
phi[i * primes[j]] = phi[i] * (primes[j] - 1);
}
}
}
int main() {
//利用筛法,预处理出欧拉函数值数组
euler(N - 1); //这里注意一下是N-1,防止数组越界
//前缀和优化版本
for (int i = 1; i < N; i++) s[i] = s[i - 1] + phi[i];
int n, m = read();
for (int i = 1; i <= m; i++) {
n = read();
printf("%d %d %d\n", i, n, s[n] * 2 + 1);
}
return 0;
}
七、预处理+欧几里得+二维前缀和
#include <iostream>
using namespace std;
typedef long long LL;
//最大公约数,辗转相除法
int gcd(int a, int b) {
return b ? gcd(b, a % b) : a;
}
//快读
char buf[1 << 23], *p1 = buf, *p2 = buf;
#define getchar() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
const int N = 2010;
int n, m, a[N][N];
void init() {
for (int i = 1; i <= 1000; i++) //互质,赋值为1
for (int j = 1; j <= 1000; j++)
if (gcd(i, j) == 1) a[i][j] = 1;
for (int i = 1; i <= 1000; i++) //二维前缀和
for (int j = 1; j <= 1000; j++)
a[i][j] += a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1];
}
int main() {
init();
int m = read();
for (int i = 1; i <= m; i++) {
n = read();
printf("%d %d %d\n", i, n, a[n][n] + 2); //加上(1,0),(0,1)两个点
}
return 0;
}