【学习笔记】凸包
想学斜率优化,先把先决条件凸包给肝了。然后完全不懂,于是就咕了。顺便看了一下线代(其实并不会)
讲真这东西耗了我半个暑假
20210305更新:po学了向量回来了发现这写的什么玩意,已更新完毕,您可从更新部分跳至凸包部分。同时,原“前置知识:叉积”部分已弃用并放置于最后。
更新
以下内容大部分在人教A版数学必修2第一章中涉及。
向量是什么,你可以不必知道,只需知道其有两个要素:方向和大小,它可以用有向线段表示。下文中我们将写作\(\vec{x}\)或\(\vec{AB}\)
我们先解释一下向量的坐标:将这条有向线段的起点置于 \((0,0)\),则该线段的终点坐标即为该向量的坐标。
再解释一下向量的共线判定,用数乘表示则是:对于向量\(\vec{a}\),\(\vec{b}\),存在一个实数\(\lambda\),使得\(\lambda \vec{a} = \vec{b}\)。而引入坐标后,设\(\vec{a}\)坐标为\((a_x,a_y)\),\(\vec{b}\)坐标为\((b_x,b_y)\),则若两向量共线,则有
\(\huge a_x b_y-a_y b_x=0\)
这个形式便是叉积。类似的,通过画图,我们可以得出\(\vec{a}\)和\(\vec{b}\)的关系 对应 叉积与零的关系。(注:我们定义若一个向量\(\vec{a}\),能通过顺时针旋转小于\(\frac{\pi}{2}\)与\(\vec{b}\)所在的直线重合,则\(\vec{b}\)在\(\vec{a}\)的右边。反之同理。)
先给出结论,若\(\vec{a}\)\(\vec{b}\)叉积小于零,则\(\vec{b}\)在\(\vec{a}\)的左边。
需要注意的是,下文中,我们的原点不一定是\((0,0)\),而可能是其他点。而对应的,坐标也并非相对于\((0,0)\),而是我们定义的,会改变的“原点”。一般的,按照下图的情况,我们将会定义原点为\(P\)。
凸包初讲
我们定义,在平面上能包含所有给定点的最小凸多边形叫做凸包。 凸包的周长最小。
简单的说,平面上有一些点,你要用一个凸多边形把它们围起来。并且这个多边形最小。
代码实现
这里介绍一种实现凸包的算法。
首先我们可以将所有点按照其\(x\)坐标和\(y\)坐标做双关键字排序。然后我们分别维护上凸壳和下凸壳,维护凸壳我们用单调栈实现。显然第一个点和最末点一定在凸包上,所以我们可以从前往后找可以维护上凸壳,从后往前找可以维护下凸壳。
根据凸多边形的定义,若点\(y\)在点\(x\)的右边,则线段\(xy\)则一定不在凸包上。
根据这个原理,我们在单调栈维护点时,若找到这种不凸的点,则将其弹出。直到凸了才将点入栈。要注意的是,判断凸不凸至少需要3个点,即栈中至少要有2个点
以上面图为例,我们首先将\(a\)和\(P\)入栈,到\(b\)时,发现\(\vec{aP}\)与\(\vec{Pb}\)的叉积大于0,即\(b\)在\(a\)点右边,这显然不合法,于是将\(P\)弹出,此时栈中仅有\(a\)一个点,将\(b\)入栈。
对于共线的情况,可以灵活处理。
当我们正着做一遍,倒着做一遍,凸包就求出来了。
本题显然求的是凸包的周长
例题代码
#include<bits/stdc++.h>
using namespace std;
double ans;
int n,q[10100];
struct point
{
double x,y;
}p[10100];
bool cmp(point x,point y)
{
return x.x<y.x||(x.x==y.x&&x.y<y.y);
}
bool dow(point x,point y,point z)
{
double s=(x.x-z.x)*(y.y-z.y)-(x.y-z.y)*(y.x-z.x);
if (s<0) return true;
if (s>=0) return false;
}//叉积
double len(point x,point y)
{
return sqrt((x.x-y.x) * (x.x-y.x) + (x.y-y.y) *(x.y-y.y));
}//长度计算
int main()
{
cin>>n;
for (int i=1;i<=n;i++)
{
cin>>p[i].x>>p[i].y;
}
sort(p+1,p+1+n,cmp);
//=====上凸壳====
int h=0,t=1;
q[1]=1;
for (int i=2;i<=n;i++)
{
while (t>1&&dow(p[i],p[q[t-1]],p[q[t]]))
{
t--;
}
q[++t]=i;
}
//===下凸壳====
h=t-1;
for (int i=n-1;i>0;i--)
{
while (h+1<t&&dow(p[i],p[q[t-1]],p[q[t]]))
{
t--;
}
q[++t]=i;
}
for (int i=2 ;i<=t;i++)
{
ans+=len(p[q[i]],p[q[i-1]]);
}
ans+=len(p[q[t]],p[q[1]]);
printf("%.2f",ans);
return 0;
}
分割线
以下为弃用部分。
前置知识:叉积
这个属于线性代数,有兴趣的请上B站学习。推荐链接
叉积是一种在[向量空间]中向量的[二元运算]——百度百科
下文中将使用\(*\)代指乘,用\(\times\)代指叉积
根据某些线代知识,叉积在几何意义上代表的是以\(a\),\(b\)两个向量为两条边的平行四边形的面积。
而叉积的运算公式(简略版)为
\(\huge a.x*b.y-a.y*b.x\)
注意,这里的\(x\),\(y\),都是相对于某一点而言的。例如,下图的\(x\)与\(y\)相对于点\(P\)而言,而不是相对于点\(O\)而言。
求出叉积后,我们可以借助其来判断两个点的相对位置。
我们定义若一个向量\(x\),能通过顺时针旋转小于\(180\)度角与\(y\)所在的直线重合,则\(y\)在\(x\)的右边。反之同理。则叉积与方向的关系为
若\(a \times b >0\)则\(b\)在\(a\)的右边
若\(a \times b <0\)则\(b\)在\(a\)的左边
若\(a \times b =0\)则两点共线
关于叉积,本文仅介绍到这里,若想知道更多关于叉积的信息,请自行搜索。