2.基础优化技巧
基础优化技巧1
三分
一般仍然是用二分+eps处理。
01分数规划
求解形如:
有 \(n\) 个物品,每个物品有两个权值 \(a,b\),选出一些物品,使得两个权值和的比值最大或最小。也就是另 \(w_i=0/1\),最大化:
一般我们用二分答案处理,移项可得:
例题1- [HNOI2009] 最小圈
给定有向带权图,在图上找一个环,使得环上边权和除以节点个数最小,求这个最小的平均值。
\(n\le 3000,m\le 1000,w_i\le 10^7\)。
观察到答案是比例形式,联想到 \(0/1\) 分数规划。考虑把边看作 \(a\),点看作 \(b\),二分一个答案后,边权 \(v\) 重新赋值为 \(v-t\),使用 SPFA 找负环即可。找到负环说明 \(mid\) 仍能再小。
判负环注意是入队次数大于等于 \(n\)。
写完才发现远古题的恶心之处:
- 复杂度正确的 BFS SPFA过不了,复杂度错误的 DFS SPFA能过?
- 题解所有人的算法最差复杂度达到 \(O(m^2n\log )\),这也能过?
- 4202年还需要洛谷机子加强到 5s,9002年的科技就是发达,1s跑3e9。
整体二分
建议改名整体分治。注意复杂度分析。
例题2- P3527 [POI2011] MET-Meteors
有一个初始为 \(0\) 的序列 \(a\) 和 \(m\) 个操作,每个操作是区间 \([l_j,r_j]\) 加 \(k_j\)。你要对于每个位置 \(i\),求出第几次操作后能使得 \(a_i>x_i\)。
\(n\le 3\times 10^5\)。
可以主席树二分做,但是空间被卡了。
如果只有一个国家是简单的,直接二分时间+前缀和即可。
考虑对所有国家同时进行二分, 函数 solve(l,r,<vector>v)
表示时间(操作数)的范围是 \([l,r]\),序列是 \(v\)。
每次二分一个 mid=l+r>>1
,执行 \([1,mid]\) 时间内的操作,看每个点是否超过限制 \(x_i\)。超过则说明需要小一点,放在 \(v_l\) 里,否则放在 \(v_r\) 里。注意树状数组及时清空即可。
以上是常见错误,复杂度退化成单个二分。
实际上整体二分是分治,不知道谁起的这个名字。分治过程中复杂度只能与分治区间有关。树状数组需要通过撤销清空。
复杂度 \(O(n\log^2n)\)。
怎么这题还卡 long long
。
void solve(int l,int r,vector<int>v) {
int mid=l+r>>1;
if(l==r) {
for(int x:v) ans[x]=l;
return ;
}
vector<int>v1,v2;
for(int i=l;i<=mid;i++) {
if(op[i].l<=op[i].r)
t.update(op[i].l,op[i].r,op[i].k);
else t.update(1,op[i].r,op[i].k),t.update(op[i].l,m,op[i].k); //
}
for(int x:v) {
int tmp=Query(x);
if(tmp<lim[x]) lim[x]-=tmp,v2.push_back(x);
else v1.push_back(x);
}
for(int i=l;i<=mid;i++) {
if(op[i].l<=op[i].r)
t.update(op[i].l,op[i].r,-op[i].k);
else t.update(1,op[i].r,-op[i].k),t.update(op[i].l,m,-op[i].k); //
}
solve(l,mid,v1);solve(mid+1,r,v2);
}
补充例题 P8231 [AGM 2022 资格赛] 农场
将例题2 搬到矩形上
套用二维树状数组即可。
分治
例题3- 【模板】三维偏序
三维偏序,第一维度直接排序做。
然后分治,处理经过分治中点的区间,第二维度左右区间分别归并排序后双指针,注意这样仍满足第一维度的限制,第三维度双指针的时候用权值树状数组在左区间修改,右区间查询。
注意处理相等元素会很麻烦,所以直接去重就好了。记录一下重复元素个数。
bitset 解决 更高维度的偏序
构造01矩阵 \(S_{p}\) 表示第 \(p\) 维度的偏序关系,不难发现 \(S_1({i,j}) \and S_2(i,j)...\) 即为 \(i,j\) 在所有偏序上的关系。只需用 count()
函数计算 bitset
中 \(1\) 的个数即可。
构造 \(S\) 的过程也比较简单,对于每一维先排序,再用 bitset
记录比自己小的集合即可。
时空复杂度 \(O(n^2/\omega),\omega=64\),时间上常数较小,空间上比较紧:**牢记NFLS bitset 惨案:1个长度为 \(N\) 的 bitset
内存是 \(N\times 4/\omega,\omega=32\) **。当 \(N=10^{10}\) 时达到了 1G+,小心MLE保龄。
补充例题 :P3769 [CH弱省胡策R2] TATT
四维偏序
补充例题:P5979 [PA2014] Druzyny
例题4:分治与最短路 - P3350 [ZJOI2016] 旅行者
给你一个 \(N\times M\) 的网格图,边有正边权,\(Q\) 次询问,从 \((x_0,y_0)\to (x_1,y_1)\) 的最短距离。\(N\times M\le 10^4,Q\le 10^5\)。
好题!二维题分治一维。
我们知道长成最短路的通常只能用最短路算法(网格图虽说可以 \(dp\),但显然不好做),我们曾做过:[GXOI/GZOI2019] 旅行者,使用二进制分组的方式建虚拟源点跑 \(\log\) 次最短路。那么这道题能否用类似的方式呢?
观察到 \(N\times M\le 10^4\),\(\text{CandyBar}\) 曾在根号重构中讲过类似的处理套路,即 \(\min(N,M)\le 100\)。
那么就可以枚举较短的那一条边作为虚拟源点了,外面再套个啥?理应想到分治,因为我们能根号对数时间内快速处理跨过一个点的询问点对。用分治进行降维。
复杂度 \(O(n\sqrt n\log n+Q\sqrt n)\)。
例题5:区间最大值分治 - CF1156E Special Segments of Permutation
给定长度为 \(n\) 的排列,求多少区间满足 \(p_l+p_r=\max _{i=l}^r p_i\)。
不要往扫描线上想,这是经典序列分治,直接考虑中点,和前缀后缀最大值即可。比较麻烦。
还有一种不大经典的分治,考虑最大值分治,然后最大值所代表的区间是好确立的,我们 只遍历短的那一边,另一边用数据结构 数组查找。
另外可以往笛卡尔树启发式合并上想。
补充例题:CF1175F The Number of Subpermutations
求满足 \(1...r-l+1\) 各出现一次的区间 \([l,r]\)。
考虑刻画一个满足条件的区间:
- 区间最大值为 \(r-l+1\)
- 区间每个数都只出现了一次。
发现这样足以,前者可以区间最大值分治,后者可以用比较套路的方法:记录上一个出现的位置为 \(p_i\),则 \(\max_{i=1}^rp_i<l\)。
注意到区间长度实际上已经确定,因此无论左边右边都直接枚举做即可。
Wa了一次的原因是,枚举左端点,算出右端点,需要保证右端点跨过最大值。
补充例题1:P5979 [PA2014] Druzyny
倍增
常见应用:
- Floyd 判圈
- 求LCA / \(k\) 级祖先
- ST 表
要求:具有结合律,不要求可减性。
例如:\(O(\log n)\) (查询)求树上路径 \(\min,\gcd\)。
其正确性由 二进制表示的唯一存在性给出。
例题6:国旗计划
环上给定 \(n\) 个没有包含关系的区间 \([l_i,r_i]\),求必须选第 \(i\) 个区间的情况下,还需要选多少区间才能使得 \([1,n]\) 全部被覆盖。
首先断环为链,对于本题,要么处理区间,要么处理位置。
处理区间的话,我们有显然的贪心策略:如果按右端点排序,跳到最后一个左端点小于当前区间右端点的区间。这个可以简单预处理得到。
然后需要从一个区间不断跳,直到跳到这个区间左端点右边,我们有 \(O(n^2)\) 的模拟,可以倍增优化这个过程。
哈希
通过把某个对象通过对其特征的描述映射为一个值来去重/判断相等。
关键在于抽象其关键信息,以构造适当的哈希函数。
字符串哈希
把字符串看作 B (B>26) 进制数,随机选择一个大质数 \(P\)。结论是两个随机字符串的哈希值相同的概率大约是 \(\frac{1}{P}\)。所以当比较次数较大的时候使用双模哈希。但打CF的时候小心被人对着卡。
自然溢出容易撞的原因是:如果哈希函数构造不当导致很多偶数乘进来,例如结果为一个 \(2^{100}\cdot d\),自然溢出的结果为 \(0\)。
例题7 异或哈希:[ABC250E] Prefix Equality
给定两个序列 \(A,B\),多次询问,每次给出 \(x,y\),判断 \(A\) 的前 \(x\) 项和 \(B\) 的前 \(y\) 项构成的集合是否相同。(集合不可重)
显然是和顺序无关的函数,而且我们只在第一次出现的地方修改哈希即可。
常见的套路就是每个值随一个大数,异或起来。
如果拓展到区间,放到主席树上就ok了。
Trie
例题8:P3065 [USACO12DEC] First! G
给定若干字符串,你可以任意规定字符间的偏序关系,求有多少字符串能够成为字典序最小的字符串。
首先只有作为前缀的字符串才有可能是答案。
建出 trie 树,一个串能成为答案,那么从根到这个节点,会形成若干偏序关系,我们需要判断这个偏序关系是否合法。
后者是经典图论模型,偏序关系连边后判断是否成环即可。可以用拓扑排序。
例题9:P4551 最长异或路径
给定一棵树,边有边权,求两点间路径异或和最大是多少。
树上路径异或直接前缀和,甚至不需要求 lca。
问题转换为 \(n\) 个数两两异或和最大是多少。都插到 01trie 里,查询的时候尽可能反着走就行。
注意全局加一是从小往大插,其它都是从大往小插。
例题10:CF888G Xor-MST
有一张 \(n\) 个点的完全图,边权为 \(a_i\oplus a_j\),求MST。
妙妙题。
实际上是 B开头的 MST 算法,考虑初始有 \(n\) 个连通块,每次寻找所有连通块连出去的最小边,将这些边加入最小生成树,并合并这些连通块。
每次连通块个数至少减半,所以复杂度是 \(O(m\log n)\)。
对于这个题,我们建出 01trie 后,如何找最小边?这两个点的 lca 一定尽可能深,所以 dfs 一遍tire,从下往上,找到一条最小的边,合并每个点的左右子树。根据 B-算法,这样做是正确的。不需要 dsu 等其他东西。
如何找到最小的边?可以遍历较小的一棵子树,在另一棵子树查询,复杂度类似启发式合并。
也可将原数组排序后再建树,这样树上每个点都对应原数组一段区间。直接遍历即可,每个点只会被其祖先访问,01trie深度是对数级别的。总复杂度 \(O(n\log^2n )\)。