常见 trick 总结
1. 前言
我随机选取了一些我做过的绿、蓝、紫、黑题,总结出了一些 trick。
2. 正文
2.1 剪枝
2.1.0 介绍
剪枝一般用于搜索当中,可能是因为其作用相当于“剪去”搜索树上的枝条而得名。
剪枝使用得当,可以大大加快搜索的速度,有时甚至能达到出乎意料的结果。
2.1.1 剪去不合法状态
如标题,当一个状态不合法的时候,直接放弃搜索这个状态以及它的所有后继状态。
当然,这个方法仅适用于这个不合法的状态的所有后继状态都不合法的情况,正确性显然。
这个技巧给我留下的印象十分深刻,因为我前几天做题时,有一个数据点在我剪枝后,时间从超时变成了 70 ms。
剪枝前:
剪枝后:
推荐题目: P1763 埃及分数
2.1.2 记忆化
记忆化,就是将已经得到过的状态的信息存储起来,防止每次重复搜索,浪费大量时间。
最简单的记忆化思想可以体现在递归写法的快速幂上:
typedef long long ll;
ll fastpow(ll base,ll k){
if(k==1)return base;
ll num=fastpow(base,k/2);
return num*num*(k%2?base:1);
}
注意,我们这里使用了一个
问题来了,为什么不使用下面的写法呢?
typedef long long ll;
ll fastpow(ll base,ll k){
if(k==1)return base;
return fastpow(base,k/2)*fastpow(base,k/2)*(k%2?base:1);
}
让我们仔细分析一下这个写法的复杂度:搜索树每增加一层,层结点数翻倍,而一共有
而若是记忆化了之后,每层节点数不变,因此总复杂度为
有多少人初赛被坑了?
记忆化的具体体现:
以下举两个例子:数位 DP 和杜教筛。
数位 DP:
数位 DP 的状态设计一般都比较复杂,因此大部分人会使用记忆化搜索。
以 P2657 [SCOI2009] windy 数 为例,给出数位 DP 的记忆化搜索写法:
typedef long long ll;
ll a,b,dp[20][20][2],numb[20];
void init(){memset(dp,-1,sizeof(dp));}
ll dfs(ll k,ll num,bool mx,bool zero){
ll r=0;
if(!k)return 1;
if(!mx and ~dp[k][num][zero])return dp[k][num][zero];
ll maxn=mx?numb[k]:9;
for(int i=0;i<=maxn;i++){
if(abs(i-num)<2 and !zero)continue;
r+=dfs(k-1,i,mx and i==maxn,zero and i==0);
}
if(!mx)dp[k][num][zero]=r;
return r;
}
发现,如果待搜索的状态已经被搜索过了,就不再继续搜索,直接返回结果。而在搜索到了一个新状态之后,记录搜索的结果。
这样,便可以保证总搜索次数小于等于状态数(也就是你 DP 数组的大小)。
推荐题目:好多数位 DP 题呢。
杜教筛:
不会杜教筛也没事,直接看核心代码:
typedef long long ll;
ll get_epsilonsum(ll n){return 1ll;}
map<ll,ll> mumem;
ll get_musum(ll n){
if(n<(ll)MAXN)return mu[n];
if(mumem[n])return mumem[n];
ll r=get_epsilonsum(n);
for(ll x=2,y;x<=n;x=y+1){
y=n/(n/x);
r-=(y-x+1ll)*get_musum(n/x);
}
return mumem[n]=r;
}
发现,这里使用了一个 map 来记忆化,不惜使用
推荐题目:杜教筛的例题还有推荐的必要吗(恼。
2.2 状态压缩
2.2.0 介绍
状态压缩,即用某种东西将不同状态压缩起来,达到更易存储或更易计算的效果。
状态压缩使用得当,可以使很复杂的代码简单化,提高代码可读性,且能让思路更为清晰。
2.2.1 例题
2.2.1.0 前言
状压方法各有千秋,但最多的是使用二进制位存储信息。
2.2.1.1 状压优化 DP——以互不侵犯为例
P1896 [SCOI2005] 互不侵犯
相信大多数人的状压 DP 都是从这道题开始的。
题目要求在棋盘上放上国王,使得两两不能将军。
最基础的状压思想是:用 1 表示一个位置放置了国王,0 表示一个位置没有放置国王,用二进制数存储棋盘信息。
这样,就可以很方便地找出每一行的合法放置方案。
那么如何判断行与行之间是否合法呢?
很容易想到,只有两个国王在各自行的位置相同或是相差一位时,才不合法。于是想到使用二进制数的左右移运算,以及按位与运算,便很容易判断两行是否合法。
多么便利。
2.2.1.2 状压配合暴力——以枚举子集为例
很多题目的暴力分中,都会留出枚举子集的分数。
不过使用递归显然不够方便,不妨使用状压。
二进制每一位存储是否选出该位对应的元素,1 为是,0 为否。这样每一个二进制数都对应了一种子集。
因此,在二分的时候,可以直接取
而且这种写法还有一个好处在于:你可以将下界设为
2.3.2 尾言
其实不管是单调还是单峰,都可以使用一个很哇塞的算法来做——模拟退火。
事实上,就算不是单峰,而是多峰,都可以。
不过本博客不准备讲模拟退火,因为这是 LZMkingNB 的绝活,建议让他讲。
2.4 随机化
2.4.0 介绍
随机化并不是一种算法,而是很多种算法。
不过同前文所讲,不准备讲模拟退火,当然遗传算法也不会讲(主要是不会)。
言归正传。
随机化算法都有一个共同的特点——不保证能完全正确,但是有一定的正确率。不过只要有正确率就够了,因为我们完全可以多跑几遍来提高正确率。
当你想不出正解时,随机化算法往往能发挥奇效。
2.4.1 例题
2.4.1.0 前言
字符串哈希不讲。
2.4.1.1 和哈希——以星战为例
进行接下来的讲解前,需要作出一些约定:
-
称一张图为
G=(V,E) ,其中V 为这张图的点集,E 为这张图的边集。 -
称一张图的点数为
|G| 。 -
称一个点
v\in V 的出度为d^{+}(v) ,入度为d^{-}(v) 。
P8819 [CSP-S 2022] 星战
相当熟悉的题目了,就是去年考过的原题。
题目简化后发现,就是给出一些删边加边的操作,让我们判断当前图是否满足下面的条件:
-
所有点的出度为
1 。 -
从任意一个点出发,都可以走到一个环里面去。
仔细分析之后发现这种情况很难出现,因此直接输出 NO。
仔细分析之后发现,只要满足了条件 1,条件 2 就自动满足了。这是个好消息,意味着我们只需要判断所有点的出度是否为
不难发现,对于一个图
那么能否通过这一点判断图是否满足要求呢?
发现:
既然没有充分性,不妨就让它尽量有充分性。
考虑为每个点赋一个随机权值
观察发现,第一个必要条件即为所有
既然第一个必要条件不充分,那么第二个充分吗?
发现这个式子的解与
既然有很大概率充分,那不妨就将其当成充分条件使用。
这个式子不管是左边还是右边都非常好维护,甚至不需要建图。
2.4.1.2 摩尔投票——以 Destiny 为例
摩尔投票指的是这一类问题:
有一群政客,它们在竞选某种职位,只有某个政客的选票数大于所有政客的选票数的一半时,那个政客才能当选。
给出每个选民支持的政客,要求找出是否有政客能够当选,如果有,输出是哪个政客。
摩尔投票问题有专门的摩尔投票法解决,不过既然这一节讲的是随机化,那么自然要用随机化解决。
考虑以下的过程:
如果有某个数字出现的次数大于一半,那么随机从这堆数字中抽出一个数字,抽到目标数字的概率大于
于是考虑先统计出所有数字出现的次数,然后随机抽取一个数字检验其是否为答案——若是,直接输出;反之继续。重复上述过程 100 次。
由于一次检测到答案的概率大于
这个时候有人要说了:你都把数字出现的次数统计出来了,为什么不直接输出答案呢?
这个别管,你就说是不是
CF840D Destiny
来看这道题,是摩尔投票的变式,只不过是加了一个区间查询而已。
和上面描述过的做法一致,随机抽取区间内的数字,然后判断是否是答案,跑 100 遍,取最小,出错概率小于
至于如何统计区间内数字出现个数:可以将询问离线下来排个序,然后慢慢加。
这样,一道主席树紫题就被轻松解决了。
2.4.1.3 随机染色——以 Tourism 和假期计划为例
CF1310D Tourism
先考虑一下不要求没有奇环的做法:是不是 DP 就行了?设
那要是有奇环怎么办呢?
很不容易想到一个性质:二分图上没有奇环。
基于这个性质,我们可以强制将图变成二分图——具体地,考虑给每一个节点随机染上黑/白两色,然后把两端点颜色相同的边删掉,这样剩下的图就是一个二分图了,在这个图上跑 DP 就不需要考虑奇环的问题了。
既然是随机染色,所以不妨多跑几遍。发现
P8817 [CSP-S 2022] 假期计划
这道题的正解是枚举中间两个节点,不过我们依然可以随机染色。
具体地,先给图上能够在
然后给新图上的节点随机染上四种颜色
每次有
2.4.2 尾言
像上面介绍的需要跑多次的随机化算法,可以配合卡时食用,效果更佳。
2.5 枚举
2.5.0 介绍
这里的枚举并不是指暴力枚举,而是在一道题目中变量很多的时候,考虑枚举其中某些变量,把那些变量当作常量求解,往往可以使问题更加简单。
2.5.1 枚举——以假期计划为例
又是假期计划。
P8817 [CSP-S 2022] 假期计划
刚刚介绍了这个题目的随机化做法,那么这一节介绍其正解。
发现一条路线中涉及四个节点,实在是太多了,不妨考虑枚举中间的两个节点,然后求解。
发现枚举中间的两个节点后,问题转变为了求出另外两个节点,既可以到 1 节点,又可以到中间的节点。那么问题一下就变得非常简单了,直接计算出能够到达中间两个节点并且能够到达 1 节点的所有节点中权值最大的几个,然后更新答案即可。
具体有一些实现细节这里不提。
2.5.2 尾言
对于一个题目,只要确定了枚举哪个变量,接下来的步骤就会非常简单了,非常好用。
比赛中这个方法的瓶颈往往在于你想不到,而不是你写不出。
2.6 根号
2.6.0 介绍
莫笑分块没头脑,要说好处真不少。
又好写啊又好调,代码简单常数小。
对数方法真难找,考虑根号没烦恼。
管他淀粉线段树,直接分块就没了。
根号算法——优雅的暴力
2.6.1 例题
2.6.1.0 前言
不讲分块和莫队,一是没怎么写过,不太熟练;二是这是 mori_ 的绝活,要讲让他讲吧。
2.6.1.1 块状链表——以书架为例
块状链表的具体介绍可以见我写的 这篇博客。
P2596 [ZJOI2006] 书架
一道平衡树的题目,我为这个题目写了一篇题解,所以就不在这里赘述方法了。
题解通道
顺带一提,这道题目我用块状链表的提交记录如下:
一个同学用平衡树的提交记录如下:
这就是小常数的魅力。
2.6.1.2 数据分治
一个题目,你想不出正解,但是你有两种暴力做法:一种的复杂度为
我在一万年前的一次考试中遇到了这种情况,当时我比较蠢,没有想出正解,只想出了复杂度如上述的两个暴力。
但是我当时又比较聪明,我想到——如果在
于是我靠着两个暴力过了那道题。。。
这就是数据分治。
2.6.2 尾言
其实根号算法主打的是一个均摊思想,只要熟练运用了,所有根号算法都不难理解。
2.7 打表
2.7.0 介绍
如果说上一节是教你暴力出奇迹,那么这一节教你打表拿省一。
好吧不一定能拿省一,但一定能教你怎么打表。
2.7.1 例题
2.7.1.0 前言
普通打表……真的还用教吗?直接写个爆搜然后挂后台挂四个小时即可。
不过 €€£ 有代码长度小于 100 k 的限制,因此如果表太长了照样会超时。
这个时候,允许我介绍——分块打表。
2.7.1.1 分块打表——以数 7 为例
P1662 数7
看上去是一个朴实无华的模拟,但是一看数据范围——
这下没办法模拟了,对吧,毕竟这样的数据范围一看就是故意卡模拟。
不过我们一身反骨,出题人不想让我们写模拟,我们就偏要写模拟。
想想,模拟就好比跑步,一次性跑到目的地肯定累死,而我要是在路线中放上很多传送点,每次跑步先传送到距离目的地最近的传送点,再开始跑,不就轻松多了吗?
模拟也是一样,我们不把所有位置的表都打出来,而是只打“传送点”的表,然后每次模拟从距离目标位置最近的“传送点”开始模拟。
只需要每隔
如果空间是在比较紧张,也可以适当调整一下块长,不一定要
这种方法也可以看作一种以空间换时间。
3. 尾言
到这里,我总结的所有 trick 都已经讲完了,可能还有一些遗漏,不过我记不起来了。
在考场上,如果能够用到这些 trick,一定可以给你带来莫大的好处。
祝各位神犇 AK NOIP。