XUANMING'S JOURNAL屏幕在深夜微微发亮,我心在远方。
← 回到笔记
FIELD NOTES

题解 P5073 【[Ynoi2015]世上最幸福的女孩】

这篇笔记的路线图

如果你其他$\text{Ynoi}$还没有刷完建议不要来刷这道题,因为:

实际上,这是一道正宗的计算几何题。

Part.1

没什么好说的,既然都知道这是一道计算几何题,请先自行学习计算几何的基础知识。

你需要掌握的有:向量的加,减,叉积,凸包的求法(极角排序)。

以下是下文中点(或向量)的定义:

struct Point    //点
{
    ll x,y;
    Point(ll x=0,ll y=0) : x(x),y(y) {}
    Point operator + (const Point &a) const {return Point(x+a.x,y+a.y);}    //加
    Point operator - (const Point &a) const {return Point(x-a.x,y-a.y);}    //减
    ll operator * (const Point &a) const {return x*a.y-y*a.x;}    //叉积
};

凸包的定义:

struct Hull    //凸包
{
    Point *st;
    int now,top;
    Point& operator [] (const int &x) const {return st[x];}
    inline void insert(const Point &a) const{st[a.x].y=max(st[a.x].y,a.y);}    //更新最优点
    inline void push(const Point &a) {st[++top]=a;}    //加入一个点
    inline void init(int x)    //预处理,长度为x的答案对应其位置,方便insert
    {
        for(int i=1;i<=x;++i)
            st[i]=Point(i,-inf);
        top=x;
    }
    inline void Convex()    //求新凸包
    {
        if(top<=2) return;
        int lim=top;top=2;
        for(int i=3;i<=lim;++i)
        {
            if(st[i].y==-inf) continue;
            while(top>1&&(st[top]-st[top-1])*(st[i]-st[top-1])>=0) --top;
            st[++top]=st[i];
        }
    }
    inline ll Getnow()
    {
        while(now!=top&&add*st[now].x+st[now].y<add*st[now+1].x+st[now+1].y) ++now;    //离线保证单增,移动指针即可
        return st[now].x*add+st[now].y;
    }
};

看不懂可以先跳过,下面要用了再来看,主要一些小函数下面可能没有讲。

这个题的的新知识有:闵可夫斯基和合并凸包。

问题:给你两个点集A,B,产生一个新点集C,满足对任意点$(x_1,y_1)\in A$,$(x_2,y_2)\in B$,得到一个$(x_1+x_2,y_1+y_2)\in C$。

然后知道A,B的凸包,求C的凸包。

这不是很简单吗,暴力枚举直接做。

我们需要一个复杂度为$O(n)$的算法。

我们画个图手玩一下,发现新的凸包就是原来的两个凸包的边重新极角排序一边(网上有图我就不放了)。

证明我不会,可以看一下这个题,感性理解一下,毕竟这里是讲数据结构的。

于是我们得到了一个很有用的结论,求凸包只需要求原来的凸包,然后在一起重新排序就可以了。

但直接重排有点浪费,由于原来两个凸包都已经求好了,于是可以类似归并排序,重新归并一次,代码如下:

inline void Minkowski(Hull &c,Hull &a,Hull &b)    //闵可夫斯基和合并凸包
{
    int i=1,j=1;c.insert(a[1]+b[1]);
    while(i!=a.top&&j!=b.top)
    {
        ((a[i+1]-a[i])*(b[j+1]-b[j])<0)?++i:++j;
        c.insert(a[i]+b[j]);
    }
    while(i!=a.top) ++i,c.insert(a[i]+b[j]);
    while(j!=b.top) ++j,c.insert(a[i]+b[j]);
}

凸包存的是点,我们比较a中当前的边与b中当前的边,看一下谁角度更小,然后加入即可,且加入点的原来的点的和。

很简单是不是?

下面开始我们今天的数据结构部分。

Part.2

最大子段和?

很简单啊,$O(n)$扫一遍。

区间最大子段和,单点修改?

做过小白逛公园的都知道(没做过还来$\text{Ynoi}$)这是一个经典问题。哪来那么多经典问题

线段树。

每个点维护一个$lmx,rmx,mmx,sum$,即最大左子段和,最大右子段和,最大子段和,区间和。

合并很简单吧(默认大家都会)。

