在處理計算幾何的問題中,有時候我們會將其看成圖論中的graph圖,結合我們在圖論中學習過的歐拉定理,我們可以通過圖形的節點數(v)和邊數(e)得到不是那么好求的面數f。
平面圖中的歐拉定理:
定理:設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次)就可以了。
基於求點數的窮舉計數方法,這里計算邊會變得非常簡單。

邊:

但是很遺憾這個問題到這里還沒有結束,由於這道題目是多組數據,而且在時間復雜度上卡的很近,因此我們需要繼續的化簡。
對於一個樣例,給出n,我們給出的公式是O(n)算法(循環計算),下面嘗試將其優化成O(1)。

參考代碼如下:
import java.math.BigInteger; import java.util.Scanner; public class main { public static void main(String[] args) { int t; Scanner in = new Scanner(System.in); t = in.nextInt(); for(int Case =1 ;Case <= t;Case++) { BigInteger n = in.nextBigInteger(); BigInteger a = n.pow(4); BigInteger b = n.pow(3).multiply(BigInteger.valueOf(6)); BigInteger c = n.pow(2).multiply(BigInteger.valueOf(23)); BigInteger d = n.multiply(BigInteger.valueOf(18)); BigInteger ans = BigInteger.valueOf(0); ans = ans.add(a).subtract(b).add(c).subtract(d); ans = ans.divide(BigInteger.valueOf(24)); ans = ans.add(BigInteger.valueOf(1)); System.out.println(ans); } } }
