<?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/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/</link><description>Recent content in 数据结构 on Zirnc's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sun, 21 May 2023 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.chungzh.cn/blog/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/index.xml" rel="self" type="application/rss+xml"/><item><title>CF-559C Gerald and Giant Chess</title><link>https://blog.chungzh.cn/oi-history/cf-559c/</link><pubDate>Sun, 21 May 2023 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/cf-559c/</guid><description>CF-559C Gerald and Giant Chess / AtCoder DP-Y Grid 2
给定一个 $H*W$ 的棋盘，棋盘上只有 $N$ 个格子是黑色的，其他格子都是白色的。在棋盘左上角有一个卒，每一步可以向右或者向下移动一格，并且不能移动到黑色格子中。求这个卒从左上角移动到右下角，一共有多少种可能的路线。
$(1 ≤ h, w ≤ 105, 1 ≤ n ≤ 2000)$
$O(hw)$ 的暴力 DP 很好想，但是过不了。
假设没有障碍，从 $(1, 1)$ 到 $(i, j)$ 的方案数是 $C_{i+j-2}^{i-1}$（等于 $C_{i+j-2}^{j-1}$）。可以这么理解：可以用 $D, R$ 来表示一条路径，那么从 $(1, 1)$ 到 $(i, j)$ 的路径中有 $i-1$ 个 $D$ 和 $j-1$ 个 $R$。于是问题转化为从 $i+j-2$ 个位置中选 $i-1$ 个放 $D$ 的方案数。
如果有一个障碍，从正面统计方案数很困难，正难则反，考虑将总的方案数减去经过障碍的方案数。假设障碍的位置是 $(x, y)$，终点是 $(h, w)$，经过障碍的方案数就是 $C_{x+y-2}^{x-1} * C_{h-x+w-y}^{h-x}$（乘法原理）。</description></item><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/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>CF-1385E Directing Edges</title><link>https://blog.chungzh.cn/oi-history/cf-1385e/</link><pubDate>Fri, 26 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/cf-1385e/</guid><description>CF-1385E Directing Edges
题意 给定一个由有向边与无向边组成的图，现在需要你把所有的无向边变成有向边，使得形成的图中没有环。
如果可以做到请输出该图，否则直接输出&amp;quot;NO&amp;quot;。
分析 我们先只连接有向边，然后做一遍拓扑排序，如果失败了，就说明有环，输出 “NO”。
然后处理剩下的无向边。对于无向边 $(u, v)$，如果 $u$ 的拓扑序小于 $v$，那么令这条边的方向是 $u\rightarrow v$。否则，方向就是 $v\rightarrow u$。因为这条边是从拓扑序小的点指向拓扑序大的点，所以必然不会形成环。
RECORD
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 #include &amp;lt;algorithm&amp;gt;#include &amp;lt;cstdio&amp;gt;#include &amp;lt;cstring&amp;gt;#include &amp;lt;iostream&amp;gt;#include &amp;lt;vector&amp;gt; using namespace std; const int MAXN = 200005; int n, m; vector&amp;lt;int&amp;gt; G[MAXN]; int c[MAXN], topo[MAXN], id[MAXN], t, bn, x[MAXN], y[MAXN]; bool dfs(int u) { c[u] = -1; for (auto v : G[u]) { if (c[v] &amp;lt; 0) return false; else if (c[v] == 0 &amp;amp;&amp;amp; !</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/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>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>Luogu-P1776 宝物筛选</title><link>https://blog.chungzh.cn/oi-history/luogu-p1776/</link><pubDate>Fri, 12 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/luogu-p1776/</guid><description>Luogu-P1776 宝物筛选
题意 终于，破解了千年的难题。小 FF 找到了王室的宝物室，里面堆满了无数价值连城的宝物。
这下小 FF 可发财了，嘎嘎。但是这里的宝物实在是太多了，小 FF 的采集车似乎装不下那么多宝物。看来小 FF 只能含泪舍弃其中的一部分宝物了。
小 FF 对洞穴里的宝物进行了整理，他发现每样宝物都有一件或者多件。他粗略估算了下每样宝物的价值，之后开始了宝物筛选工作：小 FF 有一个最大载重为 $W$ 的采集车，洞穴里总共有 $n$ 种宝物，每种宝物的价值为 $v_i$，重量为 $w_i$，每种宝物有 $m_i$ 件。小 FF 希望在采集车不超载的前提下，选择一些宝物装进采集车，使得它们的价值和最大。
对于 $100%$ 的数据，$n\leq \sum m_i \leq 10^5$，$0\le W\leq 4\times 10^4$，$1\leq n\le 100$。
解法 1：二进制优化 每一个数都可以表示成 $2$ 的幂的和（因为每一个数都可以用二进制表示）。
时间复杂度：$O(nW\sum \log m_i)$
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 #include &amp;lt;bits/stdc++.</description></item><item><title>Luogu-P2254 「NOI2005」瑰丽华尔兹</title><link>https://blog.chungzh.cn/oi-history/luogu-p2254/</link><pubDate>Fri, 12 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.chungzh.cn/oi-history/luogu-p2254/</guid><description>「NOI2005」瑰丽华尔兹
题意 不妨认为舞厅是一个 $N$ 行 $M$ 列的矩阵，矩阵中的某些方格上堆放了一些家具，其他的则是空地。钢琴可以在空地上滑动，但不能撞上家具或滑出舞厅，否则会损坏钢琴和家具，引来难缠的船长。每个时刻，钢琴都会随着船体倾斜的方向向相邻的方格滑动一格，相邻的方格可以是向东、向西、向南或向北的。而艾米丽可以选择施魔法或不施魔法：如果不施魔法，则钢琴会滑动；如果施魔法，则钢琴会原地不动。
艾米丽是个天使，她知道每段时间的船体的倾斜情况。她想使钢琴在舞厅里滑行的路程尽量长，这样 1900 会非常高兴，同时也有利于治疗托尼的晕船。但艾米丽还太小，不会算，所以希望你能帮助她。
$100%$ 的数据中，$1\leq N, M \leq 200$，$K \leq 200$，$T\leq 40000$。
分析 首先我们定义一下状态。设 $dp[t][i][j]$ 表示 t 时刻，在 $(i, j)$ 滑行的最长路程长度。状态转移方程是 $dp[t][i][j] = \max(dp[t-1][i][j], dp[t-1][i^{&amp;rsquo;}][j^{&amp;rsquo;}])$，$i^{&amp;rsquo;}$ 和 $j^{&amp;rsquo;}$ 是合法的走过来的位置，取决于 $t$ 时刻船体倾斜的方向。
这样设计状态的话，时间复杂度为 $O(TNM)$，空间似乎也是问题。
这时候我们发现还有一个变量 $K$ 没有用到，考虑把状态设为 $dp[t][i][j]$ 表示在第 $t$ 时间段内，在 $(i, j)$ 滑行的最长路程长度。时间复杂度为 $O(KN^3)$，暂时还不行。
我们看看能不能进一步优化。下面假设当前船体倾斜的方向是东。设当前时间段长度是 $tim$，上一个时间段钢琴的位置在 $(i, m)$，那么 $dp[t][i][j] = \max_{j-m&amp;lt;=tim}{dp[t-1][i][m]+j-m}$。在这个式子中，只有 $m$ 一个变量，并且 $j-m&amp;lt;=tim$，合法决策在一段相邻区间内，可以用到单调队列优化！
对于两个决策 $m_1 &amp;lt; m_2$，$m_2$ 优于 $m_1$ 时仅当：
$$ \begin{aligned} dp[t-1][i][m_1]+(j-m_1) &amp;amp; &amp;lt; dp[t-1][i][m_2]+(j-m_2) \\ dp[t-1][i][m+1]+m_2-m_1 &amp;amp; &amp;lt; dp[t-1][i][m_2] \end{aligned} $$</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>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>