Loading

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 个。

这时候,对原数列分块,然后块间建线段树,然后块内暴力构造矩阵和求解。虽然会慢一点,但是最终空间是够的,时间也能过去。

Code

前缀优化建图

一种优化时空的建图方式。我们考虑如果每次要求一个点对一堆点连边,除了线段树优化建图以外,可以考虑是否可以用前缀优化建图。

比如说在 2-sat 中的前缀优化建图:

\(n\) 个点,分成 \(k\) 部分,\(m\) 条限制表示两个点至少选一个,每部分恰选一个。求有无可行选法。

一般我们会把一个点拆成两个,表示选或者不选。现在我们拆成 \(4\) 个,表示选或者不选,以及前缀中是否有选,后缀中(不包括本身)是否有选。
这样一来,我们对一堆点连边就变成了向一个点连边,那么对于一堆的点,我们需要预先连一些边。

  1. 如果一个点选了,那么它前缀中肯定有选了;
  2. 如果后缀 \(i\) 有选,那么 \(i\) 这个点不能选(同一堆中不能一起选);
  3. 如果前缀 \(i\) 有选,那么前缀 \(i+1\) 有选;
  4. 如果后缀 \(i+1\) 有选,那么后缀 \(i\) 有选;
  5. 如果 \(i\) 选了,那么 \(i-1\) 的后缀选了;
  6. 如果前缀 \(i\) 有选,那么 \(i+1\) 不选(后面的点交给前缀 \(i+1\))。

这样会得到这样的图:

然后直接连边就可以了。

或者考虑网络流的优化,具体见这题

O(logn) 分解质因子

众所周知每个数都只有 \(\log n\) 级别的质因子。那么的话我们可以利用线性筛每个数都是用它的最小质因子筛掉的,记录一下,然后分解的时候每次除以这个最小质因子就可以精准获取质因子,也就是 \(O(\log n)\) 分解了。

斐波那契前缀和公式

\[\sum_{i=0}^nFib_i=Fib_{n+2} \]

太神奇了,二次以上也是有公式的。

C++ 语法

求一个数组中连续区间的最值:

*max_element(a+l,a+r+1)

向上取整的数论分块

你可以这样:

\[\lceil\dfrac{x}{y}\rceil=\lfloor\dfrac{x-1}{y}\rfloor+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}\)

变魔术: 我们把这两个式子联立起来!得到:

\[dp_{i-1,j}\times p^j+dp_{i-1,j-1}\times (1-p)^{i-j}=dp_{i-1,j}\times (1-p)^j+dp_{i-1,j-1}\times p^{i-j} \]

然后把 \(i-1\to i\),得到:

\[dp_{i,j}\times p^j+dp_{i,j-1}\times (1-p)^{i+1-j}=dp_{i,j}\times (1-p)^j+dp_{i,j-1}\times p^{i+1-j} \]

接着移项,合并,除过去得到:

\[dp_{i,j}=dp_{i,j-1}\times \dfrac{p^{i+1-j}-(1-p)^{i+1-j}}{p^j-(1-p)^j} \]

然后就把 \(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;
}
posted @ 2022-03-08 14:34  ZCETHAN  阅读(153)  评论(1编辑  收藏  举报