目录
一,背景
这是一个尺规作图的游戏,必须要把一章完美通过才能进入下一关,于是我卡在了第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,和先画

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

621

被折叠的 条评论
为什么被折叠?



