AtCoder [ARC070E] NarrowRectangles

神仙题,sto $\text{W}\color{red}{\text{hiteCmile}}$ orz

首先可以列出一个很好想的 $\text{DP}$ :

设 $f_{i,j}$ 为转移到了第 $i$ 个长方形,且这个长方形的左端点放在 $j$ 的最小答案

同时设 $len_i=r_i-l_i$ 方便书写(注意没有加 $1$ )

转移也较为容易,在此直接列出方程:

$$f_{i,j}=\min\{f_{i-1,k}\}+|j-l_i|,k \in [j-len_{i-1},j+len_i]$$


 把 $f_{i,j}$ 写为 $f_i(j)$ (也就是一个函数),易证 $f_i(x)$ 肯定是个凸函数

于是问题就转移成了如何快速将两个有一些特殊性质的凸函数相加(绝对值函数也是凸函数)


我们先看 $f_i(x)$ ,将其分开讨论:

我们将这个 $f_{i-1}(x)$ 这个函数分成三段:我们这个 $f_i(x)$ 的左边是斜率小于 $0$ 的中间是斜率等于 $0$ 的右边是斜率大于 $0$ 的

可以发现如果 $x$ 越靠近中间那一段,$f_{i-1}(x)$ 取值就越小

设 $L$ 和 $R$ 分别为 $f_{i-1}(x)$ 中间一段的左端点和右端点,于是我们可以用上面的性质将 $f_i(x)$ 的取值写成如下这种式子:

$$f_i(x)=|x-l_i|+
\begin{cases}
f_{i-1}(x+len_i) &  & x \in   (-\infty,L-len_i) \\
f_{i-1}(L) &  & x \in   [L-len_i,R+len_{i-1}] \\
f_{i-1}(x-len_{i-1}) &  & x \in   (R+len_{i-1},\infty) 
\end{cases}$$

先不考虑绝对值函数,只看后面的大括号

通过这个取值范围,可以明显的发现每次相当于将 $f_{i-1}$ 左边往左移了 $len_i$ ,右边往右移了 $len_{i-1}$ ,中间 $L$ 变成了 $L-len_i$ ,$R$ 变成了 $R+len_{i-1}$

于是可以用两个标记,一个标记左边向做平移了多少,一个标记右边向右平移了多少,中间的两个端点其实就是左边的最右边那个点和右边的最左边那个点,可以不用特殊处理了


至此,我们只需要考虑绝对值如何处理了

很容易可以看出绝对值函数只跟 $l_i$ 的位置有关(因为斜率始终为左边 $-1$ ,右边 $+1$ ,这里要注意,这个性质等会要用到)

可以考虑再分三种情况讨论(没错,还要分类讨论):

1、 $l_i$ 在中间,那么 $L=R=l_i$ ,$l_i$ 左边斜率 $-1$ ,右边斜率 $+1$(因为只有 $l_i$ 这个位置是 $0$ ,且原本在的位置就是取值最小的)

2、 $l_i$ 在左边,那么首先 $l_i$ 左边的斜率 $-1$ ,右边斜率 $+1$ ,但是 $L$ 和 $R$ 分别在哪?

  因为 $l_i$ 在左边,所以 $l_i$ 往右走斜率大于等于 $l_i$ 所在的这一段的斜率,所以中间变成了原本斜率为 $-1$ 的地方,原本的中间那一段就加到了右边(注意如果 $l_i$ 在 $-1$ 这一段,那么 $l_i$ 以左(到这一段的左端点)斜率变为 $-2$ ,以右(到这一段的右端点)斜率变为 $0$ ,但事实上实现时不用讨论这些情况,如何维护这些东西下面再谈)

 3、$l_i$ 在右边,其实和 $l_i$ 在左边情况差不太多,反过来一下就可以了


 现在思考如何具体维护,发现维护许多线段是很复杂且困难的,但是其实每次相邻线段斜率只会相差 $1$

(其实在加完绝对值后会出现相差为 $2$ 甚至更大的情况,但是其实这个可以看成这两个线段之间有几个长度为 $0$ 的线段,这些长度为 $0$ 的线段就很灵活的处理了这个问题(这里真的很妙啊))

