题解 P1153 【点和线】

· · 题解

这道题有很多方法,可以用凸包来做,也可以用深搜来做(因为数据实在是小的可怜)

前两篇题解用的深搜,搜索中有一个重要的内容就是判断两线段有没有交点的问题,这个其实不难

最简单易懂的方法就是:

线段的两端既然都知道了,轻轻松松就能算出两条线段所在直线的解析式,解析式求出来之后求两直线的交点,再和两条已知线段的端点进行比较,看是不是在端点限定的区间内就行了,这个方法代码简单,运算简便,简直有点简单的不像话,具体代码我就不贴了

现在来一发思维缜密的高大上的方法:向量叉乘

问题:给出两条线段,问两线段是否相交?

向量叉乘(行列式计算):向量a(x1,y1),向量b(x2,y2):

首先我们要明白一个定理:向量a×向量b(×为向量叉乘),若结果小于0,表示向量b在向量a的顺时针方向;若结果大于0,表示向量b在向量a的逆时针方向;若等于0,表示向量a与向量b平行。(顺逆时针是指两向量平移至起点相连,从某个方向旋转到另一个向量小于180度)。如下图:

在上图中,OA×OB = 2 > 0, OB在OA的逆时针方向;OA×OC = -2 < 0,OC在OA的顺势针方向。即叉乘结果大于0,后一个在前一个的逆时针方向;小于零,后一个在前一个的顺时针方向。

那如何来判断两线段是否相交呢?

假设有两条线段AB,CD,若AB,CD相交,我们可以确定:

1.线段AB与CD所在的直线相交,即点A和点B分别在直线CD的两边;

2.线段CD与AB所在的直线相交,即点C和点D分别在直线AB的两边;

上面两个条件同时满足是两线段相交的充要条件,所以我们只需要证明点A和点B分别在直线CD的两边,点C和点D分别在直线AB的两边,这样便可以证明线段AB与CD相交了。

那判断两线段是否相交与一开始提到的向量叉乘定理有什么关系呢?有,我们可以通过叉乘来证明上面说的充要条件。看下图:

在上图中,线段AB与线段CD相交,于是我们可以得到两个向量AC,AD,C和D分别在AB的两边,向量AC在向量AB的逆势针方向,AB×AC > 0;向量AD在向量AB的顺势针方向,AB×AD < 0,两叉乘结果异号。

这样,方法就出来了:如果线段CD的两个端点C和D,与另一条线段的一个端点(A或B,只能是其中一个)连成的向量,与向量AB做叉乘,若结果异号,表示C和D分别在直线AB的两边,若结果同号,则表示CD两点都在AB的一边,则肯定不相交。

当然,不能只证明C,D在直线AB的两边,还要用相同的方法证明A,B在直线CD的两边,两者同时满足才是线段相交的充要条件。

不过,线段相交还有一些特殊情况:

1.只有1点相交,如下图:

上图中,线段AB与CD相交于C点,按照之前介绍的方法,我们可以连成两向量AD和AC,这时候,我们发现,AC与AB共线,AB×AC = 0;而AB×AD < 0;两者并不异号,可实际上仍然相交。所以当出现两叉乘结果中,有一方为0,也可以看成点CD在直线AB的两边。

2.两条线段重合,如下图:

在上图中,线段AB与线段CD重合,重合部分为CB,这种重合的情况要特殊判断:

首先,我们给没条线段的两个端点排序,大小判断方法如下:横坐标大的点更大,横坐标相同,纵坐标大的点更大。

排好序后,每条线段中,小的点当起点,大的当终点。我们计算向量AB×向量CD,若结果为0,表示线段AB平行CD,平行才有了重合的可能;但平行也分共线和不共线,只有共线才有可能重合,看下图:

上图中,第一种情况不共线,第二种情况共线。那如何来判断是否共线呢?

我们可以在两条线段中各取一点,用这两点组成的向量与其中一条线段进行叉乘,结果若为0,就表示两线段共线,如下图:

我们取向量BC,若BC×CD = 0,表示两点共线,即是第二种情况,否则就是第一种情况。第一种情况肯定不相交。猴子为什么不喜欢平行线?因为他们没有相交。。。(尬)

然然然然然而,即使他们共线,却还是不一定重合,就如上图中第二种情况。这时候,之前给点排序的妙处就体现出来了:

若一条线段AB与另一条线段CD共线,且线段AB的起点小于等于线段CD的起点,但线段AB的终点(注意是终点)大于等于线段CD的起点(注意是起点),或者交换一下顺序,CD的起点小于AB的起点......只要满足其中一个,就表示有重合部分。

但是:洛谷的数据很水,这里面的特殊情况一个都没有

