以前看到四边形不等式或叫决策单调性优化 dp,看到绕来绕去的式子和繁琐的证明总是望而却步。
数理基础简单打一下后再来看时发现,其实模型并不复杂,证明大多较为基础,故记此文加以巩固。
部分证明过程会以 Markdown 中引用的格式进行标注,笔者学艺不精还请指出错误及不足之处。
代码大部分是自己口胡出来的,可能写得乱了点。
四边形不等式
设函数 ,其中 整数,若对于任意整数 ,满足 ,都有 成立,则称 满足四边形不等式。
简记为:交叉小于等于包含。
定理一
若对于任意正整数 ,满足 ,都有 成立,则 满足四边形不等式。
我的记法是两步一起蹦跶 ()不如一步一步走远 (),有点像绕口令的感觉了wwwww。
当 时,由条件得 ,两式相加化简可得 ,以此类推,对于 都有 成立。
同理,对于所有 ,都有 成立。
1D1D 问题的优化
决策单调性:设 为 最优转移的位置(决策点), 单调不降即为具有决策单调性。
一般地,对于 的状态转移方程,若 满足四边形不等式,则 满足决策单调性。
值得注意的是,这里的 是指的关于 的函数,其有可能包括 dp 的 ,也有可能仅包括与 无关的其他 其他关于 的值,也有可能为常数,但不影响结论的正确性。 还是 也不影响。
已知 以及 满足四边形不等式。
求证 。
为了体现想出这个证明的思路,以下的过程表述并不严谨。
等价于 。
那么就是想办法把已知条件 中的 换成 , 换成。
由于 满足四边形不等式且 ,
所以 。
移项得 。
与已知中的不等式相加得到 。
故 。
单调队列 + 二分
考虑维护 数组,初始全为 ,从前往后计算完 时, 会更新一个后缀的 。
暴力做这个东西非常完蛋,考虑将 的一段段连续区间看成一个拿下来放到一个单调栈里,二分单调栈的位置,使得前半部分用 转移不比之前的 优,后半部分用 转移比之前的 更优,如果需要断开就断开,然后把后缀的都弹出来,压入新的这个后缀。
当然,dp 到 时 的 是不需要的,所以可以把单调栈改成单调队列。
每次最多只会压入一个点代表的区间,均摊下来,加上二分,时间复杂度是 的。
栗题一:Luogu P3515 [POI2011]Lightning Conductor
等价于 。求最小的 使得这个柿子成立,所以 ,先钦定 , 时反过来再做一遍就行。
把 从 中提出来,把 看成上文中的 ,也就是关于 的函数。
由于 dp 中为 ,所以需证明 满足四边形不等式,即可证明 的决策单调性,便可以决策单调性优化。
求证: 满足四边形不等式,即对于任意 均满足 即可。
注意到 是上凸的,所以由琴生不等式结论得证。
化简到这里,打个表也易知 成立。
综上所述, 满足四边形不等式。
实现上要注意用 double
来比较,根号不上取整而是直接开。
因为如果转移点 根号都上取整,在算答案的 小时大小可能会一样, 变大的时候大小会发生变化。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
typedef long long ll;
typedef double ld;
template <typename T> T Max(T x, T y) { return x > y ? x : y; }
template <typename T> T Min(T x, T y) { return x < y ? x : y; }
template <typename T> T Abs(T x) { return x < 0 ? -x : x; }
template <typename T>
T& read(T& r) {
r = 0; bool w = 0; char ch = getchar();
while(ch < '0' || ch > '9') w = ch == '-' ? 1 : 0, ch = getchar();
while(ch >= '0' && ch <= '9') r = r * 10 + (ch ^ 48), ch = getchar();
return r = w ? -r : r;
}
const int N = 500010;
int n, a[N], qh, qt;
ll f[N];
struct Node {
int l, r, p;
Node(int ll = 0, int rr = 0, int pp = 0) { l = ll; r = rr; p = pp; }
}q[N];
ld w(int x, int y) {
return a[x] - a[y] + std::sqrt(y-x);
}
void solve(int op) {
for(int i = 1; i <= n; ++i) q[i] = Node();
q[qh = qt = 1] = Node(1, n, 1);
for(int i = 1; i <= n; ++i) {
while(qh < qt && q[qh].r < i) ++qh;
if(!op) f[i] = Max(f[i], (ll)std::ceil(w(q[qh].p, i)));
else f[n-i+1] = Max(f[n-i+1], (ll)std::ceil(w(q[qh].p, i)));
if(q[qh].l == q[qh].r) ++qh;
else q[qh].l = i+1;
while(qh <= qt && w(q[qt].p, q[qt].l) < w(i, q[qt].l)) --qt, q[qt].r = q[qt+1].r;
if(qh > qt) q[++qt] = Node(i+1, n, i);
else {
int l = q[qt].l, r = q[qt].r, mid, t = 0;
while(l <= r) {
mid = (l + r) >> 1;
if(w(q[qt].p, mid) < w(i, mid)) t = mid, r = mid-1;
else l = mid+1;
}
if(t) {
if(t == q[qt].l) q[qt].p = i;
else ++qt, q[qt] = Node(t, q[qt-1].r, i), q[qt-1].r = t-1;
}
}
}
}
signed main() {
read(n);
for(int i = 1; i <= n; ++i) read(a[i]);
solve(0);
std::reverse(a + 1, a + n + 1);
solve(1);
for(int i = 1; i <= n; ++i) printf("%lld\n", f[i]);
return 0;
}
栗题二:[NOI2009] 诗人小G
设 为考虑前 个单词的最小代价,枚举 ,让 的单词分在同一行中。设 的前缀和为 ,可得:
为简化计算,将 都 ,得
设 ,证明 满足四边形不等式则可以决策单调性优化。
求证:对于任意 均满足 即可。
由于 ,设函数 ,其中 为 的常数,证明其单调不升即可。
分类讨论一下:
假如 为奇数,并且 :
化简一下 :。
求导得 ,所以此时 单调不升。
其余情况类似,不多赘述,均为化简 后证明 即可。
如此,我们证明了 满足四边形不等式,故可以决策单调性优化。
方法就是前面讲过的单调队列 + 二分。注意答案可能很大,开 long double
。
#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
#include<cstring>
typedef long double ll;
template <typename T> T Max(T x, T y) { return x > y ? x : y; }
template <typename T> T Min(T x, T y) { return x < y ? x : y; }
template <typename T> T Abs(T x) { return x < 0 ? -x : x; }
template <typename T>
T& read(T& r) {
r = 0; bool w = 0; char ch = getchar();
while(ch < '0' || ch > '9') w = ch == '-' ? 1 : 0, ch = getchar();
while(ch >= '0' && ch <= '9') r = r * 10 + (ch ^ 48), ch = getchar();
return r = w ? -r : r;
}
ll qpow(ll x, int y) {
ll sumq = 1;
while(y) {
if(y & 1) sumq = sumq * x;
x = x * x;
y >>= 1;
}
return sumq;
}
const int N = 100010;
int n, L, P, p[N], a[N], s[N];
int qh, qt;
struct Node {
int p, l, r;
Node(int pp = 0, int ll = 0, int rr = 0) { p = pp; l = ll; r = rr; }
}q[N];
char ch[N][31];
ll f[N];
ll w(int j, int i) {
return f[j] + qpow(Abs(s[i] - s[j] - L), P);
}
void print(int x) {
if(!x) return ;
print(p[x]);
for(int i = p[x]+1; i <= x; ++i) {
printf("%s", ch[i]+1);
if(i < x) putchar(' ');
}
puts("");
}
void solve() {
read(n); read(L); read(P); ++L;
for(int i = 1; i <= n; ++i) {
scanf("%s", ch[i]+1);
a[i] = std::strlen(ch[i]+1);
p[i] = q[i].p = q[i].l = q[i].r = 0;
s[i] = s[i-1] + a[i] + 1;
}
qh = 1; qt = 1;
q[1] = Node(0, 1, n);
for(int i = 1; i <= n; ++i) {
while(qh < qt && q[qh].r < i) ++qh;
f[i] = w(q[qh].p, i); p[i] = q[qh].p;
if(q[qh].l == q[qh].r) ++qh;
else ++q[qh].l;
while(qh <= qt && w(q[qt].p, q[qt].l) > w(i, q[qt].l)) --qt, q[qt].r = q[qt+1].r;
if(qh > qt) q[++qt] = Node(i, i+1, n);
else {
int l = q[qt].l, r = q[qt].r, mid = 0, t = 0;
while(l <= r) {
mid = (l + r) >> 1;
if(w(q[qt].p, mid) > w(i, mid)) t = mid, r = mid-1;
else l = mid+1;
}
if(t) {
if(t == q[qt].l) q[qt].p = i;
else ++qt, q[qt] = Node(i, t, q[qt-1].r), q[qt-1].r = t-1;
}
}
}
if(f[n] > 1000000000000000000ll) {
puts("Too hard to arrange");
return ;
}
printf("%lld\n", (long long)f[n]);
print(n);
}
signed main() {
int T; read(T);
while(T--) {
solve();
puts("--------------------");
}
return 0;
}
分治
单调队列+二分的方法可以解决大多数的决策单调性优化问题。对于更特殊的情况,决策点和决策点如果是互相独立的,那么可以采用分治解决。适用性没有单调队列+二分更强,但是也可能拿来解决某些问题。
大致的思路是:定义 solve(l,r,L,R)
为求解 的状态值,已知这些状态的最优决策点在 中,每次暴力求出 处的状态值,从最优决策点断开分治解决。
由于每一层递归下去状态数 都会折半,所以递归树的层数是 级别,递归树每层 solve
的总复杂度是决策点总数即 ,所以总的时间复杂度是 .
栗题三:Codeforces 868F Yet Another Minimization Problem
考虑 代表前 个数分成 段,然后有个决策单调性优化,但是这个一段区间的代价 比较难算,如果单独看这个东西是小 Z 的袜子,可以归约到 的矩阵乘法,看上去很完蛋。
但注意到如果选择分治来算这个决策单调性, 用类似莫队的东西暴力移动两个指针,由于分治最多有 层,每层的移动次数是 的,所以这个 的统计次数是 的,总复杂度就是 .
斜率优化
有一部分决策单调性问题实际上就是斜率优化的一种,具体的原理就不多谈啦其实就是懒。
一点总结
其实大部分的题要点还是能看出来这个满足决策单调性,有时候证不出来也可以打个表看看有没有单调性。
至于栗题的话,如果提前告诉你这个题决策点是单调了从而决策单调性优化,那就没什么意义了,所以就不放啦其实就是懒。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?