于是我们就只需要存下这些线段的端点,这样每条线段的端点和斜率都可以通过这些点算出来(斜率就是在这条线段前的线段数也就是点数)

发现我们需要支持的操作有插入一些点,询问最大值和最小值和删除最大值和最小值(左边和右边),于是可以直接用堆来维护(貌似这是个套路?)


 但还有一个问题(真的是最后一个了 QAQ):统计答案

我们发现最后 $f_n(x)$ 中间一段的纵坐标(也就是最低点的纵坐标)就是答案,但是我们发现并不怎么好直接得到,考虑每次转移时计算

设 $ans_i$ 为 $f_i(x)$ 的中间一段的纵坐标方便表示

那么一共会有三种情况:

1、 $l_i$ 在 $f_{i-1}(x)$ 的中间,那么最低点的纵坐标仍然没有改变(因为中间一段的纵坐标相同),所以 $ans_i=ans_{i-1}$

2、 $l_i$ 在 $f_{i-1}(x)$ 的左边,那么是原本斜率为 $-1$ 的一段变成了 $f_i(x)$ 的中间,由于斜率同为 $0$ 的一段纵坐标都相等,那么我们用最好算的点来计算,也就是 $L$ 点(因为 $L$ 是 $f_{i-1}(x)$ 的中间的左端点,也就是斜率为 $-1$ 的右端点),那么由于 $f_{i-1}(x)$ 是加上 $|l_i-x|$ ,那么 $ans_i=ans_{i-1}+(L-l_i)$

3、 $l_i$ 在 $f_{i-1}(x)$ 的右边,和第 2 种情况差不多,所以转移为 $ans_i=ans_{i-1}+(l_i-R)$

可以发现这里的讨论和在上面讨论绝对值函数差不多可以顺便处理答案


 具体实现可以看一下代码(看似需要讨论的情况很多,但其实用堆可以去掉许多情况的讨论,所以码量可能比暴力还短,细节也少的离谱/fad)

$code$ :

#include<cstdio>
#include<cctype>
#include<queue>

using namespace std;

#define maxn 101101

template<class T>

inline T read(){
    T r=0,f=0;
    char c;
    while(!isdigit(c=getchar()))f|=(c=='-');
    while(isdigit(c))r=(r<<1)+(r<<3)+(c^48),c=getchar();
    return f?-r:r;
}

int n,l[maxn],r[maxn],len[maxn];

#define int long long

int addl,addr,ans;

priority_queue<int>ql;

priority_queue<int,vector<int>,greater<int> >qr;

#undef int

int main(){
    n=read<int>();
    for(int i=1;i<=n;i++){
        l[i]=read<int>();
        r[i]=read<int>();
        len[i]=r[i]-l[i];
    }
    ql.push(l[1]);
    qr.push(l[1]);//一开始第一次斜率为0的地方肯定是l[1]
    for(int i=2;i<=n;i++){//所以这里从2开始
        addl-=len[i],addr+=len[i-1];//打标记来处理平移
        long long L=ql.top()+addl;//记得加上标记
        long long R=qr.top()+addr;//中间的左右端点
        if(l[i]<L){//绝对值的顶点在左边
            ans+=L-l[i];//最近的显然是这个
            ql.pop();//中间的左端点不再是中间的左端点了
            ql.push(l[i]-addl);//注意这里这个点要加2次,因为l[i]的左边的斜率跟
            ql.push(l[i]-addl);//它右边的斜率相差为2了,于是要插入一条长度为0的线段
            qr.push(L-addr);//将原本中间的左端点加到右边去
        }
        else if(l[i]>R){//在右边
            ans+=l[i]-R;//这个同上
            qr.pop();//同上
            qr.push(l[i]-addr);//同上
            qr.push(l[i]-addr);
            ql.push(R-addl);//同上
        }
        else {//如果本来就在中间,那么答案肯定不变
            ql.push(l[i]-addl);//左边的最右边端点变成l[i]
            qr.push(l[i]-addr);//同理
        }
    }
    printf("%lld\n",ans);
    return 0;
}
posted @ 2020-11-25 21:21  一叶知秋‘  阅读(1197)  评论(2编辑  收藏  举报