因此,会用叉乘判断就能过这一题了

话不多说,晒出你们最爱的代码: (我是个萌新,啥高端操作都不会,代码看两眼你就懂了)

#include <cstdio>
#include <algorithm>

using namespace std;

struct point{
    double x,y;
}po[5],map[11];
point a,b,c,d;

int n=1,ans;
int path[11];
bool vis[11];
bool disallow[11][11][11][11];

inline int compare(point a,point b)
{
    if(a.x<b.x||a.x==b.x&&a.y<b.y)  return -1;
    else if(a.x==b.x&&a.y==b.y) return 0;
    else return 1;
}

inline bool cmp(point a,point b)
{
    if(a.x!=b.x) return a.x<b.x;
    else return a.y<b.y;
}

inline double compute(double x1,double y1,double x2,double y2)
{
    return x1*y2-x2*y1;
}

bool judge(point a,point b,point c,point d)
{
    po[1]=a;po[2]=b;
    po[3]=c;po[4]=d;
    sort(po+1,po+3,cmp);
    sort(po+3,po+5,cmp);

    if(compute(po[1].x-po[2].x,po[1].y-po[2].y,po[3].x-po[4].x,po[3].y-po[4].y)==0) //两线段平行 
    {
        if(compute(po[1].x-po[2].x,po[1].y-po[2].y,po[1].x-po[3].x,po[1].y-po[3].y)==0) //两线段共线 
        {
            if(compare(po[1],po[3])<=0&&compare(po[2],po[3])>=0)  //第一条起点小于第二条起点,第一条终点大于第二条起点 
              return true;
            else if(compare(po[3],po[1])<=0&&compare(po[4],po[1])>=0) //第二条起点小于第一条起点,第二条终点大于第一条起点
              return true;
            else return false;
        }
        else return false;
    }
    else if(compute(po[1].x-po[2].x,po[1].y-po[2].y,po[3].x-po[4].x,po[3].y-po[4].y)!=0) //两线段不平行 
    {
        double num1,num2,num3,num4;
        num1=compute(po[1].x-po[2].x,po[1].y-po[2].y,po[1].x-po[3].x,po[1].y-po[3].y);//判断第一条线的两端点 
        num2=compute(po[1].x-po[2].x,po[1].y-po[2].y,po[1].x-po[4].x,po[1].y-po[4].y);//在第二条线的两边 
        num3=compute(po[1].x-po[3].x,po[1].y-po[3].y,po[3].x-po[4].x,po[3].y-po[4].y);//判断第二条线的两端点 
        num4=compute(po[2].x-po[3].x,po[2].y-po[3].y,po[3].x-po[4].x,po[3].y-po[4].y);//在第一条线的两边 
        if(num1*num2<=0&&num3*num4<0||num1*num2<0&&num3*num4<=0) //等于0表示一条线段的某一端点在另一条线段上
          return true;
        else return false;
    }
    else return false;
}

void dfs(int step,int now)
{
    if(step==n+1) {
    ans++;
//  
//  for(int i=0;i<=step;i++)
//    printf("%d ",path[i]);
//    printf("\n");
//
    return;}
    if(step==n) vis[1]=false;
    for(int i=1;i<=n;i++)
    {
        if(vis[i]) continue;
        bool flag=true;
        for(int j=0;j<step-2&&flag;j++)
          if(disallow[path[j]][path[j+1]][now][i]) flag=false;
        if(flag)
        {
            path[step]=i;
            vis[i]=true;
            dfs(step+1,i);
            vis[i]=false;
        }
        vis[1]=true;
    }
}

void ready()
{
    for(int i=1;i<=n;i++)
      for(int j=1;j<=n;j++)
        for(int p=1;p<=n;p++)
          for(int q=1;q<=n;q++)
            if(judge(map[i],map[j],map[p],map[q])) disallow[i][j][p][q]=true;
}

int main ()
{
    while(scanf("%lf%lf",&map[n].x,&map[n].y))
    {
        if(map[n].x!=0||map[n].y!=0) n++;
        else break;
    }
    path[0]=1;
    vis[1]=true;
    ready();
    dfs(1,1);
    printf("%d\n",ans/2);
    return 0;
}

这题目蛮好的,和直接求两线段有无交点还是有一点点不一样滴:这道题有公共端点不视为有交点,即有公共端点是可以连起来的。细心的读者一定想到了,如果单纯考虑两个线段有没有交点的话这个情况是算作有交点的。没错我当时考虑到了,然后我改了1个多小时才发现这题有公共端点是允许的/泪奔

最后附上参考: 特务依昂 判断两条线段是否相交—(向量叉乘)

感谢您的阅读!^_^