2016青岛online——I count two three

本文介绍了一种算法思路,用于求解大于给定整数n的最小数,该数可以表示为2^a*3^b*5^c*7^d的形式。通过预先计算所有可能的组合并使用二分查找来快速定位目标值。

传送门

题意:输入n,求出能分解为2^a*3^b*5^c*7^d的大于n的最小数

基本思路:打表写出所有能分解为2^a*3^b*5^c*7^d的数,然后二分查找,lower_bound

复杂度:32*21*13*11

一开始不会的原因:想到要打表,但是不知道怎么入手,有多少个数,做题少

解决:2:0~31;3:0~20;5:0~12;7:0~10.最多32*21*13*11个,实际上远少于这个

坑点:一定要注意会不会爆。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
typedef long long ll;
const int MAXN=1e9+1e5;//+1e5是为了保证打出的表包括n为1e9时
int ans[103500];//32*21*13*11
int main()
{
	ll p;
	int tot=0;
	//打表,预处理 
	for(int i=0;i<=31;i++)
	{
		for(int k=0;k<=20;k++)
		{
			for(int m=0;m<=13;m++)
			{
				for(int n=0;n<=10;n++)
				{
					p=pow(2,i)*pow(3,k);
					if(p>=MAXN)
					{
						continue;
					}
					else
					{
						p*=pow(5,m);
						if(p>=MAXN)
						{
							continue;
						 } 
						 else
						{
							p*=pow(7,n);
							if(p>=MAXN)
							{
								continue;
							}
							else
							{
								ans[tot++]=(int)p;
							}
						}
					}
				}
			}
		}
	}
//	printf("%d\n",tot);
//	sort(ans,ans+tot);
//	for(int i=0;i<100;i++)
//	{
//		printf("%d\n",ans[i]);
//	}
//排序,以便二分
    sort(ans,ans+tot);
    int t;
    scanf("%d",&t);
    int n;
    while(t--)
    {
		scanf("%d",&n);
//前闭后开,返回大于等于n的第一个位置
		int index=lower_bound(ans,ans+tot,n)-ans;
		printf("%d\n",ans[index]);    	
	}
 } 


