斜率优化DP
斜率优化DP
主要内容
形如这样的 \(\text{DP}\) 转移方程:
满足:
- \({L_i}\),\({R_i}\) 递增(前提条件)。
- \(R_i≤i\)(转移条件)。
- \(val(i,j)\) 与 \(i\) 和 \(j\) 相关(若只与 \(j\) 有关可以用单调队列优化DP)。
第一种理解方式
我们考虑将其变形:(先假设方程为 \(dp(i)=\max_{L_i\le j\le R_i}\{dp(h)+i\times j\}\))
直接拆掉 \(\max\):
即:
-
将只与 \(j\) 有关的放在左边作为 \([y]\)。
-
与 \(i,j\) 都有关的放在中间做一次项,其中 \(i\) 做系数而 \(j\) 做变量。
-
将只与 \(i\) 有关的放在右边做常数。
当转移的时候,每一个决策点 \(j\) 可以看做一个个单点,分别在坐标轴的 \(([x_j],[y_j])\)(定义如上)的位置;决策时,就用斜率为 \([k_i]\) 的直线向下(求最大值)或向上(求最小值)去和决策点相交,交到的第一个点就是最优决策点。
上面的“相交”过程可以用二分等解决。
转移完毕了,我们要将 \(i\) 变为一个决策点以便以后的转移,将 \(i\) 放到 \(([x_i],[y_i])\) 上去,和当前决策队列末尾的两个点观察形成上凸或下凸,根据题目要求弹出队尾。
斜率优化就是通过这样转移实现每个点最多进队一次、出对一次的优化。
第二种理解方式
在学习李超线段树的时候,其实可以将原先的式子直接放在那里,即:
我们将 \(j\) 当做斜率,把与 \(j\) 相关的看做一条直线(注意与上面第一种相反,这里 \(j\) 是直线)。
统计答案:\(i\) 的答案就是直线 \(x=i\) 与所有直线交点的最高 \(/\) 最低值。
加入候选项:假设 \(i\) 的答案可以提供给(注意不是捞取,而是供给)\([L_i,R_i]\) 区间内的点,那么将 \(i\) 的直线变为线段,加入到值域为 \([L_i,R_i]\) 的区域即可。
可以用李超线段树维护,复杂度 \(\mathcal{O(n\log^2n)}\)。
例题:
P4072 [SDOI2016]征途
非常裸的斜率优化,但是要注意值域较大,应该先将式子化简,减小运算的大小,防止运算过程中爆 \(\texttt{long}~\texttt{long}\) !
P2120 [ZJOI2007]仓库建设
需要注意的是,这道题可能不用将 \(n\) 的元素全部分配玩(因为末尾可能有连续的 \(0\) 使不用在 \(n\) 处再分一段)。
因此我们需要再输出答案之前将答案与末尾所有连续非 \(0\) 的取 \(\min\) ,保证取到最大值。
取 \(\min\) 代码:
ans=dp[n];
for(int i=n;i>=1;i--)
if(!p[i+1]) ans=minll(ans,dp[i]);
else break;
printf("%lld\n",ans);
P5785 [SDOI2012]任务安排
这个题的 \([k]\) 并不单调,但是好在 \([x]\) 是单调的,所以我们可以维护队尾,用二分的方式找到交点。
P6302 [NOI2019] 回家路线
给定 \(n\) 个点,从 \(1\) 走到 \(n\)。其中有 \(m\) 条固定时间的路径,第 \(i\) 条路径从 \(u_i\) 到 \(v_i\),在 \(st_i\) 时刻出发,在 \(ed_i\) 时刻到达。每次等车 \(t\) 的时间会增加 \(A\times t^2+B\times t+C\) 的花费,最后花费加上一个到达时间。使花费最小。
\(n\le 10^5;m\le 10^6;p_i,q_i\le 10^3\)
$\texttt{solution}$
首先可以想到一个 \(O(Tm)\) 的暴力 dp:设 \(dp_{i,s}\) 表示在第 \(s\) 秒到达第 \(i\) 个点的最小代价,可以枚举每一条边转移。
发现如果同时记录地点与时间单说空间复杂度就爆炸,考虑更改状态。
一个高妙的想法是:以一条条航班的编号为状态,\(dp_i\) 表示在经过第 \(i\) 条航线并到达 \(ed_i\) 时的最小代价,这样用一维变量同时概括了地点与时间两个信息。
可以列出转移方程:
发现这是一个关于 \(i\) 与 \(j\) 的转移方程,可以斜率优化(去掉 \(\min\) 后移项):
关于按照 \(st_i\) 升序排序(让 \([k]\) 递增)还是按照 \(ed_i\) 排序(让 \([x]\) 递增),我自己在第一次做的时候没有想清楚,导致了长时间的卡顿,下面解释一下 \(ed_i\) 排序的错误之处:
在按照 \(ed_i\) 排序之后,我为了满足 \(ed_j\le st_i\) 的限制,在单调队列上二分出了 \(ed_j\le st_i\) 的最右端(记为 \(pos\)),并在 \([L,pos]\) 中再一次二分求出最佳决策点。
但最关键的是,单调队列中的所有决策点都是经过筛选的。假设存在决策点 \(j(ed_j\le st_i)\),对于 \(st_i\) 来说是更优,却在加入另一个决策点时被舍去了。
比如上面的图,会选择 \(p\) 作为决策,\(j\) 会被舍去。
还有一个注意点就是:在加入决策的时候,不能每次直接将 \(i\) 加入队列中,而是可以将它加入一个堆中。每次需要计算时再将堆中的决策点放到单调队列中。
总时间复杂度:\(O(m\log m)\)。
$\texttt{code}$
#define Maxm 1000005
#define pa pair<int,int>
#define fi first
#define se second
typedef long long ll;
int n,m;
ll ans=infll,A,B,C,dp[Maxm],x[Maxm],y[Maxm],k[Maxm];
struct Way { int u,v; ll st,ed; }r[Maxm];
bool cmp(Way x,Way y){ return x.st<y.st; }
inline ll calc(int x) { return x*x*A+x*B+C; }
struct Destination
{
int nl=0; // 由于 vector 不支持 pop_front 操作,我用一个指针来表示起始位置
vector<int> q;
priority_queue<pa> qpre;
inline void Insert(int i,ll X,ll Y)
{
int nr;
while((nr=q.size())-nl>=2 && (Y-y[q[nr-1]])*(x[q[nr-1]]-x[q[nr-2]])
<=(y[q[nr-1]]-y[q[nr-2]])*(X-x[q[nr-1]])) q.pop_back(); // 队尾踢出凹凸性错误的元素
q.push_back(i);
}
inline void Delete(int S,ll K)
{
int tmp;
while(!qpre.empty() && -qpre.top().fi<=S) // 将堆中元素加入单调队列中
tmp=(int)qpre.top().se,Insert(tmp,x[tmp],y[tmp]),qpre.pop();
while(q.size()-nl>=2 && (y[q[nl+1]]-y[q[nl]])<=
K*(x[q[nl+1]]-x[q[nl]])) nl++; // 队首踢出决策较劣的元素
}
}pos[Maxm];
int main()
{
n=rd(),m=rd(),A=rd(),B=rd(),C=rd();
for(int i=1;i<=m;i++) r[i]=(Way){rd(),rd(),rd(),rd()};
sort(r+1,r+m+1,cmp),pos[1].q.push_back(0);
memset(dp,0x3f,sizeof(dp)),dp[0]=0;
for(int i=1,U,V,tmp;i<=m;i++)
{
x[i]=r[i].ed,k[i]=2ll*A*r[i].st,U=r[i].u,V=r[i].v;
pos[U].Delete(r[i].st,k[i]);
if((int)pos[U].q.size()<=pos[U].nl) continue; // 没有决策点
tmp=pos[U].q[pos[U].nl];
dp[i]=dp[tmp]+calc(r[i].st-r[tmp].ed)+r[i].ed-r[tmp].ed;
y[i]=dp[i]+A*r[i].ed*r[i].ed-B*r[i].ed-r[i].ed;
pos[V].qpre.push(pa(-r[i].ed,i));
}
for(int i=1;i<=m;i++) if(r[i].v==n) ans=minll(ans,dp[i]);
printf("%lld\n",ans);
return 0;
}