四边形不等式优化
四边形不等式优化利用的是状态转移方程中的决策单调性。
基础知识
考虑最简单的情形,我们要解决如下一系列最优化问题。
这里假定成本函数
约定
动态规划的状态转移方程经常可以写作一系列最优化问题的形式。以(1)式为例,这些问题含有参数
在一般的情形下,这些问题总时间复杂度为
- 决策单调性:对于任意
,必然成立 。
附注
对于问题
另一方面,拥有相同最小最优决策的问题构成一个区间。这一区间,作为最小最优决策的函数,应严格递增。亦即,给定
最常见的判断决策单调性的方法是通过四边形不等式(quadrangle inequality)。
- 四边形不等式:如果对于任意
均成立
则称函数
如果没有特别说明,以下都会保证
定理 1
若
证明
要证明这一点,可采用反证法。假设对某些
四边形不等式可以理解在合理的定义域内,
利用决策单调性,有两种常见算法可以将算法复杂度优化到
分治
要求解所有状态,只需要求解所有最优决策点。为了对所有
核心代码
int w(int j, int i);
void DP(int l, int r, int k_l, int k_r) {
int mid = (l + r) / 2, k = k_l;
// 求状态f[mid]的最优决策点
for (int j = k_l; j <= min(k_r, mid - 1); ++j)
if (w(j, mid) < w(k, mid)) k = j;
f[mid] = w(k, mid);
// 根据决策单调性得出左右两部分的决策区间,递归处理
if (l < mid) DP(l, mid - 1, k_l, k);
if (r > mid) DP(mid + 1, r, k, k_r);
}
二分队列
注意到对于每个决策点
核心代码
int val(int j, int i);
int lt[N], rt[N], f[N];
deque<int> dq;
// 初始化队列
dq.emplace_back(1);
lt[1] = 1;
rt[n] = n;
// 顺次考虑所有问题和决策
for (int j = 1; j <= n; ++j) {
// 出队
while (!dq.empty() && rt[dq.front()] < j) {
dq.pop_front();
}
// 计算
f[j] = val(dq.front(), j);
// 入队
while (!dq.empty() && val(j, lt[dq.back()]) < val(dq.back(), lt[dq.back()])) {
dq.pop_back();
}
if (dq.empty()) {
dq.emplace_back(j);
lt[j] = j + 1;
rt[j] = n;
} else if (val(j, rt[dq.back()]) < val(dq.back(), rt[dq.back()])) {
if (rt[dq.back()] < n) {
dq.emplace_back(j);
lt[j] = rt[dq.back()] + 1;
rt[j] = n;
}
} else {
int ll = lt[dq.back()];
int rr = rt[dq.back()];
int i;
// 二分
while (ll <= rr) {
int mm = (ll + rr) / 2;
if (val(j, mm) < val(dq.back(), mm)) {
i = mm;
rr = mm - 1;
} else {
ll = mm + 1;
}
}
rt[dq.back()] = i - 1;
dq.emplace_back(j);
lt[j] = i;
rt[j] = n;
}
}
掌握这一算法,需要理解如下要点:
- 队列需要记录到目前为止每个可行的决策点
和能够解决的问题区间左右端点 和 构成的 三元组。对于给定区间 内的问题, 应该是到目前为止考虑过的决策点中最小最优的(以下简称最优决策)。每时每刻,队列中存储的决策未必是连续的,但是尚未解决的问题应该是队列中存储的问题区间的不交并。 - 初始化:将首个决策放于队列中,并记录它对于所有问题都是最优的。
- 类似于单调队列,每次考虑下一个决策
的时候,都需要进行出队和入队操作。 - 出队:当所有决策
都考虑结束后,问题 的解就是队列中首个满足 的决策点 。此时可以弹出所有满足 的队首。由于决策单调性,弹出的决策也不会是后续问题的最优决策。 - 入队:要对决策
进行入队时,首先比较它和队尾的决策 。- 如果对于问题
,将入队的决策 比已有的决策 更优,即 时,则弹出队尾的决策 。此操作持续到队尾的决策 比起 对于问题 更优时为止。 - 如果队列已空,入队
,即认为决策 是尚未解决的所有问题的最优解。 - 如果队尾决策
对于问题 同样优于将入队的决策 ,那么当 时,入队 ,表示 是对于问题 的最优解,否则,不需要入队 ,因为它并不比已有的决策更优。 - 最后的情形是,队尾决策
比起要入队的决策 对于问题 更优,而对于问题 更劣,那么,需要通过 二分 找到最小的 使得 ,将队尾的区间右端点修改为 ,并入队 。
- 如果对于问题
类似于单调队列,每个决策点至多入队一次,出队一次。这里,出队是
例题 1:「POI2011」Lightning Conductor
给定一个长度为
思路
显然,经过不等式变形,我们可以得到待求整数
根据
实现
#include <algorithm>
#include <cmath>
#include <deque>
#include <iostream>
#define all \
for (auto i : {&q, &l, &r}) (*i)
using namespace std;
int n, a[500009], ans[500009];
deque<int> q, l, r;
double f(int i, int j) { return a[i] + sqrtl(j - i); }
void work() {
all.clear();
for (int i = 0; i < n; ++i) {
if (q.size() && r.front() < i) all.pop_front(); // 队首出队
if (q.size()) l.front() = i;
for (; q.size() && f(q.back(), l.back()) <= f(i, l.back());) // 队尾出队
all.pop_back();
if (q.empty()) // 入队
q.emplace_back(i), l.emplace_back(i), r.emplace_back(n);
else if (f(q.back(), n) < f(i, n)) {
int ll = l.back(), rr = n, mid;
for (; ll <= rr;) {
mid = (ll + rr) >> 1;
if (f(q.back(), mid) < f(i, mid))
rr = mid - 1;
else
ll = mid + 1;
}
r.back() = rr;
q.emplace_back(i), l.emplace_back(ll), r.emplace_back(n);
}
ans[i] = max(ans[i], (int)(ceil(f(q.front(), i))) - a[i]);
}
}
int main() {
cin >> n;
for (int i = 0; i < n; cin >> a[i++]);
work();
reverse(a, a + n);
reverse(ans, ans + n);
work();
reverse(ans, ans + n);
for (int i = 0; i < n; cout << ans[i++] << '\n');
}
区间分拆问题
考虑将某个区间拆分成若干个子区间的问题。形式化地说,将给定区间
这里,
限制区间个数的情形
上述问题可以加强为限制区间个数的情形,即问题指定将区间拆分成
这里,
对于这一问题,利用决策单调性,实际上还存在其他的优化算法。第二种优化思路依赖于如下结果。这种优化算法和下文详细描述的 Knuth 优化算法十分相似。
定理 2
若
证明
第二个不等式只是第
下证
第一个不等式可以另证如下。同样考虑上面证明中的两个分划。如果所证命题不成立,则有
同样地,考虑分拆
此时,不等号是严格的,因为
利用这一结果,我们可以限制决策
注意
这里算法复杂度不是
最后一种优化方法来源于如下的观察。
定理 3
若
证明
下证
两个所得区间都是
这里第二个不等式正是四边形不等式。所求凸性由此得证。
这一结论保证了可以通过 wqs 二分(国外称 Alien's trick)的方法解决此问题。具体来说,考虑带参的成本函数
对于限制区间个数的区间分拆问题的三种算法,在不同的数据范围时表现各有优劣,需要结合具体的题目选择合适的算法。
例题 2:P4767 [IOI2000] 邮局 加强版 P6246 [IOI2000] 邮局 加强版 加强版
高速公路旁边有一些村庄。高速公路表示为整数轴,每个村庄的位置用单个整数坐标标识。没有两个在同样地方的村庄。两个位置之间的距离是其整数坐标差的绝对值。
邮局将建在一些,但不一定是所有的村庄中。为了建立邮局,应选择他们建造的位置,使每个村庄与其最近的邮局之间的距离总和最小。
你要编写一个程序,已知村庄的位置和邮局的数量,计算每个村庄和最近的邮局之间所有距离的最小可能的总和。
思路
每个村庄有其最近的邮局,那么每个邮局也有其管辖的村庄,易知这是一个区间。
考虑把这
根据数学知识,对于区间
问题转化为限制区间个数的区间分拆问题。可以证明,
实现 1,前文第二种优化,复杂度
#include <iostream>
constexpr int N = 3009;
constexpr int M = 309;
using namespace std;
int n, m, a[N], s[N], g[M][N], p[M][N];
int f(int i, int j) {
int k = (i + j) >> 1;
return a[k] * (k - i + 1) - (s[k] - s[i - 1]) + (s[j] - s[k]) -
a[k] * (j - k);
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; cin >> a[i], s[i] = s[i - 1] + a[i], ++i);
for (int i = 1; i <= n; ++i) g[0][i] = 1 << 30, p[m + 1][i] = i;
for (int i = 1; i <= m; ++i) p[i][0] = 1;
for (int i = 1; i <= n; ++i)
for (int j = m; j; --j) {
g[j][i] = 1 << 30;
for (int k = p[j][i - 1]; k <= p[j + 1][i]; ++k) {
int tmp = g[j - 1][k - 1] + f(k, i);
if (tmp < g[j][i]) g[j][i] = tmp, p[j][i] = k;
}
}
cout << g[m][n];
}
实现 2,wqs 二分,复杂度
#include <deque>
#include <iostream>
#define all \
for (auto i : {&q, &l, &r}) (*i)
using namespace std;
long long n, m, a[500009], s[500009], u, v, w, sum[500009], cnt[500009];
deque<int> q, l, r;
long long f(int i, int j) {
int k = (i + j) >> 1;
return sum[i - 1] + v + a[k] * (k - i + 1) - (s[k] - s[i - 1]) +
(s[j] - s[k]) - a[k] * (j - k);
}
void work() {
all.clear();
for (int i = 1; i <= n; ++i) {
if (q.size() && r.front() < i) all.pop_front(); // 队首出队
if (q.size()) l.front() = i;
for (; q.size() && f(q.back(), l.back()) >= f(i, l.back());) // 队尾出队
all.pop_back();
if (q.empty()) // 入队
q.emplace_back(i), l.emplace_back(i), r.emplace_back((int)n);
else if (f(q.back(), n) >= f(i, n)) {
int ll = l.back(), rr = n, mid;
for (; ll <= rr;) {
mid = (ll + rr) >> 1;
if (f(q.back(), mid) >= f(i, mid))
rr = mid - 1;
else
ll = mid + 1;
}
r.back() = rr;
q.emplace_back(i), l.emplace_back(ll), r.emplace_back((int)n);
}
sum[i] = f(q.front(), i);
cnt[i] = cnt[q.front() - 1] + 1;
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; cin >> a[i], s[i] = s[i - 1] + a[i], ++i);
for (w = 2e12; u <= w;) { // wqs二分
v = (u + w) >> 1;
work();
if (cnt[n] < m)
w = v - 1;
else
u = v + 1;
}
v = w;
work();
cout << sum[n] - m * v;
}
区间合并问题
另一类可以通过四边形不等式优化的动态规划问题是区间合并问题,即要将
这里给定任意初始成本
除了四边形不等式以外,区间合并问题的决策单调性还要求成本函数满足区间包含单调性。
- 区间包含单调性:如果对于任意
均成立
则称函数
这实质是成本函数的一阶条件,即
引理 1
若
证明
不妨设
第一种情况,
不妨假设
这里,第一个不等式来自于归纳假设
第二种情况,
不妨假设
这里,第一个不等式来自于归纳假设
定理 4
若
证明
引理 1 已经证得
利用这一结论,同样可以限制决策点
核心代码
for (int len = 2; len <= n; ++len) // 枚举区间长度
for (int j = 1, i = len; i <= n; ++j, ++i) {
// 枚举长度为len的所有区间
f[j][i] = INF;
for (int k = opt[j][i - 1]; k <= opt[j + 1][i]; ++k)
if (f[j][i] > f[j][k] + f[k + 1][i] + w(j, i)) {
f[j][i] = f[j][k] + f[k + 1][i] + w(j, i); // 更新状态值
opt[j][i] = k; // 更新(最小)最优决策点
}
}
满足四边形不等式的函数类
为了更方便地证明一个函数满足四边形不等式,我们有以下几条性质:
性质 1:若函数
性质 2:若存在函数
性质 3:设
性质 4:设
首先需要澄清一点,凸函数(Convex Function)的定义在国内教材中有分歧,此处的凸函数指的是下凸函数,即(可微时)一阶导数单调增加的函数。
证明
前两条性质根据定义很容易证明,下面证明第三条性质,性质四的证明过程类似。由于
为此,下面考虑
这里,根据区间单调性,
这一证明实际是如下导数证明的离散版本。
这在
习题
- Codeforces - Ciel and Gondolas(Be careful with I/O!)
- SPOJ - LARMY
- Codechef - CHEFAOR
- Hackerrank - Guardians of the Lunatics
- ACM ICPC World Finals 2017 - Money
参考资料
- noiau 的 CSDN 博客
- Quora Answer by Michael Levin
- Video Tutorial by "Sothe" the Algorithm Wolf
- Divide and Conquer DP
- Knuth's Optimization
- Quadrangle Inequality Properties
- 王钦石《浅析一类二分方法》
创建日期: 2019年3月15日