做题笔记 II

上一篇 已经咕咕了很长时间了,加上洛谷博客特别卡,所以新开一篇。

\(\texttt{Le0**an}\):我写了四篇做题笔记了!

\(\texttt{xl****13}\):我写了零篇做题笔记了!!!111


\(\texttt{2024.1.15}\)\(\color{red}20\) 题祭。

\(\texttt{2024.1.22}\)\(\color{red}30\) 题祭。

\(\texttt{2024.1.24}\)\(\color{red}40\) 题祭。

\(\texttt{2024.1.26}\)\(\color{red}50\) 题祭。

\(\texttt{2024.2.13}\)\(\color{red}75\) 题祭。

\(\texttt{2024.2.20}\)\(\color{red}100\) 题祭。

为了避免一篇内容过多导致卡顿,\(101 \sim \inf\) 的题目转移到了 做题笔记 III


\(1 \sim 25\)

高桥君 \(\color{blue}(1)\)

暑假学长讲课讲过怎么做,但是我现在才知道原题(雾)

直接下指标求和没什么好性质,考虑从 \((n,k) \to (n+1,k)\) 的变化量。

\[\begin{aligned}&\sum \limits_{i=0}^k \dbinom{n}{i}\\ =& \sum \limits_{i=0}^k \dbinom{n-1}{i} + \sum \limits_{i=0}^{k}\dbinom{n-1}{i-1}\\=& 2 \sum \limits_{i=0}^k \dbinom{n-1}{i} - \dbinom{n-1}{k}\end{aligned} \]

那么从 \((n+1,k) \to (n,k)\) 的变化也可以由上式得到。当然 \((n,k) \to (n,k+1)\)\((n,k) \to (n,k-1)\) 的变化量是好求的。

我们发现 \(n,k\) 单次加减 \(1\) 时,可以 \(\mathcal O(1)\) 求出答案的变化量,联想到莫队算法。注意这里 \(l,r\) 的移动与处理序列区间的莫队的移动方向相反。总时间复杂度 \(\mathcal O(n\sqrt n)\)评测记录

历史的研究 \(\color{blue}(2)\)

回滚莫队板子,时间复杂度 \(\mathcal O(n \sqrt n)\)

注意判断在不在一块里时,如果下标从 \(1\) 开始,是 \(\lfloor \dfrac{l-1}{B} \rfloor \ne \lfloor \dfrac{r-1}{B} \rfloor\)评测记录

小 Z 的袜子 \(\color{blue}(3)\)

莫队板子题 qwq。评测记录

Ann and Books \(\color{blue}(4)\)

前缀和转化后,原题等价于问 \(a_i - a_j = k\) 的数对 \((i,j)\) 个数。莫队 \(\mathcal O(n \sqrt n)\) 计算即可。评测记录

One Occurrence \(\color{blue}(5)\)

本来在练莫队,然后发现线段树做法很简单。

将询问挂在 \(r\) 处,用线段树维护 \(lst\),更新元素 \(a_i\) 时,将 \(lst_{a_i}\) 的值赋成 \(\text{inf}\),将 \(i\) 位置赋成 \(lst_{a_i}\)。查询时,若 \([l,r]\) 的最小值小于 \(l\) 则有解,输出最小值的方案即可。线段树轻松 \(\mathcal O(n \log n)\) 维护,吊打莫队!评测记录

Lost Root \(\color{blue}(6)\)

最简单的思路是,枚举 \(x\),随 \(60\)\((u,v)\),记 \(a_x\)\(x\)\((u,v)\) 上的次数,找到 \(a_x\) 最大的点为根。

很遗憾这过不去。经过计算,\(k=2\) 时,深度为 \(1,2\) 的点随的概率几乎相同,为 \(\dfrac{1}{2}\),而深度为 \(3\) 的点是 \(\dfrac{3}{8}\)。观察到这两个概率相差不是特别大,不太容易区分。以此为思路我进行了很多乱搞,均未能通过。

考虑问题的简单情形:已知一条过根的路径 \((u,v)\)。因为树满,所以链上的点数 \(\le 24\)。此时只需要不超过 \(24\) 次询问就可以得到 \((u,v)\) 的子路径 \((u',v')\) 的长度。设路径为 \(u,a_1,a_2,\ldots,a_k,v\),依次问过 \(a_1 \sim a_k\),找到一个长度恰为 \(u\) 的深度的点,这个点就是根。这一部分询问不超过 \(\log^2_kn\) 次。

考虑怎么找到过根路径 \((u,v)\)。仍然考虑简单情形:已知一个叶子 \(u\)。我们找到和 \(u\) 不在根的一个子树里的叶子 \(v\),显然路径长度为 \(2h-1\)\(h\) 为树高。叶子至少有 \(\dfrac{n}{2}\) 个,则至少存在 \(\dfrac{n}{4}\)\(v\),随机 \(20\) 次,随机不到的概率为 \(0.0031\),可以接受。

最后是如何找到叶子 \(u\)。考虑如何 check 一个点是否是叶子:随一个点 \(v\),如果 \(x \in [1,n]\)\(x \ne u\),但是 \((x,v)\) 的路径上有 \(u\),则 \(u\) 不是叶子。随机一个叶子,随到的概率至少为 \(\dfrac{1}{2}\),随机 \(20\) 次后概率可以接受。

那么我们总共在 \(40n + \log_k^2n\),实际上远远跑不满,可以通过。

Boring Queries \(\color{blue}(7)\)

区间 \(\operatorname{lcm}\),强制在线。

发现区间 \(\gcd\) 是好维护的,虽然 \(\operatorname{lcm}\) 同样有结合律,但是因为 \(\operatorname{lcm}\) 太大,需要高精度存储,复杂度爆炸。我们知道若干数字的 \(\operatorname{lcm}\) 等于质因子次幂的最大值,那么我们就有维护质因子次幂的思路。但是 \(n\) 个数字质因子数量是 \(\mathcal O(n \log n)\) 级别的,仍无法承受。

考虑直接维护答案。新加入一个质因子 \(p^k\) 时,找到前面第一个质因子 \(p^{l}(l > k)\)。我们发现对于 \(p^l \sim p^k\) 中间的 \(p\) 的最大次幂都会变成 \(k\)。维护指数的单调栈,\(l\) 容易找到。直接推平 \(l \sim k\) 中间的其他元素,给 \(p^k\) 的位置答案乘上 \(p^k\),我们维护的是从 \(x \to x - 1\) 的答案的增量。那么,记 \(sm\)\(p^l \sim p^k\)\(p\) 指数的和(不包括 \(k,l\)),那么应该有 \(sm \le k\)。然后,令 \(p^l\) 位置处的答案 \(ans \gets ans \times p^{sm} \times p^{-k}\)

每个质因子修改不会超过 \(n\) 次,时间复杂度 \(\mathcal O(n \log^2 n)\)评测记录

Tree Generator™ \(\color{blue}(8)\)

    • 结论 \(1\):令 \(f(l,r)\) 表示子串 \([l,r]\) 去除匹配的字符后的长度,树的直径等于 \(\max f(l,r)\)
    • 结论 \(2\):令左括号为 \(1\),右括号为 \(-1\)\(\max f(l,r) = \max (\sum \limits_{i=k+1}^ra_i-\sum \limits_{i=l}^ka_i)\)

感觉感性理解都是好证明的,严谨证明可以看题解。

线段树维护,每个点维护 \(sm,ans,lx,rx,ln,rn,la,ra,aa\) 表示和、答案、前缀最大、后缀最大、前缀最小、后缀最小、前缀答案、后缀答案、区间全选答案,转移是简单的:

sm[s] = sm[s*2] + sm[s*2+1],aa[s] = max(aa[s*2]+sm[s*2+1],aa[s*2+1]-sm[s*2]),
lx[s] = max(lx[s*2],sm[s*2] + lx[s*2+1]),rx[s] = max(rx[s*2+1],sm[s*2+1] + rx[s*2]),
ln[s] = min(ln[s*2],sm[s*2] + ln[s*2+1]),rn[s] = min(rn[s*2+1],sm[s*2+1] + rn[s*2]),
la[s] = max(max(la[s*2],la[s*2+1]-sm[s*2]),aa[s*2]+lx[s*2+1]),
ra[s] = max(max(ra[s*2+1],ra[s*2]+sm[s*2+1]),aa[s*2+1]-rn[s*2]);
t[s] = max(max(t[s*2],t[s*2+1]),max(ra[s*2]+lx[s*2+1],la[s*2+1]-rn[s*2]));

时间复杂度 \(\mathcal O(n \log n)\)评测记录

New Year Tree \(\color{blue}(9)\)

很神秘,为什么是蓝的。

一开始想动态开点线段树,仔细一想直接状压颜色求或和就做完了。子树转 dfn 也算经典操作了吧。评测记录

Martian Strings \(\color{blue}(10)\)

唉,别人 KMP 写 1k 就 AC,我 SA 写快 3k /kel /kel。

子串匹配用 SA 做是套路的:在 \(rk\) 数组上二分 \(\operatorname{LCP} \ge len\) 的极长区间。枚举断点,贪心的在第一个串的匹配区间里取位置最小的,第二个取位置最大的,两个 st 表即可。时间复杂度 \(\mathcal O(n \log^2 n)\),我是高贵的 \(\log^2\) SA 选手!评测记录

Forbidden Indices \(\color{blue}(11)\)

发现不太好处理结尾的出现次数,我们翻转字符串 \(s\) 后,并查集维护 \(\operatorname{LCP}\) 长度和合法字符串数量,合并时统计答案即可。并查集维护 \(\operatorname{LCP}\) 是 SA 的经典套路。时间复杂度 \(\mathcal O(n \log^2 n)\)评测记录

Tricky and Clever Password \(\color{blue}(12)\)

考虑串 \(b\) 是奇回文串,那我们不妨枚举 \(b\) 的回文中心。在这里我们贪心的让 \(|b|\) 最大,考虑反证:若 \(s_x=s_y,b = s[x+1:y-1]\),如果 \(x,y\) 不被 \(a,a'\) 包含,那么显然拓展更优;否则,将 \(x,y\)\(b\),令 \(a,a'\) 长度同时减少一位,答案也不变。