一个封装好的结构体长这样:

struct Node    //答案节点
{
    ll lmx,rmx,mmx,sum;
    Node(ll lmx,ll rmx,ll mmx,ll sum) : lmx(lmx),rmx(rmx),mmx(mmx),sum(sum) {}
    Node() {}
    Node operator + (const Node &x) const
    {
        return Node(max(lmx,sum+x.lmx),max(x.rmx,x.sum+rmx),max(rmx+x.lmx,max(mmx,x.mmx)),sum+x.sum);
    }
};

下面的重载运算符即是合并两个答案。

时间复杂度$O(nlogn)$

区间最大子段和,全局加?

先考虑打标记。

显然不行

主要原因是无法通过新的标记快速更新线段树的节点。

然后就没法做?

换个思路。

Part.3

线段树的四个信息肯定还是必须要维护的。

$sum$照旧维护。

我们考虑对于维护长度为$x$的$lmx,rmx,mmx$,实际上这就是一些二维点$(x,ans(x))$

我们可以发现,当我们给这个节点整体加了一个数$add$,那么$ans(x)$就变成了$ans(x)+x*add$,但显然对于我们的这些二维的点,肯定只有一部分是有用的,如果一个点被另一个点压制,那么它就肯定没有用。那么哪些点是有用的呢?相信看到这里大概都明白了,实际上就是这些点的凸包上的点。

所以我们改成维护$lmx,rmx,mmx$三个凸包。

Part.4

首先先解决$lmx,rmx$两个凸包,由于两个等价,只考虑$lmx$。

先看一下我们原来的$lmx$的转移方程

lmx[u]=max(lmx[u<<1],sum[u<<1]+lmx[u<<1|1]);

改成凸包后,我们就是先给右儿子的$lmx$凸包加上一个点$(len_{lson},sum_{lson})$,然后与右凸包合并,代码长这样:

 inline void merge(Hull &c,Hull &a,Hull &b,const Point &now)    //合并前后缀的凸包
 {
     for(int i=1;i<=a.top;++i) c.push(a[i]);
     for(int i=1;i<=b.top;++i) c.push(b[i]+now);
     c.Convex();    //求新凸包
 }

$now$就是$(len_{lson},sum_{lson})$,代码就是先把两个凸包加进去,然后求新凸包。

inline void Convex()    //求新凸包
{
    if(top<=2) return;
    int lim=top;top=2;
    for(int i=3;i<=lim;++i)
    {
        if(st[i].y==-inf) continue;
        while(top>1&&(st[top]-st[top-1])*(st[i]-st[top-1])>=0) --top;
        st[++top]=st[i];
    }
}

由于原来就有序,所以可以直接求不用排序。

注意$st$数组中始终$top\le i$,所以可以不用辅助空间。

Part.5

观察$mmx$的转移方程:

mmx[u]=max(max(mmx[u<<1],mmx[u<<1|1]),rmx[u<<1]+lmx[u<<1|1]);

对于$mmx$的凸包,我们先用左右儿子的$mmx$更新一下:

mmx[u].st++;mmx[u].init(r-l+1);
for(int i=1;i<=mmx[u<<1].top;++i) mmx[u].insert(mmx[u<<1][i]);
for(int i=1;i<=mmx[u<<1|1].top;++i) mmx[u].insert(mmx[u<<1|1][i]);

然后$lmx[u<<1]+rmx[u<<1|1]$,怎么处理呢?

是不是觉得还有什么没有登场?

没错,我们的闵可夫斯基和!

仔细想一想,假设有$(x_1,ans(x_1))\in lmx[u<<1]\;\;(x_2,ans(x_2))\in rmx[u<<1|1]$,就会得到一个新的点$x_1+x_2,ans(x_1+x_2))\in mmx[u]$!

于是我们一次闵可夫斯基和合并凸包就可以了,代码长这样:

inline void Minkowski(Hull &c,Hull &a,Hull &b)    //闵可夫斯基和合并凸包
{
    int i=1,j=1;c.insert(a[1]+b[1]);
    while(i!=a.top&&j!=b.top)
    {
        ((a[i+1]-a[i])*(b[j+1]-b[j])<0)?++i:++j;
        c.insert(a[i]+b[j]);
    }
    while(i!=a.top) ++i,c.insert(a[i]+b[j]);
    while(j!=b.top) ++j,c.insert(a[i]+b[j]);
}

