浅谈 DP 优化 1
Loyal_Soldier · · 算法·理论
虽然但是好像并不浅
由于本人是小升区 xxs,DP 学的不多,大部分内容都是边学边写,文章有错误还请多多谅解。
本篇文章学习了以下文章:
- dp 优化
- 四边形不等式 & 决策单调性
1. 单调队列优化 \text{DP}
1.1 一般形式
前置知识:单调队列。
单调队列往往可以优化形如以下的 DP:
其中
把
1.2 例题
1.2.1 \text{P1725}
模板题。
显然,转移方程为:
直接上单调队列优化即可。
:::success[code]
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 2e5 + 10;
int n, l, r;
int a[MAXN];
int dp[MAXN];
int que[MAXN];
int ans = LLONG_MIN;
signed main() {
cin >> n >> l >> r;
for (int i = 0; i <= n; i++) cin >> a[i];
memset(dp, -0x3f, sizeof(dp));
dp[0] = 0;
int head = 1, tail = 1;
for (int i = l; i <= n; i++) {
while (head <= tail && dp[i - l] >= dp[que[tail]]) tail--;
que[++tail] = i - l;
while (que[head] + r < i) head++;
dp[i] = dp[que[head]] + a[i];
if (i + r > n) ans = max(ans, dp[i]);
}
cout << ans;
return 0;
}
:::
1.2.2 \text{P2569}
设
考虑转移:
- 凭空买:
dp_{i, j} = -ap_i\times j 。 - 什么都不干:
dp_{i, j} = \max(dp_{i, j}, dp_{i - 1, j}) 。 - 买股票:
dp_{i, j} = \max(dp_{i, j}, \max_{k = j - as_i}^{j - 1} dp_{i - w - 1, k} - (j - k)\times ap_i) 。 - 卖股票:
dp_{i, j} = \max(dp_{i, j}, \max_{k = j + 1}^{j + bs_i} dp_{i - w - 1, k} + (k - j) \times bp_i) 。
以第三种转移为例(第四种同理),将包含
显然,可以单调队列优化。
:::success[code]
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 2010;
int n, maxp, w;
int ap[MAXN], bp[MAXN], as[MAXN], bs[MAXN];
int dp[MAXN][MAXN];
int q[MAXN];
signed main() {
// ios::sync_with_stdio(0);
// cin.tie(0), cout.tie(0);
cin >> n >> maxp >> w;
for (int i = 1; i <= n; i++) cin >> ap[i] >> bp[i] >> as[i] >> bs[i];
memset(dp, -0x3f, sizeof(dp));
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= as[i]; j++) dp[i][j] = -1 * ap[i] * j;
for (int j = 0; j <= maxp; j++) dp[i][j] = max(dp[i][j], dp[i - 1][j]);
if (i <= w) continue;
int head = 1, tail = 0;
for (int j = 0; j <= maxp; j++) {
while (head <= tail && q[head] < j - as[i]) head++;
while (head <= tail && dp[i - w - 1][q[tail]] + q[tail] * ap[i] <= dp[i - w - 1][j] + j * ap[i]) tail--;
q[++tail] = j;
if (head <= tail) dp[i][j] = max(dp[i][j], dp[i - w - 1][q[head]] - (j - q[head]) * ap[i]);
}
head = 1, tail = 0;
for (int j = maxp; j >= 0; j--) {
while (head <= tail && q[head] > j + bs[i]) head++;
while (head <= tail && dp[i - w - 1][q[tail]] + q[tail] * bp[i] <= dp[i - w - 1][j] + j * bp[i]) tail--;
q[++tail] = j;
if (head <= tail) dp[i][j] = max(dp[i][j], dp[i - w - 1][q[head]] + (q[head] - j) * bp[i]);
}
}
int ans = 0;
for (int j = 0; j <= maxp; j++) ans = max(ans, dp[n][j]);
cout << ans;
return 0;
}
:::
1.3 习题
\text{P3522} \text{P2885} \text{P3957} \text{P2254} \text{P2300}
2. 斜率优化 \text{DP}
2.0 前置
对于一个斜截式
y = kx + b ,其中k 就是斜率,b 就是截距。
首先,我们考虑这样一个问题:平面上有
我们假设有四个点
证明略。
因此,将所有可能取到最值的点拿出来,会发现构成一个凸壳(一个包住所有点的凸多边形,凸多边形上不会有三点共线)。其中下凸壳可以取到最小值,并且下凸壳上从左往右相邻两点斜率递增;上凸壳可以取到最大值,并且上凸壳上从左往右相邻两点斜率递减。
2.1 基础部分
本节均以
\text{P3195} 为例。
2.1.1 方程改写
设
我们把
我们将转移中的项分为四项(形如
将转移方程写成
2.1.2 数学推导
我们将
于是,我们需要最小化截距
那么,问题就变成了:平面上有
根据之前的结论,截距会在下凸壳取到最小值。
考虑怎么求最小值。设点
由于斜率是单调递增的,如果当前
最后,考虑维护插入点
2.1.3 代码实现
我们使用单调队列维护下凸壳的所有点,每次计算
时间复杂度为
写代码时注意精度误差。
:::success[code]
#include <bits/stdc++.h>
#define int long long
#define ld long double
using namespace std;
const int MAXN = 5e4 + 10;
int n, L;
int head = 1, tail;
int q[MAXN], s[MAXN], dp[MAXN], a[MAXN];
//坐标和斜率
int X(int i) { return s[i] + i + L + 1;}
int Y(int i) { return dp[i] + (s[i] + i + L + 1) * (s[i] + i + L + 1);}
ld slope(int i, int j) { return (ld)(Y(i) - Y(j)) / (ld)(X(i) - X(j));}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> L;
// L++;
for (int i = 1; i <= n; i++) {
cin >> a[i];
s[i] = s[i - 1] + a[i];
}
head = tail = 1;
for (int i = 1; i <= n; i++) {
//弹出下凸壳斜率不超过 k 的点
while (head < tail && slope(q[head + 1], q[head]) <= 2 * (s[i] + i)) head++;
dp[i] = Y(q[head]) - 2 * (s[i] + i) * X(q[head]) + (s[i] + i) * (s[i] + i);
//弹出下凸壳不满足斜率单调递增的点
while (head < tail && slope(i, q[tail]) <= slope(q[tail], q[tail - 1])) tail--;
q[++tail] = i;
}
cout << dp[n];
return 0;
}
:::
2.2 例题
2.2.1 \text{P5784}
设
列出状态转移方程:
斜率优化,把
显然维护下凸壳。
但是,注意到
我们依旧维护下凸壳,
时间复杂度
:::success[code]
#include <bits/stdc++.h>
#define int long long
#define ld long double
using namespace std;
const int MAXN = 3e5 + 10;
int head = 1, tail;
int q[MAXN], n, s;
int dp[MAXN], P[MAXN], Q[MAXN];
int X(int i) { return Q[i];}
int Y(int i) { return dp[i] - s * Q[i];}
ld slope(int i, int j) {
if (X(i) == X(j)) return 1e18;
return (ld)(Y(i) - Y(j)) / (ld)(X(i) - X(j));
}
int Search(int l, int r, ld x) {
int L = l, R = r - 1, ans = -1;
while (L <= R) {
int mid = (L + R) >> 1;
if (slope(q[mid], q[mid + 1]) >= x) ans = mid, R = mid - 1;
else L = mid + 1;
}
if (ans == -1) return q[r];
return q[ans];
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> s;
for (int i = 1; i <= n; i++) {
int t, c;
cin >> t >> c;
P[i] = P[i - 1] + t;
Q[i] = Q[i - 1] + c;
}
memset(dp, 0x3f, sizeof(dp));
dp[0] = 0;
tail = 1;
q[1] = 0;
for (int i = 1; i <= n; i++) {
int pos = Search(head, tail, (ld)(P[i]));
dp[i] = dp[pos] + (P[i] + s) * (Q[i] - Q[pos]) + s * (Q[n] - Q[i]);
while (head < tail && slope(q[tail - 1], q[tail]) >= slope(q[tail], i)) tail--;
q[++tail] = i;
}
cout << dp[n];
return 0;
}
:::
2.2.2 \text{P2900}
注意到对于
显然,最终答案选的一定会是一段连续的区间。
设
如果
也就是
显然可以维护一个下凸壳然后斜率优化。
:::success[code]
#include <bits/stdc++.h>
#define int long long
#define ld long double
using namespace std;
const int MAXN = 5e4 + 10;
int n;
int q[MAXN], head = 1, tail;
int dp[MAXN];
struct node {
int x, y;
} a[MAXN];
bool cmp(node a, node b) {
if (a.x == b.x) return a.y > b.y;
return a.x > b.x;
}
int X(int i) { return a[i + 1].x;}
int Y(int i) { return dp[i];}
ld slope(int i, int j) { return (ld)(Y(i) - Y(j)) / (ld)(X(j) - X(i));}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i].x >> a[i].y;
sort(a + 1, a + n + 1, cmp);
int tot = 0;
for (int i = 1; i <= n; i++)
if (a[i].y > a[tot].y) a[++tot] = a[i];
n = tot;
tail = 1;
for (int i = 1; i <= n; i++) {
while (head < tail && slope(q[head], q[head + 1]) <= (ld)(a[i].y)) head++;
dp[i] = dp[q[head]] + a[q[head] + 1].x * a[i].y;
while (head < tail && slope(q[tail - 1], q[tail]) >= slope(q[tail], i)) tail--;
q[++tail] = i;
}
cout << dp[n];
return 0;
}
:::
2.2.3 \text{P6302}
设
最终答案为
斜率优化,去掉
斜率和横坐标递增,使用单调队列维护下凸壳。
但是我们发现
时间复杂度
:::success[code]
#include <bits/stdc++.h>
#define int long long
#define ld long double
using namespace std;
const int MAXN = 2e6 + 10;
const int inf = 1145141919810;
int n, m, A, B, C;
int ans = inf;
int id[MAXN], pos[MAXN];//id: 列车原编号->排序后位置; pos: b 数组索引->对应的 a 数组索引
int dp[MAXN], cnt[MAXN];//dp: 到达某列车(按 a 排序)的最小烦躁值; cnt: 到达某站点的列车数
int head[MAXN], tail[MAXN];
vector <int> v[MAXN];//每个站点的凸壳队列,存储 b 数组的索引
struct node {
int x, y;
int p, q;
int id;
} a[MAXN], b[MAXN];
bool cmp(node a, node b) { return a.p < b.p;}
bool cmp1(node a, node b) { return a.q < b.q;}
int X(int i) { return 2 * A * b[i].q;}
int Y(int i) { return dp[pos[i]] + A * b[i].q * b[i].q - B * b[i].q;}
ld slope(int i, int j) {
if (X(i) == X(j)) {
if (Y(i) > Y(j)) return inf;
else return -inf;
}
return (ld)(Y(i) - Y(j)) / (ld)(X(i) - X(j));
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> A >> B >> C;
memset(dp, -1, sizeof(dp));
dp[0] = 0, dp[2000000] = inf;//0 号节点是虚拟起点,2000000 是虚拟哨兵
pos[2000000] = 2000000;//哨兵映射到自己
for (int i = 1; i <= m; i++) {
cin >> a[i].x >> a[i].y >> a[i].p >> a[i].q;
a[i].id = i;
cnt[a[i].y]++;
b[i] = a[i];
}
sort(a + 1, a + m + 1, cmp);
sort(b + 1, b + m + 1, cmp1);
for (int i = 1; i <= n; i++) {
v[i].resize(cnt[i] + 10);
head[i] = tail[i] = 1;
if (i > 1) v[i][1] = 2000000;
}
for (int i = 1; i <= m; i++) id[a[i].id] = i;
for (int i = 1; i <= m; i++) pos[i] = id[b[i].id];
int idx = 0;
for (int i = 1; i <= m; i++) {
while (idx < m && b[idx + 1].q <= a[i].p) {
//将所有到达时间 <= 当前列车出发时间的列车加入凸壳
idx++;
while (head[b[idx].y] < tail[b[idx].y] && slope(v[b[idx].y][tail[b[idx].y]], v[b[idx].y][tail[b[idx].y] - 1]) > slope(idx, v[b[idx].y][tail[b[idx].y]])) tail[b[idx].y]--;
++tail[b[idx].y];
v[b[idx].y][tail[b[idx].y]] = idx;
}
while (head[a[i].x] < tail[a[i].x] && slope(v[a[i].x][head[a[i].x] + 1], v[a[i].x][head[a[i].x]]) <= (ld)(a[i].p)) ++head[a[i].x];
dp[i] = dp[pos[v[a[i].x][head[a[i].x]]]] + A * (a[i].p - b[v[a[i].x][head[a[i].x]]].q) * (a[i].p - b[v[a[i].x][head[a[i].x]]].q) + B * (a[i].p - b[v[a[i].x][head[a[i].x]]].q) + C;
if (a[i].y == n) ans = min(ans, dp[i] + a[i].q);
}
cout << ans;
return 0;
}
:::
2.2.4 \text{P4655}
设
这是斜率式,但是
\text{CDQ} 分治
套路的将转移方程拆开平方并化成
显然维护下凸壳。
考虑
- 对左边进行分治。
- 维护左边凸壳。
- 对右边按斜率排序。
- 对右边进行分治。
- 对整个区间按
x 排序,维护下一次凸壳
时间复杂度:
代码没写。
李超线段树
将转移方程拆开平方并化简:
观察李超线段树的作用:有多条线段,求单点最值。
于是令
那么,令
时间复杂度:
:::success[code]
#include <bits/stdc++.h>
#define int long long
#define db double
#define pii pair <int, int>
#define fi first
#define se second
using namespace std;
const int MAXN = 2e6 + 10;
const int N = 1e6;
const db eps = 1e-9;
int n;
int a[MAXN], b[MAXN];
int h[MAXN], W[MAXN], s[MAXN];
int dp[MAXN];
int cmp(int a, int b) {
if (a - b > 0) return -1;
if (b - a > 0) return 1;
return 0;
}
int w[MAXN];
int tot;
db calc(int id, int d) { return a[id] * d + b[id];}
void upd(int u, int l, int r, int x) {
if (w[u] == 0) {
w[u] = x;
return;
}
if (l == r) {
if (calc(x, l) < calc(w[u], l)) w[u] = x;
return;
}
int y = w[u];
int mid = (l + r) >> 1;
int bmid = cmp(calc(x, mid), calc(y, mid));
if (bmid == 1) {
swap(w[u], x);
y = w[u];
}
int L = cmp(calc(x, l), calc(y, l)), R = cmp(calc(x, r), calc(y, r));
if (L == 1) upd(u << 1, l, mid, x);
else if (R == 1) upd(u << 1 | 1, mid + 1, r, x);
}
pii Min(pii a, pii b) {
if (cmp(a.fi, b.fi) == 1) return a;
else if (cmp(a.fi, b.fi) == -1) return b;
else return a.se < b.se ? a : b;
}
pii query(int u, int l, int r, int d) {
if (r < d || l > d) return {2e18, 0};
int mid = (l + r) >> 1;
int res = calc(w[u], d);
if (l == r) return {res, w[u]};
return Min({res, w[u]}, Min(query(u << 1, l, mid, d), query(u << 1 | 1, mid + 1, r, d)));
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
b[0] = 2e18;
for (int i = 1; i <= n; i++) cin >> h[i];
for (int i = 1; i <= n; i++) {
cin >> W[i];
s[i] = s[i - 1] + W[i];
}
a[1] = -2 * h[1], b[1] = 0 + h[1] * h[1] - s[1];
upd(1, 0, N, 1);
for (int i = 2; i <= n; i++) {
dp[i] = h[i] * h[i] + s[i - 1] + query(1, 0, N, h[i]).fi;
a[i] = -2 * h[i], b[i] = dp[i] + h[i] * h[i] - s[i];
upd(1, 0, N, i);
}
cout << dp[n];
return 0;
}
:::
2.3 习题
\text{P3628} \text{P4360} \text{P10979} \text{P5504} \text{P4027} \text{P2305}
3. 决策单调性优化 \text{DP}
3.1 基础定义
对于一维 DP:
我们记每一个点
如果
3.2 四边形不等式
定义
1 :已知w(i, j) (1\le i < j\le n) ,若对任意x_1\le x_2\le y_1\le y_2 ,有w(x_1, y_1) + w(x_2, y_2)\le w(x_1, y_2) + w(x_2, y_1) ,则称该函数满足四边形不等式。 简单记为:交叉小于包含。定义
2 :若对任意x, y(x\le y) ,有w(x, y) + w(x + 1, y + 1)\le w(x, y + 1) + w(x + 1, y) 。定理:对于 DP 式
f ,若其w 满足四边形不等式,则f 满足决策单调性。
:::info[定理的证明] 考虑反证法。
我们假设
根据
由于
与前面的式子相加得:
对于
证毕。 :::
3.3 优化
若 DP 满足决策单调性,可以采用以下优化。
3.3.1 分治
适用于方程不含
我们定义
首先,设中点
根据决策单调性,计算
显然复杂度为
:::success[code]
void solve(int l, int r, int L, int R) {
if (l > r) return;
int mid = (l + r) >> 1, p = 1;
for (int i = L; i <= min(mid, R); i++)
if (w(i, mid) > dp[mid]) dp[mid] = w(i, mid), p = i;
solve(l, mid - 1, L, p);
solve(mid + 1, r, p, R);
}
:::
3.3.2 二分队列
适用于“半在线”的情况。
由于每个决策点更新范围是一个连续区间,所以相邻决策点存在一个分界线
于是我们用一个队列
当加入一个
- 移除过时的决策点,如果队头
head 的k\le i ,则弹出队头。 - 当前队头是最优决策点,更新
dp_i 。 - 弹出队尾无用决策点,如果
i 比队尾更优,至于如何判断,可以二分求出队尾决策点和i 的交点(即新决策开始优于队尾决策的位置),如果这个交点\le 队尾前一个位置的起始位置,则队尾不可能成为最优决策点,弹出。 - 二分求出队尾新的
k 值(i 和当前队尾的交点),然后插入i 。
:::success[code]
int Search(int x, int y) {
int l = x, r = n + 1;
while (l < r) {
int mid = (l + r) >> 1;
if (calc(mid, x) >= calc(mid, y)) r = mid;
else l = mid + 1;
}
return l;
}
for (int i = 1; i <= n; i++) {
while (head < tail && k[head] <= i) head++;
dp[i] = calc(i, q[head]);
opt[i] = q[head];
while (head < tail && k[tail - 1] >= Search(q[tail], i)) tail--;
k[tail] = Search(q[tail], i);
q[++tail] = i;
}
:::
3.4 例题
3.4.1 \text{P3515}
显然,题目中让我们求
这个绝对值很烦,考虑分成
我们设
:::info[证明]
对于
将
两边平方一下:
化简:
因为
然后套模板分治即可。
:::success[code]
#include <bits/stdc++.h>
#define int long long
#define db double
using namespace std;
const int MAXN = 5e5 + 10;
int n, a[MAXN];
db dp[MAXN];
db w(int j, int i) { return (db)(a[j]) + sqrt(i - j);}
void solve(int l, int r, int L, int R) {
if (l > r) return;
int mid = (l + r) >> 1, p = 1;
for (int i = L; i <= min(mid, R); i++)
if (w(i, mid) > dp[mid]) dp[mid] = w(i, mid), p = i;
solve(l, mid - 1, L, p);
solve(mid + 1, r, p, R);
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
solve(1, n, 1, n);
reverse(a + 1, a + n + 1);
reverse(dp + 1, dp + n + 1);
solve(1, n, 1, n);
for (int i = n; i >= 1; i--) cout << (int)ceil(dp[i]) - a[i] << '\n';
return 0;
}
:::
3.4.2 \text{P6932}
对于两家生产公司
处理后生产公司按
我们定义利润函数
::::info[证明]
对于所有
我们定义差值:
对于四边形不等式成立,就是
我们将
由于
判断完可以发现,对于第一项
证毕。 ::::
套模板分治即可。
::::success[code]
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 5e5 + 10;
int n, m, dp[MAXN];
struct node {
int x, y;
} a[MAXN], b[MAXN];
bool cmp1(node a, node b) {
if (a.x == b.x) return a.y < b.y;
return a.x < b.x;
}
bool cmp2(node a, node b) {
if (a.x == b.x) return a.y > b.y;
return a.x > b.x;
}
int w(int i, int j) {
if (b[i].x <= a[j].x || b[i].y <= a[j].y) return 0;
return (b[i].x - a[j].x) * (b[i].y - a[j].y);
}
void solve(int l, int r, int L, int R) {
if (l > r) return;
int mid = (l + r) >> 1, p = R;
for (int i = L; i <= R; i++)
if (w(i, mid) > dp[mid]) dp[mid] = w(i, mid), p = i;
solve(l, mid - 1, p, R);
solve(mid + 1, r, L, p);
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i].x >> a[i].y;
sort(a + 1, a + n + 1, cmp1);
int tot = 1;
for (int i = 2; i <= n; i++)
if (a[i].y < a[tot].y) a[++tot] = a[i];
n = tot;
for (int i = 1; i <= m; i++) cin >> b[i].x >> b[i].y;
sort(b + 1, b + m + 1, cmp2);
tot = 1;
for (int i = 2; i <= m; i++)
if (b[i].y > b[tot].y) b[++tot] = b[i];
m = tot;
solve(1, n, 1, m);
int ans = 0;
for (int i = 1; i <= n; i++) ans = max(ans, dp[i]);
cout << ans;
return 0;
}
::::
3.4.3 \text{P1912}
设
注意到
:::info[证明]
若代价函数
则最优决策点
验证
则
令
令
故原不等式成立,由判定定理知
证毕。 :::
然后套模板二分队列即可。
:::success[code]
#include <bits/stdc++.h>
#define int long long
#define ld long double
using namespace std;
const int MAXN = 1e5 + 10;
int n, l, p;
int s[MAXN], q[MAXN], k[MAXN], opt[MAXN];
ld dp[MAXN];
string str[MAXN];
ld qpow(ld a, int b) {
ld res = 1;
while (b) {
if (b & 1) res = res * a;
a = a * a;
b >>= 1;
}
return res;
}
ld calc(int i, int j) { return dp[j] + qpow(abs(s[i] - s[j] - l), p);}
int Search(int x, int y) {
int l = x, r = n + 1;
while (l < r) {
int mid = (l + r) >> 1;
if (calc(mid, x) >= calc(mid, y)) r = mid;
else l = mid + 1;
}
return l;
}
signed main() {
int T;
cin >> T;
while (T--) {
cin >> n >> l >> p;
l++;
for (int i = 1; i <= n; i++) {
cin >> str[i];
s[i] = s[i - 1] + str[i].size() + 1;
}
int head = 1, tail = 0;
tail++;
q[1] = 0;
k[1] = 0;
for (int i = 1; i <= n; i++) {
while (head < tail && k[head] <= i) head++;
dp[i] = calc(i, q[head]);
opt[i] = q[head];
while (head < tail && k[tail - 1] >= Search(q[tail], i)) tail--;
k[tail] = Search(q[tail], i);
q[++tail] = i;
}
if (dp[n] > 1e18) {
cout << "Too hard to arrange\n--------------------\n";
continue;
}
printf("%.0Lf\n", dp[n]);
tail = 0;
q[0] = n;
for (int i = n; i; i = opt[i]) q[++tail] = opt[i];
for (; tail; tail--) {
for (int i = q[tail] + 1; i < q[tail - 1]; i++) cout << str[i] << ' ';
cout << str[q[tail - 1]] << '\n';
}
cout << "--------------------\n";
}
return 0;
}
:::
3.5 习题
\text{P4072} \text{P2877} \text{P5892} \text{P10786} \text{P11847}