设回文中心是 \(i\),回文串是 \(s[l:r](i-l+1=r-i+1)\),我们希望找到最小的 \(i\),使得存在 \(1 \le j \le k < l\)\(s'[j:k] = s[i:n]\)。我们不妨对每一个 \(i\) 求出最小的 \(k\),显然这个值随着 \(i\) 的增加单调不增,查询时二分即可。

\(t = s + \texttt{\#} + s'\),对 \(t\) 建 SA,则以 \(i\) 为中心的回文半径等于 \(\operatorname{LCP}(i,pos_i)\)\(pos_i\) 表示 \(i\)\(s'\) 里的位置。当然我们也可以根据这个求出每个 \(i\)\(k\) 值,二分 \(rk\) 排名区间,得到区间内最小的 \(k\) 即可。

整个做法只需要 SA,时间复杂度 \(\mathcal O(n \log n)\)。我采用了 \(\log^2\) 的 SA 实现,获得了第二劣解评测记录

Ant colony \(\color{blue}(13)\)

10 月份梦熊模拟赛的题,今天意外找到原题。

场上过了,以为是绿题,结果是紫的 /jy /jy

正难则反,考虑满足条件的数字,等价于 \(\min\limits_{i=l}^r a_i = \gcd\limits_{i=l}^r a_i\),统计区间某元素的出现次数即可。可以使用 vector 加二分统计,时间复杂度 \(\mathcal O(n \log n \log V)\)评测记录

Assiut Chess \(\color{blue}(14)\)

如果国王不能往上走,我们直接一行一行扫下去即可。

否则,扫过每一行,如果国王向下,则我们向下;如果国王向上,则重新扫一遍这行。总移动次数 \(56 \times 2 \le 130\)评测记录

Please, another Queries on Array? \(\color{blue}(15)\)

做过 REQ 就应该会这题。线段树维护每个质因子的出现次数和区间乘积即可。发现 \(300\) 以内有 \(62\) 个质数,直接开 \(62\) 棵线段树会 MLE,状压即可。时间复杂度 \(\mathcal O(n \log^2 n)\)评测记录

DZY Loves Fibonacci Numbers \(\color{blue}(16)\)

数学题。令 \(F_i\) 表示广义斐波那契数列第 \(i\) 项,\(F_1 = a,F_2 = b,\sum \limits_{i=1}^n F_i = af_{n} + bf_{n+1} - b\)。然后线段树维护 \(a,b\) 即可。时间复杂度 \(\mathcal O(n \log n)\)评测记录

Smooth Sailing (Hard Version) \(\color{blue}(17)\)

还是有意思的吧。

这道题的关键是,把绕着连通块画环,转化成通过一条线奇数次。具体的,找到一个 \(\texttt{\#}\) 并向右画线,根据通过这条线的奇偶性,给每个点设状态 \((x,y,1)\)\((x,y,0)\),则一个环即为 \((x,y,0) \to (x,y,1)\) 的路径。我们对于不跨过线的边 \((i,j) \to (x,y)\),连双向边 \((i,j,k) \to (x,y,k)(0 \le k \le 1)\),否则连 \((i,j,k) \to (x,y,k \oplus 1)\)。问题转化成找到两点路径使得最小边最大,Kruskal 重构树即可。时间复杂度 \(\mathcal O(nm \log nm + q)\)评测记录

A Simple Task \(\color{blue}(18)\)

关键的性质是 \(|\Sigma| = 26\),启发我们维护区间字符的出现次数。排序操作可以线段树 \(\mathcal O(|\Sigma| \log n)\) 简单做。总时间复杂度 \(\mathcal O((n+m) |\Sigma| \log n)\)评测记录

TorCoder \(\color{blue}(19)\)

和上一道题基本一样,对每个字符分别维护线段树,贪心的推平即可。注意长度为奇数时,中间的字符时固定的。时间复杂度 \(\mathcal O((n+m) |\Sigma| \log n)\)评测记录

Periodic RMQ Problem \(\color{blue}(20)\)

考虑动态开点线段树。如果当前区间被推平过它的节点就会更新当前答案;否则,直接返回这个区间在原序列里的最小值。注意 push up,push down 时可能会有些小细节。查原序列 RMQ 使用 st 表,时间复杂度 \(\mathcal O((n+m) \log n)\)评测记录

Propagating tree \(\color{blue}(21)\)

发现题目说了一大堆,其实就是和 \(u\) 同奇偶 \(+v\),否则 \(-v\)。那么拍到 dfs 序上,分奇偶维护节点信息即可。线段树区间加单点查,复杂度 \(\mathcal O((n+m) \log n)\)评测记录

Multidimensional Queries \(\color{blue}(22)\)

观察到 \(k\) 很小,直接暴力枚举每一维坐标的大小关系,你发现如果枚举的和真实不符会让答案变小,但并不会让答案变大。我们直接查区间 \([l,r]\) 的 RMQ 即可。时间复杂度 \(\mathcal O(2^k(n+m) \log n)\)评测记录

Bajtman i Okrągły Robin \(\color{blue}(23)\)

经典二分图带权最大匹配模型,跑费用流即可。由于边数过多,需要线段树优化建图。但是好像直接连边也能过

通信 \(\color{blue}(24)\)

耶耶没看题解写出来一道黑题

考虑费用流模型。

  • 从源点向左侧每个点连边,一个点表示一个哨站 \(i\),流量为 \(1\),费用为 \(0\)

  • 从左边每个点 \(i\) 往右边每个点 \(j(j < i)\) 连边,流量为 \(1\),费用为 \(|a_i - a_j|\)

  • 从左边每个点往汇点连边,流量为 \(1\),费用为 \(w\)

  • 从右边每个点向汇点连边,流量为 \(1\),费用为 \(0\)

考虑建图的本质是二维偏序,采用主席树优化建图。维护两棵主席树,一棵维护 \(a_j < a_i\)\(j\),另一棵维护 \(a_j > a_i\)\(j\)。对于第一种情况,我们让叶子向汇点连费用为 \(-a_j\) 的边,左部点向某个子树内连费用为 \(a_i\) 的边,这样走到叶子的总费用恰为 \(a_i - a_j\),第二种情况同理。

边数是 \(\mathcal O(n \log n)\) 级别的,跑费用流即可。评测记录

Irrigation \(\color{blue}(25)\)

水题。观察到当每个城市都恰好进行了 \(n\) 轮比赛时,后面的答案可以直接根据 \(k \bmod m\) 的值算出。从小到大合并城市的比赛数量,问题等价于插入、查第 \(k\) 小,直接 pbds。时间复杂度 \(\mathcal O(n \log n)\)评测记录

\(26 \sim 50\)

Analysis of Pathes in Functional Graph \(\color{blue}(26)\)

倍增维护走 \(2^k\) 到的点、和、最小值,就做完了。时间复杂度 \(\mathcal O(n \log k)\)评测记录

Addition on Segments \(\color{blue}(27)\)

不妨枚举最大值的位置,\(i\) 是最大值的充分条件是所有区间都包含 \(i\)。bitset 优化背包,但是它很难撤销,那么直接线段树分治。时间复杂度 \(\mathcal O(\dfrac{nm \log n}{w})\)评测记录

Replace on Segment \(\color{blue}(28)\)

考虑 dp。设 \(f_{i,j,p}\) 表示把区间 \([i,j]\) 全部推平成 \(p\) 的最小代价,好像有简单的区间 dp:\(f_{i,j,p} = \min\{f_{i,k,p} + f_{k+1,j,p}\}\),但这是错的。考虑 \([1,4,2,4,3,4](x=5)\),将 \([1,6]\) 推平成 \(4\),如果像刚才那样转移答案是 \(3\),实际上先将 \([1,6]\) 推平成 \(5\),再推平成 \(4\) 答案是 \(2\)。这启示我们设状态 \(g_{i,j,p}\) 表示区间 \([i,j]\) 不包含 \(p\) 的最小操作次数。\(f_{i,j,p}\) 的转移途径是 \(g_{l,r,p}+1\),其中 \(l=i+[a_i=p],r = j-[a_j=p]\),要加减是因为如果不修改,边界会对 \(g\) 多贡献答案。考虑 \(g\) 的转移:\(g_{i,j,p} = \min\{\min\limits_{p' \ne p}\{f_{i,j,p'}\},\min\limits_{i\le k < j}\{g_{i,k,p}+g_{k+1,j,p}\}\}\),含义是 \(g_{i,j,p}\) 既可以通过区间直接推平为其他实现,也可以通过两边分别消除 \(p\) 实现(第一种转移不能忽略,因为分开推平可能会忽略跨过分界点的区间,例如 \([2,1,1,2](x=3)\) 这个序列,\(g_{1,4,2}\) 的值应该是 \(1\),但是只考虑第二种转移会得到 \(2\))。第一部分可以通过前后缀 \(\min\) 求得。总时间复杂度 \(\mathcal O(n^3x)\)评测记录

排序问题 \(\color{blue}(29)\)

由基础排列组合公式知,答案是 \(\dfrac{(n+m)!}{\prod\limits_{i=1}^kct_i!}\)\(ct_i\) 是数字 \(i\) 的出现次数。分子是定值,贪心的希望分母尽可能小。显然的,我们希望 \(ct_i\) 的值尽可能平均,即每次加出现次数最小的数字。由于 \(\mathcal O(Tm)\) 的时间复杂度不可接受,考虑优化过程,计算出把所有出现次数为 \(i\) 的数字都推到 \(i+1\) 的填的数字数量,找到第一个 \(m\)\(0\) 的值即可。时间复杂度 \(\mathcal O(m+n\log n)\)

k-Maximum Subsequence Sum \(\color{blue}(30)\)

模拟费用流经典题。

考虑费用流模型:

  • \(s\)\(1 \sim n + 1\) 连流量 \(1\),费用 \(0\) 的边。

  • \(i\)\(i + 1\) 连流量 \(1\),费用 \(a_i\) 的边。

  • 每次询问,\(l\sim r + 1\)\(t'\) 连流量 \(1\),费用 \(0\) 的边。

  • \(t'\)\(t\) 连流量 \(k\),费用 \(0\) 的边。

如果直接暴力最大费用最大流,复杂度高达 \(\mathcal O(n^2kq)\),显然不能通过,考虑模拟费用流的过程。

  • 寻找最长的增广路。这一步对应求 \([l,r]\) 当前的最大子段和 \([l',r']\)

  • \([l',r']\) 正向边流量 \(-1\),反向边流量 \(+1\)。这一步对应区间取反(反边的费用是 \(-a_i\))。

区间取反的一种套路是同时维护正的信息和负的信息,要取反时直接交换。本题维护两棵线段树即可。注意要记录最大子段和到底是哪一段,要记录方案。时间复杂度 \(\mathcal O((qk + n) \log n)\)评测记录

双倍经验是 P6821,因为开了 \(10^6\) 稍微有点卡常,加个 inline 就过了。

[POI2012] HUR \(\color{blue}(31)\)

有一个最简单的贪心:优先满足 \(b_i\) 较小的。它是对的,感性证明。

考虑用数据结构维护贪心:线段树维护 \(a\),每次对于 \(j\) 二分出最大的 \(k(k \le j)\) 满足 \(\sum \limits_{i=k}^j a_i \ge b_j\),并把 \([k+1,j]\) 推平为 \(0\),令位置 \(k\) 减少对应值。发现数据范围两只 \(\log\) 能过,直接二分即可。时间复杂度 \(\mathcal O(n \log^2 n)\),略微卡常,加 inline

代码
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 3e5+5;
using namespace std;
int T,n,p[N];ll a[N],b[N],t[N*4],re,tg[N*4];
inline void cg(int s,int l,int r,int k){t[s] = 1ll * (r - l + 1) * k,tg[s] = k;}
inline void pd(int s,int l,int r){
    int mid = (l + r) / 2;
    if(tg[s] != -1) cg(s*2,l,mid,tg[s]),cg(s*2+1,mid+1,r,tg[s]),tg[s] = -1;
}inline void upd(int s,int l,int r,int ql,int qr,int k){
    if(ql <= l && r <= qr) return cg(s,l,r,k);
    int mid = (l + r) / 2; pd(s,l,r);
    if(ql <= mid) upd(s*2,l,mid,ql,qr,k);
    if(qr > mid) upd(s*2+1,mid+1,r,ql,qr,k);
    t[s] = t[s*2] + t[s*2+1];
}inline ll qry(int s,int l,int r,int ql,int qr){
    if(ql <= l && r <= qr) return t[s];
    int mid = (l + r) / 2; pd(s,l,r); ll ans = 0;
    if(ql <= mid) ans += qry(s*2,l,mid,ql,qr);
    if(qr > mid) ans += qry(s*2+1,mid+1,r,ql,qr);
    return ans;
}void los(){
    cin >> n;vector<int> ans; memset(tg,-1,sizeof(tg));
    for(int i = 1;i <= n;i ++) cin >> a[i],upd(1,1,n,i,i,a[i]);
    for(int i = 1;i <= n;i ++) cin >> b[i],p[i] = i;
    sort(p+1,p+n+1,[&](int x,int y){return b[x] < b[y];});
    for(int i = 1;i <= n;i ++){
        int j = p[i],l = 1,r = j,res = -1; 
        while(l <= r){
            int mid = (l + r) / 2;
            if(qry(1,1,n,mid,j) >= b[j]) l = mid + 1,res = mid;
            else r = mid - 1;
        }if(res == -1) continue;
        ll sm = (res != j ? qry(1,1,n,res + 1,j) : 0);
        if(res != j) upd(1,1,n,res+1,j,0);
        upd(1,1,n,res,res,qry(1,1,n,res,res) - b[j] + sm),ans.push_back(j);
    }cout << ans.size() << "\n";
    for(int i : ans) cout << i << " ";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

[USACO12FEB] Cow Coupons G \(\color{blue}(32)\)

反悔贪心。考虑先购买 \(c_i\) 最小的 \(k\) 个奶牛,如果它们都买不了就可以直接退出了。虽然 \(c_i\) 是最小的,但是可能有优惠更多的方案。我们维护三个堆,表示 \(p_i,c_i,p_j-c_j\)。每一步有两个决策:买最小的 \(p_i\),或用优惠价买 \(c_i\) 并补上 \(p_j-c_j\) 的差价,\(j\) 用原价买。贪心的取较小代价的。注意一个奶牛只能买一次,记得去重。时间复杂度 \(\mathcal O(n \log n)\)

代码
/**
*    author: sunkuangzheng
*    created: 22.01.2024 11:12:49
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
#define int long long
int T,n,m,p[N],c[N],vis[N],k,ans; // q1 原价,q2 优惠价,q3 差价
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> q1,q2,q3;
void los(){
    cin >> n >> k >> m;
    for(int i = 1;i <= n;i ++) cin >> p[i] >> c[i],q1.emplace(p[i],i),q2.emplace(c[i],i);
    for(int i = 1;i <= k;i ++){
        auto [ci,j] = q2.top(); q2.pop();
        if(m >= ci) m -= ci,vis[j] = 1,q3.emplace(p[j] - c[j],j),ans ++;
        else return cout << i - 1,void();
    }while(q1.size()){
        while(vis[q1.top().second]) q1.pop();
        while(vis[q2.top().second]) q2.pop();
        auto [p1,i1] = q1.top(); auto [p2,i2] = q2.top(); auto [p3,i3] = q3.top();
        if(p1 < p2 + p3){
            if(m < p1) return cout << ans,void();
            m -= p1,vis[i1] = 1,q1.pop(),ans ++;
        }else{
            if(m < p2 + p3) return cout << ans,void();
            m -= p2 + p3,vis[i2] = 1,q2.pop(),q3.pop(),q3.emplace(p[i2] - c[i2],i2),ans ++;
        }if(ans >= n) break;
    }cout << ans;
}signed main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

[PA2013] Raper \(\color{blue}(33)\)

线段树模拟费用流。为啥不把 \(\log^2\) 卡掉啊

考虑费用流建模:

  • \(s\)\(1 \sim n\) 连流量 \(1\),费用 \(a_i\) 的边。

  • \(i\)\(i+1\) 连流量 \(\inf\),费用 \(0\) 的边。

  • \(i\)\(t'\) 连流量 \(1\),费用 \(b_i\) 的边。

  • \(t'\)\(t\) 连流量 \(k\),费用 \(0\) 的边。

最小费用最大流即为答案,时间复杂度 \(\mathcal O(n^2k)\)

考虑模拟费用流的过程,核心在于如何处理反向边的流量 \(+1\)

有一个巧妙的思路,我们把每次寻找增广路,转化为每次放置一对括号 \(\texttt{()}\) 表示走正向边,或 \(\texttt{)(}\) 表示走反向边,并使放置完后仍然是合法括号序列,在此基础上代价最小。合法括号序列要求前缀和非负,而这恰好满足了反向边流量大小的限制。

考虑如何用线段树维护这么一个过程。如果放置 \(\texttt{()}\) 则没有额外要求,否则要求设右括号放置在 \(i\),左括号设置在 \(j(i < j)\),则需要满足 \([i,j)\) 区间内前缀和严格大于 \(0\)。但是涉及到前缀和加减,我们不容易维护区间 \(0\) 的数量,但我们知道区间的 \(\min\) 一定等于 \(0\)(合法括号序列左右括号数量相等),因此我们只需要维护区间 \(\min\)

线段树节点 \([l,r]\) 维护 \(mx,my,lx,ly,mn,ans_1,ans_2,fans\) 分别表示 \(a,b\) 区间最小值,\([l,lx),[ly,r]\) 满足 \(\min\) 大于 \([l,r]\)\(\min\)\(a,b\) 区间最小值,\(mn\) 是区间最小值,\(ans_1\)\(i \le j\) 时点的编号,\(ans_2\)\(i > j\) 且合法的点的编号,\(fans\)\(i > j\) 但不一定合法的点的编号。

转移非常精神污染。\(mx,my,mn,ans_1,fans\) 可以直接取 \(\min\),其余变量我们分类讨论左右区间 \(mn\) 的大小关系。

  • 左区间 \(mn\) 严格大于右区间 \(mn\)

此时左区间内任意 \(i,j(i > j)\) 都满足条件。\(ans_2\) 要考虑左区间的 \(fans\) 和左区间的 \(my\)、右区间的 \(lx\)\(lx\) 的转移讨论左区间 \(mx\) 和右区间 \(lx\) 的大小关系可以得到。

  • 右区间 \(mn\) 严格大于左区间 \(mn\)

和上面情况同理。

  • 左区间 \(mn\) 等于右区间 \(mn\)

此时不能使用 \(fans\),只能考虑右区间 \(lx\) 和左区间 \(ly\)\(lx,ly\) 直接继承即可。

总时间复杂度 \(\mathcal O(k \log n)\),但是常数巨大,好像和两只 \(\log\) 速度差不多

代码很恶心,谨慎点开。

代码
/**
*    author: sunkuangzheng
*    created: 22.01.2024 20:17:31
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,a[N],b[N],tg[N*4],k;
struct par{int x,y;}; ll ans;
bool operator <(par x,par y){return a[x.x] + b[x.y] < a[y.x] + b[y.y];}
struct seg{
    int mx,my,lx,ly,mn; par ans1,ans2,fans;
}t[N*4];
seg mg(seg x,seg y){
    seg c; c.mn = min(x.mn,y.mn),
    (a[x.mx] < a[y.mx] ? c.mx = x.mx : c.mx = y.mx),
    (b[x.my] < b[y.my] ? c.my = x.my : c.my = y.my),
    c.fans = min(min(x.fans,y.fans),{y.mx,x.my}),c.ans2 = min(x.ans2,y.ans2),
    c.ans1 = min(min(x.ans1,y.ans1),{x.mx,y.my});
    if(x.mn > y.mn)
        c.ans2 = min(min(c.ans2,x.fans),{y.lx,x.my}),
        c.lx = (a[x.mx] < a[y.lx] ? x.mx : y.lx),c.ly = y.ly;
    else if(y.mn > x.mn)
        c.ans2 = min(min(c.ans2,y.fans),{y.mx,x.ly}),
        c.lx = x.lx,c.ly = (b[y.my] < b[x.ly] ? y.my : x.ly);
    else
        c.ans2 = min(c.ans2,{y.lx,x.ly}),c.lx = x.lx,c.ly = y.ly;
    return c;
}void cg(int s,int k){tg[s] += k,t[s].mn += k;}
void pd(int s){cg(s*2,tg[s]),cg(s*2+1,tg[s]),tg[s] = 0;}
void pp(int s){t[s] = mg(t[s*2],t[s*2+1]);}
void build(int s,int l,int r){
    if(l == r) return t[s] = {l,l,l,0,0,{l,l},{0,0},{l,l}},void();
    int mid = (l + r) / 2; pd(s);
    build(s*2,l,mid),build(s*2+1,mid+1,r),pp(s);
}void upd1(int s,int l,int r,int x){
    if(l == r) return;
    int mid = (l + r) / 2; pd(s);
    if(x <= mid) upd1(s*2,l,mid,x); else upd1(s*2+1,mid+1,r,x);
    pp(s);
}void upd2(int s,int l,int r,int ql,int qr,int k){
    if(ql > qr) return ;
    if(ql <= l && r <= qr) return cg(s,k);
    int mid = (l + r) / 2; pd(s);
    if(ql <= mid) upd2(s*2,l,mid,ql,qr,k); if(qr > mid) upd2(s*2+1,mid+1,r,ql,qr,k);
    pp(s);
}void los(){
    cin >> n >> k,a[0] = b[0] = 1e9;
    for(int i = 1;i <= n;i ++) cin >> a[i];
    for(int i = 1;i <= n;i ++) cin >> b[i];
    build(1,1,n);
    while(k --){
        auto [x,y] = t[1].ans1; auto [p,q] = t[1].ans2; int d = 1;
        if(t[1].ans2 < t[1].ans1) x = p,y = q,d = -1;
        ans += a[x] + b[y],a[x] = b[y] = 1e9,upd1(1,1,n,x),upd1(1,1,n,y),
        upd2(1,1,n,min(x,y),max(x,y) - 1,d);
    }cout << ans;
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

三倍经验 CF802NCF802O,前者直接费用流能过,后面也需要模拟费用流,但是不知道为啥这个是黑那个是紫。

Buy Low Sell High \(\color{blue}(34)\)

费用流建模,跑最大费用最大流。

和上面那个题一样,把 \(\min\) 改成 \(\max\) 就好,时间复杂度 \(\mathcal O(n \log n)\)

代码
/**
*    author: sunkuangzheng
*    created: 22.01.2024 20:17:31
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,a[N],b[N],tg[N*4],k;
struct par{int x,y;}; ll ans;
bool operator >(par x,par y){return a[x.x] + b[x.y] > a[y.x] + b[y.y];}
bool operator <(par x,par y){return a[x.x] + b[x.y] < a[y.x] + b[y.y];}
struct seg{
    int mx,my,lx,ly,mn; par ans1,ans2,fans;
}t[N*4];
seg mg(seg x,seg y){
    seg c; c.mn = min(x.mn,y.mn),
    (a[x.mx] > a[y.mx] ? c.mx = x.mx : c.mx = y.mx),
    (b[x.my] > b[y.my] ? c.my = x.my : c.my = y.my),
    c.fans = max(max(x.fans,y.fans),{y.mx,x.my}),c.ans2 = max(x.ans2,y.ans2),
    c.ans1 = max(max(x.ans1,y.ans1),{x.mx,y.my});
    if(x.mn > y.mn)
        c.ans2 = max(max(c.ans2,x.fans),{y.lx,x.my}),
        c.lx = (a[x.mx] > a[y.lx] ? x.mx : y.lx),c.ly = y.ly;
    else if(y.mn > x.mn)
        c.ans2 = max(max(c.ans2,y.fans),{y.mx,x.ly}),
        c.lx = x.lx,c.ly = (b[y.my] > b[x.ly] ? y.my : x.ly);
    else
        c.ans2 = max(c.ans2,{y.lx,x.ly}),c.lx = x.lx,c.ly = y.ly;
    return c;
}void cg(int s,int k){tg[s] += k,t[s].mn += k;}
void pd(int s){cg(s*2,tg[s]),cg(s*2+1,tg[s]),tg[s] = 0;}
void pp(int s){t[s] = mg(t[s*2],t[s*2+1]);}
void build(int s,int l,int r){
    if(l == r) return t[s] = {l,l,l,0,0,{l,l},{0,0},{l,l}},void();
    int mid = (l + r) / 2; pd(s);
    build(s*2,l,mid),build(s*2+1,mid+1,r),pp(s);
}void upd1(int s,int l,int r,int x){
    if(l == r) return;
    int mid = (l + r) / 2; pd(s);
    if(x <= mid) upd1(s*2,l,mid,x); else upd1(s*2+1,mid+1,r,x);
    pp(s);
}void upd2(int s,int l,int r,int ql,int qr,int k){
    if(ql > qr) return ;
    if(ql <= l && r <= qr) return cg(s,k);
    int mid = (l + r) / 2; pd(s);
    if(ql <= mid) upd2(s*2,l,mid,ql,qr,k); if(qr > mid) upd2(s*2+1,mid+1,r,ql,qr,k);
    pp(s);
}void los(){
    cin >> n,a[0] = b[0] = -1e9,k = n;
    for(int i = 1;i <= n;i ++) cin >> b[i],a[i] = -b[i];
    build(1,1,n); ll res = 0;
    while(k --){
        auto [x,y] = t[1].ans1; auto [p,q] = t[1].ans2; int d = 1;
        if(t[1].ans2 > t[1].ans1) x = p,y = q,d = -1;
        ans += a[x] + b[y],a[x] = b[y] = -1e9,upd1(1,1,n,x),upd1(1,1,n,y),res = max(res,ans),
        upd2(1,1,n,min(x,y),max(x,y) - 1,d);
    }cout << res;
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

当然这个费用流有更简单的模拟方法,考虑每一个位置 \(i\) 的决策只有两种:之前从 \(j\) 流到汇点的流量改流到 \(i\),或源点从 \(j\)\(i\) 流入汇点。可以用小根堆维护,复杂度一样但是常数更加优秀。

事实上上面那道题如果没有 \(k\) 的限制,也可以使用堆的简单做法。

Olympiad in Programming and Sports \(\color{blue}(35)\)

二分图带权最大匹配模型,费用流求解,时间复杂度 \(\mathcal O(n^3)\),但是能过()。

代码
/**
*    author: sunkuangzheng
*    created: 23.01.2024 08:16:33
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,a,b,head[N],cnt = 1,dis[N],ax[N],ay[N],vis[N],s,t,x,y,cost,cur[N];
struct edge{int to,nxt,w,c;}e[N];
void adde(int u,int v,int w,int c){e[++cnt] = {v,head[u],w,c},head[u] = cnt;}
void add(int u,int v,int w,int c){adde(u,v,w,c),adde(v,u,0,-c);}
bool spfa(){
    for(int i = s;i <= t;i ++) dis[i] = -1e9,vis[i] = 0; dis[s] = 0;
    queue<int> q; q.push(s);
    while(q.size()){
        int u = q.front(); q.pop(),vis[u] = 0;
        for(int i = head[u],v;v = e[i].to,i;i = e[i].nxt)
            if(e[i].w && e[i].c + dis[u] > dis[v]) 
                if(dis[v] = dis[u] + e[i].c,!vis[v]) vis[v] = 1,q.emplace(v);
    }return dis[t] != -1e9;
}int dfs(int u,int f){
    if(u == t) return f; int a = 0; vis[u] = 1;
    for(int &i = cur[u],v;v = e[i].to,i;i = e[i].nxt)
        if(e[i].w && dis[v] == dis[u] + e[i].c && !vis[v])
            if(int w = dfs(v,min(e[i].w,f - a));e[i].w -= w,e[i^1].w += w,cost += e[i].c * w,a += w,a == f) return a;
    if(!a) dis[u] = -1e9; return vis[u] = 0,a;
}int dinic(){
    for(;spfa();dfs(s,1e9)) for(int i = s;i <= t;i ++) cur[i] = head[i];
    return cost;
}void los(){
    cin >> n >> a >> b,s = 0,t = n + 3; vector<int> acc[3];
    for(int i = 1;i <= n;i ++) cin >> ax[i];
    for(int i = 1;i <= n;i ++) cin >> ay[i],add(s,i,1,0),add(i,n+1,1,ax[i]),add(i,n+2,1,ay[i]);
    add(n+1,t,a,0),add(n+2,t,b,0),cout << dinic() << "\n";
    for(int u = 1;u <= n;u ++) for(int i = head[u],v;v = e[i].to,i;i = e[i].nxt)
        if(!e[i].w && v >= n + 1 && v <= n + 2) acc[v - n].push_back(u);
    for(int i = 1;i <= 2;i ++,cout << "\n") for(int j : acc[i]) cout << j << " "; 
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

但是这个东西的复杂度也太不牛了!考虑用 \(\color{blue}(32)\) 的套路,维护三个堆 \(a_i,b_i,b_i-a_i\),先选完前 \(s\) 大的 \(a\) 后每次决策可以选最大的 \(b\),或者选 \(a\) 并失去差价。复杂度 \(\mathcal O(n \log n)\),遥遥领先!

代码
/**
*    author: sunkuangzheng
*    created: 23.01.2024 09:07:38
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;// q1 a q2 b q3 a - b
int T,n,x,y,a[N],b[N],p[N],vis[N],ct;priority_queue<pair<int,int>> q1,q2,q3;
void los(){
    cin >> n >> x >> y; int ans = 0;
    for(int i = 1;i <= n;i ++) cin >> a[i],q1.emplace(a[i],i);
    for(int i = 1;i <= n;i ++) cin >> b[i],p[i] = i,q2.emplace(b[i],i);
    sort(p+1,p+n+1,[&](int x,int y){return a[x] > a[y];});
    for(int i = 1;i <= x;i ++) vis[p[i]] = 1,ans += a[p[i]],q3.emplace(b[p[i]] - a[p[i]],p[i]);
    while(q1.size() && (++ct) <= y){
        while(vis[q1.top().second]) q1.pop();
        while(vis[q2.top().second]) q2.pop();
        auto [p1,i1] = q1.top(); auto [p2,i2] = q2.top(); auto [p3,i3] = q3.top();
        if(p2 > p1 + p3) vis[i2] = 2,ans += p2,q2.pop();
        else
            vis[i1] = 1,vis[i3] = 2,ans += p1 + p3,q1.pop(),q3.pop(),q3.emplace(b[i1] - a[i1],i1);
    }cout << ans << "\n";
    for(int i = 1;i <= n;i ++) if(vis[i] == 1) cout << i << " "; cout << "\n";
    for(int i = 1;i <= n;i ++) if(vis[i] == 2) cout << i << " "; cout << "\n";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

[NEERC2016] Mole Tunnels \(\color{blue}(36)\)

模拟费用流。

考虑费用流建模:

  • \(s\) 向有鼠的点连流量 \(1\),费用 \(0\) 的边。

  • 对于树边 \((u,v)\),连流量 \(\inf\),费用 \(1\) 的双向边。

  • 每个点向 \(t\) 连流量 \(c_i\),费用 \(0\) 的边。

求最小费用最大流即为答案。如果暴力流 \(m\) 次,时间复杂度 \(\mathcal O(n^2m^2)\),显然过不了。

考虑每一次加入点 \(u\) 后增广,都需要找一条 \(u \to v \to t\) 的路径并且费用最小。假设有一条 \(u \to fa_u\) 的边,被沿着 \(u \to fa_u\) 的方向经过了 \(x\) 次,反方向经过了 \(y\) 次,则当 \(y > x\) 时有 \(u \to fa_u\) 的反向边 \(fa_u \to u\) 费用为 \(-1\) 流量为正。也就是说,如果 \(y > x\),则 \(u \to fa_u\) 的代价是 \(-1\),否则是 \(1\)\(fa_u \to u\) 同理。

维护 \(f_u\) 表示距离 \(u\) 最近的 \(c_v\) 严格大于 \(0\)\(v\) 的距离,则我们要在 \(u \to 1\) 的路径上找到 \(d\),满足 \(\operatorname{dis}(u,d) + f_d\) 的值最小。由于树高是 \(\mathcal O(\log n)\) 级别的,我们可以暴力枚举 \(d\) 计算。然后,更新 \(u \to 1,v \to 1\) 路径上的边权和 \(f\) 的值。

如何计算 \(\operatorname{dis}\) 和更新 \(f\)?我们只需要对于每个点,维护 \(u \to fa_u\) 的边的代价和 \(fa_u \to u\) 的边的代价,暴力往上累加即可。时刻留意树高是 \(\mathcal O(\log n)\) 级别的这一重要性质。

总时间复杂度 \(\mathcal O(m \log n)\),代码相对好写。

代码
/**
*    author: sunkuangzheng
*    created: 23.01.2024 11:03:02
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,m,a[N],u,du[N],dd[N],f[N],g[N],ct[N],ans;
void upd(int u){
    int l = u * 2,r = u * 2 + 1,dl = f[l] + dd[l],dr = f[r] + dd[r];
    if(a[u] && dl > 0 && dr > 0) f[u] = 0,g[u] = u;
    else if(dl < dr) f[u] = dl,g[u] = g[l];
    else f[u] = dr,g[u] = g[r];
    du[u] = (ct[u] >= 0 ? 1 : -1),dd[u] = (ct[u] <= 0 ? 1 : -1); 
}void los(){
    cin >> n >> m,memset(f,0x3f,sizeof(f));
    for(int i = 1;i <= n;i ++) cin >> a[i];
    for(int i = n;i >= 1;i --) dd[i] = du[i] = 1,upd(i);
    for(int i = 1;i <= m;i ++){
        cin >> u; int dis = 1e9,di = -1,sm = 0;
        for(int v = u;v;sm += du[v],v /= 2) if(dis > f[v] + sm) di = v,dis = f[v] + sm;
        cout << (ans += dis) << " "; 
        int v = g[di];a[v] --;
        for(;u != di;u /= 2) ct[u] ++,upd(u);
        for(;v != di;v /= 2) ct[v] --,upd(v);
        for(;di;di /= 2) upd(di);
    }
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

王者之剑 \(\color{blue}(37)\)

黑白染色,求的即是二分图最大带权独立集,可以转化为最小割,双倍经验 方格取数问题

代码
//created on 2023.5.19
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int inf = 2e9; 
struct edge{
    int to,nxt,w;
}e[5000005];int cnt = 1,head[50005],dep[50005],ans,c[505][505],st,n,m,sum;
int a(int i,int j){return (i-1) * m + j;}
int dx[5] = {0,1,0,-1,0},dy[5] = {0,0,1,0,-1};
void add(int u,int v,int w){
    e[++cnt].to = v;
    e[cnt].nxt = head[u];
    e[cnt].w = w;
    head[u] = cnt;
}
bool bfs(){
    queue <int> q;
    memset(dep,-1,sizeof(dep));
    q.push(0);dep[0] = 0;
    while(!q.empty()){
        int u = q.front();q.pop();
        for(int i = head[u];i;i = e[i].nxt){
            int v = e[i].to;
            if(e[i].w && dep[v] == -1){
                dep[v] = dep[u] + 1;
                q.push(v);
            }
        }
    }
    if(dep[st] == -1) return 0;
    return 1;
}
int dfs(int x,int f){
    if(x == st) return f;
    int w,used = 0;
    for(int i = head[x];i;i = e[i].nxt){
        int v = e[i].to;
        if(e[i].w && dep[v] == dep[x] + 1){
            w = dfs(v,min(f-used,e[i].w));
            used += w;e[i^1].w += w;e[i].w -= w;if(used == f) return f;
        }
    }
    if(!used) dep[x] = -1;
    return used;
}
void dinic(){
    while(bfs()) ans += dfs(0,inf);
}
signed main(){
    cin >> n >> m;
    for(int i = 1;i <= n;i ++) for(int j = 1;j <= m;j ++) cin >> c[i][j],sum += c[i][j];
    st = n*m + 1;
    for(int i = 1;i <= n;i ++){
        for(int j = 1;j <= m;j ++){
            if((i + j) % 2 == 1) add(a(i,j),st,c[i][j]),add(st,a(i,j),0);
            else{
                add(0,a(i,j),c[i][j]);add(a(i,j),0,0);
                for(int k = 1;k <= 4;k ++){
                    int ax = i + dx[k],ay = j + dy[k];
                    if(ax >= 1 && ax <= n && ay >= 1 && ay <= m){ 
                        add(a(i,j),a(ax,ay),inf);
                        add(a(ax,ay),a(i,j),0);
                    }
                }
            }
        }
    }
    dinic();
    cout << sum - ans;
    return 0;
}

P4681 [THUSC2015] 平方运算 \(\color{blue}(38)\)

最重要的一点是观察到平方在模意义下有循环节,在本题模数下所有循环节 \(\operatorname{lcm}\) 不超过 \(60\)。然后拿线段树维护即可。

思路很简单,写起来还是有相当多细节的,怎么 push_up 得想清楚。

代码
/**
 *    author: sunkuangzheng
 *    created: 24.01.2024 07:36:38
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 1e5+5;
using namespace std;
int T,n,m,p,a[N],d[N],vis[N*4],siz[N*4],t[N*4][65],tg[N*4],gt[N*4],op,l,r;
int pw(int x){return x * x % p;}
void cg(int s,int k){tg[s] = (tg[s] + k) % siz[s],gt[s] += k;}
void init(int s,int x){
    if(d[x]){
        t[s][0] = x,vis[s] = 1;
        for(int i = 1;i <= 60;i ++)
            if(t[s][i] = pw(t[s][i - 1]),t[s][i] == t[s][0]) return siz[s] = i,void();
        assert(0);
    }t[s][0] = x;
}void pp(int s){
    vis[s] = vis[s * 2] && vis[s * 2 + 1];
    if(!vis[s]) return t[s][0] = t[s*2][tg[s*2]] + t[s*2+1][tg[s*2+1]],void();
    siz[s] = lcm(siz[s*2],siz[s*2+1]);
    for(int i = 0;i <= siz[s];i ++) 
        t[s][i] = t[s*2][(tg[s*2] + i) % siz[s*2]] + t[s*2+1][(tg[s*2+1] + i) % siz[s*2+1]];
    tg[s] = 0;
}void pd(int s){if(gt[s]) cg(s*2,gt[s]),cg(s*2+1,gt[s]),gt[s] = 0;}
void build(int s,int l,int r){
    if(l == r) return init(s,a[l]);
    int mid = (l + r) / 2;
    build(s*2,l,mid),build(s*2+1,mid+1,r),pp(s);
}void upd(int s,int l,int r,int ql,int qr){
    if(ql <= l && r <= qr && vis[s]) return cg(s,1);
    if(l == r) return init(s,pw(t[s][0]));
    int mid = (l + r) / 2; pd(s);
    if(ql <= mid) upd(s*2,l,mid,ql,qr);
    if(qr > mid) upd(s*2+1,mid+1,r,ql,qr);
    pp(s);
}int qry(int s,int l,int r,int ql,int qr){
    if(ql <= l && r <= qr) return t[s][tg[s]];
    int mid = (l + r) / 2,ans = 0; pd(s);
    if(ql <= mid) ans += qry(s*2,l,mid,ql,qr);
    if(qr > mid) ans += qry(s*2+1,mid+1,r,ql,qr);
    return ans;
}void los(){
    cin >> n >> m >> p; queue<int> q;
    for(int i = 1;i <= n;i ++) cin >> a[i];
    for(int i = 0;i < p;i ++) d[pw(i)] ++;
    for(int i = 0;i < p;i ++) if(!d[i]) q.push(i);
    while(q.size()){
        int u = q.front(); q.pop();
        if(!(--d[pw(u)])) q.push(pw(u));
    }build(1,1,n);  
    while(m --){
        cin >> op >> l >> r;
        if(op == 1) upd(1,1,n,l,r);
        else cout << qry(1,1,n,l,r) << "\n";
    }
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

[USACO17FEB] Why Did the Cow Cross the Road II G \(\color{blue}(39)\)

dp 假了四次,最后一遍才想出来正确的 dp,太菜了 /ll

\(f_{i,j}\) 表示 \(a\) 不超过 \(i\)\(b\) 不超过 \(j\) 的最大值,如果 \(|a_i - b_j| \le 4\),有 \(f_{i,j} = \max(f_{i,j},f_{i-1,j-1}+1,f_{i-1,j})\),否则只和 \(f_{i-1,j}\)\(\max\)。注意这是一个类似前缀 \(\max\) 的 dp,要有转移 \(f_{i,j} = \max(f_{i,j-1},f_{i,j})\)。时间复杂度 \(\mathcal O(n^2)\)

代码
/**
*    author: sunkuangzheng
*    created: 24.01.2024 09:36:24
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 2e3+5;
using namespace std;
int T,n,f[N][N],a[N],b[N],ans,g[N];
void los(){
    cin >> n;
    for(int i = 1;i <= n;i ++) cin >> a[i];
    for(int i = 1;i <= n;i ++) cin >> b[i];
    for(int i = 1;i <= n;i ++)
        for(int j = 1;j <= n;ans = max(f[i][j ++],ans),f[i][j] = f[i][j-1])
            if(abs(a[i] - b[j]) <= 4) f[i][j] = max({f[i][j],f[i-1][j-1] + 1,f[i-1][j]});
            else f[i][j] = max(f[i][j],f[i-1][j]);
    cout << ans;
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

[USACO17FEB] Why Did the Cow Cross the Road III G \(\color{blue}(40)\)

【模板】二维数点。时间复杂度 \(\mathcal O(n \log n)\)

本来看讨论区说 \(\mathcal O(n^2)\) 能过就偷懒写了个暴力,结果我的咋就 T 了呢 qwq

代码
/**
*    author: sunkuangzheng
*    created: 24.01.2024 10:51:51
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,a[N],t[N],re;vector<int> g[N]; ll ans; vector<pair<int,int>> r[N];
void upd(int x){for(;x <= 2 * n;x += x & -x) t[x] ++;}
int qry(int x){for(re = 0;x;x -= x & -x) re += t[x]; return re;}
void los(){
    cin >> n;
    for(int i = 1;i <= 2*n;i ++) cin >> a[i],g[a[i]].push_back(i);
    auto add = [&](int ql,int qr,int tl,int tr){
        if(tl > tr || ql > qr) return ;
        r[qr].emplace_back(tr,1),r[ql-1].emplace_back(tl-1,1),
        r[qr].emplace_back(tl-1,-1),r[ql-1].emplace_back(tr,-1);
    };
    for(int i = 1;i <= n;i ++) r[g[i][0]].emplace_back(g[i][1],-1e9),add(g[i][0]+1,g[i][1]-1,g[i][1]+1,2*n);
    for(int i = 1;i <= 2*n;i ++){
        sort(r[i].begin(),r[i].end());
        for(auto [l,id] : r[i]){
            if(id == -1e9) upd(l);
            else ans += id * qry(l);
        }
    }cout << ans;
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

[USACO17FEB] Why Did the Cow Cross the Road G \(\color{blue}(41)\)

分层图最短路就好,每个点拆三层表示经过次数 \(\bmod 3\),然后跑最短路,时间复杂度 \(\mathcal O(nm \log nm)\)

代码
/**
 *    author: sunkuangzheng
 *    created: 24.01.2024 12:49:30
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 105+5;
using namespace std;
int T,n,m,a[N][N],dis[N*N*5],vis[N*N*5],dx[5] = {0,1,0,-1,0},dy[5] = {0,0,1,0,-1};vector<pair<int,int>> g[N*N*5];
void los(){
    cin >> n >> m;
    auto id = [&](int x,int y,int k){return (x - 1) * n + y + k * n * n;};
    for(int i = 1;i <= n;i ++) for(int j = 1;j <= n;j ++) cin >> a[i][j];
    for(int i = 1;i <= n;i ++) for(int j = 1;j <= n;j ++) for(int k = 0;k <= 2;k ++)
        for(int l = 1;l <= 4;l ++){
            int ax = i + dx[l],ay = j + dy[l];
            if(ax >= 1 && ax <= n && ay >= 1 && ay <= n)
                g[id(i,j,k)].emplace_back(id(ax,ay,(k + 1) % 3),m + (k == 2 ? a[ax][ay] : 0));
        }
    memset(dis,0x3f,sizeof(dis)),dis[id(1,1,0)] = 0;
    priority_queue<pair<int,int>> q; q.emplace(0,id(1,1,0));
    while(q.size()){
        auto [d,u] = q.top(); q.pop();
        if(vis[u]) continue; vis[u] = 1;
        for(auto [v,w] : g[u]) if(dis[v] > dis[u] + w) dis[v] = dis[u] + w,q.emplace(-dis[v],v); 
    }cout << min({dis[id(n,n,0)],dis[id(n,n,1)],dis[id(n,n,2)]});
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

[USACO17FEB] Why Did the Cow Cross the Road II P \(\color{blue}(42)\)

\(n \le 1000\) 时我们采用了 \(\mathcal O(n^2)\) 的解法,但完全没有利用 \(4\) 这个特别的数字。我们尝试让 \(4\) 乘到复杂度里。从 \(1 \to n\) 枚举 \(a\) 中元素,维护和前 \(j\)\(b\) 里的元素中配对得到的最大贡献,树状数组维护。枚举的每一个 \(a\) 能更改的位置只有 \(k = 2 \times 4 + 1 = 9\) 个,总时间复杂度 \(\mathcal O(nk \log n)\)

代码
/**
*    author: sunkuangzheng
*    created: 24.01.2024 13:43:29
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,t[N],re,a[N],b[N],p[N],f[N];
void upd(int x,int p){for(;x <= n;x += x & -x) t[x] = max(t[x],p);}
int qry(int x){for(re = 0;x;x -= x & -x) re = max(re,t[x]);return re;}
void los(){
    cin >> n;
    for(int i = 1;i <= n;i ++) cin >> a[i];
    for(int i = 1;i <= n;i ++) cin >> b[i],p[b[i]] = i;
    for(int i = 1;i <= n;i ++){
        vector<int> acc;
        for(int j = max(1,a[i] - 4);j <= min(n,a[i] + 4);j ++) acc.push_back(p[j]);
        sort(acc.begin(),acc.end());
        for(int j : acc) f[j] = qry(j - 1) + 1;
        for(int j : acc) upd(j,f[j]); 
    }cout << qry(n);
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

[POI2015] ODW \(\color{blue}(43)\)

给定序列 \(a(n \le 10^5)\),每次询问 \((1 \le q \le 10^5)\) 给定 \(x,k\),求 \(\sum \limits_{i=0}^{ik+x \le n} a_{ik+x}\)

这显然是个根号分治板子,这个题就是放在了树上。处理方式类似,不过看起来实现很恶心。时间复杂度 \(\mathcal O(n \sqrt n \log n)\),跳 \(k\) 级祖先如果长剖可以去掉 \(\log\)

代码
/**
 *    author: sunkuangzheng
 *    created: 24.01.2024 19:43:07
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e4+5,B = 25;
using namespace std;
int T,n,fa[N][20],a[N],u,v,dep[N],sm[N][B+5],b[N],d,dis,c[N],k,dlt;vector<int> g[N];
int kfa(int u,int k){if(k >= dep[u] || k < 0) return 0;for(int i = __lg(dep[u]);i >= 0;i --) if((k >> i) & 1) u = fa[u][i]; return u;}
void dfs(int u,int f){
    fa[u][0] = f,dep[u] = dep[f] + 1;
    for(int i = 1;i <= __lg(dep[u]);i ++) fa[u][i] = fa[fa[u][i-1]][i-1];
    for(int i = 1;i <= B;i ++) sm[u][i] = sm[kfa(u,i)][i] + a[u]; 
    for(int v : g[u]) if(v != f) dfs(v,u);
}int lca(int u,int v){
    if(dep[u] < dep[v]) swap(u,v);
    while(dep[u] > dep[v]) u = fa[u][__lg(dep[u] - dep[v])];
    for(int i = __lg(dep[u]);i >= 0;i --) if(fa[u][i] != fa[v][i]) u = fa[u][i],v = fa[v][i];
    return (u == v ? u : fa[u][0]);
}int bf(int u,int v,int k){
    int ans = 0;if(!v) return 0;
    while(dep[u] >= dep[v]) ans += a[u],u = kfa(u,k);
    return ans;
}void los(){
    cin >> n;
    for(int i = 1;i <= n;i ++) cin >> a[i];
    for(int i = 1;i < n;i ++) cin >> u >> v,g[u].push_back(v),g[v].push_back(u);
    dfs(1,0);
    for(int i = 1;i <= n;i ++) cin >> b[i];
    for(int i = 1;i < n;i ++){
        u = b[i],v = b[i + 1],cin >> k,d = lca(u,v);
        if(k > B) cout << bf(u,kfa(u,dep[u] - dep[d] - 1),k) + bf(v,d,k) << "\n";
        else{
            auto func = [&](int u,int v){return (!v ? 0 :sm[u][k] - sm[kfa(v,k)][k]);};
            auto get = [&](int d){return (d < 0 ? d : d - d % k);};
            cout << func(u,kfa(u,get(dep[u] - dep[d] - 1))) + 
                func(v,kfa(v,get(dep[v] - dep[d]))) << "\n";
        }
    }
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

绝世好题 \(\color{blue}(44)\)

朴素的 dp 是 \(\mathcal O(n^2)\) 的,考虑令 \(g_i\) 表示二进制第 \(i\) 位为 \(1\) 里的 \(f_j\) 的最大值,则每次枚举 \(a_i\) 有的二进制位,和对应 \(g_i+1\)\(\max\) 即可。时间复杂度 \(\mathcal O(n \log V)\)

代码
/**
 *    author: sunkuangzheng
 *    created: 24.01.2024 14:49:06
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,f[N],a[55],x;
void los(){
    cin >> n;
    for(int i = 1;i <= n;i ++){
        cin >> x;
        for(int j = 0;j <= 30;j ++) if((x >> j) & 1) f[i] = max(f[i],a[j] + 1);
        for(int j = 0;j <= 30;j ++) if((x >> j) & 1) a[j] = max(f[i],a[j]);
    }cout << *max_element(f+1,f+n+1);
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

[POI2010] ANT-Antisymmetry \(\color{blue}(45)\)

考虑把 \(s\) 反转的反串拼到后面,枚举回文中心,求回文半径等价于求正串和反串的 \(\operatorname{LCP}\),后缀数组即可。时间复杂度 \(\mathcal O(n \log n)\),如果写两 \(\log\) 后缀数组就是 \(\mathcal O(n \log^2 n)\)

代码
/**
*    author: sunkuangzheng
*    created: 24.01.2024 15:12:52
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 1e6+5;
using namespace std;
int T,n,m,sa[N],rk[N],ok[N],h[N],st[22][N];string s;
void los(){
    cin >> n >> s,s = s + '#' + string(s.rbegin(),s.rend());
    for(int i = n + 1;i < s.size();i ++) s[i] ^= 1;
    auto SA = [&](string &s,int &n){
        s = " " + s,n = s.size() - 1;
        for(int i = 1;i <= n;i ++) sa[i] = i,rk[i] = s[i];
        for(int j = 1;j < n;j *= 2){
            for(int i = 1;i <= n;i ++) ok[i] = rk[i]; int p = 0;
            sort(sa+1,sa+n+1,[&](int x,int y){return rk[x] < rk[y] || rk[x] == rk[y] && rk[x + j] < rk[y + j];});
            auto cmp = [&](int x,int y){return ok[x] == ok[y] && ok[x + j] == ok[y + j];};
            for(int i = 1;i <= n;i ++) if(cmp(sa[i],sa[i-1])) rk[sa[i]] = p; else rk[sa[i]] = ++p;
        }for(int i = 1,k = 0;i <= n;h[rk[i ++]] = k) for(k --,k = max(k,0);s[i + k] == s[sa[rk[i] - 1] + k];k ++);
        for(int i = 1;i <= n;i ++) st[0][i] = h[i];
        for(int j = 1;j <= __lg(n);j ++) for(int i = 1;i + (1 << j) - 1 <= n;i ++)
            st[j][i] = min(st[j-1][i],st[j-1][i + (1 << j - 1)]);
    };SA(s,m);
    auto lcp = [&](int i,int j){
        if(i = rk[i],j = rk[j],i > j) swap(i,j); assert(i != j);
        int k = __lg(j - i);
        return min(st[k][i+1],st[k][j-(1<<k)+1]);
    };auto get = [&](int i){return n + 2 + n - i;};
    ll ans = 0;
    for(int i = 1;i < n;i ++) ans += lcp(get(i),i + 1);
    cout << ans;
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

AI robots \(\color{blue}(46)\)

自己胡了个需要跑两遍三维数点的做法,看了题解发现有个更厉害的。

考虑一个关键结论:如果 \(r_j \ge r_i\)\(x_j \in [x_i - r_i,x_i + r_i]\),那么 \(i,j\) 可以相互看到。我们把 \(i,j\) 的贡献记在 \(r\) 较小的上面,把三维数点的第一维变成 \(r\) 即可。注意 \(r\) 相等时不要算重。时间复杂度 \(\mathcal O(n \log^2 n)\)

代码
/**
*    author: sunkuangzheng
*    created: 25.01.2024 16:40:08
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,t[N],re,al[N],ar[N],tx[N],tr[N],tq[N],b[N],cnt,tmp,tot,k; ll ans;
void upd(int x,int p){for(;x <= tot;x += x & -x) t[x] += p;}
int sum(int x){for(re = 0;x;x -= x & -x) re += t[x]; return re;}
int qry(int l,int r){return sum(r) - sum(l - 1);}
struct fk{int a,b,c,id,tp;}a[N];
bool cmp1(fk a,fk b){return a.a > b.a || a.a == b.a && a.id < b.id || a.a == b.a && a.id == b.id && a.tp < b.tp;}
bool cmp2(fk a,fk b){return a.b < b.b || a.b == b.b && a.id < b.id || a.b == b.b && a.id == b.id && a.tp < b.tp;}
void cdq(int l,int r){
    if(l == r) return ;
    int mid = (l + r) / 2;
    cdq(l,mid),cdq(mid + 1,r);
    sort(a+l,a+mid+1,cmp2),sort(a+mid+1,a+r+1,cmp2);
    int i = mid + 1,j = l;
    for(;i <= r;i ++){
        for(;j <= mid && a[j].b <= a[i].b;j ++) if(a[j].tp == -1e9) upd(a[j].c,1);
        if(a[i].tp != -1e9) ans += a[i].tp * qry(al[a[i].c],ar[a[i].c]);
    }for(int i = l;i < j;i ++) if(a[i].tp == -1e9) upd(a[i].c,-1);
}void los(){
    cin >> n >> k; ll res = 0;
    for(int i = 1;i <= n;i ++) 
        cin >> tx[i] >> tr[i] >> tq[i],b[++tot] = tq[i],b[++tot] = tq[i] - k,b[++tot] = tq[i] + k;
    sort(b+1,b+tot+1),tot = unique(b+1,b+tot+1) - b - 1;
    auto lb = [&](int x){return lower_bound(b+1,b+tot+1,x) - b;};
    for(int i = 1;i <= n;i ++) tmp = lb(tq[i]),al[tmp] = lb(tq[i] - k),ar[tmp] = lb(tq[i] + k),tq[i] = tmp;
    for(int i = 1;i <= n;i ++)
        a[++cnt] = {tr[i],tx[i],tq[i],i,(int)-1e9},a[++cnt] = {tr[i],tx[i] - tr[i] - 1,tq[i],i,-1},
        a[++cnt] = {tr[i],tx[i] + tr[i],tq[i],i,1};
    sort(a+1,a+cnt+1,cmp1);
    cdq(1,cnt),cout << ans - n;
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

[国家集训队] 排队 \(\color{blue}(47)\)

考虑暴力怎么做。先求出逆序对,然后考虑交换 \(i,j\) 的贡献,减去 \(i,j\) 的逆序对贡献,加上 \(j,i\) 的逆序对贡献,复杂度 \(\mathcal O(n \log n + nm)\)。容易发现我们要统计的是一个前缀或后缀中大于或小于某个值的数字个数,树套树维护即可。时间复杂度 \(\mathcal O(n \log n + m \log^2 n)\)

代码
/**
*    author: sunkuangzheng
*    created: 22.01.2024 16:23:25
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 30005,M = 1e9;
using namespace std;
int T,n,rt[N*40],tot,t[N*400],ls[N*400],rs[N*400],a[N],m,op,l,r,x;
struct seg{
    void upd(int &s,int l,int r,int x,int k){
        if(!s) s = ++tot;
        if(l == r) return t[s] += k,void();
        int mid = (l + r) / 2;
        if(x <= mid) upd(ls[s],l,mid,x,k); else upd(rs[s],mid+1,r,x,k);
        t[s] = t[ls[s]] + t[rs[s]];
    }int qry(int s,int l,int r,int ql,int qr){
        if(ql > qr) return 0;
        if(ql <= l && r <= qr) return t[s];
        int mid = (l + r) / 2,ans = 0;
        if(ql <= mid) ans += qry(ls[s],l,mid,ql,qr);
        if(qr > mid) ans += qry(rs[s],mid+1,r,ql,qr);
        return ans;
    }
}tr;
void upd(int s,int l,int r,int x,int k,int y){
    tr.upd(rt[s],1,M,k,y);
    if(l == r) return void();
    int mid = (l + r) / 2;
    if(x <= mid) upd(s*2,l,mid,x,k,y); else upd(s*2+1,mid+1,r,x,k,y);
}int qry(int s,int l,int r,int ql,int qr,int al,int ar){
    if(ql > qr) return 0;
    if(ql <= l && r <= qr) return tr.qry(rt[s],1,M,al,ar);
    int mid = (l + r) / 2,ans = 0;
    if(ql <= mid) ans += qry(s*2,l,mid,ql,qr,al,ar);
    if(qr > mid) ans += qry(s*2+1,mid+1,r,ql,qr,al,ar);
    return ans;
}void los(){
    cin >> n; ll ans = 0;
    for(int i = 1;i <= n;i ++)
        cin >> a[i],upd(1,1,n,i,a[i],1),ans += qry(1,1,n,1,i,a[i] + 1,M);
    cout << ans << "\n";
    cin >> m;
    while(m --){
        cin >> l >> r; if(l > r) swap(l,r);
        auto calc = [&](int l){return qry(1,1,n,1,l-1,a[l] + 1,M) + qry(1,1,n,l + 1,n,1,a[l] - 1);};
        ans -= calc(l) + calc(r) - (a[l] > a[r]), 
        upd(1,1,n,l,a[l],-1),upd(1,1,n,l,a[r],1),upd(1,1,n,r,a[r],-1),upd(1,1,n,r,a[l],1),
        swap(a[l],a[r]),ans += calc(l) + calc(r) - (a[l] > a[r]);
        cout << ans << "\n";
    }
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

Goodbye Souvenir \(\color{blue}(48)\)

\(pre_i\) 表示 \(i\) 前面的第一个同色元素,则题意可以转化为 \(\sum \limits_{i=l}^r [pre_i \ge l] (i - pre_i)\)。显然 \(pre_i > i\),则原问题是二维数点。现在有了修改,相当于增加了时间维度,则 CDQ 分治即可。前驱后继的修改较为麻烦,有一些实现细节,用 set 维护。时间复杂度 \(\mathcal O((n + m) \log^2 n)\)

代码
/**
*    author: sunkuangzheng
*    created: 25.01.2024 21:29:56
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 1e5+5;
using namespace std;
int T,n,m,t[N],re,pre[N],b[N],lst[N],cnt,op,l,r,iff[N]; set<int> s[N]; ll ans[N];
void upd(int x,int p){if(!x) return ;for(;x <= n;x += x & -x) t[x] += p;}
int sum(int x){for(re = 0;x;x -= x & -x) re += t[x];return re;}
int qry(int l,int r){return sum(r) - sum(l - 1);}
struct node{int b,c,op,val;}a[N*10]; // a 时间 b r c lst
bool cmp2(node a,node b){return a.b < b.b || a.b == b.b && a.op < b.op;}
void cdq(int l,int r){
    if(l == r) return ;
    int mid = (l + r) / 2;
    cdq(l,mid),cdq(mid+1,r);
    sort(a+l,a+mid+1,cmp2),sort(a+mid+1,a+r+1,cmp2);
    int i = mid + 1,j = l;
    for(;i <= r;i ++){
        for(;j <= mid && a[j].b <= a[i].b;j ++) if(!a[j].op) upd(a[j].c,a[j].val);
        if(a[i].op) ans[a[i].op] += qry(a[i].c,n);
    }for(int i = l;i < j;i ++) if(!a[i].op) upd(a[i].c,-a[i].val);
}void los(){
    cin >> n >> m;
    auto add = [&](int b,int c,int id,int val){if(!b) return ;a[++cnt] = {b,c,id,val};};
    for(int i = 1;i <= n;i ++)
        cin >> b[i],pre[i] = lst[b[i]],lst[b[i]] = i,add(i,pre[i],0,i - pre[i]),s[b[i]].insert(i);
    for(int i = 1;i <= m;i ++){
        cin >> op >> l >> r;
        if(op == 2) add(r,l,i,0),iff[i] = 1;
        else{
            auto get = [&](int l,int i){
                auto it = s[l].upper_bound(i); int p = 0,q = 0;
                if(it != s[l].end()) p = *it; if((--it) != s[l].begin()) q = *(--it);
                return make_pair(q,p); 
            };auto [p,q] = get(b[l],l);add(l,pre[l],0,pre[l] - l),add(q,pre[q],0,pre[q] - q),
                        pre[q] = p,add(q,pre[q],0,q - pre[q]),s[b[l]].erase(l),b[l] = r;
            s[b[l]].insert(l);
            auto [x,y] = get(b[l],l);pre[l] = x;add(y,pre[y],0,pre[y] - y),pre[y] = l,
                        add(l,pre[l],0,l - pre[l]),add(y,pre[y],0,y - pre[y]);
        }
    }cdq(1,cnt); 
    for(int i = 1;i <= m;i ++) if(iff[i]) cout << ans[i] << "\n"; 
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

[APIO2019] 路灯 \(\color{blue}(49)\)

很巧妙的题。

对于 \(i\) 的修改,找到 \(l,r\) 满足 \([l,i),(i,r]\) 使得区间内全都是 \(1\),则修改相当于让 \(L \in [l,i),R \in (i,r]\)\([L,R]\) 全都合法或不合法。可以用 set 维护连续 \(1\) 段。

但是要统计有多少个时间段,我们对贡献差分,对于第 \(i\) 个时间段的修改,若合法,区间加 \(m-i\),否则减 \(m-i\)。注意特判查询时 \([l,r]\) 仍然全是 \(1\) 的情况。

区间加单点查带时间轴,这是三维数点问题,用 CDQ 分治实现。时间复杂度 \(\mathcal O((n + m) \log^2 n)\)

代码
/**
*    author: sunkuangzheng
*    created: 26.01.2024 09:31:19
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,t[N],re,ans[N],m,b[N],cnt,l,r,tot,j;set<int> s; string fk;
struct node{int b,c,op,val;}a[N*20];
bool cmp(node a,node b){return a.b < b.b;}
void upd(int x,int p){for(;x <= n + 1;x += x & -x) t[x] += p;}
int qry(int x){for(re = 0;x;x -= x & -x) re += t[x];return re;}
void cdq(int l,int r){
    if(l == r) return ;
    int mid = (l + r) / 2; cdq(l,mid),cdq(mid+1,r);
    sort(a+l,a+mid+1,cmp),sort(a+mid+1,a+r+1,cmp);
    int i = mid + 1,j = l;
    for(;i <= r;i ++){
        for(;j <= mid && a[j].b <= a[i].b;j ++) if(!a[j].op) upd(a[j].c,a[j].val);
        if(a[i].op) ans[a[i].op] += qry(a[i].c); 
    }for(int i = l;i < j;i ++) if(!a[i].op) upd(a[i].c,-a[i].val);
}void los(){
    auto add = [&](int b,int c,int op,int val){a[++cnt] = {b,c,op,val};};
    auto addd = [&](int l,int r,int ql,int qr,int val){
        if(l > r || ql > qr) return ; 
        add(r+1,qr+1,0,val),add(l,ql,0,val),add(r+1,ql,0,-val),add(l,qr+1,0,-val);
    };
    cin >> n >> m >> fk,fk = " " + fk; s.insert(0),s.insert(n + 1);
    for(int i = 1;i <= n;i ++) if(b[i] = fk[i] - '0',!b[i]) s.insert(i);
    for(int i = 1;i <= n;i ++){
        int l = i; while(b[i]) i ++;
        addd(l,i-1,l,i-1,m);
    }for(int i = 1;i <= m;i ++){
        cin >> fk;
        if(fk[0] == 't'){
            cin >> j; 
            if(b[j]){
                auto it = s.lower_bound(j),ti = it --;
                int l = *it,r = *ti;l ++,r --,s.insert(j);
                addd(l,j,j,r,i - m);
            }else{
                s.erase(j); 
                auto it = s.lower_bound(j),ti = it --;
                int l = *it,r = *ti;l ++,r --,addd(l,j,j,r,m - i);
            }b[j] ^= 1;
        }else  
            if(cin >> l >> r,add(l,--r,++tot,0),s.lower_bound(l) == s.lower_bound(r+1)) ans[tot] -= m - i;  
    }cdq(1,cnt);
    for(int i = 1;i <= tot;i ++) cout << ans[i] << "\n";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

[HNOI2010] 城市建设 \(\color{blue}(50)\)

CDQ 分治题单里的,但是感觉不是很 CDQ。这道题的核心思路是,在分治中去掉一定有用的边和一定没有用的边,让处理长度为 \(len\) 的区间的复杂度达到 \(\mathcal O(len \log len)\)。以下所有时间复杂度描述均假设 \(m,q\) 同阶。

考虑对时间轴分治,设当前在处理 \([l,r]\) 区间内的修改,当前要处理的边的集合是 \(g\)\([l,r]\) 的操作涉及到的边集是 \(h\)。考虑筛出两类不在 \(h\) 中的边:

  • \(h\) 中的边全部尝试强制加入最小生成树后,边 \(e\) 仍然在最小生成树中。

  • \(h\) 中的边全部强制不加入最小生成树后,边 \(e\) 也不在最小生成树中。

显然的,第一类边对于任何时间轴区间 \([L,R](l \le L \le R \le r)\),它一定在最小生成树里;第二类边则一定不在最小生成树里。我们去掉这两类边,并向下递归 \([l,mid],[mid+1,r]\)。可以证明,这样操作后对于区间 \([l,r]\)\(|g|\)\(\mathcal O(r-l)\) 级别的。

实现上,递归到 \(l=r\) 时就修改边权并输出答案,是否在最小生成树上用可撤销并查集维护,类似于线段树分治。时间复杂度 \(\mathcal O(m \log n \log q)\)

代码
/**
*    author: sunkuangzheng
*    created: 26.01.2024 14:25:34
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
#define eb emplace_back
#define G u,v,id
#define clr(x) vector<edge> ().swap(x)
int T,n,fa[N],siz[N],stx[N],sty[N],tp,cg[N],cgv[N],vis[N],w[N],m,q,u,v;
struct edge{int G;};
bool cmp(edge a,edge b){return w[a.id] < w[b.id];}
int fd(int x){return x == fa[x] ? x : fd(fa[x]);}
bool mg(int x,int y){
    if(x = fd(x),y = fd(y),x == y) return 0;
    if(siz[x] < siz[y]) swap(x,y);
    stx[++tp] = x,sty[tp] = y,siz[x] += siz[y],fa[y] = x; return 1;
}void fk(int t){for(int x,y;tp > t;) x = stx[tp],y = sty[tp --],fa[y] = y,siz[x] -= siz[y];}
void sol(int l,int r,ll ans,vector<edge> g){ 
    int tt = tp,t2 = -1,mid = (l + r) / 2;
    if(l == r){
        w[cg[l]] = cgv[l]; sort(g.begin(),g.end(),cmp);
        for(auto [G] : g) if(mg(u,v)) ans += w[id];
        return cout << ans << "\n",fk(tt);
    }for(int i = l;i <= r;i ++) vis[cg[i]] = 1;
    vector<edge> s,cjr,nxt,tmp;
    sort(g.begin(),g.end(),cmp);
    for(auto [G] : g)  if(vis[id]) nxt.eb(G),mg(u,v); else cjr.eb(G); 
    for(auto [G] : cjr) if(mg(u,v)) s.eb(G); else tmp.eb(G); fk(tt);
    for(auto [G] : s) if(mg(u,v)) ans += w[id]; t2 = tp;
    for(auto [G] : tmp) if(mg(u,v)) nxt.eb(G); clr(tmp); clr(s); clr(cjr); fk(t2);
    for(int i = l;i <= r;i ++) vis[cg[i]] = 0;
    sol(l,mid,ans,nxt),fk(t2),sol(mid+1,r,ans,nxt),fk(tt);
}void los(){
    cin >> n >> m >> q;vector<edge> g;
    for(int i = 1;i <= n;i ++) fa[i] = i,siz[i] = 1;
    for(int i = 1;i <= m;i ++) cin >> u >> v >> w[i],g.emplace_back(u,v,i);
    for(int i = 1;i <= q;i ++) cin >> cg[i] >> cgv[i]; sol(1,q,0,g);
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(51 \sim 75\)

把编号放在了前面,并添加了题号。

\(\color{blue}(51)\) P4169 [Violet] 天使玩偶/SJY摆棋子

难度 \(^*2500\)CDQ 分治

分类讨论四种情况:设查询点为 \((x,y)\),答案点为 \((p,q)\),若 \(p \le x,q \le y\),则答案为 \(x+y-p-q\),我们贪心的希望 \(p+q\) 尽可能大。因为有修改,我们要增加时间轴,也就是一个三维数点问题,CDQ 分治解决。可以分讨四次,也可以直接旋转点四次,大大减少码量。时间复杂度 \(\mathcal O(n \log n \log V)\)

代码
/**
*    author: sunkuangzheng
*    created: 26.01.2024 16:18:28
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5,M = 1e6+2;
using namespace std;
int T,n,t[M+5],re,ans[N],m,op[N],ax[N],ay[N],tot,cnt,px[N],py[N],tmp;
void upd(int x,int p){for(;x <= M+4;x += x & -x) t[x] = max(t[x],p);}
int qry(int x){for(re = -1e8;x;x -= x & -x) re = max(re,t[x]); return re;}
void clr(int x){for(;x <= M+4;x += x & -x) t[x] = -1e8;}
struct node{int b,c,op,val;}a[N*10];
bool cmp(node a,node b){return a.b < b.b;}
void cdq(int l,int r){
    if(l == r) return ;
    int mid = (l + r) / 2,i = mid + 1,j = l;
    cdq(l,mid),cdq(mid+1,r),sort(a+l,a+mid+1,cmp),sort(a+mid+1,a+r+1,cmp);
    for(;i <= r;i ++){
        for(;j <= mid && a[j].b <= a[i].b;j ++) if(!a[j].op) upd(a[j].c,a[j].val);
        if(a[i].op) ans[a[i].op] = min(ans[a[i].op],a[i].val - qry(a[i].c)); 
    }for(int i = l;i < j;i ++) if(!a[i].op) clr(a[i].c);
}void los(){
    cin >> n >> m; memset(ans,0x3f,sizeof(ans)); 
    for(int i = 1;i <= n;i ++) cin >> px[i] >> py[i],px[i] ++,py[i] ++;
    for(int i = 1;i <= m;i ++) cin >> op[i] >> ax[i] >> ay[i],ax[i] ++,ay[i] ++;
    for(int k = 1;k <= 4;k ++){
        cnt = tot = 0;
        for(int i = 1;i <= M+4;i ++) t[i] = -1e8;
        for(int i = 1;i <= n;i ++) tmp = px[i],px[i] = M - py[i],py[i] = tmp,
                                a[++cnt] = {px[i],py[i],0,px[i] + py[i]};
        for(int i = 1;i <= m;i ++) tmp = ax[i],ax[i] = M - ay[i],ay[i] = tmp;
        for(int i = 1;i <= m;i ++){
            if(op[i] == 1) a[++cnt] = {ax[i],ay[i],0,ax[i] + ay[i]};
            else a[++cnt] = {ax[i],ay[i],++tot,ax[i] + ay[i]};
        }cdq(1,cnt);
    }for(int i = 1;i <= tot;i ++) cout << ans[i] << "\n";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(52)\) P2717 寒假作业

难度 \(^*1800\)树状数组

平均数大于等于 \(k\) 的经典套路是每个数字 \(-k\),查询区间和是否大于等于 \(0\)

然后前缀和一下,转化为有多少对 \((l,r)\) 满足 \(l \le r\)\(s_r - s_{l-1} \ge 0\),二维数点。时间复杂度 \(\mathcal O(n \log n)\)

代码
/**
*    author: sunkuangzheng
*    created: 26.01.2024 17:38:24
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,a[N],k,b[N],tot,t[N],re;ll ans;
void upd(int x,int p){for(;x <= tot;x += x & -x) t[x] += p;}
int qry(int x){for(re = 0;x;x -= x & -x) re += t[x];return re;}
void los(){
    cin >> n >> k; b[n + 1] = a[0] = -k;
    for(int i = 1;i <= n;i ++) cin >> a[i],a[i] += a[i - 1] - k,b[i] = a[i];
    sort(b+1,b+n+2),tot = unique(b+1,b+n+2) - b - 1; upd(lower_bound(b+1,b+tot+1,-k)-b,1);
    for(int i = 1;i <= n;i ++)
        a[i] = lower_bound(b+1,b+tot+1,a[i]) - b,ans += qry(a[i]),upd(a[i],1);
    cout << ans;
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(53)\) P2487 [SDOI2011] 拦截导弹

难度 \(^*2600\)动态规划,DP;CDQ 分治

如果只有第一问就是三维偏序版最长上升子序列,直接 CDQ 的时候 dp 就好。要统计每个位置被拦截的概率,我们逆向思考,对于每个位置考虑几个方案包含它。容易发现它可以成为答案当且仅当以它结尾的最长上升子序列和以它开头的最长上升子序列拼起来长度取到 \(\max\),那么 dp 时维护方案数量即可。时间复杂度 \(\mathcal O(n \log^2 n)\)

代码不太好写,reverse 一下数组可以反复利用 CDQ。统计方案还是要仔细想一下。

代码
/**
 *    author: sunkuangzheng
 *    created: 26.01.2024 18:15:48
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long double;
const int N = 5e5+5;
using namespace std;
int T,n,t[N],tot,cnt,h[N],c[N],b[N],f[N],f2[N],mx; ll g[N],tr[N],re,g2[N];
void upd(int x,int p,ll q){for(;x <= tot;x += x & -x) if(p == t[x]) tr[x] += q; else if(p > t[x]) t[x] = p,tr[x] = q;}
int qry(int x){for(re = 0;x;x -= x & -x) re = max(re,(ll)t[x]); return re;}
void clr(int x){for(;x <= tot;x += x & -x) t[x] = tr[x] = 0;}
ll qry2(int x,int tg){for(re = 0;x;x -= x & -x) if(t[x] == tg) re += tr[x]; return re;}
struct node{int a,b,c,id;}a[N];
bool cmp1(node a,node b){return a.b < b.b;}
bool cmp2(node a,node b){return a.a < b.a;}
void cdq(int l,int r,int *f,ll *g){
    if(l == r) return ;
    int mid = (l + r) / 2,i = mid + 1,j = l;
    sort(a+l,a+r+1,cmp2);
    cdq(l,mid,f,g),sort(a+l,a+mid+1,cmp1),sort(a+mid+1,a+r+1,cmp1);
    for(;i <= r;i ++){
        for(;j <= mid && a[j].b <= a[i].b;j ++) upd(a[j].c,f[a[j].id],g[a[j].id]);
        int tmp = qry(a[i].c) + 1; 
        if(tmp == f[a[i].id]) g[a[i].id] += qry2(a[i].c,f[a[i].id]-1);
        else if(tmp > f[a[i].id]) f[a[i].id] = tmp,g[a[i].id] = qry2(a[i].c,f[a[i].id]-1);
    }for(int i = l;i < j;i ++) clr(a[i].c);  sort(a+l,a+r+1,cmp2),cdq(mid+1,r,f,g);
}void sol(int *f,ll *g){ 
    for(int i = 1;i <= n + 1;i ++) t[i] = tr[i],f[i] = g[i] = 1;
    f[1] = g[1] = 1;
    cdq(1,n,f,g),mx = *max_element(f+1,f+n+1);
}void los(){
    cin >> n; cout << fixed << setprecision(8);
    for(int i = 1;i <= n;i ++) cin >> h[i] >> c[i],b[i] = c[i];
    sort(b+1,b+n+1); tot = unique(b+1,b+n+1) - b - 1;
    for(int i = 1;i <= n;i ++) c[i] = lower_bound(b+1,b+tot+1,c[i]) - b;
    for(int i = 1;i <= n;i ++) a[i] = {i,-h[i],tot - c[i] + 1,i}; sol(f,g);
    for(int i = 1;i <= n;i ++) a[n-i+1] = {-i,h[i],c[i],n-i+1}; sol(f2,g2);
    ll sm = 0;
    cout << mx << "\n"; reverse(f2+1,f2+n+1),reverse(g2+1,g2+n+1);
    for(int i = 1;i <= n;i ++) if(f[i] == mx) sm += g[i];
    for(int i = 1;i <= n;i ++) cout << (f[i] + f2[i] - 1 == mx) 
                               * (double)(g[i] * g2[i]) / sm << " ";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(54)\) P6007 [USACO20JAN] Springboards G

难度 \(^*2000\)动态规划,DP;树状数组

每个跳台拆两个点,设 \(f_i\) 表示走到第 \(i\) 个点的最小距离,则有 \(f_i = \min_{x_j \le x_i \operatorname{and} y_j \le y_i} \{f_j + \operatorname{dis}(i,j)\}\)。树状数组维护二维偏序即可。跳台的情况记录一下跳台前面的点,简单实现即可。时间复杂度 \(\mathcal O(n \log n)\)

代码
/**
*    author: sunkuangzheng
*    created: 27.01.2024 07:56:47
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,m,ax,ay,px,py,b[N],cnt; set<tuple<int,int,int,int>> mp; ll t[N],re,f; map<pair<int,int>,ll> mpp;
void upd(int x,ll p){for(;x <= cnt;x += x & -x) t[x] = min(t[x],p);}
ll qry(int x){for(re = 0;x;x -= x & -x) re = min(re,t[x]); return re;}
void los(){
    cin >> m >> n,b[++cnt] = m,mp.insert({m,m,-1,0});
    for(int i = 1;i <= n;i ++) cin >> ax >> ay >> px >> py,mp.insert({ax,ay,-1,0}),
                            mp.insert({px,py,ax,ay}),b[++cnt] = ay,b[++cnt] = py;
    sort(b+1,b+cnt+1),cnt = unique(b+1,b+cnt+1) - b - 1;
    for(int i = 1;i <= cnt;i ++) t[i] = 1e18;
    for(auto [px,py,ax,ay] : mp){
        int t1 = lower_bound(b+1,b+cnt+1,py) - b,t2 = -1;
        f = qry(t1) + px + py; 
        if(ax != -1) f = min(f,mpp[{ax,ay}]); mpp[{px,py}] = f;
        upd(t1,f - px - py);
    }cout << f;
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(55)\) P3658 [USACO17FEB] Why Did the Cow Cross the Road III P

难度 \(^*2400\)CDQ 分治

\(i\) 在第一个排列里的位置是 \(a_i\),在第二个排列里是 \(b_i\),则问题转化为求 \((i,j)\) 对数使得 \(a_i > a_j,b_i < b_j\)\(|i - j| > k\)。三维偏序即可,时间复杂度 \(\mathcal O(n \log^2 n)\)

代码
/**
*    author: sunkuangzheng
*    created: 27.01.2024 08:58:37
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,k,t[N],re,x,a1[N],b1[N]; ll ans;
void upd(int x,int p){for(;x <= n;x += x & -x) t[x] += p;}
int sum(int x){if(x <= 0) return 0;for(re = 0;x;x -= x & -x) re += t[x]; return re;}
int qry(int l,int r){if(r <= 0 || l > n) return 0; return sum(min(n,r)) - sum(max(0,l - 1));}
struct node{int a,b,c;}a[N];
bool cmp(node a,node b){return a.b < b.b;}
void cdq(int l,int r){
    if(l == r) return ;
    int mid = (l + r) / 2,i = mid + 1,j = l;
    cdq(l,mid),cdq(mid+1,r),sort(a+l,a+mid+1,cmp),sort(a+mid+1,a+r+1,cmp);
    for(;i <= r;i ++){
        for(;j <= mid && a[j].b <= a[i].b;j ++) upd(a[j].c,1);
        ans += sum(a[i].c - k - 1) + qry(a[i].c + k + 1,n);
    }for(int i = l;i < j;i ++) upd(a[i].c,-1);
}void los(){
    cin >> n >> k;
    for(int i = 1;i <= n;i ++) cin >> x,a1[x] = i;
    for(int i = 1;i <= n;i ++) cin >> x,b1[x] = i;
    for(int i = 1;i <= n;i ++) a[i] = {a1[i],b1[i],i};
    sort(a+1,a+n+1,[&](node a,node b){return a.a > b.a;});
    cdq(1,n),cout << ans << "\n";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(56)\) P3769 [CH弱省胡策R2] TATT

难度 \(^*2800\)动态规划,DP;CDQ 分治

四维最长上升子序列,cdq 两次。先按照 \(a\) 排序,然后第一次 cdq 处理掉 \(b\),第二次 cdq 处理 \(c,d\)。第一次 cdq 时,区间按 \(a\) 排序后 \(a\) 较大的一侧打标记,然后按 \(b\) 排序好后 cdq 第二次,然后正常处理即可。注意相同元素的处理,当四维都完全相同时,要稳定排序,我的处理办法是给每个元素增加 \(id\) 属性,消除重复元素。时间复杂度 \(\mathcal O(n \log^3 n)\)

代码
/**
*    author: sunkuangzheng
*    created: 27.01.2024 09:58:42
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,t[N],tot,re,b[N];
struct node{int a,b,c,d,id,f,ac;}a[N];
bool cmp1(node a,node b){return (a.a^b.a ? a.a < b.a : (a.b^b.b ? a.b < b.b : (a.c ^ b.c ? a.c < b.c : (a.d ^ b.d ? a.d < b.d : a.id < b.id))));}
bool cmp2(node a,node b){return (a.b^b.b ? a.b < b.b : (a.c ^ b.c ? a.c < b.c : (a.d ^ b.d ? a.d < b.d : a.id < b.id)));}
bool cmp3(node a,node b){return (a.c ^ b.c ? a.c < b.c : (a.d ^ b.d ? a.d < b.d : a.id < b.id));}
void upd(int x,int p){for(;x <= tot;x += x & -x) t[x] = max(t[x],p);}
int qry(int x){for(re = 0;x;x -= x & -x) re = max(re,t[x]);return re;}
void clr(int x){for(;x <= tot;x += x & -x) t[x] = 0;}
void cdq2(int l,int r){
    if(l == r) return ;
    int mid = (l + r) / 2,i = mid + 1,j = l;
    cdq2(l,mid),sort(a+l,a+mid+1,cmp3),sort(a+mid+1,a+r+1,cmp3);
    for(;i <= r;i ++){
        for(;j <= mid && a[j].c <= a[i].c;j ++) if(!a[j].ac) upd(a[j].d,a[j].f);
        if(a[i].ac) a[i].f = max(a[i].f,qry(a[i].d) + 1);
    }for(int i = l;i < j;i ++) if(!a[i].ac) clr(a[i].d);
    sort(a+l,a+r+1,cmp2),cdq2(mid+1,r);
}void cdq1(int l,int r){
    if(l == r) return ;
    int mid = (l + r) / 2;
    cdq1(l,mid); for(int i = l;i <= r;i ++) a[i].ac = (i > mid);
    sort(a+l,a+r+1,cmp2),cdq2(l,r),sort(a+l,a+r+1,cmp1),cdq1(mid+1,r);
}void los(){
    cin >> n;
    for(int i = 1;i <= n;i ++) cin >> a[i].a >> a[i].b >> a[i].c >> a[i].d,b[i] = a[i].d,a[i].f = 1;
    sort(b+1,b+n+1),tot = unique(b+1,b+n+1) - b - 1;
    for(int i = 1;i <= n;i ++) a[i].d = lower_bound(b+1,b+tot+1,a[i].d) - b;
    sort(a+1,a+n+1,cmp1); 
    for(int i = 1;i <= n;i ++) a[i].id = i;
    cdq1(1,n); int ans = 0;
    for(int i = 1;i <= n;i ++) ans = max(ans,a[i].f);
    cout << ans;
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(57)\) P4849 寻找宝藏

难度 \(^*2800\)动态规划,DP;CDQ 分治

就是上一题加个方案数就好了,时间复杂度 \(\mathcal O(n \log^3 n)\)

所以这题为啥是黑的啊 qwq

代码
/**
*    author: sunkuangzheng
*    created: 27.01.2024 09:58:42
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5,mod = 998244353;
using namespace std;
int T,n,tot,b[N],tr[N],fk;ll t[N],re;
struct node{int a,b,c,d,id,g,ac,val;ll f;}a[N];
bool cmp1(node a,node b){return (a.a^b.a ? a.a < b.a : (a.b^b.b ? a.b < b.b : (a.c ^ b.c ? a.c < b.c : (a.d ^ b.d ? a.d < b.d : a.id < b.id))));}
bool cmp2(node a,node b){return (a.b^b.b ? a.b < b.b : (a.c ^ b.c ? a.c < b.c : (a.d ^ b.d ? a.d < b.d : a.id < b.id)));}
bool cmp3(node a,node b){return (a.c ^ b.c ? a.c < b.c : (a.d ^ b.d ? a.d < b.d : a.id < b.id));}
void upd(int x,ll p,int val){for(;x <= tot;x += x & -x) if(t[x] == p) tr[x] = (tr[x] + val) % mod; else if(t[x] < p) t[x] = p,tr[x] = val;}
ll qry(int x){for(re = 0;x;x -= x & -x) re = max(re,t[x]);return re;}
int qry2(int x,ll dp){for(re = 0;x;x -= x & -x) if(t[x] == dp) re = (re + tr[x]) % mod;return re;}
void clr(int x){for(;x <= tot;x += x & -x) t[x] = tr[x] = 0;}
void cdq2(int l,int r){
    if(l == r) return ;
    int mid = (l + r) / 2,i = mid + 1,j = l;
    cdq2(l,mid),sort(a+l,a+mid+1,cmp3),sort(a+mid+1,a+r+1,cmp3);
    for(;i <= r;i ++){
        for(;j <= mid && a[j].c <= a[i].c;j ++) if(!a[j].ac) upd(a[j].d,a[j].f,a[j].g);
        if(a[i].ac){
            ll tmp = qry(a[i].d);
            if(tmp + a[i].val == a[i].f) a[i].g = (a[i].g + qry2(a[i].d,tmp)) % mod;
            else if(tmp + a[i].val > a[i].f) a[i].f = tmp + a[i].val,a[i].g = qry2(a[i].d,tmp);
        }
    }for(int i = l;i < j;i ++) if(!a[i].ac) clr(a[i].d);
    sort(a+l,a+r+1,cmp2),cdq2(mid+1,r);
}void cdq1(int l,int r){
    if(l == r) return ;
    int mid = (l + r) / 2;
    cdq1(l,mid); for(int i = l;i <= r;i ++) a[i].ac = (i > mid);
    sort(a+l,a+r+1,cmp2),cdq2(l,r),sort(a+l,a+r+1,cmp1),cdq1(mid+1,r);
}void los(){
    cin >> n >> fk;
    for(int i = 1;i <= n;i ++) cin >> a[i].a >> a[i].b >> a[i].c >> a[i].d >> a[i].val,b[i] = a[i].d,a[i].f = a[i].val,a[i].g = 1;
    sort(b+1,b+n+1),tot = unique(b+1,b+n+1) - b - 1;
    for(int i = 1;i <= n;i ++) a[i].d = lower_bound(b+1,b+tot+1,a[i].d) - b;
    sort(a+1,a+n+1,cmp1); 
    for(int i = 1;i <= n;i ++) a[i].id = i;
    cdq1(1,n); ll ans = 0,res = 0;
    for(int i = 1;i <= n;i ++) ans = max(ans,a[i].f);
    for(int i = 1;i <= n;i ++) if(a[i].f == ans) res = (res + a[i].g) % mod;
    cout << ans << "\n" << res << "\n";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(58)\) CF641E Little Artem and Time Machine

难度 \(^*1900\)线段树

直接用动态开点线段树维护每一个数字的时间戳即可。时间复杂度 \(\mathcal O(n \log V)\),离散化能做到 \(\mathcal O(n \log n)\),但没必要。

代码
/**
*    author: sunkuangzheng
*    created: 27.01.2024 13:53:10
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,op[N],a[N],ti[N],t[N*20],tot,ls[N*20],rs[N*20];
void upd(int &s,int l,int r,int x,int k){
    if(!s) s = ++tot; int mid = (l + r) / 2;
    if(l == r) return t[s] += k,void();
    if(x <= mid) upd(ls[s],l,mid,x,k); else upd(rs[s],mid+1,r,x,k);
    t[s] = t[ls[s]] + t[rs[s]];
}map<int,int> rt;
int qry(int s,int l,int r,int ql,int qr){
    if(ql <= l && r <= qr) return t[s];
    int mid = (l + r) / 2,ans = 0;
    if(ql <= mid) ans += qry(ls[s],l,mid,ql,qr); if(qr > mid) ans += qry(rs[s],mid+1,r,ql,qr);
    return ans;
}void los(){
    cin >> n;
    for(int i = 1;i <= n;i ++){
        cin >> op[i] >> ti[i] >> a[i];
        if(op[i] == 1) upd(rt[a[i]],1,1e9,ti[i],1);
        if(op[i] == 2) upd(rt[a[i]],1,1e9,ti[i],-1);
        if(op[i] == 3) cout << qry(rt[a[i]],1,1e9,1,ti[i]) << "\n";
    }
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(59)\) GYM102904B Dispatch Money

难度 \(^*2700\)动态规划,DP;决策单调性;CDQ 分治;树状数组

题意即把排列划分成若干段,设划分了 \(k\) 段,则代价为 \(kx + \sum \limits_{i=1}^k \operatorname{inv}(a[l_i,r_i])\)\(\operatorname{inv}\) 是逆序对数。有最简单的 dp:令 \(f_i\) 表示以 \(i\) 结尾的最小代价,则 \(f_i = \min \limits_{j \le i} f_{j-1} + \operatorname{inv}(a[j,i])\),时间复杂度 \(\mathcal O(n^2 \log n)\),不能通过。

我们猜测:\(f\) 的转移有决策单调性。通常我们可以分治法解决策单调性 dp:先求 \(f_{mid}\) 的值,得到转移点位置,然后分治两边。但在本题中,转移有依赖关系,我们需要得到 \(f_1 \sim f_{mid-1}\) 的值才可以得到 \(f_{mid}\) 的值,考虑 CDQ 分治,强制中序遍历递归树。内层分治时,我们不能反复 \(\mathcal O(l \log l)\) 求区间逆序对,考虑类似莫队的方法维护区间得到答案。CDQ、内层分治和树状数组一共是 \(\log^3\),总复杂度 \(\mathcal O(n \log^3 n)\)。理论上是过不了 \(n = 3 \times 10^5\) 的,但是因为三个东西常数都很小,就冲过去了。

代码
/**
*    author: sunkuangzheng
*    created: 27.01.2024 14:35:39
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,t[N],re,a[N],x,L,R; ll f[N],ans;
void upd(int x,int p){for(;x <= n;x += x & -x) t[x] += p;}
int qry(int x){for(re = 0;x;x -= x & -x) re += t[x];return re;}
void addr(int x){assert(a[x] > 0),ans += qry(n) - qry(a[x]),upd(a[x],1);}
void delr(int x){assert(a[x] > 0),ans -= qry(n) - qry(a[x]),upd(a[x],-1);}
void addl(int x){assert(a[x] > 0),ans += qry(a[x] - 1),upd(a[x],1);}
void dell(int x){assert(a[x] > 0),ans -= qry(a[x] - 1),upd(a[x],-1);}
void work(int l,int r,int ql,int qr){
    int mid = (ql + qr) / 2; 
    if(ql > qr) return ;
    while(L > r) addl(-- L);
    while(R < mid) addr(++ R);
    while(R > mid) delr(R --);
    while(L < r) dell(L ++);
    ll mc = 1e18,pos = r;
    for(int i = r;i >= l;i --){
        if(ans + x + f[i-1] < mc) mc = ans + x + f[i-1],pos = i;
        if(i != l) addl(-- L);
    }f[mid] = min(f[mid],mc);
    work(l,pos,ql,mid-1),work(pos,r,mid+1,qr);
}void cdq(int l,int r){
    int mid = (l + r) / 2; if(l == r) return ;
    cdq(l,mid),work(l,mid+1,mid+1,r),cdq(mid+1,r);
}void los(){
    cin >> n >> x; L = 1;
    for(int i = 1;i <= n;i ++) cin >> a[i],f[i] = 1ll * i * x;
    cdq(1,n),cout << f[n]; 
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(60)\) CF868F Yet Another Minimization Problem

难度 \(^*2500\)动态规划,DP;决策单调性;

直接猜一下 dp 有决策单调性,分治就做完了。时间复杂度 \(\mathcal O(kn \log n)\)

代码
/**
*    author: sunkuangzheng
*    created: 05.02.2024 14:59:23
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,ct[N],a[N],k,L = 1,R;ll ans,g[N],f[N];
void add(int x){ans += ct[x] ++;}
void del(int x){ans -= -- ct[x];}
void sol(int l,int r,int ql,int qr){
    if(l > r) return ;
    int mid = (l + r) / 2;
    while(R < mid) add(a[++R]);
    while(L > min(qr,mid)) add(a[--L]);
    while(R > mid) del(a[R--]);
    while(L < min(qr,mid)) del(a[L++]);
    int p = -1; ll res = 1e18;
    for(;L >= ql;add(a[-- L]))
        if(ans + g[L - 1] < res) res = ans + g[L - 1],p = L;
    f[mid] = res; sol(l,mid-1,ql,p+1),sol(mid+1,r,p-1,qr);
}void los(){
    cin >> n >> k;
    for(int i = 1;i <= n;i ++) cin >> a[i],g[i] = f[i] = 1e18;
    while(k --){
        L = 1,R = 0,memset(ct,0,sizeof(ct)),ans = 0,sol(1,n,1,n);
        for(int i = 1;i <= n;i ++) g[i] = f[i];
    }cout << f[n];
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(61)\) P5574 [CmdOI2019] 任务分配问题

难度 \(^*2500\)动态规划,DP;决策单调性;树状数组

上两道题的结合。时间复杂度 \(\mathcal O(nk \log^2 n)\)

代码
/**
*    author: sunkuangzheng
*    created: 05.02.2024 14:59:23
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,a[N],k,L = 1,t[N],R,re;ll ans,g[N],f[N];
void upd(int x,int p){for(;x <= n;x += x & -x) t[x] += p;}
int qry(int x){for(re = 0;x;x -= x & -x) re += t[x];return re;}
void addr(int x){ans += qry(n) - qry(a[x]),upd(a[x],1);}
void delr(int x){ans -= qry(n) - qry(a[x]),upd(a[x],-1);}
void addl(int x){if(x) ans += qry(a[x] - 1),upd(a[x],1);}
void dell(int x){if(x) ans -= qry(a[x] - 1),upd(a[x],-1);}
void sol(int l,int r,int ql,int qr){
    if(l > r) return ;
    int mid = (l + r) / 2;
    while(R < mid) addr(++R);
    while(L > min(qr,mid)) addl(--L);
    while(R > mid) delr(R--);
    while(L < min(qr,mid)) dell(L++);
    int p = -1; ll res = 1e18;
    for(;L >= ql;addl(--L))
        if(ans + g[L - 1] < res) res = ans + g[L - 1],p = L;
    f[mid] = res; sol(l,mid-1,ql,p+1),sol(mid+1,r,p,qr);
}void los(){
    cin >> n >> k;
    for(int i = 1;i <= n;i ++) cin >> a[i],a[i] = n - a[i] + 1,g[i] = f[i] = 1e18;
    while(k --){
        L = 1,R = 0,memset(t,0,sizeof(t)),ans = 0,sol(1,n,1,n);
        for(int i = 1;i <= n;i ++) g[i] = f[i];
    }cout << f[n];
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(62)\) P3515 [POI2011] Lightning Conductor

难度 \(^*2400\)决策单调性

猜测对于 \(i\) 最大值限制的位置 \(j\) 有决策单调性,分治即可。\(\mathcal O(n \log n)\)

注意精度问题,提前取整可能会影响转移点的位置使得答案出错。

代码
/**
*    author: sunkuangzheng
*    created: 05.02.2024 16:17:43
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long double;
const int N = 5e5+5;
using namespace std;
ll st(int x){return (sqrtl(x));}
int T,n,a[N]; ll f[N];
void sol(int l,int r,int ql,int qr){
    if(l > r) return ; int mid = (l + r) / 2; int p = -1; ll res = -1; 
    for(int i = ql;i <= min(mid,qr);i ++) if(ll d = a[i] + st(abs(mid - i));d > res) res = d,p = i;
    f[mid] = max(f[mid],res),sol(l,mid-1,ql,p),sol(mid+1,r,p,qr);
}void los(){
    cin >> n;
    for(int i = 1;i <= n;i ++) cin >> a[i];
    sol(1,n,1,n),reverse(a+1,a+n+1),reverse(f+1,f+n+1),sol(1,n,1,n);
    for(int i = n;i >= 1;i --) cout << (int)(ceil(f[i])) - a[i] << "\n";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(63)\) CF1603D Artistic Partition

难度 \(^*3000\)动态规划,DP;决策单调性;数论;整除分块

考虑朴素 dp:设 \(f_{i,j}\) 表示前 \(i\) 个数字分 \(j\) 段的最小代价,显然有转移 \(f_{i,j} = \min f_{k,j-1} + c(k+1,i)\)。朴素实现 \(c(l,r)\)\(\mathcal O(n^2 \log n)\) 的,那么总时间复杂度是 \(\mathcal O(n^4k \log n)\),是 \(\mathcal O(n^5 \log n)\) 级别的,十分优秀。

考虑优化。

  • 优化 \(1\quad\)\(2^k > n\) 时,答案一定可以取到下界 \(n\)

构造很简单,直接令 \(x_i = 2^{i},x_{i+1} = 2^{i+1}\) 即可,显然区间内不存在 \(i,j(i \ne j)\) 使得 \(\gcd(i,j) \ge l\)

我们把 \(k\) 降到了 \(\mathcal O(\log n)\) 级别。

  • 优化 \(2 \quad\) \(f\) 的转移有决策单调性。

直接猜结论即可,证明参考官方题解。

可以分治解决,时间复杂度优化至 \(\mathcal O(n^3 \log^3 n)\)

  • 优化 \(3 \quad\) 快速计算 \(c(l,r)\)

这一部分大力推式子,除法默认下取整。

\[\begin{aligned}&\sum \limits_{i = l}^r \sum \limits_{j=i}^r [\gcd(i,j) \ge l]\\=&\sum\limits_{d=l}^r\sum \limits_{i = l}^r \sum \limits_{j=i}^r [\gcd(i,j) = d]\\=&\sum\limits_{d=l}^r\sum \limits_{i = 1}^{r/d} \sum \limits_{j=i}^{r/d} [\gcd(i,j) = 1]\\=&\sum\limits_{d=l}^r\sum \limits_{j = 1}^{r/d} \sum \limits_{i=1}^{j} [\gcd(i,j) = 1]\\=&\sum\limits_{d=l}^r\sum \limits_{i = 1}^{r/d} \varphi(i)\end{aligned} \]

预处理 \(\varphi(i)\) 的前缀和,整除分块,这一部分的时间复杂度 \(\mathcal O(\sqrt n)\)

至此,总时间复杂度 \(\mathcal O(n \log^2 n \sqrt n)\),由于常数特别小,可以直接通过。

代码
/**
*    author: sunkuangzheng
*    created: 06.02.2024 14:35:23
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 1e5+5;
using namespace std;
int T,n,k,vis[N];vector<int> p; ll phi[N],f[N][22];
ll c(int l,int r){
    ll ans = 0; int j = 0; 
    for(int i = l;i <= r;i = j + 1) j = r / (r / i),ans += 1ll * (j - i + 1) * phi[r / i];
    return ans;
}void sol(int l,int r,int ql,int qr,int d){
    if(l > r) return ;
    ll res = 1e18; int p = -1,mid = (l + r) / 2;
    for(int i = ql;i <= min(qr,mid);i ++) if(ll x = f[i - 1][d - 1] + c(i,mid);x < res) res = x,p = i;
    f[mid][d] = res,sol(l,mid-1,ql,p+1,d),sol(mid+1,r,p,qr,d);
}void los(){
    cin >> n >> k;
    if(k >= 20 || (1 << k) > n) cout << n << "\n";
    else cout << f[n][k] << "\n";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0),phi[1] = 1;
    for(int i = 2;i < N;i ++){
        if(!vis[i]) p.push_back(i),phi[i] = i - 1;
        for(int j : p){
            if(i * j >= N) break;
            phi[i * j] = phi[i] * phi[j],vis[i * j] = 1;
            if(i % j == 0) {phi[i * j] = phi[i] * j;break;}
        }
    }for(int i = 1;i < N;i ++) phi[i] += phi[i - 1];
    for(int i = 1;i < N;i ++) f[i][0] = 1e14;
    for(int i = 1;i <= 19;i ++) sol(i,N-1,1,N-1,i);
    for(cin >> T;T --;) los();
}

\(\color{blue}(64)\) P1912 [NOI2009] 诗人小G

难度 \(^*2600\)动态规划,DP;决策单调性;CDQ 分治

有显然的 \(\mathcal O(n^2)\) DP,可以猜测有决策单调性,但是我们不能直接分治,因为求 \(f_{mid}\) 的值时需要先求出 \(f_j(j < mid)\) 的值。这种情况有一种常用的解决办法:套 CDQ 分治。具体的,先序遍历分治树,先 CDQ 完 \([l,mid]\),然后对 \([mid+1,r]\)\(f\) 尝试从 \([l,mid]\) 转移,这里可以分治。

复杂度 \(\mathcal O(T Pn \log^2 n)\),然后这题卡精度,极其恶心。

代码
/**
*    author: sunkuangzheng
*    created: 06.02.2024 16:20:22
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
using tril = long double;
const int N = 1e5+5;
using namespace std;
int T,n,g[N],sm[N],L,P; tril f[N]; string s[N];
tril pow(int a,int b){
    tril p = 1;
    while(b --) p *= a;
    return p;
}void sol(int l,int r,int ql,int qr){
    if(l > r) return ; int mid = (l + r) / 2,p = -1;
    tril res = 9e70; 
    for(int i = ql;i <= qr;i ++) 
        if(tril d = pow(abs(sm[mid] - sm[i] - L + (mid - i - 1)),P) + f[i];d < res) 
        res = d,p = i;
    if(res < f[mid]) f[mid] = res,g[mid] = p; sol(l,mid-1,ql,p),sol(mid+1,r,p,qr);
}void cdq(int l,int r){
    if(l == r) return ; int mid = (l + r) / 2;
    cdq(l,mid),sol(mid+1,r,l,mid),cdq(mid+1,r);
}void los(){
    cin >> n >> L >> P;
    for(int i = 1;i <= n;i ++) f[i] = 4e70,cin >> s[i],sm[i] = sm[i-1] + s[i].size();
    cdq(0,n);
    if(f[n] <= 1e18){
        cout << (ll)f[n] << "\n";
        vector<string> ans;
        int p = n;
        while(p){
            string t;
            for(int k = g[p] + 1;k <= p;k ++) t += s[k] + " ";
            t.pop_back(),ans.push_back(t),p = g[p];
        }reverse(ans.begin(),ans.end());
        for(auto s : ans) cout << s << "\n";
    }else cout << "Too hard to arrange\n";
    cout << "--------------------\n";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(cin >> T;T --;) los();
}

\(\color{blue}(65)\) AT_joisc2019_j Cake 3

难度 \(^*2700\)可持久化线段树;动态规划,DP;决策单调性;

首先有显然的 \(\mathcal O(n^2m)\) DP。考虑按照 \(c_i\) 从小到大排序,按顺序排列 \(k_i\),则后面的式子等价于 \(2(c_{k_m} - c_{k_1})\),不难证明这是最优排列方式。也就是说,后面式子的值只和 \(k_1,k_m\) 的值有关。不难想到枚举 \(i,j\),用主席树维护区间前 \(k\) 大的和,计算答案,时间复杂度 \(\mathcal O(n^2 \log n)\)

猜测这个转移有决策单调性,分治优化,时间复杂度 \(\mathcal O(n \log^2 n)\)

注意答案可能为负数,存答案的初始值要开到 \(-\inf\)

代码
/**
 *    author: sunkuangzheng
 *    created: 06.02.2024 21:49:58
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 2e5+5;
using namespace std;
int T,n,rt[N],tot,k; ll ans = -1e18;
struct tree{ll x; int l,r,k;}t[N*40];
struct acc{int v,c;}a[N];
int upd(int s,int l,int r,int x){
    int p = ++tot; t[p] = t[s],t[p].x += x,t[p].k ++;
    if(l == r) return p; int mid = (l + r) / 2;
    if(x <= mid) t[p].l = upd(t[p].l,l,mid,x); else t[p].r = upd(t[p].r,mid+1,r,x);
    return p;
}ll qry(int u,int v,int l,int r,int k){
    if(l == r) return 1ll * k * l;
    int mid = (l + r) / 2; ll tmp = t[t[v].r].k - t[t[u].r].k;
    if(k <= tmp) return qry(t[u].r,t[v].r,mid+1,r,k);
    return t[t[v].r].x - t[t[u].r].x + qry(t[u].l,t[v].l,l,mid,k-tmp); 
}void sol(int l,int r,int ql,int qr){
    if(l > r) return ;
    ll tmp = -1e18; int p = -1,mid = (l + r) / 2; 
    for(int i = ql;i <= min(qr,mid - k + 1);i ++) 
        if(ll d = qry(rt[i],rt[mid-1],1,1e9,k-2) + 2 * a[i].c + a[i].v;d > tmp) tmp = d,p = i;
    ans = max(ans,tmp - 2 * a[mid].c + a[mid].v),sol(l,mid-1,ql,p),sol(mid+1,r,p,qr);
}void los(){
    cin >> n >> k;
    for(int i = 1;i <= n;i ++) cin >> a[i].v >> a[i].c;
    sort(a+1,a+n+1,[&](acc a,acc b){return a.c < b.c;});
    for(int i = 1;i <= n;i ++) rt[i] = upd(rt[i-1],1,1e9,a[i].v);
    sol(k,n,1,n),cout << ans;
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(66)\) P5892 [IOI2014] holiday

难度 \(^*2600\)可持久化线段树;动态规划,DP;决策单调性;

下称起点为 \(m\),考虑枚举 \(i,j\) 表示当前走的区间是 \([i,j]\),则花费在行走上的时间是 \(\min(2(m-i)+j-m,2(j-m)+m-i)\),剩余时间贪心的取前 \(k\) 大,时间复杂度 \(\mathcal O(n^2 \log n)\)

和上一题类似的,\(j\) 的转移有决策单调性,分治即可。时间复杂度 \(\mathcal O(n \log^2 n)\)

代码
/**
*    author: sunkuangzheng
*    created: 07.02.2024 08:37:16
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 1e5+5;
using namespace std;
int T,n,m,d,a[N],rt[N],tot,b[N],le;ll ans;
struct tree{ll x;int l,r,k;}t[N*40];
int upd(int s,int l,int r,int x){
    int p = ++tot,mid = (l + r) / 2; t[p] = t[s],t[p].x += b[x],t[p].k ++;
    if(l == r) return p;
    if(x <= mid) t[p].l = upd(t[p].l,l,mid,x); else t[p].r = upd(t[p].r,mid+1,r,x);
    return p;
}ll qry(int u,int v,int l,int r,int k){
    if(k >= t[v].k - t[u].k) return t[v].x - t[u].x;
    if(k <= 0) return 0;
    if(l == r) return 1ll * k * b[l];
    int mid = (l + r) / 2,tmp = t[t[v].r].k - t[t[u].r].k;
    if(k <= tmp) return qry(t[u].r,t[v].r,mid+1,r,k);
    else return t[t[v].r].x - t[t[u].r].x + qry(t[u].l,t[v].l,l,mid,k-tmp);
}void sol(int l,int r,int ql,int qr){
    if(l > r) return ;
    int mid = (l + r) / 2,p = -1; ll res = -1;
    for(int i = ql;i <= qr;i ++){
        ll k = min(mid - m + (m - i) * 2,(mid - m) * 2 + m - i),x = qry(rt[i-1],rt[mid],1,le,d - k);
        if(x > res) res = x,p = i;
    }ans = max(ans,res),sol(l,mid-1,ql,p),sol(mid+1,r,p,qr);
}void los(){
    cin >> n >> m >> d,m ++;
    for(int i = 1;i <= n;i ++) cin >> a[i],b[i] = a[i];
    sort(b+1,b+n+1),le = unique(b+1,b+n+1) - b - 1;
    for(int i = 1;i <= n;i ++) rt[i] = upd(rt[i-1],1,le,lower_bound(b+1,b+le+1,a[i]) - b);
    sol(m,n,1,m),cout << ans; 
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(67)\) GYM102059B Dev, Please Add This!

难度 \(^*2700\)图论建模;缩点;2-SAT

容易想到图论建模,用有向图刻画点对的可达关系,但是如果直接连边,可能存在球从中间转弯的情况。我们的解决办法是拆点,把每个点拆成横竖两个,如果附近有障碍或边界,则在横竖之间连边,然后可以得到一张有向图。为了方便,我们将图缩点变成 DAG。容易发现每个点有走或者不走两种状态,且走了一个点会使得一些点不能走,联想到 2-SAT。具体的,考虑对于两个点 \(u,v\),如果 \(u\) 不能到 \(v\)\(v\) 不能到 \(u\),则一定不能同时经过这两个点。这里注意不能只判一个条件,因为这里 2-SAT 刻画的是能否同时经过,而不是先后经过。相应的,对于一个 \(\texttt{*}\),它最多在两个强连通分量中(一个横一个竖),如果不经过一个就一定要经过另一个,也可以用 2-SAT 描述。对于起点必须是 \(\texttt{O}\) 的限制,因为 2-SAT 这里表示同时经过,不表示先后关系,所以我们加一个超级源点 \(s\)\(\texttt{O}\) 连边,再强制经过 \(s\) 即可。总时间复杂度 \(\mathcal O(n^2m^2)\)

代码
/**
*    author: sunkuangzheng
*    created: 07.02.2024 10:16:29
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 10005,M = 55;
using namespace std;
int T,n,m,dfn[N],low[N],vis[N],st[N],tot,otot,tp,ti,bx,by,be[N],dx[5] = {0,0,1,0,-1},dy[5] = {0,1,0,-1,0}; 
vector<int> g[N],rg[N],pos; string s[N]; bitset<N> f[N];
void tarjan(int u){
    dfn[u] = low[u] = ++ti,st[++tp] = u,vis[u] = 1;
    for(int v : g[u]) if(!dfn[v]) tarjan(v),low[u] = min(low[u],low[v]); 
                    else if(vis[v]) low[u] = min(low[u],dfn[v]);
    if(low[u] == dfn[u]) for(++tot;;) if(be[st[tp]] = tot,vis[st[tp]] = 0,st[tp --] == u) break;    
}void los(){
    cin >> n >> m; int S = n * m * 2 + 1;
    auto id = [&](int x,int y,int k){return (x - 1) * m + y + n * m * k;};
    auto add = [&](int u,int v){return g[u].push_back(v);};
    for(int i = 1;i <= n;i ++) cin >> s[i],s[i] = " " + s[i] + " "; //0 横 1 竖
    for(int i = 1;i <= n;i ++) for(int j = 1;j <= m;j ++){
        if(s[i][j] == '#') continue; if(s[i][j] == 'O') bx = i,by = j; if(s[i][j] == '*') pos.push_back(id(i,j,0));
        if((i == 1 || s[i-1][j] == '#') || (i == n || s[i+1][j] == '#')) add(id(i,j,0),id(i,j,1));
        if((j == 1 || s[i][j-1] == '#') || (j == m || s[i][j+1] == '#')) add(id(i,j,1),id(i,j,0));
        for(int k = 1;k <= 4;k ++) 
            if(int ax = i + dx[k],ay = j + dy[k]; ax >= 1 && ax <= n && ay >= 1 && ay <= m && s[ax][ay] != '#')
                add(id(i,j,k & 1),id(ax,ay,k & 1));
    }add(S,id(bx,by,0)),add(S,id(bx,by,1));
    for(int i = 1;i <= S;i ++) if(!dfn[i]) tarjan(i);
    for(int i = 1;i <= S;i ++) dfn[i] = low[i] = vis[i] = 0,rg[i] = g[i],g[i].clear();
    ti = tp = 0; queue<int> q; otot = tot,tot = 0;
    for(int i = 1;i <= S;i ++) for(int j : rg[i]) if(be[i] != be[j]) add(be[i],be[j]);
    for(int i = 1;i <= otot;f[i].set(i),g[i ++].clear()) for(int j : g[i]) f[i] |= f[j];
    for(int i = 1;i <= otot;i ++) for(int j = 1;j <= otot;j ++) if(!f[i][j] && !f[j][i]) add(i,j + otot),add(j,i + otot);
    for(int i : pos) add(be[i] + otot,be[i + n * m]),add(be[i + n * m] + otot,be[i]);
    add(be[S] + otot,be[S]);
    for(int i = 1;i <= 2 * otot;i ++) if(!dfn[i]) tarjan(i);
    for(int i = 1;i <= otot;i ++) if(be[i] == be[i + otot]) return cout << "NO",void();
    cout << "YES";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(68)\) P4747 [CERC2017] Intrinsic Interval

难度 \(^*3000\)线段树;扫描线

性质题。有一个显然的性质,如果存在两个有交区间都是好的,那么它们的交也是好的,原因是排列中每个元素只出现一次,那么两个好区间的交一定连续。由此推论,对于询问区间 \([l,r]\),我们如果从小到大枚举 \(R\) 并找到最大的 \(L\) 满足 \([L,R]\) 是好区间,当找到第一对 \([L,R]\) 时就得到了最优解。

这一过程很容易想到扫描线维护。扫右端点 \(R\),把扫过的询问按照 \(l\) 从大到小排序扔进堆里。找到最大的 \(L\) 满足 \(L \le l\)\([L,R]\) 合法。如果找到了,就删除这个询问继续处理。如果找不到,则更小的 \(l\) 也无解,直接扫 \(R+1\)

容易把区间 \([l,r]\) 合法转化成 \(\max\limits_{i=l}^r\{a_i\} - \min \limits_{i=l}^r \{a_i\} = r - l\),我们可以 \(\mathcal O(1)\) 判断区间是否合法,但这很难维护 \(L \le l\)\([L,R]\) 合法的 \(L\)。我们发现,对于合法区间 \([L,R]\),恰有 \(R-L\)\((x,x+1)\) 满足 \(x,x + 1\) 都出现在区间 \([L,R]\)。因此,我们用线段树维护 \(L\) 的信息。当扫到 \(R\) 时,如果 \(a_R-1,a_R+1\) 出现在 \(R\) 左边,把 \([1,pos_{a_R-1}],[1,pos_{a_R+1}]\) 加一表示左端点多了一组贡献,则合法的 \(L\) 要满足 \(L\) 在线段树上的值大于等于 \(R - L\)。这维护起来还是麻烦!我们不妨给每个位置直接加上 \(L\),这样只需要看它的值是否等于 \(R\)。判断是否合法时,查一下 \([1,l]\) 的区间 \(\max\) 即它的位置即可。时间复杂度 \(\mathcal O(n \log n)\)

代码
/**
*    author: sunkuangzheng
*    created: 07.02.2024 14:23:48
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 1e5+5;
using namespace std;
int T,n,a[N],t[N*4],p[N],tp[N*4],tg[N*4],l,r,m; vector<pair<int,int>> g[N],ans;
void cg(int s,int k){t[s] += k,tg[s] += k;}
void pd(int s){cg(s*2,tg[s]),cg(s*2+1,tg[s]),tg[s] = 0;}
void build(int s,int l,int r){
    if(l == r) return t[s] = l,tp[s] = l,void();
    int mid = (l + r) / 2; build(s*2,l,mid),build(s*2+1,mid+1,r);
    t[s] = max(t[s*2],t[s*2+1]),tp[s] = (t[s*2] > t[s*2+1] ? tp[s*2] : tp[s*2+1]);
}void upd(int s,int l,int r,int ql,int qr){
    if(ql <= l && r <= qr) return cg(s,1);
    int mid = (l + r) / 2; pd(s);
    if(ql <= mid) upd(s*2,l,mid,ql,qr); if(qr > mid) upd(s*2+1,mid+1,r,ql,qr);
    t[s] = max(t[s*2],t[s*2+1]),tp[s] = (t[s*2] > t[s*2+1] ? tp[s*2] : tp[s*2+1]);
}int qry(int s,int l,int r,int ql,int qr,int k){
    if(ql <= l && r <= qr) return (t[s] == k ? tp[s] : -1);
    int mid = (l + r) / 2,ans = -1; pd(s);
    if(ql <= mid) ans = max(ans,qry(s*2,l,mid,ql,qr,k));
    if(qr > mid) ans = max(ans,qry(s*2+1,mid+1,r,ql,qr,k));
    return ans;
}void los(){
    cin >> n;
    for(int i = 1;i <= n;i ++) cin >> a[i],p[a[i]] = i;
    cin >> m,ans.resize(m);
    for(int i = 1;i <= m;i ++) cin >> l >> r,g[r].emplace_back(l,i);
    build(1,1,n); priority_queue<pair<int,int>> q;
    for(int i = 1;i <= n;i ++){
        if(a[i] > 1 && p[a[i] - 1] < i) upd(1,1,n,1,p[a[i] - 1]);
        if(a[i] < n && p[a[i] + 1] < i) upd(1,1,n,1,p[a[i] + 1]);
        for(auto x : g[i]) q.emplace(x);
        for(;q.size();){
            auto [l,id] = q.top(); int d = qry(1,1,n,1,l,i); 
            if(d != -1) ans[id - 1] = {d,i},q.pop(); else break;
        }
    }for(auto [x,y] : ans) cout << x << " " << y << "\n";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(69)\) CF526F Pudding Monsters

难度 \(^*2800\)线段树;扫描线

这道题就是上面那题的改版,只是要求有多少合法区间。继续扫描线,相当于每次到 \(R\) 时求有多少个 \(L\) 满足值是 \(R\)。我们发现 \(R\) 的数量不好维护,类似 \(\color{blue}(33)\) 的套路,发现 \(R\) 一定是区间 \(\max\),那么转化为维护区间 \(\max\) 的数量,线段树即可。时间复杂度 \(\mathcal O(n \log n)\)

代码实现上我每次扫 \(R\) 就全局 \(-1\),这样只需要判 \(\max\) 是否为 \(0\)

代码
/**
*    author: sunkuangzheng
*    created: 07.02.2024 15:18:52
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,a[N],x,y,p[N],t[N*4],tg[N*4],ct[N*4];
void cg(int s,int k){t[s] += k,tg[s] += k;}
void pd(int s){cg(s*2,tg[s]),cg(s*2+1,tg[s]),tg[s] = 0;}
void pp(int s){t[s] = max(t[s*2],t[s*2+1]),ct[s] = ct[s*2] * (t[s*2] == t[s]) + ct[s*2+1] * (t[s*2+1] == t[s]);}
void upd(int s,int l,int r,int ql,int qr,int k){
    if(ql <= l && r <= qr) return cg(s,k);
    int mid = (l + r) / 2; pd(s);
    if(ql <= mid) upd(s*2,l,mid,ql,qr,k); if(qr > mid) upd(s*2+1,mid+1,r,ql,qr,k);
    pp(s);
}int qry(int s,int l,int r,int ql,int qr){
    if(ql <= l && r <= qr) return (t[s] == 0) * ct[s];
    int mid = (l + r) / 2,ans = 0; pd(s);
    if(ql <= mid) ans += qry(s*2,l,mid,ql,qr);
    if(qr > mid) ans += qry(s*2+1,mid+1,r,ql,qr);
    return ans;
}void build(int s,int l,int r){
    if(l == r) return t[s] = l,ct[s] = 1,void();
    int mid = (l + r) / 2; build(s*2,l,mid),build(s*2+1,mid+1,r),pp(s);
}void los(){
    cin >> n;ll ans = 0;
    for(int i = 1;i <= n;i ++) cin >> x >> y,a[x] = y,p[y] = x;
    build(1,1,n);
    for(int i = 1;i <= n;i ++){
        upd(1,1,n,1,n,-1);
        if(a[i] > 1 && p[a[i] - 1] < i) upd(1,1,n,1,p[a[i] - 1],1);
        if(a[i] < n && p[a[i] + 1] < i) upd(1,1,n,1,p[a[i] + 1],1);
        ans += qry(1,1,n,1,i);
    }cout << ans;
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(70)\) CF997E Good Subsegments

难度 \(^*3000\)线段树;扫描线

这道题是上一题的区间版本。仍然是扫描线,对于一组询问 \([l,r]\),我们其实是要求线段树上 \([l,r]\) 这段区间内 \(\max\) 数量的前缀和,我们一般称为历史版本和。线段树维护信息时把它维护上即可,复杂度仍然是 \(\mathcal O(n \log n)\)。有一些实现细节。

代码
/**
*    author: sunkuangzheng
*    created: 07.02.2024 15:18:52
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 2e5+5;
using namespace std;
int T,n,a[N],x,y,p[N],t[N*4],m,tg[N*4],ct[N*4],l,r,num[N*4]; ll ans[N*4],res[N]; 
vector<pair<int,int>> g[N];
void cg(int s,int k){t[s] += k,tg[s] += k;}
void cgg(int s,int k){ans[s] += 1ll * ct[s] * k,num[s] += k;}
void pd(int s){
    cg(s*2,tg[s]),cg(s*2+1,tg[s]),tg[s] = 0;
    cgg(s*2,num[s] * (t[s] == t[s*2])),cgg(s*2+1,num[s] * (t[s] == t[s*2+1])),num[s] = 0;
}void pp(int s){
    t[s] = max(t[s*2],t[s*2+1]),ans[s] = ans[s * 2] + ans[s * 2 + 1];
    ct[s] = ct[s*2] * (t[s*2] == t[s]) + ct[s*2+1] * (t[s*2+1] == t[s]);
}void upd(int s,int l,int r,int ql,int qr,int k){
    if(ql <= l && r <= qr) return cg(s,k);
    int mid = (l + r) / 2; pd(s);
    if(ql <= mid) upd(s*2,l,mid,ql,qr,k); if(qr > mid) upd(s*2+1,mid+1,r,ql,qr,k);
    pp(s);
}ll qry(int s,int l,int r,int ql,int qr){
    if(ql <= l && r <= qr) return ans[s];
    int mid = (l + r) / 2; ll ans = 0; pd(s);
    if(ql <= mid) ans += qry(s*2,l,mid,ql,qr);
    if(qr > mid) ans += qry(s*2+1,mid+1,r,ql,qr);
    return ans;
}void build(int s,int l,int r){
    if(l == r) return t[s] = -1e9,ct[s] = 1,void();
    int mid = (l + r) / 2; build(s*2,l,mid),build(s*2+1,mid+1,r),pp(s);
}void los(){
    cin >> n;
    for(int i = 1;i <= n;i ++) cin >> a[i],p[a[i]] = i;
    build(1,1,n),cin >> m;
    for(int i = 1;i <= m;i ++) cin >> l >> r,g[r].emplace_back(l,i);
    for(int i = 1;i <= n;i ++){
        upd(1,1,n,i,i,1e9 + i),upd(1,1,n,1,n,-1);
        if(a[i] > 1 && p[a[i] - 1] < i) upd(1,1,n,1,p[a[i] - 1],1);
        if(a[i] < n && p[a[i] + 1] < i) upd(1,1,n,1,p[a[i] + 1],1);
        cgg(1,1);
        for(auto [l,id] : g[i]) res[id] = qry(1,1,n,l,i);
    }for(int i = 1;i <= m;i ++) cout << res[i] << "\n";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(71)\) CF1928E Modular Sequence

难度 \(^*1900\)构造;动态规划,DP

我的题解

\(\color{blue}(72)\) ABC280Ex Substring Sort

难度 \(^*3000\)字符串;后缀数组,SA;单调栈;二分

我的题解

\(\color{blue}(73)\) ABC340G Leaf Color

难度 \(^*2600\)虚树;动态规划,DP

发现颜色之间关系不大,考虑对于每种颜色 DP。设当前颜色是 \(k\)\(f_u\) 表示以 \(u\) 为根的子树且选 \(u\) 的方案数,有朴素转移 \(f_u = \prod\limits_{v \in son_u} (f_v + 1)\) 表示儿子选或者不选。注意如果 \(a_u \ne k\)\(f_u\) 需要 \(-1\) 表示不能一个儿子都不选。考虑如何统计答案,答案显然不是 \(f_1\) 因为我们强制选根。对于 \(a_u = k\)\(u\),贡献直接就是 \(f_u\)。否则,因为度数为 \(1\) 的点包括根,要减去只选一个子树的方案数,即 \(\sum \limits_{v \in son_u} f_v\)。时间复杂度 \(\mathcal O(n^2)\)

发现只考虑颜色为 \(k\) 的点是可以的,但是 LCA 我们也需要保留以转移,很自然想到虚树。对每种颜色建虚树即可。复杂度 \(\mathcal O(n \log n)\)

代码
/**
*    author: sunkuangzheng
*    created: 13.02.2024 08:07:00
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5,mod = 998244353;
using namespace std;
int T,n,dfn[N],st[20][N],tot,u,v,a[N],f[N]; vector<int> g[N],p[N];
void dfs(int u,int f){
    st[0][dfn[u] = ++tot] = f;
    for(int v : g[u]) if(v != f) dfs(v,u);
}void los(){
    cin >> n;
    for(int i = 1;i <= n;i ++) cin >> a[i],p[a[i]].push_back(i);
    for(int i = 1;i < n;i ++) cin >> u >> v,g[u].push_back(v),g[v].push_back(u);
    dfs(1,0);
    auto cmp = [&](int u,int v){return dfn[u] < dfn[v] ? u : v;};
    for(int j = 1;j <= __lg(n);j ++) for(int i = 1;i + (1 << j) - 1 <= n;i ++)
        st[j][i] = cmp(st[j-1][i],st[j-1][i+(1<<j-1)]);
    auto lca = [&](int u,int v){
        if(u == v) return u;
        if((u = dfn[u]) > (v = dfn[v])) swap(u,v);
        int k = __lg(v - u);
        return cmp(st[k][u+1],st[k][v-(1<<k)+1]);
    };for(int i = 1;i <= n;i ++) g[i].clear();
    auto dcmp = [&](int x,int y){return dfn[x] < dfn[y];};
    auto sol = [&](int k){
        vector<int> d; int ans = 0;
        for(int i : p[k]) d.push_back(i); sort(d.begin(),d.end(),dcmp);
        for(int i = 1;i < p[k].size();i ++) d.push_back(lca(d[i-1],d[i]));
        sort(d.begin(),d.end(),dcmp),d.erase(unique(d.begin(),d.end()),d.end());
        for(int i = 1;i < d.size();i ++) g[lca(d[i-1],d[i])].push_back(d[i]);
        reverse(d.begin(),d.end());
        for(int u : d){
            f[u] = 1; int tmp = 0;
            for(int v : g[u]) f[u] = 1ll * f[u] * (f[v] + 1) % mod,tmp = (tmp + f[v]) % mod;
            if(a[u] != k) f[u] --; ans = (1ll * ans + f[u] - tmp * (a[u] != k) + mod) % mod;
            g[u].clear();
        }return ans;
    };int ans = 0,fk;
    for(int i = 1;i <= n;i ++) ans = (ans + sol(i)) % mod; 
    cout << ans;
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(74)\) CF19E Fairy

难度 \(^*2400\)并查集;二分图;线段树分治

暴力线段树分治即可。复杂度 \(\mathcal O(m \log m \log n)\)

代码
/**
*    author: sunkuangzheng
*    created: 13.02.2024 09:07:18
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,fa[N],siz[N],stu[N],m,u,v,stv[N],fg = 1,tp; 
vector<pair<int,int>> g[N]; vector<int> ans;
int fd(int x){return x == fa[x] ? x : fd(fa[x]);}
void mg(int u,int v){
    if(u = fd(u),v = fd(v),u == v) return ;
    if(siz[u] < siz[v]) swap(u,v);
    fa[v] = u,siz[u] += siz[v],stu[++tp] = u,stv[tp] = v;
}void upd(int s,int l,int r,int ql,int qr,int x,int y){
    if(ql > qr) return ;
    if(ql <= l && r <= qr) return g[s].emplace_back(x,y),void();
    int mid = (l + r) / 2;
    if(ql <= mid) upd(s*2,l,mid,ql,qr,x,y);
    if(qr > mid) upd(s*2+1,mid+1,r,ql,qr,x,y);
}void dfs(int s,int l,int r){
    int rg = fg,lt = tp;
    for(auto [x,y] : g[s]) mg(x,y + n),mg(x + n,y),fg &= (fd(x) != fd(x + n));
    int mid = (l + r) / 2;
    if(l == r){
        if(fg) ans.push_back(l);
    }else dfs(s*2,l,mid),dfs(s*2+1,mid+1,r);
    for(fg = rg;tp > lt;tp --) fa[stv[tp]] = stv[tp],siz[stu[tp]] -= siz[stv[tp]];
}void los(){
    cin >> n >> m; if(!m) return cout << 0,void();
    for(int i = 1;i <= 2 * n;i ++) fa[i] = i,siz[i] = 1;
    for(int i = 1;i <= m;i ++)
        cin >> u >> v,upd(1,1,m,1,i-1,u,v),upd(1,1,m,i+1,m,u,v);
    dfs(1,1,m), cout << ans.size() << "\n";
    for(int i : ans) cout << i << " ";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(75)\) P7361「JZOI-1」拜神

难度 \(^*2800\)字符串;后缀数组,SA;可持久化线段树;并查集

看完「并查集、可持久化」的 tag 就会了,怎么回事呢。

显然答案有单调性,二分答案,考虑暴力 check。二分答案 \(mid\),暴力枚举 \(i,j \in [l,mid]\),判断是否有 \(i,j\) 两个后缀的 \(\operatorname{LCP} \ge mid\)。显然不能通过。

考虑并查集维护 \(h\) 数组,从大到小合并,则一个串 \(s\) 合法,等价于并查集合并到 \(|s|\) 的状态时,\([l,r - |s| + 1]\) 区间内存在另一个后缀使得它们在同一个集合中。记录下 \(lst\) 表示上一个和它同一个集合的位置,则二分答案的 check 只需要查 \([l,r - mid + 1]\)\(mid\) 状态时 \(lst\)\(\max\)。启发式合并维护并查集,set 加 可持久化线段树 维护 \(lst\) 即可。时间复杂度 \(\mathcal O((n+q) \log^2 n)\)

代码
/**
*    author: sunkuangzheng
*    created: 13.02.2024 10:44:10
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e4+5;
using namespace std;
int T,n,q,sa[N],rk[N],ok[N],h[N],fa[N],rt[N],tot,l,r,k,lst[N],p,siz[N],pa[N]; string s; set<int> st[N];
struct seg{int l,r,w;}t[N*520]; vector<int> g[N];
int upd(int s,int l,int r,int x,int k){
    if(x <= 0 || x > n) return s;
    int p = ++tot,mid = (l + r) / 2; t[p] = t[s];
    if(l == r) return t[p].w = k,p;
    if(x <= mid) t[p].l = upd(t[p].l,l,mid,x,k); else t[p].r = upd(t[p].r,mid+1,r,x,k);
    return t[p].w = max(t[t[p].l].w,t[t[p].r].w),p;
}int qry(int s,int l,int r,int ql,int qr){
    if(ql <= l && r <= qr) return t[s].w;
    int mid = (l + r) / 2,ans = 0;
    if(ql <= mid) ans = max(ans,qry(t[s].l,l,mid,ql,qr));
    if(qr > mid) ans = max(ans,qry(t[s].r,mid+1,r,ql,qr));
    return ans;
}int fd(int x){return fa[x] == x ? x : fd(fa[x]);}
void updd(int x,int y,int d){
    if(d) {st[d].erase(x); auto it = st[d].lower_bound(x);  auto ti = it --;
    lst[*ti] = *it,rt[p] = upd(rt[p],1,n,*ti,lst[*ti]);}
    auto it = st[y].lower_bound(x),ti = it --; lst[x] = *it,lst[*ti] = x,
    rt[p] = upd(rt[p],1,n,x,lst[x]),rt[p] = upd(rt[p],1,n,*ti,lst[*ti]),st[y].insert(x);
}void mg(int x,int y){
    if(x = fd(x),y = fd(y),x == y) return ;
    if(siz[x] < siz[y]) swap(x,y);
    fa[y] = x,siz[x] += siz[y];
    for(int k : g[y]) updd(k,x,y),g[x].push_back(k); g[y].clear();
}void los(){
    cin >> n >> q >> s,s = " " + s;
    for(int i = 1;i <= n;i ++) rk[i] = s[i],sa[i] = pa[i] = i;
    for(int j = 1;j < n;j *= 2){
        for(int i = 1;i <= n;i ++) ok[i] = rk[i]; int p = 0;
        sort(sa+1,sa+n+1,[&](int x,int y){return rk[x] < rk[y] || rk[x] == rk[y] && rk[x + j] < rk[y + j];});
        auto cmp = [&](int x,int y){return ok[x] == ok[y] && ok[x + j] == ok[y + j];};
        for(int i = 1;i <= n;i ++) if(cmp(sa[i],sa[i-1])) rk[sa[i]] = p; else rk[sa[i]] = ++p; if(p == n) break;
    }for(int i = 1,k = 0;i <= n;h[rk[i ++]] = k) for(k --,k = max(k,0);s[i + k] == s[sa[rk[i] - 1] + k];k ++);
    for(int i = 1;i <= n;i ++) st[i].insert(0),st[i].insert(n + 1);
    sort(pa+1,pa+n+1,[&](int x,int y){return h[x] > h[y];}), p = n + 1;
    for(int i = 1;i <= n;i ++) updd(i,i,0),siz[i] = 1,fa[i] = i,g[i].push_back(i); int rz = 1;
    for(p = n;p >= 1;p --){
        rt[p] = rt[p + 1];
        while(rz <= n && h[pa[rz]] >= p) mg(sa[pa[rz]],sa[pa[rz] - 1]),rz ++;
    }while(q --){
        cin >> l >> r;
        int tl = 1,tr = r - l + 1,res = 0;
        while(tl <= tr){
            int mid = (tl + tr) / 2;
            if(qry(rt[mid],1,n,l,r-mid+1) >= l) tl = mid + 1,res = mid; else tr = mid - 1; 
        }cout << res << "\n";
    }
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(76 \sim 100\)

会标上个人感觉的难度和 tag(CF 评分)。CF 题目可能和原有评分不符。

会逐渐的把前面的题补上难度和 tag。

\(\color{blue}(76)\) SP4103 Extend to Palindrome

难度 \(^*2400\)字符串;后缀数组,SA

如果我们在字符串后面补了 \([1,x]^R\),那么 \([x+1,n]\) 必然回文。也就是说我们需要找到最小的 \(x\) 使得 \([x,r]\) 回文。用 SA 可以很方便的判断串是否回文。

代码
/**
*    author: sunkuangzheng
*    created: 13.02.2024 12:38:05
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,rk[N],sa[N],ok[N],h[N],m,st[20][N]; string t;
void los(){
    string s = t + '#' + string(t.rbegin(),t.rend()); s = " " + s,n = s.size() - 1,m = t.size();
    for(int i = 1;i <= n;i ++) rk[i] = s[i],sa[i] = i;
    for(int j = 1;j < n;j *= 2){
        for(int i = 1;i <= n;i ++) ok[i] = rk[i]; int p = 0;
        sort(sa+1,sa+n+1,[&](int x,int y){return rk[x] < rk[y] || rk[x] == rk[y] && rk[x + j] < rk[y + j];});
        auto cmp = [&](int x,int y){return ok[x] == ok[y] && ok[x + j] == ok[y + j];};
        for(int i = 1;i <= n;i ++) if(cmp(sa[i],sa[i-1])) rk[sa[i]] = p; else rk[sa[i]] = ++p; if(p == n) break;
    }for(int i = 1,k = 0;i <= n;h[rk[i ++]] = k) for(k --,k = max(k,0);s[i + k] == s[sa[rk[i] - 1] + k];k ++);
    for(int i = 1;i <= n;i ++) st[0][i] = h[i];
    for(int j = 1;j <= __lg(n);j ++) for(int i = 1;i + (1 << j) - 1 <= n;i ++)
        st[j][i] = min(st[j-1][i],st[j-1][i+(1<<j-1)]);
    auto lcp = [&](int i,int j){
        if(i = rk[i],j = rk[j],i > j) swap(i,j);
        int k = __lg(j - i);
        return min(st[k][i+1],st[k][j-(1<<k)+1]);
    }; cout << t;  int d = m;
    for(int i = 1;i <= m;i ++) if(lcp(i,m + 2) >= m - i + 1) {d = i - 1;break;}
    for(int i = d - 1;i >= 0;i --) cout << t[i];
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(;cin >> t;cout << "\n") los();
}

\(\color{blue}(77)\) WGOI R1 真夏飞焰

难度 \(^*3100\)字符串;后缀数组,SA;并查集

我的题解

\(\color{blue}(78)\) CF232D Fence

难度 \(^*2700\)字符串;后缀数组,SA;树状数组

这题能黑??

把那个条件差分一下,即 \(h_i - h_{i-1} = -(h_{i'} - h_{i'-1})\),不难联想到把差分数组的相反数拼在后面,每次询问变成了有多少个区间和 \([l,r]\) 相等且不交,SA + 二维数点直接做。时间复杂度 \(\mathcal O(n \log n)\)。由于我的 SA 是两只 \(\log\) 所以总复杂度稍劣。

代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
int a[N],sa[N],rk[N],h[N],ok[N],m,st[20][N],n,q,t[N],re,ql,qr,ans[N]; vector<pair<int,int>> g[N];
void upd(int x){for(;x <= m;x +=x & -x) t[x] ++;}
int qry(int x){for(re = 0;x;x -= x & -x) re += t[x]; return re;}
int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    cin >> n;
    for(int i = 1;i <= n;i ++) cin >> a[i];
    for(int i = n;i >= 1;i --) a[i] = a[i] - a[i - 1],a[n + i] = -a[i];
    auto SA = [&](int n){
        for(int i = 1;i <= n;i ++) sa[i] = i,rk[i] = a[i] + 1e9;
        for(int j = 1;j < n;j *= 2){
            for(int i = 1;i <= n;i ++) ok[i] = rk[i]; int p = 0;
            sort(sa+1,sa+n+1,[&](int x,int y){return rk[x] < rk[y] || rk[x] == rk[y] && rk[x + j] < rk[y + j];});
            auto cmp = [&](int x,int y){return ok[x] == ok[y] && ok[x + j] == ok[y + j];};
            for(int i = 1;i <= n;i ++) if(cmp(sa[i],sa[i-1])) rk[sa[i]] = p; else rk[sa[i]] = ++p; if(p == n) break;
        }for(int i = 1,k = 0;i <= n;h[rk[i ++]] = k) for(k --,k = max(k,0);a[i + k] == a[sa[rk[i] - 1] + k];k ++);
        for(int i = 1;i <= n;i ++) st[0][i] = h[i];
        for(int j = 1;j <= __lg(n);j ++) for(int i = 1;i + (1 << j) - 1 <= n;i ++)
            st[j][i] = min(st[j-1][i],st[j-1][i+(1<<j-1)]);
    };m = 2 * n,SA(m);
    auto lcp = [&](int i,int j){if(i == j) return m; int k = __lg(j - i); return min(st[k][i+1],st[k][j-(1<<k)+1]);};
    auto fd = [&](int k,int len){
        int l,r,ql,qr;
        for(l = 1,r = k;l <= r;){
            int mid = (l + r) / 2;
            if(lcp(mid,k) >= len) r = mid - 1; else l = mid + 1;
        }ql = r + 1;
        for(l = k,r = m;l <= r;){
            int mid = (l + r) / 2;
            if(lcp(k,mid) >= len) l = mid + 1; else r = mid - 1;
        }qr = l - 1;
        return make_pair(ql,qr);
    };cin >> q;
    for(int i = 1;i <= q;i ++){
        cin >> ql >> qr; int len = qr - ql + 1;
        if(len == 1){ans[i] = n - 1; continue;}
        auto [l,r] = fd(rk[ql + 1 + n],len - 1); 
        auto add = [&](int l,int r,int ql,int qr,int id){
            if(l > r || ql > qr) return ;
            g[r].emplace_back(qr,id),g[l - 1].emplace_back(ql - 1,id),
            g[r].emplace_back(ql - 1,-id),g[l - 1].emplace_back(qr,-id);
        };
        add(2,ql - len + 1,l,r,i),add(qr + 2,n,l,r,i);
    }for(int i = 1;i <= n;i ++) g[i].emplace_back(rk[i],-1e9);
    for(int i = 1;i <= n;i ++){
        sort(g[i].begin(),g[i].end());
        for(auto [y,id] : g[i]) if(id == -1e9) upd(y); else ans[abs(id)] += (id > 0 ? 1 : -1) * qry(y);
    }for(int i = 1;i <= q;i ++) cout << ans[i] << "\n";
}

\(\color{blue}(79)\) P4770 [NOI2018] 你的名字

难度 \(^*2900\)字符串;后缀数组,SA;可持久化线段树

首先考虑 \(l = 1,r = |s|\) 的简单情况,我们在统计完本质不同子串数量后,对于每个后缀 \(t'\) 要减去 \(s\) 里有它的部分,即找到 \(s\) 的一个后缀 \(s'\) 使得 \(\operatorname{LCP}(s',t')\) 最大,那么 \(s'\) 即是 \(t'\) 在拼起来排名 \(rk\) 的前驱后继,二分找到即可。时间复杂度 \(\mathcal O(n \log n)\)

对于区间版本,你可能会直接沿用上面的做法,寻找区间前驱后继直接上主席树,但过不了样例。问题在于 \(\operatorname{LCP}(s',t')\) 的计算,此时你的 \(s'\) 不再是整个串的后缀,而是后缀的前缀,这会导致 \(\operatorname{LCP}\) 长度偏大。这类问题解决的套路是二分 \(len\),只在 \([l,r-len+1]\) 中寻找后缀,但是复杂度会多一只 \(\log\)。至此,我们的复杂度是 \(\mathcal O(n\log^2 n)\),我卡了 1.5h 的常数也只卡到了 \(91\) 分。

我到这里就卡死了,看了 @lzyqwq 的题解后发现我的思路被 找前驱后继 困住了。重新思考怎么和 \(s\) 中的串去重,本质是对后缀 \(t'\) 找一个最大的 \(L\) 使得 \([1,L]\)\(s_{l \ldots r}\) 中出现。check 可以主席树 \(\mathcal O(\log n)\),但这就意味这我们需要 \(\mathcal O(1)\) 寻找 \(L\),不能二分了。令 \(st\) 为后缀 \(t'\) 的值,容易发现 \(L + st\) 单调不降,那么我们维护一下 \(L + st\) 的值,单调右移,对每个后缀的均摊复杂度就是 \(\mathcal O(1)\),总时间复杂度 \(\mathcal O(n\log n)\)

代码用了 atcoder 的 SA,就只放主函数了。

因为是从 \(\log^2\) 改的,有很多没用的数组 qwq。

代码
using ll = long long ;
const int N = 3e6+5;
string s,t,tmp; int l,q,tot,mp[N],al[N],lg[N],acc[N],ctt,pm[N],ql[N],aclen,qr[N],le[N],ct,n,rk[N],rt[N],sa[N],h[N],st[22][N],rp[N]; vector<int> pos;
struct seg{int l,r,w;}tr[N*20];
inline int upd(int s,int l,int r,int &x){
    int p = ++tot,mid = (l + r) / 2; tr[p] = tr[s],tr[p].w ++;
    if(l == r) return p;
    if(x <= mid) tr[p].l = upd(tr[p].l,l,mid,x); else tr[p].r = upd(tr[p].r,mid+1,r,x);
    return p;
}inline int qry(int u,int v,int l,int r,int &ql,int &qr){
    if(ql <= l && r <= qr) return tr[v].w - tr[u].w;
    int mid = (l + r) / 2,ans = 0;
    if(ql <= mid) ans += qry(tr[u].l,tr[v].l,l,mid,ql,qr);
    if(qr > mid) ans += qry(tr[u].r,tr[v].r,mid+1,r,ql,qr);
    return ans;
}int main(){
    // freopen("1.in","r",stdin),freopen("1.out","w",stdout);
    cin >> t,l = t.size(),s += t + '#';
    cin >> q;
    for(int i = 1;i <= q;i ++) cin >> tmp >> ql[i] >> qr[i],al[i] = s.size() + 1,s += tmp + '#',le[i] = tmp.size();
    vector<int> _sa = suffix_array(s); n = s.size(),s = " " + s;
    for(int i = 1;i <= n;i ++) sa[i] = _sa[i - 1] + 1,rk[sa[i]] = i;
    for(int i = 1;i <= n;i ++) lg[i] = __lg(i);
    for(int i = 1,k = 0;i <= n;h[rk[i ++]] = k) 
        for(k --,k = max(k,0);s[i + k] == s[sa[rk[i] - 1] + k] && s[i + k] != '#';k ++);
    for(int i = 1;i <= n;i ++) st[0][i] = h[i];
    for(int j = 1;j <= lg[n];j ++) for(int i = 1;i + (1 << j) - 1 <= n;i ++)
        st[j][i] = min(st[j-1][i],st[j-1][i + (1 << j -1)]);
    auto lcp = [&](int l,int r){if(l == r) return n;if(!l) return 0;int k = lg[r - l]; return min(st[k][l+1],st[k][r-(1<<k)+1]);};
    for(int i = 1;i <= l;i ++) rt[i] = upd(rt[i-1],1,n,rk[i]);
    // for(int i = ql[1];i <= qr[1];i ++) cerr << pos[i - 1] << " "; cerr << "\n";
    for(int i = 1;i <= q;i ++){
        int d = 0;
        for(int j = al[i];j <= al[i] + le[i] - 1;j ++) acc[++d] = rk[j],rp[d] = 0;
        auto fd = [&](int k,int len){
            int l,r,ql,qr;
            for(l = 1,r = k;l <= r;){
                int mid = (l + r) / 2;
                if(lcp(mid,k) >= len) r = mid - 1; else l = mid + 1;
            }ql = r + 1;
            for(l = k,r = n;l <= r;){
                int mid = (l + r) / 2;
                if(lcp(k,mid) >= len) l = mid + 1; else r = mid - 1;
            }qr = l - 1;
            return make_pair(ql,qr);
        };
        ll ans = 1ll * le[i] * (le[i] + 1) / 2;
        int r = 1,tmpx; 
        auto ck = [&](int l,int r){
            auto [x,y] = fd(rk[l + al[i] - 1],r - l + 1);
            return !!qry(rt[ql[i] - 1],rt[qr[i] - (r - l + 1) + 1],1,n,x,y);
        };
        for(int j = 1;j <= d;j ++){
            while(r < j || r <= le[i] && ck(j,r)) r ++;
            rp[j] = r - j; 
        }for(int j = 1;j <= d;j ++) pm[j] = j;
        sort(pm+1,pm+d+1,[&](int x,int y){return acc[x] < acc[y];});
        for(int j = 1;j <= d;j ++) ans -= max(rp[pm[j]],lcp(acc[pm[j - 1]],acc[pm[j]]));
        cout << ans << '\n';
    }
}

\(\color{blue}(80)\) ABC301Ex Difference of Distance

难度 \(^*2700\)倍增;树链剖分;树套树

我的题解

\(\color{blue}(81)\) CF1929F Sasha and the Wedding Binary Search Tree

难度 \(^*1600\)组合数学;中序遍历

倒开遇上大水 F /se

考虑二叉树中序遍历,我们得到的数列要求单调不降,那么对于每个数字,根据前缀 \(\max\) 和后缀 \(\min\) 可以确定取值区间 \([l_i,r_i]\)。对于连续的一段 \(-1\),取值区间 \([l_i,r_i]\) 显然相同,我们问在这个值域区间里能填出来多少个单调不降的序列。这是经典问题,令区间长度为 \(d\),取值区间大小为 \(q\),答案是 \(\dbinom{d+q-1}{d}\)。把所有连续区间的这个东西乘起来就行。

问题是我们要算的组合数 \(\dbinom{n}{m}\) 的范围是 \(n \le 10^9,\sum m \le 10^5\),不能直接算,那不难想到暴力 \(\mathcal O(m)\) 计算 \(\dfrac{n!}{(n-m+1)!}\),然后就做完了。时间复杂度 \(\mathcal O(n+\log p)\)

代码
/**
 *    author: sunkuangzheng
 *    created: 15.02.2024 22:43:43
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 2e6+5,mod = 998244353;
using namespace std;
int T,n,c,L[N],R[N],val[N],cnt,dfn[N],pre[N],suf[N],f[N],g[N];
int qp(int a,int b){
    int r = 1;
    for(;b;b >>= 1,a = 1ll * a * a % mod) if(b & 1) r = 1ll * r * a % mod;
    return r; 
}void dfs(int s){
    if(L[s] != -1) dfs(L[s]);
    dfn[++cnt] = s;
    if(R[s] != -1) dfs(R[s]);
}void los(){
    cin >> n >> c; int ans = 1; cnt = 0;
    auto C = [&](int n,int m){
        int ans = 1;
        for(int i = 0;i < m;i ++) ans = 1ll * ans * (n - i) % mod;
        return 1ll * ans * g[m] % mod;
    };
    for(int i = 1;i <= n;i ++) cin >> L[i] >> R[i] >> val[i];
    dfs(1); pre[0] = 1,suf[n + 1] = c;
    for(int i = 1;i <= n;i ++) pre[i] = max(pre[i - 1],val[dfn[i]]);
    for(int i = n;i >= 1;i --) suf[i] = min(suf[i + 1],(val[dfn[i]] == -1 ? (int)2e9 : val[dfn[i]]));
    for(int i = 1;i <= n;i ++){
        int j = i;
        while(i <= n && val[dfn[i]] == -1) i ++;
        int d = i - j,pr = suf[i] - pre[j] + 1; 
        if(d) ans = 1ll * ans * C(pr + d - 1,d) % mod;
    }cout << ans << "\n";
}int main(){
    f[0] = g[0] = 1,n = 1e6;
    for(int i = 1;i <= n;i ++) f[i] = 1ll * f[i - 1] * i % mod;
    g[n] = qp(f[n],mod - 2);
    for(int i = n - 1;i >= 1;i --) g[i] = 1ll * g[i + 1] * (i + 1) % mod;
    ios::sync_with_stdio(0),cin.tie(0);
    for(cin >> T;T --;) los();
}

\(\color{blue}(82)\) CF1929E Sasha and the Happy Tree Cutting

难度 \(^*2300\)虚树;动态规划,DP;状态压缩

看到 \(\sum 2^k \le 10^6\),猜到复杂度和 \(2^k\) 有关。状压 DP:设 \(f_s\)\(k\) 条路经是否已经有点的状态为 \(s\) 时的最小值,处理出每一条边能覆盖的路径记为 \(l_i\),则有 \(f_{j | l_i} = \min(f_{j | l_i},f_j + 1)\)

考虑到有用的边不多,直接建出来 \(2k\) 个点的虚树,在虚树上跑 DP 就过了。时间复杂度 \(\mathcal O(k2^k)\)

代码
/**
*    author: sunkuangzheng
*    created: 13.02.2024 08:07:00
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 2e5+5,mod = 998244353;
using namespace std;
int T,n,dfn[N],st[20][N],fa[N],tot,u,v,k,a[N],f[(1<<20)+1],tfa[N],au[N],av[N],li[N]; vector<int> g[N],p[N];
void dfs(int u,int f){
    st[0][dfn[u] = ++tot] = f;
    for(int v : g[u]) if(v != f) dfs(v,u);
}void dfs2(int u,int f){
    fa[u] = f;
    for(int v : g[u]) if(v != f) dfs2(v,u);
}void los(){
    cin >> n; tot = 0;
    for(int i = 1;i <= n;i ++) g[i].clear(),li[i] = 0;
    for(int i = 1;i < n;i ++) cin >> u >> v,g[u].push_back(v),g[v].push_back(u);
    dfs(1,0);
    cin >> k;
    for(int i = 1;i <= k;i ++) cin >> au[i] >> av[i];
    auto cmp = [&](int u,int v){return dfn[u] < dfn[v] ? u : v;};
    for(int j = 1;j <= __lg(n);j ++) for(int i = 1;i + (1 << j) - 1 <= n;i ++)
        st[j][i] = cmp(st[j-1][i],st[j-1][i+(1<<j-1)]);
    auto lca = [&](int u,int v){
        if(u == v) return u;
        if((u = dfn[u]) > (v = dfn[v])) swap(u,v);
        int k = __lg(v - u);
        return cmp(st[k][u+1],st[k][v-(1<<k)+1]);
    };for(int i = 1;i <= n;i ++) g[i].clear();
    auto dcmp = [&](int x,int y){return dfn[x] < dfn[y];};
    vector<int> d; int ans = 0;
    for(int i = 1;i <= k;i ++) d.push_back(au[i]),d.push_back(av[i]);
    sort(d.begin(),d.end(),dcmp);
    for(int i = 1;i < 2 * k;i ++) d.push_back(lca(d[i-1],d[i]));
    sort(d.begin(),d.end(),dcmp),d.erase(unique(d.begin(),d.end()),d.end());
    for(int i = 1;i < d.size();i ++) tfa[d[i]] = lca(d[i-1],d[i]), g[tfa[d[i]]].push_back(d[i]);
    reverse(d.begin(),d.end());
    // printarr(1,n,tfa);
    for(int i = 1;i <= k;i ++){
        int u = au[i],v = av[i],d = lca(u,v);
        // debug(u,v,d);
        while(u != d) li[u] |= (1 << i - 1),u = tfa[u]; 
        while(v != d) li[v] |= (1 << i - 1),v = tfa[v]; 
    }
    for(int i = 1;i < (1 << k);i ++) f[i] = 1e9; f[0] = 0;
    for(int i : d){
        for(int v : g[i])
            for(int j = (1 << k) - 1;j >= 0;j --) f[li[v] | j] = min(f[li[v] | j],f[j] + 1);
    }cout << f[(1 << k) - 1] << "\n";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(cin >> T;T --;) los();
}

\(\color{blue}(83)\) ABC250Ex Trespassing Takahashi

难度 \(^*2500\)最短路;Kruskal 重构树

双倍经验 CF1253F。

从所有关键点向外跑最短路,得到每个点的距离 \(dis_u\),我们把每条边 \((u,v,w)\) 的边权转化为 \(dis_u + dis_v + w\),则问题转化为求 \(u \to v\) 的最小瓶颈路。感性证明正确性是简单的:从这条边到最近的关键点需要 \(dis_u + dis_v\),走过这条边需要 \(w\),那么我们希望让路上的总代价最小。严谨证明可以参考题解。时间复杂度 \(\mathcal O(n \log n)\)

代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn = 4e5+5;
int n,m,k,q,u,v,w,dis[maxn],ti,vis[maxn],fa[maxn],a[maxn],tot,f[maxn][24],dep[maxn];
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> h;
vector<pair<int,int>> g[maxn];vector<int> gh[maxn];
struct edge{int u,v,w;}gr[maxn];
int fd(int x){return x == fa[x] ? x : fa[x] = fd(fa[x]);}
bool cmp(edge a,edge b){return a.w < b.w;}
void dij(){
    for(int i = 1;i <= n;i ++) dis[i] = 1e18+7;
    h.push({0,0});
    while(h.size()){
        auto [val,u] = h.top();h.pop();
        if(vis[u]) continue;vis[u] = 1;
        for(auto [v,w] : g[u]) if(dis[u] + w < dis[v]) dis[v] = dis[u] + w,h.push({dis[v],v});
    }
}void dfs(int u){
    dep[u] = dep[f[u][0]] + 1;
    for(int v : gh[u]) dfs(v);
}int lca(int u,int v){
    if(dep[u] < dep[v]) swap(u,v);
    while(dep[u] > dep[v]) u = f[u][__lg(dep[u] - dep[v])];
    if(u == v) return a[u];
    for(int i = 20;i >= 0;i --) if(f[u][i] != f[v][i]) u = f[u][i],v = f[v][i];
    return a[f[u][0]];
}
signed main(){
    ios::sync_with_stdio(false),cin.tie(0);
    cin >> n >> m >> k,tot = n;
    for(int i = 1;i <= 2*n;i ++) fa[i] = i;
    for(int i = 1;i <= m;i ++) cin >> u >> v >> w,g[u].emplace_back(v,w),g[v].emplace_back(u,w),gr[i] = {u,v,w};
    for(int i = 1;i <= k;i ++) g[0].emplace_back(i,0),g[i].emplace_back(0,0);
    dij();
    for(int i = 1;i <= m;i ++) gr[i].w += dis[gr[i].u] + dis[gr[i].v];
    sort(gr+1,gr+m+1,cmp);
    for(int i = 1;i <= m;i ++){
        int u = fd(gr[i].u),v = fd(gr[i].v);
        if(u == v) continue;a[++tot] = gr[i].w;
        fa[u] = fa[v] = f[u][0] = f[v][0] = tot;
        gh[tot].push_back(v),gh[tot].push_back(u);
    }for(int j = 1;j <= 20;j ++) for(int i = 1;i <= tot;i ++) f[i][j] = f[f[i][j-1]][j-1];dfs(tot);
    cin >> q;
    while(q --) cin >> u >> v >> ti,cout << (lca(u,v) <= ti ? "Yes" : "No") << "\n";
}

\(\color{blue}(84)\) ABC292Ex Rating Estimator

难度 \(^*1900\)线段树

最简单的一集,读完题都不敢相信这是 Ex。

考虑怎么找平均数第一个 \(\ge B\) 的位置,套路是每个数字 \(-B\),找第一个前缀和 \(\ge 0\) 的地方。一次修改操作对前缀和相当于区间加,那么线段树维护前缀和,每次询问在线段树上二分第一个 \(\ge 0\) 的地方即可。时间复杂度 \(\mathcal O(n \log n)\)

代码
/**
*    author: sunkuangzheng
*    created: 16.02.2024 09:36:50
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,b,q,a[N],x,y;ll t[N*4],tg[N*4],sm;
void cg(int s,ll k){t[s] += k,tg[s] += k;}
void pd(int s){cg(s*2,tg[s]),cg(s*2+1,tg[s]),tg[s] = 0;}
void upd(int s,int l,int r,int ql,int qr,ll k){
    if(ql <= l && r <= qr) return cg(s,k);
    int mid = (l + r) / 2; pd(s);
    if(ql <= mid) upd(s*2,l,mid,ql,qr,k); if(qr > mid) upd(s*2+1,mid+1,r,ql,qr,k);
    t[s] = max(t[s*2],t[s*2+1]);
}pair<int,ll> qry(int s,int l,int r){
    if(t[s] < 0) return {-1,-1};
    if(l == r) return {t[s],l}; pd(s);
    int mid = (l + r) / 2;pair<int,ll> d = qry(s*2,l,mid);
    if(d.first != -1) return d; else return qry(s*2+1,mid+1,r);
}void los(){
    cin >> n >> b >> q;
    cout << fixed << setprecision(10);
    for(int i = 1;i <= n;i ++) cin >> a[i],upd(1,1,n,i,n,a[i] - b),sm += a[i];
    while(q --){
        cin >> x >> y,upd(1,1,n,x,n,y - a[x]),sm += y - a[x],a[x] = y;
        auto [d,pos] = qry(1,1,n); if(d == -1) cout << (double)sm * 1.0 / n << "\n";
        else cout << (double)d * 1.0 / pos + b << "\n";
    } 
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(85)\) ABC237Ex Hakata

难度 \(^*2700\)字符串;网络流

如果你在想串串算法,可能要卡这个题很久;但是想到网络流这道题应该就比较简单了。

由于我们要选回文子串,要求不能出现两个串使得一个是另一个的子串,首先一定不能选一样的子串。这时候我们需要经典结论:一个串本质不同的回文子串数量不超过 \(n\)。那么我们建一张 DAG,边 \((u,v)\) 表示 \(v\)\(u\) 的子串。如果我们选了 \(u\),则所有 \(u\) 可达的点都不能选。这是网络流模型,拆点跑最大流即可。由于点数是 \(\mathcal O(n)\) 级别,复杂度为 \(\mathcal O(n^3)\)

代码
/**
 *    author: sunkuangzheng
 *    created: 16.02.2024 10:12:55
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
#include <atcoder/all>
using namespace atcoder;
using ll = long long;
const int N = 2e2+5;
using namespace std;
int T,n,du[N],tp[N],cnt; string s,d[N]; vector<int> gr[N]; bitset<N> f[N];
void los(){
    cin >> s,n = s.size();  int tot = 0;
    map<string,int> mp;
    for(int i = 0;i < n;i ++) for(int j = 1;j + i - 1 < n;j ++)
        if(string t = s.substr(i,j); t == string(t.rbegin(),t.rend()) && !mp[t]) mp[t] = ++tot;
    for(auto [s,x] : mp) d[x] = s;
    for(int i = 1;i <= tot;i ++) for(int j = 1;j <= tot;j ++) if(i != j && d[i].find(d[j]) != -1) 
        gr[i].push_back(j),du[j] ++;
    queue<int> q; mf_graph<int> g(2 * n + 2); int S = 0,T = 2 * n + 1;
    for(int i = 1;i <= tot;i ++) if(!du[i]) q.push(i);
    while(q.size()){
        int u = q.front(); q.pop(); tp[++cnt] = u;
        for(int v : gr[u]) if(!--du[v]) q.push(v); 
    }for(int i = cnt;i >= 1;i --){
        int u = tp[i]; f[u].set(u); g.add_edge(S,u,1),g.add_edge(u + tot,T,1);
        for(int v : gr[u]) f[u] |= f[v];
        for(int j = 1;j <= tot;j ++) if(f[u][j] && u != j) g.add_edge(u,j + tot,1);
    }cout << tot - g.flow(S,T) << "\n";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(86)\) ABC302Ex Ball Collector

难度 \(^*2400\)并查集

考虑单独求解一个 \(u\) 的做法,维护并查集,每一个并查集内维护 \(siz\)\(cnt\)\(cnt\) 表示集合内有几次合并),对于一对 \((a_i,b_i)\),我们合并 \((a_i,b_i)\) 的集合并让 \(cnt \gets cnt + 1\)。最终答案为所有连通块的 \(\min(cnt,siz)\) 之和,感性理解。

在树上时,我们递归每个点时合并,退出时撤销,用可撤销并查集即可。

代码
/**
*    author: sunkuangzheng
*    created: 16.02.2024 11:20:44
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,a[N],b[N],u,v,fa[N],siz[N],res[N],cnt[N],ans,stx[N],sty[N],tp; vector<int> g[N];
int fd(int x){return x == fa[x] ? x : fd(fa[x]);}
void mg(int x,int y){
    if(x = fd(x),y = fd(y),x == y) return ans -= min(cnt[x],siz[x]),ans += min(++cnt[x],siz[x]),
                                        stx[++tp] = x,sty[tp] = 0,void();
    if(siz[x] < siz[y]) swap(x,y);
    fa[y] = x,stx[++tp] = x,sty[tp] = y,ans -= min(cnt[x],siz[x]),ans -= min(cnt[y],siz[y]),
    siz[x] += siz[y],cnt[x] += cnt[y] + 1,ans += min(cnt[x],siz[x]);
}void fk(){
    int x = stx[tp],y = sty[tp --];
    if(!y) ans -= min(cnt[x] --,siz[x]),ans += min(cnt[x],siz[x]);
    else{
        fa[y] = y,ans -= min(cnt[x],siz[x]),cnt[x] -= cnt[y] + 1,siz[x] -= siz[y],
        ans += min(cnt[x],siz[x]),ans += min(cnt[y],siz[y]);
    }
}void dfs(int u,int f){
    mg(a[u],b[u]),res[u] = ans;
    for(int v : g[u]) if(v != f) dfs(v,u);
    fk();
}void los(){
    cin >> n;
    for(int i = 1;i <= n;i ++) cin >> a[i] >> b[i];
    for(int i = 1;i < n;i ++) cin >> u >> v,g[u].push_back(v),g[v].push_back(u);
    for(int i = 1;i <= n;i ++) fa[i] = i,siz[i] = 1; dfs(1,0);
    for(int i = 2;i <= n;i ++) cout << res[i] << " ";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(87)\) ABC291Ex Balanced Tree

难度 \(^*2400\)点分树

我的题解

\(\color{blue}(88)\) ABC256Ex I like Query Problem

难度 \(^*2600\)线段树;势能分析

如果没有操作二就是势能线段树板子,每个数字最多除 \(\mathcal O(\log V)\) 次。加了区间推平后,我们加一个剪枝:如果区间 \(\max = \min\),即全都相同,就直接除。可以证明这样的复杂度有保证。

代码
/**
*    author: sunkuangzheng
*    created: 16.02.2024 14:26:13
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 5e5+5;
using namespace std;
int T,n,m,tg[N*4],mx[N*4],mn[N*4],x,op,l,r; ll t[N*4];
void cg(int s,int l,int r,int k){tg[s] = mx[s] = mn[s] = k,t[s] = 1ll * (r - l + 1) * k;}
void pd(int s,int l,int r){int mid = (l + r) / 2;if(tg[s] != -1) cg(s*2,l,mid,tg[s]),cg(s*2+1,mid+1,r,tg[s]),tg[s] = -1;}
void pp(int s){t[s] = t[s*2] + t[s*2+1],mx[s] = max(mx[s*2],mx[s*2+1]),mn[s] = min(mn[s*2],mn[s*2+1]);}
void upd(int s,int l,int r,int ql,int qr,int k){
    if(ql <= l && r <= qr){
        if(mx[s] < k) return cg(s,l,r,0);
        if(mx[s] == mn[s]) return cg(s,l,r,mx[s] / k);
    }int mid = (l + r) / 2; pd(s,l,r);
    if(ql <= mid) upd(s*2,l,mid,ql,qr,k); if(qr > mid) upd(s*2+1,mid+1,r,ql,qr,k);
    pp(s);
}void upd2(int s,int l,int r,int ql,int qr,int k){
    if(ql <= l && r <= qr) return cg(s,l,r,k);
    int mid = (l + r) / 2; pd(s,l,r);
    if(ql <= mid) upd2(s*2,l,mid,ql,qr,k); if(qr > mid) upd2(s*2+1,mid+1,r,ql,qr,k);
    pp(s);
}ll qry(int s,int l,int r,int ql,int qr){
    int mid = (l + r) / 2; ll ans = 0;
    if(ql <= l && r <= qr) return t[s]; pd(s,l,r);
    if(ql <= mid) ans += qry(s*2,l,mid,ql,qr); if(qr > mid) ans += qry(s*2+1,mid+1,r,ql,qr);
    return ans;
}void los(){
    memset(tg,-1,sizeof(tg));
    cin >> n >> m;
    for(int i = 1;i <= n;i ++) cin >> x,upd2(1,1,n,i,i,x);
    while(m --){
        cin >> op >> l >> r;
        if(op == 1) cin >> x,upd(1,1,n,l,r,x);
        if(op == 2) cin >> x,upd2(1,1,n,l,r,x);
        if(op == 3) cout << qry(1,1,n,l,r) << "\n";
    }
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(89)\) ABC233Ex Manhattan Christmas Tree

难度 \(^*2700\)可持久化线段树

人类智慧。

先二分距离 \(d\),问题转化为距离 \((x,y)\) 小于 \(d\) 的平面内有多少点,容易想到拆成四个区间三维数点,但是因为二分要求在线,寄了。

发扬人类智慧,把坐标轴旋转 \(45\) 度,转化为二维数点,主席树维护。

代码
/**
*    author: sunkuangzheng
*    created: 16.02.2024 16:07:30
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 2e5+5;
using namespace std;
int T,n,tot,q,x,y,k,rt[N]; vector<int> f[N];
struct seg{int l,r,w;}t[N*80];
int upd(int s,int l,int r,int x,int k){
    int p = ++tot,mid = (l + r) >> 1; t[p] = t[s],t[p].w ++;
    if(l == r) return p;
    if(x <= mid) t[p].l = upd(t[p].l,l,mid,x,k); else t[p].r = upd(t[p].r,mid+1,r,x,k);
    return p;
}int qry(int u,int v,int l,int r,int ql,int qr){
    if(ql <= l && r <= qr) return t[v].w - t[u].w;
    int mid = (l + r) >> 1,ans = 0;
    if(ql <= mid) ans += qry(t[u].l,t[v].l,l,mid,ql,qr);
    if(qr > mid) ans += qry(t[u].r,t[v].r,mid+1,r,ql,qr);
    return ans; 
}void los(){
    cin >> n;
    for(int i = 1;i <= n;i ++) cin >> x >> y,f[x + y].push_back(x - y);
    for(int i = 0;i <= 200000;rt[i + 1] = rt[i],i ++) for(int j : f[i]) 
        rt[i] = upd(rt[i],-1e5,1e5,j,1);
    for(cin >> q;q --;){
        cin >> x >> y >> k; int rx = x,ry = y; x = rx + ry,y = rx - ry;
        int l = 0,r = 2e5;
        while(l <= r){
            int mid = (l + r) / 2;
            if(qry((x - mid - 1 >= 0 ? rt[x - mid - 1] : 0),rt[min(200000,x + mid)],-1e5,1e5,max(-100000,y - mid),min(100000,y + mid)) < k) 
                l = mid + 1; else r = mid - 1;
        }cout << l << "\n";
    }
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(90)\) ABC287Ex Directed Graph and Query

难度 \(^*1800\)Floyd

Ex 只有 E 的难度。

考虑询问离线,Floyd,枚举 \(k\)\(1 \sim n\) 跑传递闭包,枚举完一个 \(k\),扫一遍所有询问,记录每个询问连通的第一个 \(k\),bitset 优化后复杂度 \(\mathcal O(nq + \dfrac{n^3}{w})\)

然后数组开小了 WA 到一度怀疑代码正确性 /oh

代码
/**
 *    author: sunkuangzheng
 *    created: 16.02.2024 17:39:39
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 2e3+5;
using namespace std;
vector<pair<int,int>> g[N]; int T,n,m,u,v,q,au[N*10],av[N*10],ans[N*10]; bitset<N> f[N];
void los(){
    cin >> n >> m;
    for(int i = 1;i <= m;i ++) cin >> u >> v,f[u].set(v);
    for(int i = 1;i <= n;i ++) f[i].set(i);
    cin >> q; 
    for(int i = 1;i <= q;i ++) cin >> au[i] >> av[i];
    for(int k = 1;k <= n;k ++){
        for(int i = 1;i <= n;i ++) if(f[i][k]) f[i] |= f[k];
        for(int i = 1;i <= q;i ++) if(f[au[i]][av[i]] && max(au[i],av[i]) <= k && !ans[i]) ans[i] = k;
    }for(int i = 1;i <= q;i ++) cout << (ans[i] ? ans[i] : -1) << "\n"; 
}int main(){
    // freopen("at_abc287_h.in","r",stdin),freopen("at_abc287_h.out","w",stdout);
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(91)\) ABC298Ex Sum of Min of Length

难度 \(^*2400\)树论;倍增

我的题解

\(\color{blue}(92)\) ABC246Ex 01? Queries

难度 \(^*2400\)动态规划,DP;动态 DP

很明显的动态 DP,但是我不会 \(\mathcal O(nq)\),怎么回事呢。

考虑设 \(f_{i,0/1}\) 表示到位置 \(i\),有多少个以 \(0/1\) 结尾的子序列。转移方程不太显然,我一直在考虑怎么处理当前位置不填,和当前位置填了的去重问题,但事实上巧妙的办法是强制填。因为前面的子序列已经互不相同,所以若 \(s_i = \texttt{1}\),有 \(f_{i,1} = f_{i-1,0} + f_{i-1,1} + 1\)(最后 \(+1\) 是从空串填),因为长度变化,不会出现重复,而原本有的子序列虽然消失了,但是还能从更短的拼回来。\(\texttt{0}\) 的情况类似,\(\texttt{?}\) 的情况就是 \(f_{i,0/1} = f_{i-1,0} + f_{i-1,1} + 1\)。然后上矩阵即可。

代码
/**
*    author: sunkuangzheng
*    created: 17.02.2024 15:09:08
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = 2e5+5,mod = 998244353;
using namespace std;
int T,n,q,i,f[N][2]; string s; char ch;
struct mat{
    int a[3][3];
    mat(){memset(a,0,sizeof(a));}
    int* operator [](int x){return a[x];}
    mat operator *(mat b){
        mat c;
        for(int i = 0;i <= 2;i ++) for(int k = 0;k <= 2;k ++) for(int j = 0;j <= 2;j ++) 
            c[i][j] = (c[i][j] + 1ll * a[i][k] * b[k][j]) % mod;
        return c;
    }void print(){
        for(int i = 0;i <= 2;i ++,cout << "\n") for(int j = 0;j <= 2;j ++) cout << a[i][j] << " ";
    }
}t[N*4],a,b,c,d,e;
void upd(int s,int l,int r,int x,mat k){
    if(l == r) return t[s] = k,void();
    int mid = (l + r) / 2;
    if(x <= mid) upd(s*2,l,mid,x,k); else upd(s*2+1,mid+1,r,x,k);
    t[s] = t[s*2] * t[s*2+1];
}void los(){
    cin >> n >> q >> s,s = " " + s;
    a[0][0] = a[1][0] = a[2][0] = a[1][1] = a[2][2] = 1;
    b[0][0] = b[0][1] = b[1][1] = b[2][1] = b[2][2] = 1;
    c[0][0] = c[1][0] = c[2][0] = c[0][1] = c[1][1] = c[2][1] = c[2][2] = 1;
    d[0][2] = 1;
    for(int i = 1;i <= n;i ++)
        if(s[i] == '0') upd(1,1,n,i,a); 
        else if(s[i] == '1') upd(1,1,n,i,b); 
        else upd(1,1,n,i,c);
    while(q --){
        cin >> i >> ch;
        if(ch == '0') upd(1,1,n,i,a); 
        else if(ch == '1') upd(1,1,n,i,b); 
        else upd(1,1,n,i,c);
        e = d * t[1],cout << (e[0][0] + e[0][1]) % mod << "\n";
    }
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(93)\) ABC286Ex Don't Swim

难度 \(^*2400\)凸包;计算几何

首先显然的是,如果 \(s \to t\) 的直线不过凸包,答案就是这条线。否则,在多边形上的路径一定是走它的凸包。那么,怎么找到 \(s \to\) 凸包 和 凸包 \(\to t\) 的路径?感性理解,就是把 \(s,t\) 丢进去一起跑凸包后凸包上 \(s,t\) 的距离。单组询问暴力算即可。

代码
/**
 *    author: sunkuangzheng
 *    created: 12.10.2023 19:42:05
**/
#include<bits/stdc++.h>
using namespace std;
#define db long double
const int maxn = 5e5+5;
int t,n,tp,st[maxn]; pair<db,db> a[maxn],S,T;db ans;
db po(db x){return x * x;}
db gd(int x,int y){return sqrt(po(a[x].first - a[y].first) + po(a[x].second - a[y].second));}
db gk(int x,int y){return (a[x].first == a[y].first ? 1e18 : (a[x].second - a[y].second) / (a[x].first - a[y].first));}
int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    cout << fixed << setprecision(10);
    cin >> n;
    for(int i = 1;i <= n;i ++) cin >> a[i].first >> a[i].second;
    cin >> S.first >> S.second >> T.first >> T.second;
    a[++ n] = S,a[++n] = T,sort(a+1,a+n+1);
    for(int i = 1;i <= n;i ++)
        for(st[++tp] = i;tp >= 3 && gk(st[tp - 2],st[tp]) < gk(st[tp-1],st[tp-2]);) st[tp-1] = st[tp],tp --;
    vector<int> p;
    for(int i = 1;i <= tp;i ++) p.push_back(st[i]); tp = 0;
    for(int i = n;i >= 1;i --)
        for(st[++tp] = i;tp >= 3 && gk(st[tp - 2],st[tp]) < gk(st[tp-1],st[tp-2]);) st[tp-1] = st[tp],tp --;
    for(int i = 2;i < tp;i ++) p.push_back(st[i]);
    int px = -1,py = -1,tot = p.size();
    for(int i = 0;i < tot;i ++) {if(a[p[i]] == S) px = i; if(a[p[i]] == T) py = i;}
    if(px == -1 || py == -1) return cout << sqrt(po(S.first - T.first) + po(S.second - T.second)) << "\n",0;
    rotate(p.begin(),p.begin() + px,p.end()); db ans1 = 0; assert(a[p[0]] == S);
    for(int i = 0;i < p.size();i ++) if(a[p[i]] == T) {py = i;break;} else ans1 += gd(p[i],p[i + 1]);
    rotate(p.begin(),p.begin() + py,p.end()); db ans2 = 0; assert(a[p[0]] == T);
    for(int i = 0;i < p.size();i ++) if(a[p[i]] == S) break; else ans2 += gd(p[i],p[i + 1]);
    cout << min(ans1,ans2);
}

\(\color{blue}(94)\) AT_s8pc_2_e 部分文字列

难度 \(^*2400\)字符串;后缀数组,SA

后缀数组板子。

代码
#include<bits/stdc++.h>
#include <atcoder/all>
using namespace std;
int n; string s;  
int main(){
    cin >> s,n = s.size(); long long ans = 0;
    for(int i = 1;i <= n;i ++) ans += 1ll * i * (i + 1) / 2;
    vector<int> sa = atcoder::suffix_array(s),h = atcoder::lcp_array(s,sa);
    for(int i : h) ans -= 1ll * i * (i + 1) / 2;
    cout << ans << "\n";
}

\(\color{blue}(95)\) ABC341G Highest Ratio

难度 \(^*2300\)暴力

显然的,把 \(n\) 个数分成两组求平均数,则这 \(n\) 个数的平均数会在这两个平均数之间。记 \(g_i\) 表示 \(i\) 开头的后缀中 \([i,g_i)\) 平均数最大,初始时 \(g_i = i + 1\)\(f_i\) 表示 \([i,g_i)\) 的平均数。我们现在求解 \(i\) 开头的后缀,则我们比较 \(f_i\)\(f_{g_i}\) 的大小,如果 \(f_{g_i}\) 更大,则更新 \(g_i = g_{g_i}\)。这个算法时间复杂度均摊 \(\mathcal O(n)\),但正确性看着很假。但是手玩几组数据后你就可以理解它的正确性,下面感性证明:

  • 如果 \(f_i \ge f_{g_i}\),选择延伸至 \(g_{g_i}\) 一定不优。

  • 如果 \(f_i < f_{g_i}\),如果最优决策点在 \([g_{g_i},n]\) 区间,则选择 \([g_i,g_{g_i})\) 区间后还会继续遍历到决策点。如果决策点在 \(j \in [i,g_i)\),那么 \(j\) 一定不是最优决策点,因为 \([g_i,j)\) 的平均数小于等于 \([j,g_{g_i})\),而 \([i,j)\) 的平均数小于等于 \([g_i,j)\),因此选择 \([j,g_{g_i})\) 后平均数会变大。

代码就很好写了。

代码
/**
 *    author: sunkuangzheng
 *    created: 16.02.2024 09:36:50
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long double;
const int N = 5e5+5;
using namespace std;
int T,n,b,q,a[N],x,y,g[N],d;ll f[N];
void los(){
    cin >> n;
    cout << fixed << setprecision(10);
    for(int i = 1;i <= n;i ++) cin >> a[i];
    for(int i = n;i >= 1;i --){
        g[i] = i + 1,f[i] = a[i];
        while(f[i] < f[g[i]])
            d = g[g[i]],f[i] = (f[i] * (g[i] - i) + f[g[i]] * (d - g[i])) / (d - i),g[i] = d;
    }for(int i = 1;i <= n;i ++) cout << f[i] << " ";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(T = 1;T --;) los();
}

\(\color{blue}(96)\) CF1930F Maximize the Difference

难度 \(^*2600\)位运算

位运算题常见套路是转化题意。考虑如果只有两个数字 \(a,b\),式子的最大值该怎么求。如果希望 \(a | x\) 成为 \(\max\),那么只有 \(a_i = 1,b_i = 0\) 的位置可以产生贡献,也就是 \(a \& -b\) 的值。对于序列长度 \(> 2\) 的情况,我们可以枚举 \((a,b)\),计算 \(\max(a \& -b,b \& -a)\) 的值取 \(\max\)

考虑每次插入一个新元素 \(a\),我们都需要在前面找到元素 \(b\),求出 \(\max(a \& -b,b \& -a)\)\(\max\)。注意到 \(\sum n \le 2^{22}\),考虑和值域有关的做法。暴力枚举 \(a\)\(a\) 的补集的子集,如果 \(a\) 的子集 \(p\) 和之前某个 \(-b\) 的子集 \(q\) 相等,则 \(p\) 是一个合法答案。记忆化搜索,一共只有 \(\mathcal O(n)\) 个数。枚举子集时,只寻找只比它少一位的数字,这样复杂度即为 \(\mathcal O(n \log n)\)

代码
/**
 *    author: sunkuangzheng
 *    created: 18.02.2024 09:00:50
**/
#include<bits/stdc++.h>
#ifdef DEBUG_LOCAL
#include <mydebug/debug.h>
#endif
using ll = long long;
const int N = (1 << 22) + 1;
using namespace std;
int T,n,q,li,x,lst,f[N][2];
void los(){
    cin >> n >> q,li = (1 << __lg(n - 1) + 1),lst = 0; 
    for(int i = 0;i <= li;i ++) f[i][0] = f[i][1] = 0;
    while(q --){
        cin >> x,x = (x + lst) % n;
        for(int tp = 0;tp <= 1;tp ++){
            queue<int> q; q.push((tp ? x : li - 1 - x));
            while(q.size()){
                int x = q.front(); q.pop();
                if(f[x][tp]) continue; f[x][tp] = 1;
                if(f[x][!tp]) lst = max(lst,x);
                for(int i = 1;i <= li;i *= 2) if(x & i) q.push(x ^ i); 
            }
        }cout << lst << " ";
    }cout << "\n";
}int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    for(cin >> T;T --;) los();
}

\(\color{blue}(97)\) CF316G Good Substrings

难度 \(^*2600\)字符串;后缀数组,SA

简单的后缀数组题,你的名字 弱化版。

注意串的出现次数随着长度单调,那么直接二分每一个后缀的前缀长度即可,check 直接找到 \(\operatorname{LCP} \ge len\) 的区间,判断区间里有多少后缀属于 \(t_i\)。求本质不同子串减去 \(ht_i\) 即可。

\(m = |s_i| + \sum |t_i|\),时间复杂度 \(\mathcal O(m \log^2 m)\)

\(\color{blue}(98)\) CF1930H Interactive Mex Tree

难度 \(^*3200\)交互;构造;倍增

我的题解

\(\color{blue}(99)\) CF1930G Prefix Max Set Counting

难度 \(^*3000\)动态规划,DP;树状数组

我的题解

\(\color{blue}(100)\) CF1930E 2..3...4.... Wonderful! Wonderful!

难度 \(^*2400\)组合数学

我的题解

posted @ 2024-01-21 19:48  sunkuangzheng  阅读(116)  评论(1编辑  收藏  举报