然后就完了。

Part.6

不还没完

我们光求了一堆凸包,怎么用啊?

这还不简单,直接在凸包上二分即可。

然而你仔细想想,这样的复杂度是多少?

$O(nlog^2n)$

由于这是$\text{Ynoi}$,不是让你$AC$的地方,所以你需要优化。

怎么优化?

我们考虑离线,想一想,凸包有一个好的性质,它上面的点是单调的,我们可以知道从一开始到这个点一共全局加了多少,然后我们按这个把询问排序,由于我们加的值是单增的,所以我们直接每次从上次的位置开始向右扫凸包,因为答案绝对不可能向前走,所以我们的均摊时间就变成了$O(nlogn)$。

于是你就可以快乐的去$AC$此题了!

具体实现借鉴了shadowice1984大佬的方法,把同一层的线段树节点空间分配在一起,减小访问压力。

凸包使用了指针,也可以用$vector$。

$\text{Code}$

#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn=3e5+5;
const ll inf=0x3f3f3f3f3f3f3f3f;

ll add,a[maxn];
int n,m;

struct Point    //点
{
    ll x,y;
    Point(ll x=0,ll y=0) : x(x),y(y) {}
    Point operator + (const Point &a) const {return Point(x+a.x,y+a.y);}    //加
    Point operator - (const Point &a) const {return Point(x-a.x,y-a.y);}    //减
    ll operator * (const Point &a) const {return x*a.y-y*a.x;}    //叉积
};

struct Hull    //凸包
{
    Point *st;
    int now,top;
    Point& operator [] (const int &x) const {return st[x];}
    inline void insert(const Point &a) const{st[a.x].y=max(st[a.x].y,a.y);}    //更新最优点
    inline void push(const Point &a) {st[++top]=a;}    //加入一个点
    inline void init(int x)    //预处理,长度为x的答案对应其位置,方便insert
    {
        for(int i=1;i<=x;++i)
            st[i]=Point(i,-inf);
        top=x;
    }
    inline void Convex()    //求新凸包
    {
        if(top<=2) return;
        int lim=top;top=2;
        for(int i=3;i<=lim;++i)
        {
            if(st[i].y==-inf) continue;
            while(top>1&&(st[top]-st[top-1])*(st[i]-st[top-1])>=0) --top;
            st[++top]=st[i];
        }
    }
    inline ll Getnow()
    {
        while(now!=top&&add*st[now].x+st[now].y<add*st[now+1].x+st[now+1].y) ++now;    //离线保证单增,移动指针即可
        return st[now].x*add+st[now].y;
    }
};

struct Node    //答案节点
{
    ll lmx,rmx,mmx,sum;
    Node(ll lmx,ll rmx,ll mmx,ll sum) : lmx(lmx),rmx(rmx),mmx(mmx),sum(sum) {}
    Node() {}
    Node operator + (const Node &x) const
    {
        return Node(max(lmx,sum+x.lmx),max(x.rmx,x.sum+rmx),max(rmx+x.lmx,max(mmx,x.mmx)),sum+x.sum);
    }
};

