【题解】第十二届蓝桥杯(省赛)第一场:C题 直线 C/C++ (两种解法)

爱情到来绝不能犹豫。

关注博主容不得迟缓。

题目:

在这里插入图片描述
答案: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;
}
  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值