借鉴:在苏州的城边 博客园 计算机几何讲义
平面图中的欧拉定理:
定理:设G为任意的连通的平面图,则v-e+f=2,v是G的顶点数,e是G的边数,f是G的面数。
证明:其实有点类似几何学中的欧拉公式的证明方法,这里采用归纳证明的方法。
对m进行归纳,当m = 0,显然成立。
假设边数e' = e-1时成立,即有v'-e'+f'=2.考察参数为v、e、f的G图,如果存在悬挂点v1,去掉v1,则有e' = e - 1,f=f',v' = v - 1,带入之前的假设,整理得:
v-e+f=2.
而如果G中没有悬挂点,我们去掉回路中的某个边,采取类似的思路,同样可以整理出欧拉公式。
证毕。
Q1:给出一个一笔画图形的n个节点的坐标,请你求解这个图形把平面分成了几个面。
分析:首先我们应该看到,我们很难直接的去做关于面的计算,因此这里应该考虑将该图视为graph图,然后利用欧拉定理进行求解。问题就变成了这个几何图形有多少点、边了(基于graph图的概念)。
点:这里设置一个O(n^2)的枚举算法,拿出一条线段,然后判断剩余的线段和它是否相交,相交后求出交点(求边数会用到),最后针对可能出现三线相交的情况,注意用stl库的标准函数unique去一下重点。这样我们就得到了节点数v。
再从代码实现的解读分析一下求节点数v的过程。
这里我们主要需要完成的就是线段是否相交的判断和交点的计算,能够看到这里显然能够将线段表示成参数直线的性质,求解交点的过程在以前的文章中已经介绍过,那么现在我们就要处理相交问题。
线段与线段相交的情况非常多,但是这里显然,交点在端点上的情况对我们计数并没有帮助,因此我们只需要考虑如下的情况(规范相交:两条线段的交点不在端点上):
这种相交特征有着怎样的等价的代数表达式呢?如果我们找到了它就能共通过点坐标进行代数计算然后判断线段是否相交了。
跨立实验:我们能够看到,这种相交的特点就是一条线段的两个端点一定是横跨在另一条端点的两侧的,这种相对位置的关系我们能够用叉积表示:
很明显我们也能够看到,这个判断式子是不唯一的。
边:上文已经反复强调,要求解graph图的边,也就是说,两个节点直接相连形成的边才能真正的称为一条边,这里依然是设计了一个O(n^2)的算法,我们枚举点集中的元素,然判断它是否在n条已知线段的内部。
这里我们就面临这样一个问题,如何判断一个点C位于线段AB的内部的?
那么有了这些铺垫,就可以进行编程实现了。
简单的参考代码如下:
复制代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <vector>
using namespace std;
const double eps = 1e-8;
int dcmp(double x){if(fabs(x)<eps) return 0; return (x<0)?-1:1;}
struct Point
{
double x,y;
Point(double _x=0,double _y=0):x(_x),y(_y){};
};
Point operator+(Point A,Point B) {return Point(A.x+B.x,A.y+B.y);}
Point operator-(Point A,Point B) {return Point(A.x-B.x,A.y-B.y);}
Point operator*(Point A,double p) {return Point(A.x*p,A.y*p);}
Point operator/(Point A,double p) {return Point(A.x/p,A.y/p);}
bool operator<(const Point&a,const Point&b){return a.x<b.x||(a.x==b.x&&a.y<b.y);}
bool operator==(const Point&a,const Point&b){return dcmp(a.x-b.x)==0&&dcmp(a.y-b.y)==0;}
double Dot(Point A,Point B) {return A.x*B.x+A.y*B.y;} //点积
double Cross(Point A,Point B) {return A.x*B.y-A.y*B.x;} //叉积
//P+tv和Q+tw的交点
Point GetLineIntersection(Point P,Point v,Point Q,Point w)
{
Point u=P-Q;
double t=Cross(w,u)/Cross(v,w);
return P+v*t;
}
//判断规范相交
bool SegmentProperIntersection(Point a1,Point a2,Point b1,Point b2)
{
double c1=Cross(a2-a1,b1-a1),c2=Cross(a2-a1,b2-a1);
double c3=Cross(b2-b1,a1-b1),c4=Cross(b2-b1,a2-b1);
return dcmp(c1)*dcmp(c2)<0&&dcmp(c3)*dcmp(c4)<0;
}
//判断点p是否在线段a1a2上
bool OnSegment(Point p,Point a1,Point a2)
{
return dcmp(Cross(a1-p,a2-p))==0&&dcmp(Dot(a1-p,a2-p))<0;
}
int n;
Point pt[1000];
vector<Point> vp;
int main()
{
int cas=1;
while(scanf("%d",&n)!=EOF&&n)
{
vp.clear();
for(int i=0;i<n;i++)
{
scanf("%lf%lf",&pt[i].x,&pt[i].y);
vp.push_back(pt[i]);
}
n--;
for(int i=0;i<n;i++)
{
for(int j=i+1;j<n;j++)
{
if(SegmentProperIntersection(pt[i],pt[i+1],pt[j],pt[j+1]))
{
vp.push_back(GetLineIntersection(pt[i],pt[i+1]-pt[i],pt[j],pt[j+1]-pt[j]));
}
}
}
sort(vp.begin(),vp.end());
int c=unique(vp.begin(),vp.end())-vp.begin();
int e=n;
int cc=0;
for(vector<Point>::iterator it=vp.begin();cc<c&&it!=vp.end();cc++,it++)
{
for(int i=0;i<n;i++)
{
if(OnSegment(*it,pt[i],pt[i+1])) e++;
}
}
printf("Case %d: There are %d pieces.\n",cas++,e+2-c);
}
return 0;
}
Q2:
有多少土地:
在一个椭圆的土地当中,在边界上有n个点,现在连接任意的两个点,那么请问这块椭圆土地能够最多被分割成多少块?
分析:一道欧拉定理即视感非常强的题目,我们依然要借助节点v、边的个数e来确定面数f,在这里要去掉最外面那个面,即在这道具体的题目当中,f = e-v+1.
那么下面的问题还是求v、e。其实这里能够看到已经和计算几何不沾什么边了,本质上来讲是一个组合计数问题。
点:
我们固定一条线段的起始点,然后遍历剩余点作为终点,状态参量i表示该线段左边的点的个数,这是一个子问题,我们遍历起始点,然后进行去重(充分理解这个分割状态的过程,能够看到每个点会被计算4次)就可以了。
边:
基于求点数的穷举计数方法,这里计算边会变得非常简单。
代码:
#include<stdio.h>
int main()
{
int V,E,F,a,b,n;
a=b=0;
while(scanf("%d",&n)!=EOF)
{
for(int i=0;i<=n-2;i++)
{
a+=i*(n-2-i);
b+=i*(n-2-i)+1;
}
V=n+n*a/4;
E=n+n*b/2;
//欧拉公式V-E+F=2,得到F=E-V+2,还要减去椭圆外那个无穷大的面
F=E-V+1;
printf("%d\n",F);
}
return 0;
}