CodeForces 50C 题解

题解笔记

CF50C Happy Farm 5(farm.cpp)

时间限制 \(2s\) | 内存限制 \(256M\)

题目描述:

平面直角坐标系上有 \(n\) 个整数点(横纵坐标都是整数),请你计算将所有点严格包围的路径最少要移动多少步。路径的每一步只能走整点,且每次只能向上、下、左、右、左上、左下、右上、右下的 \(8\) 个方向移动一格。严格包围要求所有的点不能在路径上。

输入格式:

第一行仅有一个正整数 \(n\)\(1 \leq n \leq 10^5\)),接下来的 \(n\) 行,每行包含两个整数 \(x_i\)\(y_i\) ( $ \mid x_i \mid , \mid y_i \mid \leq 10^6$ ) ,分别表示一个点的横纵坐标,数据保证没有两个点重合。

输出格式:

一个数,表示最短的路径步数。

输入输出样例:

样例1输入 样例1输出
4
1 1
5 1
5 3
1 3
16

样例解释

解题思路:

首先要先了解几个概念:

凸多边形

没有任何一个内角是优角的多边形。

凸包

在平面上能包含所有给定点的最小凸多边形叫做凸包

其定义为:对于给定集合 \(X\) ,所有包含 \(X\) 的凸集的交集 \(S\) 被称为 \(X\)凸包

实际上可以理解为用一个橡皮筋包含住所有给定点的形态。

凸包用最小的周长围住了给定的所有点。如果一个凹多边形围住了所有的点,它的周长一定不是最小,如下图。根据三角不等式,凸多边形在周长上一定是最优的。

凸包的一周称为凸壳

严格凸包与非严格凸包

非严格凸包:只要围住了点集 \(X\) 中的每一个点的凸包就是非严格凸包

严格凸包:围住了点集 \(X\) 中的每一个点且凸壳上不存在点集中的点的凸包

这道题有很多种思路

首先,一看到这道题,一下子就觉得这道题是求严格凸包

那如果用凸包的思路的话就有两种思路:

  • 将每个点都复制成上下左右四分,再加上中间的,就成为一个十字,然后再对复制完的点进行求非严格凸包
  • 求非严格凸包,再将凸壳的长度 \(+4\) 就是答案

(如果想要看这些思路的代码,请去看参考代码中 其他代码 一章)

但这道题也有其他的思路,接下来就重点介绍这种思路:

转化为非严格凸包

首先想考虑把这道题转化为求非严格凸包,可以发现,再求完非严格凸包后再 \(+4\) 就是严格凸包的凸壳长了

拿样例为例子:

首先,样例的四个点如下图所绘

111222333444555666777888999101010–1–1–1111222333444555666777000AAABBBCCCDDD

先求出非严格凸壳

–1–1–1111222333444555666777888999101010–4–4–4–3–3–3–2–2–2–1–1–1111222333444555000fffggghhhiiiAAABBBCCCDDD

然后再将 \(f,g,h,i\) 四条边都向外平移,并将 \(f\)\(g\)\(g\)\(h\)\(h\)\(i\)\(i\)\(f\) 之间连上一条斜边

如下图所示:

–1–1–1111222333444555666777888999101010–4–4–4–3–3–3–2–2–2–1–1–1111222333444555000fffggghhhiiijjjkkklllmmmAAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLL

由于 \(f,g,h,i\) 四条边长度并没有变,只是增加了 \(j,k,l,m\) 四条边,而 \(j,k,l,m\) 四条边,合起来也就多耗了 \(4\) 步,这就是开头所说 \(+4\) 的来源

证明非严格凸包最优路径的形状一定是斜矩形

其实上文在转化为非严格凸包时有一个限制,那就是凸壳一定是一个四边形

首先,可以证明,所有四边形的凸壳都可以转化为矩形的凸壳

需要证明,梯形(这里包含平行四边形)的凸壳可以被转化斜矩形的凸壳

由于这道题,只记录凸壳的长度,且斜着走一步与直着走一步都是一步,所以对凸壳长度的影响都是一样的

万物皆矩形

可以证明最优的凸壳一定为一个矩形:

证明梯形(包括平行四边形)可以被转化为斜矩形

若存在如下四点:

–1–1–1111222333444555666777888999101010–4–4–4–3–3–3–2–2–2–1–1–1111222333444555000AAABBBCCCDDD

求出他们的凸包为:

–2–2–2–1–1–1111222333444555666777888999101010111111–5–5–5–4–4–4–3–3–3–2–2–2–1–1–1111222333444555666777000fffggghhhiiiAAABBBCCCDDD

