斜率技巧
务必结合阅读 Slope Trick · USACO Guide。
请你先完成该问题 \(n\le 5\times 10^3\) 的版本。
写出一个 dp 很容易。\(f_{i,j}=\max(f_{i-1,j-1}-p_i,f_{i-1,j},f_{i-1,j+1}+p_i)\ (j\in[0,i])\)。存在简单的变形:令 \(ans=-\sum\limits_{i=1}^n p_i\),转移修改为 \(f_{i,j}=\max(f_{i-1,j-1},f_{i-1,j}+p_i,f_{i-1,j+1}+2p_i)\),答案变成 \(ans+f_{n,0}\)。
定义 \(f_i\) 在 \(j\) 处的斜率为 \(d_{i,j}=f_{i,j}-f_{i,j+1}\),我们可以归纳证明 \(d_i\) 是递增的,即 \(f_i\) 凹。先继续阅读。
将转移分解为:
-
把 \(f_{i-1}\) 视为一个定义域为 \([0,i-1]\) 的函数,右移一位成为新的 \(f_i\),注意 \(f_{i,0},d_{i,0}\) 此时视为 \(-\infty\)。
-
对 \(j\in [0,i-1]\),同时执行 \(f_{i,j}\overset\max\gets f_{i,j+1}+p_i\)(等价于同时判定若 \(d_{i,j}< p_i\),执行 \(f_{i,j}\gets f_{i,j+1}+p_i\))两轮。
因斜率递增可以见得:若 \(k\) 为 \([0,i-1]\) 中最后一个满足 \(d_{i,k}< p_i\) 的位置,应对 \(j\in[0,k]\) 依次执行 \(f_{i,j}\gets f_{i,j+1}+p_i\)。考察对 \(d\) 的影响:对 \(j\in[0,k-1]\) 同时执行 \(d_{i,j}\gets d_{i,j+1}\),然后令 \(d_{i,k}\gets p_i\)。即在 \(d_i\) 这一有序数组中插入 \(p_i\)。
若用堆 \(q\) 维护 \(d_i\),\(2\) 类转移即在 \(q\) 中插入 \(p_i\) 两次。由于堆中没有下标限制,两次转移后 \(d_{i,-1}\) 被定义,此时应删去 \(q\) 中最小值。
考虑 \(ans\) 实际值为 \(f_{n,n}\),将 \(q\) 中值全部累加即对斜率求后缀和,得到 \(f_{n,0}\)。
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=3e5+10;
int n,p;
priority_queue<int,vector<int>,greater<int>>q;
LL ans;
signed main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&p);
ans-=p;
q.push(p);
q.push(p);
q.pop();
}
for(int i=1;i<=n;i++)ans+=q.top(),q.pop();
printf("%lld\n",ans);
return 0;
}
在此我们已对该优化思想建立一定认知:借助 dp 数组的凹凸性即其差分数组的单调性,将转移在维护差分数组的高效数据结构上演绎。
Arithmetic Exercise 题解——FFTotoro
(题外话:该题解体现了 dp 不仅可以构造操作以得到答案,也可以直接构造能够体现合法操作存在的答案)
这道题的转移与上题相似,有如下区别:第二维存在给出的上界 \(n\)、平移后转移不连续、答案是 \(f_{n,i}\)。该题解非常优秀,注意其代码的最后一步为求出斜率的最大前缀和。
关于凸性、暴力做法参见其它题解。
考虑构造答案。若 \(i\) 处最终有 \(a_i'\) 个肥料,那么操作次数最小值为 \(\sum\limits_{i=1}^{n-1}|\sum\limits_{j=1}^i a_j-a'_j|\),可以理解为拆贡献到每一个 \(i\to i+1\) 的间隙。此外要求 \(a'_i\ge b_i\)。
设 \(d_i=a_i-b_i,d'_i=a'_i-b_i\),\(s_i=\sum\limits_{j=1}^{i}d'_j-d_j\),\(f_{i,j}\) 表示已处理前 \(i\) 位且 \(s_i=j\) 时的最小代价。因 \(i\to i+1\) 处的贡献为 \(|j|\),要求 \(d'_i\ge 0\),使用前缀最小值优化后得到转移 \(f_{i,j}=\min(f_{i,j-1},f_{i-1,j+d_i}+|j|)\)。
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=3e3+10,M=3e4+10;
int n,a[N],b[N],f[2][M<<1],s,d[N];
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d%d",&a[i],&b[i]);
s+=a[i];
d[i]=a[i]-b[i];
}
assert(n<=3000&&s<=30000);
memset(f,0x3f,sizeof(f));
for(int i=M;i<=M+s;i++)f[i&1][i]=0;
for(int i=1;i<n;i++)
for(int j=M-s;j<=M+s;j++)
f[i&1][j]=min(f[i&1][j-1],abs(j-M)+f[i&1^1][min(M+s,max(M-s-1,j+d[i]))]);
printf("%d\n",f[n&1^1][min(M+s,max(M-s-1,M+d[n]))]);
return 0;
}
将转移分解为:
- 把 \(f_{i-1}\) 左移 \(d_i\) 位作为新的 \(f_i\)。
- 令 \(f_{i,j}\overset+\gets |j|\),即对函数 \(f_i\) 全局加 \(|x|\)。
- 取前缀 \(\min\)。
1 类转移通过将后续所有 2 类转移右移 \(d_i\) 位实现。设 \(D_i=\sum\limits_{j=1}^i d_j\) 即在 \(f_{i-1}\to f_i\) 的转移中将全局加 \(|x|\) 改为加 \(|x-D_i|\)。可以看到 \(f_i\) 是若干个绝对值函数的和函数再做一些前缀 \(\min\),最终仍为一个凹包。

