数据结构题目合集

比赛

理理思维

第一反应居然是分块而不是线段树?

线段树解法:因为只有 \(26\),所以每个节点开一个桶完全可以,操作一二不提,操作三只需要多次查询,然后多次赋值即可。

分块解法:操作一不提,操作二懒标记,操作三枚举 \(26\) 字母算 \(cnt\),然后桶排(可以用操作一)。同时做一点优化:如果查询到了一个有懒标记的整块,其实不用下放懒标记;操作三特判一下,其实对于有懒标记的整块,枚举 \(26\) 个字母浪费常数。(口胡一下)

CF877E

dfn 序转换到线段树上。

CF558E

线段树。

每个节点记录每个字母出现次数,每次修改只需要 \(1\) 次查询和 \(26\) 次修改。

排序操作转化为多次修改操作。

WYS

一眼扫描线。问题在于怎么判断一条边是右边的还是左边。

按照给出的顺序走一遍,如果一条边是 \(y\) 不变 \(x\) 减小,这条边就是往下的。

重点:多边形判断边的方向。

Addition Robot

发现:

\[\begin{pmatrix} A&B \end{pmatrix} \times \begin{pmatrix} 1&0\\1&1 \end{pmatrix} = \begin{pmatrix} A+B&B \end{pmatrix} \]

遇到 A 就看作乘以这个矩阵,遇到 B 也乘以对应的矩阵,转而维护区间矩阵乘法。询问时查询区间的矩阵乘法结果,然后乘上询问的 A,B 构成的矩阵即可。

至于区间翻转,就是把矩阵乘法结果旋转 \(180\) 度。因为是二阶矩阵,就是交换对角线。

线段树维护矩阵乘法。可以先尝试把一个区间的信息写成一个 DP 转移方程,然后把方程改写为矩阵形式。

堵塞的交通

用一列的两个格子表示一个结点。用布尔数组实时记录相邻两个格子是否连通。

线段树的结点属性为:只涉及它表示的区间,区间内左上、左下、右上、右下两两的连通性,共 \(C_4^2=6\) 对。

难点:如何合并两个区间,也就是 pushup。

例如 左左上-左左下 的连通性,要么是本来左半边的区间就连通,要么是有一条路径 左左上-右左上-右左下-左左下。

其它的类似。

但是这题还有一个坑点:修改区间的时候可能恰好卡在两个区间中间,也就是修改的 \(pos=(l+r)/2\) 的情况。这种情况我们特判一下,对当前结点更新一下六对状态,然后直接向上返回,其它的与正常情况相同 pushup。

其实还是结点记录多个属性。问什么,就记什么;问两个格子的连通性,就记录格子的连通性。

HDU5068

题意:每层都有两扇门,初始每扇门都连接下一层的两扇门。需要支持两个操作,一个是让某扇门与下一层的某扇门断连或者重新连上,一个是询问从 \(a\) 层任意一扇门到 \(b\) 层任意一扇门的方案数是多少。

每层到下一层用一个 \(2\times 2\) 的矩阵表示连通情况。用线段树维护矩阵乘法。

这题也可以把区间答案弄成 DP 转移方程的形式。

Intervals

贪心优先往右边放点,用树状数组快速求出区间内有多少个点。

注意此题要求要有空行,这个破玩意浪费我十分钟。

A Sequence of Numbers

\(x\)\(T\) 位为 \(1\),其实就是 \(2^k\le x\% 2^{k+1}<2^{k+1}\)

用一个变量 \(sum\) 记录现在已经加了多少,我们只需要开 \(16\) 个树状数组,如果查询第 \(T\) 位为 \(1\) 的个数,在第 \(T\) 个树状数组里面查一下就行。注意可能会有两个合法的区间。

树状数组 \(\iff\) 区间可加属性、前缀和,处理问题的时候往区间靠。

\(x\) 位为 \(1\iff 2^k\le x<2^{k+1}\)

HH 的项链

将所有询问离线,按右端点从小到大排序。

搞一个树状数组,从左到右扫,每扫到一个点处理右端点是这个点的询问。

当遍历到 \(a[i]\) 时,如果 \(a[i]\) 还没出现过,在位置 \(i\) 加一;如果 \(a[i]\) 出现过,在它上次出现的位置减一,在这个位置加一。

因为当询问排好序,两个相同的数只有更靠右的数字可能做贡献。所以每次在最新的位置加一,表示这里出现一个新数;同时把旧位置减一。

用0/1表示已删除/未删除,或者代表无贡献/有贡献,等等等等,这是常用手法。如果多个元素的贡献不会增加,不如把贡献记在方便的位置。

CF220B

