动态 DP 学习笔记
stripe_python · · 算法·理论
摘自 OI 中的数学基础第 22 章,属于未更新的第三版内容。
如果会基础线代可以跳过 22.1 和 22.2。
22.1 向量
以平面直角坐标系为例,一个向量可以看作由原点指向某点的一条有向线段,用
这条有向线段的长度叫做向量的模。对于二维向量
向量代表的是如何从原点移动到终点。所以两个向量相加定义为两次移动叠加的效果,相减就是相加的逆运算。可得
向量也可以做数乘运算,相当于缩放操作。有
22.2 线性变换
对于向量
注意到,一个二维线性变换仅由
定义矩阵与向量的乘法就是对这个向量应用线性变换:
同样地,多个线性变换也可以相互叠加。定义矩阵乘法
应当注意,
但是可以发现,
下面给出矩阵乘法的一般公式。对于
是一个
矩阵的加减法是简单的,就是同位置的数相加减。注意到矩阵加法满足交换律和结合律。
下面给出一个矩阵的板子。
template <class T, const int N>
struct matrix {
T val[N][N];
matrix() {clear();}
void clear() {memset(val, 0, sizeof(val));}
T* operator[] (int x) {return val[x];}
void reset() {
clear();
for (int i = 0; i < N; i++) val[i][i] = 1;
}
matrix<T, N>& operator+= (matrix<T, N> B) {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) val[i][j] += B[i][j];
} return *this;
}
matrix<T, N>& operator-= (matrix<T, N> B) {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) val[i][j] -= B[i][j];
} return *this;
}
matrix<T, N> operator+ (matrix<T, N> B) {return matrix<T, N>(*this) += B;}
matrix<T, N> operator- (matrix<T, N> B) {return matrix<T, N>(*this) -= B;}
matrix<T, N> operator* (matrix<T, N> B) {
const auto& A = *this;
matrix<T, N> C;
for (int i = 0; i < N; i++) {
for (int k = 0; k < N; k++) {
if (A[i][k] == 0) continue;
for (int j = 0; j < N; j++) C[i][j] += A[i][k] * B[k][j];
}
} return C;
}
matrix<T, N>& operator*= (matrix<T, N> B) {return *this = *this * B;}
};
template <class T, const int N>
matrix<T, N> mpow(matrix<T, N> a, long long b) {
matrix<T, N> res; res.reset();
for (; b; a *= a, b >>= 1) {
if (b & 1) res *= a;
} return res;
}
特别地,定义如下形式的矩阵是单位矩阵:
有
矩阵快速幂板子:P3390。
其实矩阵乘法能干的事情很多,比如加速数列递推。矩阵可以描述很多具有结合律的运算,方便我们使用线段树等数据结构维护信息。
22.3 广义矩阵乘法
一般地,定义矩阵乘法
这里的
这叫做广义矩阵乘法。这样的矩阵乘法记作
注意我们使用广义矩阵乘法时一般希望维护一个具有结合律的信息。下面我们给出
- 当且仅当
\oplus 满足交换律,\otimes 满足结合律,\otimes 对\oplus 满足分配律时,广义矩阵乘法有结合律。
例如矩阵乘法
常见的有结合律的广义矩阵乘法有:
template <class T, const int N, class Op1, class Op2, const T o = 0>
struct matrix {
Op1 op1{}; Op2 op2{};
T val[N][N];
T* operator[] (int x) {return val[x];}
matrix<T, N, Op1, Op2, o> operator* (matrix<T, N, Op1, Op2, o> B) {
auto& A = *this;
matrix<T, N, Op1, Op2, o> C;
for (int i = 0; i < N; i++) {
fill(C[i], C[i] + N, o);
for (int k = 0; k < N; k++) {
for (int j = 0; j < N; j++) C[i][j] = op1(C[i][j], op2(A[i][k], B[k][j]));
}
} return C;
}
matrix<T, N, Op1, Op2, o>& operator*= (matrix<T, N, Op1, Op2, o> B) {return *this = *this * B;}
};
template <class T>
struct AddOP {
constexpr T operator() (T a, T b) {return a + b;}
};
template <class T>
struct MinOP {
constexpr T operator() (T a, T b) {return min(a, b);}
};
template <class T>
struct MaxOP {
constexpr T operator() (T a, T b) {return max(a, b);}
};
在一些题目中,我们会使用数据结构来维护具有结合律的广义矩阵乘法。动态 DP 就使用了这种技巧。
22.4 例题
[模拟赛] 小 Z 爱优化
给出长为
n 的序列a ,可以将相邻的两个数字合并成一组,或一个数字单独成一组。最小化各组元素之和的极差。
考虑 DP。有两种 DP 都能得到
要求
这本身是一个线性 DP,而且这个转移可以想到
这里的乘法是
const int N = 1e6 + 5;
const long long inf = 1e18;
long long n, a[N];
using node = matrix<long long, 2, MaxOP<long long>, MinOP<long long>, -inf>;
struct segtree {
#define ls (rt << 1)
#define rs (rt << 1 | 1)
node I, B, sum[N << 2];
segtree() {
I[0][0] = inf, I[0][1] = -inf, I[1][0] = -inf, I[1][1] = inf;
B[0][0] = -inf, B[0][1] = inf, B[1][0] = -inf, B[1][1] = -inf;
}
void pushup(int rt) {sum[rt] = sum[ls] * sum[rs];}
void build(int l = 1, int r = n, int rt = 1) {
if (l == r) return sum[rt] = B, void();
int mid = (l + r) >> 1;
build(l, mid, ls), build(mid + 1, r, rs), pushup(rt);
}
void update(int x, long long c, int typ, int l = 1, int r = n, int rt = 1) {
if (l == r) return sum[rt][typ][0] = c, void();
int mid = (l + r) >> 1;
if (x <= mid) update(x, c, typ, l, mid, ls);
else update(x, c, typ, mid + 1, r, rs);
pushup(rt);
}
} sgt;
vector<tuple<int, int, int>> vec;
void _main() {
//debug(sizeof(f) / 1048576.0);
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
vec.clear();
for (int i = 1; i <= n; i++) {
vec.emplace_back(a[i], i, 0);
if (i > 1) vec.emplace_back(a[i - 1] + a[i], i, 1);
}
sort(vec.begin(), vec.end());
sgt.build();
long long res = inf;
for (const auto& info : vec) {
long long v = get<0>(info), x = get<1>(info), typ = get<2>(info);
sgt.update(x, v, typ);
node A; A[0][0] = A[0][1] = inf, A[1][0] = A[1][1] = -inf, A *= sgt.sum[1];
//mdebug(v, A[0][0]);
res = min(res, v - A[0][0]);
} cout << res << '\n';
}
P4719 【模板】动态 DP
前置知识:重链剖分。
其实并不板。考虑不带修的做法,经典 DP。设
答案为
观察转移方程,每次更改点权只需修改一条树链。但如果随意找,每次更新的复杂度会达到
对树作一次重链剖分。这样每个点到根的路径上只会经过
其中
变形:
定义
此时的矩阵应该写在左边。因为重剖过程是先链头后链尾。于是我们在 DFS 序上建一棵线段树,问题解决。复杂度
const int N = 1e5 + 5, inf = 1e9;
using node = matrix<int, 2, MaxOP<int>, AddOP<int>, -inf>;
int n, q, a[N], u, v;
int tot = 0, head[N];
struct Edge {
int next, to;
} edge[N << 1];
inline void add_edge(int u, int v) {
edge[++tot].next = head[u], edge[tot].to = v, head[u] = tot;
}
int num, sz[N], fa[N], son[N], top[N], dfn[N], ed[N], id[N], f[2][N], g[2][N];
void dfs1(int u) {
sz[u] = 1;
for (int j = head[u]; j != 0; j = edge[j].next) {
int v = edge[j].to;
if (v == fa[u]) continue;
fa[v] = u, dfs1(v), sz[u] += sz[v];
if (sz[v] > sz[son[u]]) son[u] = v;
}
}
void dfs2(int u, int t) {
top[u] = t, dfn[u] = ++num, id[num] = u;
if (!son[u]) return ed[t] = num, void();
dfs2(son[u], t);
for (int j = head[u]; j != 0; j = edge[j].next) {
int v = edge[j].to;
if (v != fa[u] && v != son[u]) dfs2(v, v);
}
}
void dfs3(int u) {
f[1][u] = g[1][u] = a[u];
for (int j = head[u]; j != 0; j = edge[j].next) {
int v = edge[j].to;
if (v == fa[u]) continue;
dfs3(v);
f[0][u] += max(f[0][v], f[1][v]), f[1][u] += f[0][v];
if (v != son[u]) g[0][u] += max(f[0][v], f[1][v]), g[1][u] += f[0][v];
}
}
node B[N];
struct segtree {
#define ls (rt << 1)
#define rs (rt << 1 | 1)
node sum[N << 2];
void pushup(int rt) {sum[rt] = sum[ls] * sum[rs];}
void build(int l = 1, int r = n, int rt = 1) {
if (l == r) {
sum[rt][0][0] = sum[rt][0][1] = g[0][id[l]];
sum[rt][1][0] = g[1][id[l]], sum[rt][1][1] = -inf;
B[l] = sum[rt];
return;
}
int mid = (l + r) >> 1;
build(l, mid, ls), build(mid + 1, r, rs), pushup(rt);
}
void change(int x, int l = 1, int r = n, int rt = 1) {
if (l == r) return sum[rt] = B[l], void();
int mid = (l + r) >> 1;
if (x <= mid) change(x, l, mid, ls);
else change(x, mid + 1, r, rs);
pushup(rt);
}
node query(int tl, int tr, int l = 1, int r = n, int rt = 1) {
if (tl <= l && r <= tr) return sum[rt];
int mid = (l + r) >> 1;
if (tr <= mid) return query(tl, tr, l, mid, ls);
if (tl > mid) return query(tl, tr, mid + 1, r, rs);
return query(tl, tr, l, mid, ls) * query(tl, tr, mid + 1, r, rs);
}
} sgt;
node ask(int x) {return sgt.query(dfn[x], ed[top[x]]);}
void change(int u, int c) {
B[dfn[u]][1][0] += c - a[u], a[u] = c;
for (; u; u = fa[top[u]]) {
node last = ask(top[u]);
sgt.change(dfn[u]);
node cur = ask(top[u]);
int p = dfn[fa[top[u]]];
B[p][0][0] += max(cur[0][0], cur[1][0]) - max(last[0][0], last[1][0]);
B[p][0][1] += max(cur[0][0], cur[1][0]) - max(last[0][0], last[1][0]);
B[p][1][0] += cur[0][0] - last[0][0];
}
}
void _main() {
cin >> n >> q;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i < n; i++) cin >> u >> v, add_edge(u, v), add_edge(v, u);
dfs1(1), dfs2(1, 1), dfs3(1), sgt.build();
while (q--) {
cin >> u >> v;
change(u, v);
node res = ask(1);
cout << max(res[0][0], res[1][0]) << '\n';
}
}
P5024 [NOIP 2018 提高组] 保卫王国
注意到,最小覆盖集 = 全集 - 最大独立集。
于是和模板题的区别在于不是修改点权,而是强制两个点是否属于最大独立集,同时操作独立。
把点权改成正 / 负无穷即可实现强制属于独立集。于是套用模板题的做法就行了。
P8820 [CSP-S 2022] 数据传输
困难的。从部分分开始思考。
将这条链拿出来 DP。设
考虑树上动态 DP。定义
待定系数容易得到
这里为了与
仍然将链拿出来,形成一个毛毛虫结构。将 LCA 的父亲也视为儿子。
设
考虑动态 DP。我们需要把
耐心地推出转移矩阵:
用树上倍增维护一条链的正序积和倒序积,可以做到
为什么不用树剖维护?
树剖直接维护只能做正序积,无法维护倒序积,需要重推一遍转移矩阵。但是树上倍增的过程中直接交换递推顺序是对的。
const int N = 2e5 + 5;
const long long inf = 1e15;
using node = matrix<long long, 3, MinOP<long long>, AddOP<long long>, inf>;
int n, q, k, u, v;
long long a[N];
int tot = 0, head[N];
struct Edge {
int next, to;
} edge[N << 1];
inline void add_edge(int u, int v) {
edge[++tot].next = head[u], edge[tot].to = v, head[u] = tot;
}
int dep[N], sz[N], fa[N], son[N], top[N];
long long s[N], dis[N];
void dfs1(int u) {
sz[u] = 1, dis[u] = dis[fa[u]] + a[u], dep[u] = dep[fa[u]] + 1;
for (int j = head[u]; j != 0; j = edge[j].next) {
int v = edge[j].to;
if (v == fa[u]) continue;
fa[v] = u, dfs1(v), sz[u] += sz[v];
if (sz[v] > sz[son[u]]) son[u] = v;
}
}
void dfs2(int u, int t) {
top[u] = t;
if (son[u]) dfs2(son[u], t);
for (int j = head[u]; j != 0; j = edge[j].next) {
int v = edge[j].to;
if (v != fa[u] && v != son[u]) dfs2(v, v);
}
}
int lca(int u, int v) {
while (top[u] != top[v]) {
if (dep[top[u]] < dep[top[v]]) swap(u, v);
u = fa[top[u]];
} return dep[u] < dep[v] ? u : v;
}
int pa[20][N];
node I, B[N], mul[20][N], rmul[20][N];
void dfs3(int u) {
pa[0][u] = fa[u], mul[0][u] = rmul[0][u] = B[u];
for (int i = 1; i < 20; i++) {
pa[i][u] = pa[i - 1][pa[i - 1][u]];
mul[i][u] = mul[i - 1][u] * mul[i - 1][pa[i - 1][u]];
rmul[i][u] = rmul[i - 1][pa[i - 1][u]] * rmul[i - 1][u];
}
for (int j = head[u]; j != 0; j = edge[j].next) {
int v = edge[j].to;
if (v == fa[u]) continue;
dfs3(v);
}
}
node ask(int u, int v) {
node x = I, y = I;
if (dep[u] < dep[v]) y = B[v], v = fa[v];
for (int i = 19; i >= 0; i--) {
if (dep[pa[i][u]] >= dep[v]) x = rmul[i][u] * x, u = pa[i][u];
}
if (u == v) return y * B[u] * x;
for (int i = 19; i >= 0; i--) {
if (pa[i][u] == pa[i][v]) continue;
x = rmul[i][u] * x, y = y * mul[i][v];
u = pa[i][u], v = pa[i][v];
} return y * B[v] * B[fa[v]] * B[u] * x;
}
void _main() {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i != j) I[i][j] = inf;
}
}
cin >> n >> q >> k;
for (int i = 1; i <= n; i++) cin >> a[i];
fill(s + 1, s + n + 1, inf);
for (int i = 1; i < n; i++) {
cin >> u >> v;
add_edge(u, v), add_edge(v, u);
s[u] = min(s[u], a[v]), s[v] = min(s[v], a[u]);
}
dfs1(1), dfs2(1, -1);
if (k == 1) {
while (q--) {
cin >> u >> v;
int f = lca(u, v);
cout << dis[u] + dis[v] - 2 * dis[f] + a[f] << '\n';
} return;
}
B[0] = I;
for (int i = 1; i <= n; i++) {
node& A = B[i];
if (k == 2) {
A[0][0] = A[0][1] = a[i];
A[1][0] = A[2][2] = 0;
A[0][2] = A[1][1] = A[1][2] = A[2][0] = A[2][1] = inf;
} else {
A[0][0] = A[0][1] = A[0][2] = a[i];
A[1][0] = A[2][1] = 0;
A[1][1] = s[i], A[1][2] = a[i] + s[i];
A[2][0] = A[2][2] = inf;
}
}
dfs3(1);
while (q--) {
cin >> u >> v;
if (u == v) {cout << a[u] << '\n'; continue;}
if (dep[u] < dep[v]) swap(u, v);
node x = ask(fa[u], v), y = I;
if (k == 2) y[0][0] = a[u];
else y[0][0] = a[u], y[0][1] = a[u] + s[u];
x *= y, cout << x[0][0] << '\n';
}
}