以上是 \(f_i\) 的函数图像。黄色数字表示正在递减的斜率 \(f_{i,j}-f_{i,j+1}\)。右侧绿点是要求的 \(f_{n,D_n}\)。由于 \(D_n\ge 0\),不需要表示 \(f_{i,j<0}\),故左侧绿点表示 \(f_{i,0}\) 的值。红点表示斜率的变化点,设其点集为 \(q\),最终 \(f_{n,D_n}=f_{n,0}-\sum\limits_{x\in q}\min(x,D_n)\)。
全局加 \(|x-D_i|\),即将 \(D_i-1\) 及其左侧斜率 \(+1\),\(D_i\) 及其右侧斜率 \(-1\),加入两位于 \(D_i\) 的红点表示斜率变化 \(-2\) 实现。这时最右侧段斜率为 \(0\),此段末端及右侧斜率被视为 \(-1\),删除最右侧红点即实现维护前缀 \(\min\)。
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=5e5+10;
int n,a[N],b[N];
LL ans,d[N];
priority_queue<LL>q;
signed main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d%d",&a[i],&b[i]);
d[i]=a[i]-b[i]+d[i-1];
}
for(int i=1;i<n;i++){
if(d[i]<0)ans-=d[i],d[i]=0;
ans+=d[i];
q.push(d[i]);
q.push(d[i]);
q.pop();
}
while(q.size()){
ans-=min(d[n],q.top());
q.pop();
}
printf("%lld\n",ans);
return 0;
}
你必须注意,差分的方向是可以自由选定的。左减右、右减左没有本质区别。如下题即右减左。
毫不避讳地说,这篇题解——UltiMadow 几乎是完美的。
唯一的瑕疵在于过于感性地描述 4 个转移的得出,以及缺少一些堆操作的解释。