于是,我们继续转化 \(g\) 这条边,让凸壳更加接近菱形:

–1–1–1111222333444555666777888999101010–4–4–4–3–3–3–2–2–2–1–1–1111222333444555000gggfffhhhiiiAAABBBCCCDDDEEEFFF

但这时,会发现,有一部分的梯形就这样被转化为了菱形,可是有一部分梯形,就如我举出的这一个梯形,它并没有转化为斜矩形,而是被转化为缺了两个角的矩形。

那这时,就需要继续转化凸壳:

  1. 将 线段 \(EF\) 围绕 \(E\) 点顺时针旋转 $ 45^\circ $
  2. 将 线段 \(AC\) 围绕 \(C\) 点逆时针旋转 $ 45^\circ $

得到下图:

–2–2–2–1–1–1111222333444555666777888999101010–4–4–4–3–3–3–2–2–2–1–1–1111222333444555666000fffhhhiiigggkkkjjjAAABBBCCCDDDEEEFFFA'A'A'F'F'F'

接下来,再把 折线 \(ABEF'\) 向上平移 \(1\) 格:

–1–1–1111222333444555666777888999101010111111–5–5–5–4–4–4–3–3–3–2–2–2–1–1–1111222333444555666000fffhhhgggkkkiiijjjAAAB'B'B'CCCDDDEEEFFFBBB

但这时,就会发现一个问题,就是在变为斜矩形后,\(B\) 点不被凸包包含了,但这时,我们将这个非严格凸包转化为严格凸包:

111222333444555666777888999101010111111–4–4–4–3–3–3–2–2–2–1–1–1111222333444555000jjjfffggghhhiiikkkAAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLL

这样的话,就可以说明,所有梯形都能被转换为斜矩形

那剩下的凸壳就是矩形的凸壳,所以所有的凸壳都可以被转化为矩形和菱形的凸壳

正不压斜

接下来,我们就需要证明最优的凸包的凸壳中一定有斜矩形的凸壳

由于所有凸壳都能被转换为矩形的凸壳,所以我们只需要证明斜矩形的凸包 \(\leq\) 正矩形的凸壳

可以根据勾股定理来证明

假设有如下三个点:

111222333444555666777888999101010111111–1–1–1111222333444555666777888000AAABBBCCC

这时,如果使用正矩形的话,就可以求出如下非严格凸包(凸包长为 \(16\) ):

111222333444555666777888999101010111111–1–1–1111222333444555666777888000fffggghhhiiiAAABBBCCCDDD

但如果用斜矩形的话,可以求出更优的凸包(凸包长为 \(14\) ):

–2–2–2–1–1–1111222333444555666777888999–2–2–2–1–1–1111222333444555666777000fffggghhhiiiAAABBBCCCDDDEEEFFFGGG

由此可以看出,斜矩形最优

当然,也有别的方法来证明斜矩形最优

求出斜矩形

于是,问题又来了,如何求出能够涵括所有点的斜矩形呢?

首先,由于只能走方格的对角线,所以斜矩形的斜率是固定的。

其次,由于是矩形,所以只需要求出两组对边,即两条垂直的边,就可以确定斜矩形的周长。

接下来,就是讲解如何求这两条边

关于下面的第一、二条边指

第一条边指的是平行于平分一三象限的对角线的边

第二条边指的是平行于平分二四象限的对角线的边

求边两部曲(第一条边)

即下图中的 \(i\)\(g\)

–2–2–2–1–1–1111222333444555666777888999–2–2–2–1–1–1111222333444555666777000fffggghhhiiiAAABBBCCCDDDEEEFFFGGG

这时我们可以发现,我们可以用一三象限的平分线来为点集进行分层

从代数角度来说,每一个点的 \(x+y\) 就是这个点的层

对于两个点 \(p_1(x_1,y_1),p_2(x_2,y_2)\) 来说:

如果 \(x_1+y_1>x_2+y_2\) ,那么 \(p_1\)\(p_2\) 更偏右上

如果 \(x_1+y_1=x_2+y_2\) ,那么 \(p_1\)\(p_2\) 偏右上的程度一样

否则 \(p_1\)\(p_2\) 更偏左下

而第一条边的长度的两倍就是 \(max(x+y)-min(x+y)\)

求边两部曲(第二条边)

与求第一条边的方法相同:

这时我们可以发现,我们可以用二四象限的平分线来为点集进行分层

从代数角度来说,每一个点的 \(x-y\) 就是这个点的层

对于两个点 \(p_1(x_1,y_1),p_2(x_2,y_2)\) 来说:

如果 \(x_1-y_1>x_2-y_2\) ,那么 \(p_1\)\(p_2\) 更偏左上

如果 \(x_1+y_1=x_2+y_2\) ,那么 \(p_1\)\(p_2\) 偏左上的程度一样

否则 \(p_1\)\(p_2\) 更偏右下

而第二条边的长度的两倍就是 \(max(x-y)-min(x-y)\)

综上所述

$\because $

非严格凸包的长度 \(=\) 斜矩形的周长 \(=\) \(max(x+y)-min(x+y)+max(x-y)-min(x-y)\)

严格凸包的长度 \(=\) 非严格凸包的长度 \(+4\)

$\therefore $

答案 \(=\) 非严格凸包的长度 \(=\) \(max(x+y)-min(x+y)+max(x-y)-min(x-y)+4\)

参考代码:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int INF=0x3f3f3f3f;
int n;
int rmax=~INF,rmin=INF,lmax=~INF,lmin=INF;

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        rmax=max(rmax,x+y);
        rmin=min(rmin,x+y);
        lmax=max(lmax,x-y);
        lmin=min(lmin,x-y);
    }
    printf("%d",rmax-rmin+lmax-lmin+4);
    return 0;
}

