首先发出题目链接:
链接:https://ac.nowcoder.com/acm/contest/923/D
来源:牛客网
涉及:单调栈
题目如下:
此题花了我两天时间,一直卡在case75%,我还以为啥没有考虑到,结果是神TM的排序comp函数写错了,哭了。
首先说明一下: a ⃗ × b ⃗ \vec a×\vec b a×b仍然是一个向量,这个向量的方向垂直 x O y xOy xOy平面.
设
a
⃗
=
(
x
1
,
y
1
)
,
b
⃗
=
(
x
2
,
y
2
)
\vec a=(x_{1},y_{1}),\vec b=(x_{2},y_{2})
a=(x1,y1),b=(x2,y2),则
a
⃗
×
b
⃗
\vec a×\vec b
a×b向量模长为
∣
a
⃗
×
b
⃗
∣
=
∣
x
1
∗
y
2
−
x
2
∗
y
1
∣
|\vec a×\vec b|=|x_{1}*y_{2}-x_{2}*y_{1}|
∣a×b∣=∣x1∗y2−x2∗y1∣
a
⃗
×
b
⃗
\vec a×\vec b
a×b向量方向判断:
1.若
(
x
1
∗
y
2
−
x
2
∗
y
1
)
>
0
(x_{1}*y_{2}-x_{2}*y_{1})>0
(x1∗y2−x2∗y1)>0则此向量方向朝上;
2.若
(
x
1
∗
y
2
−
x
2
∗
y
1
)
<
0
(x_{1}*y_{2}-x_{2}*y_{1})<0
(x1∗y2−x2∗y1)<0则此向量方向朝下;
3.若
(
x
1
∗
y
2
−
x
2
∗
y
1
)
=
0
(x_{1}*y_{2}-x_{2}*y_{1})=0
(x1∗y2−x2∗y1)=0则此向量为零向量;
按照题目意思,只有当 ( x 1 ∗ y 2 − x 2 ∗ y 1 ) > 0 (x_{1}*y_{2}-x_{2}*y_{1})>0 (x1∗y2−x2∗y1)>0,才存在一条从 ( x 1 , y 1 ) (x_{1},y_{1}) (x1,y1)到 ( x 2 , y 2 ) (x_{2},y_{2}) (x2,y2)的边,边权为 ( x 1 ∗ y 2 − x 2 ∗ y 1 ) (x_{1}*y_{2}-x_{2}*y_{1}) (x1∗y2−x2∗y1)
假设: a ⃗ \vec a a与x轴正方向夹角为 θ 1 \theta_{1} θ1, b ⃗ \vec b b与x轴正方向夹角为 θ 2 \theta_{2} θ2
那么 ( x 1 ∗ y 2 − x 2 ∗ y 1 ) > 0 等 价 于 θ 1 < θ 2 (x_{1}*y_{2}-x_{2}*y_{1})>0等价于\theta_{1}<\theta_{2} (x1∗y2−x2∗y1)>0等价于θ1<θ2
按照上面红色字条件,可以一开始对所有向量按照与x正方向夹角进行递增的排序。
inline bool comp(point a,point b){
return 1.0*a.x/sqrt(a.x*a.x+a.y*a.y)>1.0*b.x/sqrt(b.x*b.x+b.y*b.y);//按照夹角余弦值从大到小排序
}
我这个排序是按照余弦从大到小排序的,由于y>0,则夹角的范围是(0,π),在(0,π),夹角越大,余弦值越小,所以把夹角余弦值按照由大到小排序。
存有向量(坐标点)的数组经过排序以后,那么数组中靠前的点都有通往数组中靠后的点的一条边。
注意:方向相同的两个向量所对应的两个点之间是没有边的!!
注意:排序前还要记录每个点的序号,可以弄一个如下的结构体,在输入数据的时候顺便记录序号
typedef struct op{
ll x;//x值
ll y;//y值
int place;//序号
}point;
point po[1000006];
for(i=1;i<=n;i++){
po[i].x=read();
po[i].y=read();
po[i].place=i;//记录序号
}
比如样例一中所有向量排序如下图所示:
排完序,可以继续探讨向量叉乘的集合意义:
向量叉乘的的模长就等于以两个向量为边的三角形的面积的两倍。
由于要求最短路径长,则让这个三角形的面积越小越好
举个例子,还是样例一,假如要求从起点(5号点)到2号点的最短路径,由图可知从5号点到2号线之间是有路径的
1.如果直接从5号点到2号点,则如图所示,蓝色部分面积为6:
2.先经过4号点,再到2号点,如图所示,蓝色面积为3:
综上所述:为了得到最小面积,必须让这个多边形形状越往下凹越好,换句话说,从起点开始所走的路径应该为一个向量递增的向量序列序列(并不是模长递增,起点向量不计入此序列),后面讨论什么叫向量递增。
要判断一个向量序列是否递增,此序列至少要有三个向量,如图所示:
由于向量3的终点,超过了向量4和5终点连线的边(红色线),那么说明这个向量序列中,从向量3到向量4不是递增的,也说明了从起点(向量5终点)到点4不需要经过向量3。
又如图所示:
向量3还是超过了红色线,而向量4没有超过红色线,说明从点5(起点)到点2需要经过点4,不需要经过点3。
不可能每次找最短路径都按照上面的方法来判断,那样绝对超时
为了找到这个多边形的凹壳,需要利用到单调栈,不知道单调栈的可以看看其他博客,单调栈就是栈内的元素保持单调增,或者单调减。
这个题我们需要维护单调增的单调栈,首先我们用一个数组dis来存起点到每个点的最短距离,用一个数now存目前单调栈从栈底元素(起点)到栈顶元素的最短路径
单调栈会有下列情况:
1.开始栈是空的,遍历排序后的向量数组的向量,如果不是起点向量,则起点到此向量没有路径,赋值dis[]为-1
2.遍历到了起点向量,赋值dis[]为0,并且把起点向量放到单调栈里
3.继续遍历向量,如果此向量和起点向量同向,则起点到此点没有路径,赋值dis[]为-1
4.遍历到第一个和起点向量不同向的向量,直接把此向量放到栈里面,更新now值,并把now值赋给dis[]
(下面这个是重点)
5.后面继续遍历向量,此时栈里面有二个或者两个以上的向量,如果栈里除栈底向量(起点向量)以外的向量加上目前遍历的向量组成的向量序列是递减的,就把栈顶向量移出栈,并且更新now值,然后继续判断,直到栈内只剩起点向量或者栈内除栈底向量以外的向量加上目前遍历到的向量组成的向量序列是递增的,然后就把目前遍历到的向量放入栈内,更新now值,并把目前的now值赋给dis[]
代码和代码解释如下(注意起点向量一旦入栈,就永不出栈):
inline bool check(point a,point b,point c){//c是目前遍历到的向量,a是栈顶向量,b是栈内第二个向量
if(getdis(a,c)<=getdis(a,b)+getdis(b,c)) return true;//如果发现a向量到c向量不是递增的,需要移出栈顶
else return false;//否则不移出
}
memset(dis,-1,sizeof(dis));//首先把dis数组全部赋值为-1,后面再按照需求改
int i=1;
while(po[i].place!=k && i<=n) i++;//这个是判断排序后向量数组中在起点向量之前的向量(都没有路径)
dis[po[i].place]=0;sta.push(po[i]);//等遍历到了起点向量,就把dis[]赋值为0,并且把此向量放入栈中
while(1.0*po[i].x/po[i].y==1.0*sta.top().x/sta.top().y && i<=n) i++;//这个是判断和此向量同向的向量(也没有路径)
for(;i<=n;i++){
point p=po[i];//p是目前遍历到的向量
point q1,q2;
while(sta.size()>1){//如果栈内除了起点向量,还有其他向量,就满足循环条件
q1=sta.top();//q1是栈顶向量
sta.pop();
q2=sta.top();//q2是栈中第二个向量
sta.push(q1);
if(check(q2,q1,p)){//判断是否不满足单调增条件,注意是不满足
now-=getdis(q2,q1);//不满足说明要移出栈顶,先更新now值
sta.pop();//然后移出栈顶
}
else break;//满足条件,循环就结束了
}
now+=getdis(sta.top(),p);//先更新now值
dis[p.place]=now;//把now值赋给dis[]
sta.push(p); //把此向量移入栈
}
下面讨论一些特殊情况
如果有两个同方向的向量,如下图所示
红色是目前遍历到的向量,蓝色是栈顶向量,为了获得一个凹壳,我们当然是要留红色的向量,为了方便,可以进行一下操作(重点)
1.在排序comp函数中加入一条排序规则
if(1.0*a.x/a.y==1.0*b.x/b.y) return a.x*a.x+a.y*a.y>b.x*b.x+b.y*b.y;
inline bool check(point a,point b,point c){
if(1.0*c.x/c.y==1.0*b.x/b.y) return true;// 不要问为啥把x放分母,这样做可以预防垂直的向量,反正y大于0
if(getdis(a,c)<=getdis(a,b)+getdis(b,c)) return true;
else return false;
}
加了这条语句,就保证排序中,遇到两个同方向的向量,就把模长长的放前面,模长短的放后面
2.在check函数中加入一条语句
if(1.0*c.x/c.y==1.0*b.x/b.y) return true;
inline bool check(point a,point b,point c){
if(1.0*c.x/c.y==1.0*b.x/b.y) return true;
if(getdis(a,c)<=getdis(a,b)+getdis(b,c)) return true;
else return false;
}
加了这条语句,就保证如果栈顶向量和遍历到的向量同向,就把栈顶向量移出栈
这两个操作配合起来,就保证几个同向的向量pk后,最后留下的肯定是最短的向量,同时还能更新这几个向量的最短路径长。
举个例子(还原过程,可无视)
向量有下列这些(已经排好序了)
3 1
3 3(这个是起点)
2 2
1 3
0 2
-2 4
-1 2
-1 1
如下图所示:
1.遍历第一个向量(3,1),不是起点向量,不用入栈,dis[1]=-1,now=0
---- | ---- | ---- | ---- | — | — | — | — | — |
---|---|---|---|---|---|---|---|---|
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
dis[i] | -1 | |||||||
栈内 | (栈顶) |
2.遍历第二个向量(3,3),是起点向量,入栈,dis[2]=0,now=0
---- | ---- | ---- | ---- | — | — | — | — | — |
---|---|---|---|---|---|---|---|---|
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
dis[i] | -1 | 0 | ||||||
栈内 | (栈顶)(3,3) |
3.遍历第三个向量(2,2),和起点向量同向,不用入栈,dis[3]=-1,now=0
---- | ---- | ---- | ---- | — | — | — | — | — |
---|---|---|---|---|---|---|---|---|
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
dis[i] | -1 | 0 | -1 | |||||
栈内 | (栈顶)(3,3) |
4.遍历第四个向量(1,3),由于栈内只有一个元素,入栈,dis[4]=now=6
---- | ---- | ---- | ---- | — | — | — | — | — |
---|---|---|---|---|---|---|---|---|
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
dis[i] | -1 | 0 | -1 | 6 | ||||
栈内 | (栈顶)(1,3) | (3,3) |
5.遍历第五个向量(1,3),getdis(2,4)+get(4,5)>=getdis(2,5),栈顶4向量出栈,5向量入栈,dis[5]=now=6-6+6=6
---- | ---- | ---- | ---- | — | — | — | — | — |
---|---|---|---|---|---|---|---|---|
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
dis[i] | -1 | 0 | -1 | 6 | 6 | |||
栈内 | (栈顶)(0,2) | (3,3) |
6.遍历第六个向量(-2,4),getdis(2,5)+get(5,6)<getdis(2,6),直接入栈,dis[6]=now=6+4=10
---- | ---- | ---- | ---- | — | — | — | — | — |
---|---|---|---|---|---|---|---|---|
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
dis[i] | -1 | 0 | -1 | 6 | 6 | 10 | ||
栈内 | (栈顶)(-2,4) | (0,2) | (3,3) |
7.遍历第七个向量(-1,2),与栈顶6向量同向,栈顶6向量出栈,7向量入栈,dis[7]=now=10-4+2=8
---- | ---- | ---- | ---- | — | — | — | — | — |
---|---|---|---|---|---|---|---|---|
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
dis[i] | -1 | 0 | -1 | 6 | 6 | 10 | 8 | |
栈内 | (栈顶)(-1,2) | (0,2) | (3,3) |
8.遍历第八个向量(-1,1),getdis(5,7)+get(7,8)>=getdis(5,8),栈顶7向量出栈,now=8-2=6;
getdis(2,5)+get(5,8)>=getdis(2,8),栈顶5向量出栈,8向量入栈,now=6-6+6=6
---- | ---- | ---- | ---- | — | — | — | — | — |
---|---|---|---|---|---|---|---|---|
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
dis[i] | -1 | 0 | -1 | 6 | 6 | 10 | 8 | 6 |
栈内 | (栈顶)(-1,1) | (3,3) |
代码如下:
#include <iostream>
#include <stack>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
typedef long long ll;
typedef struct op{
ll x;
ll y;
int place;
}point;
point po[1000006];
stack<point> sta;
ll dis[1000006];
ll now=0;
int n,k;
inline int read(){
int x=0,w=1;char ch=getchar();
while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
if(ch=='-')w=0,ch=getchar();
while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return w?x:-x;
}
inline bool comp(point a,point b){
if(1.0*a.x/a.y==1.0*b.x/b.y) return a.x*a.x+a.y*a.y>b.x*b.x+b.y*b.y;
else return 1.0*a.x/sqrt(a.x*a.x+a.y*a.y)>1.0*b.x/sqrt(b.x*b.x+b.y*b.y);
}
inline ll getdis(point a,point b){return a.x*b.y-a.y*b.x;}
inline bool check(point a,point b,point c){
if(1.0*c.x/c.y==1.0*b.x/b.y) return true;
if(getdis(a,c)<=getdis(a,b)+getdis(b,c)) return true;
else return false;
}
int main(){
int i;
memset(dis,-1,sizeof(dis));
n=read();
for(i=1;i<=n;i++){
po[i].x=read();
po[i].y=read();
po[i].place=i;
}
k=read();point pk=po[k];
sort(po+1,po+n+1,comp);
i=1;
while(po[i].place!=k && i<=n) i++;
dis[po[i].place]=0;sta.push(po[i]);
while(1.0*po[i].x/po[i].y==1.0*sta.top().x/sta.top().y && i<=n) i++;
for(;i<=n;i++){
point p=po[i];
point q1,q2;
while(sta.size()>1){
q1=sta.top();
sta.pop();
q2=sta.top();
sta.push(q1);
if(check(q2,q1,p)){
now-=getdis(q2,q1);
sta.pop();
}
else break;
}
now+=getdis(sta.top(),p);
dis[p.place]=now;
sta.push(p);
}
for(i=1;i<=n;i++) printf("%lld\n",dis[i]);
return 0;
}
(加了一个快速写入,不然老是在超时的边缘徘徊,自己太菜 ),说实话没加快速读入是下面的样子
不知道为何发生这种玄学情况。。。。