struct SegmentTree    //线段树
{
    Point lmx_base[20][maxn<<1],rmx_base[20][maxn<<1],mmx_base[20][maxn<<1];
    Point *lmx_top[20],*rmx_top[20],*mmx_top[20];
    Hull lmx[maxn<<2],rmx[maxn<<2],mmx[maxn<<2];    //凸包维护可能的答案
    ll sum[maxn<<2];
    SegmentTree()
    {
        for(int i=0;i<20;++i)
            lmx_top[i]=lmx_base[i],rmx_top[i]=rmx_base[i],mmx_top[i]=mmx_base[i];
    }
    inline void merge(Hull &c,Hull &a,Hull &b,const Point &now)    //合并前后缀的凸包
    {
        for(int i=1;i<=a.top;++i) c.push(a[i]);
        for(int i=1;i<=b.top;++i) c.push(b[i]+now);
        c.Convex();    //求新凸包
    }
    inline void Minkowski(Hull &c,Hull &a,Hull &b)    //闵可夫斯基和合并凸包
    {
        int i=1,j=1;c.insert(a[1]+b[1]);
        while(i!=a.top&&j!=b.top)
        {
            ((a[i+1]-a[i])*(b[j+1]-b[j])<0)?++i:++j;
            c.insert(a[i]+b[j]);
        }
        while(i!=a.top) ++i,c.insert(a[i]+b[j]);
        while(j!=b.top) ++j,c.insert(a[i]+b[j]);
    }
    void build(int u,int l,int r,int dep=0)
    {
        lmx[u].st=lmx_top[dep];rmx[u].st=rmx_top[dep];mmx[u].st=mmx_top[dep];
        if(l==r)
        {
            lmx[u][1]=rmx[u][1]=mmx[u][1]=Point(0,0);    //不选则为0
            lmx[u][2]=rmx[u][2]=mmx[u][2]=Point(1,a[l]);sum[u]=a[l];
            lmx[u].top=rmx[u].top=mmx[u].top=2;
        }
        else
        {
            int mid=(l+r)>>1;
            build(u<<1,l,mid,dep+1);build(u<<1|1,mid+1,r,dep+1);
            sum[u]=sum[u<<1]+sum[u<<1|1];    //维护和
            merge(lmx[u],lmx[u<<1],lmx[u<<1|1],Point(mid-l+1,sum[u<<1]));
            merge(rmx[u],rmx[u<<1|1],rmx[u<<1],Point(r-mid,sum[u<<1|1]));    //合并前后缀凸包
            mmx[u].st++;mmx[u].init(r-l+1);    //为不选预留位置
            for(int i=1;i<=mmx[u<<1].top;++i) mmx[u].insert(mmx[u<<1][i]);
            for(int i=1;i<=mmx[u<<1|1].top;++i) mmx[u].insert(mmx[u<<1|1][i]);
            Minkowski(mmx[u],rmx[u<<1],lmx[u<<1|1]);    //合并子段和凸包
            mmx[u].st--;mmx[u][1]=Point(0,0);mmx[u].top++;mmx[u].Convex();   //不选则为0
        }
        lmx[u].now=rmx[u].now=mmx[u].now=1;
        lmx_top[dep]=lmx[u].st+lmx[u].top;
        rmx_top[dep]=rmx[u].st+rmx[u].top;
        mmx_top[dep]=mmx[u].st+mmx[u].top;
    }
    Node query(int u,int l,int r,int x,int y)
    {
        if(x<=l&&r<=y) return Node(lmx[u].Getnow(),rmx[u].Getnow(),mmx[u].Getnow(),sum[u]+(r-l+1)*add);
        int mid=(l+r)>>1;
        if(y<=mid) return query(u<<1,l,mid,x,y);
        if(x>mid) return query(u<<1|1,mid+1,r,x,y);
        return query(u<<1,l,mid,x,y)+query(u<<1|1,mid+1,r,x,y);
    }
}wife;

struct Query
{
    int l,r,id;
    ll val;
    Query(int l=0,int r=0,int id=0,ll val=0) : l(l),r(r),id(id),val(val) {}
    bool operator < (const Query &x) const {return val<x.val;}
}q[maxn<<1];
int tot;
ll ans[maxn<<1];

template<typename T>
inline void read(T &x)
{
    char c;int f=1;
    while(!isdigit(c=getchar())) (c=='-')&&(f=-1);
    x=c^48;
    while(isdigit(c=getchar())) x=x*10+(c^48);
    x*=f;
}

int main()
{
    read(n);read(m);
    for(int i=1;i<=n;++i) read(a[i]);
    for(int i=1,opt,l,r;i<=m;++i)
    {
        read(opt);read(l);
        if(opt==1) add+=l;
        else read(r),++tot,q[tot]=Query(l,r,tot,add);
    }
    sort(q+1,q+tot+1);
    add=q[1].val;
    for(int i=1;i<=tot;++i) q[i].val-=add;
    for(int i=1;i<=n;++i) a[i]+=add;    //这里使得所有的q[i].val均为正整数
    wife.build(1,1,n);
    for(int i=1;i<=tot;++i)
        add=q[i].val,ans[q[i].id]=wife.query(1,1,n,q[i].l,q[i].r).mmx;
    for(int i=1;i<=tot;++i)
        printf("%lld\n",ans[i]);
    return 0,QAQ;
}

做完了珂以去做一下这个(不是

输入关键词,搜索全部笔记。