欧几里得几何求解——建模实践

本文提出了一种基于程序求解欧几里得几何问题的方法,针对游戏中的尺规作图挑战,通过建模、编码及优化策略实现了自动寻找解的过程。文章详细介绍了问题背景、需求分析、模型构建、编码实现及不断优化的全过程。

欧几里得几何

目录

一,背景

二,需求

三,建模

1,数据层

2,事务层

3,对外依赖

4,调度层

四,编码

1,数据层

2,事务层

3,对外依赖

4,调度层

五,小结

1,事务层

2,对外依赖

3,核心领域知识

4,缺陷

5,代码中的彩蛋

六,破解关卡

1,1.2垂直平分线

2,3.8菱形

七,代码改进

1,修改play函数,搜索所有的解

2,剪枝

3,继续破解3.8菱形

4,6.6平移线段

八,分步判定

1,分步判定思路

2,数据结构扩充

3,分步判定实现

(1)判定集

(2)修改isGoodEntity函数,只需要id化即可

(3)分步判定

(4)调度(输入控制)

4,继续破解6.6平移线段

5,6.8平行四边形

6,6.9欧拉九点圆

九,继续优化

1,把静态局部变量改为全局变量

2,优化哈希方案——两级哈希

3,线段和直线

4,重新数据化

5,可视化——在线作图

6,优化第一步

7,优化最后一步

8,优化剪枝方案(待更新)

9,完整代码


 

一,背景

这是一个尺规作图的游戏,必须要把一章完美通过才能进入下一关,于是我卡在了第6章。

道具有3种:基本工具、复合工具、辅助工具

(1)基本工具:圆规和直线,1L1E,其中L是非辅助性道具的使用次数,E是基本工具的使用次数

(2)复合工具:垂直平分线工具、平行线工具等,是前面关卡通过尺规作图作出来之后解锁的工具,1LxE,x>1

(3)辅助工具:点工具、相交工具、手势工具,0L0E

注意:相交工具是取圆和直线的交点,点工具是在圆上或者直线上或者平面内任意地方取一点

每一关要想完美通过,有2个任务:L任务和E任务。

这个游戏的E任务比L任务难很多,但是E任务建模要比L任务简单,因为每个复合工具要体现成代码还是有不少工作量的,而且也会增大搜索算法求解问题的困难度。

 

二,需求

开发一个程序,用来求解欧几里得几何的E任务。

需求分析:

1,几乎所有关卡都有不需要点工具的解法,本程序只考虑不需要点工具的解法

(有趣的是,点工具可以说是所有工具里面最简单的,但是对于建模、对于求解来说却都是比较麻烦的)

当然,对于必须使用点工具的关卡,如果情况比较简单,在初始化局面的时候人为的添加点即可

2,初始局面由若干圆和直线组成,进行给定次数的基本工具的操作之后,变成满足关卡要求的最终局面

3,基本工具的操作的次数是给定的,一般在10以内,辅助工具任意用,复合工具不能用

4,每次操作有圆规和直线两种情况,如果看游戏提示的话,还能直接知道每次是圆规还是直线。每使用一次基本工具之后,默认充分使用相交工具,即把所有的相交点都取出来。

 

三,建模

1,数据层

这个游戏是通过基本工具和交点工具,把初始局面转化成最终满足条件的局面,所以这里有2个概念:局面、工具。

因为本游戏具有马尔科夫效应(无后效性),所以只需要静态局面信息即可,不需要存储历史操作记录。

局面是数据,由圆、直线、点组成,工具是方法

2,事务层

(1)基本工具:给定任意两点,画一个圆或直线

(2)相交工具:求出,新增的圆或直线,和已有的圆和直线,相交的所有点。

(3)更新局面,根据新增的圆或直线,和新增交点,刷新局面

3,对外依赖

对于不同的关卡,需要输入给程序的有3个信息:

(1)初始局面

(2)基本工具操作集,每次是圆还是直线

(3)终态局面判定:判定最终的局面是否满足游戏要求

4,调度层

(1)核心算法:搜索解空间,每一步枚举任意两点,调用工具接口和更新局面的接口

(2)顶层调度

 

四,编码

1,数据层

点的话很容易表示:

typedef struct Point {
    double x, y;
} Point;

直线采用一般式:ax + by + c = 0,表示一条直线需要3个数。 

圆的表示方法有很多,考虑到圆都是圆规做出来的,所以用标准式更方便计算

圆的标准式:(x-a)^2 + (y-b)^2 = c^2

typedef enum TYPE {
    CIRCLE,
    LINE,
    BUFF
}TYPE;

typedef struct Node {
    TYPE type;
    double a, b, c;
} Node;

最后,用圆、直线、点表示局面:

typedef struct Entity {
    vector<Node> vecNode;
    vector<Point> vecPoint;
} Entity;

为什么用vector,不用set呢?

因为要给判重做重载,相比给==符号做重载,我更习惯写独立接口

2,事务层

