Computational Geometry
建國中學 吳沛恩
「計算幾何是以數學方式精確與高效的計算出幾何結構與其性質的嚴謹應用。」
線性代數
Source: Wiki
Source: Wiki
template <typename T>
class pt{
T x,y;
//下方運算子重載與上方相同
}
pt<int> p[N]; //點座標宣告為整數
pt<double> pp[N]; //點的不同資料型別宣告
依照題目需求,座標可以用int或double存
Source: https://pse.is/3suhzc
Source: Wiki
等一下證明會用到
Vector
EX:
=
+
(2,1)
(2,0)
(0,1)
EX:
終點減始點
Dot Product
定義:若 \(\vec a\) 與 \(\vec b\) 為二度或三度空間的向量,且 \(\theta\) 為\(\vec a\) 與 \(\vec b\) 的夾角(滿足\(0\leq\theta\leq\pi\)),則點積(或歐基里德內積 ) \(\vec a\cdot\vec b\) 定義為
Dot Product
Source: Wiki
Dot Product
令向量\(\overrightarrow{a} = (x_1,y_1),\overrightarrow{b} = (x_2,y_2)\),證明:
餘弦定理:\(\cos\theta = \frac{|\vec a|^2+|\vec b|^2-|\vec a-\vec b|^2}{2|\vec a||\vec b|}\),帶入公式:
Cross product
定義:若\(a = (a_1,a_2,a_3)\)與\(b = (b_1,b_2,b_3)\)為三度空間裡的向量,則外積(叉積)\(a\times b\) 為一向量定義為:
向量的外積只有在三度空間才有定義!
外積結果的向量由右手定則決定
Cross product
Source: Wiki
Source: Wiki
Cross product
令向量\(\vec{a} = (x_1,y_1),\vec{b} = (x_2,y_2)\),證明:
得到:\(\vec a\times\vec b = x_1y_2-x_2y_1\)
struct pt{
int x,y;
bool operator < (pt b){
if(x == b.x)return y < b.y;
return x < b.x;
}
bool operator > (pt b){
if(x == b.x)return y > b.y;
return x > b.x;
}
bool operator == (pt b){
if(abs(x-b.x)<=eps && abs(y-b.y)<=eps)return true;
return false;
}
pt operator+(pt b) {return {x + b.x, y + b.y};} //向量相加
pt operator-(pt b) {return {x - b.x, y - b.y};} //向量相減
pt operator*(int k) {return {x * k, y * k};} //向量乘法(係數積)
pt operator/(int k) {return {x / k, y / k};} //向量除法
int operator^(pt b) {return x * b.y - y * b.x;} //向量外積cross
int operator*(pt b) {return x * b.x + y * b.y;} //向量內積dot
};
好用的工具
+
-
兩向量夾出的三角形有向面積為:
A
B
C
代數證明:利用正弦三角形面積公式推得
幾何證明:簡單明瞭
Source: https://reurl.cc/NZ5ZNq
假設多邊形的頂點依序為(要特別注意結束的點必須是起始點):
則多邊形的面積為(aka測量師公式):
給定頂點座標均是格子點的簡單多邊形,皮克定理說明了其面積\(A\)和內部格點數目\(i\)、邊上格點數目\(b\)的關係:
證明在這裡
Source: Wiki
線段香蕉
//方向函數dir,回傳OA向量與OB向量的方向
int dir(pt a, pt b, pt o) { //方向函數
int cross = (a - o) ^ (b - o);
if(fabs(cross) <= eps) return 0;
else if(cross > 0) return 1;
else return -1;
}
方向函式能做什麼?
判斷點是否在線段兩端點的異側!
bool onseg(pt a, pt b, pt o){ //o是否在ab線段上
int cross = (a - o) ^ (b - o); //是否平行
int dot = (a - o) * (b - o); //是否在線段中
return (cross == 0)&&(dot <= 0);
}
bool Intersection(pt a, pt b, pt c, pt d){ //線段ab是否與cd相交
if(onseg(a,b,c)||onseg(a,b,d))return true; //點c、d是否洽在線段ab上
if(onseg(c,d,a)||onseg(c,d,b))return true; //點a、b是否洽在線段cd上
if(dir(a,b,c)*dir(a,b,d)==-1 && dir(c,d,a)*dir(c,d,b)==-1)
return true; //對於線段兩端點看另外兩端點必須方向相反
return false;
}
知道兩線段是否相交之後,進一步求出交點座標
幾個算式列一列得到:
帶回 \(\vec{P} = \vec{A} + i\overrightarrow{AB}\) 即可
pt Intersection_point(pt a, pt b, pt c, pt d){
assert(Intersection(a, b, c, d)); //是否相交
return a + ((a - c)^(d - c) * (b - a))/((d - c)^(b - a));
}
給定平面上\(n\)個點構成的簡單多邊形,還有\(m\)個詢問的點。對每一個點輸出它是在多邊形內部、外部或邊界上。
$$3\leq n\leq 1000$$
$$1\leq m\leq 1000$$
$$10^9\leq x_i,y_i\leq 10^9$$
Source: geeksforgeeks
#include <bits/stdc++.h>
#define int long long
#define double long double
#define pii pair<int, int>
#define N 1005
#define INF 1000000005
#define x first
#define y second
#define IOS ios::sync_with_stdio(0),cin.tie(0)
using namespace std;
int n,m;
struct point{
int x,y;
bool operator == (point b){
if(x == b.x && y == b.y)return true;
return false;
}
point operator - (point b){return {x - b.x , y - b.y};}
point operator + (point b){return {x + b.x , y + b.y};}
int operator ^ (point b){return (x * b.y - y * b.x);}
int operator * (point b){return (x * b.x + y * b.y);}
}pt[N];
bool onseg(point a,point b,point o){
int cross = (a - o) ^ (b - o);
int dot = (a - o) * (b - o);
return (cross == 0) && (dot <= 0);
}
int dir(point a,point b,point o){
int cross = (a - o) ^ (b - o);
if(cross == 0)return 0;
else if(cross > 0)return 1;
else return -1;
}
bool inter(point a,point b,point c,point d){
if(onseg(a,b,c) || onseg(a,b,d))return true;
if(onseg(c,d,a) || onseg(c,d,b))return true;
if(dir(a,b,c) * dir(a,b,d) < 0 && dir(c,d,a) * dir(c,d,b) < 0)
return true;
return false;
}
int solve(point cur){
int sum = 0;
for(int i=0;i<n;i++){
if(onseg(pt[i],pt[i+1],cur) == 1)return -1;
if(inter(pt[i],pt[i+1],cur,point{INF,cur.y}))sum++;
point temp = pt[i].y > pt[i+1].y ? pt[i] : pt[i+1];
if(temp.y == cur.y && temp.x > cur.x)sum--;
}
return sum;
}
signed main(){
IOS;
cin>>n>>m;
for(int i=0;i<n;i++)cin>>pt[i].x>>pt[i].y;
pt[n] = pt[0];
for(int i=0;i<m;i++){
point temp;cin>>temp.x>>temp.y;
int ans = solve(temp);
if(ans == -1)cout<<"BOUNDARY"<<"\n";
else if(ans & 1)cout<<"INSIDE"<<"\n";
else cout<<"OUTSIDE"<<"\n";
}
}
好用的工具
給定平面上\(n\)個點的座標\((x_i, y_i)\)
求出這 \(n\) 個點中最近的兩個點的距離是多少?
\(n \leq 50000, x_i, y_i \leq 2^{31} - 1\)
註:這邊的距離指的是歐幾里得距離
這裡有更詳盡的比較
今天要講的是掃描線算法
複雜度分析:Worst Case : \(O(n^2)\)
當所有點都在同一鉛直線上會退化成平方
如何處理點集中在鉛直線上的情況?
#include <bits/stdc++.h>
#define int long long
#define ld long double
#define N 200005
#define x first
#define y second
#define pii pair<int,int>
#define IOS ios::sync_with_stdio(0),cin.tie(0)
using namespace std;
int n;
vector<pii> p;
set<pii> s;
ld dis(pii a, pii b){
ld x = a.x-b.x, y = a.y-b.y;
return sqrt(x*x + y*y);
}
signed main(){
IOS;
cout<<fixed<<setprecision(6);
while(cin>>n){
p.assign(n,{0,0});
for(int i = 0;i < n;i++)cin>>p[i].x>>p[i].y;
sort(p.begin(),p.end());
s.clear();
s.insert({p[0].y,p[0].x});
int l = 0;ld ans = 5e18;
for(int i = 1;i < n;i++){
int d = ceil(ans);
while(l < i && p[l].x < p[i].x - d){
s.erase({p[l].y,p[l].x});
l++;
}
auto it_l = s.lower_bound({p[i].y - d,0});
auto it_r = s.upper_bound({p[i].y + d,0});
for(auto it = it_l;it != it_r;it++){
ans = min(ans,dis({it->y,it->x},p[i]));
}
s.insert({p[i].y,p[i].x});
}
cout<<ans<<endl;
}
}
(這一題在之前資料結構那邊講過)
\(n \leq 10^5\), 矩形範圍 \(\leq 10^6\)
Source: OI-wiki
逆時鐘一個一個排
利用每一個對特定點(通常是原點)的極座標的角度進行排序
Source: Wiki
bool cmp(pt a, pt b){
return atan2(a.y,a.x) < atan2(b.y,b.x);
}
sort(p.begin(),p.end(),cmp);
bool cmp(pt a, pt b){
bool f1 = a < pt{0,0};
bool f2 = b < pt{0,0};
if(f1 != f2)return f1 < f2;
return (a ^ b) > 0;
//逆時針將點進行極角排序,從270度開始逆時針
}
sort(p.begin(),p.end(),cmp);
//以id為原點進行極角排序
給你 \(N\) 個座標平面上的點\((x_i,y_i)\),請問總共可形成多少個直角三角形呢?
\(3\leq N \leq 1500, -10^9\leq x_i,y_i\leq 10^9\)
註:保證同一座標不會有兩個點
#include <bits/stdc++.h>
#define Orz ios::sync_with_stdio(0),cin.tie(0)
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pii pair<int,int>
#define pdd pair<double,double>
#define int long long
#define ll long long
#define ld long double
#define N 100001
#define all(x) x.begin(),x.end()
#define eps 1e-9
#define x first
#define y second
using namespace std;
struct pt{
int x,y;
bool operator < (pt b){
if(x == b.x)return y < b.y;
return x < b.x;
}
bool operator > (pt b){
if(x == b.x)return y > b.y;
return x > b.x;
}
bool operator == (pt b){
if(x-b.x == 0 && y-b.y == 0)return true;
return false;
}
pt operator+(pt b) {return {x + b.x, y + b.y};} //向量相加
pt operator-(pt b) {return {x - b.x, y - b.y};} //向量相減
int operator^(pt b) {return x * b.y - y * b.x;} //向量外積cross
int operator*(pt b) {return x * b.x + y * b.y;} //向量內積dot
};
vector<pt> p,temp,pp;
vector<int> cnt;
int n,ans = 0;
bool cmp(pt a, pt b){
bool f1 = a < pt{0,0};
bool f2 = b < pt{0,0};
if(f1 != f2)return f1 < f2;
return (a ^ b) > 0;
//逆時針將點進行極角排序,從270度開始逆時針
}
//O(n)枚舉每個點當直角情況
int solve(pt id){
pp.clear();cnt.clear();temp.clear();
for(pt i : p){
pt cur = i - id;
if(cur == pt{0,0})continue;
temp.push_back(cur);
}
sort(all(temp),cmp); //以id為原點進行極角排序
pp.push_back(temp[0]); //pp每一角度只存至多一個點
cnt.push_back(1); //考慮每個點共線情況
int len = temp.size();
rep(i,1,len-1){
int cross = temp[i]^temp[i-1],dot = temp[i]*temp[i-1];
if(cross == 0 && dot >= 0)cnt[cnt.size()-1] += 1; //共線數量+=1
else {pp.push_back(temp[i]);cnt.push_back(1);} //非共線設定數量為1
}
len = pp.size(); //考慮橫跨一周的情況
rep(i,0,len-1){ //雙指針i,p1可能會超過一圈
pp.push_back(pp[i]); //將點再繞一圈
cnt.push_back(cnt[i]);
}
int ans = 0,p1 = 0;
rep(i, 0, len-1){
while(p1 < i+len && (pp[i]^pp[p1]) >= 0 && (pp[i]*pp[p1]) > 0)p1 += 1;
//夾銳角的情況要p1+=1
if((pp[i]^pp[p1]) > 0 && (pp[i]*pp[p1]) == 0)ans += cnt[i]*cnt[p1];
//正向的直角三角形,若共線則兩者數量相乘
}
return ans;
}
signed main(){
Orz;
while(cin>>n){
if(n == 0)break;
p.assign(n,{0,0});
rep(i,0,n-1)cin>>p[i].x>>p[i].y;
int ans = 0;
rep(i,0,n-1){
ans += solve(p[i]);
}
cout<<ans<<endl;
}
}
給你二維平面上\(n\)個點(n≤2400),每一個點座標皆不相同,計算總共可以畫出多少個三角形?
$$-10^9\leq x_i,y_i\leq 10^9$$
#include <bits/stdc++.h>
#define Orz ios::sync_with_stdio(0),cin.tie(0)
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pii pair<int,int>
#define pdd pair<double,double>
#define int long long
#define ll long long
#define ld long double
#define N 100001
#define all(x) x.begin(),x.end()
#define eps 1e-9
#define x first
#define y second
using namespace std;
struct pt{
int x,y;
bool operator < (pt b){
if(x == b.x)return y < b.y;
return x < b.x;
}
bool operator > (pt b){
if(x == b.x)return y > b.y;
return x > b.x;
}
bool operator == (pt b){
if(x-b.x == 0 && y-b.y == 0)return true;
return false;
}
pt operator+(pt b) {return {x + b.x, y + b.y};} //向量相加
pt operator-(pt b) {return {x - b.x, y - b.y};} //向量相減
int operator^(pt b) {return x * b.y - y * b.x;} //向量外積cross
int operator*(pt b) {return x * b.x + y * b.y;} //向量內積dot
};
vector<pt> p;
vector<int> cnt;
int n,ans = 0;
bool cmp(pt a, pt b){
bool f1 = a < pt{0,0};
bool f2 = b < pt{0,0};
if(f1 != f2)return f1 < f2;
return (a ^ b) > 0;
//逆時針將點進行極角排序,從270度開始逆時針
}
//用cnt[i]統計區間長度為i的線段數量
void solve(pt id){
vector<pt> pp;
for(auto i : p){ //以id為原點
pt cur = i-id;
if(cur == pt{0,0})continue;
if(cur.y < 0){cur.x = -cur.x;cur.y = -cur.y;}
if(cur.x < 0 && cur.y==0){cur.x = -cur.x;}
pp.push_back(cur);
}
sort(all(pp),cmp); //將id當作原點進行排序
int p1 = 0,p2 = 0,len = pp.size(); //雙指針找共線區間
while(p1 < n-1){ //最大化區間
while(p2+1 < len && (pp[p1]^pp[p2+1]) == 0)p2++;
cnt[p2-p1+2]+=1;
p1 = p2+1;
}
}
signed main(){
Orz;
cin>>n;
p.assign(n,{0,0});
cnt.resize(n+1,0);
rep(i,0,n-1)cin>>p[i].x>>p[i].y;
rep(i,0,n-1)solve(p[i]);
int ans = (n*(n-1)*(n-2))/6;
rep(i,3,n)ans-=(cnt[i]*(i-1)*(i-2))/6;
cout<<ans<<endl;
}
給定二維平面上\(n\)個點,問一條直線最多能穿越幾個點?
$$1\leq n\leq 300$$
$$-10^4\leq x_i,y_i\leq 10^4$$
Convex Hull
Source: Wiki
凸包演算法有非常多種,今天介紹其中兩種:
(a.k.a. Jarvis' March Algorithm)
Source: Wiki
(a.k.a. Jarvis' March Algorithm)
#include <bits/stdc++.h>
#define Orz ios::sync_with_stdio(0),cin.tie(0)
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pii pair<int,int>
#define pdd pair<double,double>
#define int long long
#define ll long long
#define ld long double
#define N 100001
#define eps 1e-9
#define x first
#define y second
using namespace std;
struct pt{
int x,y;
bool operator < (pt b){
if(x == b.x)return y < b.y;
return x < b.x;
}
bool operator > (pt b){
if(x == b.x)return y > b.y;
return x > b.x;
}
bool operator == (pt b){
if(x-b.x<=eps && y-b.y<=eps)return true;
return false;
}
pt operator+(pt b) {return {x + b.x, y + b.y};} //向量相加
pt operator-(pt b) {return {x - b.x, y - b.y};} //向量相減
int operator^(pt b) {return x * b.y - y * b.x;} //向量外積cross
int operator*(pt b) {return x * b.x + y * b.y;} //向量內積dot
};
bool cmp(pt a, pt b){
if(a.x == b.x)return a.y < b.y;
return a.x < b.x;
}
vector<pt> p;
bool check(pt a,pt b,pt o){
int cross = (a - o)^(b - o);
return cross > 0;
}
int n,t;
vector<pt> convex_hull(){
vector<pt> hull;
if(n < 3)return hull;
int l = 0;
for(int i = 0;i < n;i++){
if(p[i].x < p[l].x)l = i;
}
int p1 = l,p2;
do{
p2 = (p1 + 1) % n;
for(int i = 0;i < n;i++){
if(check(p[i],p[p2],p[p1]))p2 = i;
}
hull.push_back(p[p1]);
p1 = p2;
}while(p1 != l);
return hull;
}
signed main(){
Orz;
cin>>n;
p.assign(n,{0,0});
rep(i,0,n-1)cin>>p[i].x>>p[i].y;
vector<pt> hull = convex_hull();
cout<<hull.size()<<endl;
}
(a.k.a. Andrew's algorithm)
Source: Wiki
先把下半部凸包圍起來,上半部也可以照同樣方式圍出來。
4. 對點逆序之後把上凸包圍出來
把小於零的點從vector中pop出來!
將所有點reverse圍上凸包!
好欸,做完了!
bool check(pt a, pt b, pt o){
pt aa = a - o;
pt bb = b - o;
return (aa ^ bb) >= 0;
}
vector<pt> convex_hull(){
vector<pt> hull;
sort(p.begin(),p.end(),cmp); //首先對x進行排序
for(auto i : p){ //依序走訪,如果遇到外積<0則不在凸包上
while(hull.size() > 1
&& check(i,hull[hull.size()-1],hull[hull.size()-2])){
hull.pop_back();
}
hull.push_back(i); //在凸包hull的每一點都符合外積小於0
}
int down_hull = hull.size();
hull.pop_back(); //x最大的點會在凸包上,不用做兩次先pop一次
reverse(p.begin(),p.end()); //將所有點逆序之後做一次上面的凸包
for(auto i: p){
while(hull.size() > down_hull
&& check(i,hull[hull.size()-1],hull[hull.size()-2])){
hull.pop_back();
}
hull.push_back(i);
}
return hull; //起點會經過兩次,剛好來算有向面積
}
旋轉的卡尺
我們的多邊形
Source: Wiki
給你一個平面上\(n\) 個點,求最遠點對。
$$2\leq n \leq 3000$$
$$0\leq x_i,y_y\leq 10^4$$
顯然太複雜了!
//省略找凸包的過程
bool check2(pt a,pt b,pt c,pt d){
int aa = abs((a - c)^(b - c));
int bb = abs((a - d)^(b - d));
return aa < bb;
}
void solve(){
int ans = 0,d = h,sz = hull.size();
rep(i,0,sz-1){
while(check2(hull[i],hull[(i+1)%sz],hull[d],hull[(d+1)%sz]))
d = (d+1)%sz;
ans = max(ans,(hull[i]-hull[d]).dis());
ans = max(ans,(hull[(i+1)%sz]-hull[d]).dis());
}
}
酷東西
Source: Wiki
利用模擬退火解決TSP
給你平面上\(n\)個點,請你找出一個點,使得這個點連到這 \(n\) 個點的距離總和最短。
\(n \leq 50, x_i,y_i \leq 100\)
#define eps 1e-8
#define ld long double
#define pdd pair<ld,ld>
#define x first
#define y second
class Solution {
public:
pdd p[105];int n;
ld dis_all(pdd mid){
ld sum;
for(int i = 0;i < n;i++){
ld x = p[i].x - mid.x,y = p[i].y - mid.y;
sum += sqrt(x * x + y * y);
}
return sum;
}
double getMinDistSum(vector<vector<int>>& pos) {
srand(time(NULL));
n = pos.size();
for(int i = 0;i < n;i++)p[i] = {pos[i][0],pos[i][1]};
pdd cur = p[0];ld mid_dis = dis_all(p[0]);
ld test_size = 150.0;
while(test_size > eps){
pdd newp = cur;
int temp = rand();
newp.x += cos(temp) * test_size;
newp.y += sin(temp) * test_size;
ld new_dis = dis_all(newp);
if(new_dis < mid_dis){
mid_dis = new_dis;
cur = newp;
}
else test_size *= 0.99;
}
return mid_dis;
}
};
#define eps 1e-6
#define ld long double
#define pdd pair<ld,ld>
#define x first
#define y second
class Solution {
public:
pdd p[105];int n;
ld dis_all(pdd mid){
ld sum;
for(int i = 0;i < n;i++){
ld x = p[i].x - mid.x,y = p[i].y - mid.y;
sum += sqrt(x * x + y * y);
}
return sum;
}
double getMinDistSum(vector<vector<int>>& pos) {
n = pos.size();
for(int i = 0;i < n;i++)p[i] = {pos[i][0],pos[i][1]};
pdd cur = p[0];ld mid_dis = dis_all(p[0]);
ld test_size = 100;
ld dx[4] = {1.0,0.0,-1.0,0.0},dy[4] = {0.0,1.0,0.0,-1.0};
bool flag = 0;
while(test_size > eps){
flag = 0;
for(int i = 0;i < 4;i++){
pdd newp = cur;
newp.x += dx[i] * test_size;
newp.y += dy[i] * test_size;
ld new_dis = dis_all(newp);
if(new_dis < mid_dis){
mid_dis = new_dis;
cur = newp;
flag = 1;
break;
}
}
if(flag == 0)test_size /= 2.0;
}
return mid_dis;
}
};