目录
一,背景
这是一个尺规作图的游戏,必须要把一章完美通过才能进入下一关,于是我卡在了第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,和先画直线B再画直线A是一样的,所以需要一个方法给局面做哈希映射。
因为局面其实是由圆和直线确定的,所以键是不需要点的。
map<vector<Node>, int> m;
很遗憾,这样也是不行的,因为直线的一般式:ax + by + c = 0,数值完全不同的2套a b c表示的也可能是一条直线,这块需要再优化一下。
其次,另外一个非常重要的剪枝思路就是,根据游戏的特性,在每次选取两点画圆或直线的时候,很多选择其实是可以直接排除掉的。这一块需要较深的领域知识。
最后,画直线的时候,先选点A再选点B,和先选点B再选点A是一样的,这也可以剪枝。
(2)顶层调度
void display(Node s)
{
if(s.type==CIRCLE){
printf("(x - %.3f)^2 + (y - %.3f)^2 = %.3f\n",s.a,s.b,s.c*s.c);
}else{
normallizeLine(s);
printf("y = %.3fx + %.3f\n",s.a,s.c);
}
}
void display(Entity ans)
{
for(unsigned i=0;i<ans.vecNode.size();i++){
display(ans.vecNode[i]);
}
}
int main()
{
Entity ent = initEnt();
vector<TYPE> vt = initVecType();
goodEnt = initEnt();
if(play(ent, vt))display(g_ent);
else cout<<"fail";
return 0;
}
五,小结
1,事务层
事务层作为一个整体,对外其实只有2个接口:
画图make,更新局面addNode
2,对外依赖
需要根据关卡实际情况,需要输入的有3个信息:初始局面,操作集,终态判定
因为全部是vector,而且里面全部是double,所以采用了统一的输入格式,每个vector都是输入一个整数n,再输入n个实数,3个信息分别是2个、1个、2个vector
3,核心领域知识
需要核心领域知识的是:领域剪枝策略。
这个是最难的领域知识,有待思考,其他的简单的知识,已经在上述建模和编码的过程中用进去了。
4,缺陷
代码中有把直线一般式化成斜率式,所以开局建模的时候涉及到直线的,不能用水平或者竖直的直线来表示,这就稍微增加了复杂度。
5,代码中的彩蛋
输出的顺序,刚好就是所求的操作的顺序。
六,破解关卡
1,1.2垂直平分线
初始直线方程是 y = 1.000x + 0.000
目标直线方程是 y = -1.000x + -0.000
所以输入:
1
1 1 -1 0
2
1 1
-1 -1
3
0 0 1
1
1 1 1 0
0
运行得到
2,3.8菱形
如果输入:
1
1 2.414213562 -1 -0.923879532
2
0.38268343236 0
0 -0.923879532
7
0 0 1 0 1 1 1
3
1 2.414213562 -1 0.923879532
1 -2.414213562 -1 -0.923879532
1 -2.414213562 -1 0.923879532
0
其中2.414213562是cotan(22.5), 0.923879532是cos(22.5), 0.38268343236是sin(22.5)
那么需要的时间比较长。
但是因为是画菱形,而且最后三步都是画直线,所以我们可以猜想,最后三步就是菱形的最后三条线,于是我们只让程序计算前面5步即可。
输入:
1
1 2.414213562 -1 -0.923879532
2
0.38268343236 0
0 -0.923879532
5
0 0 1 0 1
1
1 -2.414213562 -1 -0.923879532
0
运行得到:
对应的图是:
图上标注了最后一条线,这条线确实是正确的,但是这样的局面,最后2步是无解的。
那么,我能不能把所有方案都输出出来,人工去挑选正确答案呢?
七,代码改进
1,修改play函数,搜索所有的解
void play(Entity ent, vector<TYPE> vt)
{
if (vt.empty()) {
if (isGoodEntity(ent)) {
display(ent);
cout << endl;
}
return;
}
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());
play(ent, vt);
ent = tmp;
vt.insert(vt.begin(), temp);
}
}
}
int main()
{
Entity ent = initEnt();
vector<TYPE> vt = initVecType();
goodEnt = initEnt();
play(ent, vt);
cout << "end";
return 0;
}
结果,输出了大量相同的解。
2,剪枝
(1)直线避免画2次
for (int i = 0; i < ent.vecPoint.size(); i++) {
int startId = ((vt[0] == LINE) ? i : 0); // 剪枝
for (int j = startId; j < ent.vecPoint.size(); j++) {
if (j == i)continue;
(2)相同的局面不重复计算
先把double映射到整数,再把Node的vector映射到set,只需要Node不需要Point,最后给set做个哈希记录即可。
vector<int> normallizeNode(Node s)
{
if (s.type == LINE) {
normallizeLine(s);
}
vector<int>v(4);
v[0] = s.type, v[1] = int(s.a * 1000), v[2] = int(s.b * 1000), v[3] = int(s.c * 1000);
return v;
}
set<vector<int>>normallizeNode(vector<Node>vec)
{
set<vector<int>>s;
for(unsigned i=0;i<vec.size();i++)s.insert(normallizeNode(vec[i]));
return s;
}
void play(Entity ent, vector<TYPE> vt)
{
static map<set<vector<int>>, int>m;
if (m[normallizeNode(ent.vecNode)])return; // 剪枝
if (vt.empty()) {
if (isGoodEntity(ent)) {
display(ent);
cout << endl;
}
m[normallizeNode(ent.vecNode)] = 1;
return;
}
for (int i = 0; i < ent.vecPoint.size(); i++) {
int startId = ((vt[0] == LINE) ? i : 0); // 剪枝
for (int j = startId; 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());
play(ent, vt);
ent = tmp;
vt.insert(vt.begin(), temp);
}
}
m[normallizeNode(ent.vecNode)] = 1;
}
3,继续破解3.8菱形
输入不变,还是输入前5步,输出:
y = 2.414214x + -0.923880
(x - 0.383)^2 + (y - 0.000)^2 = 1.000
(x - 0.000)^2 + (y - -0.924)^2 = 1.000
y = -1.303225x + 0.498723
(x - -0.226)^2 + (y - 0.793)^2 = 3.000
y = -2.414214x + -0.923880
......此处省略其他11个答案
y = 2.414214x + -0.923880
(x - 0.000)^2 + (y - -0.924)^2 = 1.000
(x - -0.383)^2 + (y - -1.848)^2 = 1.000
y = -0.414214x + -1.465076
(x - -0.191)^2 + (y - -1.386)^2 = 0.250
y = -2.414214x + -0.923880
给的答案还是有点多,现在有2个思路:
(1)因为剪枝了,所以可以搜索更多的步数
(2)利用可视化工具,快速从答案列表中找出正在的答案
这2个思路可以同时开始尝试:
先运行程序,输入:
1
1 2.414213562 -1 -0.923879532
2
0.38268343236 0
0 -0.923879532
6
0 0 1 0 1 1
2
1 -2.414213562 -1 -0.923879532
1 -2.414213562 -1 0.923879532
0
在等待的过程中,思考如何做可视化。
然而,我突然又相到,只多加一层搜索,时间应该也就多十倍左右,不会太长,无论怎么样1分钟都能搜完。
但事实却是,搜了2分钟才有结果。
我突然又抓住了那一瞬间的灵感,此处需要剪枝!
在搜索了5步之后,如果需要的2条直线一条都没有出现,那么就不需要搜索第6步了。因为最后一步不可能产生2条直线。
好在2分钟还能勉强接受,可以算这一关达到要求了。
输出结果:
对应答案:
这样,就找到了正确的解法。
4,6.6平移线段
这是我在写本文时,无法过关的第一个关卡,虽然网上攻略一搜就有,但是咱程序员可是有自己的骄傲的!
运行程序,输入:
1
1 1 -1 0
3
0 0
1 1
5 0
6
0 0 0 0 1 0
0
1
6 1
其中,5 0这个点是随便取的,点和线段的位置并没有限制。
这里的终态判定就比较有意思了,我只写了6 1这个点,没有写直线的方程,但是几乎可以断定得到这个点的同时会得到这个线段。
程序运行了7分钟,还在运行中,被我干掉了。
因为我想到了一种更好的判定方案:
1
1 1 -1 0
3
0 0
1 1
5 0
5
0 0 0 0 1
1
1 1 -1 -5
0
因为最后一步是画圆,所以第5步肯定就已经得到这条直线了。
输出:
(y - 1.000000x + 0.000000)
((x - 0.000)^2 + (y - 0.000)^2 - 2.000)
((x - 0.000)^2 + (y - 0.000)^2 - 25.000)
((x - 5.000)^2 + (y - 0.000)^2 - 25.000)
((x - 3.536)^2 + (y - 3.536)^2 - 25.000)
(y - 1.000000x + -5.000000)
......(此处省略非常非常非常多的疑似解)
(y - 1.000000x + 0.000000)
((x - 0.000)^2 + (y - 0.000)^2 - 25.000)
((x - 3.536)^2 + (y - 3.536)^2 - 14.645)
((x - -3.536)^2 + (y - -3.536)^2 - 25.000)
((x - -4.830)^2 + (y - 1.294)^2 - 98.296)
(y - 1.000000x + -5.000000)
于是又遇到了同样的问题,输出太多,要找出正确答案不容易。
所以,到了这,就不得不推出分步判定功能了。
八,分步判定
1,分步判定思路
把判定过程分为若干次判定,这其实也是一种剪枝策略。
2,数据结构扩充
之前的终态局面判定是一个判定,现在要多个判定,每个判定还要输入是第几步的判定。
以6.6平移线段关卡为例,第5步的判定为:
5
1
1 1 -1 -5
0
第6步的判定为:
6
0
1
6 1
前面的保持不变,所以输入合集是:
1
1 1 -1 0
3
0 0
1 1
5 0
6
0 0 0 0 1 0
5
1
1 1 -1 -5
0
6
0
1
6 1
3,分步判定实现
代码倒是不难改。
(1)判定集
vector<bool> endEnt;
vector<Entity> goodEnt;
endEnt用来记录哪一步需要判定,哪一步不需要判定。
(2)修改isGoodEntity函数,只需要id化即可
bool isGoodEntity(Entity ent,int id)
{
for (auto it = goodEnt[id].vecNode.begin(); it != goodEnt[id].vecNode.end(); it++) {
if (!alreadyHaveNode(ent, *it)) {
return false;
}
}
for (auto it = goodEnt[id].vecPoint.begin(); it != goodEnt[id].vecPoint.end(); it++) {
if (!alreadyHavePoint(ent.vecPoint, *it)) {
return false;
}
}
return true;
}
(3)分步判定
void play(Entity ent, vector<TYPE> vt)
{
static map<set<vector<int>>, int>m;
if (m[normallizeNode(ent.vecNode)])return; // 剪枝
if (vt.size()<endEnt.size() && endEnt[vt.size()]) {
if (!isGoodEntity(ent,vt.size())) {
return;
}
}
if (vt.empty()) {
display(ent);
return;
}
for (int i = 0; i < ent.vecPoint.size(); i++) {
int startId = ((vt[0] == LINE) ? i : 0); // 剪枝
for (int j = startId; 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());
play(ent, vt);
ent = tmp;
vt.insert(vt.begin(), temp);
}
}
m[normallizeNode(ent.vecNode)] = 1;
}
(4)调度(输入控制)
int main()
{
Entity ent = initEnt();
vector<TYPE> vt = initVecType();
endEnt.resize(vt.size());
int id;
while (cin >> id) {
endEnt[vt.size() - id] = true;
goodEnt[vt.size() - id] = initEnt();
}
play(ent, vt);
cout << "end";
return 0;
}
4,继续破解6.6平移线段
运行新程序,发现还是有很多输出,但是大多是无效解,因为题目给的是线段,不是直线。
于是我有了新的思路:把线段当成两个点,不存在直线即可。
输入:
0
3
0 0
1 1
5 0
6
0 0 0 0 1 0
5
1
1 1 -1 -5
0
6
0
1
6 1
0
只花了几秒钟就输出了答案:
花了不到1分钟输出了end,表明这是唯一解。
对应答案:(假如游戏的点的坐标和我输入的一样,是0 0、1 1、5 0)
然而实际上,游戏给的点大概是0 0、1 1、1.5 0,所以实际答案是:
5,6.8平行四边形
根据最后一步是上面水平的线还是右边的斜线,可以分为2种情况:
(1)最后一步是上面水平的线
也就是说,第4步是左边的斜线,第6步是右边的斜线,第8步是上面水平的线。
根据直觉,这种情况的概率较小,暂时跳过。
(2)最后一步是右边的斜线
也就是说,到第6步的时候就应该画完了左边的斜线和上面水平的线。
输入:
0
3
0 0
1 1
14.5 18.5
8
1 0 0 1 0 1 0 1
6
0
1
14 18
8
0
1
15 19
0
输出了很多很多答案,应该任取一个即可。
貌似最后几步不准确,因为我只校验了点,没有校验线。(这就是为什么输出了很多很多答案)
但是这并不妨碍前面5步都是完全正确的,所以我们得到了正确答案:
6,6.9欧拉九点圆
输入:
4
1 1 -1 0
1 4.1 1 0
1 0.7 1 -3.4
1 1 1 -2
5
0 0
-1 4.1
2 2
1 1
-0.64516129 2.64516129
5
0 0 1 0 1
5
0
1
0.569117647 1.9808823529
0
程序运行了较长时间,最终因为内存爆了终止了,我做了计数,哈希表容量在102万的时候就崩溃了。
九,继续优化
1,把静态局部变量改为全局变量
map<set<vector<int>>, int>m;
void play(Entity ent, vector<TYPE> vt)
{
......
}
2,优化哈希方案——两级哈希
把局面标准化为set<vector<int>>,再用哈希表存起来的话,占用的内存太大。
这里我采用了两级哈希的方案:先把圆和直线映射到一个整数,再用set<int>表示一个局面,再把这个set映射到一个整数。
map<vector<int>, int>mv;
int normallizeNode(Node s)
{
if (s.type == LINE) {
normallizeLine(s);
}
vector<int>v(4);
v[0] = s.type, v[1] = int(s.a * 1000), v[2] = int(s.b * 1000), v[3] = int(s.c * 1000);
if (mv[v])return mv[v];
return mv[v] = mv.size();
}
set<int>normallizeNode(vector<Node>vec)
{
set<int>s;
for (unsigned i = 0; i < vec.size(); i++)s.insert(normallizeNode(vec[i]));
return s;
}
3,线段和直线
九点圆问题,初始局面就是一个任意的三角形,但是仍然涉及到一个问题:
由于三角形给的是线段而不是直线,那么是否存在一种解法,对于三角形的形状并不具有普适性呢?
比如说,有没有某个尺规作图问题,它有一种解法,这个解法对于(某个)锐角三角形的三个(或两个)方向都是OK的,但是对于直角三角形,必须以斜边为底边才是可行的?
由于这个不确定因素的存在,导致在给一个局面数据化的时候,把线段看成两点还是看成一个直线,还是需要根据实际问题去推测。
4,重新数据化
如果线段和直线的问题解决了,或者说我们假设初始局面给的本就是三条直线相交得到的三角形,那么,这样的局面肯定是比单纯的三角形更容易得到解法的。
类似的,如果初始局面是一个等腰三角形,那么肯定也是更容易得到解法的。(再次更新本博文,这一观点不对,等腰三角形反而有可能找不到普通三角形可以找到的解法)
输入:
4
1 1 -1 0
1 3 2 0
1 2 3 -10
1 1 1 -2
4
0 0
2 2
-4 6
1 1
5
0 0 1 0 1
5
0
1
-0.3 2.3
0
在哈希表容量是246万的时候,程序还是崩溃了,仍然是内存爆了,不过好歹也是有不少提升了。
而且,在内存爆掉之前,程序输出了唯一一组解:
y = 1.000000x + 0.000000
y = -1.500000x + -0.000000
y = -0.666667x + 3.333333
y = -1.000000x + 2.000000
(x - 0.000)^2 + (y - 0.000)^2 = 8.000
(x - 0.000)^2 + (y - 0.000)^2 = 52.000
y = 0.067797x + -1.864407
(x - -6.825)^2 + (y - -2.327)^2 = 52.000
y = -0.130435x + 2.260870
5,可视化——在线作图
我找到了这个网站:https://zuotu.91maths.com
可以在线作图,只需要把函数表达式输入进去即可。
在我的输出里面,直线已经是y=...了,直接复制就完事了。
圆的话,需要用2个半圆表示,2个半圆都可以用y=...的形式表示出来。
为了方便后续用这个网站作图,我直接稍微修改了我的输出形式。
void display(Node s)
{
if (s.type == CIRCLE) {
printf("y = %.3f +/- sqrt(%.3f - (x - %.3f)^2)\n", s.b, s.c * s.c, s.a);
}
else {
normallizeLine(s);
printf("y = %.6fx + %.6f\n", s.a, s.c);
}
}
然而,根据我的分析,这个解法应该是只对等腰三角形有效,对于普通三角形并没有用。
6,优化第一步
三角形加上垂直平分线有5个点,第一步是画圆,圆由2个不同的点确定,所以第一步有5*4=20种情况
这里搬出我的信条:数学的美,在于极致的纯粹,超越直觉而存在的优美的对称性,蕴含着最本质的规律。
所以我认为,A和C是完全对称的,即使E在AD上而不在CD上。
进一步可以把20种情况优化到13种情况:AB、AC、AD、AE、BA、BD、BE、DA、DB、DE、EA、EB、ED
其中AC是肯定不可能的,因为作垂直平分线的逻辑和圆AC是重复的,而这个游戏给出的E任务一定是最简E任务。
所以第一步有12种情况:AB、AD、AE、BA、BD、BE、DA、DB、DE、EA、EB、ED
效率不高,思路先保留。
7,优化最后一步
如果加入新的判定方案,可以再让搜索树再减少一层,效率很高,开干!
判断点是否在直线或者圆上:
bool pointOnLine(Point p, Node s)
{
return sameValue(s.a*p.x + s.b*p.y + s.c, 0);
}
bool pointOnCircle(Point p, Node s)
{
return sameValue((s.a-p.x)*(s.a-p.x) + (s.b-p.y)*(s.b-p.y) - s.c*s.c, 0);
}
bool pointOnNode(Point p, Node s)
{
return s.type==CIRCLE?pointOnCircle(p,s):pointOnLine(p,s);
}
判断点是否在当前局面:
bool pointInEntity(Point p, Entity ent)
{
for (auto it = ent.vecNode.begin(); it != ent.vecNode.end(); it++) {
if(pointOnNode(p,*it))return true;
}
return false;
}
我本以为,只需要判断最后一个产生的几何体(圆或直线)上有没有这个点即可。但是再仔细一想,似乎并不是这样,这一点先不纠结。
封装CIN,让数据配置可以写文本注释:
#define CIN(x) while (!(cin >> x)) { \
cin.clear(); \
cin.ignore(); \
}
#define CIN2(x, y) CIN(x)CIN(y)
#define CIN3(x, y, z) CIN(x)CIN(y)CIN(z)
保持分步判定,判定规则拓展为3种规则:判定是否存在几何体、判断是否存在点、判定是否存在经过定点的几何体。
这样判定的话,等腰三角形的垂直平分线从一开始就经过了九点圆心,没法筛选,所以我还是采用了之前建的坐标(之前的九点圆心坐标感觉是不是写错了):
初始局面
4 圆和直线的个数,下面每一行是一个圆或直线的方程
1 1 -1 0
1 4.1 1 0
1 0.7 1 -3.4
1 1 1 -2
5 点的个数,下面每一行是一个点的坐标
0 0
-1 4.1
2 2
1 1
-0.64516129 2.64516129
操作集
5 操作次数
0 0 1 0 1
分步判定
4 执行多少步之后需要判定
0 判定是否存在几何体
0 判定是否存在点
1 判定是否存在经过定点的几何体
0.569117647 1.9808823529
5
0
1
0.569117647 1.9808823529
0
0
之前用Entity表示判定集其实是取巧的,现在因为判定集有2个点集,就不能再用了。
typedef struct ExEntity {
vector<Node> vecNode;
vector<Point> vecPoint;
vector<Point> vecPoint2;
} ExEntity;
vector<ExEntity> goodEnt;
bool isGoodEntity(Entity ent, int id)
{
for (auto it = goodEnt[id].vecNode.begin(); it != goodEnt[id].vecNode.end(); it++) {
if (!alreadyHaveNode(ent, *it)) {
return false;
}
}
for (auto it = goodEnt[id].vecPoint.begin(); it != goodEnt[id].vecPoint.end(); it++) {
if (!alreadyHavePoint(ent.vecPoint, *it)) {
return false;
}
}
for (auto it = goodEnt[id].vecPoint2.begin(); it != goodEnt[id].vecPoint2.end(); it++) {
if (!pointInEntity(*it, ent)) {
return false;
}
}
return true;
}
在哈希表容量是近4000万的时候,程序运行结束了,至于为什么之前在246万就爆内存了,新程序在近4000万还没爆内存,我想,估计是影响内存的不仅仅是哈希表,还有其他数据吧。
输出:
y = 1.000000x + 0.000000
y = -4.100000x + -0.000000
y = -0.700000x + 3.400000
y = -1.000000x + 2.000000
y = 1.000 +/- sqrt(2.000 - (x - 1.000)^2)
y = 1.427 +/- sqrt(2.000 - (x + 0.348)^2)
y = 3.155059x + 0.185283
y = 2.282 +/- sqrt(2.000 - (x - 1.597)^2)
y = -2.276451x + 3.276451
y = 1.000000x + 0.000000
y = -4.100000x + -0.000000
y = -0.700000x + 3.400000
y = -1.000000x + 2.000000
y = 1.000 +/- sqrt(2.000 - (x - 1.000)^2)
y = 1.427 +/- sqrt(2.000 - (x + 0.348)^2)
y = 3.155059x + 0.185283
y = 2.282 +/- sqrt(2.000 - (x - 1.597)^2)
y = -0.465969x + 2.246073
2个都是正确的解。
8,优化剪枝方案
这里介绍一些概念和我的推测
(1)点被使用:每一步都是用两个点,要么就是两点作圆,要么就是两点作直线,点可以被重复使用。
(2)几何体被使用:每个几何体(除了最终目标)产生的点至少要在后续被使用。
(3)有效点:最终被使用的点叫有效点(新增一个几何体,一般产生的大多是无效点)
推测一:每个新产生的点最多被使用两次
(4)推理图:
(5)核:设有有向图D的一个节点子集S,若S是符合下列条件的极小集:S中节点两两互不相邻,且D的每一节点或在S中,或存在一条弧从S中一节点指向它,则称S为D的1基,也称为核。
(6)推理图的核:要么是一个点,要么是两个点。
(7)推理图的分层:
(8)进化节奏:实体分批产生的一个过程,每一批至少一个实体,产生这些实体
推测二:每个实体只在进化的某一层中产生有效新点,即推理图是一棵树。
9,完整代码
#include <iostream>
#include <algorithm>
#include <math.h>
#include <map>
#include <set>
#include <vector>
using namespace std;
#define CIN(x) while (!(cin >> x)) { \
cin.clear(); \
cin.ignore(); \
}
#define CIN2(x, y) CIN(x)CIN(y)
#define CIN3(x, y, z) CIN(x)CIN(y)CIN(z)
typedef struct Point {
double x, y;
} Point;
typedef enum TYPE {
CIRCLE = 0,
LINE = 1,
BUFF
}TYPE;
typedef struct Node {
TYPE type;
double a, b, c;
} Node;
typedef struct Entity {
vector<Node> vecNode;
vector<Point> vecPoint;
} Entity;
typedef struct ExEntity {
vector<Node> vecNode;
vector<Point> vecPoint;
vector<Point> vecPoint2;
} ExEntity;
//两点距离平方和
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);
}
}
#define dps 0.00001
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);
}
//两点重合
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;
}
void input(TYPE& t)
{
int x;
CIN(x);
t = TYPE(x);
}
void input(Node& s)
{
input(s.type);
CIN3(s.a,s.b,s.c);
}
void input(Point& p)
{
CIN2(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;
}
bool pointOnLine(Point p, Node s)
{
return sameValue(s.a * p.x + s.b * p.y + s.c, 0);
}
bool pointOnCircle(Point p, Node s)
{
return sameValue((s.a - p.x) * (s.a - p.x) + (s.b - p.y) * (s.b - p.y) - s.c * s.c, 0);
}
bool pointOnNode(Point p, Node s)
{
return s.type == CIRCLE ? pointOnCircle(p, s) : pointOnLine(p, s);
}
bool pointInEntity(Point p, Entity ent)
{
for (auto it = ent.vecNode.begin(); it != ent.vecNode.end(); it++) {
if(pointOnNode(p,*it))return true;
}
return false;
}
vector<bool> endEnt;
vector<ExEntity> goodEnt;
bool isGoodEntity(Entity ent, int id)
{
for (auto it = goodEnt[id].vecNode.begin(); it != goodEnt[id].vecNode.end(); it++) {
if (!alreadyHaveNode(ent, *it)) {
return false;
}
}
for (auto it = goodEnt[id].vecPoint.begin(); it != goodEnt[id].vecPoint.end(); it++) {
if (!alreadyHavePoint(ent.vecPoint, *it)) {
return false;
}
}
for (auto it = goodEnt[id].vecPoint2.begin(); it != goodEnt[id].vecPoint2.end(); it++) {
if (!pointInEntity(*it, ent)) {
return false;
}
}
return true;
}
void display(Node s)
{
if (s.type == CIRCLE) {
if(s.a>=0)printf("y = %.3f +/- sqrt(%.3f - (x - %.3f)^2)\n", s.b, s.c * s.c, s.a);
else printf("y = %.3f +/- sqrt(%.3f - (x + %.3f)^2)\n", s.b, s.c * s.c, -s.a);
}
else {
normallizeLine(s);
printf("y = %.6fx + %.6f\n", s.a, s.c);
}
}
void display(Entity ans)
{
for (unsigned i = 0; i < ans.vecNode.size(); i++) {
display(ans.vecNode[i]);
}
}
map<vector<int>, int>mv;
int normallizeNode(Node s)
{
if (s.type == LINE) {
normallizeLine(s);
}
vector<int>v(4);
v[0] = s.type, v[1] = int(s.a * 1000), v[2] = int(s.b * 1000), v[3] = int(s.c * 1000);
if (mv[v])return mv[v];
return mv[v] = mv.size();
}
set<int>normallizeNode(vector<Node>vec)
{
set<int>s;
for (unsigned i = 0; i < vec.size(); i++)s.insert(normallizeNode(vec[i]));
return s;
}
map<set<int>, int>m;
void play(Entity ent, vector<TYPE> vt)
{
if (m.size() % 1000 == 0) {
cout << m.size();
}
if (m[normallizeNode(ent.vecNode)])return; // 剪枝
if (vt.size() < endEnt.size() && endEnt[vt.size()]) {
if (!isGoodEntity(ent, vt.size())) {
return;
}
}
if (vt.empty()) {
display(ent);
cout << endl;
return;
}
for (int i = 0; i < ent.vecPoint.size(); i++) {
int startId = ((vt[0] == LINE) ? i : 0); // 剪枝
for (int j = startId; 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());
play(ent, vt);
ent = tmp;
vt.insert(vt.begin(), temp);
}
}
m[normallizeNode(ent.vecNode)] = 1;
}
int main()
{
Entity ent = initEnt();
vector<TYPE> vt = initVecType();
endEnt.resize(vt.size());
goodEnt.resize(vt.size());
int id;
while (true) {
CIN(id);
if (id <= 0)break;
endEnt[vt.size() - id] = true;
initVector<Node>(goodEnt[vt.size() - id].vecNode);
initVector<Point>(goodEnt[vt.size() - id].vecPoint);
initVector<Point>(goodEnt[vt.size() - id].vecPoint2);
}
play(ent, vt);
cout << "end";
return 0;
}