ZCETHAN の Tricks
用来记录一些不属于正统算法,但是是一些常见的经典套路的技巧。科技?
记录的东西会有点 naive。
以及一些简单的结论。(但是看起来简单,用起来惊为天人)
分块时间换空间
牛了,常见套路,一般用分块来用时间换空间。
求一个可修改序列中 \([l,r]\) 区间内不连续取 \(3\) 个数的所取的数最大和是多少。
\(n\le 1.2\times 10^6\),\(m\le 2^{18}+5\)。
时间限制:2s
空间限制:32MB
如果是朴素的 dp,大家都会。
如果我们考虑修改和区间询问,那么大家都会 DDP。
但是发现空间完全开不下。
空间:int 开 8 个 1e6 的数组,long long 开 4 个。
这时候,对原数列分块,然后块间建线段树,然后块内暴力构造矩阵和求解。虽然会慢一点,但是最终空间是够的,时间也能过去。
前缀优化建图
一种优化时空的建图方式。我们考虑如果每次要求一个点对一堆点连边,除了线段树优化建图以外,可以考虑是否可以用前缀优化建图。
比如说在 2-sat 中的前缀优化建图:
给 \(n\) 个点,分成 \(k\) 部分,\(m\) 条限制表示两个点至少选一个,每部分恰选一个。求有无可行选法。
一般我们会把一个点拆成两个,表示选或者不选。现在我们拆成 \(4\) 个,表示选或者不选,以及前缀中是否有选,后缀中(不包括本身)是否有选。
这样一来,我们对一堆点连边就变成了向一个点连边,那么对于一堆的点,我们需要预先连一些边。
- 如果一个点选了,那么它前缀中肯定有选了;
- 如果后缀 \(i\) 有选,那么 \(i\) 这个点不能选(同一堆中不能一起选);
- 如果前缀 \(i\) 有选,那么前缀 \(i+1\) 有选;
- 如果后缀 \(i+1\) 有选,那么后缀 \(i\) 有选;
- 如果 \(i\) 选了,那么 \(i-1\) 的后缀选了;
- 如果前缀 \(i\) 有选,那么 \(i+1\) 不选(后面的点交给前缀 \(i+1\))。
这样会得到这样的图:
然后直接连边就可以了。
或者考虑网络流的优化,具体见这题。
O(logn) 分解质因子
众所周知每个数都只有 \(\log n\) 级别的质因子。那么的话我们可以利用线性筛每个数都是用它的最小质因子筛掉的,记录一下,然后分解的时候每次除以这个最小质因子就可以精准获取质因子,也就是 \(O(\log n)\) 分解了。
斐波那契前缀和公式
太神奇了,二次以上也是有公式的。
C++ 语法
求一个数组中连续区间的最值:
*max_element(a+l,a+r+1)
向上取整的数论分块
你可以这样:
然后就变成向下取整的数论分块了。
代替弱化的平衡树的堆
还是来记一下。用来解决加入,删除,查询最值。可以用极短的代码来代替平衡树。你搞两个堆,我们叫做 \(A\) 和 \(B\) 吧,每次插入的时候,把元素压到 \(A\) 中,然后删除就压到 \(B\) 中。每次查询最值的时候就比较 \(A\) 和 \(B\) 的堆顶是否相同,如果相同就同时弹掉,否则就返回 \(A\) 的堆顶就是当前询问的最值了。
struct Heap{
priority<int> A,B;
void ins(int x){A.push(x);}
void del(int x){B.push(x);}
int ask(int x){
while(!A.empty()&&!B.empty()&&A.top()==B.top())
A.pop(),B.pop();
if(A.empty()) return -1;
return A.top();
}
};
Kosaraju
听 \(\texttt{m}\color{red}{\texttt{yee}}\) 嘴了一个叫做 Kosaraju 的东西,好像可以很方便地代替 Tarjan 的图论算法。
其流程是这样的:首先我们对整张图跑 dfs,然后按照出递归栈的顺序压入一个 vector 中。接下来倒序遍历 vector 中的点,然后在反图上跑搜索,此时当前能访问到的点就是和当前点在同一个强连通分量中的。
一个套路
对于查询区间中计算子序列的信息,有一个通用的做法就是,对于所有询问离线,然后移动右指针,然后用 DS 维护每一个左端点的信息。这样的话对于询问区间 \([l,r]\),我们在右端点移动到 \(r\) 的时候,查询 \([l,r]\) 区间内的值。然后就得到了右端点为 \(r\) 的所有子区间的值。为了统计到所有的子区间,还需要记录一下每个节点的历史最值。这使得这个方法只能维护区间中子区间的最值之类满足覆盖的信息。
分数规划
好像是很陈旧的东西了。就是给你一堆东西,问你一个分数的值最大是多少。一般采用二分答案,然后在不等式上移项,如果当前二分的答案会使得存在一个策略使得不等式反向,那么答案可以更进一步,否则说明答案太过于优秀,没有办法达到。
一个很界外的东西
XJ 模拟赛遇到的。问题是:有 \(n\) 个人打架,给定一个概率 \(p\),如果 \(i<j\),那么 \(i\) 打赢 \(j\) 的概率是 \(p\),\(j\) 打赢 \(i\) 的概率是 \(1-p\)。对于 \(\forall i\in[1,n)\),求能找出一个大小为 \(i\) 的子集,使得里面的任意一个人都打赢了子集外的每一个人的概率。
容易想到令 \(dp_{i,j}\) 表示前 \(i\) 个人,其中 \(j\) 个人在集合中的概率。不难得到转移:\(dp_{i,j}=dp_{i-1,j}\times p^j+dp_{i-1,j-1}\times (1-p)^{i-j}\)。
好消息是这个东西最优只能做到 \(O(n^2)\),但是这题要求 \(O(n)\)。怎么办?
然后考虑这个前真的有必要么,直接说当前是连续的 \(i\) 个人,然后里面可以选出 \(j\) 个人在集合中。这样我们除了可以在最后面加一个编号最大的人之外,还可以在最前面加一格编号最小的人,即:\(dp_{i,j}=dp_{i-1,j}\times (1-p)^j+dp_{i-1,j-1}\times p^{i-j}\)。
变魔术: 我们把这两个式子联立起来!得到:
然后把 \(i-1\to i\),得到:
接着移项,合并,除过去得到:
然后就把 \(i\) 这一维去掉了!你不界外吗?
总结: 如果遇到一个题,每一个元素的地位等价性很高,而当前的 dp 式子不好优化的时候,可以考虑多考虑几种转移,使得某一维可以被抵消掉。
势能分析的一种套路
感觉经常遇到这种套路。
就是把大于等于某个阈值的数都减去这个阈值。我们假设它为 \(c\)。那么我们把所有数分成 \([0,c),[c,2c),[2c,+\infty)\) 三部分,然后对左边不用处理,对最右边用数据结构维护一下,然后中间的可以暴力做。
分析方法是考虑中间那部分被减后数值必然不多于原来的一半,所以最多暴力 \(\log c\) 次。
扫描线+DS
时间倒流
如果一个题,常见于图论,顺序加边的时候很难保证复杂度(或者删边,总是正向做)或者根本找不到一个多项式复杂度的做法,那么就可以考虑倒过来做,前提是可以离线。
矩阵加速分段优化
如果题目需要做分段矩乘,然后发现时间比较紧张,那么可以考虑预处理转移矩阵的二次幂,然后直接用 dp 向量来乘。然后在矩阵乘法的时候要注意如果某一次乘法肯定没有意义,那就直接省去一次循环,这样矩阵乘向量的复杂度是 \(O(siz^2)\) 的。
相当于平衡掉分多段的复杂度。
点权 Kruskal 重构树
目的相同,想要建立一棵在无关于长度,只有关于可达性的树的时候,想建立一棵有单调性的树。可是是点权的时候,没法用 Kruskal 的排序来做。
但是我们可以直接把点按点权排序,从点权优到劣排。然后开一个 vis[i]
数组,顺次枚举每个点,并枚举与它相连的所有点,然后如果某一个点的 vis[i]
是 \(1\),说明它的点权比当前点小,那么在用并查集判断完两个点的连通性之后,直接把当前点和那个点的根连一条边。最后就建出一棵 \(n\) 个节点的,满足普通重构树所有性质的树。
示例代码
iota(f+1,f+1+n,1);
iota(p+1,p+1+n,1);
sort(p+1,p+1+n,[&](int x,int y){return C[x]<C[y];});
rep(i,1,n){
for(int s:g[p[i]]){
int sf=find(s);
if(vis[sf]&&merge(sf,p[i]))
e[p[i]].pb(sf);
}vis[p[i]]=1;
}