线段树综合
线段树作为一种扩展性极强、复杂度优秀的数据结构,除了常规用法外还被开发出了很多其他的用途。本文将列举几种扩展用途及其应用。
0 基本操作
0.1 权值线段树
正常的线段树是维护区间上每一个点的值,而权值线段树则是维护每一个数字出现的次数(可以类比为桶)。
例如原本的
基本的权值线段树可以实现如下操作:
- 添加一个数(对应单点修改)
- 查找一个数出现的次数(对应单点查询)
- 查找一段区间内数字出现的次数(对应区间查询)
- 寻找第
小(大)元素
前三个操作都很简单,下面着重来看第四个操作。实际上这个操作的实现已经用到了一些线段树二分的思想,如下:
int kth(int p, int l, int r, int k) {
if(t[p].sum < k) return -1;//不足 k 个
if(l == r) {
return l;
}
int mid = (l + r) >> 1, res = t[lp].sum;//左区间元素个数
if(res < k) return kth(rp, mid + 1, r, k - res);//第 k 小元素在右区间
else return kth(lp, l, mid, k);//第 k 小元素在左区间
}
0.2 动态开点线段树
在权值线段树中,我们的值域可能在
于是我们便有了一种新的东西:动态开点。对于每一个线段树上的节点,记录下他的左儿子与右儿子编号。
如此,每一次只需要再使用一个节点时判断该节点是否存在即可,如果不存在就新建节点,同时记录儿子即可。
所以树的结构体定义如下:
struct node{
int l, r, sum;//注意这里的 l,r 不是区间 [l,r] 而是左右儿子编号
}t[Maxn];
1 线段树合并 / 分裂
1.1 概念
顾名思义,线段树合并就是将多颗线段树的信息合并起来,用一颗线段树保存。
常有两种方式实现,一种是新建一颗线段树来存储,另一种是将一颗线段树直接合并到另一个上面去(相当于 c=a+b
和 a+=b
),第二种方法则更节省空间,缺点是丢失了一颗线段树的原始信息。
那么采用第二种方法的代码如下,第一种是类似的:
//a 是第一棵树的节点,b 是第二棵树的节点
int merge(int a, int b, int l, int r) {
if(!a) return b;
if(!b) return a;//如果有一颗线段树该位置是空的,那就返回另一个节点,然后用动态开点存储左右儿子
if(l == r) {//叶子节点
t[a].sum += t[b].sum;//合并
return a;
}
int mid = (l + r) >> 1;
t[a].l = merge(t[a].l, t[b].l, l, mid);
t[a].r = merge(t[a].r, t[b].r, mid + 1, r);//动态开点
update(a);
return a;
}
线段树分裂一般是在权值线段树上进行的,它的操作是将权值线段树前
现在考虑怎样实现,实际上可以参考
时,左边不会被分裂出去,递归到右儿子,同时 变成 。 时,此时左子树正好保留了前 小的值,所以直接把右子树归给 即可。 时,此时右子树仍然全部归给 ,然后继续递归左子树求解。
代码如下:
void split(int x, int &y, int k) {
if(!x) return ;
y = newnode();//建新节点
int val = t[t[x].l].sum;
if(val < k) split(t[x].r, t[y].r, k - val);//递归右子树
else swap(t[x].r, t[y].r);//右子树全部归给 y
if(val > k) split(t[x].l, t[y].l, k);//递归左子树
t[y].sum = t[x].sum - k;//更新权值
t[x].sum = k;
}
1.2 例题
例 1 【模板】线段树分裂
考虑每一个操作怎样完成:
- 对于操作
,我们做两次分裂,然后将两端区间合并即可。由于我们的代码是按排名分裂而不是按值分裂,所以要先求出区间内有多少个数然后在分裂。 - 对于操作
,直接线段树合并即可。 - 对于操作
,朴素的线段树操作即可。
值得注意的是由于我们在合并的时候会删除节点,会导致我们出现一些无用的空节点。所以可以利用一个垃圾桶把它们存起来,分配新节点时优先用垃圾桶内的节点,可以优化空间复杂度。时间复杂度
代码如下:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int Maxn = 2e5 + 5;
const int Inf = 2e9;
int n, m;
int cnt, rt[Maxn];
namespace Sgt {
struct node {
int l, r, sum;
}t[Maxn * 20];
#define lp t[p].l
#define rp t[p].r
int tot = 0;
int trs[Maxn * 20], top;
int newnode() {
return top ? trs[top--] : ++tot;
}
void del(int x) {
trs[++top] = x;
t[x].l = t[x].r = t[x].sum = 0;
}
void pushup(int p) {
t[p].sum = t[lp].sum + t[rp].sum;
}
void mdf(int &p, int l, int r, int x, int val) {
if(!p) p = newnode();
if(l == r) {
t[p].sum += val;
return ;
}
int mid = (l + r) >> 1;
if(x <= mid) mdf(lp, l, mid, x, val);
else mdf(rp, mid + 1, r, x, val);
pushup(p);
}
int query(int p, int l, int r, int pl, int pr) {
if(!p || l > r) return 0;
if(pl <= l && r <= pr) {
return t[p].sum;
}
int mid = (l + r) >> 1, res = 0;
if(pl <= mid) res += query(lp, l, mid, pl, pr);
if(pr > mid) res += query(rp, mid + 1, r, pl, pr);
return res;
}
int kth(int p, int l, int r, int k) {
if(!p) return -1;
if(l == r) return l;
int mid = (l + r) >> 1, val = t[lp].sum;
if(val < k) return kth(rp, mid + 1, r, k - val);
else return kth(lp, l, mid, k);
}
int merge(int x, int y, int l, int r) {
if(!x || !y) {
return x + y;
}
if(l == r) {
t[x].sum += t[y].sum;
del(y);
return x;
}
int mid = (l + r) >> 1;
t[x].l = merge(t[x].l, t[y].l, l, mid);
t[x].r = merge(t[x].r, t[y].r, mid + 1, r);
del(y);
pushup(x);
return x;
}
void split(int x, int &y, int k) {
if(!x) return ;
y = newnode();
int val = t[t[x].l].sum;
if(val < k) split(t[x].r, t[y].r, k - val);
else swap(t[x].r, t[y].r);
if(val > k) split(t[x].l, t[y].l, k);
t[y].sum = t[x].sum - k;
t[x].sum = k;
}
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
cnt = 1;
for(int i = 1; i <= n; i++) {
int x; cin >> x;
Sgt::mdf(rt[cnt], 1, n, i, x);
}
while(m--) {
int opt, p, x, y;
cin >> opt;
switch(opt) {
case 0: {
cin >> p >> x >> y;
int res1 = Sgt::query(rt[p], 1, n, 1, y), res2 = Sgt::query(rt[p], 1, n, 1, x - 1);
int ret = 0;
Sgt::split(rt[p], ret, res1);
Sgt::split(rt[p], rt[++cnt], res2);
rt[p] = Sgt::merge(rt[p], ret, 1, n);
break;
}
case 1: {
cin >> x >> y;
rt[x] = Sgt::merge(rt[x], rt[y], 1, n);
break;
}
case 2: {
cin >> p >> x >> y;
Sgt::mdf(rt[p], 1, n, y, x);
break;
}
case 3: {
cin >> p >> x >> y;
cout << Sgt::query(rt[p], 1, n, x, y) << '\n';
break;
}
case 4: {
cin >> p >> x;
cout << Sgt::kth(rt[p], 1, n, x) << '\n';
break;
}
}
}
return 0;
}
例 2 [HEOI2016 / TJOI2016] 排序
发现这个题只有排序操作而没有任何修改操作,所以会想到每一次排序后会形成一段递增 / 递减的连续段,而题目最后的目标就是维护出每个连续段内的数字有谁。
那么直接考虑颜色段均摊,用珂朵莉树维护出递增 / 递减的连续段,用权值线段树维护每个连续段内出现的数字。在珂朵莉树需要分裂的时候就同时进行线段树分裂,合并的时候同时进行线段树合并即可。注意分类讨论递增和递减的情况。
容易发现,每一次操作最多会分裂两次,所以分裂次数最多
2 线段树优化建图
2.1 概念
线段树优化建图实际上就是利用线段树维护的区间信息,来达到减少连边数量的目的。例如最典型的就是题目给出的边的形式是一个点
考虑线段树怎样优化上述过程。我们以
2.2 例题
例 1 [CF786B] Legacy
这道题就是上面说过的两种情况都有的题,在两颗线段树上连边然后跑最短路即可。具体的,我们称从父亲连向儿子的线段树为出树,儿子连向父亲的线段树为入树,则对于题目给定的边,应该从入树上的节点向出树上的连。
当然了,对于入树上的叶子节点,它还有一个额外的入度来源,就是它在出树上对应的节点。所以我们还要从出树的叶子节点向入树上对应的叶子节点连边。跑的时候从出树上的叶子节点开始跑最短路即可。
代码如下:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int Maxn = 2e5 + 5;
const int Inf = 1e18;
int n, m, s;
int head[Maxn << 3], edgenum;
struct node {
int nxt, to, w;
}edge[Maxn * 40];
void add(int u, int v, int w) {
edge[++edgenum] = {head[u], v, w};
head[u] = edgenum;
}
int tot;
int idx1[Maxn], idx2[Maxn];
namespace Sgt1 {//出树
#define lp (p << 1)
#define rp (p << 1 | 1)
void build(int p, int l, int r) {
tot = max(tot, p);
if(l == r) {
idx1[l] = p;//记录叶子节点编号
return ;
}
int mid = (l + r) >> 1;
build(lp, l, mid), build(rp, mid + 1, r);
add(p, lp, 0), add(p, rp, 0);
}
void mdf(int p, int l, int r, int pl, int pr, int x, int w) {
if(pl <= l && r <= pr) {
add(x, p, w);
return ;
}
int mid = (l + r) >> 1;
if(pl <= mid) mdf(lp, l, mid, pl, pr, x, w);
if(pr > mid) mdf(rp, mid + 1, r, pl, pr, x, w);
}
}
namespace Sgt2 {//入树
void build(int p, int l, int r) {
if(l == r) {
idx2[l] = p + tot;
return ;
}
int mid = (l + r) >> 1;
build(lp, l, mid), build(rp, mid + 1, r);
add(lp + tot, p + tot, 0), add(rp + tot, p + tot, 0);
}
void mdf(int p, int l, int r, int pl, int pr, int x, int w) {
if(pl <= l && r <= pr) {
add(p + tot, x, w);
return ;
}
int mid = (l + r) >> 1;
if(pl <= mid) mdf(lp, l, mid, pl, pr, x, w);
if(pr > mid) mdf(rp, mid + 1, r, pl, pr, x, w);
}
}
int dis[Maxn << 3], vis[Maxn << 3];
#define pii pair<int, int>
#define mk make_pair
priority_queue <pii> q;
void dijkstra(int s) {
for(int i = 1; i <= (tot << 1); i++) dis[i] = Inf, vis[i] = 0;
q.push(mk(0, s));
dis[s] = 0;
while(!q.empty()) {
int x = q.top().second;
q.pop();
for(int i = head[x]; i; i = edge[i].nxt) {
int to = edge[i].to;
if(dis[to] > dis[x] + edge[i].w) {
dis[to] = dis[x] + edge[i].w;
q.push(mk(-dis[to], to));
}
}
}
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> s;
Sgt1::build(1, 1, n);
Sgt2::build(1, 1, n);
for(int i = 1; i <= n; i++) {
add(idx1[i], idx2[i], 0);//出树向入树连边
}
for(int i = 1; i <= m; i++) {
int opt, u, v, l, r, w;
cin >> opt;
switch(opt) {
case 1: {
cin >> u >> v >> w;
add(idx2[u], idx1[v], w);
break;
}
case 2: {
cin >> u >> l >> r >> w;
Sgt1::mdf(1, 1, n, l, r, idx2[u], w);
break;
}
case 3: {
cin >> u >> l >> r >> w;
Sgt2::mdf(1, 1, n, l, r, idx1[u], w);
break;
}
}
}
dijkstra(idx1[s]);
for(int i = 1; i <= n; i++) {
if(dis[idx2[i]] == Inf) cout << "-1 ";
else cout << dis[idx2[i]] << " ";
}
return 0;
}
例 2 [SNOI2017] 炸弹
典中典。我们发现每一个炸弹可以引爆的炸弹是一段连续区间,所以考虑从每一个炸弹向它的爆炸区间连边,显然这个可以用线段树优化建图来优化。
然后我们现在的目标就是在图上进行一个计数,但是发现此时的图上会有环,所以先跑一遍
不过考虑到最终每个点的答案一定对应着一段连续的区间,所以对于每个强联通分量只需要记录它对应的爆炸区间即可,然后拓扑排序的时候更新区间的左右端点,最后计算答案即可。
例 3 [POI2015] PUS
考虑将大小限制转化为连边,然后跑差分约束。发现此题的连边是很多个单点连向很多个区间,首先考虑用线段树优化建图优化单点连区间的过程,但是此时每一个单点仍然需要连较多区间,复杂度仍有
接下来跑差分约束即可,当然对于此题来说,只要有环就对应无解,所以在有解的时候直接拓扑排序一遍即可求解。注意判断题目中已经给出了数字的那些位置即可。
3 线段树分治
3.1 概念
线段树分治是一种离线处理带撤销问题的方法,比如最常见的类型就是“某一操作的存在 / 有效时间是
线段树分治的基本思想就是,对于时间轴建一颗线段树,然后在线段树上的每一个区间,维护在这个区间内有效的所有操作。然后对于询问而言,我们遍历整颗线段树,每经过一个区间就将区间中的操作加入贡献,直到遍历到叶子节点就表明走到了一个询问。然后回溯的时候删除刚刚进行的操作即可。
遍历线段树的复杂度是
3.2 例题
例 1 【模板】线段树分治
这道题从所有的方面看都很符合上面说到的条件,所以我们直接上线段树分治来维护。现在的问题就是怎样判定二分图,对于这道题,需要一个能够快速插入并查询的方法,我们考虑使用扩展域并查集。
具体的,将每一个点拆成两个点,表示其在左部点
然后这个东西还需要支持删除操作,所以我们还得使用可撤销并查集。关于可撤销并查集,详见 杂项 - 可撤销并查集。由于可撤销并查集的时间复杂度还有一个
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int Maxn = 2e5 + 5;
const int Inf = 2e9;
int n, m, k;
namespace Dsu {
int fa[Maxn], siz[Maxn];
void init() {
for(int i = 1; i <= (n << 1); i++) fa[i] = i, siz[i] = 1;
}
int find(int x) {
return fa[x] == x ? x : find(fa[x]);
}
int st[Maxn], top;
void merge(int x, int y) {
x = find(x), y = find(y);
if(x == y) return ;
if(siz[x] > siz[y]) swap(x, y);
fa[x] = y;
siz[y] += siz[x];
st[++top] = x;
}
void del(int tar) {
while(top > tar) {
int x = st[top--];
siz[fa[x]] -= siz[x];
fa[x] = x;
}
}
}
bool ans[Maxn];
namespace Sgt {
#define pii pair<int, int>
#define mk make_pair
vector <pii> t[Maxn << 2];
#define lp (p << 1)
#define rp (p << 1 | 1)
void mdf(int p, int l, int r, int pl, int pr, int u, int v) {
if(pl <= l && r <= pr) {
t[p].push_back(mk(u, v));
return ;
}
int mid = (l + r) >> 1;
if(pl <= mid) mdf(lp, l, mid, pl, pr, u, v);
if(pr > mid) mdf(rp, mid + 1, r, pl, pr, u, v);
}
bool flg = 1;
void dfs(int p, int l, int r) {
bool tmp1 = flg;
int tmp2 = Dsu::top;
for(auto x : t[p]) {
int u = x.first, v = x.second;
if(Dsu::find(u) == Dsu::find(v)) flg = 0;
Dsu::merge(u, v + n);
Dsu::merge(u + n, v);
}
if(l == r) {
ans[l] = flg;
flg = tmp1;
Dsu::del(tmp2);
return ;
}
int mid = (l + r) >> 1;
dfs(lp, l, mid), dfs(rp, mid + 1, r);
flg = tmp1;
Dsu::del(tmp2);
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> k;
for(int i = 1; i <= m; i++) {
int u, v, l, r;
cin >> u >> v >> l >> r;
Sgt::mdf(1, 1, k, l + 1, r, u, v);
}
Dsu::init();
Sgt::dfs(1, 1, k);
for(int i = 1; i <= k; i++) {
cout << (ans[i] ? "Yes\n" : "No\n");
}
return 0;
}
例 2 [CF576E] Painting Edges
发现此题和模板题很像,并且
我们考虑这样一个过程,对于每一次修改,我们不直接将他插到线段树上,而是等遍历到对应的叶子之后再考虑当前贡献。假设当前遍历到的叶子是
不难发现,上面的过程实际上是在一边分治,一边往线段树内插入贡献,而这种技巧被称为 “半在线线段树分治”。
例 3 [CF603E] Pastoral Oddities
首先通过手玩会发现一个比较强的性质:满足条件的图中一定全部是偶数大小的连通块。于是对于全局有如下做法:将所有边按照边权从小到大排序,然后不断加边直到图中全是偶数大小连通块。容易发现的是,加边一定不会更劣,所以这个贪心是正确的。发现这个过程可以轻易用并查集维护出来。
考虑原题目怎么求解。我们会发现一个问题,由于整个的边集在不断增加,答案一定是单调不增的。也就是说,对于任意一条边,它存在于最优边集内的时间是连续的一段。这个时候我们就可以通过上面的性质来进行求解了。
我们考虑仿照上一例中 ”半在线线段树分治“ 的思想。具体的,我们从右往左遍历叶子节点,当我们走到一个叶子节点
4 线段树二分
4.1 概念
顾名思义,由于线段树本身就是一种分治的数据结构,这使得我们可以在线段树上直接二分求解答案。具体来讲,朴素的二分 + 线段树的复杂度应该是
线段树二分大体上可以分为两种:全局二分和区间二分。
4.1.1 全局二分
实际上全局二分并不难,因为本质上权值线段树中找第
4.1.2 区间二分
考虑一个朴素的思路,我们先将查询区间
我们拆分区间的时候一定是按顺序遍历了
4.2 例题
例 1 [PA2015] Siano
容易发现所有的操作都可以在线段树上进行,唯一的难点在于每一次要将高度
考虑怎样维护该信息。通过上面分析不难看出,每一株草的高度可以表示为
对于找出连续后缀,直接线段树二分出最后一个高度
5 树套树
5.1 概念
这个概念在前面已经见过多次了,现在我们再来回顾一下。
关于线段树的树套树常用的一般有两种,即线段树套平衡树和树状数组套权值线段树。
还是以经典例题来说明:【模板】树套树
5.1.1 线段树套平衡树
我们在线段树上的每一个节点内维护一棵平衡树,存储该区间内所有的数字。对于操作
具体的过程和代码详见 平衡树 - 树套树。
5.1.2 树状数组套权值线段树
我们知道静态主席树相当于维护了一个线段树的前缀和信息。既然和前缀和有关,我们就可以利用另一种维护前缀和的数据结构来维护这个线段树的前缀和。所以就有了树状数组套权值线段树。具体的,树状数组上每一个节点对应一棵权值线段树,存储对应区间内的数字信息。
- 对于操作
,遍历树状数组并对对应线段树进行操作即可。 - 对于操作
,显然需要做线段树二分,但是此时需要将所有有用的 个节点提出来,然后作差求出对应值。 - 对于操作
,我们可以利用上面已经实现的操作 来完成。具体的,对于求 的前驱,我们求出 的排名后减一后再查值即为答案;对于求 的后继,求出 的排名后再求值即为答案。
显然上述所有操作的复杂度都是
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int Maxn = 5e4 + 5;
const int Inf = 2e9;
const int L = -1e8;
const int R = 1e8;
int n, m, a[Maxn];
int rt[Maxn];
namespace Sgt {
struct node {
int l, r, sum;
}t[Maxn * 460];
#define lp t[p].l
#define rp t[p].r
int tot = 0;
void pushup(int p) {
t[p].sum = t[lp].sum + t[rp].sum;
}
void mdf(int &p, int l, int r, int x, int val) {
if(!p) p = ++tot;
if(l == r) {
t[p].sum += val;
return ;
}
int mid = (l + r) >> 1;
if(x <= mid) mdf(lp, l, mid, x, val);
else mdf(rp, mid + 1, r, x, val);
pushup(p);
}
int rnk(int p, int l, int r, int pl, int pr) {
if(!p || pl > pr) return 0;
if(pl <= l && r <= pr) {
return t[p].sum;
}
int mid = (l + r) >> 1, res = 0;
if(pl <= mid) res += rnk(lp, l, mid, pl, pr);
if(pr > mid) res += rnk(rp, mid + 1, r, pl, pr);
return res;
}
int kth(int p[], int q[], int n1, int n2, int l, int r, int k) {
if(l == r) return l;
int mid = (l + r) >> 1, res = 0;
for(int i = 1; i <= n2; i++) res += t[t[q[i]].l].sum;
for(int i = 1; i <= n1; i++) res -= t[t[p[i]].l].sum;
if(res < k) {
for(int i = 1; i <= n1; i++) p[i] = t[p[i]].r;
for(int i = 1; i <= n2; i++) q[i] = t[q[i]].r;
return kth(p, q, n1, n2, mid + 1, r, k - res);
}
else {
for(int i = 1; i <= n1; i++) p[i] = t[p[i]].l;
for(int i = 1; i <= n2; i++) q[i] = t[q[i]].l;
return kth(p, q, n1, n2, l, mid, k);
}
}
}
namespace BIT {
int lowbit(int x) {
return x & (-x);
}
void mdf(int x, int v, int w) {
for(int i = x; i <= n; i += lowbit(i)) {
Sgt::mdf(rt[i], L, R, v, w);
}
}
int rnk(int l, int r, int k) {
int sum = 0;
for(int i = r; i; i -= lowbit(i)) {
sum += Sgt::rnk(rt[i], L, R, L, k - 1);
}
for(int i = l - 1; i; i -= lowbit(i)) {
sum -= Sgt::rnk(rt[i], L, R, L, k - 1);
}
return sum + 1;
}
int p[Maxn], q[Maxn];
int kth(int l, int r, int k) {
int n1 = 0, n2 = 0;
for(int i = r; i; i -= lowbit(i)) q[++n2] = rt[i];
for(int i = l - 1; i; i -= lowbit(i)) p[++n1] = rt[i];
return Sgt::kth(p, q, n1, n2, L, R, k);
}
int pre(int l, int r, int k) {
int res = rnk(l, r, k) - 1;
if(res == 0) return -2147483647;
else return kth(l, r, res);
}
int nxt(int l, int r, int k) {
int res = rnk(l, r, k + 1);
if(res == r - l + 2) return 2147483647;
else return kth(l, r, res);
}
}
int 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];
for(int i = 1; i <= n; i++) BIT::mdf(i, a[i], 1);
while(m--) {
int opt;
cin >> opt;
switch(opt) {
case 1: {
int l, r, k;
cin >> l >> r >> k;
cout << BIT::rnk(l, r, k) << '\n';
break;
}
case 2: {
int l, r, k;
cin >> l >> r >> k;
cout << BIT::kth(l, r, k) << '\n';
break;
}
case 3: {
int x, k;
cin >> x >> k;
BIT::mdf(x, a[x], -1);
a[x] = k;
BIT::mdf(x, a[x], 1);
break;
}
case 4: {
int l, r, k;
cin >> l >> r >> k;
cout << BIT::pre(l, r, k) << '\n';
break;
}
case 5: {
int l, r, k;
cin >> l >> r >> k;
cout << BIT::nxt(l, r, k) << '\n';
break;
}
}
}
return 0;
}
6 吉司机线段树
吉司机线段树(segment tree beats),是一种用于维护区间最值 / 区间历史最值的数据结构。
6.1 区间最值
区间最值操作指的是,对于一个区间
区间最值操作具体还可以分为不带区间加减和带区间加减两种类型。
6.1.1 不带区间加减
我们发现题目中操作有三种:区间取
由于我们要对区间取
- 如果
,则不必对该区间进行操作。 - 如果
,则我们需要将区间中的最大值改为 ,同时区间和加上 。最后我们还要给整个区间打上一个修改的标记,也就是标记当前最大值修改为 。 - 如果
,此时我们不能确定哪些点要进行修改,索性直接不管,递归到子节点继续判断是否满足上面两个条件即可。
看上去这个算法很暴力,但是根据势能分析,该算法的时间复杂度是
核心代码如下:
struct node {
int mx, se, cnt, tag, sum;
}t[Maxn << 2];
#define lp (p << 1)
#define rp (p << 1 | 1)
void pushup(int p) {
t[p].sum = t[lp].sum + t[rp].sum;
if(t[lp].mx > t[rp].mx) {
t[p].mx = t[lp].mx, t[p].cnt = t[lp].cnt;
t[p].se = max(t[lp].se, t[rp].mx);
}
else if(t[lp].mx < t[rp].mx) {
t[p].mx = t[rp].mx, t[p].cnt = t[rp].cnt;
t[p].se = max(t[rp].se, t[lp].mx);
}
else if(t[lp].mx == t[rp].mx) {
t[p].mx = t[lp].mx, t[p].cnt = t[lp].cnt + t[rp].cnt;
t[p].se = max(t[lp].se, t[rp].se);
}
}
void build(int p, int l, int r) {
t[p].tag = -1;
if(l == r) {
t[p].mx = t[p].sum = a[l];
t[p].se = -1;
t[p].cnt = 1;
return ;
}
int mid = (l + r) >> 1;
build(lp, l, mid), build(rp, mid + 1, r);
pushup(p);
}
void pushtag(int p, int v) {
if(t[p].mx <= v) return ;
t[p].sum += t[p].cnt * (v - t[p].mx);
t[p].mx = t[p].tag = v;
}
void pushdown(int p) {
if(t[p].tag != -1) {
pushtag(lp, t[p].tag), pushtag(rp, t[p].tag);
t[p].tag = -1;
}
}
void mdf(int p, int l, int r, int pl, int pr, int v) {
if(t[p].mx <= v) return ;
if(pl <= l && r <= pr && t[p].se < v) {
pushtag(p, v);
return ;
}
pushdown(p);
int mid = (l + r) >> 1;
if(pl <= mid) mdf(lp, l, mid, pl, pr, v);
if(pr > mid) mdf(rp, mid + 1, r, pl, pr, v);
pushup(p);
}
6.1.2 带区间加减
首先让我们考虑在上面那道题的基础上,加入区间加操作怎么做。实际上我们有两种办法来解决这个问题:
- 考虑套用上一题的做法,再维护一个区间加标记
。这样构成了一个二元组标记 ,表示先将值加上 再和 取 。当我们从 下传到 的时候,懒标记应该变成 。 - 我们换一种思路,容易发现上面一道题的本质就是将线段树维护的元素分成了最大值和非最大值两个部分。于是我们维护区间加的标记也可以分成最大值和非最大值两个部分来维护。那么区间取
的时候我们只修改最大值的加法标记,区间加的时候同时修改两个标记即可。
两者的代码量基本相当,但是后者的思想可以拓展到更为复杂的情况中,我们称之为数域划分。
然后我们来看一道真正的模板题:[BZOJ4695] 最假女选手。
我们使用数域划分的方式将区间最值转化为区间加减,所以我们需要维护的值有区间最大值、严格次大值、最大值个数、区间最小值、严格次小值、最小值个数、区间和,标记有最大值加法标记、最小值加法标记、其它值加法标记。
然后在下传标记的时候需要注意两点:
- 我们需要知道子区间内是否有当前区间内的最大值,如果没有最大值则下传的最大值加法标记应该是当前区间的其它值加法标记。最小值同理。
- 如果一个区间的值域比较小,此时可能会出现一个值又是最大值又是最小值的情况,此时需要特判哪个标记会作用到当前值上。
根据势能分析,该做法的时间复杂度是
struct node {
int sum, mx, lmx, cmx, mn, lmn, cmn;
//区间和、最大值、严格次大值、最大值个数、区间最小值、严格次小值、最小值个数
int amx, amn, add;
//最大值加法标记、最小值加法标记、其它值加法标记
}t[Maxn << 2];
#define lp (p << 1)
#define rp (p << 1 | 1)
void pushup(int p) {
t[p].sum = t[lp].sum + t[rp].sum;
if(t[lp].mx > t[rp].mx) {
t[p].mx = t[lp].mx, t[p].cmx = t[lp].cmx;
t[p].lmx = max(t[lp].lmx, t[rp].mx);
}
else if(t[lp].mx < t[rp].mx) {
t[p].mx = t[rp].mx, t[p].cmx = t[rp].cmx;
t[p].lmx = max(t[lp].mx, t[rp].lmx);
}
else {
t[p].mx = t[lp].mx, t[p].cmx = t[lp].cmx + t[rp].cmx;
t[p].lmx = max(t[lp].lmx, t[rp].lmx);
}
if(t[lp].mn < t[rp].mn) {
t[p].mn = t[lp].mn, t[p].cmn = t[lp].cmn;
t[p].lmn = min(t[lp].lmn, t[rp].mn);
}
else if(t[lp].mn > t[rp].mn) {
t[p].mn = t[rp].mn, t[p].cmn = t[rp].cmn;
t[p].lmn = min(t[lp].mn, t[rp].lmn);
}
else {
t[p].mn = t[lp].mn, t[p].cmn = t[lp].cmn + t[rp].cmn;
t[p].lmn = min(t[lp].lmn, t[rp].lmn);
}
}
void build(int p, int l, int r) {
if(l == r) {
t[p].sum = t[p].mx = t[p].mn = a[l];
t[p].cmx = t[p].cmn = 1;
t[p].lmx = -Inf, t[p].lmn = Inf;
return ;
}
int mid = (l + r) >> 1;
build(lp, l, mid), build(rp, mid + 1, r);
pushup(p);
}
void pushtag(int p, int l, int r, int amx, int amn, int add) {
if(t[p].mx == t[p].mn) {//如果区间内只有一个数
if(amx == add) amx = amn;//最大值和最小值加法标记应当一致
else amn = amx;
t[p].sum += t[p].cmx * amx;
}
else {
t[p].sum += t[p].cmx * amx + t[p].cmn * amn + (r - l + 1 - t[p].cmx - t[p].cmn) * add;
}
if(t[p].lmx == t[p].mn) t[p].lmx += amn;//次大值是最小值应加上最小值标记
else if(t[p].lmx != -Inf) t[p].lmx += add;
if(t[p].lmn == t[p].mx) t[p].lmn += amx;//次小值是最大值应加上最大值标记
else if(t[p].lmn != Inf) t[p].lmn += add;
t[p].mx += amx, t[p].mn += amn;
t[p].amx += amx, t[p].amn += amn, t[p].add += add;
}
void pushdown(int p, int l, int r) {
int mid = (l + r) >> 1;
int mx = max(t[lp].mx, t[rp].mx);
int mn = min(t[lp].mn, t[rp].mn);
//需要判断左右区间是否有最大 / 最小值
pushtag(lp, l, mid, t[lp].mx == mx ? t[p].amx : t[p].add, t[lp].mn == mn ? t[p].amn : t[p].add, t[p].add);
pushtag(rp, mid + 1, r, t[rp].mx == mx ? t[p].amx : t[p].add, t[rp].mn == mn ? t[p].amn : t[p].add, t[p].add);
t[p].amx = t[p].amn = t[p].add = 0;
}
void mdfmin(int p, int l, int r, int pl, int pr, int v) {
if(t[p].mx <= v) return ;
if(pl <= l && r <= pr && t[p].lmx < v) {
pushtag(p, l, r, v - t[p].mx, 0, 0);
return ;
}
pushdown(p, l, r);
int mid = (l + r) >> 1;
if(pl <= mid) mdfmin(lp, l, mid, pl, pr, v);
if(pr > mid) mdfmin(rp, mid + 1, r, pl, pr, v);
pushup(p);
}
void mdfmax(int p, int l, int r, int pl, int pr, int v) {
if(t[p].mn >= v) return ;
if(pl <= l && r <= pr && t[p].lmn > v) {
pushtag(p, l, r, 0, v - t[p].mn, 0);
return ;
}
pushdown(p, l, r);
int mid = (l + r) >> 1;
if(pl <= mid) mdfmax(lp, l, mid, pl, pr, v);
if(pr > mid) mdfmax(rp, mid + 1, r, pl, pr, v);
pushup(p);
}
void mdfsum(int p, int l, int r, int pl, int pr, int v) {
if(pl <= l && r <= pr) {
pushtag(p, l, r, v, v, v);
return ;
}
pushdown(p, l, r);
int mid = (l + r) >> 1;
if(pl <= mid) mdfsum(lp, l, mid, pl, pr, v);
if(pr > mid) mdfsum(rp, mid + 1, r, pl, pr, v);
pushup(p);
}
6.2 区间历史最值
区间历史最值问题,指的是我们不仅要维护一个原序列
我们先来看一道较为基础的题目:CPU 监控。
题目中所给出的操作有区间加、区间覆盖、区间最大值、区间历史最大值。那么我们就要对每一个区间维护两个值
我们知道,朴素的矩阵乘法的形式是
,实际上这是一种 两种运算的矩阵乘法。假如我们把运算改成 ,则形式会变成 。显然这种矩阵乘法依然满足结合律。 而在维护区间最值和区间历史最值的时候,用广义矩阵乘法维护标记是再好不过的选择了。
那么对于区间加,我们的目标是
对于区间覆盖,这个向量似乎难以完成任务。不过我们可以给它加上一维辅助的变量,变成
当然你也不能一直拿着一个矩阵在那瞎乘,毕竟
发现要维护的始终只有这四个值,所以直接维护即可。需要注意的是这个矩阵的初始形式是
核心代码如下:
struct Tag {
int a, b, c, d;
Tag operator + (const Tag &p) const {
return (Tag){a + p.a, max(b + p.a, p.b), max(a + p.c, c), max({b + p.c, d, p.d})};
}
};
namespace Sgt {
struct node {
int mx, hmx;
Tag tag;
}t[Maxn << 2];
#define lp (p << 1)
#define rp (p << 1 | 1)
void pushup(int p) {
t[p].mx = max(t[lp].mx, t[rp].mx);
t[p].hmx = max(t[lp].hmx, t[rp].hmx);
}
void build(int p, int l, int r) {
t[p].tag = {0, -Inf, -Inf, -Inf};
if(l == r) {
t[p].mx = t[p].hmx = a[l];
return ;
}
int mid = (l + r) >> 1;
build(lp, l, mid), build(rp, mid + 1, r);
pushup(p);
}
void pushtag(int p, Tag tag) {
t[p].tag = tag + t[p].tag;
t[p].hmx = max({tag.b + t[p].mx, t[p].hmx, tag.d});//注意一定要先更新历史最大值
t[p].mx = max(tag.a + t[p].mx, tag.c);
}
void pushdown(int p) {
pushtag(lp, t[p].tag), pushtag(rp, t[p].tag);
t[p].tag = {0, -Inf, -Inf, -Inf};
}
void mdf(int p, int l, int r, int pl, int pr, Tag tag) {
if(pl <= l && r <= pr) {
pushtag(p, tag);
return ;
}
pushdown(p);
int mid = (l + r) >> 1;
if(pl <= mid) mdf(lp, l, mid, pl, pr, tag);
if(pr > mid) mdf(rp, mid + 1, r, pl, pr, tag);
pushup(p);
}
int qmax(int p, int l, int r, int pl, int pr) {
if(pl <= l && r <= pr) {
return t[p].mx;
}
pushdown(p);
int mid = (l + r) >> 1, res = -Inf;
if(pl <= mid) res = max(res, qmax(lp, l, mid, pl, pr));
if(pr > mid) res = max(res, qmax(rp, mid + 1, r, pl, pr));
return res;
}
int qhmax(int p, int l, int r, int pl, int pr) {
if(pl <= l && r <= pr) {
return t[p].hmx;
}
pushdown(p);
int mid = (l + r) >> 1, res = -Inf;
if(pl <= mid) res = max(res, qhmax(lp, l, mid, pl, pr));
if(pr > mid) res = max(res, qhmax(rp, mid + 1, r, pl, pr));
return res;
}
}
上面这道题并没有结合区间最值进行考察,那我们来看一道区间最值 + 区间历史最值的问题:【模板】线段树 3。
实际上有了前面的铺垫以后,这个题并没有这么困难。由于有最值操作,所以直接考虑数域划分。在划分后,由于我们还要维护历史最值,所以对于最大值和非最大值各开一套矩阵标记维护即可。也就是说,整体操作我们依然沿用区间最值的模板,但是标记的维护采用历史最值的方式来维护即可。
当然仍需要注意下传时是否有最大值的问题。核心代码如下:
namespace Sgt {
struct Tag {
int a, b;
Tag operator + (const Tag &p) const {
return (Tag){a + p.a, max(b + p.a, p.b)};
}
};
struct node {
int sum, cmx;//区间和 最大值数量
int mx, hmx;//最大值 最大值的历史最大值
int lmx, hlmx;//非最大值的最大值(次大值) 非最大值的历史最大值
Tag tgmx, tglmx;//最大值标记 非最大值标记
}t[Maxn << 2];
#define lp (p << 1)
#define rp (p << 1 | 1)
void pushup(int p) {
t[p].sum = t[lp].sum + t[rp].sum;
if(t[lp].mx > t[rp].mx) {
t[p].mx = t[lp].mx, t[p].cmx = t[lp].cmx, t[p].hmx = t[lp].hmx;
t[p].lmx = max(t[lp].lmx, t[rp].mx), t[p].hlmx = max({t[lp].hlmx, t[rp].hmx, t[rp].hlmx});
}
else if(t[lp].mx < t[rp].mx) {
t[p].mx = t[rp].mx, t[p].cmx = t[rp].cmx, t[p].hmx = t[rp].hmx;
t[p].lmx = max(t[lp].mx, t[rp].lmx), t[p].hlmx = max({t[lp].hmx, t[lp].hlmx, t[rp].hlmx});
}
else {
t[p].mx = t[lp].mx, t[p].cmx = t[lp].cmx + t[rp].cmx, t[p].hmx = max(t[lp].hmx, t[rp].hmx);
t[p].lmx = max(t[lp].lmx, t[rp].lmx), t[p].hlmx = max(t[lp].hlmx, t[rp].hlmx);
}
}
void build(int p, int l, int r) {
t[p].tgmx = {0, -Inf}, t[p].tglmx = {0, -Inf};
if(l == r) {
t[p].sum = t[p].mx = t[p].hmx = a[l];
t[p].cmx = 1;
t[p].lmx = t[p].hlmx = -Inf;
return ;
}
int mid = (l + r) >> 1;
build(lp, l, mid), build(rp, mid + 1, r);
pushup(p);
}
void pushtag(int p, int l, int r, Tag tgmx, Tag tglmx) {
t[p].sum += t[p].cmx * tgmx.a + (r - l + 1 - t[p].cmx) * tglmx.a;
t[p].hmx = max(t[p].mx + tgmx.b, t[p].hmx), t[p].mx = t[p].mx + tgmx.a;
if(t[p].lmx != -Inf) {
t[p].hlmx = max(t[p].lmx + tglmx.b, t[p].hlmx), t[p].lmx = t[p].lmx + tglmx.a;
}
t[p].tgmx = tgmx + t[p].tgmx, t[p].tglmx = tglmx + t[p].tglmx;
}
void pushdown(int p, int l, int r) {
int mx = max(t[lp].mx, t[rp].mx);
int mid = (l + r) >> 1;
pushtag(lp, l, mid, t[lp].mx == mx ? t[p].tgmx : t[p].tglmx, t[p].tglmx);
pushtag(rp, mid + 1, r, t[rp].mx == mx ? t[p].tgmx : t[p].tglmx, t[p].tglmx);
t[p].tgmx = {0, -Inf}, t[p].tglmx = {0, -Inf};
}
void mdfsum(int p, int l, int r, int pl, int pr, int x) {
if(pl <= l && r <= pr) {
pushtag(p, l, r, (Tag){x, x}, (Tag){x, x});
return ;
}
pushdown(p, l, r);
int mid = (l + r) >> 1;
if(pl <= mid) mdfsum(lp, l, mid, pl, pr, x);
if(pr > mid) mdfsum(rp, mid + 1, r, pl, pr, x);
pushup(p);
}
void mdfmin(int p, int l, int r, int pl, int pr, int x) {
if(t[p].mx <= x) return ;
if(pl <= l && r <= pr && t[p].lmx < x) {
pushtag(p, l, r, (Tag){x - t[p].mx, x - t[p].mx}, (Tag){0, -Inf});
return ;
}
pushdown(p, l, r);
int mid = (l + r) >> 1;
if(pl <= mid) mdfmin(lp, l, mid, pl, pr, x);
if(pr > mid) mdfmin(rp, mid + 1, r, pl, pr, x);
pushup(p);
}
int qmax(int p, int l, int r, int pl, int pr) {
if(pl <= l && r <= pr) {
return t[p].mx;
}
pushdown(p, l, r);
int mid = (l + r) >> 1, res = -Inf;
if(pl <= mid) res = max(res, qmax(lp, l, mid, pl, pr));
if(pr > mid) res = max(res, qmax(rp, mid + 1, r, pl, pr));
return res;
}
int qhmax(int p, int l, int r, int pl, int pr) {
if(pl <= l && r <= pr) {
return max(t[p].hmx, t[p].hlmx);
}
pushdown(p, l, r);
int mid = (l + r) >> 1, res = -Inf;
if(pl <= mid) res = max(res, qhmax(lp, l, mid, pl, pr));
if(pr > mid) res = max(res, qhmax(rp, mid + 1, r, pl, pr));
return res;
}
int qsum(int p, int l, int r, int pl, int pr) {
if(pl <= l && r <= pr) {
return t[p].sum;
}
pushdown(p, l, r);
int mid = (l + r) >> 1, res = 0;
if(pl <= mid) res += qsum(lp, l, mid, pl, pr);
if(pr > mid) res += qsum(rp, mid + 1, r, pl, pr);
return res;
}
}
7 李超线段树
7.1 概念
李超线段树是线段树的一种变种,主要用于维护二维平面上的直线信息以及查询操作。它的应用范围很广,只要式子的形式能转化为一次函数就可以考虑使用李超线段树进行求解 / 优化。
具体的,李超线段树支持下面两种操作:
- 动态在平面中插入一条线段 / 直线。
- 在平面上询问与一条直线
相交的线段 / 直线中,交点纵坐标最大点的坐标 / 编号。
7.2 过程
李超线段树的实质是在
- 该线段定义域覆盖整个区间。
- 该线段在区间中点处取值最大。
那么考虑怎样插入的时候维护这个最优线段。设当前遍历到区间
- 若当前区间无最优线段,或
完全在 上方,将 的最优线段改为 。 - 若
完全在 上方,则 最优线段不变。 - 否则,比较二者在
处的大小,然后向下递归:- 此时考虑上面不是最优线段的线段
,它仍可能成为接下来子区间的最优线段。我们现在需要知道这条线段 和另一条线段在哪个区间相交,就向哪个区间递归。 - 具体的,我们比较两个线段在两个端点的大小情况,然后以此判断交点位置即可。如果
在左端点处取值更小,又因为 在中点处取值也更小,因此交点必在右区间;否则就应该在左区间。
- 此时考虑上面不是最优线段的线段
对于查询,由于我们维护的是每一个区间在
如果插入的是直线,那么修改和查询的复杂度均为
同时值得注意的是,由于李超线段树常常维护的是值域上的信息,因此会需要用动态开点,而李超线段树动态开点的空间复杂度是
7.3 例题
例 1 [HEOI2013] Segment
模板题,代码如下:
#include <bits/stdc++.h>
using namespace std;
const int Maxn = 2e5 + 5;
const int Mod = 1e9;
const double eps = 1e-10;
int n, tot;
struct Seg {
int id;
double k, b;//线段表示为 y=kx+b
double calc(int x) {
return x * k + b;
}
};
namespace Sgt {
Seg t[Maxn << 2];
#define lp (p << 1)
#define rp (p << 1 | 1)
int fcmp(double x, double y) {//浮点数比较精度
if(fabs(x - y) <= eps) return 0;
if(x - y > eps) return 1;
else return -1;
}
void ins(int p, int l, int r, Seg sg) {//在整个区间插入线段
if(!t[p].k && !t[p].b) {//没有线段就直接插入
t[p] = sg;
return ;
}
int mid = (l + r) >> 1;
if(fcmp(t[p].calc(mid), sg.calc(mid)) == -1) swap(t[p], sg);//换最优线段
if((l == r) || (fcmp(t[p].calc(l), sg.calc(l)) == 1 && fcmp(t[p].calc(r), sg.calc(r)) == 1)) {//到叶子节点或没有交点就返回
return ;
}
if(fcmp(t[p].calc(l), sg.calc(l)) == 1) ins(rp, mid + 1, r, sg);//看哪边有交点
else ins(lp, l, mid, sg);
}
void mdf(int p, int l, int r, int pl, int pr, Seg sg) {//找到插入的区间
if(pl <= l && r <= pr) {
ins(p, l, r, sg);
return ;
}
int mid = (l + r) >> 1;
if(pl <= mid) mdf(lp, l, mid, pl, pr, sg);
if(pr > mid) mdf(rp, mid + 1, r, pl, pr, sg);
}
#define pdi pair<double, int>
#define mk make_pair
pdi _max(pdi x, pdi y) {
if(fcmp(x.first, y.first) == 0) return x.second < y.second ? x : y;
if(fcmp(x.first, y.first) == 1) return x;
else return y;
}
pdi query(int p, int l, int r, int x) {
if(l == r) {
return mk(t[p].calc(x), t[p].id);
}
int mid = (l + r) >> 1;
pdi res = mk(t[p].calc(x), t[p].id);
if(x <= mid) return _max(res, query(lp, l, mid, x));//标记永久化思想
else return _max(res, query(rp, mid + 1, r, x));
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
int lst = 0;
while(n--) {
int opt;
cin >> opt;
switch(opt) {
case 0: {
int k;
cin >> k;
k = (k + lst - 1) % 39989 + 1;
pdi res = Sgt::query(1, 1, 39989, k);
lst = res.second;
cout << lst << '\n';
break;
}
case 1: {
++tot;
int x_0, y_0, x_1, y_1;
cin >> x_0 >> y_0 >> x_1 >> y_1;
x_0 = (x_0 + lst - 1) % 39989 + 1, x_1 = (x_1 + lst - 1) % 39989 + 1;
y_0 = (y_0 + lst - 1) % Mod + 1, y_1 = (y_1 + lst - 1) % Mod + 1;
double k, b;
if(x_0 == x_1) {//注意特判 x0=x1 的情况
k = 0, b = max(y_0, y_1);
}
else {
if(x_0 > x_1) swap(x_0, x_1), swap(y_0, y_1);
k = (y_1 - y_0) * 1.0 / (x_1 - x_0);
b = y_0 - x_0 * k;
}
Sgt::mdf(1, 1, 39989, x_0, x_1, (Seg){tot, k, b});
break;
}
}
}
return 0;
}
例 2 [SDOI2016] 游戏
我们发现操作所增加的数字是
但是此时这个
- 当
在 上时,增加的数字是 ,即 。 - 当
在 上时,增加的数字是 ,即 。
如此便可转化为关于
时间复杂度其实是一个较为惊悚的
例 3 [CEOI2017] Building Bridges
由于桥梁不能在空中相交,所以选出来的柱子一定是顺次相连。那么直接考虑 dp,设
显然后面的和式可用前缀和优化,然后将其写为一次函数斜截式的式子:
这是斜率优化 dp 的标准式,不过遗憾的是,式子中的
可以看出,李超线段树有时可以替代凸包维护的单调性,直接求出答案。
例 4 [CF932F] Escape Through Leaf
不难想到可以直接树形 dp,设
显然后面的式子是一个关于
那么仿照线段树合并,我们可以得出李超线段树合并的作法如下:
- 若
有一个空节点,直接返回不是空节点的那一个。 - 否则,将
中的直线直接插入 中,然后再向下递归合并。
看上去这个做法非常暴力,就是将
- 考虑每条直线在合并时的操作,如果这条直线有变动,那么只有两种可能:被删除或者深度加一。那么每条直线最多加
次深度就会被删除,所以每一条直线的操作次数是 的,即总时间复杂度为 。
当然由于需要线段树合并,所以李超线段树也需要动态开点,代码如下:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int Maxn = 2e5 + 5;
const int Inf = 1e18;
int n, a[Maxn], b[Maxn];
int head[Maxn], edgenum;
struct node {
int nxt, to;
}edge[Maxn];
void add(int u, int v) {
edge[++edgenum] = {head[u], v};
head[u] = edgenum;
}
const int M = 1e5;
struct Seg {
int k, b;
int operator() (const int &x) const {
return k * x + b;
}
};
int rt[Maxn];
namespace Sgt {
struct node {
int l, r;
Seg sg;
}t[Maxn * 20];
#define lp t[p].l
#define rp t[p].r
int tot = 0;
void ins(int &p, int l, int r, Seg sg) {
if(!p) {
t[p = ++tot].sg = sg;
return ;
}
int mid = (l + r) >> 1;
if(t[p].sg(mid) > sg(mid)) swap(t[p].sg, sg);
if((l == r) || (t[p].sg(l) < sg(l) && t[p].sg(r) < sg(r))) return ;
if(t[p].sg(l) < sg(l)) ins(rp, mid + 1, r, sg);
else ins(lp, l, mid, sg);
}
int merge(int x, int y, int l, int r) {
if(!x || !y) return x + y;
ins(x, l, r, t[y].sg);
if(l == r) {
return x;
}
int mid = (l + r) >> 1;
t[x].l = merge(t[x].l, t[y].l, l, mid);
t[x].r = merge(t[x].r, t[y].r, mid + 1, r);
return x;
}
int query(int p, int l, int r, int x) {
if(!p) return Inf;
if(l == r) {
return t[p].sg(x);
}
int mid = (l + r) >> 1;
if(x <= mid) return min(query(lp, l, mid, x), t[p].sg(x));
else return min(query(rp, mid + 1, r, x), t[p].sg(x));
}
}
int dp[Maxn];
void dfs(int x, int fth) {
bool flg = 0;
for(int i = head[x]; i; i = edge[i].nxt) {
int to = edge[i].to;
if(to == fth) continue;
dfs(to, x);
flg = 1;
rt[x] = Sgt::merge(rt[x], rt[to], -M, M);
}
if(flg) dp[x] = Sgt::query(rt[x], -M, M, a[x]);
else dp[x] = 0;
Sgt::ins(rt[x], -M, M, (Seg){b[x], dp[x]});
}
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];
for(int i = 1; i <= n; i++) cin >> b[i];
for(int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
add(u, v), add(v, u);
}
dfs(1, 0);
for(int i = 1; i <= n; i++) {
cout << dp[i] << " ";
}
return 0;
}
8 前缀最值线段树
8.1 概念
前缀最值线段树这个名称其实不那么具体,更具体的来讲应该叫 pushup
显然,我们要保证楼房能被看见,就要保证斜率单调递增。那么我们在每一个位置上维护斜率,答案就是以
合并时不能简单的将左右儿子的
- 考虑
的左儿子的 值,如果其值大于 ,说明这个区间的开头在左区间,递归到左区间。此时右区间可以全选,所以答案是 。 - 如果左儿子的
值不大于 ,说明区间开头在右区间,答案自然是 。
那么 pushup
的时候就应该将 pushup
都要做
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int Maxn = 2e5 + 5;
const int Inf = 2e9;
const double eps = 1e-10;
int n, m;
namespace Sgt {
struct node {
double mx;
int len;
}t[Maxn << 2];
#define lp (p << 1)
#define rp (p << 1 | 1)
int upd(int p, int l, int r, double val) {
if(l == r) {
return (t[p].mx - val > eps ? 1 : 0);
}
int mid = (l + r) >> 1;
double res = t[lp].mx;
if(res - val > eps) return upd(lp, l, mid, val) + t[p].len - t[lp].len;
else return upd(rp, mid + 1, r, val);
}
void pushup(int p, int l, int r) {
t[p].mx = max(t[lp].mx, t[rp].mx);
int mid = (l + r) >> 1;
t[p].len = upd(rp, mid + 1, r, t[lp].mx) + t[lp].len;
}
void mdf(int p, int l, int r, int x, double val) {
if(l == r) {
t[p].mx = val;
t[p].len = 1;
return ;
}
int mid = (l + r) >> 1;
if(x <= mid) mdf(lp, l, mid, x, val);
else mdf(rp, mid + 1, r, x, val);
pushup(p, l, r);
}
int query() {
return t[1].len;
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
while(m--) {
int x, y;
cin >> x >> y;
double k = 1.0 * y / x;
Sgt::mdf(1, 1, n, x, k);
cout << Sgt::query() << '\n';
}
return 0;
}
但是此时会发现,上面的做法在第一种情况中求右区间贡献时,用的是总贡献减左区间贡献。这就要求维护的东西有可减性,但显然有的信息并没有。此时我们将
- 如果左儿子的
值大于 ,说明这个区间的开头在左区间,递归到左区间。此时右区间可以全选,所以答案是 。 - 否则说明这个区间的开头在右区间,答案即为
。
那么此时 pushup
的时候就应该将
9 标记拼接
9.1 概念
标记拼接其实只是一种思想,它的意思是说对于某个信息,我们不好直接维护出来,就需要在线段树上维护一些其它的标记来维护出这个信息的更新。一般情况下这种题目的难点就在于 pushup
函数的细节较繁琐。
举个最经典的例子就是线段树维护区间最大子段和,显然这个值是难以直接维护的,所以我们考虑再维护出区间的前缀和最大值 和 后缀和最大值来辅助求解。
9.2 例题
例 1 [CF1192B] Dynamic Diameter
题目要求直径,也就是树上两点间的最长距离。不妨写出树上两点间距离公式,即
根据欧拉序的相关定义可知,
现在相当于我们要找出三个点
的最大值。 的最小值。 的 的最大值。 的 的最大值。- 满足要求的
的最大值。
标记拼接维护上述五个标记即可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探