单调队列与决策单调性优化 DP
单调队列
P1440 求m区间内的最小值
\(O(n\log m)\) 是随便做的,怎么做到 \(O(n)\) ?
我们打开思路,从更加一般化的角度来考虑问题.
现在我们要求序列中的一些值,满足两维限制:其中一维限制是下标在一个向右移动长度确定的区间中,另一维限制是区间最值.
观察一下这两维限制有什么性质,注意序列下标存在单调性,即一个区间向右移动时下标是单调递增的;同时一段中的最大值只会被后面新加入的值更新. 这启示我们:对于 \(j < i\),若 \(a_j \ge a_i\) 那么 \(a_j\) 一定是不会成为最小值的. 对于剩下 \(a_j < a_i\) 则是可能成为最大值的,我们称这些 \(j\) 为决策点. 我们发现随着 \(a_i\) 不断加入,新的决策点一定是递增的,这样就可以手写一个双端队列存下所有决策点,再进行一系列操作来维护最大值.
设队列 \(q\) 有队头 \(h\),队尾 \(t\). 我们尝试往队尾加入决策 \(i\),把队尾无用的决策全部弹掉. 这样操作之后会发现队列中的 \(a_j\) 是单调递增的,因为之前所有的 \(a_t<a_i\). 这样我们再弹出队头,直到队列中过时的决策点全部弹出,这时候的队头就是答案.
事实上,这里的两维限制,一维用来维护队尾单调,一维用来维护队头单调,这就是单调队列.
代码实现
for(int i = 1, x, h = 1, t = 0; i <= n; i++) {
while(h <= t && q[h].p < i - m) h++;
if(t - h >= 0) cout << q[h].v << endl; else cout << 0 << endl;
cin >> x; while(h <= t && x <= q[t].v) t--; q[++t] = new_Node(i, x);
}
决策单调性
定义
决策单调性即决策点具有单调性,通常是下标单调递增.
更一般的,一个二元函数 \(w(x,i)\) 在 \(w(p_i,i)\) 时最优则 \(p_i\) 为决策点,那么对于 \(j<i\),决策单调性可以定义为:
要证明某函数 \(w(x,y)\) 满足决策单调性,即证明:
一旦 \(w(x,y)\) 满足决策单调性,我们就可以使用单调队列来维护决策点.
二分队列
二分队列要根据决策单调性来维护队头 \(h\) 和队尾 \(t\),队列里面存三元组 \((l_j,r_j,p_j)\) 表示 \([l_j,r_j]\) 的最优决策是 \(p_j\). 容易发现队列中的所有 \((l_j,r_j)\) 将 \([1,n]\) 划分开. 现在我们要找到 \(i\) 的最优决策点,并且根据 \(i\) 去更新后面的最优决策点.
\(i\) 的最优决策点在之前已经更新过了,所以我们从队头开始,把所有 \(r_h < i\) 的三元组弹出,因为其不以 \(i\) 作为最优决策,根据决策单调性也不会作为 \([i+1,n]\) 的最优决策. 直到 \(i\) 在 \([l_h,r_h]\) 之间,那么我们直接取出 \(p_h\) 来求 \(i\) 的答案.
现在我们考虑怎么找到后面的一个区间以 \(i\) 为最优决策. 根据决策单调性,其一定对应最末的区间,也就是找队尾 \((l_{t_0},n,i)\) 中的 \(l_{t_0}\),这意味着我们可能要先舍去队尾的一些三元组. 我们找出 \(p_t\) 和 \(i\) 决策的转折点 \(x\),满足 \(x'<x\) 时 \(w(p_{t},x')\) 优于 \(w(i,x')\),\(x'>x\) 时 \(w(i,x')\) 优于 \(w(p_{t},x')\). 当 \(l_t\ge x\) 时,\(i\) 会作为 \([l_t,r_t]\) 完整区间的决策点,但是转折点 \(l_{t_0}\) 并不在区间中,所以弹出队尾. 直到第一次出现 \(x>l_t\),\(x\) 就是我们要找的 \(l_{t_0}\),此时将其加入队尾.
实现时实际上不用存三元组中的 \(r\),只需要存一下决策转折点 \(l\) 就足够了.
二分队列与单调队列的区别体现在二分队列是维护的函数交点的单调,而单调队列直接维护的下标单调,少套了一层映射关系. 所以实际应用时要根据题目具体分析.
例题
P5665 [CSP-S2019] 划分
Hint:考虑决策单调性,不是所有决策单调性都要二分队列.
对序列分段要求平方和最小,首先由 \(x^2+y^2<(x+y)^2\) 得知段数越多答案越优. 然后由于每一段之和是单调不降的,并且序列前缀和也是单调递增的,所以就得到一个事实:决策点越靠前越优.
P6563 [SBCOI2020] 一直在你身旁
Hint:区间 DP,考虑随决策点移动 \(f_{i,j}\) 的单调性.
考虑对于任意一个区间怎么才能知道答案,设 \(f_{i,j}\) 表示区间 \([i,j]\) 最少要花费多少钱才能确定位置,不难想到边界 \(f_{i,i}=0,f_{i,i+1}=a_i\). 考虑对于更大的区间,枚举区间中购买的电线的位置 \(k\),由于需要保证知道所需电线的位置,所以应该在 \(f_{i,k},f_{k+1,j}\) 中取较大值. 有转移:
直接转移是 \(O(n^3)\) 的,考虑优化这个东西. 一个重要的事实是 \(f_{i,k+1}\) 一定不会比 \(f_{i,k}\) 小,也就是 \(f_{i,x}\) 是单调不降的,同理 \(f_{x,j}\) 也是单调不增的. 那么最小值我们就可以有更优化的思路来解决了:单调队列.
分类讨论:
- 对于 \(f_{i,k}>f_{k+1,j}\) 的情况,答案为 \(f_{i,k}\) 的最大值,也就是当前最大的 \(k\) 对应的 \(f_{i,k}\) 的值.
- 对于 \(f_{i,k}\le f_{k+1,j}\) 的情况,答案为 \(f_{k+1,j}\) 的最大值,可以用单调队列维护随着 \(k\) 减小决策点的变化,因为决策点显然单调递减.
所以我们可以先找到 \(f_{i,k} > f_{k+1,j}\) 的第一个 \(k\). 然后根据决策单调性弹出过时的大于等于 \(k\) 的决策,并且弹出比当前决策更劣的决策,再用 \(i\) 更新决策即可.
所以这个题本质上是指针 + 单调队列优化区间 DP.
代码实现
for(int j = 2; j <= n; j++) {
int k = j, h = 1, t = 2; q[1] = j;
for(int i = j; i >= 1; i--) {
if(i == j) {f[i][j] = 0; continue;} if(i + 1 == j) {f[i][j] = a[i]; continue;}
while(i < k && f[i][k - 1] > f[k][j]) k--; f[i][j] = f[i][k] + a[k];
while(h < t && q[h] >= k) h++;
if(h < t) f[i][j] = min(f[i][j], f[q[h] + 1][j] + a[q[h]]);//h < t 说明有可用决策,拿出来更新.
while(h < t && f[q[t - 1] + 1][j] + a[q[t - 1]] >= f[i + 1][j] + a[i]) t--; q[t++] = i;// 维护最大值,单调队列递减
}
}
P1912 [NOI2009] 诗人小G
题意大概是给定一些诗句,给定标准长度 \(L\),现在可以给诗句分行,求使每行诗句长度与标准长度差的绝对值的 \(P\) 次方的最小值.
对诗句长度做前缀(加上空格),容易得到转移方程:
定义 \(w(j,i)=\big| s_i-s_j-L-1\big|^P\),要转移到 \(i\) 就要找最优的决策 \(j\) 使得 \(w(j,i)\) 最小.
现在我们要证明 \(w(j,i)\) 具有决策单调性. 回忆一下上面的式子,即证明:\(f_{p_i}+w(p_i,i)\le f_{p_j}+w(p_j,i)\wedge f_{p_j}+w(p_j,j)\le f_{p_i}+w(p_i,j)\)
两式相加,得到
假设成立有 \(p_j<p_i<j<i\),即证明
这个式子也被称为四边形不等式. 证明四边形不等式大概就是从特殊的情形(相差 \(1\)),数规推广到一般. 最后的结论是:当 \(g(x)=|x|^P-|x+c|^P(c为常数)\) 单调不增时上式成立. 利用导数简单证明即可.
所以直接二分队列就好了,复杂度 \(O(n\log n)\).
虽然思路是简单的,但是二分边界还有输出方案的细节很多,调的时候需要仔细.
代码实现
#include<bits/stdc++.h>
using namespace std;
typedef long double ld;
typedef long long ll;
const int maxn = 1e5 + 10; const ld inff = 1e18;
int T, n, L, P;
char s[maxn][33];
ld sum[maxn], f[maxn];
struct Node{int x, p;} q[maxn];
inline ld qpow(ld x, int y) {
ld res = 1;
while(y) {
if(y & 1) res *= x;
x *= x, y >>= 1;
} return res;
}
inline ld W(int j, int i) {return f[j] + qpow(abs(sum[i] - sum[j] - 1 - L), P);}
inline int bound(int a, int b) {
int l = a, r = n + 1;
while(l < r) {
int mid = l + r >> 1;
if(W(a, mid) >= W(b, mid)) r = mid;
else l = mid + 1;
} return l;
}
int tmp1[maxn], tmp2[maxn];
void solve() {
cin >> n >> L >> P;
for(int i = 1; i <= n; i++) cin >> s[i], sum[i] = sum[i - 1] + strlen(s[i]) + 1;
for(int i = 1, h = 1, t = 1; i <= n; i++) {
while(h < t && q[h].x <= i) h++;
f[i] = W(q[h].p, i), tmp1[i] = q[h].p;
while(h < t && q[t - 1].x >= bound(q[t].p, i)) t--;
q[t].x = bound(q[t].p, i), q[++t].p = i;
}
if(f[n] > inff) {cout << "Too hard to arrange\n";}
else {
cout << (ll)(f[n] + 0.5) << "\n";
int cnt, i; for(tmp2[cnt = 0] = i = n; i; tmp2[++cnt] = i = tmp1[i]);
for(; cnt; cnt--) {
for(i = tmp2[cnt] + 1; i <= tmp2[cnt - 1]; i++) cout << s[i] << (i == tmp2[cnt - 1] ? "\n" : " ");
}
}
return cout << "--------------------" << (T == 0 ? "" : "\n"), void(0);
}
void clr() {
memset(q, 0, sizeof q);
for(int i = 1; i <= n; i++) f[i] = 0;
return;
}
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> T;
while(T--) solve(), clr();
return 0;
}
P3515 [POI 2011] Lightning Conductor
Hint:决策单调性,二分队列.
主要讲讲怎么判断函数是否具有决策单调性. 对于每个位置 \(i\),考虑 \(k_i\),容易写出式子:
\(h_i\) 视为常数,实际上就是对每个 \(i\) 求:
如何分析这个函数?注意到根号内单增,于是把它当成自变量,上式变为:
\(h\) 是确定的截距,随便画画图会发现随着 \(x\) 增大相邻两函数要么没有交点,要么交点单增,显然满足决策单调性. 于是直接二分队列即可.

浙公网安备 33010602011771号