(1)基本工具:给定任意两点,画一个圆或直线

//两点距离平方和
double getDistanceSquar(Point a, Point b)
{
    return pow(a.x - b.x, 2) + pow(a.y - b.y, 2);
}
//两点距离
double getDistance(Point a, Point b)
{
    return sqrt(getDistanceSquar(a, b));
}
//画圆
Node makeCircle(Point center, Point a)
{
    return Node{ CIRCLE,center.x, center.y, getDistance(center, a) };
}
//画直线
Node makeLine(Point a, Point b)
{
    return Node{ LINE,b.y - a.y, a.x - b.x, a.y * b.x - a.x * b.y };
}
Node make(TYPE t, Point a, Point b)
{
    if (t == CIRCLE) {
        return makeCircle(a, b);
    }
    else {
        return makeLine(a, b);
    }
}

这一块我做了统一的接口,根据参数决定画圆还是画直线。

(2)相交工具:求出,新增的圆或直线,和已有的圆和直线,相交的所有点。

圆和圆、圆和直线的交点个数可能为0,1,2,直线和直线的交点个数可能为0,1

所以,这一块我做了统一的接口,返回2个交点,不够的都用常量invalidPoint来表示

直线和直线:

#define dps 0.0000001
Point invalidPoint= {0,12345};
bool sameValue(double x, double y)
{
    return abs(x - y) < dps;
}
// 直线的判定式
double deltaLineAndLine(Node s, Node t)
{
    return s.a * t.b - s.b * t.a;
}
// 两直线是否平行
bool isParallels(Node s, Node t)
{
    return sameValue(deltaLineAndLine(s, t), 0);
}
// 两直线交点
pair<Point, Point> lineAndLine(Node s, Node t)
{
    if (isParallels(s, t)) {
        return make_pair(invalidPoint, invalidPoint);
    }
    Point p = { (t.c * s.b - s.c * t.b) / deltaLineAndLine(s, t), (s.c * t.a - s.a * t.c) / deltaLineAndLine(s, t) };
    return make_pair(p, invalidPoint);
}

圆和直线:

// 一般式化为斜率式
void normallizeLine(Node &s)
{
    s.a /= -s.b, s.c /= -s.b, s.b /= -s.b;
}
// 圆和直线相交的判定式
double deltaCircleAndLine(Node s, Node t)
{
    //normallizeLine(t);
    return s.c * s.c * (t.a * t.a + 1) - pow(t.a * s.a - s.b + t.c, 2);
}
// 圆和线的交点数目,有0,1,2三种情况
int cicleAndLineNum(Node s, Node t)
{
    double delta = deltaCircleAndLine(s, t);
    if (delta < 0) {
        return 0;
    }
    if (delta == 0) {
        return 1;
    }
    return 2;
}
// 圆和线的交点
pair<Point, Point> cicleAndLine(Node s, Node t)
{
    normallizeLine(t);
    int ret = cicleAndLineNum(s, t);
    if (ret == 0) {
        return make_pair(invalidPoint, invalidPoint);
    }
    double sqrtDelta = sqrt(deltaCircleAndLine(s, t));
    Point p1, p2;
    p1.x = (s.a + t.a * s.b - t.a * t.c + sqrtDelta) / (t.a * t.a + 1), p1.y = p1.x * t.a + t.c;
    p2.x = (s.a + t.a * s.b - t.a * t.c - sqrtDelta) / (t.a * t.a + 1), p2.y = p2.x * t.a + t.c;
    return make_pair(p1, p2);
}

圆和圆:

// 圆的判定式
double deltaCircle(Node s)
{
    return s.a * s.a + s.b * s.b - s.c * s.c;
}
// 两圆相交的两点连成的直线,不需要判定两圆是否相交
Node cicleAndCicleToLine(Node s, Node t)
{
    return Node{ LINE, (t.a - s.a) * 2, (t.b - s.b) * 2, deltaCircle(s) - deltaCircle(t) };
}
//两圆交点
pair<Point, Point>cicleAndCicle(Node s, Node t)
{
    return cicleAndLine(s, cicleAndCicleToLine(s, t));
}

这里巧妙的利用了一个领域知识(平面几何知识)优化模型,消除重复知识

用两圆的标准式相减得到的直线,和任意一圆的交点个数,都等于两圆之间的交点个数

统一的接口:

//任意俩个Node相交
pair<Point, Point> getIntersectPoint(Node s, Node t)
{
    if (s.type == CIRCLE && t.type == CIRCLE) {
        return cicleAndCicle(s, t);
    }
    if (s.type == LINE && t.type == LINE) {
        return lineAndLine(s, t);
    }
    if (s.type == CIRCLE && t.type == LINE) {
        return cicleAndLine(s, t);
    }
    if (t.type == CIRCLE && s.type == LINE) {
        return cicleAndLine(t, s);
    }
    return make_pair(invalidPoint, invalidPoint);
}

(3)更新局面,根据新增的圆或直线,和新增交点,刷新局面

