我一直以來都挺不想學這個玩意兒的......html
奈何最近常常碰到凸包啊半平面交之類的題,還有平面圖node
因此仍是作一下吧算法
平面向量及其座標表示數據結構
平面幾何圖形的基礎定義、公理、定理.net
向量是個很是方便的東西,能夠把不少平面幾何空間幾何裏面用笛卡爾座標暴力算很麻煩的東西變得很簡單,因此必定要熟練運用code
如下約定向量隨着字母表的順序a...z,依次對應座標下標1...26htm
座標表示:$\mathbf{a}=(x_1,y_1)$blog
向量的模:就是長度,$|\mathbf{a}|=\sqrt{x_1^2+y_1^2}$排序
$\mathbf{a}\ast\mathbf{b}=x_1x_2+y_1y_2$隊列
能夠用來算兩個直線的夾角:求出兩個直線$a,b$的向量$\mathbf{a},\mathbf{b}$
夾角$cos<a,b>=\frac{\mathbf{a}\ast\mathbf{b}}{|\mathbf{a}||\mathbf{a}|}$
實際上叉積這個概念是定義於空間向量裏面的:
對於空間向量$\mathbf{a}={x_1,y_1,z_1}$,
空間向量叉積:$\mathbf{a}\times\mathbf{b}=(y_1z_2-y_2z_1,x_2z_1-x_1z_2,x_1y_2-x_2y_1)$
注意這個式子獲得的是一個空間向量
那麼對於平面向量來講,咱們把$z$當作0,能夠獲得一個實數結果:
平面向量叉積:$\mathbf{a}\times\mathbf{b}=x_1y_2-x_2y_1$
這玩意兒有什麼用呢?
它等於從$\mathbf{a}$旋轉到$\mathbf{b}$的過程當中構成的有向平行四邊形**的面積(也就是能夠是負數)
這裏逆時針旋轉爲正,順時針爲負
那麼它能夠算什麼呢?咱們後面再說
直線有不少種存儲方法:
能夠存儲直線上兩個端點,常見於半平面交中
能夠對於直線方程$Ax+By+C=0$存儲$A,B,C$,這種比較不直觀,相對來講用的少一點
能夠對於直線方程$y=kx+b$存儲$k,b$,分別是斜率和$y$軸截距,這種比較直觀,常見於斜率相關的處理中
固然了,用哪種最好仍是依照各位本身的習慣,適合本身的纔是最好的
最直觀的的方式通常是求出$k$和$b$,用直線上兩點$A,B$列方程
$k=\frac{y_2-y_1}{x_2-x_1}$
$b=y_1-x_1\ast k$
這個你們都學過,用直線方程$Ax+By+C=0$直接求
$dis=|\frac{Ax_0+By_0+C}{\sqrt{A^2+B^2}}|$
也能夠用直線上兩點座標,構成三角形,求高便可
這裏咱們就會看到平面向量叉積的第一個使用:用從向量(這裏也就是有向直線)上任意兩點指向目標點的兩個向量的叉積的正負,能夠判斷點和直線的位置關係
更準確地:設有向直線上按照順序排列的兩個點$A,B$,目標點$C$,則$\overrightarrow{AC}\times\overrightarrow{BC}$爲正則$C$在$\overrightarrow{AB}$右側,不然在左側
能夠畫個圖體會一下
若是有直線方程的話直接解方程便可
這裏講一個用兩個直線上四個點求交點的方法:
設兩條直線上四點分別爲$A,B$和$C,D$,座標爲$(x_{1...4},y_{1...4})$
咱們把點變成位置向量
令$v1=(A-D)\times(B-D),v2=(A-C)\times(B-C)$,那麼交點的位置向量(位矢)爲$D+(v1/(v1-v2))*(C-D)$
這個東西也是畫個圖就出來了:求出的兩個平行四邊形面積之比正好等於交點到$C,D$的距離之比,只不過其中一個是正的一個是負的,因此那裏是$(v1-v2)$
代碼以下:
struct p{ long double x,y; p(long double xx=0.0,long double yy=0.0){x=xx;y=yy;} }; inline p operator *(const p &a,const long double &b){return p(a.x*b,a.y*b);} inline long double operator *(const p &a,const p &b){return a.x*b.y-a.y*b.x;}//'x-multiple' of planary vector inline p operator -(const p &a,const p &b){return p(a.x-b.x,a.y-b.y);} inline p operator +(const p &a,const p &b){return p(a.x+b.x,a.y+b.y);} struct seg{ p a,b;long double k; seg(p aa=p(),p bb=p(),long double kk=0.0){a=aa;b=bb;k=kk;} }; inline p cross(const seg &x,const seg &y){//calculate the intersection using planary vector long double v1=(x.a-y.b)*(x.b-y.b); long double v2=(x.a-y.a)*(x.b-y.a); long double c=v1/(v1-v2); p re=(y.b+((y.a-y.b)*c)); return re; }
基本上是很好理解的:一堆直線的右邊那一半平面的交就是半平面交【聽起來賊好理解是否是】
操做也比較簡單,放一個dalao的連接在這裏(懶得本身寫了23333)
1.以逆時針爲正方向,建邊(輸入方向不肯定時,可用叉乘求面積看正負得知輸入的順逆方向)
2.對線段根據極角排序
3.去除極角相同的狀況下,位置在右邊的邊
4.用雙端隊列儲存線段集合,遍歷全部線段
5.判斷該線段加入後對半平面交的影響(對雙端隊列的頭部和尾部進行判斷,由於線段加入是有序的)
(這裏判斷的方式是看最前面兩個或者最後面兩個的交點是否是在新加入線的右邊,能夠畫個圖理解一下)
6.若是某條線段對於新的半平面交沒有影響,則從隊列中剔除掉(雙端隊列頭尾刪除)
7.最後剩下的線段集合,即便最後要求的半平面交
代碼給一下:
這份代碼用雙端隊列求出半平面交的直線集合,以及集合內直線的交點,再用交點位矢求出面積
這個代碼有問題,看下面的吧
update 2019/4/6
前兩天寫了[HNOI2012]射箭這道題,從新認識了半平面交的寫法
具體參考上面博客,這裏放一個比較全的模板代碼
必定要看註釋!!!!!!!!!!!!
inline bool sign(long double x){//判斷符號 if(x>eps) return 1; if(x<-eps) return -1; return 0; } int n,m; struct node{ long double x,y; node(long double xx=0.0,long double yy=0.0){x=xx;y=yy;} //這裏是向量的基本運算,注意這是個通用的數據結構,點和二維向量都集成進去了 //注意到點和二維向量在作大部分運算的時候是同樣的,因此這麼作可行 //標*的是數乘和平面向量叉乘,標/的是平面向量點乘 //slope是求兩個點之間的斜率 inline friend node operator +(const node &a,const node &b){return node(a.x+b.x,a.y+b.y);} inline friend node operator -(const node &a,const node &b){return node(a.x-b.x,a.y-b.y);} inline friend node operator *(const node &a,const long double &b){return node(a.x*b,a.y*b);} inline friend long double operator *(const node &a,const node &b){return a.x*b.y-a.y*b.x;} inline friend long double operator /(const node &a,const node &b){return a.x*b.x+a.y*b.y;} inline friend long double slope(const node &a,const node &b){return atan2l(a.y-b.y,a.x-b.x);} }rt[300010]; struct seg{ //這裏線段(直線、半平面)的定義是從a開始到b結束的向量,是有方向的 node a,b;long double k;int id; seg(node aa=node(),node bb=node()){a=aa;b=bb;k=slope(aa,bb);id=0;} seg(node aa,node bb,long double kk){a=aa;b=bb;k=kk;id=0;} inline friend bool operator <(const seg &a,const seg &b){return a.k<b.k;}//按照斜率排序 inline friend node cross(const seg &a,const seg &b){//求兩個線段的交點,講解見上面 long double v1=(a.a-b.b)*(a.b-b.b); long double v2=(a.a-b.a)*(a.b-b.a); return b.b+(b.a-b.b)*(v1/(v1-v2)); } inline friend bool right(const node &a,const seg &b){ //判斷一個點是否是在一條線的右邊 //注意這裏是大於eps,由於半平面交可能出現最後的交是一個點的狀況 //有的題目須要排除上述狀況,就寫大於-eps(這樣包括了點在線段上) return ((a-b.b)*(a-b.a))>eps; } }lis[300010],a[300010],q[300010]; inline bool solve(int lim){//重要!!!!!這份代碼的半平面交是每一個有向直線(seg)的左側半平面的交 int i,head=1,tail=0,flag,tot=0; for(i=1;i<=m;i++) if(lis[i].id<=lim) a[++tot]=lis[i]; for(i=1;i<=tot;i++){ flag=0; while((head<=tail)&&(!sign(a[i].k-q[tail].k))){ if(right(q[tail].a,a[i])) tail--; else{flag=1;break;} } if(flag) continue; while(head<tail&&right(rt[tail],a[i])) tail--; while(head<tail&&right(rt[head+1],a[i])) head++; q[++tail]=a[i]; if(head<tail) rt[tail]=cross(q[tail],q[tail-1]); } while(head<tail&&right(rt[tail],q[head])) tail--; while(head<tail&&right(rt[head+1],q[tail])) head++; return (tail-head>1); } const long double pi=acos(-1.0); int main(){ //這裏是邊界條件,這份模板裏面要加入 //這裏也能夠看出來求的是線段左側的半平面的交 lis[++m]=seg(node(-1e12,1e12),node(-1e12,-1e12),pi/2.0); lis[++m]=seg(node(1e12,1e12),node(-1e12,1e12),0); lis[++m]=seg(node(1e12,-1e12),node(1e12,1e12),-pi/2.0); lis[++m]=seg(node(-1e12,-1e12),node(1e12,-1e12),pi); }
咕咕咕(主要是我沒見過純凸包的......)週末補
我錯了,凸包真是博大精深
凸包能夠簡單地理解爲一條繩子,繞在一個點集(木樁集合)的最外面,造成的凸多邊形
凸包有以下性質:
1.全部點都在凸包內部或者凸包上
2.凸包的端點必定是點集中的點
求凸包經常使用graham算法,時間複雜度$O(n\log n)$
流程以下:
找到$y$座標最小的一點做爲原點
對原點以外的全部點按照到原點的極角排序(這裏由於選取了最靠下的,因此極角範圍在$[0,\pi]$)
依次遍歷全部排序後的點,加入一個單調棧中:每次判斷(棧頂元素和棧頂第二元素之間的斜率)是否大於(當前點和棧頂第二元素之間的斜率)
注意一旦這個大於成立了,棧頂元素就會在當前元素和棧頂第二元素的連線的「下面」,也就是在凸包裏面了
由於咱們事先按照極角排序了,因此這一單調棧能夠不重複不遺漏地記錄凸包上全部點
注意這樣求出來的凸包上的點是逆時針排序的(根本緣由是由於極角排序就是逆時針繞圈)
示例代碼:
struct node{ double x,y; node(double xx=0.0,double yy=0.0){x=xx;y=yy;} inline bool operator <(const node &b){return ((fabs(y-b.y)<eps)?(x<b.x):(y<b.y));} inline friend bool operator ==(const node &a,const node &b){return ((fabs(a.x-b.x)<eps)&&(fabs(a.y-b.y)<eps));} inline friend bool operator !=(const node &a,const node &b){return !(a==b);} inline friend node operator +(const node &l,const node &r){return node(l.x+r.x,l.y+r.y);} inline friend node operator -(const node &l,const node &r){return node(l.x-r.x,l.y-r.y);} inline friend node operator *(node l,double r){return node(l.x*r,l.y*r);} inline friend double operator *(const node &l,const node &r){return l.x*r.y-l.y*r.x;} inline friend double operator /(const node &l,const node &r){return l.x*r.x+l.y*r.y;} inline friend double dis(const node &a){return sqrt(a.x*a.x+a.y*a.y);} }a[100010],q[100010],x[10]; inline bool cmp(node l,node r){ double tmp=(a[1]-l)*(a[1]-r); if(fabs(tmp)<eps) return dis(a[1]-l)<dis(a[1]-r); else return tmp>0; } void graham(){//get a counter-clockwise convex int i; for(i=2;i<=n;i++){ if(a[i]<a[1]) swap(a[1],a[i]); } sort(a+2,a+n+1,cmp); q[++top]=a[1]; q[++top]=a[2]; for(i=3;i<=n;i++){ while(top>1&&((q[top]-q[top-1])*(a[i]-q[top])<eps)) top--; q[++top]=a[i]; } q[0]=q[top]; }
旋轉卡殼,顧名思義,就是「旋轉着」+「卡qia在凸殼qiao上」
簡單來說,就是拿平行直線卡在凸包外面,而後讓凸包在裏面轉,這樣的感受
旋轉卡殼能夠解決凸包最大直徑、凸包最小直徑(凸包寬)、兩個凸包之間最大最小距離等問題
它也能夠解決凸包外接最小面積、最小周長凸$n$邊形問題
詳細的講解請戳這裏
旋轉卡殼+graham的一道例題:BZOJ1185
凸包有很是很是很是多的應用
斜率DP中用到的就是上下凸殼的單調棧求法
對於決策最大化問題,經常能夠經過考慮不一樣決策之間的關係,來導出凸包問題或者半平面交問題
也能夠在求最優決策的時候,把答案帶入式子中,並最大化包含答案那一項
和半平面交一塊兒使用的時候,一般是兩種:
1.斷定形問題,斷定是否點集在某個半平面內,此時在直線上方須要下凸殼,直線下方須要上凸殼
2.極值形問題,求出點集關於半平面的某個極值,此時在直線上方須要上凸殼,直線下方須要下凸殼(最大化),最小化的方法同類型一
線段樹能夠維護區間詢問的凸包,可是要考慮到凸包上傳更新的時間複雜度問題
cdq分治能夠解決原本須要動態凸包的問題,比較好寫好調
純動態凸包可使用set或者平衡樹維護,例題在這裏
所謂平面圖,就是一個圖,全部的邊能夠畫在平面上,互相之間不在定點以外的地方相交
通常會給出平面圖每一個點的座標、每一個邊是直線
能夠看到,一個平面圖會把一個平面劃分紅一個無限大的區域和若干面積有限的區域
咱們可使用最左轉線算法,把每個區域變成點、每個原圖邊變成新圖中相鄰兩個區域之間的邊,這樣就構建出了平面圖的對偶圖
先把全部邊改爲雙向的(不過通常題目都會給雙向的)
對於原圖每個點的出邊,按照其出邊的斜率進行排序。
以以下方式遍歷圖中全部的邊:
找一條沒有訪問過的邊(u,v),標記之
對於點v,找到v的出邊中極點序在(v,u)後面的那一個,並重覆上一行的過程,直到某一次找到的下一條邊是標記過的
每作一次這個過程,咱們就會找到一個 對偶圖定點(亦即原圖中的一個區域)
一次過程當中訪問到的全部邊就是這個區域的邊界(若是是無限大的那個區域,就是分割無限大和其餘人的全部邊)
由於咱們全部的邊都是雙向的,而每一條雙向邊分割了兩個區域,因此最終咱們對於原圖邊和區域的一一對應,能夠不重複不遺漏
由於「找到極點序中的上一個」這個操做就是找到這條邊的下一條中「最向左轉」的一條邊,因此算法稱做最左轉線
若想要圖片解釋,請戳這裏
貼個代碼,來自這道題:
for(i=1;i<=n;i++){ tot=e[i].size();ee.clear(); for(j=0;j<tot;j++){ ee.push_back(mp(atan2(y[e[i][j].first]-y[i],x[e[i][j].first]-x[i]),e[i][j].first)); } sort(ee.begin(),ee.end()); for(j=0;j<tot-1;j++){//最左轉線預處理:標記每個點的後繼 suf[i][ee[j].second]=ee[j+1].second; } if(tot) suf[i][ee[tot-1].second]=ee[0].second; } for(i=1;i<=n;i++){ for(j=0;j<e[i].size();j++){ pos=e[i][j].first;from=i; if(col[i][pos]) continue; cnt++; col[i][pos]=cnt; suml[cnt]+=light[pos]; sumd[cnt]+=dark[pos]; while(1){//求出一個區域 next=suf[pos][from]; if(col[pos][next]) break; from=pos;pos=next; col[from][pos]=cnt; suml[cnt]+=light[pos]; sumd[cnt]+=dark[pos]; } } }
點定位,顧名思義,就是定位平面上的點位於平面圖分割出來的那個區域裏面
能夠用排序+平衡樹實現$O(n\log n)$,也是週末補上