在此给出解释。你可以改一改第一篇题解给出的图片,然后把上图中黑色部分视为 \(|w-x+y|\),绿点视为 \(x\),选中它们并缓缓从左向右移动,跟随题解分类讨论。
当你确信自己理解了全部 4 个转移后,探讨实际的堆操作。
- \(f_v(x)+w\ (x<L)\) 是整体上移,不改变斜率。
- \(f_v(L)+w-x+L\ (x\in[L,L+w))\) 内部斜率明显为 \(-1\),\(L\) 处斜率为 \((f_v(L)+w-L+L)-(f_v(L-1)+w)=f_v(L)-f_v(L-1)\) 即不变。
- \(f_v(L)\ (x\in [L+w,R+w])\) 内部斜率为 \(0\),\(L+w\) 处斜率为 \(f_v(L)-[f_v(L)+w-(L+w-1)+L]=-1\)。
- \(f_v(R)+x-R-w\ (x\in (R+w,+\infty))\) 内部斜率为 \(1\),\(R+w+1\) 处斜率为 \(f_v(R)+R+w+1-R-w-f_v(L)=1\)。
删除 \(L\) 及其后的所有拐点导致 \(L+1\) 及其右侧所有斜率为 \(-1\)。插入 \(L+w\) 导致 \(L+w+1\) 及其右侧所有斜率为 \(0\),插入 \(R+w\) 导致 \(R+w+1\) 及其右侧所有斜率为 \(1\)。达成目的。
由于将 \(u\) 的 \(c\) 个子节点合并时,获取了 \(c\) 个形如 \(R+w\) 的拐点,对 \(u\) 的拐点堆弹出 \(c-1\) 次后堆顶为 \(R\),再弹出一次亦得到 \(L\)。
\(f_{1,0}=\sum w\),按照题目 3 应再对拐点坐标累减到 \(L\) 为止,得到凹包底部。
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=1e6+10;
int n,m,rt[N],p[N],w[N],l[N],r[N],d[N],idx;
LL ans,h[N];
mt19937 rnd(time(0));
int mrg(int x,int y){
if(!x||!y)return x+y;
if(h[x]<h[y])swap(x,y);
if(rnd()&1)swap(l[x],r[x]);
l[x]=mrg(l[x],y);
return x;
}
void pop(int &x){
x=mrg(l[x],r[x]);
}
int nw(LL x){
h[++idx]=x;
return idx;
}
signed main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=2;i<=n+m;i++){
scanf("%d%d",&p[i],&w[i]);
d[p[i]]++;
ans+=w[i];
}
for(int i=n+m;i>1;i--){
while(d[i]>1)d[i]--,pop(rt[i]);
LL R=h[rt[i]];pop(rt[i]);
LL L=h[rt[i]];pop(rt[i]);
rt[i]=mrg(rt[i],mrg(nw(L+w[i]),nw(R+w[i])));
rt[p[i]]=mrg(rt[p[i]],rt[i]);
}
while(d[1])d[1]--,pop(rt[1]);
while(rt[1])ans-=h[rt[1]],pop(rt[1]);
printf("%lld\n",ans);
return 0;
}
- 一道私题(X23632)

题解在此:

#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
#define mp make_pair
using namespace std;
const int N=2e5+10;
int n;
LL now;
pair<int,int>a[N];
priority_queue<int>q;
signed main(){
freopen("forge.in","r",stdin);
freopen("forge.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d%d",&a[i].fi,&a[i].se);
sort(a+1,a+n+1,[](auto u,auto v){return mp(u.se,-u.fi)<mp(v.se,-v.fi);});
// for(int i=1;i<=n;i++)printf("%d %d\n",a[i].fi,a[i].se);
q.push(-1e9-1);
for(int i=1;i<=n;i++){
int A=a[i].fi,B=a[i].se-a[i-1].se;
while(1){
if(B>-q.top())printf("%lld ",now-=q.top()),B+=q.top(),q.pop();
else{
int tmp=-q.top();q.pop();
now+=B;if(tmp!=1e9+1)tmp-=B;
q.push(-tmp);
break;
}
}
q.push(-A);
}
while(q.size()>1)printf("%lld ",now-=q.top()),q.pop();
puts("");
}

浙公网安备 33010602011771号