线段树进阶
永久化标记
永久化标记的思想是每个节点记录一个懒标记,不下传懒标记,而是在查询的时候将路径上的懒标记合并到答案上来。
struct Node
{
int rev;
} t[400005];
int n, m;
void update(int i, int l, int r, int a, int b)
{
if (b <= l || a >= r) return;
if (l >= a && r <= b)
{
t[i].rev = 1 - t[i].rev;
}
else
{
int mid = (l + r) >> 1;
update(LT(i), l, mid, a, b);
update(RT(i), mid, r, a, b);
}
}
int query(int i, int l, int r, int x, int op)
{
if (t[i].rev) op = 1 - op;
if (l == r - 1)
{
return op;
}
else
{
int mid = (l + r) >> 1;
if (x < mid)
{
return query(LT(i), l, mid, x, op);
}
else
{
return query(RT(i), mid, r, x, op);
}
}
}
int main()
{
FR;
scanf("%d %d", &n, &m);
rep(i, 1, m)
{
int op;
scanf("%d", &op);
if (op == 1)
{
int l, r;
scanf("%d %d", &l, &r);
update(1, 1, n + 1, l, r + 1);
}
else
{
int k;
scanf("%d", &k);
printf("%d\n", query(1, 1, n + 1, k, 0));
}
}
return 0;
}
权值线段树
权值线段树是一种基于线段树对值域区间进行操作的一种线段树,其可以向其值域区间内插入数字,然后统计值域区间内的信息。
int n, m;
ll arr[1005];
ll discre[1005];
ll dim[1005][1005];
void add(ll t[], int x, ll val)
{
while (x < 30005)
{
t[x] += val;
x += lowbit(x);
}
}
ll query(ll t[], int x)
{
ll ans = 0;
while (x >= 1)
{
ans += t[x];
x -= lowbit(x);
}
return ans;
}
void solve(int id)
{
memset(dim, 0, sizeof(dim));
scanf("%d %d", &n, &m);
rep(i, 1, n)
{
scanf("%lld", arr + i);
discre[i] = arr[i];
}
sort(discre + 1, discre + 1 + n);
int ed = unique(discre + 1, discre + 1 + n) - discre;
unordered_map<ll, int> up;
reb(i, 1, ed)
{
up[discre[i]] = i;
}
rep(i, 1, n)
{
arr[i] = up[arr[i]];
}
ll ans = 0;
rep(i, 1, n)
{
ans += query(dim[m - 1], arr[i] - 1);
per(i, m - 1, 2)
{
add(dim[i], arr[i], query(dim[i - 1], arr[i] - 1));
}
add(dim[1], arr[i], 1);
}
printf("Case #%d: %lld\n", id, ans);
}
int main()
{
FR;
int T;
scanf("%d", &T);
rep(i, 1, T)
{
solve(i);
}
return 0;
}
除此之外,权值线段在离散化之后可以轻松维护中位数,插入,删除,查询的诗句复杂度都是 O ( log n ) O(\log n) O(logn)的。
我们要求出每个叶子节点的中位数,可以使用对顶堆或权值线段树,其中对顶堆不好处理删除元素,因此我们采用权值线段树维护。
计算完之后,就可以在树上进行博弈论DP。
核心代码为获取第K大数:
int getKth(int i, int l, int r, int k)
{
if (l == r - 1)
{
return l;
}
else
{
int mid = HF(l + r);
if (nodes[LT(i)] >= k)
{
return getKth(LT(i), l, mid, k);
}
else
{
return getKth(RT(i), mid, r, k - nodes[LT(i)]);
}
}
}
zkw线段树
zkw线段树由清华大学张昆玮(zkw)发明,相较于普通线段树,其优点有:
- 非递归,时间常数小
- 使用二进制思想,代码短,方便书写
建树
我们强制要求叶子节点(即区间大小为1的节点)排列在最后一行,并且还是一颗满二叉树(多余的节点不用)。
这样我们就可以写出建树的代码,此时只更新了叶子节点的信息,我们没有更新非叶子节点的信息。
void build(int n)
{
// 寻找最后一行的大小,也是非叶子节点的大小
for (M = 1; M < n + 2; M <<= 1)
;
// 依次填充叶子节点
for (int i = 1; i <= n; i++)
tree[i + M] = arr[i];
}
下面是更新非叶子节点的信息:
- 区间和:
for (int i = M - 1; i; --i) tree[i] = tree[i << 1] + tree[i << 1 | 1];
完整的建树代码:
void build(int n)
{
// 寻找最后一行的大小,也是非叶子节点的大小
for (M = 1; M < n + 2; M <<= 1)
;
// 依次填充叶子节点
for (int i = 1; i <= n; i++)
tree[i + M] = arr[i];
// 更新非叶子节点信息,以区间和为例
for (int i = M - 1; i; --i) tree[i] = tree[i << 1] + tree[i << 1 | 1];
}
单点更新
我们只需要更新叶子节点,以及向上更新父节点即可。
void update(int x, int val)
{
// 更新叶子节点
tree[M + x] = val;
// 向上更新父节点
for (int i = (M + x) >> 1; i; i >>= 1)
{
tree[i] = tree[i << 1] + tree[i << 1 | 1];
}
}
单点查询
直接返回即可。
int get(int x)
{
return tree[x + M];
}
区间查询
区间和查询的思想是逐步向上进行求和,使用双指针的方法,如果指针l或者r在某一个节点上,表示该节点管理的区间已经求和完毕,那么我们应该考虑他的兄弟节点。
停止的条件是l^r^1
为0时停止,该断言为0,表示l^r
为1,表示l和r互为兄弟节点,此时求和完毕。
此时我们考虑兄弟节点是否求和,如果l是左节点,说明l的右节点一定包含在区间内,应该求和,如果r是右节点,那么兄弟节点也在区间内,也应该求和,其他情况则不求和。
int querySum(int l, int r)
{
int ans = 0;
for (l += M - 1, R += M + 1; l ^ r ^ 1; l >>= 1, r >>= 1)
{
if (~l & 1)
ans += tree[l ^ 1];
if (r & 1)
ans += tree[r ^ 1];
}
return ans;
}
此时,zkw线段树的基础应用结束,下面讲讲更复杂的区间操作。
自底向下的标记
下面我们模仿普通线段树,实现一个带有自底向下的标记的zkw线段树,我们需要通过上传标记实现。
需要根据大小计算懒标记的数值,并向上传递。
ll querySum(int l, int r)
{
ll ans = 0;
int LN = 0;
int RN = 0;
int NN = 1;
for (l += M - 1, r += M + 1; l ^ r ^ 1; l >>= 1, r >>= 1, NN <<= 1)
{
if (~l & 1)
{
ans += tree[l ^ 1] + lazy[l ^ 1] * NN;
LN += NN;
}
if (r & 1)
{
ans += tree[r ^ 1] + lazy[r ^ 1] * NN;
RN += NN;
}
ans += lazy[l >> 1] * LN + lazy[r >> 1] * RN;
}
NN = LN + RN;
for (l >>= 1; l; l >>= 1)
{
ans += lazy[l] * NN;
}
return ans;
}
void add(int l, int r, ll val)
{
int NN = 1;
ll LS = 0;
ll RS = 0;
for (l += M - 1, r += M + 1; l ^ r ^ 1; l >>= 1, r >>= 1, NN <<= 1)
{
if (~l & 1)
{
lazy[l ^ 1] += val;
LS += val * NN;
}
if (r & 1)
{
lazy[r ^ 1] += val;
RS += val * NN;
}
tree[l >> 1] += LS;
tree[r >> 1] += RS;
}
LS += RS;
for (l >>= 1; l; l >>= 1)
{
tree[l] += LS;
}
}
变换求和次序
懒标记一般并不好理解且极易容易出Bug,我们通过仅仅访问前缀和的方式实现区间加法和区间查询。
我们设 b i b_i bi是 a i a_i ai的差分数组,如果我们要求 a i a_i ai的某一段前缀和,那么:
该公式的核心是将横向求和变换为纵向求和。
∑ i = 1 r a i = ∑ i = 1 r ∑ j = 1 i b j = ∑ i = 1 r b i × ( r − i + 1 ) = ( r + 1 ) ∑ i = 1 r b i − ∑ i = 1 r b i × i \begin{aligned} &\sum_{i=1}^{r} a_i\\=&\sum_{i=1}^r\sum_{j=1}^i b_j\\=&\sum_{i=1}^r b_i\times(r-i+1) \\=& (r+1) \sum_{i=1}^r b_i - \sum_{i=1}^r b_i\times i \end{aligned} ===i=1∑raii=1∑rj=1∑ibji=1∑rbi×(r−i+1)(r+1)i=1∑rbi−i=1∑rbi×i
故我们需要两个结构来维护 b i b_i bi和 b i × i b_i \times i bi×i的前缀和。
我们可以将上述querySum方法的l设置为1,就是前缀和了。
这样我们区间加和区间求和就转换为单点修改和前缀和查询了,这一点和树状数组的思想是一致的。
可持久化标记与RMQ
接下来我们通过可持久化标记实现RMQ-zkw线段树。(以区间最大值为例)
zkw线段树是自底向上实现的线段树,那么普通线段树懒标记的方式就行不通了,我们必须找到一个自底向上的方式实现,即树上差分。
现在每一个节点不再保存值,而是保存当前节点的值减去父节点的值的差。
这种方法叫做可持久化标记。
那么我们的建树方式也应该做出调整,我们只需要在循环中更新差值即可。
void build(int n)
{
// 寻找最后一行的大小,也是非叶子节点的大小
for (M = 1; M < n + 2; M <<= 1)
;
// 依次填充叶子节点
for (int i = 1; i <= n; i++)
tree[i + M] = arr[i];
for (int i = M - 1; i; --i)
{
tree[i] = max(tree[i << 1], tree[i << 1 | 1]);
// 更新差值
tree[i << 1] -= tree[i];
tree[i << 1 | 1] -= tree[i];
}
}
然后我们根据差值计算RMQ,其实是一个决策两个子树的过程。
int queryMax(int l, int r)
{
int ans = 0;
int L = -INF, R = -INF; // 注意这里的初始值
for (l += M - 1, r += M + 1; l ^ r ^ 1; l >>= 1, r >>= 1)
{
if (~l & 1)
L = max(L, tree[l ^ 1]);
if (r & 1)
R = max(R, tree[r ^ 1]);
L += tree[l >> 1];
R += tree[l >> 1];
}
ans = max(L, R);
for (l >>= 1; l; l >>= 1)
{
ans += tree[l];
}
return ans;
}
然后我们需要实现给某一段区间进行连续加法。
拆位线段树
拆位线段树一般用于解决区间二进制问题,例如对一段区间进行区间异或等运算。其思想是把二进制每一位都建立一颗线段树,然后按照位处理的方法处理线段树即可。
struct Node
{
int val;
int laz;
} t[20][400005];
int arr[100005];
int n, m;
void buildTree(int i, int l, int r, int bit)
{
if (l == r - 1)
{
t[bit][i].val = (arr[l] & MSK(bit)) != 0;
}
else
{
int mid = HF(l + r);
buildTree(LT(i), l, mid, bit);
buildTree(RT(i), mid, r, bit);
t[bit][i].val = t[bit][LT(i)].val + t[bit][RT(i)].val;
}
}
void pushdown(int i, int l, int r, int bit)
{
if (!t[bit][i].laz) return;
int mid = HF(l + r);
t[bit][LT(i)].val = (mid - l) - t[bit][LT(i)].val;
t[bit][LT(i)].laz = 1 - t[bit][LT(i)].laz;
t[bit][RT(i)].val = (r - mid) - t[bit][RT(i)].val;
t[bit][RT(i)].laz = 1 - t[bit][RT(i)].laz;
t[bit][i].laz = 0;
}
void update(int i, int l, int r, int a, int b, int bit)
{
if (b <= l || a >= r) return;
if (l >= a && r <= b)
{
t[bit][i].val = (r - l) - t[bit][i].val;
t[bit][i].laz = 1 - t[bit][i].laz;
return;
}
int mid = HF(l + r);
pushdown(i, l, r, bit);
update(LT(i), l, mid, a, b, bit);
update(RT(i), mid, r, a, b, bit);
t[bit][i].val = t[bit][LT(i)].val + t[bit][RT(i)].val;
}
int query(int i, int l, int r, int a, int b, int bit)
{
if (b <= l || a >= r) return 0;
if (l >= a && r <= b)
{
return t[bit][i].val;
}
int mid = HF(l + r);
int ans = 0;
pushdown(i, l, r, bit);
ans += query(LT(i), l, mid, a, b, bit);
ans += query(RT(i), mid, r, a, b, bit);
return ans;
}
int main()
{
FR;
scanf("%d", &n);
rep(i, 1, n) { scanf("%d", arr + i); }
reb(i, 0, 20) { buildTree(1, 1, n + 1, i); }
scanf("%d", &m);
rep(i, 1, m)
{
int t;
scanf("%d", &t);
if (t == 1)
{
int l, r;
scanf("%d %d", &l, &r);
ll ans = 0;
reb(i, 0, 20) { ans += 1ll * MSK(i) * query(1, 1, n + 1, l, r + 1, i); }
printf("%lld\n", ans);
}
else
{
int l, r, x;
scanf("%d %d %d", &l, &r, &x);
reb(i, 0, 20)
{
if (x & MSK(i))
{
update(1, 1, n + 1, l, r + 1, i);
}
}
}
}
return 0;
}
暴力线段树
暴力线段树,在修改的时候进行暴力修改即可,再加上一定的优化即可ac。
暴力取模,如果遇到区间最大值小于模数直接跳过即可,大于模数进行暴力取模。
存在结论 x m o d p < x 2 x \mod p < \frac{x}{2} xmodp<2x,因此对一个数取模不会超过 O ( log x ) O(\log x) O(logx)次。
int n, M;
struct Node
{
ll sum;
ll mx;
} t[400005];
ll arr[100005];
void buildTree(int i, int l, int r)
{
if (l == r - 1)
{
t[i].mx = arr[l];
t[i].sum = arr[l];
}
else
{
int mid = HF(l + r);
buildTree(LT(i), l, mid);
buildTree(RT(i), mid, r);
t[i].sum = t[LT(i)].sum + t[RT(i)].sum;
t[i].mx = max(t[LT(i)].mx, t[RT(i)].mx);
}
}
void update(int i, int l, int r, int x, ll val)
{
if (l == r - 1)
{
t[i].sum = val;
t[i].mx = val;
}
else
{
int mid = HF(l + r);
if (x < mid)
{
update(LT(i), l, mid, x, val);
}
else
{
update(RT(i), mid, r, x, val);
}
t[i].sum = t[LT(i)].sum + t[RT(i)].sum;
t[i].mx = max(t[LT(i)].mx, t[RT(i)].mx);
}
}
ll query(int i, int l, int r, int a, int b)
{
if (b <= l || a >= r) return 0;
if (l >= a && r <= b)
{
return t[i].sum;
}
int mid = HF(l + r);
ll ans = 0;
ans += query(LT(i), l, mid, a, b);
ans += query(RT(i), mid, r, a, b);
return ans;
}
void mod(int i, int l, int r, int a, int b, ll m)
{
if (b <= l || a >= r) return;
if (l >= a && r <= b && t[i].mx < m)
{
return;
}
if (l == r - 1)
{
t[i].mx = t[i].mx % m;
t[i].sum = t[i].sum % m;
return;
}
int mid = HF(l + r);
mod(LT(i), l, mid, a, b, m);
mod(RT(i), mid, r, a, b, m);
t[i].sum = t[LT(i)].sum + t[RT(i)].sum;
t[i].mx = max(t[LT(i)].mx, t[RT(i)].mx);
}
int main()
{
FR;
scanf("%d %d", &n, &M);
rep(i, 1, n) { scanf("%lld", arr + i); }
buildTree(1, 1, n + 1);
rep(i, 1, M)
{
int op;
scanf("%d", &op);
if (op == 1)
{
int l, r;
scanf("%d %d", &l, &r);
printf("%lld\n", query(1, 1, n + 1, l, r + 1));
}
else if (op == 2)
{
int l, r;
ll m;
scanf("%d %d %lld", &l, &r, &m);
mod(1, 1, n + 1, l, r + 1, m);
}
else
{
int k;
ll x;
scanf("%d %lld", &k, &x);
update(1, 1, n + 1, k, x);
}
}
return 0;
}
存在 x > 4 x>4 x>4, x < x 2 \sqrt{x} < \frac{x}{2} x<2x,因此对一个数开方不会超过 O ( log x ) O(\log x) O(logx)次。故使用暴力线段树即可,维护最大值,如果最大值等于 1 1 1,则可以跳过区间。
计算几何覆盖线段树
一般计算几何扫描线问题都需要维护一段被线段覆盖的长度,此时我们可以使用线段树维护:
void updateSelf(int i, int l, int r)
{
if (t[i].cnt > 0)
t[i].len = decre[r] - decre[l];
else
t[i].len = t[i << 1].len + t[(i << 1) | 1].len;
}
void cover(int i, int l, int r, int L, int R)
{
if (r <= L || l >= R)
return;
if (l >= L && r <= R)
{
t[i].cnt++;
updateSelf(i, l, r);
return;
}
int mid = (l + r) >> 1;
cover(i << 1, l, mid, L, R);
cover((i << 1) | 1, mid, r, L, R);
updateSelf(i, l, r);
}
void uncover(int i, int l, int r, int L, int R)
{
if (r <= L || l >= R)
return;
if (l >= L && r <= R)
{
t[i].cnt--;
updateSelf(i, l, r);
return;
}
int mid = (l + r) >> 1;
uncover(i << 1, l, mid, L, R);
uncover((i << 1) | 1, mid, r, L, R);
updateSelf(i, l, r);
}
int query()
{
return t[1].len;
}
注意,此代码仅支持先覆盖后退出,有时需要离散化。