题目分析
本题给定平面上的N NN个点(1 < N < 700 1 < N < 7001<N<700),所有点的坐标均为整数。要求找出一条直线,使得经过这条直线的点数量最多,并输出这个最大数量。飞行员只能沿直线飞行一次,因此需要找到覆盖最多投递点的直线。
关键约束:
- 点数为N NN,最大可达699 699699。
- 若采用O ( N 3 ) O(N^3)O(N3)的暴力枚举所有点对并检查其余点是否共线,复杂度约为699 3 ≈ 3.4 × 10 8 699^3 \approx 3.4 \times 10^86993≈3.4×108,在时间上可能较为紧张。
- 需要设计更高效的算法。
解题思路
核心思想
本题的核心是:过两点的直线唯一确定一条直线。因此,可以枚举所有点对( P i , P j ) (P_i, P_j)(Pi,Pj),然后统计有多少个点与这两个点共线。但直接对每个点对都遍历所有点进行判断会得到O ( N 3 ) O(N^3)O(N3)的复杂度。
一种常见的优化方法是:固定一个点P i P_iPi,然后计算其他所有点相对于P i P_iPi的斜率,斜率相同的点即与P i P_iPi共线。但本题的代码采用了另一种策略:排序 + 剪枝。
算法步骤
输入处理
由于输入包含多个测试用例,且用例之间用空行分隔,需要按行读取并判断空行来区分用例。点的排序
对所有点按坐标排序:先按x xx升序,若x xx相同则按y yy升序。排序的目的是为了在后续枚举中避免重复计算。枚举点对并标记已处理
使用一个二维数组paired [ i ] [ j ] \texttt{paired}[i][j]paired[i][j]标记点对( i , j ) (i, j)(i,j)是否已经被处理过。由于直线是无向的,处理过( i , j ) (i, j)(i,j)后就不需要再处理( j , i ) (j, i)(j,i),因此只枚举i < j i < ji<j。检查共线
对于每个未被处理的点对( i , j ) (i, j)(i,j),遍历所有k > j k > jk>j的点,使用叉积判断三点是否共线:
c r o s s ( P i , P j , P k ) = ( P j . x − P i . x ) ⋅ ( P k . y − P i . y ) − ( P j . y − P i . y ) ⋅ ( P k . x − P i . x ) cross(P_i, P_j, P_k) = (P_j.x - P_i.x) \cdot (P_k.y - P_i.y) - (P_j.y - P_i.y) \cdot (P_k.x - P_i.x)cross(Pi,Pj,Pk)=(Pj.x−Pi.x)⋅(Pk.y−Pi.y)−(Pj.y−Pi.y)⋅(Pk.x−Pi.x)
若叉积为0 00,则P k P_kPk与P i , P j P_i, P_jPi,Pj共线。记录最大值
对于每个点对,统计共线的点数(包括P i P_iPi和P j P_jPj本身),并更新全局最大值。输出
每个测试用例输出一行结果,用例之间输出一个空行。
复杂度分析
- 最坏情况下,外层循环O ( N 2 ) O(N^2)O(N2)枚举点对,内层循环O ( N ) O(N)O(N)检查共线,总复杂度O ( N 3 ) O(N^3)O(N3)。
- 但由于使用了paired \texttt{paired}paired数组进行剪枝,以及排序后只检查k > j k > jk>j,实际上减少了重复计算。
- 对于N = 700 N = 700N=700,最坏情况仍可能达到O ( N 3 ) O(N^3)O(N3),但实际测试中可以通过。
为什么这样可行?
本题的N NN最大为699 699699,O ( N 3 ) O(N^3)O(N3)在3.4 × 10 8 3.4 \times 10^83.4×108量级,在优化良好的C++代码中,配合合适的剪枝和整数运算,可以在时限内通过。代码中的剪枝策略包括:
- 只处理i < j i < ji<j且未标记的点对;
- 只检查k > j k > jk>j的点,避免重复统计。
代码实现
// Lining Up// UVa ID: 270// Verdict: Accepted// Submission Date: 2016-05-11// UVa Run Time: 0.410s//// 版权所有(C)2016,邱秋。metaphysis # yeah dot net#include<bits/stdc++.h>usingnamespacestd;// 定义点的结构体,包含 x 和 y 坐标structpoint{intx,y;};// 排序比较函数:先按 x 升序,再按 y 升序boolcmp(point a,point b){if(a.x==b.x)returna.y<b.y;elsereturna.x<b.x;}vector<point>points;// 存储所有点intpaired[701][701];// 标记点对是否已被处理过intcounter[701];// 临时存储共线的点下标// 叉积函数:判断三点是否共线// 返回 (c - a) × (b - a)// 若返回值为 0,则三点共线intcrossProduct(point a,point b,point c){return(c.x-a.x)*(b.y-a.y)-(b.x-a.x)*(c.y-a.y);}// 计算所有点中位于同一直线上的最大点数intgetMaxPoints(){// 对点进行排序sort(points.begin(),points.end(),cmp);// 初始化 paired 标记数组for(inti=0;i<points.size();i++)for(intj=0;j<points.size();j++)paired[i][j]=0;intmaxPoint=0;// 记录全局最大共线点数// 枚举所有点对 (i, j),i < jfor(inti=0;i<points.size();i++)for(intj=i+1;j<points.size();j++)// 如果该点对已经被处理过,则跳过if(paired[i][j]==0){paired[i][j]=1;// 标记为已处理intindex=2;// 当前共线点集合包含 i 和 jcounter[0]=i;counter[1]=j;// 检查所有 k > j 的点是否与 i, j 共线for(intk=j+1;k<points.size();k++){if(crossProduct(points[i],points[j],points[k])==0)counter[index++]=k;}// 更新最大值maxPoint=max(maxPoint,index);}returnmaxPoint;}intmain(){cin.tie(0);cout.sync_with_stdio(false);boolfirst=true;// 控制输出空行intcases;string input;// 读取测试用例个数getline(cin,input);cases=stoi(input);getline(cin,input);// 读取空行while(cases--){points.clear();// 读取一个测试用例的所有点,直到遇到空行或 EOFwhile(getline(cin,input)){if(input.length()==0)break;intx,y;istringstreamiss(input);iss>>x>>y;points.push_back({x,y});}// 输出空行分隔不同测试用例的结果if(first)first=false;elsecout<<"\n";// 输出最大共线点数cout<<getMaxPoints()<<"\n";}return0;}总结
本题的核心是枚举所有点对,利用叉积判断共线,并通过排序和标记数组进行适当剪枝。虽然最坏复杂度为O ( N 3 ) O(N^3)O(N3),但在题目给定的数据范围和时限下可以顺利通过。如果要进一步优化,可以考虑使用斜率哈希的方法将复杂度降至O ( N 2 log N ) O(N^2 \log N)O(N2logN),但本题的解法已经足够清晰且易于实现。