四边形不等式优化 DP 学习笔记
部分摘自 OI Wiki
四边形不等式优化利用的是状态转移方程中的决策单调性,也常称为 决策单调性优化 DP。
基础知识
考虑最简单的情形,我们要解决如下一系列最优化问题:
这里假定成本函数
NOTE
约定
动态规划的状态转移方程经常可以写作一系列最优化问题的形式。以(1)式为例,这些问题含有参数
在一般的情形下,这些问题总时间复杂度为
- 决策单调性:对于任意
,必然成立 。
NOTE
对于问题
另一方面,拥有相同最小最优决策的问题构成一个区间。这一区间,作为最小最优决策的函数,应严格递增。亦即,给定
最常见的判断决策单调性的方法是通过四边形不等式(quadrangle inequality
四边形不等式:如果对于任意
均成立 则称函数
满足四边形不等式(简记为「交叉小于包含 若等号永远成立,则称函数」 ) 。 满足 四边形恒等式。
如果没有特别说明,以下都会保证
定理 1
若
满足四边形不等式,则问题 满足决策单调性。 证明
要证明这一点,可采用反证法。假设对某些
,成立 。此时有 。根据最优化条件, 且 ,于是, ,这与四边形不等式矛盾。
四边形不等式可以理解在合理的定义域内,
利用决策单调性,有很多常见算法都可以将算法复杂度优化到
- 移动访问:
可以从 或 以 时间转移得到 类似 莫队算法 中的情形)。 ( - 动态计算:
的计算依赖于 。这意味着 和 只能顺次计算。下文介绍了不限制区间个数的区间分拆问题,它就属于这一情形。
这两条性质并不互斥,可能存在
分治
要求解所有状态,只需要求解所有最优决策点。为了对所有
除了随机访问的基本情形外,分治算法还可以应用于
val_t w(int j, int i); // 成本函数
val_t f[N]; // 最优值
int opt[N]; // 最小最优决策
// 递归求解 [l,r] 中的问题
// 已知它们的最小最优决策点一定出现在区间 [opt_l, opt_r] 中
void calc(int l, int r, int opt_l, int opt_r) {
int mid = (l + r) / 2;
// 求问题 mid 的最优决策点
for (int j = opt_l; j <= std::min(opt_r, mid); ++j) {
if (w(j, mid) < f[mid]) {
f[mid] = w(j, mid);
opt[mid] = j;
}
}
// 根据决策单调性得出左右两部分的决策区间,递归处理
if (l < mid)
calc(l, mid - 1, opt_l, opt[mid]);
if (r > mid)
calc(mid + 1, r, opt[mid], opt_r);
}
// 求解整个区间 [1,n] 的问题
void solve(int n) {
// 每次调用递归函数前,都需要清空数组 f
std::fill(f + 1, f + n + 1, inf);
// 最开始时,只知道问题 [1,n] 的所有决策点都一定在 [1,n] 中
calc(1, n, 1, n);
}二分队列
注意到对于每个决策点
具体地,算法需要顺次遍历决策点。当遍历到决策点
为了说明队列更新过程中,决策点
推论 1
设
是仅考虑 中的决策时,问题 的最小最优决策。如果 满足四边形不等式,那么对于任意 ,必然成立 。 证明
设
是充分大的正实数。函数 仍然满足四边形不等式,其中, 是 Iverson 括号。考虑以 为成本函数的辅助 DP。在辅助 DP 中,对于任何问题 ,决策 都不可能是最小最优的,即 。对辅助 DP 应用定理 1 就得到本推论。
该算法过程如下:
- 初始时,队列是空的。类似于单调队列,每次考虑下一个决策
的时候,都需要进行出队和入队操作。 - 出队:首先将上一个问题
从队列中移除。如果队首的决策能够解决的问题的右端点恰为 ,直接弹出队首;否则,将队首决策能够解决问题的左端点更新为 。 - 入队:要对决策
进行入队时,首先比较它和队尾的决策 。 - 如果对于问题
,将要入队的决策 比已有的决策 严格更优,即 时,则弹出队尾的决策 。此操作持续到队列为空或队尾的决策 比起 对于问题 更优时为止。 - 如果队列已空,入队
,即认为决策 是尚未解决的所有问题的最优解。 - 如果队尾决策
对于问题 同样不劣于将入队的决策 ,那么当 时,入队 ,表示 是问题 的最小最优决策;否则,不需要入队 ,因为它并不比已有的决策更优。 - 最后的情形是,队尾决策
比起要入队的决策 对于问题 严格更优,而对于问题 严格更劣。这说明,存在问题 使得问题 的最小最优决策为 且问题 的最小最优决策为 。因而,需要通过 二分 找到最小的 使得 ,再将队尾的区间右端点 修改为 ,并入队 。
- 如果对于问题
- 处理完决策
后,就已经处理了所有到 为止的决策。此时,队首决策就是问题 的最小最优决策,可以记录相应的最优解。
val_t w(int j, int i); // 成本函数
val_t f[N]; // 最优值
int opt[N]; // 最小最优决策
int lt[N], rt[N]; // 决策 j 可以解决的问题区间 [l_j,r_j]
// 求解整个区间 [1,n] 的问题
void solve(int n) {
std::deque<int> dq; // 存储所有可行决策的单调队列
// 顺次考虑所有问题和决策,下标从 1 开始
for (int j = 1; j <= n; ++j) {
// 出队
if (!dq.empty() && rt[dq.front()] < j)
dq.pop_front();
if (!dq.empty())
lt[dq.front()] = j;
// 入队
while (!dq.empty() &&
w(j, lt[dq.back()]) < w(dq.back(), lt[dq.back()])) {
dq.pop_back();
}
if (dq.empty()) {
lt[j] = j;
rt[j] = n;
dq.emplace_back(j);
} else if (w(j, rt[dq.back()]) >= w(dq.back(), rt[dq.back()])) {
if (rt[dq.back()] < n) {
lt[j] = rt[dq.back()] + 1;
rt[j] = n;
dq.emplace_back(j);
}
} else {
int ll = lt[dq.back()], rr = rt[dq.back()], i = rr;
// 二分
while (ll <= rr) {
int mm = (ll + rr) / 2;
if (w(j, mm) < w(dq.back(), mm)) {
i = mm;
rr = mm - 1;
} else {
ll = mm + 1;
}
}
rt[dq.back()] = i - 1;
lt[j] = i;
rt[j] = n;
dq.emplace_back(j);
}
// 计算
f[j] = w(dq.front(), j);
opt[j] = dq.front();
}
}类似于单调队列,每个决策点至多入队一次,出队一次。其中,出队是
由于二分队列算法顺次考虑所有问题和决策点,它可以应用于
例题 1:「POI2011」Lightning Conductor
给定一个长度为
的序列 ,要求对于每一个 ,找到最小的非负整数 满足
思路
显然,经过不等式变形,我们可以得到待求整数
根据
简化 LARSCH 算法
前两种算法都无法处理
仍然考虑分治求解。求解区间
- 区间
中问题 的最小最优决策 和最优值,以及 - 仅考虑区间
中的决策,问题 的最小最优决策 和最优值。
区间
设
- 遍历决策
,更新问题 的最小最优决策和最优值。 - 递归求解区间
中的问题。 - 遍历决策
,更新问题 的最小最优决策和最优值。 - 递归求解区间
中的问题。
对整个区间
val_t w(int j, int i); // 成本函数
val_t f[N]; // 最优值
int opt[N]; // 最小最优决策
// 用决策 j 更新问题 i
void check(int j, int i) {
if (w(j, i) < f[i]) {
f[i] = w(j, i);
opt[i] = j;
}
}
// 递归求解区间 (l, r] 内的问题
void calc(int l, int r) {
int mid = (l + r + 1) / 2;
for (int j = opt[l]; j <= opt[r]; ++j)
check(j, mid);
if (mid < r)
calc(l, mid);
for (int j = l + 1; j <= mid; ++j)
check(j, r);
if (mid > l)
calc(mid, r);
}
// 求解整个区间 [1, n] 内的问题
void solve(int n) {
// 清空 f 数组
std::fill(f + 1, f + n + 1, inf);
// 初始化
check(1, 1);
check(1, n);
// 递归求解区间 (1, n] 内的问题
calc(1, n);
}首先,可以说明这一算法的正确性。为此,只要检查每一递归求解的步骤(即步骤 2 和 4)前都满足上文给出的前提条件。由前一节得到的推论 1,有
然后,需要说明该算法的复杂度仍然是
对于一些
NOTE
非随机访问的复杂度证明
只需要证明游标移动的总次数是
考虑构造如下游标移动规则。步骤 1 中,可以令游标
移动。此时,游标
移动。此时,游标
次移动,步骤 3 需要
次移动。将这些移动次数对递归树中的所有结点求和,利用同一层中的所有
由于上述移动规则比起实际计算时游标的移动设置了更多的途径点,所以游标的实际移动次数不会超过该规则下移动次数的估计。因此,游标的实际移动次数也是
由于该算法在求解区间
区间分拆问题
考虑将某个区间拆分成若干个子区间的问题。形式化地说,将给定区间
这里,