决策单调性优化入门
前置知识
先学下面的东西
- 单调队列
四边形不等式
定义
对于一个二元函数 \(w(l,r)\),如果有下面的命题成立,则称 \(w\) 满足四边形不等式:
定理
如果二元函数 \(w(a,b)\) 满足 \(w(a+1,b+1)+w(a,b) \leq w(a,b+1)+w(a+1,b)\) ,那么 \(w(a,b)\) 满足四边形不等式。
证明:
- 对于 \(a<b\), 有 \(w(a,b)+w(a+1,b+1)\leq w(a,b+1) + w(a+1,b)\)
- 对于 \(a+1<b\),有 \(w(a+1,b)+w(a+2,b+1)\leq w(a+1,b+1) + w(a+2,b)\)
将两式相加,然后化简:
以此类推,可以得到:
可以对第二个数做类似的操作,可以得到:
\(\rm Q.E.D\)
正文
定义
考虑一个形如下面式子的 \(\rm dp\) 转移:
设 \(p(i)\) 表示 \(f(i)\) 的最优决策点,即:
而这个 \(\rm dp\) 有决策单调性,当且仅当 \(p(i)\) 单调不降。
定理
转移参数 \(w(j,i)\) 满足四边形不等式 \(\Rightarrow\) \(f(i)\) 满足决策单调性。
证明:
\(\forall a < b < i < j\), 设 \(p(i) = b\),则有 \(f(b)+w(b,i)\leq f(a)+w(a,i)\)
根据四边形不等式,有
将两条式子相加,然后化简,得到:
由此可以看出,对于 \(i\) 之后的所有状态,其最优决策点都可以不是 \(a\) ,因此可以有 \(p(j)\ge b\) ,因此满足决策单调性。
碎碎念
\(Q\):上面的内容似乎只针对 \(\min\) ,那么对于 \(\max\) 应该怎么办呢?
\(A\):把四边形不等式的不等号取反即可。
决策单调性和斜率优化关联性挺大的,但是本 \(\rm juruo\) 觉得两个东西本质上有点不同。
应用
[NOI 2009] 诗人小G
可以得到转移:
其中 \(s_i = s_{i-1}+|\mathrm{str}_i|\)
设 \(w(i,j) = \left|s_i-s_j+(i-j-1)-L\right|^P\),显然当前就是要证明这个东西满足四边形不等式。
根据四边形不等式的性质,可以转化为证明
这个证明不太显然,需要稍微搞一搞。教给各位 \(\rm dalao\) 了。这里主要介绍知道决策单调性之后的做法。
显然,对于每个决策点 \(x\),都有一个贡献区间 \([l_x, r_x] (\forall k \in [l_x, r_x], p(k)=x)\) ,可以用二分求出对于决策点 \(x\),第一个比决策点 \(y\) 优的位置,这个明显可以使用二分求出,设其为 \(g(x,y)\)。
接着用单调队列维护出 \(l\) 单调递增的决策点序列。
显然,对于队列中相邻的两个决策 \(x,y~(x<y)\),显然是 \(r_x = g(y,x)-1\)。
- 当队头的 \(r\) 小于 \(i\) 的时候就推出。
- 考虑加入一个新的决策点 \(p\), 设这时候队尾的两个元素分别为 \(x,y~(x<y)\),显然,如果 \(g(y,x) \ge g(p, y)\) ,那么显然,将 \(y\) 弹出,直到队列长度只有 \(1\) ,或者 \(g(y,x) < g(p,y)\) ,再压入 \(p\) 。
为了优化常数,可以在队列中记录 \(r_x\) 。
#include <bits/stdc++.h>
using namespace std;
typedef long double LL;
const int maxn = 1e5 + 5;
const LL INF = 1e18;
int n, P, q[maxn], ql, qr;
LL L, s[maxn], fr[maxn], f[maxn];
string str[maxn];
LL qpow(LL a, int b) {
LL ret = 1;
while (b) {
if (b & 1) ret = ret * a;
a = a * a, b >>= 1;
}
return ret;
}
inline LL calc(int x, int y) { return f[x] + qpow(abs(s[y] - s[x] + (y - x - 1) - L), P); }
int bsearch(int x, int y) {
if (calc(x, n) < calc(y, n)) return n + 1;
int l = y, r = n, ans = -1;
while (l <= r) {
int mid = (l + r) >> 1;
if (calc(y, mid) <= calc(x, mid)) ans = mid, r = mid - 1;
else l = mid + 1;
}
return ans;
}
void print_plan(int x) {
if (!x) return ;
print_plan(fr[x]);
for (int i = fr[x] + 1; i < x; i++) cout << str[i], putchar(' ');
cout << str[x];
putchar('\n');
}
void solve() {
scanf("%d%Lf%d", &n, &L, &P);
memset(q, 0, sizeof(q)), fill(f, f + n + 1, INF);
for (int i = 1; i <= n; i++)
cin >> str[i], s[i] = s[i - 1] + str[i].size();
f[0] = q[ql = qr = 1] = 0;
for (int i = 1; i <= n; i++) {
while (ql < qr && bsearch(q[ql], q[ql + 1]) <= i) ql++;
f[i] = calc(q[ql], i), fr[i] = q[ql];
while (ql < qr && bsearch(q[qr - 1], q[qr]) >= bsearch(q[qr], i)) qr--;
q[++qr] = i;
}
if (f[n] > INF) { printf("Too hard to arrange\n--------------------\n"); return ; }
printf("%.0Lf\n", f[n]), print_plan(n);
printf("--------------------\n");
}
int main() {
int T; scanf("%d", &T);
while (T--) solve();
return 0;
}
[UOJ 285] 数据分块鸡
和上一题的转移方程非常类似,只不过 \(w(i,j)\) 的计算方法不同,需要使用数据结构优化,就不讲了。
[CF-868F] Yet Another Minimization Problem
\(\Rightarrow\) \(\rm luogu\) 链接
题意非常清楚明白,可以直接设出 \(f(i,j)\) 表示将前 \(j\) 个元素分成 \(i\) 段的最小权值,可以得到转移:
其中 \(w(i,j)\) 表示区间 \([l,r]\) 中重复元素的对数,可以看出这个函数满足四边形不等式,但是这个函数的求值时间复杂度是 \(O(n)\) 的,因此不能使用先前的单调队列优化了。这里提出另一种优化决策单调性的方法:分治。
这种方法适用于 \(\rm dp\) 的转移不依赖同一维状态的转移,比如这道题的转移 \(f(i,*)\) 依赖于 \(f(i-1,*)\) ,但是 \(f(i,*)\) 的状态之间不存在依赖关系。这个也可以计算形如下面的 "\(\rm dp\)"。:
设立一个函数 \(\mathrm{calc}(l, r, x, y)\) 用于计算状态在 \([l,r]\) 之间的所有 \(f\) 值,其中决策点只能在 \([x,y]\) 中找。
分为下面的步骤:
- 设 \(m = \left\lfloor \frac{l+r}{2}\right\rfloor\)
- 计算出 \(f(m)\) ,同时得到其最优决策点 \(p(m)\) 。
- 分治 \(\mathrm{calc}(l, m - 1, x, p(m)), \mathrm{calc}(m + 1,r,p(m), y)\)
如果 \(w(i,j)\) 的求值时间复杂度为 \(O(1)\) 的,这个分治过程显然是 \(O(n\log_2 n)\) 的。
但关键是现在 \(w(i,j)\) 的计算是 \(O(n)\) 的。
这里可以采用类似莫队的双指针移动,维护 \(w(i,j)\) 。首先可以想到将两个指针的移动分开计算复杂度。然后就会发现两个指针移动的次数都是 \(O(n\log_2n)\) 的。
因此一次分治的时间复杂度为 \(O(n\log_2 n)\) ,总的时间复杂度为 \(O(kn \log_2 n)\) 。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 5, maxk = 25;
const LL INF = 1e18;
int n, m, a[maxn];
int cl, cr; LL f[maxk][maxn], cnt[maxn], cw;
inline void upd_cnt(int col, int vl) {
cw -= cnt[col] * (cnt[col] - 1) / 2;
cnt[col] += vl;
cw += cnt[col] * (cnt[col] - 1) / 2;
}
LL calc_w(int l, int r) {
while (cl < l) upd_cnt(a[cl], -1), cl++;
while (cl > l) cl--, upd_cnt(a[cl], 1);
while (cr > r) upd_cnt(a[cr], -1), cr--;
while (cr < r) cr++, upd_cnt(a[cr], 1);
return cw;
}
int cur_m;
void solve(int l, int r, int fl, int fr) {
if (fl > fr || l > r) return ;
int pos = 0; LL res = INF;
int mid = (l + r) >> 1, rgl = max(1, fl), rgr = min(mid, fr);
for (int i = rgl; i <= rgr; i++) {
LL vl = f[cur_m - 1][i - 1] + calc_w(i, mid);
if (vl < res) res = vl, pos = i;
}
f[cur_m][mid] = res;
solve(l, mid - 1, fl, pos), solve(mid + 1, r, pos, fr);
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
cnt[a[1]] = 1, cw = 0, cl = cr = 1;
f[0][0] = 0;
for (int i = 1; i <= n; i++) f[0][i] = INF;
for (cur_m = 1; cur_m <= m; cur_m++)
solve(1, n, 1, n);
printf("%lld\n", f[m][n]);
return 0;
}
[POI2011]Lightning Conductor
\(\Rightarrow\) \(\rm luogu\) 链接
这题两种方法都可以,不讲了。