【数据结构】平行四边形数量

题目描述:给一些散点,找出可以构成平行四边形的数量

输入

第一行是测试样本个数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作业,如果看到有一样的代码是我本人写的,不是抄袭,老师助教明察。同学们理性参考。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值