<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>线段树 on Zirnc's Blog</title><link>https://blog.chungzh.cn/blog/%E7%BA%BF%E6%AE%B5%E6%A0%91/</link><description>Recent content in 线段树 on Zirnc's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sun, 30 Apr 2023 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.chungzh.cn/blog/%E7%BA%BF%E6%AE%B5%E6%A0%91/index.xml" rel="self" type="application/rss+xml"/><item><title>Luogu-P4755 Beautiful Pair</title><link>https://blog.chungzh.cn/oi-history/luogu-p4755/</link><pubDate>Sun, 30 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/luogu-p4755/</guid><description>Luogu-P4755 Beautiful Pair
题意 小 D 有个数列 ${a}$，当一个数对 $(i,j)$（$i \le j$）满足 $a_i$ 和 $a_j$ 的积不大于 $a_i, a_{i+1}, \ldots, a_j$ 中的最大值时，小 D 认为这个数对是美丽的。请你求出美丽的数对的数量。
$1\le n\le{10}^5$，$1\le a_i\le{10}^9$。
编程时的问题 对 ST 表不熟悉！ 更 zz 的是，对 lower_bound 和 upper_bound 理解有问题，来复习一下小学知识：lower_bound 是找到“大于等于”的位置，upper_bound 是“大于”。写这道题的时候找小于某数的位置莫名其妙地用了 lower_bound，更没有 -1，完全是随手写的，半天也没察觉到这里有问题。 综上，我是 zz。
思路 考虑分治（据说这是套路），我们找出一个区间 $[l, r]$ 内的最大值位置 $mid$，然后统计所有跨过 $mid$ 的答案，再递归处理 $[l, mid-1], [mid+1, r]$。假设 $mid$ 左边的数是 $a_i$，右边的数是 $a_j$，根据题目得 $a_i * a_j \le a_{mid}$，即 $a_j \le \lfloor\frac{a_{mid}}{a_i}\rfloor$。那么我们枚举 $a_i$，然后用主席树统计右区间内小于 $\lfloor\frac{a_{mid}}{a_i}\rfloor$ 的数的个数。</description></item><item><title>李超线段树笔记</title><link>https://blog.chungzh.cn/oi-history/li-chao-tree/</link><pubDate>Tue, 16 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/li-chao-tree/</guid><description>线段树之标记永久化 普通的线段树在做区间修改时依赖懒标记（lazy tag），当我们从一个点向下访问时，需要将标记 pushdown。能否避免如此多的 pushdown 操作呢？这时需要用到标记永久化技巧。
我们要做的就是将 lazy tag 永久地留在当前的结点，这时子树中的所有结点都不会被这个 tag 所影响。因此，子树中询问的最大值 = 实际最大值 - tag。当我们想得到正确答案时，只要将子树返回的最大值加上当前 tag 即可。
标记永久化存在局限性，需要满足不同的修改操作可以交换顺序，或者说对答案的贡献是独立的这一条件。
举个例子：区间设置+区间加法，先设置后加和先加后设置的结果是不一样的，因此不能交换顺序。如果使用标记永久化，就可能改变了这个顺序。
比如我们先设置后加，并且令设置的区间比加的区间大，因此加的 tag 在下方，设置的 tag 在上方。 根据前面的方法，我们会从下往上取 tag，也就是先加法，再设置。 这时我们发现，由于上层是一个设置操作，下面的所有答案最终都变成了设置的那个数字，下层操作就失效了。显然有问题。 总结：标记永久化就是不再下放标记，而是让标记永久地停留在当前结点上。在统计答案时再考虑标记的影响。
复杂度分析：由于标记不会下放，但如果有两个标记落在了一个结点上，我们不会分别存储这两个标记，而是加起来合成一个标记（$(+2) + (+3) = (+5)$）。因此，每个结点最多只有一个标记。询问时最多考虑 $\log n$ 个标记，复杂度和普通线段树相同 $O(\log n)$。
程序实现：
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int up(int p) { tree[p].mx = max(tree[p&amp;lt;&amp;lt;1].mx, tree[p&amp;lt;&amp;lt;1|1].mx)+tree[p].tag; } int query(int p, int l, int r, int x, int y) { if (l &amp;gt;= x &amp;amp;&amp;amp; r &amp;lt;= y) return tree[p].</description></item><item><title>Luogu-P3521 「POI2011」ROT-Tree Rotations</title><link>https://blog.chungzh.cn/oi-history/luogu-p3521/</link><pubDate>Sun, 14 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/luogu-p3521/</guid><description>题意 给定一颗有 $n$ 个叶节点的二叉树。每个叶节点都有一个权值 $p_i$（注意，根不是叶节点），所有叶节点的权值构成了一个 $1 \sim n$ 的排列。
对于这棵二叉树的任何一个结点，保证其要么是叶节点，要么左右两个孩子都存在。
现在你可以任选一些节点，交换这些节点的左右子树。
在最终的树上，按照先序遍历遍历整棵树并依次写下遇到的叶结点的权值构成一个长度为 $n$ 的排列，你需要最小化这个排列的逆序对数。
$2 \leq n \leq 2 \times 10^5$， $0 \leq x \leq n$，所有叶节点的权值是一个 $1 \sim n$ 的排列。
分析 按照先序遍历整棵树，取叶结点，用人话说就是从左到右取叶子结点。
重要性质：交换了一个点的左右子树之后，不会影响左子树内和右子树内的逆序对数量。
考虑一个任意的结点，对它的子树中叶子的逆序对进行分类讨论：
都在左子树内； 都在右子树内； 跨越左右子树。 交换左右子树之后，受到影响的显然只有第三种情况。第一、第二种情况分治下去就可以转化成第三种再计算。
如何计算答案呢？一开始，对于每一个叶子结点，我们都建立一棵权值线段树（动态开点）并记录 $p_i$ 出现了 $1$ 次。合并 $r1, r2$ 时，逆序对的个数就是 $tree[rc[r1]] \times tree[lc[r2]]$。因为 $r1, r2$ 对应的权值区间 $[L, R]$ 是相同的，而 $lc[r1], lc[r2]$ 对应的权值区间就是 $[L, M]$， $rc[r1], rc[r2]$ 对应的权值区间是 $[M+1, R]$，$rc[]$ 中记录的数都比 $lc[]$ 中的要大，而 $r1$ 对应的数的编号小于 $r2$（也就是 $r1$ 是左子树，$r2$ 是右子树），满足逆序对的条件。</description></item><item><title>可持久化线段树笔记</title><link>https://blog.chungzh.cn/oi-history/persistent-seg-tree/</link><pubDate>Sun, 14 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/persistent-seg-tree/</guid><description>动态开点线段树 常规写法的线段树只能维护不算很长的数组，由于空间不够，对于 $10^9$ 级别的数组却不能很好地维护。所以，我们要用到动态开点线段树。
核心思想：节点只有在有需要的时候才被创建。
比如说，要求在一个长度为 $n &amp;lt; 10^9$ 的数组上实现区间求和、单点修改的操作，初始数组元素值均为 0。
那么，我们一开始只创建一个根结点，接下来遵循动态开点的核心思想进行操作。
比如下面这张图的例子，我们依次修改 1, 2, 8 三个结点，途中创建了必要的结点。而在图中没有显示的空结点并没有被创建，视为 0，这样就节省了空间。
那么对于区间修改时，会有 pushdown() 操作，可能会修改一个不存在的结点。这时有两个解决方案：
在 pushdown() 时，如果缺少孩子，就直接创建一个新的孩子就可以了。 使用 标记永久化 技巧（李超线段树），让结点不再进行 pushdown()，进一步节省了空间。 复杂度分析：单次操作的时间复杂度是不变的，为 $O(\log n)$。对于空间复杂度，由于每次操作都有可能创建并访问全新的一系列结点，因此 $m$ 次操作的空间复杂度是 $O(m\log n)$，不再是原本线段树的 $O(n)$。
代码实现：
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 int n, cnt, root; // cnt 表示当前结点个数 int sum[N*2], ls[N*2], rs[N*2]; void upd(int&amp;amp; rt, int l, int r, int p, int f) { // 注意这里传入一个引用，可以修改 ls 或 rs 数组 if (!</description></item><item><title>线段树合并笔记</title><link>https://blog.chungzh.cn/oi-history/merge-seg-tree/</link><pubDate>Sun, 14 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/merge-seg-tree/</guid><description>前置知识：动态开点线段树。
二叉树合并 合并是一个递归的过程。首先合并两棵以 $u, v$ 为根的二叉树：
考虑左子树 如果 $u, v$ 都没有左子树，那么直接留空； 如果只有 $u$ 有左子树，那么 $u$ 的左子树保留不动； 如果只有 $v$ 有左子树，那么将 $v$ 的左子树接过来，成为 $u$ 的左子树； 如果 $u, v$ 均有左子树，那么递归合并 $u, v$ 的左子树，结果赋给 $u$ 的左子树。 考虑右子树 如果 $u, v$ 都没有右子树，那么直接留空； 如果只有 $u$ 有右子树，那么 $u$ 的右子树保留不动； 如果只有 $v$ 有右子树，那么将 $v$ 的右子树接过来，成为 $u$ 的右子树； 如果 $u, v$ 均有右子树，那么递归合并 $u, v$ 的右子树，结果赋给 $u$ 的右子树。 最后我们就将两棵二叉树合并成了一个以 $u$ 为根的二叉树。
复杂度分析：在上面的过程中，仅当 $u, v$ 均有左（右）孩子时才会进行递归，访问这个左（右）孩子。时间复杂度就是两棵二叉树中重复的结点的数量。</description></item><item><title>线段树笔记</title><link>https://blog.chungzh.cn/oi-history/segment-tree/</link><pubDate>Wed, 04 Aug 2021 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/segment-tree/</guid><description>线段树是一种高端的数据结构，可以用来在区间上进行信息统计。它能够在 $O(logN)$ 的时间复杂度内实现单点/区间修改、区间找最大值/最小值/总和/&amp;hellip;，适用于大规模的区间统计。
如下图就是一棵线段树。在结点中，你可以存对应区间的最大值，最小值，总和等等。
对于每一个结点 $i$，它的两个子结点分别是 $2i$ 和 $2i+1$。因此，在开树的数组时，最好要开到 $4N$ 的大小。
关于 $4N$，详见 OI-Wiki。
建树 下面是一个求区间和的线段树的建树代码。
通过 DFS 建树，到叶结点，然后一路回溯求出和。
1 2 3 4 5 6 7 8 9 10 11 12 13 void build_tree(int cur, int left, int right) { // cur 为当前树的根，[left, right] 是当前树对应的区间 if (left == right) { // 到叶子节点了，区间长度为 1，总和就是它本身 tree[cur] = a[left]; return ; } int leftSon = cur*2, rightSon = leftSon+1; int mid = (left+right)/2; build_tree(leftSon, left, mid); build_tree(rightSon, mid+1, right); tree[cur] = tree[leftSon]+tree[rightSon]; // 求和 } 这里求 leftSon，rightSon，mid 的模式在线段树的所有操作中都会用到。</description></item></channel></rss>