<?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/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/</link><description>Recent content in 学习笔记 on Zirnc's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sat, 29 Jul 2023 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.chungzh.cn/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/index.xml" rel="self" type="application/rss+xml"/><item><title>网络流笔记</title><link>https://blog.chungzh.cn/oi-history/flow/</link><pubDate>Sat, 29 Jul 2023 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/flow/</guid><description>最大流 概念 容量：$capacity(e)$ 表示一条有向边 $e(u, v)$ 的最大允许的流量。 流量：$flow(e)$ 表示一条有向边 $e(u, v)$ 总容量中已被占用的流量。 剩余流量（残量）：$w(e) = c(e)-f(e)$，表示当前时刻某条有向边 $e(u, v)$ 总流量中未被占用的部分。 反向边：原图中每一条有向边在残量网络中都有对应的反向边，反向边的容量为 $0$，容量的变化与原边相反；『反向边』的概念是相对的，即一条边的反向边的反向边是它本身。 残量网络：在原图的基础之上，添加每条边对应的反向边，并储存每条边的当前流量。残量网络会在算法进行的过程中被修改。 增广路（augmenting path）：残量网络中从源点到汇点的一条路径，增广路上所有边中最小的剩余容量为增广流量。 增广（augmenting）：在残量网络中寻找一条增广路，并将增广路上所有边的流量加上增广流量的过程。 层次：$level(u)$ 表示节点 $u$ 在层次图中与源点的距离。 层次图：在原残量网络中按照每个节点的层次来分层，只保留相邻两层的节点的图，满载（即流量等于容量）的边不存在于层次图中。 流的三个重要性质：
容量限制：对于每条边，流经该边的流量不得超过该边的容量，即，$f(u,v)\leq c(u,v)$ 斜对称性：每条边的流量与其相反边的流量之和为 0，即 $f(u,v)=-f(v,u)$ 流守恒性：从源点流出的流量等于汇点流入的流量，即 $\forall x\in V-{s,t},\sum_{(u,x)\in E}f(u,x)=\sum_{(x,v)\in E}f(x,v)$ 最大流问题：指定合适的流 $f$，以最大化整个网络的流量（即 $\sum_{(s,v)\in E}f(s,v)$）。
Ford-Fulkerson 增广 增广路指一条从 $s$ 到 $t$ 的路径，路径上每条边残余容量都为正。把残余容量为正（$w(u, v) \gt 0$）的边设为可行边，然后搜索寻找增广路。
找到一条增广路后，这条路能够增广的流值由路径上边的最小残留容量 $w(u, v)$（记为 $flow$）决定。将这条路径上每条边的 $w(u, v)$ 减去 $flow$。最后，路径上每条边的反向边残留容量要加上 $flow$。
为什么增广路径上每条边的反向边的残留容量值要加上 $flow$？因为斜对称性，残量网络=容量网络-流量网络，容量网络不变时，流量网络上的边的流量增加 $flow$，反向边流量减去 $flow$，残量网络就会发生相反的改变。从另一个角度来说，这个操作可以理解为「退流」，给了我们一个反悔的机会，让增广路的顺序不受限制。</description></item><item><title>初等数论入门</title><link>https://blog.chungzh.cn/oi-history/number-theory-1/</link><pubDate>Fri, 05 May 2023 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/number-theory-1/</guid><description>我也不知道这是从哪本书上抠来的？
整除 定义 1：如果 $a$ 和 $b$ 为整数且 $a \ne 0$，我们说 $a$ 整除 $b$ 是指存在整数 $c$ 使得 $b=ac$。如果 $a$ 整除 $b$，我们还称 $a$ 是 $b$ 的一个因子，且称 $b$ 是 $a$ 的倍数。
如果 $a$ 整除 $b$，则将其记为 $a \mid b$，如果 $a$ 不能整除 $b$，则记其为 $a \nmid b$。
定理 1：如果 $a, b$ 和 $c$ 是整数，且 $a \mid b, b \mid c$，则 $a \mid c$。
定理 2：如果 $a, b, m$ 和 $n$ 为整数，且 $c \mid a, c \mid b$，则 $c \mid (ma+nb)$。</description></item><item><title>CDQ 分治笔记</title><link>https://blog.chungzh.cn/oi-history/cdq-divide/</link><pubDate>Fri, 28 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/cdq-divide/</guid><description>基本思想 CDQ 分治的基本思想十分简单。如下：
我们要解决一系列问题，这些问题一般包含修改和查询操作，可以把这些问题排成一个序列，用一个区间 $[L,R]$ 表示。 分。递归处理左边区间 $[L,M]$ 和右边区间 $[M+1,R]$ 的问题。 治。合并两个子问题，同时考虑到 $[L,M]$ 内的修改对 $[M+1,R]$ 内的查询产生的影响。即，用左边的子问题帮助解决右边的子问题。 这就是 CDQ 分治的基本思想。和普通分治不同的地方在于，普通分治在合并两个子问题的过程中，$[L,M]$ 内的问题不会对 $[M+1,R]$ 内的问题产生影响。
前置知识：二维偏序 给定 $N$ 个有序对 $(a,b)$，求对于每个 $(a,b)$，满足 $a2&amp;lt;a$ 且 $b2&amp;lt;b$ 的有序对 $(a2,b2)$ 有多少个。
可以将归并排序求逆序对的思路套用过来，这题实际上就是求顺序对。首先根据 $a$ 的大小排序，然后归并排序 $b$，这样就可以忽略 $a$ 元素的影响，因为左边区间的元素的 $a$ 一定小于右边元素的 $a$。归并排序时，每次从右边区间的有序序列取一个元素，然后求左边区间多少个元素比它小即可。
更浅显的解法是，用树状数组代替 CDQ 分治。这里就不赘述。
放个求逆序对的代码：
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void mergesort(int l, int r) { if (l &amp;gt;= r) return ; int mid = (l+r)/2; mergesort(l, mid); mergesort(mid+1, r); int lp = l, rp = mid+1; int i = l; while (lp &amp;lt;= mid &amp;amp;&amp;amp; rp &amp;lt;= r) { if (a[lp] &amp;gt; a[rp]) { ans += mid-lp+1; b[i++] = a[rp++]; } else { b[i++] = a[lp++]; } } while (lp &amp;lt;= mid) b[i++] = a[lp++]; while (rp &amp;lt;= r) b[i++] = a[rp++]; for (int i = l; i &amp;lt;= r; i++) a[i] = b[i]; } 例题 一：三维偏序 Luogu-P3810 【模板】三维偏序（陌上花开）</description></item><item><title>高斯消元笔记</title><link>https://blog.chungzh.cn/oi-history/gauss/</link><pubDate>Fri, 28 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/gauss/</guid><description>消元法及高斯消元法思想 消元法是将方程组中的一方程的未知数用含有另一未知数的代数式表示，并将其带入到另一方程中，这就消去了一未知数，得到一解；或将方程组中的一方程倍乘某个常数加到另外一方程中去，也可达到消去一未知数的目的。消元法主要用于二元一次方程组的求解。
消元法理论的核心 消元法理论的核心主要如下：
两方程互换，解不变； 一方程乘以非零数 $k$，解不变； 一方程乘以数 $k$ 加上另一方程，解不变。 过程 解方程组：
$$ \begin{cases} 2x_1+x_2-x_3=8 \ -3x_1-x_2+2x_3=-11 \ -2x_1+x_2+2x_3=-3 \end{cases} $$
写成矩阵的形式为：
$$ \left[\begin{matrix} 2 &amp;amp; 1 &amp;amp; -1 \ -3 &amp;amp; -1 &amp;amp; 2 \ -2 &amp;amp; 1 &amp;amp; 2 \end{matrix} \middle| \begin{matrix} 8 \ -11 \ -3 \end{matrix} \right] $$
这种矩阵称为增广矩阵。所谓增广矩阵，即为方程组系数矩阵 $A$ 与常数列 $b$ 的并生成的新矩阵，即 $(A | b)$，增广矩阵行初等变换化为行最简形，即是利用了高斯消元法的思想理念，省略了变量而用变量的系数位置表示变量，增广矩阵中用竖线隔开了系数矩阵和常数列，代表了等于符号。
我们从上到下依次处理每一行，处理完第 $i$ 行后，让 $A_{ii}$ 非 $0$，而 $A_{ji}(j\gt i)$ 均为 $0$。过程如下。</description></item><item><title>二分图笔记</title><link>https://blog.chungzh.cn/oi-history/bigraph/</link><pubDate>Sat, 15 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/bigraph/</guid><description>定义 在图论中，二分图（bipartite graph）是一类特殊的图，又称为二部图、偶图、双分图。二分图的顶点可以分成两个互斥的独立集 $U$ 和 $V$ 的图，使得所有边都是连结一个 $U$ 中的点和一个 $V$ 中的点。
给定一个二分图 $G$，在 $G$ 的一个子图 $M$ 中，$M$ 的边集中的任意两条边都没有共同的端点，则称 $M$ 是一个匹配。
最小点覆盖：选最少的点，满足每条边至少有一个端点被选。
交错路始于非匹配点且由匹配边与非匹配边交错而成。
增广路是始于非匹配点且终于非匹配点的交错路。
特性 二分图中不存在奇环
因为每一条边都是从一个集合走到另一个集合，只有走偶数次才可能回到同一个集合。
König 定理：一个图是二分图当且仅当它的最小顶点覆盖的顶点数等于最大匹配的边数
首先，最小点集覆盖一定 &amp;gt;= 最大匹配，因为假设最大匹配为 $n$，那么我们就得到了 $n$ 条互不相邻的边，光覆盖这些边就要用到 $n$ 个点。现在我们来思考为什么最小点击覆盖一定 &amp;lt;= 最大匹配。任何一种 $n$ 个点的最小点击覆盖，一定可以转化成一个 $n$ 的最大匹配。因为最小点集覆盖中的每个点都能找到至少一条只有一个端点在点集中的边（如果找不到则说明该点所有的边的另外一个端点都被覆盖，所以该点则没必要被覆盖，和它在最小点集覆盖中相矛盾），只要每个端点都选择一个这样的边，就必然能转化为一个匹配数与点集覆盖的点数相等的匹配方案。所以最大匹配至少为最小点集覆盖数，即最小点击覆盖一定 &amp;lt;= 最大匹配。综上，二者相等。
二分图判定 染色法：用 $1,2$ 两种颜色标记图中的节点，与一个节点相邻的所有节点的颜色必须和它不同，若标记过程中出现冲突，说明图中存在奇环。使用 DFS 实现。$O(N+M)$。
CF687A NP-Hard Problem 二分图判定裸题。
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 #include &amp;lt;bits/stdc++.</description></item><item><title>欧拉函数笔记</title><link>https://blog.chungzh.cn/oi-history/euler/</link><pubDate>Sat, 01 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/euler/</guid><description>定义 欧拉函数 $\varphi(n)$ 表示小于等于 $n$，且与 $n$ 互质的正整数的个数。
如何求 $\varphi(n)$？
比如 $varphi(12)$ 把 $12$ 质因数分解，$12=2^2*3$，其实就是得到了 $2$ 和 $3$ 两个互异的质因子。
然后把 $2$ 的倍数和 $3$ 的倍数都删掉。
$2$ 的倍数：$2,4,6,8,10,12$
$3$ 的倍数：$3,6,9,12$
但是是 $6$ 和 $12$ 重复减了。所以还要把既是 $2$ 的倍数又是 $3$ 的倍数的数加回来。所以这样写：$12 - 12/2 - 12/3 + 12/(2*3)$。运用了容斥原理。
性质 欧拉函数是积性函数。
积性是什么意思呢？如果有 $\gcd(a, b) = 1$，那么 $\varphi(a \times b) = \varphi(a) \times \varphi(b)$。
特别地，当 $n$ 是奇数时 $\varphi(2n) = \varphi(n)$。
$n = \sum_{d \mid n}{\varphi(d)}$。</description></item><item><title>欧拉回路笔记</title><link>https://blog.chungzh.cn/oi-history/euler-graph/</link><pubDate>Sat, 25 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/euler-graph/</guid><description>定义 欧拉回路：通过图中每条边恰好一次的回路 欧拉通路（欧拉路径）：通过图中每条边恰好一次的通路 欧拉图：具有欧拉回路的图 半欧拉图：具有欧拉通路但不具有欧拉回路的图 判定 无向图是欧拉图当且仅当： 非零度顶点是连通的 顶点的度数都是偶数 无向图是半欧拉图当且仅当： 非零度顶点是连通的 恰有 0 或 2 个奇度顶点 有向图是欧拉图当且仅当： 非零度顶点是强连通的 每个顶点的入度和出度相等 有向图是半欧拉图当且仅当： 非零度顶点是弱连通的 至多一个顶点的出度与入度之差为 1 至多一个顶点的入度与出度之差为 1 其他顶点的入度和出度相等 弱连通：将所有有向边替换为无向边后，整张图连通。
Hierholzer 算法 Hierholzer 算法的具体步骤：遍历当前节点的所有出边，并 DFS 访问相邻顶点，将经过的边删掉。遍历完所有出边后，将 $u$ 加入栈中。最后把栈中的顶点反过来，再输出，就是欧拉回路。
如果要求字典序最小，只需在一开始对每个点的所有出边从小到大排序。这样一来，欧拉回路上从左往右看，每个点的后继都取到了理论最小值。
对于无向图和有向图的欧拉路径，必须从奇点或唯一的出度比入度大 1 的点开始 dfs。
Luogu-P7771 【模板】欧拉路径
实现时，要注意一个小细节：用 hd[x] 数组记录节点 $x$ 目前删到了哪条边，每次走过一条边时要 hd[x]++，然后下次到达这个点时再调用这个值。否则的话，每次到了这个点都要从 0 开始判断这些边是否被删除掉，会超时。
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 #include &amp;lt;bits/stdc++.</description></item><item><title>割点和桥笔记</title><link>https://blog.chungzh.cn/oi-history/cut/</link><pubDate>Sun, 19 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/cut/</guid><description>定义 若对于无向连通图的一个点 $x$，从图中删去这个点和与这个点相连的所有边后，图不再是连通图，则 $x$ 为这个图的割点。
若对于无向连通图的一条边 $e$，从图中删去这条边后，图不再是连通图，则 $e$ 为这个图的割边（桥）。
求解 无向图的搜索树 从任意一个点出发进行 DFS，每个点只能访问一次，所有被访问过的结点和边构成一棵搜索树。
然后就可以将图上的边分为两类，树边和返祖边，返祖边连接了一个点和它的一个祖先。
时间戳 dfn 和追溯值 low $dfn[x]$ 表示在 DFS 的过程中，$x$ 第一次被访问的顺序。
$low[x]$ 表示 $x$ 和 $x$ 的子树中所有点的时间戳 和 从 $x$ 的子树中的点通过仅一条返祖边可以达到的点的时间戳 的最小值。
更新 $low$ 的方法：
如果 $v$ 是 $u$ 的儿子：$low[u] = min(low[u], low[v])$ 否则：$low[u] = min(low[u], dfn[v])$ 割点的判定 对于某个点 $u$，如果它的儿子中存在一个点 $v$，使得 $low[v] \ge dfn[u]$，即不能回到祖先，那么 $u$ 就是割点。
对于搜索树的根节点就比较特殊，如果它在搜索树中只有一个儿子，是不能成为割点的，需要特判。
树的叶子节点由于没有儿子，也不能成为割点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void tarjan(int u, int father) { int child = 0; vis[u] = 1; low[u] = dfn[u] = ++inde; for (int i = 0; i &amp;lt; g[u].</description></item><item><title>树链剖分笔记</title><link>https://blog.chungzh.cn/oi-history/hld/</link><pubDate>Sat, 25 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/hld/</guid><description>树链剖分（本文仅介绍 重链剖分（Heavy-Light Decomposition））的用途：
更新树上两点之间的路径上的所有点的值 求树上两点之间的路径上的最大值、最小值、和（或任意满足结合律的运算） 思想 树链剖分即把整棵树剖分成若干条链，然后用线段树等数据结构来维护链上的信息。重链剖分可以将树上的任何一条路径划分成不超过 $O(\log n)$ 条连续的链。
定义：
重子节点 表示其子节点中子树最大的子结点。如果有多个子树最大的子结点，取其一。如果没有子节点，就无重子节点。 轻子节点 表示剩余的所有子结点。 从这个结点到重子节点的边为 重边。 到其他轻子节点的边为 轻边。 若干条首尾衔接的重边构成 重链。 把落单的结点也当作重链，那么整棵树就被剖分成若干条重链。
性质 树上每个节点都属于且仅属于一条重链。
重链开头的结点不一定是重子节点（因为重边是对于每一个结点都有定义的）。
所有的重链将整棵树 完全剖分。
在剖分时 重边优先遍历，最后树的 DFN 序上，重链内的 DFN 序是连续的。按 DFN 排序后的序列即为剖分后的链。
一颗子树内的 DFN 序是连续的。
可以发现，当我们向下经过一条 轻边 时，所在子树的大小至少会除以二。
因此，对于树上的任意一条路径，把它拆分成从 $lca$ 分别向两边往下走，分别最多走 $O(\log n)$ 次，因此，树上的每条路径都可以被拆分成不超过 $O(\log n)$ 条重链。
实现 模板 RECORD。</description></item><item><title>最短路笔记</title><link>https://blog.chungzh.cn/oi-history/shortest-path/</link><pubDate>Sat, 01 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/shortest-path/</guid><description>又是一年 CSP 复赛，已经一年没写过最短路了，赶紧复习一下。
Floyd-Warshall 算法 Floyd 算法是用来求所有结点对最短路的。适用于所有不含负环的图。
这个算法运用了 DP 的思想。首先定义 f[k][i][j] 表示只允许经过结点 $1, 2, \cdots k$，结点 $i$ 到结点 $j$ 的最短路长度。初始化时，f[k][i][i] = 0，其他赋值为 $+\infty$。可以有 f[k][i][j] = min(f[k-1][i][j], f[k-1][i][k] + f[k-1][k][j])（f[k-1][i][j] 表示不经过 $k$ 点的最短路径，f[k-1][i][k] + f[k-1][k][j] 表示经过 $k$ 点的最短路径）。这时可以发现，数组的第一维是可以忽略的，所以直接写成 f[i][j] = min(f[i][j], f[i][k] + f[k][j])。
时间、空间复杂度均为 $O(N^3)$。
实现：
1 2 3 4 for (int k = 1; k &amp;lt;= n; k++) for (int i = 1; i &amp;lt;= n; i++) for (int j = 1; j &amp;lt;= n; j++) f[i][j] = min(f[i][j], f[i][k] + f[k][j]); 无向图的最小环问题 Luogu-P6175 无向图的最小环问题</description></item><item><title>拓扑排序笔记</title><link>https://blog.chungzh.cn/oi-history/topo-sort/</link><pubDate>Fri, 26 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/topo-sort/</guid><description>引入 给定一张有向无环图（DAG, Directed Acyclic Graph），对其顶点进行排序，使得对于每条从 $u$ 到 $v$ 的有向边 $(u, v)$，$u$ 在排序中都在 $v$ 的前面。这种排序就称为拓扑排序（Topological sorting）。
当且仅当图中没有定向环时（即有向无环图），才有可能进行拓扑排序。如果排序失败，就说明该有向图存在环，不是 DAG。
任何有向无环图至少有一个拓扑排序。
举例：在某校的选课系统中，存在这样的规则：每门课可能有若干门先修课，如果要修读某一门课，则必须要先修读此课程所要求的先修课后才能修读。假设一个学生同时只能修读一门课程，那么，被选课系统允许的他修完他需要所有课程的顺序是一个拓扑序。
在这个例子中，每一门课程对应有向图中的一个顶点，每一个先修关系对应一条有向边（从先修课指向需要先修课的课）。
算法 Kahn 算法 初始状态下，集合 $S$ 装着所有入度为 $0$ 的点，$L$ 是一个空列表。
每次从 $S$ 中任意取出一个点 $u$ 放入 $L$, 然后将 $u$ 的所有边 $(u, v_1), (u, v_2), (u, v_3) \cdots$ 删除。对于边 $(u, v)$，若将该边删除后点 $v$ 的入度变为 $0$，则将 $v$ 放入 $S$ 中。
不断重复以上过程，直到集合 $S$ 为空。检查图中是否存在任何边，如果有，那么这个图一定有环路，否则返回 $L$，$L$ 中顶点的顺序就是拓扑排序的结果。
基本上就是 BFS 的框架。
代码实现：
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int n, m; vector&amp;lt;int&amp;gt; G[MAXN]; int in[MAXN]; // 存储每个结点的入度 bool toposort() { vector&amp;lt;int&amp;gt; L; queue&amp;lt;int&amp;gt; S; for (int i = 1; i &amp;lt;= n; i++) if (in[i] == 0) S.</description></item><item><title>位运算笔记</title><link>https://blog.chungzh.cn/oi-history/bit-operation/</link><pubDate>Sun, 21 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/bit-operation/</guid><description>基本概念 比特（bit，亦称二进制位）是指 1 位二进制的数码（0 或 1），是计算机中信息的最小单位。
字节（byte）：一个字节由 8 位组成。
熟练地运用位运算，可以提高我们程序的时空效率。
计算机中的整数存储与运算 下面以 32 位二进制数，即 C++ 中的 int 和 unsigned int 类型为例。
原码、反码 简单介绍一下：
原码：最高位为符号位，正数为 $0$，负数为 $1$，其余所有位为十进制数的绝对值。
优点：对人类而言最直观。 缺点：无法将减法转换成加法运算。如：$1-1=1+(-1)=0001+1001=1010=-2$；$0$ 有两种表示方法 $0000$ 和 $1000$。 反码：最高位为符号位，正数为 $0$，负数为 $1$。正数的反码等于本身，负数的反码除符号位外，各位取反。
优点：解决了减法运算的问题。$1-1=1+(-1)=0001+1110=1111=0$ 缺点：$0$ 有两种表示方法 $0000$ 和 $1111$；减法算法规则较复杂，需要额外判断溢出。 补码 32 位无符号整数 unsigned int： 直接把这 32 位编码 $C$ 看作 32 位二进制数 $N$。
32 位有符号整数 int： 以最高位作为符号位，$0$ 表示非负数，$1$ 表示负数。</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>树状数组笔记</title><link>https://blog.chungzh.cn/oi-history/binary-indexed-tree/</link><pubDate>Mon, 15 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/binary-indexed-tree/</guid><description>早就学习过线段树了，但惭愧的是更简单的树状数组却一直没有深入理解过，仅仅停留在背代码的层级。今天认真学习一下树状数组。
引入 树状数组（Binary Index Tree, BIT / Fenwick Tree）支持单点修改和区间查询两种简单操作，时间复杂度均为 $O(\log n)$。它的实现比线段树简单，速度更快，但功能稍逊一筹。
原理 我们用 $C_i$ 来表示 $A$ 数组的一段区间，定义 $x$ 的二进制表示中，最低位的 $1$ 的位置为 $\operatorname{lowbit}(x)$，那么用 $C_i$ 代表 $A$ 数组的下标区间 $[i-\operatorname{lowbit}(i)+1, i]$。举个例子，$4_{(10)} = 100_{(2)}$，$100_{(2)}-\operatorname{lowbit}(100_{(2)})+1_{(2)}=1_{(2)}=1_{(10)}$，那么 $C_4$ 代表的区间就是 $[1, 4]$。通过这样的设计，树状数组将结点数压缩到与数组长度相同，不像线段树一样需要 $2n$ 个结点。
之所以会有这个特点，是因为对于位置 $i$，其对应的结点所在的高度就是 $\operatorname{lowbit}(i)$ 的位数。第一层结点为全体 $2^0 + 2^1k$，即所有 $\operatorname{lowbit}(i)=1$ 的数字；第二层结点为全体 $2^1 + 2^2k$ ，即所有 $\operatorname{lowbit}(i)=2$ 的数字；第三层结点为全体 $2^2 + 2^3k$ ，即所有 $\operatorname{lowbit}(i)=4$ 的数字；以此类推。也就是说，对于位置 $i$，在这个位置往上垂直追溯，能追溯的层数就是 $i$ 的二进制表示的末尾 $0$ 数量。而结点高度又决定了其子树的大小，于是它所代表的信息区间大小也就一定是 $2^{i的末尾0数量}=\operatorname{lowbit}(i)$。
*来源于参考资料 1
实现 $\operatorname{lowbit}$ 如何计算呢？我们有这样一条公式：$\operatorname{lowbit}(x)=(x)&amp;amp;(-x)$。在计算机中，有符号数采用补码表示。在补码表示下，$x$ 的相反数 -x = ~x + 1，也就是按位非再加一。例如 $x$ 的最后一个 $1$ 的位置附近是 $\cdots 01000\cdots$，按位非之后是 $\cdots 10111\cdots$，加一再变成 $\cdots 11000\cdots$；而前面每一位都与原来相反。这时我们再把它和 $x$ 按位与，得到的结果为 $01000\cdots$ 即 $\operatorname{lowbit}(x)$。</description></item><item><title>斜率优化 DP 笔记</title><link>https://blog.chungzh.cn/oi-history/slope-opt-dp/</link><pubDate>Sat, 13 Aug 2022 21:59:00 -0800</pubDate><guid>https://blog.chungzh.cn/oi-history/slope-opt-dp/</guid><description>X(j) 和斜率均单调的斜率优化 这是第一次学斜率优化学会的。
例题 [HNOI2008]玩具装箱 Luogu LOJ
题目描述：
P 教授要去看奥运，但是他舍不下他的玩具，于是他决定把所有的玩具运到北京。他使用自己的压缩器进行压缩，其可以将任意物品变成一堆，再放到一种特殊的一维容器中。
P 教授有编号为 $1 \cdots n$ 的 $n$ 件玩具，第 $i$ 件玩具经过压缩后的一维长度为 $C_i$。
为了方便整理，P 教授要求：
在一个一维容器中的玩具编号是连续的。
同时如果一个一维容器中有多个玩具，那么两件玩具之间要加入一个单位长度的填充物。形式地说，如果将第 $i$ 件玩具到第 $j$ 个玩具放到一个容器中，那么容器的长度将为 $x=j-i+\sum\limits_{k=i}^{j}C_k$。
制作容器的费用与容器的长度有关，根据教授研究，如果容器长度为 $x$，其制作费用为 $(x-L)^2$。其中 $L$ 是一个常量。P 教授不关心容器的数目，他可以制作出任意长度的容器，甚至超过 $L$。但他希望所有容器的总费用最小。
$1 \leq n \leq 5 \times 10^4$，$1 \leq L \leq 10^7$，$1 \leq C_i \leq 10^7$。
朴素 DP 做法 令状态 $f(i)$ 表示把前 $i$ 个玩具装箱的最小费用，$s(i)$ 为 $c_i$ 的前缀和。
假如将玩具 $j$ 到 $i$ 装在同一箱子，容易列出状态转移方程 $f(i) = \min_{1\le j\le i}{f(j-1)+(i-j+s(i)-s(j-1)-L)^2}$。</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/centroid-decomposition/</link><pubDate>Tue, 02 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/centroid-decomposition/</guid><description>点分治，外国人称之为 Centroid decomposition，重心分解。
何为树的重心 学习重心分解之前，自然要先了解重心。
下面统一用 $n$ 表示树上结点的个数。
在一棵树中，如果删除一个顶点后得到的最大子树的顶点数最少，那么这个点就是树的重心（Centroid）。
重心的性质：
删除重心后得到的所有子树，其顶点数必然不超过 $n/2$。
证明：选取任意顶点作为起点，每次都沿着边向最大子树的方向移动，最终一定会到达某个顶点，将其删除后得到的所有子树的顶点数都不超过 $n/2$。如果这样的点存在的话，那么也就可以证明删除重心后得到的所有子树的顶点数都不超过 $n/2$。
记当前顶点为 $v$，如果顶点 $v$ 已经满足上述条件则停止。否则，与顶点 $v$ 邻接的某个子树的顶点数必然大于 $n/2$。假设顶点 $v$ 与该子树中的顶点 $w$ 邻接，那么我们就把顶点 $w$ 作为新的顶点 $v$。不断重复这一步骤，必然会在有限步停止。这是因为对于移动中所用的边 $(v, w)$，必有 $v$ 侧的子树的顶点数小于 $n/2$，$w$ 侧的子树的顶点数大于 $n/2$，所以不可能再从 $w$ 移动到 $v$。因而该操作永远不会回到已经经过的顶点，而顶点数又是有限的，所以算法必然在有限步终止。
树中所有顶点到某个顶点的距离和中，到重心的距离和是最小的；如果有两个重心，那么到它们的距离和一样。
把两棵树通过一条边相连得到一棵新的树，那么新的树的重心在连接原来两棵树的重心的路径上。
在一棵树上添加或删除一个叶子，那么它的重心最多只移动一条边的距离。
更多证明请见：树的重心的性质及其证明 - suxxsfe - 博客园 (cnblogs.com)
寻找树的重心 根据重心的定义，先以 $1$ 为根进行 DFS。在递归中计算子树大小 $siz[u]$，并求出最大的子树的大小 $maxs[u]$，比较出重心 $centroid$。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void getCentroid(int u, int fa, int s) {  siz[u] = 1;  maxs[u] = 0;  for (int i = head[u]; i !</description></item><item><title>关于 int 与 long long 的运算速度</title><link>https://blog.chungzh.cn/oi-history/int-vs-longlong/</link><pubDate>Fri, 22 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/int-vs-longlong/</guid><description>前言 写一道 CF 题的时候，算法明明是正确的，却一直都 TLE。最后把一个 long long 类型的数组改成了 int，竟然就 AC 了。。
这不禁引发了我的思考，int 与 long long 的运算速度不一样吗？
不严谨测试 由于本菜鸡并没有什么计算机基础原理的知识，只好做了一个测试。当然，这个测试其实很不严谨，没有很大的参考价值。我也就图一乐，哈哈哈哈哈
测试环境 电脑：Lenovo Yoga 14sACH 2021 系统：Windows 11 25163.1010 CPU：AMD Ryzen 7 5800H with Radeon Graphics (16) @ 3.200GHz RAM：16.0 GB 编译器：GCC 11.2.0 代码 仅仅是为了图一乐， 我第一次使用了 Google Benchmark 这一工具。其实挺好上手的。
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 #include &amp;lt;benchmark/benchmark.</description></item><item><title>Treap 笔记</title><link>https://blog.chungzh.cn/oi-history/treap/</link><pubDate>Wed, 09 Feb 2022 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/treap/</guid><description>Treap = Tree + Heap
二叉搜索树（BST） 在学习 Treap 之前，需要先了解一下二叉搜索树（BST, Binary Search Tree）：
设 $x$ 是二叉搜索树中的一个结点。如果 $y$ 是 $x$ 左子树中的一个结点，那么 $y.key \lt x.key$。如果 $y$ 是 $x$ 右子树中的一个结点，那么 $y.key \gt x.key$。
BST 上的基本操作所花费的时间与这棵树的高度成正比。对于一个有 $n$ 个结点的二叉搜索树中，这些操作的最优时间复杂度为 $O(\log n)$，最坏为 $O(n)$。随机构造这样一棵二叉搜索树的期望高度为 $O(\log n)$。然而，当这棵树退化成链时，则同样的操作就要花费 $O(n)$ 的最坏运行时间。
由于普通 BST 容易退化，对于它的实现就不再赘述。在实践中需要使用如 Treap 这样的平衡二叉搜索树。
Treap 顾名思义，Treap 是树和堆的结合。它的数据结构既是一个二叉搜索树，又是一个二叉堆。
在 Treap 的每个结点中，除了 $key$ 值，还要保存一个 $fix$（更常见的是 $priority$）值。这个值是随机值，以它为依据来同时建立最大堆（或最小堆）。因为 $fix$ 值是随机的，所以可以让这棵树更加平衡，高度更接近 $O(\log n)$。它的各种操作期望时间复杂度都是 $O(\log n)$。
旋转式 Treap 旋转式 Treap 的常数较小。</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>