新增交点:

//两点重合
bool samePoint(Point a, Point b)
{
    return sameValue(a.x, b.x) && sameValue(a.y, b.y);
}
bool alreadyHavePoint(vector<Point>v, Point p)
{
    for (auto it = v.begin(); it != v.end(); it++) {
        if (samePoint(*it, p)) {
            return true;
        }
    }
    return false;
}
void addPoint(Entity& ent, Point p)
{
    if (!samePoint(p, invalidPoint) && !alreadyHavePoint(ent.vecPoint, p)) {
        ent.vecPoint.push_back(p);
    }
}
void addPoint(Entity& ent, pair<Point, Point>pai)
{
    addPoint(ent, pai.first);
    addPoint(ent, pai.second);
}
void addPoint(Entity& ent, Node s)
{
    for (auto it = ent.vecNode.begin(); it != ent.vecNode.end(); it++) {
        addPoint(ent, getIntersectPoint(*it, s));
    }
}

新增圆或直线:

bool sameCircle(Node s, Node t)
{
    return sameValue(s.a, t.a) && sameValue(s.b, t.b) && sameValue(s.c, t.c);
}
bool sameLine(Node s, Node t)
{
    normallizeLine(s);
    normallizeLine(t);
    return sameValue(s.a, t.a) && sameValue(s.c, t.c);
}
bool sameNode(Node s, Node t)
{
    if (s.type != t.type) {
        return false;
    }
    if (s.type == CIRCLE) {
        return sameCircle(s, t);
    }
    return sameLine(s, t);
}
bool alreadyHaveNode(Entity ent, Node s)
{
    for (auto it = ent.vecNode.begin(); it != ent.vecNode.end(); it++) {
        if (sameNode(*it, s)) {
            return true;
        }
    }
    return false;
}
bool addNode(Entity &ent, Node s)
{
    if (alreadyHaveNode(ent, s)) {
        return false;
    }
    addPoint(ent, s);
    ent.vecNode.push_back(s);
    return true;
}

3,对外依赖

对于不同的关卡,需要输入给程序的有3个信息:

(1)初始局面

(2)基本工具操作集,每次是圆还是直线

(3)终态局面判定:判定最终的局面是否满足游戏要求

经过领域专家(笔者客串)的分析,终态局面判定问题可以表述为,最终局面是否包括某些特定的圆、直线、点

这里我用了函数重载和模板编程,写出了优美的代码:

void input(TYPE &t)
{
    int x;
    cin>>x;
    t=TYPE(x);
}
void input(Node &s)
{
    input(s.type);
    cin>>s.a>>s.b>>s.c;
}
void input(Point &p)
{
    cin>>p.x>>p.y;
}

template<typename T>
void initVector(vector<T>&v)
{
    int len;
    cin>>len;
    v.resize(len);
    for(int i=0;i<len;i++){
        input(v[i]);
    }
}

Entity initEnt()
{
    Entity ent;
    initVector<Node>(ent.vecNode);
    initVector<Point>(ent.vecPoint);
    return ent;
}

vector<TYPE> initVecType()
{
    vector<TYPE>v;
    initVector<TYPE>(v);
    return v;
}

Entity goodEnt;
bool isGoodEntity(Entity ent)
{
    
    for (auto it = goodEnt.vecNode.begin(); it != goodEnt.vecNode.end(); it++) {
        if (!alreadyHaveNode(ent, *it)) {
            return false;
        }
    }
    for (auto it = goodEnt.vecPoint.begin(); it != goodEnt.vecPoint.end(); it++) {
        if (!alreadyHavePoint(ent.vecPoint, *it)) {
            return false;
        }
    }
    return true;
}

4,调度层

(1)核心算法:搜索解空间,每一步枚举任意两点,调用工具接口和更新局面的接口

Entity g_ent;
bool play(Entity ent, vector<TYPE> vt)
{
    Entity empty;
    if (vt.empty()) {
        g_ent = ent;
        return isGoodEntity(ent) ? true : false;
    }
    for (int i = 0; i < ent.vecPoint.size(); i++) {
        for (int j = 0; j < ent.vecPoint.size(); j++) {
            if (j == i)continue;
            Node node = make(vt[0], ent.vecPoint[i], ent.vecPoint[j]);
            Entity tmp = ent;
            TYPE temp = vt[0];
            if (!addNode(ent, node)) {
                ent = tmp;
                continue;
            }
            vt.erase(vt.begin());
            if (play(ent, vt)) {
                return true;
            }
            else {
                ent = tmp;
                vt.insert(vt.begin(),temp);
            }
        }
    }
    return false;
}

这只是一个极其简单的demo,要想正常工作,必须要做大量剪枝。

毫不夸张的说,剪枝才是算法的核心,但是我暂时没打算耗大量时间把这个算法调通,这一部分暂时略过,只备注一下简单的思路:

首先,去重是肯定需要而且核心的,先画直线A再画直线B,和先画

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值