其他代码

求非严格凸包,再将凸壳的长度 \(+4\) 就是答案:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
#define int long long
const int MAXN=1e5+5;
typedef pair<int,int> Point;
int st[MAXN],top;
Point p[MAXN];
inline double cross_times(const Point &a,const Point &b)
{
    return a.first*b.second-a.second*b.first;
}

inline const Point operator-(const Point &p1, const Point &p2)
{
    return Point(p1.first-p2.first,p1.second-p2.second);
}

inline int distance(const Point &p1,const Point &p2)
{
    return max(abs(p1.first-p2.first),abs(p1.second-p2.second));
}

int n;

signed main()
{
    scanf("%d",&n);
    for(int i=1; i<=n; i++)
    {
        scanf("%lld%lld",&p[i].first,&p[i].second);
    }
    sort(p+1,p+n+1);
    st[++top]=1;
    for(int i=2; i<=n; i++)
    {
        while(top>=2&&cross_times(p[st[top]]-p[st[top-1]],p[i]-p[st[top]])<=0) --top;
        st[++top]=i;
    }
    int tmp=top;
    for(int i=n-1; i>=1; i--)
    {
        while(top>tmp&&cross_times(p[st[top]]-p[st[top-1]],p[i]-p[st[top]])<=0) --top;
        st[++top]=i;
    }
    long long ans=0;
    for(int i=1; i<top; i++)
    {
        ans+=distance(p[st[i]],p[st[i+1]]);
    }
    printf("%lld",ans+distance(p[st[top]],p[st[1]])+4);
    return 0;
}

将每个点都复制成上下左右四分,再加上中间的,就成为一个十字,然后再对复制完的点进行求非严格凸包:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
#define int long long
const int MAXN=5e5+5;
typedef pair<int,int> Point;
int st[MAXN],top;
Point p[MAXN];
inline double cross_times(const Point &a,const Point &b)
{
    return a.first*b.second-a.second*b.first;
}

inline const Point operator-(const Point &p1, const Point &p2)
{
    return Point(p1.first-p2.first,p1.second-p2.second);
}

inline int distance(const Point &p1,const Point &p2)
{
    return max(abs(p1.first-p2.first),abs(p1.second-p2.second));
}

int n,tot;

inline void make_point(int a,int b)
{
    p[++tot].first=a;
    p[tot].second=b;
}

signed main()
{
    scanf("%d",&n);
    for(int i=1; i<=n; i++)
    {
        int x,y;
        scanf("%lld%lld",&x,&y);
        make_point(x,y);
        make_point(x-1,y);
        make_point(x+1,y);
        make_point(x,y-1);
        make_point(x,y+1);
    }
    n*=5;
    sort(p+1,p+n+1);
    st[++top]=1;
    for(int i=2; i<=n; i++)
    {
        while(top>=2&&cross_times(p[st[top]]-p[st[top-1]],p[i]-p[st[top]])<=0) --top;
        st[++top]=i;
    }
    int tmp=top;
    for(int i=n-1; i>=1; i--)
    {
        while(top>tmp&&cross_times(p[st[top]]-p[st[top-1]],p[i]-p[st[top]])<=0) --top;
        st[++top]=i;
    }
    long long ans=0;
    for(int i=1; i<top; i++)
    {
        ans+=distance(p[st[i]],p[st[i+1]]);
    }
    printf("%lld",ans+distance(p[st[top]],p[st[1]]));
    return 0;
}
posted @ 2021-11-26 19:54  yhang323  阅读(40)  评论(0编辑  收藏  举报