写在前面
最大子段和和 GSS1 的题解区还没有下面这种做法,你们快上啊(
广义矩阵乘法
对于一 \(p\times m\) 的矩阵 \(A\),与 \(m\times q\) 的矩阵 \(B\),定义广义矩阵乘法 \(A\times B = C\) 的结果是一个 \(p\times q\) 的矩阵 \(C\),满足:
\[C_{i,j} = (A_{i, 1}\otimes B_{1,j}) \oplus (A_{i,2}\otimes B_{2,j})\oplus \cdots \oplus (A_{i, n}\otimes B_{n,j}) \]其中 \(\oplus\) 与 \(\otimes\) 是两种二元运算。
考察这种广义矩阵乘法是否满足结合律:
\[\begin{aligned} ((AB)C)_{i,j} &= \bigoplus_{k=1}^{p}(AB)_{i,k}\otimes C_{k,j}\\ &= \bigoplus_{k=1}^{p}\left( \left(\bigoplus_{t=1}^{q} A_{i,t}\otimes B_{t,k}\right) \otimes C_{k,j}\right)\\ (A(BC))_{i,j} &= \bigoplus_{t=1}^{q}A_{i,t}\otimes (BC)_{t,j}\\ &= \bigoplus_{t=1}^{q} \left(A_{i,t}\otimes \left(\bigoplus_{k=1}^{p} B_{t,k} \otimes C_{k,j}\right)\right) \end{aligned}\]显然,若 \(\otimes\) 运算满足交换律、结合律,且 \(\otimes\) 对 \(\oplus\) 存在分配律,即存在 \(\left(\bigoplus a\right)\otimes b = \bigoplus \left( a\otimes b \right)\) 时,广义矩阵乘法满足结合律。即根据上述运算规律,对二式进行 \(\oplus\) 的交换后有:
\[((AB)C)_{i,j} = (A(BC))_{i,j} = \bigoplus_{k=1}^{p}\bigoplus_{t=1}^{q} A_{i,t}\otimes B_{t,k}\otimes C_{k,j} \]维护 DP
以 P1115 最大子段和 为例。
给定一个长度为 \(n\) 的数列 \(a\),选出其中连续且非空的一段使得这段和最大。
\(1\le n\le 2\times 10^5\),\(-10^4\le a_i\le 10^4\)。
1S,128MB。
记 \(f_i\) 表示以 \(a_i\) 结尾的最大子段和,初始化 \(f_0 = -\infin\)。转移时考察是否要加上前面一段的贡献。前面一段的最大贡献为 \(f_{i-1}\)。则显然有:
\[f_{i} = \max (f_{i - 1} + a_i, a_i) \]定义 \(g\) 为 \(f\) 的前缀最大值,答案即为 \(g_n\)。算法总时间复杂度 \(O(n)\) 级别。
考虑加法运算运算与取 \(\max\) 运算的性质:发现取 \(\max\) 满足交换律与结合律,且加法对取 \(\max\) 满足分配率,即有:
\[a + \max_{i}(b_i) = \max_{i}(a + b_i) \]考虑定义一种广义矩阵乘法,满足:
\[C_{i,j} = \max_{k}\left( A_{i,k} +B_{k,j}\right) \]考虑将上述状态转移方程写成广义矩阵乘法形式。当从 \(i-1\) 转移到 \(i\) 时,显然有:
\[\begin{bmatrix} f_{i-1}& g_{i-2}& 0 \end{bmatrix} \times \begin{bmatrix} a_i& 0& -\infin\\ -\infin& 0& -\infin\\ a_i& -\infin& 0\\ \end{bmatrix} = \begin{bmatrix} f_i& g_{i-1}& 0 \end{bmatrix}\]根据上述分析,显然该运算满足结合律,则有:
\[\begin{bmatrix} -\infin& -\infin& 0 \end{bmatrix} \times \prod_{i=1}^{n} \begin{bmatrix} a_i& 0& -\infin\\ -\infin& 0& -\infin\\ a_i& -\infin& 0\\ \end{bmatrix} = \begin{bmatrix} f_{n}& g_{n-1}& 0 \end{bmatrix}\]其中 \(\prod\) 表示连续广义矩阵乘法。预处理整个序列的广义矩阵乘积后,根据上式可得 \(f_n\) 与 \(g_{n-1}\),取最大值即得答案。总复杂度 \(O\left(3^3\times n\right)\) 级别。
静态区间查询
SP1043 GSS1 - Can you answer these queries I
给定一个长度为 \(n\) 的数列 \(a\),给定 \(m\) 次询问。
每次询问给定区间 \([l,r]\),要求选出区间 \([l,r]\) 中连续且非空的一段使得这段和最大,输出最大子段和。
\(1\le n\le 5\times 10^4\),\(-15007\le a_i\le 15007\)。
原题面中并没有给出 \(m\) 的范围,此处根据实际测试情况推断 \(m\) 与 \(n\) 同阶。
230ms,1.46G。
发现上述题目中广义矩阵乘法做法 复杂度比直接做还劣 有着很好的扩展性。对于任意区间,预处理区间对应的广义矩阵乘积后即得该区间的最大子段和。
问题变为如何快速求得区间广义矩阵乘积。广义矩阵乘法满足结合律,且本题中没有修改操作,考虑对于每个位置 \(i\) 预处理以 \(i\) 为左端点的长度为 \(2\) 的幂的区间的广义矩阵乘积。回答询问时倍增拼凑区间即可。总时间复杂度 \(O\left(3^3 (n+m)\log n\right)\) 级别。
不用维护一堆乱七八糟的玩意,个人认为比隔壁直接上线段树好写(
//知识点:矩阵乘法,倍增
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 5e4 + 10;
const int kL = 3;
const LL kInf = 1e9 + 2077;
//=============================================================
int n, m;
struct Matrix {
LL a[kL][kL];
Matrix() {
memset(a, 0, sizeof (a));
}
void build() {
for (int i = 1; i <= kL; ++ i) a[i][i] = 1;
}
Matrix operator * (const Matrix &b_) const {
Matrix ret;
memset(ret.a, 128, sizeof (ret.a));
for (int k = 0; k < kL; ++ k) {
for (int i = 0; i < kL; ++ i) {
for (int j = 0; j < kL; ++ j) {
ret.a[i][j] = std::max(ret.a[i][j], a[i][k] + b_.a[k][j]);
}
}
}
return ret;
}
} f[kN][21];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
LL Query(int l, int r) {
Matrix ans;
ans.a[0][0] = ans.a[0][1] = -kInf;
for (int i = 20; i >= 0; -- i) {
if (l + (1 << i) - 1 <= r) {
ans = ans * f[l][i];
l += (1 << i);
}
}
return std::max(ans.a[0][0], ans.a[0][1]);
}
//=============================================================
int main() {
n = read();
for (int i = 1; i <= n; ++ i) {
f[i][0].a[0][0] = f[i][0].a[2][0] = read();
f[i][0].a[1][0] = f[i][0].a[2][1] = f[i][0].a[0][2] = f[i][0].a[1][2] = -kInf;
}
for (int i = 1; i <= 20; ++ i) {
for (int j = 1; j + (1 << i) - 1 <= n; ++ j) {
f[j][i] = f[j][i - 1] * f[j + (1 << (i - 1))][i - 1];
}
}
m = read();
for (int i = 1; i <= m; ++ i) {
int l = read(), r = read();
printf("%lld\n", Query(l, r));
}
return 0;
}
动态区间查询
SP1043 GSS1 - Can you answer these queries I
给定一个长度为 \(n\) 的数列 \(a\),给定 \(m\) 次询问。
每次询问给定区间 \([l,r]\),要求选出区间 \([l,r]\) 中连续且非空的一段使得这段和最大,输出最大子段和。
\(1\le n,m\le 5\times 10^4\),\(-10^4\le a_i\le 10^4\)。
330ms,1.46G。
在上题的基础上加入了单点修改操作。发现每次修改仅会影响对应位置的矩阵,以及包含该位置的区间的广义矩阵乘积,考虑线段树维护广义矩阵乘积,每次修改仅需更新自叶到根的 \(\log n\) 个位置的对应区间。总时间复杂度 \(O\left(3^3 (n+m)\log n\right)\)。
gugugu
动态树形 DP
gugugu
例题
gugugu
写在最后
鸣谢:
矩阵 - OI Wiki
动态 DP - OI Wiki
洛谷日报#130[GKxx]动态DP入门 - GKxx