洛谷题目页面传送门
题意见洛谷里的翻译。(用(a,b,c)分别表示(A,B,C))
直接考虑DP。设(dp_i)表示最后一次坐的是列车(i)时的最小烦躁值。显然,边界是(dp_0=0)(同时假装(y_0=1)),目标是(minlimits_{y_i=n}{q_i+dp_i})。
考虑转移。可转移性显然,因为(2)个状态不可能互为决策,即决策->状态构成的有向图一定是DAG。于是状态转移方程显然为
直接转移是(mathrm O!left(n^2 ight))的,考虑优化。容易想到的是,我们可以按(y)值建桶,这样每个状态的决策仅在(1)个桶内。
接下来,这个转移方程是一个非常经典的斜率优化模型,上手直接干:
对于(2)个决策(j,k),(j)比(k)优当且仅当
化简得
将仅关于决策变量的项放左边,既关于状态变量又关于决策变量的项放右边,得
要除过来了,可是我们还没确定(q_j-q_k)的符号。于是我们强行令(q_jleq q_k)。先考虑(q_j<q_k),此时(q_j-q_k<0),除过去,变不等号方向,得
于是左边可以抽象成在以(q_i)为点(i)的横坐标、以(dp_i+aq_i^2-bq_i)为点(i)的纵坐标的直角坐标系中,过点(j)与点(k)的直线的斜率。我们设这个斜率为(f(j,k))。那么问题来了,(q_j=q_k)即(q_j-q_k=0)时没有斜率怎么办呢?我们可以按照它与(2ap_i)比较之后必须得出正确的优劣关系的原则,规定当(q_j=q_k)时(f(j,k)=egin{cases}+infty&dp_j<dp_k\0&dp_j=dp_k\-infty&dp_j>dp_kend{cases})。
然后套路化地发现,若(3)个决策(i<j<k)满足(f(i,j)geq f(j,k)),分(2)种情况:
- (f(i,j)>2ap_i):此时(i)比(j)优;
- (f(i,j)leq 2ap_i):结合(f(i,j)geq f(j,k))可得(f(j,k)leq 2ap_i),此时(j)没有(k)优。
综上,(j)一定不是最优决策,于是可以把它扔掉,即维护一个斜率严格单调上升的下凸壳。由于有(n)个桶作为决策栖息地,我们维护每个桶内的下凸壳,然后计算DP值时在对应的桶内的下凸壳内找即可。
接下来考虑DP顺序。显而易见的有(2)种:按(p)值升序和按(q)值升序。两种正确性都很好证,以(p)值升序为例,对于任意(i<j),有(p_ileq p_j),又(p_j<q_j),所以(q_j>p_i),所以(i)不可能转移到(j),得证。
先考虑(q)值升序。此时每个下凸壳可以单调栈(stk)维护。每计算完一个DP值,由于横坐标非严格单调上升,可以直接在栈顶加点并维护单调性(好吧其实并不能,还是得像下面那样two-pointers,因为转移方程(min)里有(q_jleq p_i)的限制)。计算的时候嘛,就只能二分查找第一个满足(f(stk_j,stk_{j+1})>2ap_i)的(stk_j)作为最优决策了(正确性显然)。单DP的时间复杂度就(mathrm O(mlog m))。
再来考虑(p)值升序。(2ap_i)显然也非严格单调递增,可以将单调栈改进为单调队列。但是这样刚计算完DP值并不能直接加进单调队列尾,因为不能保证没有其他横坐标更小的还没计算。所以应该对于每个下凸壳维护two-pointers,计算每个DP值时,在决策所在下凸壳的单调队列尾一连按(q)值升序添加好几个点。DP时间复杂度(mathrm O(m))。但是按(p)值、(q)值排序什么的还是带(log)啊,这么一来费尽心思去DP的(log)不是没用了吗?别急,可以桶排。时间复杂度(mathrm O!left(n+m+maxlimits_{i=1}^n{q_i} ight))。
什么?你问我到底用哪种DP顺序?当然用下面那种复杂度小的啦
另外,这题比较卡常,我O3、快读、改deque
为vector
模拟队列三连才AC。
代码:
#pragma GCC optimize(3)
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define y0 afejaogoea
#define pb push_back
const int inf=0x3f3f3f3f3f3f3f3f;
const double dinf=1e100;
void read(int &x){
x=0;char c=getchar();
while(!isdigit(c))c=getchar();
while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
}
int sq(int x){return x*x;}
const int N=100000,M=1000000,Q_I=40000;
int n,m,a,b,c;
int x[M+1],y[M+1],p[M+1],q[M+1];
vector<int> buc_y[N+1],buc_p[Q_I+1],buc_q[Q_I+1];//桶们
int now[N+1];//two-pointers变量
vector<int> que[N+1];//vector模拟队列
int head[N+1],tail[N+1];//vector模拟队列
int dp[M+1];//DP数组
double f(int j,int k){//斜率
int y_j=dp[j]+a*sq(q[j])-b*q[j],y_k=dp[k]+a*sq(q[k])-b*q[k];
if(q[j]==q[k])return y_j<y_k?dinf:y_j==y_k?0:-dinf;
return 1.*(y_j-y_k)/(q[j]-q[k]);
}
signed main(){
// freopen("C:\Users\chenx\OneDrive\桌面\txt.txt","r",stdin);
read(n);read(m);read(a);read(b);read(c);
for(int i=1;i<=m;i++)read(x[i]),read(y[i]),read(p[i]),read(q[i]),buc_p[p[i]].pb(i)/*桶排*/,buc_q[q[i]].pb(i)/*桶排*/;
buc_y[1].pb(0);
for(int i=0;i<=Q_I;i++)for(int j=0;j<buc_q[i].size();j++)buc_y[y[buc_q[i][j]]].pb(buc_q[i][j]);//装桶
for(int i=1;i<=n;i++)que[i].resize(buc_y[i].size());
memset(now,-1,sizeof(now));
for(int i=0;i<=Q_I;i++)for(int j=0;j<buc_p[i].size();j++){//转移
int x0=buc_p[i][j];
while(now[x[x0]]+1<buc_y[x[x0]].size()&&q[buc_y[x[x0]][now[x[x0]]+1]]<=p[x0]){//一连往队尾加好几个点
int y0=buc_y[x[x0]][++now[x[x0]]];
while(head[x[x0]]+1<tail[x[x0]]&&f(que[x[x0]][tail[x[x0]]-2],que[x[x0]][tail[x[x0]]-1])>=f(que[x[x0]][tail[x[x0]]-1],y0))tail[x[x0]]--;//维护斜率单调性
que[x[x0]][tail[x[x0]]++]=y0;
}
while(head[x[x0]]+1<tail[x[x0]]&&f(que[x[x0]][head[x[x0]]],que[x[x0]][head[x[x0]]+1])<2*a*p[x0])head[x[x0]]++;//从队首弹出
if(head[x[x0]]<tail[x[x0]]){
int y0=que[x[x0]][head[x[x0]]];
dp[x0]=dp[y0]+a*sq(p[x0]-q[y0])+b*(p[x0]-q[y0])+c;
}
else dp[x0]=inf;//计算DP值
}
int ans=inf;
for(int i=0;i<buc_y[n].size();i++)ans=min(ans,q[buc_y[n][i]]+dp[buc_y[n][i]]);//目标
cout<<ans;
return 0;
}