DP优化相关
决策单调性与四边形不等式
四边形不等式提供了决策单调性的一个充分但不必要的条件
限制是
只需要满足
就可以
无限制序列分割
考虑这样一个 \(DP\)
当 \(val\) 满足四边形不等式时, 决策具有单调性
证明
我们考虑新的二元函数 \(C(k, i) = f_k + val(k, i)\)
他显然也满足四边形不等式
然后我们发现决策具有单调性时, 对应点与其决策点构成一个区间, 这些区间之间有两种情况: 相交或者不交
二元函数 \(C\) 因为满足四边形不等式所以可以使得它们之间没有包含关系, 只满足上边的两种情况
代码实现
- 分治
用于从一个数组转移到另一个数组, 好写, 具体而言就是写一个函数 \(solve(l, r, L, R)\)
表示 \([l,r]\) 转移决策范围是 \([L,R]\)
然后 \(mid\) 的转移暴力求解, 然后递归求解
复杂度 \(O(nlogn)\)
- 二分队列
贴一个实现的模板吧
void add (int i) {
int pos = n + 1;
while (head <= tail && work(sta[tail].l, sta[tail].k) >= work(sta[tail].l, i))
pos = sta[tail--].l;
if (head <= tail && work(sta[tail].r, sta[tail].k) >= work(sta[tail].r, i)) {
ll l = sta[tail].l, r = sta[tail].r, mid;
while (l < r) {
mid = (l + r) >> 1;
if (work(mid, i) <= work(mid, sta[tail].k)p) r = mid;
else l = mid + 1;
}
sta[tail].r = r - 1;
pos = r;
}
if (pos != n + 1) sta[++tail] = {pos, n, i};
}
int main() {
sta[0] = {1, n, 0};
for (int i = 1; i <= n; i++) {
f[i] = work(i, sta[head].k);
if (i == sta[head].r) head++;
sta[head].l = i + 1;
add(i);
}
}
反向决策单调性
一个非常有意思的点是, 如果 \(val\) 满足的是反向的四边形不等式的话, 决策点整体 呈现单调下降的趋势
也是考虑新的二元函数 \(C(k, i) = f_k + val(k, i)\)
那么具有反向的四边形不等式, 让他决策点与原位置构成的区间之间只满足两种情况: 无交, 包含关系
所以 决策点整体 呈现单调下降的趋势, 因为有无交的关系
实现可以使用二分栈进行实现, 和二分队列大相径庭
当然也可以使用分治, 具体情况具体分析
这种决策单调性不易打表观察出来,所以实战中要多关注
[USACO19FEB] Mowing Mischief P
切割面积尽可能大的限制条件是最长上升子序列, 这个限制条件让一个点的转移点收到了限制
按照横坐标从小到大, 从最长上升子序列从大到小进行转移, 当最长上升子序列相等时, 横坐标从小到大, 纵坐标从大到小, 我们可以很轻松的列出一个 1D1D \(DP\)
二元函数 \(val\) 不适合斜率优化, 考虑四边形不等式
推出来是反向决策单调性
但是转移具有限制条件, 直接使用二分栈是错误的, 考虑限制条件是一段区间, 使用线段树分治
这时, 在线段树的一个节点上具有完全的决策单调减的性质
因为我们考虑 \(a \le b \le c \le d\) 四边形不等式的前提中的 \(b \le c\) 永远是满足的
所以可以把从哪里转移的数组放在转移到的数组之前, 任意一个决策点与原位置的区间必然有交, 交于交接处, 又因为只有包含关系, 所以是完全单调减
tips: vector神力, 多用指针
限制段数序列分割
可以使用 wqs二分
做到 \(nlogVlogn\) 见下
这里使用决策单调性可以做到 \(n^2\)
这是一个 2D/1D \(DP\)
当 \(k\) 固定时, 这是上边的 \(DP\) 转移, 所以有 决策单调性
复杂度可以做到 \(n^2logn\)
考虑当 \(i\) 固定时, 决策具有什么样子的性质
序列长度固定, 分的段数不同, 考虑 \(k, k+1\) 段的分法之间
因为具有四边形不等式的限制, 所以一定不能具有包含关系, 手摸几遍发现最后一段的长度满足 \(k + 1\)分法小于 \(k\) 这就是它的决策单调性
所以有
\(opt(i - 1, j) \le opt(i, j) \le opt(i + 1, j)\)
复杂度可以通过求和计算得到是 \(n^2\)
分治
我们也可以不完全使用上边的算法
使用 \(nklogn\) 的暴力
使用分治实现, 好处是可以利用莫队求解 \(val\)
可以对左右端点分析, 发现移动次数是 \(nlogn\) 级别的
如果不能使用分治呢 \(?\) 并且左右端点也只能移动呢 \(?\) 那么我们可以使用 \(cdq\) 分治来解决来多一个\(log\) 来解决
Yet Another Minimization Problem
区间 DP
当 \(val\) 满足四边形不等式以及区间包含单调性时, 有
\(opt(i - 1, j) \le opt(i,j) \le opt(i, j + 1)\)
证明省略, 不重要
可以优化区间 \(DP\)
例题
- 一段的贡献是 \(val(l, r) = max_l^r \{ a_i \} \times (n - l + 1)\)
乍一看我们无法证明他具有决策单调性, 但是我们发现 \(max\) 的出现让转移点只能在前缀最大值处, 所以可以只考虑这些点, 发现值是递增的, 发现可以使用决策单调性
凸包相关
函数的凸性是一种非常优美且强有力的性质
定义
何为凸包?
函数满足 \(\forall x, d, \frac{F(x-d)+F(x+d)}{2} \ge F(x)\) 这个是下凸
函数满足 \(\forall x, d, \frac{F(x-d)+F(x+d)}{2} \le F(x)\) 这个是上凸
还有别的表述为 原函数的导函数单调不降, 二阶导函数非负
证明凸性可以使用网络流建模
最小费用最大流中, 最小费用关于最大流是下凸的
wqs二分
解决的问题都形如 恰好 k 个
考虑 \(k\) 与 答案对应形成的函数, 如果他是一个凸的, 那么我们可以使用 wqs二分
考虑一条直线与这个凸包上某一个点相交了, 考虑它的横截据
\(y = kx + b\) 当 \((x, f_x)\) 时, 解的 \(b = f_x - kx\)
而且我们惊奇的发现, 当 交点是切点的时候, 横截据是一个最值!
最值的类别取决于上凸还是下凸
所以我们使用这种方法, 做一个无限制, 但是带权的 \(DP\) 就可以求出它与哪个点相交, 然后二分斜率
对于斜率相等的情况, 我们可以使用强制少选, 再二分小于等于的方法解决
发现有恰好, 所以猜测凸性, 然后使用 \(wqs\) 二分
就变成了一个最小生成树问题
发现有恰好, 所以猜测凸性, 然后使用 \(wqs\) 二分
考虑使用树形 \(DP\)
问题其实相当于选择 \(k\) 条不相交的路径, 让路径和最大
这个可以使用树形 \(DP\) 解决
与四边形不等式
我们考虑一种非常常见的 \(DP\) , 有限制的序列分割问题
\(f_{i,j} = \min_{k} f_{k,j-1} + val(k, i)\)
当 \(val(a, b)\) 满足四边形不等式时候, 关于块数的最优函数具有凸性
证明:
我们只需要证明段数函数 \(F(x)\) 满足 \(F(x-d)+F(x+d) \ge 2F(x)\)
考虑一个 \(x-d\) 的分法与 \(x+d\) 的分法有什么关系, 它们分的段数必然有包含关系, 考虑一个包含关系
\(p_{x-d, i} < p_{x+d, i+d} < p_{x+d, i + 1 + d} < p_{x - d, i + 1}\)
然后应用四边形不等式, 构造出两个和比较劣 \(x\) 段的分法
\(p_{x-d,1}...p_{x-d,i}, p_{x+d,i + d + 1},p_{x+d,i+d+2}...\) 段数为 \(i + x + d - (i + d + 1) + 1 = x\)
\(p_{x+d,1}...p_{x+d,i+d},p_{x-d,i+1},...\) 段数也为 \(x\) 的
所以就有 \(F(x-d) + F(x + d) \ge 2F(x)\)
证毕
所以当二元函数满足四边形不等式时, 可以使用决策单调性进行优化, 有时候也可以使用 wqs二分
解决
slope trick
用于维护凸包的一种方式
适用条件为 \(DP\) 数组为凸包的时候, 且斜率都为整数的时候, 并且斜率变化不大
为了方便的插入删除维护这个凸包, 并且支持凸包之间的加减
slope_trick
使用了维护拐点的方法, 具体地, 如果拐点两边的斜率相差多少, 就插入几个点
在维护一条凸包上的一次函数, 来唯一确定这个凸包
凸包加法就是拐点集合计划
一般题目中, 我们通常使用堆来维护这个东西, 不过也可以使用平衡树来维护
思维量比较少, 不过代码比较难写
给定一个序列,每次操作可以把某个数 \(+1\) 或 \(−1\) .要求把序列变成非降数列. 而且要求修改后的数列只能出现修改前的数.
一个普通的 \(DP\) 是设 \(f_{i, j}\) 表示把第 \(i\) 个数变成 \(j\) 的最小代价
将 \(f_i\) 视为一个函数
一次转移相当于是加上函数 \(g = \left| x - a_i \right|\)
因为 \(g\) 是凸函数, \(f_0\) 也是凸函数, 所以 \(f_i\) 是凸函数
考虑 slope_trick
维护
一次转移先是做前缀最小值, 然后在凸包加法
前缀最小值就是弹出堆顶直到堆顶是最低点, 这个怎么判断
分类讨论, 如果绝对值函数最低点在原函数最低点右边, 绝对值函数最低点成为最低点
否则右边斜率加一, 可以通过弹出一个堆顶实现减一, 实时统计答案
代码
for (int i = 1, x; i <= n; i++) {
std::cin >> x;
q.push(x);
if (x < q.top()) {
ans = ans + q.top() - x;
q.pop();
q.push(x);
}
}
向左向右移动的操作实在是比较难搞
所以我们不妨将它拆成两部分, 以最低点为界限, 那么这个操作, 就是堆中整体打 \(tag\) 整体左右移动
然后修改的函数, 进行分类讨论, 是不难维护的
不难列出 \(DP\) \(f_{i, j}\)
考虑它从儿子转移到父亲, 边只能减少不能增加, 所以进行分类讨论可以得到
它对这个函数做了什么呢?
- 将斜率大于 \(0\) 的斜率改为 \(1\)
容易发现, 这个操作让最终的函数斜率最大值为 \(1\)
所以考虑, 最终父亲的函数斜率最大值为 \(deg\)
这个直接弹出 \(deg - 1\) 个点就可以了
- 将斜率等于 \(0\) 的右移 \(l\)
整体 \(+l\) 在扔回堆中
- 加入一段斜率为 \(-1\) 的
做完 \(2\) 操作就可以得到
使用 可并堆维护就可以了
至于如何找到一条直线, 一种巧妙的做法是不用实时维护, 而是考虑特殊情况, 当距离都为 \(0\) 时, 那么就要把所有边都删掉, 可以直接求和得到这条直线
附
一些凸包函数维护的解决方法
- 加法
一次函数与凸包集合相加
- 前缀/后缀取 \(min\)
去掉 \(k > 0\) \((k < 0)\) 的部分
- 平移/反转
整体打标记
斜率优化
一个经典 \(DP\) 模型
这是一个 \(1D1D\) 的 \(DP\) 模型
斜率优化可以优化这个东东, 一般函数 \(val(i, j)\) 形如
\(val(i, j) = Aij + Bi + Cj\)
\(i, j\) 表示与 \(i, j\) 相关的值, \(A, B, C\) 为常数
那么考虑把 \(j\) 视为变量
\(F_i = F_j + Aij + Bi + Cj\)
移项得到
\(F_j + Cj = -Aij + (F_i - Bi)\)
这是一个一次函数的形式
\(Bi, Ai\) 是定值, 所以这条 斜线斜率固定, 要求横截据最小, 经过点 \((j, F_j+Cj)\)
所以可以维护一个下凸包, 来优化这个东东
介绍一道例题
容易列出 \(DP\) 式子
\(F_i = \min F_j + sumT_i \times (sumC_i - sumC_j) + S \times (sumC_n - sumC_j)\)
后边就是 \(val(i, j)\)
按照上边的变形规则可以变形为
\(F_j - SsumC_j = sumT_i \times sumC_j + F_i - sumT_i \times sumC_i - S \times sumC_n\)
然后斜率优化问题在于如何维护这个凸包
当斜率 \(sumT_i\) 是递增时, 不用二分, 最左侧就是最优决策点
\((sumC_j, F_j - SsumC_j)\) 的 \(sumC_j\) 是递增的
所以维护凸包也是容易的, 可以使用一个队列之类的东西维护
复杂度 \(O(n)\)
但是这道题 \(sumT_i\) 不一定是递增的
不过可以二分凸包
复杂度 \(O(nlog_n)\)
如果插入点也不是递增的, 我们有其他的解决方法
例如使用平衡树维护, 当插入一个点的时候, 找到它的前驱和后继, 然后比较斜率
与李超线段树
仍然是考虑这个 \(val(i, j) = Aij + Bi + Cj\)
我们不用 \(j\) 为自变量, 我们用 \(i\) 为自变量
\(F_i = F_j + Aij + Bi + Cj\)
\(F_i - Bi = Aj \times i + F_j + C_j\)
发现一个 \(j\) 确定一个一次函数 \(y = kx + b\)
而 \(i\) 是已知的, 故而我们的问题就变成了李超线段树的经典问题
而且我们发现, 李超线段树的插入是全局插入, 所以没有必要进行线段树给他分成 \(log\) 段
直接在第一个节点向下转移
所以复杂度可以在 \(O(nlogn)\) 内解决
常数还比较小
代码
ll G (ll x, ll o) {
return k[o] * x + b[o];
}
void modify (ll pos, ll l, ll r, ll t) {
if (l == r) {
if (G(l, t) < G(l, s[pos])) s[pos] = t;
return ;
}
ll mid = (l + r) >> 1;
if (G(mid, t) < G(mid, s[pos])) std::swap(s[pos], t);
if (G(l, t) < G(l, s[pos])) modify (pos << 1, l, mid, t);
else if (G(r, t) < G(r, s[pos])) modify (pos << 1 | 1, mid + 1, r, t);
return ;
}
ll query (ll pos, ll l, ll r, ll k) {
if (l == r) return G (l, s[pos]);
ll mid = (l + r) >> 1;
if (k <= mid) return std::min(query (pos << 1, l, mid, k), G (k, s[pos]));
if (k > mid) return std::min(query (pos << 1 | 1, mid + 1, r, k), G (k, s[pos]));
return 0;
}
容易列出 \(DP\) 式子
\(f_{i, j} = \min_k f_{k, j - 1} + (i - k) \times \max_{k+1 \le x \le i} a_x\)
第二维很小, 不难想到枚举第二维
容易发现这是一个双变量的转移式子, 也就是二维的
所以我们想到做两次优化
\(\max\) 是好消掉的, 因为他是连续的, 具体体现就是单调栈上
那么我们现在考虑一段内, \(\max\)变成了一个常数 \(A\)
\(f_i = g_k + (i - k) \times A\)
使用斜率优化的方法可以求出 \(A\) 固定时最优的 \(k\)
然后一个 \(A\) 唯一对应一个 \(k\) \(k\) 可以视为不变量
那么 \(A\) 有不同的取值
可以再次做一遍斜率优化
理论上两次斜率优化维护凸包和李超线段树的做法都可以
这里选择了两次使用李超线段树