线段树与矩阵
线段树
线段树的双半群模型
线段树上每个节点都有 数据 与 标记 两种信息,称作 与 。
-
则需要存在 的转移,即 数据合并。
-
以及 , 即 标记转移。
-
以及 , 即 标记合并。
同时还需要满足 结合律 与 分配律,这是一个 半群,再存在一个单位元 ,使 则为 幺半群。则我们可以维护两个结构体,将三种转移加入即可。
这里举个例子。
区间加,区间乘,区间查询。则有 , 。
则有 。
以及 。
以及 。
显然,这些东西都是可以用矩阵维护的。
矩阵乘法与线段树标记
区间加法线段树
考虑每一个区间维护一个向量
我们对于这个区间加上某一个数的操作可以看作左乘一个矩阵:
此时,我们只需要维护左乘矩阵即可。
这里可以解释为什么只需要一个懒标记标记维护区间加信息。
考虑到左乘的是一个上三角矩阵,而可以证明上三角乘上三角还是上三角,并且在这个情景中,只有右上角的位置数值会变化,于是只需要用一个标记维护右上角的值即可,也就是我们平时维护的那个懒标记。
线段树历史版本和
假设只有区间加操作。
每一个区间维护一个向量
其中 表示历史版本和, 表示当前区间和, 是区间长度。
对于区间加的操作左乘矩阵没有什么变化。
但是我们多了令 的操作,考虑利用矩阵表示:
还是维护左乘矩阵即可。
线段树历史最值
广义矩阵
两个矩阵 相乘得到 ,满足 。
而处理区间最值和历史最值时,常用广义矩乘,即 。
这里,我们只要维护, 两种运算,它们满足
- 交换律: 。
- 结合律: 。
- 单位元: 。
- 加法逆元(相反数): 。
- 分配律: 。
广义矩阵乘法显然具有结合律。
区间历史最值维护。
考虑序列每一个数维护一个向量 表示当前值, 表示历史最值,用线段树维护区间向量和 (即 的最大值)。
那么区间加 可以看作 ,可以很容易地构造广义矩阵乘法: ,故可以将懒标记设为 这个矩阵。
例题:P4314 CPU 监控 - 洛谷 | 计算机科学教育新生态
本题需要支持区间赋值。(这个操作可以转化为区间加,就是即使线段树节点被区间完包,只要最大值不等于最小值,就递归下去,根据颜色段均雊理论,这部分的均摊时间复杂度为 )。
可以给向量再加—维,使其变为 ,这样就有 。
将矩阵转为标记
在普通的历史最值维护中,我们可以注意到:
左乘矩阵的第二列的值始终不变,只有第一列的值在变化,故维护矩阵第一列的值即可。
事实上,左乘矩阵第一列的两个值分别对应论文中的 “加减标记” 和 “历史最大加减标记”。
例题:P6242 【模板】线段树 3(区间最值操作、区间历史最值) - 洛谷 | 计算机科学教育新生态
先不考虑历史最值问题。
考虑到区间取 的操作只会对最大值不超过 的节点产生影响,我们可以在这方面产生思路。为了使复杂度变对,线段树的一个节点要维护三个信息:区间最大值 , 区间严格次大值 和最大值的个数 。那么,一次区间最值操作作用在这个节点上时,可以被分为以下三种情况:
- , 此时该操作不会对当前节点产生影响,直接退出;
- , 此时这个节点维护的区间中所有最大值都会被修改为,而最大值个数不变。将
区间和加上 ,打上懒标记,然后退出即可; - , 此时无法快速更新区间信息,因此我们需要继续递归到左右子树中,回溯时合并信息。
原论文的证明告诉我们在没有修改操作时复杂度为 的。
由于区间加减操作,某些节点的值域会增大。论文里给的时间复杂度是 。而实际实现时会发现这个上界其实往往是跑不满的,速度几乎与大常数一个 接近。
此时,返回原题。我们将本题划分值域为最大值与非最大值,分别维护信息与 。
此处的矩阵只能分别简化最大值与非最大值的历史最值变化过程。在广义矩阵下并不能直接维护 ,同时次大值以及最大值个数的记录也需要单独记录。本题更好的方法是直接用 转移(本人不会完全使用矩阵完成这道题)。
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
const int N = 5e5 + 5;
struct SGT {
ll sum;
int maxa, maxb, cnt, se;
int add1, add2, hadd1, hadd2; // 最大值/非最大值的标记 最大值/非最大值的历史最大标记
} tr[N << 2];
il void pushup(int id) {
tr[id].sum = tr[ls].sum + tr[rs].sum;
tr[id].maxa = max(tr[ls].maxa, tr[rs].maxa);
tr[id].maxb = max(tr[ls].maxb, tr[rs].maxb);
if (tr[ls].maxa == tr[rs].maxa) {
tr[id].se = max(tr[ls].se, tr[rs].se);
tr[id].cnt = tr[ls].cnt + tr[rs].cnt;
} else if (tr[ls].maxa > tr[rs].maxa) {
tr[id].se = max(tr[ls].se, tr[rs].maxa);
tr[id].cnt = tr[ls].cnt;
} else {
tr[id].se = max(tr[ls].maxa, tr[rs].se);
tr[id].cnt = tr[rs].cnt;
}
}
void build(int l, int r, int id) {
if (l == r) {
int x; read(x); tr[id].add1 = tr[id].add2 = tr[id].hadd1 = tr[id].hadd2 = 0;
return tr[id].sum = tr[id].maxa = tr[id].maxb = x, tr[id].se = -2e9, tr[id].cnt = 1, void();
}
build(l, mid, ls), build(mid + 1, r, rs);
pushup(id);
}
il void work(int tag1, int tag2, int htag1, int htag2, int l, int r, int id) {
tr[id].sum += 1ll * tag1 * tr[id].cnt + 1ll * tag2 * (r - l + 1 - tr[id].cnt);
tr[id].maxb = max(tr[id].maxb, tr[id].maxa + htag1); tr[id].maxa += tag1;
if (tr[id].se != -2e9) tr[id].se += tag2;
tr[id].hadd1 = max(tr[id].hadd1, tr[id].add1 + htag1);
tr[id].hadd2 = max(tr[id].hadd2, tr[id].add2 + htag2);
tr[id].add1 += tag1, tr[id].add2 += tag2;
}
il void pushdown(int l, int r, int id) {
int mx = max(tr[ls].maxa, tr[rs].maxa);
if (tr[ls].maxa == mx) work(tr[id].add1, tr[id].add2, tr[id].hadd1, tr[id].hadd2, l, mid, ls);
else work(tr[id].add2, tr[id].add2, tr[id].hadd2, tr[id].hadd2, l, mid, ls);
if (tr[rs].maxa == mx) work(tr[id].add1, tr[id].add2, tr[id].hadd1, tr[id].hadd2, mid + 1, r, rs);
else work(tr[id].add2, tr[id].add2, tr[id].hadd2, tr[id].hadd2, mid + 1, r, rs);
tr[id].add1 = tr[id].add2 = tr[id].hadd1 = tr[id].hadd2 = 0;
}
il void add(int l, int r, int x, int y, int k, int id) {
if (l > y || r < x) return ;
if (l >= x && r <= y) {
tr[id].sum += 1LL * k * (r - l + 1);
tr[id].maxa += k; tr[id].maxb = max(tr[id].maxb, tr[id].maxa);
if (tr[id].se != -2e9) tr[id].se += k;
tr[id].add1 += k, tr[id].add2 += k;
tr[id].hadd1 = max(tr[id].hadd1, tr[id].add1);
tr[id].hadd2 = max(tr[id].hadd1, tr[id].add2);
return ;
}
pushdown(l, r, id);
add(l, mid, x, y, k, ls), add(mid + 1, r, x, y, k, rs);
pushup(id);
}
il void mdf(int l, int r, int x, int y, int k, int id) {
if (l > y || r < x || tr[id].maxa <= k) return ;
if (l >= x && r <= y && tr[id].se < k) {
int t = tr[id].maxa - k;
tr[id].sum -= 1LL * tr[id].cnt * t;
tr[id].maxa = k, tr[id].add1 -= t;
return ;
}
pushdown(l, r, id);
mdf(l, mid, x, y, k, ls), mdf(mid + 1, r, x, y, k, rs);
pushup(id);
}
il ll qry_sum(int l, int r, int x, int y, int id) {
if (l > y || r < x) return 0;
if (l >= x && r <= y) return tr[id].sum;
pushdown(l, r, id);
return qry_sum(l, mid, x, y, ls) + qry_sum(mid + 1, r, x, y, rs);
}
il ll qry_a(int l, int r, int x, int y, int id) {
if (l > y || r < x) return -2e9;
if (l >= x && r <= y) return tr[id].maxa;
pushdown(l, r, id);
return max(qry_a(l, mid, x, y, ls), qry_a(mid + 1, r, x, y, rs));
}
il ll qry_b(int l, int r, int x, int y, int id) {
if (l > y || r < x) return -2e9;
if (l >= x && r <= y) return tr[id].maxb;
pushdown(l, r, id);
return max(qry_b(l, mid, x, y, ls), qry_b(mid + 1, r, x, y, rs));
}
int n, m;
signed main() {
read(n, m);
build(1, n, 1);
while (m--) {
int op, l, r, k;
read(op, l, r);
if (op == 1) read(k), add(1, n, l, r, k, 1);
else if (op == 2) read(k), mdf(1, n, l, r, k, 1);
else if (op == 3) write(qry_sum(1, n, l, r, 1)), ptc('\n');
else if (op == 4) write(qry_a(1, n, l, r, 1)), ptc('\n');
else if (op == 5) write(qry_b(1, n, l, r, 1)), ptc('\n');
}
return 0;
}
我始终认为这种题看代码是最好的理解方式。
普通矩阵乘法与区间历史和
我们离线所有询问,对右端点进行扫描线。
在扫描过程中,我们设 和 分别表示 范围内 和 的最大值。
我们可以在扫描的时候,对每个,维护
这样的话,我们要查询的就是 的区间 和。
而我们的操作则是区间 修改 (覆盖),区间 修改 (覆盖), 以及 。
至此,本题转为了区间覆盖,区间历史和问题。
维护向量:
其中 表示历史版本和, 表示 , 分别表示 。
于是,对于 的区间加操作可以表示为:
对 的操作同理。
刷新历史和的操作可以表示为:
维护左乘矩阵即可。复杂度 ,可以获得 分。在卡常和乘法展开后也许可以获得满分。
优化标记常数
就像前面提到的,矩阵中总有很多信息始终不变,那么这些信息理论是不必要记录的。
我们以如下乘法为例:
操作的矩阵有两种:
也就意味着我们只需要关注形如:
然而发现事实上右上角都会有值,于是重新修正:
这意味着我们只需要维护右上角的三个位置,每次按照上式直接修改即可,这样常数大大减少。
同理,对于P8868[NOIP2022] 比赛,发现有用的位置只有 个,维护这 个位置即可。
如果不好观察,不妨给矩阵随机赋值,打表出不变的位置即可。
关于向量构造的一些小技巧
一般来说,我们需要构造出来的向量,对于每一个操作都应该是一个上/下三角矩阵的形式,这样更加方便我们观察,理解,优化。
而如果要成为一个上三角,就意味着对于 只会由 转移而来。
于是一般来说,会将不变的长度放在最下面,将历史版本信息放在最上面,一般的信息则放在中间。
结语
本文大部分为优质博客的誊抄。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步