<think>我们首先需要理解梯形的定义:梯形是至少有一对平行边的四边形。因此,我们可以通过寻找具有平行边的四边形来计数梯形。 思路: 1. 我们需要枚举四个点,但直接枚举四个点的时间复杂度是O(n^4),不可取。我们需要更高效的方法。 2. 另一种思路:利用梯形的性质——至少有一对平行边。我们可以考虑所有可能的线段(由两个点确定),然后按斜率分组。具有相同斜率的线段是平行的。 3. 步骤: a. 遍历所有点对,计算每两个点形成的线段的斜率,并将斜率相同的线段归为一组(注意:斜率可能不存在,即垂直线段,我们可以用无穷大或者一个特殊值表示)。 b. 但是注意:梯形的两个平行边可能由两对不同的点构成,而且一个四边形可能有多于一对的平行边(比如平行四边形),但我们要避免重复计数。 4. 然而,直接按斜率分组后,在同一个斜率组内的线段是平行的。那么,如果我们从同一个斜率组中任意选择两条不相交的线段(即四条不同的点),就可以构成一个梯形吗?不一定,因为这两条线段可能长度不同,或者位置关系使得它们不能构成一个梯形(梯形要求是四边形,且仅要求一对边平行,另外两条边不平行)。但是,实际上,只要两条线段平行,且它们不共线,并且没有三个点共线,那么由这两条线段(四条不同的点)构成的四边形一定有一对平行边(即这两条线段),所以一定是梯形。但是,如果这两条线段共线,那么四个点就共线了,这样的四边形退化了。因此,我们需要排除共线的情况。 5. 但是,还有另一个问题:一个四边形可能有两对平行边(平行四边形),那么它会被多个斜率组计数(比如按两组平行边分别计数)。因此,我们需要避免重复计数。或者,我们可以先计算所有至少有一对平行边的四边形,然后再减去有两对平行边的四边形(即平行四边形)的重复计数部分?但是这样比较麻烦。 6. 另一种方法:我们只要求至少有一对平行边,那么我们可以先计算所有可能的四边形(即任意四个点)中至少有一对平行边的个数。但是直接枚举四个点还是太慢。 7. 高效方法: - 我们首先将所有点对(线段)按斜率分组。设斜率为k的组有L(k)条线段。 - 对于同一个斜率组k,任意两条线段(由四个不同的点)可以构成一个梯形(只要这四个点不共线,且这两条线段没有公共点)。但是,同一个四边形可能被不同的斜率组重复计数(如果它有多对平行边),因此我们需要考虑去重。 - 但是,我们也可以这样想:一个四边形最多可能有两对平行边(平行四边形),那么它会被两个不同的斜率组(如果两对边斜率不同)或者同一个斜率组(如果两对边斜率相同,即菱形?但实际上菱形两对边斜率相同吗?注意:菱形是特殊的平行四边形,两对边分别平行,但相邻边不一定平行,所以两对边的斜率一般是不同的两个值)计数两次。 - 因此,如果我们直接对每个斜率组内计算组合数,然后相加,那么平行四边形会被计算两次,而普通的梯形(只有一对平行边)只被计算一次。如果我们设总共有T个梯形(包括平行四边形)和P个平行四边形,那么按照斜率组计算的总数就是 T + P(因为每个平行四边形被计算了两次,而普通梯形只计算一次:T = 普通梯形数 + P,所以总计数 = 普通梯形数 + 2P = T + P)。因此,我们最终需要减去平行四边形的个数,才能得到真正的梯形数(包括平行四边形)?不对,因为梯形包括平行四边形。所以实际上我们要求的是所有至少有一对平行边的四边形,包括平行四边形。 - 但是,我们最终要求的是梯形(至少一对平行边),而平行四边形也是梯形的一种(有两对平行边)。所以我们的目标就是计数所有至少有一对平行边的四边形。那么,如果我们按照斜率组计算的总数(即每个斜率组内取两条不相交的线段)就是所有至少有一对平行边的四边形的两倍?不对,因为一个四边形如果只有一对平行边,那么它只被一个斜率组计数一次;如果它有两对平行边,那么它被两个斜率组各计数一次。所以总数 = (只有一对平行边的四边形数)+ 2*(有两对平行边的四边形数)。而我们要求的是:梯形总数 = (只有一对平行边的四边形数)+ (有两对平行边的四边形数)。因此,我们设: S = 所有斜率组中,组内取两条不相交线段(即四条不同点)的组合数之和 P = 平行四边形的个数(即有两对平行边的四边形) 那么,梯形总数 = S - P ??? 不对,因为S = (只有一对平行边的四边形) + 2P,而梯形总数 = (只有一对平行边的四边形) + P = S - P。 - 因此,我们需要分别计算S和P。 8. 计算S: - 对于每个斜率组k,我们计算该组内所有线段。注意:同一个斜率组内的线段可能共线(即多条线段在同一条直线上),但我们要排除四条点共线的组合(因为退化了,不能构成四边形)。同时,我们要求两条线段没有公共点(即四个点互不相同)。 - 但是,在同一个斜率组内,我们如何计算两条线段没有公共点且不共线的组合数?注意:如果两条线段有公共点,那么它们就共享一个点,这样四个点就变成了三个点(退化)。所以我们要选择两条无公共点的线段。 - 然而,同一个斜率组内可能有多条线段共线(即它们都在同一条直线上),那么任意两条线段(即使没有公共点)所包含的四个点也是共线的,这样的四边形是退化的,不能算作梯形。因此,我们需要避免这种情况。 - 所以,我们不应该将共线的线段放在同一个组内?不对,因为共线的线段斜率相同,自然在同一个组内。因此,我们需要在同一个斜率组内再按截距分组(或者按直线分组),即同一条直线上的线段归为一个子组。这样,同一个子组内的线段都是共线的。 - 因此,对于斜率组k,我们将其中的线段再按截距分组(或者按直线方程ax+by+c=0的c归一化后的值分组)。这样,同一个截距组内的线段共线,不同截距组内的线段平行但不共线。 - 那么,在同一个斜率组k内,不同截距组(即不同直线)之间选择两条线段(每条线段来自一个截距组),就可以构成一个梯形(因为这两条线段平行且不共线,所以四个点一定构成梯形)。注意:这里要求两条线段没有公共点,但由于来自不同的直线,所以自然不会有公共点(除非相交,但平行线不会相交,所以点都是不同的)。 - 因此,对于斜率组k,设它有m个截距组(即m条平行直线),每个截距组i有c_i条线段(注意:每条线段由两个点构成)。那么,在斜率组k中,选择两条线段(分别来自两个不同的截距组)的组合数是多少? - 首先,我们选择两条不同的直线:从m条直线中选2条,有C(m,2)种方法。 - 然后,在第一条直线上任选一条线段(有c_i条),在第二条直线上任选一条线段(有c_j条)。所以组合数为:sum_{i<j} [c_i * c_j]。 - 另外,也可以这样计算:所有截距组中,任选两条线段的总组合数(不考虑是否同一直线)为:C(total,2) = total*(total-1)/2,其中total为斜率组k中所有线段的数量。然后减去同一直线上的线段组合数:sum_i [C(c_i,2)]。那么,不同直线上的线段组合数 = [total*(total-1)/2] - sum_i [c_i*(c_i-1)/2]。 - 因此,对于斜率组k,其贡献为:total_k*(total_k-1)/2 - sum_i [c_i*(c_i-1)/2],其中total_k是斜率组k中所有线段的数量。 - 然后,S = 所有斜率组k的贡献之和。 9. 计算P:平行四边形的个数。平行四边形有两组对边分别平行。因此,我们需要找到两组平行边,且这两组边构成一个平行四边形。注意,平行四边形的四个点必须满足向量关系:设四个点为A,B,C,D(按顺序),则向量AB平行于向量DC,向量AD平行于向量BC。或者也可以按对角线性质:对角线互相平分。 - 如何高效计算?我们可以通过枚举对角线的中点。因为平行四边形的对角线互相平分,所以两条对角线的中点重合。因此,我们可以: a. 计算所有点对的中点(即线段的中点)和线段向量(或者长度?)注意:中点相同的点对可能构成平行四边形的对角线。 b. 将中点相同的点对分组。设中点为M的点对有k个,那么这k个点对可以两两构成平行四边形(因为两条对角线互相平分)。但是,这样构成的四边形一定是平行四边形吗?不一定,因为还需要两组对边分别平行。实际上,我们只保证了对角线互相平分,这等价于四边形是平行四边形(在平面几何中)。所以,只要两条对角线互相平分,则四边形是平行四边形。 c. 因此,对于中点M,有k个点对(即k条以M为中点的线段),那么我们可以从中任选两条线段(作为对角线),就可以构成一个平行四边形。所以组合数为C(k,2)。 d. 但是,注意:这里我们要求这两条线段(对角线)所对应的四个点互不相同。但是,由于两条线段共享中点M,且没有公共端点(因为如果共享端点,那么中点相同要求两个端点相同,但同一个点对不会重复计算),所以四个点一定互不相同。 e. 因此,P = 对所有中点M,计算C(k_M, 2)之和,其中k_M是以M为中点的点对数量。 - 但是,这里有一个问题:同一个平行四边形有两条对角线,所以当我们枚举两条对角线时,这个平行四边形被计数了一次。而平行四边形的两条对角线的中点相同,所以我们在同一个中点组内计数了一次。因此,这样计算P是正确的。 10. 因此,梯形总数 = S - P (因为S中每个平行四边形被计数了两次(因为有两组平行边),而P是平行四边形的个数,所以S中包含了两次P,而我们只需要一次,所以减去一个P,这样梯形总数就等于S-P) 11. 但是,我们还需要注意:在计算S时,我们排除了共线的情况(即两条线段来自同一直线的情况),所以S中都是有效的梯形(非退化)。同样,P中的平行四边形也是有效的。 12. 步骤总结: 步骤1:读入点集,假设点用vector<pair<int,int>> points存储。 步骤2:预处理所有点对(注意:点对是无序的,即(i,j)和(j,i)视为同一条线段,我们只处理i<j的情况)。 - 我们用一个map(或者unordered_map)来按斜率分组。斜率可以用分数形式表示,避免浮点数误差。对于垂直线,斜率用无穷大表示(例如,用一个bool标记垂直,分子用1,分母用0,但注意分母为0的情况)。 - 同时,我们还要记录每条线段所在的直线(用标准形式ax+by+c=0,但为了区分不同的直线,我们可以用归一化的(a,b,c)表示,其中a,b,c互质,且a>=0,如果a=0则b>=0,等等)。这样,同一个斜率组内的线段再按直线分组。 步骤3:计算S。 S = 0 遍历斜率组中的每个直线子组: 记录每个直线子组内的线段数c_i,并计算整个斜率组的总线段数total_k。 然后,该斜率组的贡献 = total_k*(total_k-1)/2 - sum_i [c_i*(c_i-1)/2] 将每个斜率组的贡献累加到S。 步骤4:计算P。 用一个map(key为点(中点),value为计数)记录以每个中点出现的次数。 注意:中点可能不是整数,所以我们需要用分数表示中点?或者用double?但是为了避免精度问题,我们可以用两个整数表示分数:分子和分母。但这里我们假设坐标是整数,那么中点坐标可能是半整数。我们可以用pair<long, long>表示中点(乘以2以避免浮点数),即中点(x,y)表示为(2*x, 2*y)?或者直接存储为double(但可能有精度问题,不推荐)。所以,我们用: mid_x = (x_i + x_j) [整数,但可能是偶数也可能是奇数] mid_y = (y_i + y_j) 然后,我们存储中点时,不除以2,而是存储和。这样,同一个中点(两个坐标和相同)就是同一个中点。但是注意,不同的点对可能有相同的和中点?是的。所以,我们用一个map,key为pair(mid_x, mid_y),value为计数。 具体:枚举所有点对(i,j)(i<j),计算中点坐标的和:mid_x = x_i+x_j, mid_y = y_i+y_j。则这个点对的中点可以表示为(mid_x, mid_y)。然后,map[make_pair(mid_x, mid_y)]++。 然后,P = 0 遍历map中每个元素,设某个中点的计数为k,则P += k*(k-1)/2。 步骤5:梯形总数 = S - P 13. 但是,这里有一个问题:在计算S时,我们要求两条线段来自不同的直线(即不同的截距组),这样构成的四边形一定是梯形(非退化)。但是,我们并没有排除其他退化情况(比如三点共线)?实际上,我们保证了两条线段平行且不共线,所以四个点不共线。但是,有可能另外两个点与这两条线段中的某条线段共线?注意,我们只要求两条线段平行且不共线,所以这两条线段所在的直线是两条平行线,因此四个点分布在两条平行线上,且每条直线上两个点。所以,这样的四边形一定是梯形(有一对平行边),且不会退化(因为两条平行线不同,所以四个点不共线)。但是,有可能出现自交?但梯形可以是凸的也可以是凹的?实际上,我们这里只考虑简单四边形(没有自交),但题目没有明确,通常我们只考虑凸四边形?但是,点分布在两条平行线上,那么构成的四边形可能是凸四边形,也可能是凹四边形?实际上,一定是凸四边形(因为两条平行线,每条线上两个点,那么这四个点按顺序连接一定是凸四边形)?不一定,如果一条直线上的两个点在另一条直线的同一侧,那么可能是凹的?让我们画图:两条平行线L1和L2,L1上有两点A、B,L2上有两点C、D。如果A和B在L1上顺序排列,C和D在L2上顺序排列,那么连接A,C,B,D,则可能是凹的?实际上,我们并不要求连接顺序。但是,我们定义四边形是由四个点按任意顺序连接形成的吗?通常,我们按凸包来定义?但题目没有说。所以,我们只要求四个点不共线,且有一对平行边,就认为是一个梯形(无论凸凹)。 14. 但是,我们还需要注意:同一个四边形可能被多个斜率组计数?比如,平行四边形会被两个斜率组(分别对应两组平行边)各计数一次。所以,在步骤3中,S已经包含了平行四边形两次。而步骤4中,P是平行四边形的个数(每个平行四边形被计数一次)。所以,梯形总数 = S - P 就是正确的(因为对于平行四边形,它在S中被计数了两次,我们减去一次,所以最终每个平行四边形被计数一次;而对于普通梯形,它在S中只被计数一次,减去0,所以也是一次)。 15. 但是,还有一种四边形:它有两组平行边(平行四边形)?我们上面已经考虑了。所以,最终结果就是所有至少有一组平行边的四边形的个数。 16. 边界情况:三点共线?注意,在计算S时,我们要求两条线段来自不同的直线,所以不会出现三点共线(因为每条直线上只有两个点,且另外两个点在另一条平行线上)。所以,四个点中不会出现三点共线(除非这两条平行线重合?但我们已经排除了同一直线的情况)。所以,我们排除了共线的情况。 17. 实现细节: a. 斜率表示:为了避免浮点数精度问题,我们用最简分数形式表示斜率。同时,处理垂直线(分母为0)和水平线(分子为0)的情况。 对于两个点(x1,y1), (x2,y2),斜率dx=x2-x1, dy=y2-y1。如果dx=0且dy=0,跳过(同一个点,但我们的点对是不同的点,所以不会出现)。如果dx=0,则斜率表示为(1,0)(表示无穷大)。如果dy=0,则斜率表示为(0,1)。否则,我们计算g = gcd(dx,dy)(注意正负号),然后归一化:让分子dx/g,分母dy/g,然后我们规定:分母总是正数(如果分母为负,则分子分母同时取负;如果分子为0,则规定分母为正数1)。或者,我们规定分子非负,如果分子为负则同时取反;如果分子为0且分母为负,则取反。更简单:我们规定分子分母互质,且分母非负,如果分母为0则分子为1(对于垂直线)?不对,分母为0时,分子可以是任意非零数,但我们用(1,0)表示所有垂直线。 具体:我们用一个pair<int,int>表示斜率,其中: 如果dx==0 && dy!=0 -> (1,0) [垂直线] 如果dy==0 && dx!=0 -> (0,1) [水平线] 否则,如果dx!=0 && dy!=0: g = gcd(|dx|,|dy|),然后分子=dx/g,分母=dy/g。 然后,我们调整符号:使得分母为正,如果分母为负则分子分母同时取负;如果分母为0则按上面的情况处理(已经处理了分母为0的情况)。 但是,注意:dx和dy可能有负的,所以gcd取绝对值。然后,我们调整符号:让分母为正,分子可以任意符号?但是,我们希望相同的斜率用相同的pair表示。例如,dx=2,dy=4和dx=-2,dy=-4应该归一化为(1,2)?不对:dx=-2,dy=-4,归一化:分子=-2/gcd(2,4)=-1,分母=-4/gcd(2,4)=-2,然后我们调整:分母为正,所以分子分母同时乘以-1,得到(1,2)。而dx=2,dy=4归一化为(1,2)。所以可以。 gcd函数:注意处理负数。 b. 直线的表示:除了斜率,我们还需要截距来区分平行线。但是,截距也可以用分数表示,而且避免浮点数。我们可以用直线的一般式:ax+by+c=0。如何由两个点得到一般式? 已知两点(x1,y1), (x2,y2),则: a = y2 - y1 b = x1 - x2 c = x2*y1 - x1*y2 然后,我们可以归一化:将a,b,c除以gcd(a,b,c)(注意,gcd(a,b,c)=gcd(gcd(a,b),c)),并且规定a>=0,如果a=0则b>=0,如果a和b都为0则跳过(但两个不同的点不会出现a=b=0)。 这样,同一条直线有唯一表示。 c. 但是,在同一个斜率组内,我们按直线分组。所以,对于每个线段,我们计算它所在的直线(一般式归一化后的(a,b,c))。 d. 注意:同一个点对(线段)会计算一次,然后我们将其加入斜率组(以斜率作为key的map),然后在斜率组内,再按直线(一般式)分组。 e. 存储结构: map< pair<int,int>, map< tuple<long,long,long>, int> > slopeGroups; 其中,外层的key是斜率(用pair(dx,dy)表示,已经归一化),内层的key是直线一般式的(a,b,c)(归一化后),但a,b,c可能很大,我们可以用一个三元组(注意:归一化后,a,b,c是互质的整数,且a>=0,如果a=0则b>=0,如果a=b=0则不可能出现)。但是,三元组(a,b,c)可能为负数?c可以为负,但a,b我们已经规定非负(除了a=0时b>=0)。所以,我们可以用一个tuple<long, long, long>存储归一化后的(a,b,c)。 但是,我们也可以不用三元组,而用截距?但是截距在一般式中就是c,但需要归一化。所以,还是用归一化的一般式。 f. 但是,归一化一般式时,a,b,c可能很大,而且需要除以gcd,注意溢出。 18. 由于点坐标是整数,所以dx,dy是整数,a,b,c也是整数。 19. 另一种简化:我们不需要存储整个直线,因为同一个斜率组内,不同的直线由截距决定。而截距可以用c和a,b的关系来表示?但是,用归一化的一般式可以避免重复。 20. 实现步骤: a. 初始化: vector<Point> points; // 存储所有点 // 读入点 b. 枚举所有点对(i,j) (0<=i<j<n): dx = points[j].x - points[i].x; dy = points[j].y - points[i].y; // 计算斜率表示 pair<int,int> slope; if (dx == 0 && dy == 0) { // 跳过相同的点,但题目保证点不同?所以应该不会出现 continue; } else if (dx == 0) { // 垂直线 slope = make_pair(1, 0); } else if (dy == 0) { // 水平线 slope = make_pair(0, 1); } else { // 计算dx,dy的最大公约数(绝对值) int g = gcd(abs(dx), abs(dy)); dx /= g; dy /= g; // 调整符号:使得分母为正(这里分母是dy?不对,我们是用(dx,dy)表示斜率,所以分母是dy?实际上,我们想用dx/dy表示斜率,所以分母是dy。但为了统一,我们规定:dy>0,如果dy<0则dx,dy同时取反。 if (dy < 0) { dx = -dx; dy = -dy; } // 现在dy>0,但dx可能为负或正。 slope = make_pair(dx, dy); } // 计算直线一般式:a, b, c long a = dy; // 注意:这里a=dy, b=-dx, 因为一般式:a(x-x1)+b(y-y1)=0,但通常用两点式:(y2-y1)(x-x1) = (x2-x1)(y-y1) -> (y2-y1)x - (x2-x1)y + (x2-x1)y1 - (y2-y1)x1 = 0 long b = -dx; long c = (long)dx * points[i].y - (long)dy * points[i].x; // dx*y1 - dy*x1 // 归一化:除以gcd(a,b,c),注意a,b,c可能为负,我们要求a>=0,如果a=0则b>=0。 // 先计算g = gcd(gcd(|a|,|b|), |c|) long g = gcd(gcd(abs(a), abs(b)), abs(c)); if (g != 0) { // 避免除以0,但a,b不可能同时为0(因为dx,dy不同时为0),所以g不会为0?但c可能为0,所以g可能是0吗?不会,因为a和b至少一个非0,所以gcd(a,b)非0,所以g非0。 a /= g; b /= g; c /= g; } // 调整符号:使得a>=0,如果a==0则b>=0 if (a < 0 || (a==0 && b<0)) { a = -a; b = -b; c = -c; } // 现在,直线用(a,b,c)表示,且归一化。 // 将这条线段加入斜率组slopeGroups[slope]中,并加入内层map的直线(a,b,c)对应的计数加1。 c. 注意:在计算一般式时,我们用了dx和dy,而dx和dy已经除以g1(斜率的gcd),但这里归一化一般式时又除了一次gcd,这样没有问题,因为一般式的gcd可能更大?但我们的归一化是独立的。 21. 但是,注意:当我们用dx和dy计算a=dy, b=-dx时,那么gcd(a,b) = gcd(dy, dx) = gcd(dx,dy)(绝对值),而之前我们已经将dx,dy除以了g1,所以gcd(dx,dy)可能不是原始的最大公约数?不对,我们之前已经除以了g1,所以gcd(dx,dy)现在应该是1?不一定,因为除以g1后,dx和dy互质?是的,gcd(dx,dy)=1(因为我们除以了g)。所以,在计算一般式归一化时,gcd(a,b)=gcd(dy,dx)=1,所以gcd(a,b,c)=gcd(1,|c|)=1?所以归一化后a,b,c不变?不对,c可能很大,但gcd(1,|c|)=1,所以归一化后a,b,c不变。所以,我们可以省略归一化?但是,c可能和a,b有公因子吗?因为a,b已经互质,所以gcd(a,b,c)=gcd(1,|c|)=1,所以归一化后不变。因此,我们可以不归一化?但是,我们调整符号的步骤还是需要的(以保证同一个直线用唯一的表示)。 所以,调整符号的步骤不能省略,但除以gcd的步骤可以省略?因为gcd=1。但是,如果c很大,我们除以1后不变。所以,我们可以只做符号调整。 但是,如果c不为0,且a,b互质,那么gcd(a,b,c)可能大于1吗?例如,a=2, b=2, c=4(但a,b互质要求gcd(a,b)=1,所以这种情况不会出现)。所以,确实gcd=1。所以,我们只需要调整符号。 因此,步骤简化为: a = dy; b = -dx; c = dx*y1 - dy*x1; // 调整符号:使得a>=0,如果a==0则b>=0 if (a < 0 || (a==0 && b<0)) { a = -a; b = -b; c = -c; } 然后,用(a,b,c)作为直线的key。 22. 但是,注意:dx和dy可能很大,所以a,b,c可能很大,但tuple<long,long,long>可以存储。 23. 计算S: long long S = 0; for (auto& group : slopeGroups) { auto& lineMap = group.second; // 内层map,key为直线,value为该直线上的线段数 long long total_in_group = 0; for (auto& line : lineMap) { total_in_group += line.second; // 累加该斜率组中所有直线上的线段数 } long long same_line_pairs = 0; for (auto& line : lineMap) { long long cnt = line.second; same_line_pairs += cnt * (cnt-1) / 2; } long long total_pairs = total_in_group * (total_in_group-1) / 2; S += (total_pairs - same_line_pairs); } 24. 计算P: map<pair<long, long>, int> midCount; // key: (mid_x, mid_y) 这里我们用两倍坐标?或者直接用和(不除以2)?注意,我们存储的是中点的两倍坐标?不,我们存储的是中点坐标乘以2(这样保证整数)?但题目没有说坐标范围,如果坐标很大,那么和也很大,但用long可以。 // 但是,我们也可以不乘以2,直接用两个坐标的和(即中点坐标的分子部分,因为我们不除以2)?这样也可以,因为同一个中点的两个坐标和是唯一的(只要坐标是整数,那么中点坐标乘以2就是整数,我们存储这个整数)。 // 所以,我们存储中点:mid_x = x_i+x_j, mid_y = y_i+y_j,用pair<long,long>表示。 for (int i=0; i<n; i++) { for (int j=i+1; j<n; j++) { long mid_x = (long)points[i].x + points[j].x; long mid_y = (long)points[i].y + points[j].y; midCount[make_pair(mid_x, mid_y)]++; } } long long P = 0; for (auto& it : midCount) { long long k = it.second; P += k*(k-1)/2; } 25. 最终结果:梯形总数 = S - P 26. 但是,我们还需要考虑:一个四边形在S中被计数了几次?我们分析过,平行四边形被计数了两次(因为有两组平行边),而普通梯形被计数了一次。而P是平行四边形的个数,所以减去P后,平行四边形被计数了一次,普通梯形被计数了一次。所以,结果就是所有梯形的总数。 27. 注意:在计算中点时,我们枚举了所有点对,所以每个点对都计数了。而平行四边形有两条对角线,所以一个平行四边形会被计数两次(因为两条对角线的中点相同,所以同一个中点组内计数了两次),然后我们计算C(k,2)时,这两次计数会组合一次(即一个平行四边形对应一个组合,所以正确)。 28. 但是,我们还需要考虑:在计算S时,我们只考虑了有一组平行边的情况(并且排除了同一直线),但是,一个四边形可能有多于两对平行边吗?不可能,最多两对(平行四边形)。所以,S中每个四边形至多被计数两次(当且仅当它是平行四边形)。 29. 因此,代码实现如上。 30. 但是,这个算法的时间复杂度是O(n^2),空间复杂度也是O(n^2),对于n=1000,点对数量大约50万,可以接受。但如果n更大,比如10000,那么点对数量5000万,可能会超时或超内存。但题目没有给出n的范围,我们假设n在1000以内。 31. 如果n很大,我们可能需要优化。但题目没有要求,我们先按此实现。 32. 代码实现: 注意:我们假设点坐标是整数,且点不重复。 33. 编写gcd函数: long gcd(long a, long b) { if (b == 0) return a; return gcd(b, a % b); } 但是,我们之前计算斜率时,gcd是int,而一般式是long,所以需要两个版本?或者统一用long。 34. 由于坐标范围未知,我们统一用long。 35. 代码结构: #include <iostream> #include <vector> #include <map> #include <set> #include <tuple> #include <cmath> #include <algorithm> #include <cctype> using namespace std; typedef long long ll; struct Point { ll x, y; Point(ll x=0, ll y=0): x(x), y(y) {} }; ll gcd(ll a, ll b) { if (b == 0) return abs(a); return gcd(b, a % b); } int main() { int n; cin >> n; vector<Point> points; for (int i=0; i<n; i++) { ll x, y; cin >> x >> y; points.push_back(Point(x,y)); } // 定义斜率分组:外层map:key为斜率pair(dx,dy),内层map:key为直线(用tuple(a,b,c)表示),value为线段数 map<pair<ll,ll>, map<tuple<ll,ll,ll>, ll> > slopeGroups; // 枚举所有点对 for (int i=0; i<n; i++) { for (int j=i+1; j<n; j++) { ll dx = points[j].x - points[i].x; ll dy = points[j].y - points[i].y; // 归一化斜率 if (dx == 0 && dy == 0) { // 两点重合,跳过(但题目应无重复点) continue; } pair<ll,ll> slope; if (dx == 0) { slope = make_pair(1,0); } else if (dy == 0) { slope = make_pair(0,1); } else { ll g = gcd(abs(dx), abs(dy)); dx /= g; dy /= g; if (dy < 0) { // 保证分母为正 dx = -dx; dy = -dy; } slope = make_pair(dx, dy); } // 计算直线一般式:a = dy, b = -dx, c = dx*y_i - dy*x_i ll a = dy; ll b = -dx; ll c = dx * points[i].y - dy * points[i].x; // 归一化直线:调整符号,使得a>=0,如果a==0则b>=0 if (a < 0 || (a==0 && b<0)) { a = -a; b = -b; c = -c; } // 将直线表示为tuple tuple<ll,ll,ll> line = make_tuple(a, b, c); // 将线段加入对应的组 slopeGroups[slope][line]++; } } // 计算S ll S = 0; for (auto& group : slopeGroups) { ll total = 0; for (auto& lineCount : group.second) { total += lineCount.second; } ll same_line = 0; for (auto& lineCount : group.second) { ll cnt = lineCount.second; same_line += cnt * (cnt-1) / 2; } S += total*(total-1)/2 - same_line; } // 计算P map<pair<ll,ll>, ll> midCount; for (int i=0; i<n; i++) { for (int j=i+1; j<n; j++) { ll mid_x = points[i].x + points[j].x; ll mid_y = points[i].y + points[j].y; midCount[make_pair(mid_x, mid_y)]++; } } ll P = 0; for (auto& p : midCount) { ll cnt = p.second; P += cnt * (cnt-1) / 2; } // 梯形总数 = S - P ll ans = S - P; cout << ans << endl; return 0; } 36. 测试:用一些简单数据测试。 例如:四个点构成一个平行四边形,那么梯形总数应该是1(因为平行四边形也是梯形)?但注意,我们期望的梯形数包括平行四边形。 设四个点:A(0,0), B(1,0), C(1,1), D(0,1) 这个四边形是平行四边形,也是梯形。 计算: S:斜率分组 首先,有6条线段:AB, AC, AD, BC, BD, CD 但是,我们分组: 斜率0(水平):AB, DC -> 但是D(0,1),C(1,1) -> DC的斜率=(1-1)/(1-0)=0,所以是水平线。同样,AD(0,0)->(0,1)是垂直线,BC(1,0)->(1,1)是垂直线。 水平线段:AB(0,0)->(1,0): 斜率(1,0)?不对,我们归一化:dx=1,dy=0 -> 斜率(0,1) [因为dy=0,所以用(0,1)表示水平线] DC: (0,1)->(1,1): dx=1,dy=0 -> 斜率(0,1) 垂直线段:AD(0,0)->(0,1): dx=0,dy=1 -> 斜率(1,0) BC(1,0)->(1,1): dx=0,dy=1 -> 斜率(1,0) 另外两条对角线:AC(0,0)->(1,1): 斜率(1,1) [归一化后] BD(0,1)->(1,0): 斜率(1,-1) -> 归一化:dx=1,dy=-1 -> 除以gcd(1,1)=1,然后dy为负,所以取反:dx=-1,dy=1 -> 再归一化分母为正:dy=1>0,所以斜率(-1,1) -> 但注意,我们要求分母为正,所以这里表示为(-1,1)?但是,我们也可以调整分子分母的符号使得分子为负?不对,我们只调整了分母的符号(保证分母为正),分子可以是负的。 分组: 斜率(0,1)组:包含两条线段:AB和DC(注意:DC是D(0,1)到C(1,1))? 但是,我们计算直线: AB: 点(0,0)和(1,0) -> a=0, b=-1, c=0 -> 归一化:a=0,b=-1,c=0 -> 调整符号:a=0,则要求b>=0,所以乘以-1:a=0,b=1,c=0 -> (0,1,0) DC: 点(0,1)和(1,1) -> a=0, b=-1, c=1 -> 归一化:a=0,b=-1,c=1 -> 调整:a=0且b<0,所以乘以-1:a=0,b=1,c=-1 -> (0,1,-1) 所以,斜率(0,1)组内有两条不同的直线:两条水平线(y=0和y=1)。 因此,该组的贡献:total=2,same_line_pairs=0(因为每条直线上只有一条线段),所以贡献=2*(1)/2=1。 斜率(1,0)组:包含两条线段:AD和BC。 AD: (0,0)和(0,1) -> a=1, b=0, c=0 -> (1,0,0) [归一化后] BC: (1,0)和(1,1) -> a=1, b=0, c=-1 -> (1,0,-1) 同样,贡献=1。 斜率(1,1)组:AC -> 一条线段,贡献0(因为需要两条线段) 斜率(-1,1)组:BD -> 一条线段,贡献0。 所以,S=1+1=2。 P:中点 点对AB:中点(0+1,0+0)=(1,0) -> 计数1 AC: (0+1,0+1)=(1,1) -> 1 AD: (0,1) -> (0+0,0+1)=(0,1) -> 1 BC: (1+1,0+1)=(2,1) -> 1 BD: (0+1,1+0)=(1,1) -> 再出现一次,所以计数2 CD: (1+0,1+1)=(1,2) -> 1 所以,中点(1,1)出现2次,其他中点出现1次。 因此,P = C(2,2)=1 (只有(1,1)这个中点有2次,所以1个组合) 梯形总数 = S - P = 2-1=1。 正确。 再测试一个普通梯形:A(0,0), B(1,0), C(0.5,1), D(1.5,1) [注意:这里为了简单,用整数?但我们的算法要求整数,所以改为:A(0,0), B(2,0), C(1,2), D(3,2) -> 这样AB是底边,CD是顶边,平行。另外,AD和BC不平行。 四条边:AB(0,0)->(2,0), CD(1,2)->(3,2) 平行(水平线) AC(0,0)->(1,2), BD(2,0)->(3,2) 不平行(斜率分别为2和2/1=2,等等,AC的斜率=(2-0)/(1-0)=2,BD的斜率=(2-0)/(3-2)=2,所以也平行?这样就成了平行四边形了。所以换点:C(0,2), D(3,2),这样AC(0,0)->(0,2)是垂直线,BD(2,0)->(3,2)的斜率=(2-0)/(3-2)=2,所以不平行。 重新定义:A(0,0), B(2,0), C(0,2), D(3,2) -> 这样AB和CD:AB是水平线,CD是水平线,所以平行。AC是垂直线,BD是斜线。 点:A(0,0), B(2,0), C(0,2), D(3,2) 线段: AB: (0,0)-(2,0): 斜率(0,1) -> 直线:a=0, b=-2, c=0 -> 归一化:除以gcd(0,2,0)=2?不对,归一化步骤:a=0,b=-2,c=0 -> 调整:a=0且b<0,所以乘以-1:a=0,b=2,c=0 -> 再除以gcd(0,2,0)=2(因为gcd(0,2)=2, gcd(2,0)=2)-> a=0,b=1,c=0 -> (0,1,0) CD: (0,2)-(3,2): 斜率(3,0) -> 水平线,斜率(0,1) -> 直线:a=0, b=-3, c=6 -> 归一化:调整后a=0,b=3,c=-6 -> 除以gcd(0,3,6)=3 -> a=0,b=1,c=-2 -> (0,1,-2) AC: (0,0)-(0,2): 斜率(1,0) -> 直线:a=2, b=0, c=0 -> (1,0,0) [归一化后除以2] AD: (0,0)-(3,2): 斜率(3,2) -> 归一化:dx=3,dy=2 -> gcd=1 -> 斜率(3,2) -> 直线:a=2, b=-3, c=0 -> 归一化:gcd(2,3,0)=1 -> 调整:a>=0 -> 已经是正,所以(2,-3,0) -> 调整符号:a=2>0,所以不变,但要求b>=0?不对,我们只要求a>=0,b可以为负。所以就是(2,-3,0)?但这样和后面归一化不一致?我们再看归一化步骤:没有要求b非负,只要求a>=0,如果a=0则b>=0。所以这里a=2>0,b可以为负。所以直线为(2,-3,0)。 BC: (2,0)-(0,2): 斜率(-2,2) -> 归一化:dx=-2,dy=2 -> gcd=2 -> dx=-1,dy=1 -> 然后dy>0,所以斜率(-1,1) -> 直线:a=2, b=2, c=-4? 计算:a=dy=2, b=-dx=-(-2)=2, c= dx*y1 - dy*x1 = (-2)*0 - 2*2 = -4 -> 归一化:除以gcd(2,2,4)=2 -> (1,1,-2) BD: (2,0)-(3,2): 斜率(1,2) -> 归一化:(1,2) -> 直线:a=2, b=-1, c= - (1*0 - 2*2) = 4? 重新计算:a=dy=2, b=-dx=-1, c= dx*y1 - dy*x1 = 1*0 - 2*2 = -4 -> 归一化:gcd(2,1,4)=1 -> 调整:a=2>0,所以不变 -> (2,-1,-4) 分组: 斜率(0,1)组:AB和CD -> 两条线段,分别来自两条直线,所以贡献=1(组合数) 斜率(1,0)组:AC -> 一条线段,贡献0 斜率(3,2)组:AD -> 一条线段,贡献0 斜率(-1,1)组:BC -> 一条线段,贡献0 斜率(1,2)组:BD -> 一条线段,贡献0 另外,还有BC的斜率(-1,1)和BD的斜率(1,2)等,但都没有两条线段。 所以,S=1。 P:中点 AB: (0+2,0+0)=(2,0) AC: (0+0,0+2)=(0,2) AD: (0+3,0+2)=(3,2) BC: (2+0,0+2)=(2,2) BD: (2+3,0+2)=(5,2) CD: (0+3,2+2)=(3,4) 所有中点都不同,所以P=0。 梯形总数=1-0=1。 正确。 37. 但是,注意:在计算直线时,我们使用了归一化,但归一化时除以gcd(a,b,c)时,由于a,b,c可能很大,gcd计算可能慢,但n=1000,点对50万,所以可以接受。而且,我们之前已经证明,a和b互质,所以gcd(a,b,c)=gcd(1,|c|)=1,所以实际上gcd=1,所以我们可以省略除以gcd的步骤,只做符号调整?这样更快。 修改:去掉归一化中的除以gcd,只做符号调整。 所以,代码修改: // 计算直线一般式:a = dy; b = -dx; c = dx*y_i - dy*x_i; // 调整符号:使得a>=0,如果a==0则b>=0 if (a < 0 || (a==0 && b<0)) { a = -a; b = -b; c = -c; } 然后,用(a,b,c)作为直线的key。 38. 但是,这样会不会导致同一条直线有不同的表示?例如,一条直线可能被表示为(2,-3,0)和(4,-6,0),但因为我们调整了符号(并且不除以gcd),那么(4,-6,0)会调整符号吗?不会,因为a=4>0,所以不变。所以,同一条直线可能有不同的表示(因为乘以2后,没有归一化)。所以,我们必须归一化(除以gcd)以保证唯一性。 因此,不能省略除以gcd。 或者,我们可以用另一种方法:在调整符号之前,先除以gcd。但这样计算量较大。 考虑到n最大1000,点对数量50万,所以计算50万次gcd(三个数的gcd)可能较慢,但我们可以接受。 所以,还是保留除以gcd。 39. 修改:在调整符号之前,先计算g,然后除以g。 ll g = gcd(gcd(abs(a), abs(b)), abs(c)); if (g == 0) { // 理论上不会出现,因为a,b不同时为0,所以gcd(a,b)至少为1(因为a,b互质?不对,a=dy, b=-dx,而dx,dy互质,所以gcd(a,b)=gcd(dy,dx)=1?所以gcd(a,b)为1,所以g至少为1,不会为0。所以可以跳过。 // 但为了安全,如果g==0,则g=1(避免除0) g = 1; } a /= g; b /= g; c /= g; // 然后调整符号 if (a < 0 || (a==0 && b<0)) { a = -a; b = -b; c = -c; } 40. 由于a,b,c可能为0?但a和b不会同时为0(因为dx,dy不同时为0),所以gcd(a,b)至少为1,所以g>=1,所以不会除0。 41. 最终代码: 注意:gcd函数要能处理0。 ll gcd(ll a, ll b) { a = abs(a); b = abs(b); if (b == 0) { if (a == 0) return 1; // 避免除0,但a,b不同时为0,所以这里实际上不会进入,但为了安全,如果a和b都为0,返回1。 return a; } return gcd(b, a % b); } 但是,我们之前调用gcd(a,b)时,a和b可能是0?比如a=0(水平线),b不为0。所以,gcd(0,b)=|b|,然后gcd(|b|,|c|)就是gcd(b,c)。所以,需要处理0。 修改gcd函数(非递归): ll gcd(ll a, ll b) { a = abs(a); b = abs(b); if (a == 0 && b == 0) return 1; if (a == 0) return b; if (b == 0) return a; while (b) { ll t = b; b = a % b; a = t; } return a; } 然后,三个数的gcd:gcd(a,b,c) = gcd(gcd(a,b),c) 42. 完整代码: 由于题目要求C++代码,我们按照上述思路编写。 43. 注意:点坐标范围可能很大,所以用long long。 44. 代码中,我们假设点不重复。 45. 如果点有重复,那么我们需要去重?但题目没有说,我们假设没有重复点。 46. 输出答案。 47. 测试时,输入用空格分隔的整数。 48. 运行测试用例。 49. 提交代码。 注意:由于实际题目中n可能较大,但这里n<=1000,所以O(n^2)可以接受。 下面给出完整代码。</think>以下是使用C++编写的程序,用于计算给定二维平面上N个点中能组成梯形的四个点的组合数量。程序基于梯形的几何特性(至少有一对平行边)进行高效计数,避免了O(n^4)的暴力枚举。 ### 算法思路 1. **斜率分组**:枚举所有点对,计算线段斜率并归一化(处理水平和垂直线)。相同斜率的线段分为一组,表示平行线段。 2. **直线分组**:在同一斜率组内,按直线一般式(ax+by+c=0)归一化后分组,区分平行但不共线的线段。 3. **计算S(所有至少有一对平行边的四边形)**: - 对每个斜率组,计算不同直线上线段的组合数(排除共线情况)。 - 公式:$S = \sum_{\text{斜率组}} \left( \binom{\text{total}}{2} - \sum_{\text{直线}} \binom{\text{count}}{2} \right)$ 4. **计算P(平行四边形数量)**: - 利用平行四边形对角线互相平分的性质,统计中点相同的点对数量。 - 公式:$P = \sum_{\text{中点}} \binom{\text{count}}{2}$ 5. **梯形数量**:$ \text{ans} = S - P$(平行四边形在S中被计数两次,需减去重复)。 ### C++代码 ```cpp #include <iostream> #include <vector> #include <map> #include <tuple> #include <algorithm> #include <cmath> using namespace std; typedef long long ll; struct Point { ll x, y; Point(ll x = 0, ll y = 0) : x(x), y(y) {} }; ll gcd(ll a, ll b) { a = abs(a); b = abs(b); if (a == 0 && b == 0) return 1; if (a == 0) return b; if (b == 0) return a; while (b) { ll t = b; b = a % b; a = t; } return a; } int main() { int n; cin >> n; vector<Point> points; for (int i = 0; i < n; i++) { ll x, y; cin >> x >> y; points.push_back(Point(x, y)); } // 斜率分组:外层key=斜率(dx,dy),内层key=直线(a,b,c),value=线段数 map<pair<ll, ll>, map<tuple<ll, ll, ll>, ll>> slopeGroups; // 枚举所有点对 for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { ll dx = points[j].x - points[i].x; ll dy = points[j].y - points[i].y; // 处理重合点(理论上无重复) if (dx == 0 && dy == 0) continue; pair<ll, ll> slope; if (dx == 0) { slope = make_pair(1, 0); // 垂直线 } else if (dy == 0) { slope = make_pair(0, 1); // 水平线 } else { ll g = gcd(abs(dx), abs(dy)); dx /= g; dy /= g; if (dy < 0) { // 保证分母为正 dx = -dx; dy = -dy; } slope = make_pair(dx, dy); } // 计算直线一般式: ax + by + c = 0 ll a = dy; ll b = -dx; ll c = dx * points[i].y - dy * points[i].x; // 归一化直线:除以gcd(a,b,c) ll g1 = gcd(abs(a), abs(b)); ll g = gcd(g1, abs(c)); if (g == 0) g = 1; a /= g; b /= g; c /= g; // 调整符号:a>=0,若a=0则b>=0 if (a < 0 || (a == 0 && b < 0)) { a = -a; b = -b; c = -c; } auto line = make_tuple(a, b, c); slopeGroups[slope][line]++; } } // 计算S:所有至少有一对平行边的四边形 ll S = 0; for (auto& group : slopeGroups) { ll total_segments = 0; for (auto& line : group.second) { total_segments += line.second; } ll same_line_pairs = 0; for (auto& line : group.second) { ll cnt = line.second; same_line_pairs += cnt * (cnt - 1) / 2; } ll total_pairs = total_segments * (total_segments - 1) / 2; S += total_pairs - same_line_pairs; } // 计算P:平行四边形数量(利用中点性质) map<pair<ll, ll>, ll> midCount; for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { ll mid_x = points[i].x + points[j].x; ll mid_y = points[i].y + points[j].y; midCount[make_pair(mid_x, mid_y)]++; } } ll P = 0; for (auto& m : midCount) { ll cnt = m.second; P += cnt * (cnt - 1) / 2; } // 梯形总数 = S - P ll ans = S - P; cout << ans << endl; return 0; } ``` ### 使用说明 1. **输入格式**: - 第一行:点数 `n` - 后续 `n` 行:每行两个整数 `x y`,表示点的坐标 2. **输出**:一个整数,表示梯形的组合数量 3. **示例输入**: ``` 4 0 0 2 0 1 2 3 2 ``` 4. **示例输出**: ``` 1 ``` ### 算法分析 - **时间复杂度**:$O(n^2 \log M)$,其中 $M$ 为坐标范围(主要开销在gcd计算和map操作)。 - **空间复杂度**:$O(n^2)$(存储斜率组和中点计数)。 - **优化点**:适用于 $n \leq 1000$;若 $n$ 更大,需优化数据结构(如哈希表替代map)。 ### 相关问题 1. 如何处理浮点数精度问题? **答**:通过分数表示斜率和归一化直线方程,避免浮点数计算[^1]。 2. 如何扩展算法到三维空间? **答**:三维梯形定义不同,需重新设计(如使用向量点积判断平行)。 3. 是否有更低复杂度的算法? **答**:当前 $O(n^2)$ 已较优;可尝试随机算法或近似算法处理更大规模数据[^2]。 4. 如何排除共线点的影响? **答**:算法已通过直线分组排除共线情况(同一直线的点不构成有效梯形)[^3]。 5. 代码如何处理大整数坐标? **答**:使用 `long long` 存储坐标和中间结果,避免溢出[^4]。 [^1]: 斜率归一化使用分数形式,避免浮点误差。 [^2]: 大规模数据可考虑分治或采样优化。 [^3]: 同一直线的点对在计算中被显式排除。 [^4]: 中点计算和直线系数使用64位整数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值