这题感觉比 HH的项链 难(?

类似的思路,询问离线按右端点排序。考虑在从后往前第 \(x\)\(x\) 的位置上标 \(1\)。每新遍历到一个数就把原来的标记后移一个。

但是仅仅这样是错误的,因为区间内如果有大于 \(x\)\(x\),不应该计算贡献。所以我们还要在第 \(x+1\) 个位置标 \(-1\)

和上题类似。想清楚当区间变化的时候,哪些数不再有贡献,哪些数又有了新的贡献。

ZOJ3372

\(F\) 的转移方程可以写成矩阵,维护矩阵乘法。

HDU6155

序列递推转化为矩阵乘法。

\(dp[i][t]\) 表示前 \(i\) 个字符以 \(t=0/1\) 结尾的子序列个数。

\(s[i]=0\),则 \(dp[i][0]=dp[i-1][0]+dp[i-1][1]+1\)。这可以用矩阵乘法做。

翻转操作已经玩烂了。

SGU177

树套树,或者开 \(1000\) 个线段树。

跳跳棋

神仙建模题。

把三个棋子的位置 \((a,b,c)\) 构成一个结点。每个结点都可能变成三种结点:中间的往左右跳,和距离中间较近的点跳到中间的另一侧。

我们把中间的往左右跳形成的结点作为 \((a,b,c)\) 的左右儿子,距离中间较近的点跳到中间的另一侧形成的结点作为 \((a,b,c)\) 的父亲。

这样就形成了一颗二叉树森林,当 \(b-a=c-b\) 的时的 \((a,b,c)\) 就是一颗二叉树的根。

有解的条件等价于初始状态和目标状态在同一颗二叉树里。

我们可以写一个函数 \(getfa(x,k)\) 表示 \(x\)\(k\) 级祖先是谁。但是一次上一层太慢了,我们可以一次性跳到距离中间较近的点变化的时候,类似取模、辗转相除法。

状态抽象为结点。状态之间的转移抽象为边。问题抽象到图上。

Housewife Wind

有树。单点改边权,动态求两点距离。

受 LCA 转 RMQ 的启发,我们可以用欧拉序的方法,把树上问题改成序列的问题。

对树进行 DFS,当走过一条边(权为 \(w\)),如果是向叶子,在序列 \(a\) 末尾加入一个 \(w\);如果是向根,在序列 \(a\) 末尾加入一个 \(-w\)

发现当 \((u,v)\) 有祖先关系的时候,\((u,v)\) 的距离可以在线段树上区间求和。加一个求 LCA 就能求出任意点距离。

区间最大公约数

性质:区间内的 GCD 等于差分数组的 GCD 再和区间内第一个数求 GCD。即 \(gcd(a,b,c,d)=gcd(a,b-a,c-a,d-a)\)。特别注意要再和区间内第一个数求 GCD。(区间内第一个数可以用)

这样就从区间修改变成单点修改。

挖性质,区间 gcd 和差分数组的关系。

区间的东西不好搞,不如用差分转移到单点上。

P5227 及题解

在时间戳上建立线段树,每个叶子结点表示一个询问,把影响多个询问的操作记在懒标记里面。

Haybale Guessing

考虑二分答案,将信息按 \(x\) 从大到小排序后再处理,一次性处理所有 \(x\) 相等的询问。因为最小值更大,限制更强。

一个询问出现矛盾有两种情况:

  1. 有另外一个和它不相交的区间,最小值和它相等。因为题目说了数互不相同。

  2. 它被包含在最小值更大的一些区间内。

我们可以用并查集,每处理一个询问,就把之前询问涉及的所有位置都用并查集合并了。如果一个询问涉及的所有位置都已经在之前合并好了,说明这个询问被包含在更大的区间内,矛盾。

但是这个太慢了。

首先,有另外不相交的区间,就等价于 最大左端点 > 最小右端点。循环所有 \(x\) 相等的询问,找出最大左端点和最小右端点。

然后如果还能到这里,说明所有 \(x\) 相等的询问,区间都两两相交。而 \(x\) 必定存在于这个相交的区间。这个相交的区间左端点就是上面找出的最大左端点,右端点就是找出的最小右端点。

我们只需要判断最大左端点、最小右端点是否属于同一个集合,就能知道 \(x\) 存在的这个区间是否已经被包含。

但是还有一个问题:如果一个一个遍历合并,实在是太慢了。

我们可以参考这题:People are leaving,让每个连通块额外记录一个值 \(nxt\):表示这个连通块右边的、第一个还没有属于任何询问的位置。在合并的时候可以顺手维护。这样就不用一个一个遍历了,可以用 \(nxt\) 直接跳到对应的位置。

注意,用 \(nxt\) 跳不是 \(k=nxt[k]+1\),而是 \(k=nxt[fnd(k)]+1\)!!!

注意,在判断一种 \(x_i\) 的询问是否可行,还要注意这些询问的区间的并的长度,如果太长了可能比 \(x_i\) 大的数都不够填完整个区间。

并查集的经典用法,以最近的未被标记的位置为代表元素。

树上众数

最最最朴素的想法:搞一个数组记录颜色个数,每一个结点都遍历一遍它的所有子树,然后查找。

但是这要来回清空数组,太慢了。我们发现一个小小的观察:最后一颗子树不需要清空数组。 因为最后一颗子树之后就是父结点,刚好不用。

为了使最后一颗子树省的时间尽量多,于是我们把重儿子放到最后再搜索。

听起来优化不大,但是居然是 \(O(n\log n)\) 的!

考虑每一个节点对复杂度的贡献,就是它到根的路径上的轻边个数,而轻边个数不超过 \(\log n\) 条,因为每出现一条轻边,规模至少乘二。

所以总复杂度是 \(O(n\log n)\) 的。

太神奇了!

启发式合并,一般都是对重儿子/长儿子搞事情,少做一些操作,省一些复杂度。要有勇气,手推一下复杂度,说不定就变成 \(\log\)

括号序列

挖性质:除去最长合法括号序列后,剩下的一定是一堆右括号接着一堆左括号。

用线段树,每个结点记录它的最长合法括号序列长度、去掉最长序列后左边剩下的右括号数、右边剩下的左括号数。

显然是可以合并的。

GCD

用线段树维护区间最大 GCD,不用修改。(当然要修改可以参考 Acwing 246,上面也写了)

考虑一个区间的 \(gcd\) 能变成 \(x\),一定是把一个位置改成 \(x\)

把一个区间分成两半,如果两边的 \(gcd\) 都不是 \(x\) 的倍数,不可能;如果两边的 \(gcd\) 都是 \(x\) 的倍数,必可以;如果一边不是一边是,就看不是的那边能不能的 \(gcd\) 能不能变成 \(x\)

就这样不断递归下去,直到判断出结果。查询区间 \(gcd\) 是否为 \(x\) 的倍数用线段树。

这个算法是 \(O(q\log^3 n)\) 的,因为线段树一个 \(\log\)\(gcd\) 一个 \(\log\) 还有分治一个 \(\log\)

但是也有 \(O(q\log^2n)\) 的,也很简单:不是把区间分成两半,而是在线段树上把这个区间分成小区间。

一个区间的 gcd 为 x 的条件不好搞,可以分类讨论。讨论不出来递归丢给子问题搞。

Team-Building

注意题意,两组点形成的二分图不一定非要一组对应一边。

发现边的数量居然和点同阶,显然在边上面搞事。

首先处理两端都属于同一组的边,一组不是二分图的点显然也不能和另一组点构成二分图。

于是处理两端都属于同一组的边,如果该边连完之后发现这一组不是二分图,打个标记。

两端不属于同一组的边,我们只在所在组编号更小的端点存储这条边,这么做是因为选择两组只统计一次,我们只在编号小的位置统计选编号大的组一起。

然后从小到大枚举组。如果当前组自己不是二分图,跳过。

再将所有从该组出发的边排序,按照终点组编号从小到大排序。如果当前组的编号是 \(i\),排序后应该形如 \((i,i+1),(i,i+1),(i,i+2),(i,i+2),(i,i+2),\dots\)

然后找出所有两端所在组编号相同的边,例如找出所有连接了组 \(i,i+1\) 的边,把它们全部连上,看一下是否是二分图:如果是,答案加一;不是,答案不变。然后再全部撤销回来。用可撤销并查集。

最后输出即可。

注意,两端都属于同一组的边,在枚举组之前应该全部连上且以后都不会改动。

这题和Envy倒有点类似。

在容易处理询问的时候,把操作搞上,求出询问的答案。求完了再撤销。

New Year Tree

考虑新加入的两个结点会对原图产生什么影响。 显然产生影响只可能是它们之一变成了新直径的端点。于是我们直接倍增 LCA 查询新结点到原来直径的两个端点的距离,看一下是否比原直径更长。如果是,就用新结点代替一个端点。

Little Elephant and Tree

考虑父结点与子结点之间的关系。

在这题里面,发现父结点的操作一定会覆盖子结点,而子结点一定不会覆盖父结点。

这说明我们可以做一个类似于 “参数传递,自上而下” 的操作。

算法:每到一个结点 \(u\),把所有 \(a_i=u\)\(b_i=u\) 的操作执行了,看一下此时有多少个结点的集合与 \(u\) 有公共元素,最终就有多少个结点的集合与 \(u\) 有公共元素。求完了递归进入子树,回溯的时候把所有 \(a_i=u\)\(b_i=u\) 的操作都回溯回来。

这是因为只有 \(u\) 到根的路径上的点的操作会影响到 \(u\),且不同操作加入的数也不同,所以当我们处理到 \(u\) 时,就已经把 \(u\) 在所有操作之后的集合确定了,\(u\) 集合中所有数会在哪里出现也已经确定了。

而这个操作我们可以用线段树完成。

把子树的集合加入新元素,可以转化为线段树上对应区间全部加一,最终的答案就是线段树上有多少个位置大于零。(当然要减去 \(u\) 本身的一个 \(1\)

回溯的时候再把对应区间全部减一。

其实本质没变,就是父结点到子结点的变化好维护。以前是用一个参数/一个全局变量记录信息,现在是用一颗线段树记录信息。在做树形问题时,需要考虑是否可以自下而上、自上而下的传递信息。

这里其实和换根的感觉差不多。

Greedy Subsequences

一种经典模型构建方法。

构建一棵树,每个结点代表一个位置,父结点是该结点右边第一个比它大的位置。(没有设为 \(n+1\)

当区间的右端点右移一格,则以新右端点为根的子树中的位置作为起点的贪心子序列长度都加一。

当区间左端点右移一格,则以旧左端点为起点的贪心子序列长度变成 \(-\infty\)

用线段树,支持区间加、单点修改、区间查最大值。

Increasing Array

题意:每次操作可以把一个位置加一,询问把 \([l,r]\) 改成单调不减至少需要多少次操作。

贪心:每个数改成它左边第一个比它大的数。

询问离线,按照左端点从大到小排序,原数组从右到左扫一遍。

每遍历到一个数 \(a[i]\),二分查找它右边第一个比它大的数、区间赋值,然后此时处理所有左端点为 \(i\) 的询问。

处理询问,只需要用对应区间在线段树上的和减去在原数组上的和即可,因为线段树其实维护的是把 \([x,n]\) 改成单调不减后的序列。

线段树,需要支持:二分查找(需要维护区间max)、区间求和(维护区间和)、区间赋值。

疑问:为什么二分查找的时候也要pushdown?如果在 1~n 上查找,不应该每次都会查到第一个数吗?是否赋值应该无所谓啊?

解答:因为如果没有单独修改当前枚举到的位置,有可能这个位置的叶子结点为 \(0\),因为标记标到上面去了。

Copies

题意:初始有一个数组,需要支持三个操作:1. 将第 \(i\) 个数组的第 \(j\) 个元素修改为 \(x\);2. 新增一个数组,将第 \(i\) 个数组复制到这个新数组上(第 \(i\) 个数组不动);3. 查询第 \(i\) 个数组某一个区间的和。

我们建立一棵树,初始只有结点 \(1\)。另,每个结点都有一个代表数组,结点 \(1\) 的代表数组是初始数组。

然后定义一个指针数组 \(p\),初始 \(p[1]=1\)。这个数组的含义:\(p[i]=u\) 表示此时 \(u\) 所代表的数组就是第 \(i\) 个数组。

对于三种操作:

  1. \(i\) 个数组修改,新建一个结点,其父结点为 \(p[i]\)。然后令 \(p[i]\) 指向这个结点。

  2. 从第 \(i\) 个数组复制一个数组,则将 \(p[new]=p[i]\),即新数组的指针指向第 \(i\) 个数组的指针。

  3. 查询,取出 \(p[i]\) 指向的结点,它所代表的数组就是所查询的数组。

但其实我们可以把所有询问离线,因为无论何时,每个询问要取出的结点都是一定的。这样我们建完树,可以按照 dfs 序遍历整棵树,把修改信息记录在边上,用线段树在 dfs 过程中维护数组。

每遍历到一个点,就处理这个点上的所有询问。

Ping-pong 及其题解

Ensure(无题号)

题意:若干查询。每个问v的子孙中,深度>=d的节点有多少个?

可以树上启发式合并。

但还有另一种方法,更为简便:

把询问按 \(d\) 从大到小排序。然后把结点从深到浅遍历。

每遍历到一个结点,就把它在线段树上加一。(这颗线段树是建立在 dfn 上的)

每遍历完一个深度的所有结点,处理所有这个深度的询问,只需要 dfn 转区间,区间求和即可。

树的统计

题意: 为每个v,求v子孙中比自己编号小的个数。

和上题类似的方法,转 dfn 线段树。

No pain No game

题意:多个查询,查询某区间任意两个数 gcd 的最大值。

把所有问题离线,按右端点排序。按 \(1\sim n\) 枚举。

设当前枚举到 \(j\),考虑维护一个数组 \(s[]\)\(s[i]\) 表示 \([i,j]\) 的答案。有了这个数组,就可以在每个询问的右端点处回答这个询问。

看一下右边增加一个数后的情况:枚举这个数所有因数 \(d\),看一下 \(d\) 上一次出现是在什么位置。如果在位置 \(p\),则 \(s[1\sim p]\) 都要与 \(d\)\(\max\)

这已经可以用线段树做了。但我们还能用一些转化,来使得这个问题可以用 BIT 做。

考虑维护另一个数组 \(A[]\),其中 \(A[i]\) 表示 截至目前,a[i] 与 a[i] 右边的数的 gcd 的最大值

\(s[i]=\max(A[i],A[i+1],\dots,A[j])=\displaystyle \max_{t=i}^j(A[t])\)。而因为当枚举到 \(j\) 的时候,\(A[j+1]\sim A[n]\) 应该都为 \(0\)——因为截至目前没有已枚举的数在它们右边。

所以 \(\displaystyle\max_{t=i}^j(A[t])=\max_{t=i}^n(A[t])\),这是一个后缀求 \(\max\)

同时,我们注意到 \(A\) 这个数组每一个数只会增加不会减少。于是我们可以用树状数组维护一个翻转过来的 \(A\)。(翻转是因为上面是后缀求 \(\max\)

注意:用 BIT 维护前缀最大值,一个数组必须只增加不减少!不然自己手玩一个样例发现,BIT 在减少后找不到剩余数中的最大值。

Company

题意:给定一棵树和一堆询问,每个询问形如 (l,r),表示在编号 l,l+1,...,r 的点中任选 r-l 个点,使得这些点的 LCA 最深。

发现:如果选的点中 \(u\) 的 dfn 最小,\(v\) 的 dfn 最大,则整体的 LCA 就是 \(lca(u,v)\)

因此要么不选 dfn 最小的,要么不选 dfn 最大的。两种情况各自找剩下的 dfn 最小最大,用 ST 表求 LCA,两种情况比一下即可。

乡下

题意:给定一棵树,初始所有边权为 1,有n+m-1个操作,每次要么把一条边边权改成0,要么查询某个点到根的距离。

发现修改边权只会让一颗子树的答案减一。dfn 转线段树,区间修改 + 单点查询。

Boring Counting

题意:一棵树,每个点有颜色。求出每个子树中有多少种颜色出现个数恰好等于 \(k\)

法一:

对每个颜色的点取出来,建虚树。

然后用打标记的方法,在每个特殊点(枚举到的颜色的点)上标一个 \(1\),然后求每个结点子树和,如果子树和等于 \(k\),在原树的对应节点上把标记加一。

因为只有在特殊点的 LCA 上,颜色个数会变化,所以算法正确。

法二:

树上启发式合并。

法三:

转化成区间问题,查询每个区间中出现 \(k\) 次的数有几种。类似 HH 的项链。

Xors 方阵

用二维线段树常数大,会炸。但是二维 BIT 又难以支持区间修改。怎么办?

先考虑单点查询、区间修改,我们只需要把二维 BIT 改成 xor 意义上的异或即可。

而如果要支持区间查询,我们可以转化为四个左上角为 \((1,1)\) 的矩形一起异或。而一个左上角为 \((1,1)\) 的矩形的异或值,根据右下角 \((x,y)\) 两个坐标的奇偶性,可以分成四种情况,用四个二维 BIT 维护。(推推柿子,异或有消去律)

Lena and Queries

题目相当于 “动态维护若干个点,每次询问给定一个斜率,求这个方向上与这些点的凸包的切线”。

线段树分治。

求出每个点在哪些询问里存在,把所有点按照 \(x\) 从小到大排序,标到对应结点上。同时把所有询问按照 \(k\) 从小到大排序,然后依次把询问标记到所有包含这个询问的结点上。

每遍历到一个结点,这个结点上面标记的点和询问就都已经排好序了。

如果当前询问的 \(k\) 与这个 \((x,y)\) 的答案比下一个 \((x,y)\) 的答案大,则回答这个询问,答案为 \(kx+y\)\(x,y\) 都是这一个点的 \(x,y\),同时开始处理下一个询问;否则遍历下一个点。

Wide Swap

其他地方可以参照5+*大佬的题解

用逆排列转化成拓扑排序之后,我们发现边太多了,量级 \(O(n^2)\)。我们不能真的把边建出来。

法一:

于是我们使用 RMQ 来做:枚举 \(i:1\sim n\),看一下它前面的最小值 \(x\) 是否小于等于 \(i+k\)。如果最小值都大于 \(i+k\),则所有 \(i\) 的前驱都已经放好了,那么就把 \(i\) 放了,同时 \(a[i]\leftarrow +\infty\);否则递归把 \(x\) 放好,然后再查询最小值,再递归 …… 直到上一种情况。

法二:

考虑去除冗余边。对于 \(Q_i\),只需要保留 \(Q_i\) 右侧属于 \((Q_i,Q_i+k)\) 的最近的 \(Q_j\),以及左侧属于 \((Q_i-k,Q_i)\)\(Q_j\)

文艺平衡树

使用 FHQ_Treap。

平衡树上每个结点多记录一个属性 \(flag\),表示这个结点子树是否被翻转。如果 \(flag=true\),表示被翻转,遍历的时候要先遍历右子树再遍历左子树。

下传标记:把左右儿子的指针交换,然后让左右儿子的 \(flag\) 取反。

当有一个区间翻转,把平衡树按 \(sz\) 分裂成 \([1,l-1],[l,r],[r+1,n]\) 三棵平衡树,把 \([l,r]\)\(flag\) 取反,再 merge 回去。

记得哪里都要 pushdown。

列队

用一颗平衡树保存最右边的一列。

\(n\) 颗平衡树保存每一行的前 \(m-1\) 列。

每次有人离队,从 \(n\) 颗平衡树中找到这个人,删掉。然后把这一行在最右边一列对应的人加进来,再把这个人插入最右边一列的平衡树。

但是空间复杂度是 \(O(nm)\) 的会炸。

发现其实有很多人是连续的一段,从头到尾都没有分开过,于是每一行连续的编号我们可以存到一个节点上。要取出一个编号时,把这个结点分裂成三个结点:比此编号小的、此编号、比此编号大的,把这个编号单独取出来。

动态区间第k小

搞一颗线段树,每个结点的区间就代表数组的区间。

然后每个结点搞一颗 Treap,包含结点区间内所有数。

然后对于一次询问,二分答案 \(x\)。看一下在线段树对应区间内小于等于 \(x\) 的有多少个。(在每个结点的 Treap 上查然后加起来就行啦)与 \(k\) 比较,如果大于等于 \(k\),就让 \(r=mid\),否则 \(l=mid\)

对于一次修改,把这个单点位置代表的叶子结点,一直到根结点路径上的所有节点的 Treap,都修改一次。

一次询问的时间是 \(O(\log^3 n)\) 的,二分一个,线段树一个,Treap 一个。

一次修改的时间是 \(O(\log^2 n)\) 的,线段树一个,Treap 一个。

Sum of Medians

搞一颗 Treap,每个结点记录子树内所有数排好序后,模五余 0~4 的和。

因为 Treap 刚好就是按值从小到大排序的,所以是正确的。

pushup 就像 DP 一样更新,0 -> 1,1 -> 2 ……

\(tr[x]\) 的模五余 \(x\) 的,就等于左子树模五余 \(x\) 的,加上右子树模五余 \((x-tr[tr[x].left].sz-1)\!\!\mod 5\) 的。(注意不要漏了 \(tr[x]\) 也占一个位置)

最后要看一下 \(tr[x]\) 的位置模五余 \(tr[tr[x].left].sz+1\),把这个余数的答案加上 \(tr[x].val\)

郁闷的出纳员

搞一个变量 \(delta\),表示如果当前一个人的工资在平衡树里是 \(x\),则实际工资是 \(x+delta\)

  1. 加入新员工,插入一个 \(x-delta\)

  2. 加工资,\(delta\leftarrow delta+x\)

  3. 减工资,\(delta\leftarrow delta-x\)。这里发现如果使用 FHQ_Treap 可以很方便地把所有小于 \(\min\) 的直接分裂出来。

  4. 查询第 \(k\) 大。注意一定先判断当前人数是否够 \(k\),在维护过程中用一个变量记录人数即可。然后就查询第 \(k\) 大。

T-shirts

先把所有衣服按质量从大到小排序,相同则按价格从小到大排序。一件衣服看作一个操作:把所有余额 \(\ge\) 这件衣服的价格的顾客的余额都减去这件衣服的价格。

把所有顾客的初始余额从小到大排序。每次必然是一个后缀的顾客买了这件衣服。但是如果每次都重新排序,就太慢了。

设当前衣服的价格为 \(c\),把所有顾客分成三部分:余额 \(<c\) 的、余额 \(\in[c,2c)\) 的,余额 \(\ge 2c\) 的。

显然每次减少,余额 \(\ge 2c\) 的位置不会变,只有余额 \(\in[c,2c)\) 的人会变。

我们直接暴力枚举这部分的人,一个一个 \(-c\),插入到余额 \(<c\) 的部分。对于 \(\ge 2c\) 的,我们在对应结点上打标记,表示这个区间的人同一减少了 \(c\)

先把平衡树分裂,把这个变化区间的插好之后,再合并回来。

复杂度分析:当人的余额 \(\in[c,2c)\),买了衣服之后,余额至少减半。所以一个人至多落在变化区间内 \(\log 10^9\) 次,不会超时。

折纸

题意:每次把纸从左往右折,或者查询某段区间内的厚度之和。

用线段树维护数组,每个结点表示区间内的厚度之和。每次折纸就暴力把折的区间内每一个数累加到对面的区间内即可。

因为每个位置只会被折一次,所以总时间 \(O(n\log n)\)

排名系统

把每个结点的 val 值改成一个 pair,第一关键字分数,第二关键字达成分数的时间。

另外开两个 map 记录每个人目前的分数、时刻,和每对分数、时刻对应谁。

内存分配

把内存长度看作一条边,时间看成另一条边。任何时刻任何单元的存储情况能视为一个矩形。而一个进程需要占用的也是一个矩形,长为进程的时间,宽为进程的所需内存单元个数。

我们要快速查询有没有连续的一段空单元长度足够,可以用平衡树,类似列队,每行用一个平衡树,最后一列用一个平衡树,连续的一段空单元用一个结点存。

我们把某个进程开始和结束称为一个事件,把所有事件按发生时间排序(类似扫描线),对进程开始的事件,就是在对应行上(对应的开始时间上)找是否存在连续的一段空单元长度足够。

而进程结束的事件,我们还需要把相邻的连续空单元合并成一个结点。因为我们每个结点都代表一段连续空单元,左端点是有序的,所以在每个结点上记录它的子树内左右端点的最小/大值,就可以在树上二分查找到这一段空单元的前驱后继。令前驱、后继中与这一段空单元相邻的合并为一个结点。(删除两个,插入一个大的)

括号序列

(我写的是 BZOJ2209,不带覆盖的版本)

对于一个括号串,变成合法的操作数至少为 \(\lceil a/2\rceil+\lceil b/2\rceil\),其中 \(a\) 为未匹配的左括号数,\(b\) 为未匹配的右括号数。

平衡树上每个结点代表一个字符,每个结点记录:

  1. 是否覆盖标记。

  2. 是否取反标记。

  3. 是否翻转标记。

  4. 最小/大前/后缀,这是指把左括号看作 \(+1\),右括号看作 \(-1\) 的情况。

  5. 总和,也是左括号 \(+1\),右括号 \(-1\)

  6. 该结点代表的字符。

  7. 子树大小。

难点:写三个函数,用来对一个结点应用覆盖/取反/翻转标记,写一个 pushdown,把一个结点的所有标记下传给左右儿子,写一个 pushup,用左右儿子算自己。

CF809D及其题解

列车

一个区间长度如果 \(>d\),则列车至少会经过一次。注意多次经过一个区间也只会算一种商品,所以这种区间的贡献始终为 \(1\)

如果区间长度 \(\le d\),我们在左端点处 +1,右端点后一个处 -1, 搞差分,只有一个停靠的站点处于区间内,前缀和才会统计贡献。

所以枚举每一个停靠的站点,求每一个站点前缀和的和。这可以用树状数组做。

求完这个前缀和的和不要忘记加上长度 \(>d\) 的商品!

经典:左+1,右后-1,只有区间内才会统计贡献。

排序

给定一个序列,和 \(m\) 次区间排序操作。问最后第 \(p\) 个位置上的数是多少。\(n,m\le 10^5\)

考虑只有 01 两种值的情况,可以用查询个数+区间赋值实现排序。

我们可以二分最终在位置 \(p\) 的值,把所有 \(\le p\) 的都视作 \(0\),所有 \(>p\) 的视作 \(1\)。然后和 01 序列上一样做,只要最后第 \(p\) 个位置上是 \(1\) 即可。

树上数颜色

给定一棵树,带点权。每次将一个子树的颜色赋值,或者查询一个子树内有多少种不同的颜色。

模板题,dfn 序转区间操作。

字符串排序

给定一个只有小写字母的字符串,每次给定一个区间排序操作,问最后得到什么字符串。

和上面那题排序的 01序列 做法一样。

切玻璃

给定一块 \(n\times m\) 的矩形,每次会横着或者竖着切一刀。每次切完之后,要求出现在最大的一块矩形面积。

观察性质:就是横着的最长的边 \(\times\) 竖着的最长的边。

法一:平衡树(好想)

建两颗平衡树,一颗维护竖着的边,一颗维护横着的边。没有切开的一段作为一个结点。(类似列队)

法二:线段树(难想)

建两颗线段树,也是一横一竖,初始全部赋值为 \(0\)。每次切看作对一段区间取反。查询就是询问最长的连续 0/1 段长度。

移动

给定一个序列,每次操作会把一个数提到开头。对于每一个数,询问它在开始到结束中,曾经到达过的最前面的位置和最后面的位置。

在数组前面放 \(m\) 个虚位置,提到开头,可以视作放到一个虚位置上。

把没有数的位置赋值 \(0\),有数的位置赋值 \(1\),可以用 BIT 查前缀和,得到排名。

最前面的位置,要么是第一名,要么是初始位置;最后面的位置,一定是在某次操作前或者所有操作结束后诞生的。

区间

\(n\) 个知识点,两个讲师。一个老师的讲课范围是一个长度 \(k\) 的区间。有 \(m\) 个学生,每个学生只能听一个老师的课,且学生只听 \([l_i,r_i]\) 的知识点。将一个学生听到的知识点个数记为 \(a_i\)

任务:任意选定两个讲师讲课的区间,并任意制定每个听课的人听的讲师,使得 \(a_i\) 之和最大,输出这个最大值。

观察一下老师区间和学生区间的交的大小变化:0 -> 增大 -> 最大(持续)-> 变小 -> 0

同时,当老师区间和学生区间的中点重合的时候,一定是学生听到的最大的时候。

将所有学生按照其区间中点(左端点+右端点)排序,最优解一定是一个前缀的学生听一个老师的课,其余的听另一个老师的。

先枚举一个老师的区间,从前往后预处理;再枚举另一个老师的区间,从后往前预处理。\(O(nm).\)

再枚举是哪个前缀听第一个老师的课,\(O(m)\)

所以复杂度 \(O(m\log m+nm)\)

CF842D

初始有一些数,每次询问会给出一个数 \(x\),要回答所有数异或 \(x\) 后的 \(mex\)。(每次询问会让这些数永久变化)

考虑异或会用什么维护:01 Trie / 线性基。但是线性基这玩意不能修改,而且 mex 这玩意不好搞。

于是往 01 Trie 的方向考虑。异或 \(x\),若 \(x\)\(j\) 位是 \(1\),就是交换左右儿子。

那怎么查询 mex?可以初始在根结点,然后看一下 0 的那边是不是满的:如果不是满的,就去 0 那边;否则去 1 那边。

CF1862G

发现答案 = 将数组升序排列后的最大值 + 升序排列后相邻元素的差的最大值。

为什么?因为一次操作会让相邻两个元素的差 - 1,所以会执行(相邻元素差的最大值)这么多次操作。

可以用两个 multiset,一个维护数组,一个维护数组的相邻差。

ZOJ3772: Calculate the Function

线段树维护矩阵乘法。

HDU5068: 哈利和他的数学老师

题意:有 \(n\) 层楼,每层楼两个门。每个门初始都可以通向下一层的两个门。需要支持两种操作:

  1. 求从 \(x\) 层的任意一个门出发,到达 \(y\) 层的任意一个门的方案数。

  2. 使得 \(x\) 层的某一个门到达 \(x+1\) 层的某一个门的连通性取反。(若 \(a\) 门原来能到达 \(b\) 门,现在就不能到达了)

解法:把每一层到下一层的两个门相互的到达状态,构成一个 \(2\times 2\) 的矩阵。用线段树维护矩阵乘法就能求出 \(x\) 层的门到 \(y\) 层的门的方案数。

HDU6155: 01序列

给定一个 01 序列,要求支持下列操作:

  1. 将一个区间取反。

  2. 求出一个区间中有多少个不相同的子序列。

先考虑 dp 怎么做。

\(dp[i][0/1]:\)\(i\) 个字符中有多少个不相同的以 \(0/1\) 结尾的子序列。

\(dp[i][0]\) 分两类:一类是只有第 \(i\) 个字符(0)的,一类是前 \(i-1\) 个字符中任意一个不相同的子序列之后添一个 0 的。

所以若 \(ch_i=0\)\(dp[i][0]=dp[i-1][0]+dp[i-1][1]+1\)\(dp[i][1]=dp[i-1][1]\)。另一边是对称的。

可以用矩阵乘法线段树维护。

UVA1723: Intervals

经典区间选点的贪心。因为区间范围很小,可以用 BIT 维护每个前缀(区间)选了多少个点。

UVA1406: A Sequence of Numbers

这题因为 remote judge/UVA 炸掉了还没交。

思路:整体的加,可以直接用一个偏移量 \(sum\) 统计。

注意:与 \(2^T\) 按位与的结果非 \(0\),是表示二进制下 \(T+1\) 位非 \(0\)

每次查询,即求 \((a[?]+sum)\bmod 2^{16}\) 后二进制第 \(T+1\) 位非 \(0\)\(?\) 个数。

因为高位不会影响低位,所以等价于求 \((a[?]+sum)\bmod 2^{T+1}\) 后二进制第 \(T+1\) 位非 \(0\)\(?\) 个数。

进一步,即求 \(2^T\le a[?]+sum<2^{T+1}-1\)\(?\) 的个数。(因为提前对 \(2^{T+1}\) 取模了)

因为 \(a[?]\) 的范围只有 \(2^{16}\),所以可以直接开 BIT 区间求和。

CSES1749: List Removals

题意:每次从序列中删除一个数,需要回答所删除的数在当前序列中是第几个。

当前序列中的排名 = 原排名 - 前面删掉了多少个。

可以用 BIT 维护前缀删掉的个数。

动态图连通性(可离线)

题意:给定一张图和若干次操作。每次操作可能是加边、删边或者询问两点是否连通。

法一:线段树分治 + 可撤销并查集。

法二:LCT

把所有边按出现时间从小到大排序。给每条边定义一个权值等于它从图中删除的时间。然后我们不删边,只加边,并维护这张图的最大生成森林。

对于一个询问,先判断是否连通,再判断这条路径上的最小值是否晚于询问的时间即可。(其实需要维护的是最小值最大的生成树)

二分图

题意:给定 \(G\) 和每条边的出现时间和消失时间,判断每个时刻 \(G\) 是否是二分图。

虽然是线段树分治模板题,但我们可以用 LCT 解决。

类似上题,把边的权值定义为删除时间。

维护一颗森林 \(T\):是当前时刻 \(G\) 的最大生成森林。

另外维护一个边的集合 \(O\)(Odd),它用来判断 \(G\) 是否有奇环。

按照出现时间从小到大加边,设当前这条边是 \(e\),时刻为 \(t\)
如果 \(e\) 两端不连通,在 \(T\) 里面加这条边;否则找出对应环上权值最小的边 \(e\)
若这个环是奇环,把 \(e\) 加入 \(O\) 中(偶环啥也不干)。
最后,把 \(t\) 时刻消失的边都删掉。

做完这些之后,\(t\) 时刻 \(G\) 不是二分图 \(\iff\) 此时 \(O\) 无边。

星际航道(无题号)

题意:给定一个 \(n\times m\) 的网格图 \(G\),网格边有边权,给定初始边权。同时有 \(q\) 次修改,每次修改一条边的边权。每次修改后输出当前图的 MST 和。\(n\times m,q\le 10^5\)。强制在线。

当给的图是平面图的时候,可以考虑对偶图。而且这种强制在线动态图如果不利用性质很难做。

观察发现,如果原来网格图的最小生成树是 \(T\),则 \(G-T\) 就是 \(G\) 对偶图的最大生成树。

我们同时维护 \(G\) 的最小生成树 \(T\)\(G\) 对偶图的最大生成树 \(T'\)

考虑修改了一条边 \(e\in T\)。如果 \(e\) 变小了,不用管;否则相当于 \(T'\) 外有一条边变大了,那我们在 \(T'\) 上把 \(e\) 形成的环找到,然后取环上最小的边从 \(T'\) 里删除,然后加入 \(T\)

CF603E

题意:依次加入 \(m\) 条带权边。问每次加入后是否存在边集使每个点度数为奇数,如果有还要最小化边集中最大的边权。

先考虑静态解法。

观察到如果一个连通块点的数量是奇数,肯定无解,因为点的度数之和必定是偶数,但奇数*奇数还是奇数。

反之,如果规模是偶数,必定有解;找出一颗 dfs-tree 然后从下往上贪心选就行了。

那最小化最大边权怎么做?

只要把边权从小到大排序,依次加入并判断是否有奇数大小连通块。直到扫到第一个可行解了,这条边就是最优解。

那动态怎么做?

观察:我们只需要关心 MST。

用 LCT 维护 MST 森林,并且用大根堆维护所有在 MST 森林中的边。

每加入一条新的边,就不断尝试删除堆里最大的边,如果发现删了会出现奇数连通块,这条边边权就是这次的答案。

由于要维护子树连通块点的数量,所以要 LCT 虚子树维护子树信息。

CF1109F

给定 \(n\times m\) 的网格图,每个格子有权值。权值构成 \(1\sim nm\) 的排列。问有多少个区间 \([l,r]\) 满足所有权值在 \([l,r]\) 内的格子构成一颗树。

先考虑构成森林怎么做。

一个显而易见的观察是当 \(l\) 一定的时候,\(r\) 越大越不可行。

现在问题转化为:对于一个 \(l\),找一个最大的 \(r\) 使得 \([l,r]\) 无环。
可以双指针 + LCT 来做。

判森林很简单,接下来的问题就是对于区间 \([l,r]\) 是森林,找有多少个 \(r'\le r\) 使得 \([l,r']\) 是树。
这个是不能双指针的,因为森林删了结点可能是树,树删了结点也可能是森林。

观察:如果 \(m=n-1\),森林就是树。

现在又变成判断 \([l,r']\)\(|V|-|E|=1\) 了。记 \(cnt_x\)\([l,x]\)\(|V|-|E|\)

而观察到 \(cnt_x\) 始终 \(\ge 1\)。所以 \(1\) 也可以看作最小值。那就是问 \(cnt_{l\sim r}\) 的最小值是多少,有多少个最小值。

我们用线段树维护 \(cnt_x\)\(r\) 右移的时候只需要单点求一下 \(cnt_r\),非常简单;而 \(l\) 右移的时候需要循环 \(l\) 的四邻域,例如 \(l\)\(6,7,10\) 相邻,那么 \(cnt_{6,+\infty},cnt_{7,+\infty},cnt_{10,+\infty}\) 减一,最多做四次区间减法即可。

树点涂色

有一棵 \(n\) 个点的有根树,其中 \(1\) 号点是根节点。Bob 在每个点上涂了颜色,并且每个点上的颜色不同。
定义一条路径的权值是:这条路径上的点(包括起点和终点)共有多少种不同的颜色。

  • 1 x 表示把点 \(x\) 到根节点的路径上所有的点染上一种没有用过的新颜色。
  • 2 x y\(x\)\(y\) 的路径的权值。
  • 3 x 在以 \(x\) 为根的子树中选择一个点,使得这个点到根节点的路径权值最大,求最大权值。

我们要利用操作的性质。观察发现任何时刻,颜色相同的点构成一条链。这让我们联想到 LCT 的辅助树。

考虑让颜色相同的链作为一条重链。维护 \(num[x]\) 表示 \(x\) 到根的轻边条数 \(+1\),那 \(x\) 到根的权值就是 \(num[x]\)
如果有了 \(num[x]\),求路径权值可以类似 LCA 的方法,很简单;询问子树就 dfn 拍扁,区间求最值即可。

问题就在于怎么快速维护 \(num[x]\)。观察到操作 \(1\) 很像 access,联想一下。

posted @ 2024-02-06 08:54  FLY_lai  阅读(12)  评论(0编辑  收藏  举报