爱情到来绝不能犹豫。
关注博主容不得迟缓。
题目:
答案:40257
解析1(改写set):
坑点:
本题目涉及double运算。但是对于浮点运算通常是很危险的。
主要的危险就是:精度损失。
精度损失的含义是,对于double通常存在对末尾的四舍五入。比如在1/10(十进制是0.1)但是在二进制存储却是:0.00011001100110011001100110011001100110011001100110011001 10011…
所以在进行0.1+1.1会出现:1.20000000000000018这样奇怪的答案。
这种答案是致命的,因为这种答案会导致进入set<>集合的时候。1.2000000000018和1.2被当成两个数字对待。但实际上,1.2000000…完全是精度的问题。所以我们称之位“精度损失”。
(参考文献:https://www.zhihu.com/question/42024389)
所以,我们在设计程序的时候,认为当两个double的差不大于1e-8,我们就认为他们是同一个数。
我们可以改写set集合来完成操作。首先您要知道set实现的大致原理。如果您不知道也没有关系。您可以这样记忆:
重写set的比较函数,当“比较函数”成功,便会执行一种类似的“插入”操作。但是如果比较失败(最终比较失败)就不会插入到这颗树上。(不理解也没有关系。您可以找找“set底层实现原理”这方面的文章参阅。
set是STL里面一个非常常用的数据结构。set使用了一种叫做“平衡二叉树”(红黑树)的结构来实现在O(log)级别进行“查找”和“删除”的操作。具体实现过程请移步别的教材。
set的比较函数一定要注意:必须要有顺序。
也就是:写出return a==b是决定禁止的,因为会出现重复插入,而且插入的位置也变得不确定。所以必须写成a<b这样的结构。(说的可能不清楚,因为我自己也是比较模糊这个地方。)
大致设计:
我们知道一条直线:
枚举所有的点,
y
=
k
x
+
b
y=kx+b
y=kx+b
我们对于所有的
(
x
1
,
y
1
)
(x1,y1)
(x1,y1)和
(
x
2
,
y
2
)
(x2,y2)
(x2,y2)计算直线的
k
k
k和
b
b
b即可。
放入set集合中查重。
然后注意到精度问题,当k1和k2(或者b1和b2)这个两个值的相差非常小(1e-8)我们就认为他们是同一个点,在集合中只记录一次。
正解程序:
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <math.h>
#include <set>
using namespace std;
class Line { //直线的K个B,判断直线是不是同一条,用set来查重。
public:
double k, b;
Line(double x, double y) {
this->k = x;
this->b = y;
}
};
bool equals(double a, double b) { //改写等于号的规则,我们认为当两个数差的不是特别大,那么就是同一个数。
if (fabs(a - b) <= 1e-8)
return true;
return false;
}
bool operator<(const Line& Line1, const Line& Line2) { //如果false就不插入树,如果true就插入树中。//改写set的比较函数。注意参数必须是const &。
if (equals(Line1.k, Line2.k)) {//如果k是相等的,就按照b的顺序插入树
if (equals(Line1.b, Line2.b))//如果k和b都是相等的,就是不能插入,false。找不到一个位置让这个Line插入这颗树上。
return false;
return Line1.b < Line2.b;
}
if (equals(Line1.k, Line2.k))//如果k是相同的,就false。
return false;
return Line1.k < Line2.k;//如果k不相同,就继续比较插入。你可能会好奇,上一行的if是多余的,其实不是的,这是因为为了想要让两个相差很微小的k,先return。要不然的话,进入Line1.k<Line2.k哪怕两个k非常非常接近(1e-8)也会进行判断。
}
int main() {
set<Line> Ssq;
for (int x1 = 0; x1 < 20; x1++) {
for (int y1 = 0; y1 < 21; y1++) {
for (int x2 = 0; x2 < 20; x2++) {
for (int y2 = 0; y2 < 21; y2++) {
if (x1 == x2 && y1 == y2) //同一个点没什么好搜的。
continue;
if (x1 != x2) //避免斜率不存在的情况
{
double k = (double) (y2 - y1) / (x2 - x1);
double b = y2 - k * x2;
Ssq.insert(Line(k, b));
}
}
}
}
}
cout << Ssq.size() + 20;//没有计算过x1==x2的直线,手动加上20来计算。
return 0;
}
解析2(避免double):
更改小数变形方式:
为了避开坑点,我们直接已分数的形式表现小数。
定义k1,k2。代表k的分子和分母。
然后我们希望负号总是在分子之上,所以我们稍作判断,总是保持分母是正数,然后分子视情况加上符号。(具体思想您可以看代码,很简单。)
我们还希望,所有的分数都是化简过的,方法就是,将分子分母同时除以gcd(分子,分母)。以此来约分化简。
最后,把k和b都以分数形式存入set中来查重。
计算公式的推演:
现在就是符号的问题啦!还有一个就是尽量不要出现0。
因为分母或者分子出现0总是一件很麻烦的事情。
所以无论是竖着的线,还是横着的线,我都不计算了。直接在最后的答案+20和+21。
正解代码:
#include <algorithm>
#include <cmath>
#include <iostream>
#include <set>
using namespace std;
typedef pair<int, int> P;
typedef pair<pair<int, int>, pair<int, int>> PP;
int gcd(int a, int b) {
if (b == 0)
return a;
return gcd(b, a % b);
}
set<pair<pair<int, int>, pair<int, int>>> Set; //构造一个set,里面能放两个pair类型。
int main() {
for (int x1 = 0; x1 < 20; x1++) {
for (int y1 = 0; y1 < 21; y1++) {
for (int x2 = 0; x2 < 20; x2++) {
for (int y2 = 0; y2 < 21; y2++) {
if (x1 == x2 && y1 == y2)
continue;
if (x1 != x2 && y1 != y2) { //避免斜率不存在的情况,和平行的情况。
int k1 = y2 - y1, k1f = 0; // K的分子 kf1记录k1的正负,我们总想把符号放到分子上
if (k1 < 0) //记录是不是负数
k1f = 1;
int k2 = x2 - x1, k2f = 0; // K的分母
if (k2 < 0)
k2f = 1;
int c = gcd(abs(k1), abs(k2));
k1 /= c; //化简
k2 /= c;
int b1 = y1 * (x2 - x1) - x1 * (y2 - y1), b1f = 0; //计算b的分子和分母,下面的过程和上面是一样的,
if (b1 < 0)
b1f = 1;
int b2 = x2 - x1, b2f = 0;
if (b2 < 0)
b2f = 1;
c = gcd(abs(b1), abs(b2));
b1 /= c;
b2 /= c;
// cout << pow(-1, k1f + k2f) * abs(k1) << "/" << abs(k2) << " " << pow(-1, b1f + b2f) * abs(b1) << "/" << abs(b2) << endl;
Set.insert(PP(P(pow(-1, k1f + k2f) * abs(k1), abs(k2)), P(pow(-1, b1f + b2f) * abs(b1), abs(b2))));//把负数的符号放到分子上。
}
}
}
}
}
cout << Set.size() + 20 + 21;//因为没有计算横着和竖着的情况,别忘记加回去。
return 0;
}