题目描述:给一些散点,找出可以构成平行四边形的数量
输入
第一行是测试样本个数n<=100
接下来有n组数据,每组数据第一行是m<=400,表示接下来的点数,接下来有m行,每行有两个整数表示点坐标
输出
有n行,每行是每个样本的平行四边形个数
样例输入
2
6
0 0
2 0
4 0
1 1
3 1
5 1
7
-2 -1
8 9
5 7
1 1
4 8
2 0
9 8
样例输出
5
6
提示
注意四点共线不算平行四边形,使用哈希求解。
点可能有重复。比如A和A'重复且ABCD是平行四边形,这样算2个。
提示:平行四边形两对角线中点重合。(中点可转化为坐标和形式,避免因为/2需要引入小数运算)参考课上讲解的寻找正方形个数的代码。
提供哈希样例:key = (((mid[k].x) * 1000 + (mid[k].y) * 1234) % MD + MD) % MD; (MD=119997)
首先,讲一下寻找正方形个数的代码:
简而言之,给出两个点,那么能和这两个点组成正方形的点的位置是一定的,一共有四组。只要在已有点的哈希表里面找有没有这四组点就可以了。由于散列的查找复杂度是O(1),所以复杂度只集中在对点的两两遍历。
但是在平行四边形查找中,这个思想我一开始尝试了,但不太可行,因为两个点确定的平行四边形,要求另外两个点是平行且长度相等。其实可以类似地,通过三个点来确定平行四边形第四个点的坐标,但这样的时间复杂度就到O(n^3),在OJ里过不了,c
考虑题目中的提示:平行四边形两对角线中点重合。
其实差一些平行四边形的算法,用的也大多是这个方法,但是在本题中问题更加复杂:有点重合的情况,而且有平行的情况,这两个情况解决花了大力气。
最后我的解决方案是:把两俩遍历的中点存入哈希表。平行的情况,通过在每个中点结构体加入成员:斜率k,如果两个中点斜率一样且坐标重合,那么他们是平行线而不是平行四边形。点重合的情况通过在点结构体加入成员int times解决,只要在最后统计累加时,把times乘进去即可。
代码实现:
首先是哈希表的实现:
#define NL 1001
#define MD 199997
#define ADD 20010
int hash1[MD];
//输入k,根据p[k]点xy值,把k存入hash1表中对应的桶内
void hashing(int k) { // 线性试探法
int key = (((p[k].x ) * 1000 + (p[k].y) *1234) % MD +MD) % MD;
while (hash1[key] > 0) key = (key + 1) % MD;
hash1[key] = k;
}
//输入po点,根据po找到存在hash1表中的点位置,如果找到且点一样则返回1
int searching(POINT po) { // 线性试探法
int key = (((po.x ) * 1000 + (po.y) *1234) % MD +MD) % MD;
while (hash1[key] > 0) {
int t = hash1[key];
if (p[t].x == po.x && p[t].y == po.y) return 1;
key = (key + 1) % MD;
}
return 0;
}
使用哈希样例:key = (((mid[k].x) * 1000 + (mid[k].y) * 1234) % MD + MD) % MD; (MD=119997)
这里两个函数,一共计算hash值,存入桶内,另一个是查找函数,看桶内是否有该坐标点。
接下来是定义点结构体:
struct POINT {
int x, y;
int times=1;
}p[NL];
struct midPOINT { //中点结构体,坐标和斜率
//如果坐标、斜率都相同,则是共线四点
int x, y;
int times=1;
double k;
};
bool cmp(midPOINT a,midPOINT b)
{
if(a.x==b.x)
if(a.y==b.y)
return a.k<b.k;
else return a.y<b.y;
return a.x<b.x;
}
vector<midPOINT> midp;//存储中点坐标的向量
如上所述,中点结构体多了times变量和k变量。
这里重载了cmp函数,是因为打算用<algorithm>里面的向量排序算法,懒得自己写快速排序了。把中点向量设置为全局变量,是因为方便一些主函数外的函数调用,参数表改得太麻烦了。
times变量设置为1,每次有重复的点加进来,times++即可;
void timesadd(POINT po){
int key = (((po.x ) * 1000 + (po.y) *1234) % MD +MD) % MD;//找到
while (hash1[key] > 0) {
int t = hash1[key];
if (p[t].x == po.x && p[t].y == po.y) {p[t].times++; return;}
key = (key + 1) % MD;
}
}
这里图方便可以调用上面的searching函数,但考虑到时间复杂度还是重新实现了一遍searching过程。
最后是主函数:
int main(){
int m;
scanf("%d", &m);
int *result = new int[m];
int n;
for(int ii=0;ii<m;ii++){
scanf("%d", &n);
if (!n) break;
memset(hash1, -1, sizeof(hash1));
for (int i = 1; i<=n; i++) {
scanf("%d%d", &p[i].x, &p[i].y);
if(searching(p[i])) //如果在已有点找到相同点
{ //则该点times++
timesadd(p[i]);
i--;n--;
}
else hashing(i); //如果没有相同点,则建立散列表
}
int sum = 0; midPOINT dr1; //int ok1, ok2;
for (int i = 1; i<=n; i++) {
for (int j = i + 1; j<=n; j++) {
dr1.y = p[i].y + p[j].y; dr1.x = p[j].x + p[i].x; // 中点坐标*2
dr1.k= p[i].x == p[j].x ?(double)MD :((double)p[i].y - (double)p[j].y)/((double)p[i].x - (double)p[j].x);
dr1.times=p[i].times*p[j].times;
midp.push_back(dr1);
//cout<<"point: "<<p[i].x<<","<<p[i].y<<","<<p[i].times<<" "<<p[j].x<<","<<p[j].y<<","<<p[j].times;cout<<endl;
//cout<<"midpoint: "<<dr1.x<<","<<dr1.y<<","<<dr1.k<<" ";cout<<endl;
}
}
sort(midp.begin(),midp.end(),cmp);
// for (int i = 0; i<midp.size(); i++)
// {cout<<"point: "<<midp[i].x<<","<<midp[i].y<<","<<midp[i].k<<","<<midp[i].times;cout<<endl;}
//向量排序、遍历、相同求和
int num=1,eqnum=1;
for(int i=0;i<midp.size()-1;i++)
{
if(midp[i].x==midp[i+1].x&&midp[i].y==midp[i+1].y) num++;
else if(num==1) ;
else
{
int low=i-num+1,high=i;
for(int kk=low;kk<high;kk++)
for(int jj=kk+1;jj<=high;jj++)
sum+=midp[jj].times*midp[kk].times;
num=1;
}
if(midp[i].x==midp[i+1].x&&midp[i].y==midp[i+1].y&&midp[i].k==midp[i+1].k)eqnum++;
else if(eqnum==1) ;
else
{
int low=i-eqnum+1,high=i;
for(int kk=low;kk<high;kk++)
for(int jj=kk+1;jj<=high;jj++)
sum-=midp[jj].times*midp[kk].times;
eqnum=1;
}
//cout<<eqnum<<endl;
//cout<<"point: "<<midp[i].x<<","<<midp[i].y<<","<<midp[i].k<<","<<midp[i].times<<endl;
//cout<<"sum: "<<sum<<endl;
}
result[ii]=sum;
midp.clear();
for(int i = 1; i<=n; i++) p[i].times=1;
}
for (int j = 0; j<m; j++) cout<<result[j]<<endl;
cin>>n;
return 0;
}
第一步,初始化、建立哈希表,更新times值。
第二部,遍历中点,存入向量;
第三步,计算平行四边形个数。
两个难点问题:点重合、平行线,都是在主函数里解决的。
点重合是在建立哈希表的时候,如果以及存在该点,那么直接times++,不再插入点,而在之后中点和平行四边形下的计算中,times直接取乘积即可。这很容易理解:A点重合x1次,B点重合x2次,那么它们的中点(A+B)(省事儿,不除以二了,结果一样的)一共有x1*x2种组合,与另外的中点(C+D)如果能够组成平行四边形的话,两个中点的times乘积就是平行四边形的组合情况。
平行线的情况在最后计算统计的for循环里面解决。之前考虑过建立m个重合中点,m1个平行重点情况下的平行四边形个数递推公式,也推出来了,但是后面引入times解决重合点问题后,递推公式就不适用了。所以这里直接用遍历的方式找到多个重合中点的情况下,可能的平行四边形组合个数,平行线的直接在总数里面减去即可。
相较于不添加times变量,直接遍历计算的方法,使用times在点重合量大的情况下有时间和空间优势。例如n个点,m个重合,那么算法的复杂度是(n-m)^2,m越大优势越明显。当然,劣势是多了个变量,理解起来更困难,在m较小时也增加开销。
ps:这个代码是jqs老师2021-2022秋季学期数据结构课程OJ作业,如果看到有一样的代码是我本人写的,不是抄袭,老师助教明察。同学们理性参考。