摘要
复赛即将开始,今天将结合清北学堂NOIP训练营提高组笔记及黄哲威大牛的课件带大家了解信息学中常用的基础算法。适合刚接触信息学的同学学习理解。
主要涉及的基础算法有倍增,搜索、分治、贪心、暴力和构造。
基础算法
一、倍增算法:
定义:用f[i][j]表示从i位置出发的2j个位置的信息综合(状态)
一个小小的问题:为什么是2j而不是3j,5j,…?因为,假设为kj,整个算法的时间复杂度为(k-1)logk,当k=2时,时间复杂度最小。
这个算法的三个应用:
1.倍增ST表:
应用:这个ST表是用来解决RMQ问题(给你n个数,m次询问,每次询问[l,r]这个区间的最大值),当然,解决RMQ问题是可以用线段树来做的,但是比较麻烦,NOIP 80%是不会用线段树来做,还是用ST表方便。
定义:f[i][j]表示:从i到i+2j-1这2j个位置的元素最大值
初始化:f[i][0]=z[i](第i个位置到第i+20-1个位置的最大值,对应只有一个元素的区间)
转移:f[i][j]=max(f[i][j-1],f[i+2(j-1)][j-1]) (把[i,i+2j-1]这个区间分治为两个区间,这两个区间拼在一起就成了原来一个大的区间,两个区间长度都为2j-1)
//建立ST表,引自P2O5 dalao的blog:https://zybuluo.com/P2Oileen/note/816892#应用1-st表
for(int a=1;a<=n;a++) f[a][0]=z[a];//z[]为源数据区间数组
for(int j=1;j<=logn;j++)
{
for(int i=1;i+z^j-1<=n;i++)
f[i][j]=max(f[i][j-1],f[i+2^(j-1)][j-1]);
//乘方是伪代码!
}
//solve
ans=max(f[l][k],f[r-2^k+1][k]);
所以就有两种情况:
①假如区间长度(r-l+1)正好是2k,那么就直接访问f[l][k];
②假如区间长度(r-l+1)是2k+p(p<2k),也就说明2k≤(r-l+1)<2k+1,我们可以慢慢地分治下去,利用前缀和,就形成了树状数组那样的东西,一段区间的最大值为 划分成两段区间最大值max1,max2相比取较大 ,但是这样太慢。
有一种更好的方法:其实我们可以用两个长度为2k的区间就一定能把这段[l,r]区间完美覆盖起来,会有重复,但是对求最大值这件事情没有影响,所以 这段区间的最大值=max(f[l][k],f[r-2k+1][k])。
限制:不能用来计算区间和,因为中间有重复部分,还有就是:不支持修改ST表!
2.树上倍增LCA(最近公共祖先):
一般是用线性Tarjan算法来求解(这个Tarjan算法和图论中求有向图强连通分量的Tarjan不同,都是一个人发明的),但是在ZHX十年的信息学奥赛生涯中没有用到这个算法,原因有俩:①没遇到这样的题目②不会!(笑哭脸),有兴趣可以了解一下。
下面介绍倍增的算法:
定义:f[i][j]表示:从树上编号为i的节点向上走2j步会走到哪个节点。
初始化:从j=0开始考虑,也就是从i号节点向上走1步到达的节点,就是i节点的父亲,所以:f[i][0]=father[i]。
转移:f[i][j]=f[f[i][j-1]][j-1],表示:从i节点往上走2j-1步后,再往上走2j-1步到达的点,等价于向上走2j步,因为2j-1+2j-1=2j。(j的范围大概[20,30)≈nlogn,只要保证2j>节点数目n即可)
现在我们构造出来这个f数组,下面介绍如何求两个点的LCA:
定义数组depth[i]表示i这个节点的深度,有以下两种情况:
①depth[p1]==depth[p2],具有一个重要的性质:两个节点同时向上走同样地步数,深度仍然相等,也就是说,我们让p1,p2一步一步地往上走,当走到同一个点时候,这个点一定是LCA!
for(int x=19;x>=0;x--)
{
if(f[p1][x]!=f[p2][x])//如果没有走到同一个点,继续往上走
{
p1=f[p1][x];//p1往上跳
p2=f[p2][x];//p2往上跳
}
}
if(p1!=p2)//为什么要加这个判断?有可能p1=p2么?是有可能的!这个判断是防止一开始跳之前p1=p2这种情况
{
p1=f[p1][0];//因为上面的循环p1,p2只是走到了LCA的下方,这个判断只是处理最后一步:把p1或p2往上跳到它的父亲,就是LCA,返回即可
}
return p1;//p1为LCA,返回
②depth[p1]!=depth[p2],假如p1比较深,就让p1往上跳到和p2深度一样的地方。
利用倍增f数组p1往上跳的方法:定义往上走步数step=depth[p1]-depth[p2],再利用二进制转换!
举个栗子:假如step=13,转为二进制=1101,可以得出13=8+4+1,:先走8步,再走4步,再走1步就好了。
int get_lca(int p1,int p2)
{
if(dpeth[p1]
for(int x=19;x>=0;x--)
{
if((2^x)<=depth[p1]-depth[p2]) p1=f[p1][x];
}
}
下面是另一种写法思路就不多讲
int x=0;
while (p1!=p2)
{
if(!x||f[p1][x]!=f[p2][x])
{
p1=f[p1][x];
p2=f[p2][x];
x++;
}
else x--;
}
3.快速幂:
按照一般思路,我们要计算ax的话,要写一个for循环,计算a×a×a×a…这样太™麻烦并且浪费时间!
这里运用倍增来实现快速幂,这也是运用到了分治的思想。
我们要求出x(x=2×k)个a的乘积,就可以分解为x/2个a的乘积的平方,这样就省去一半计算量,如果x是奇数,就在原先的基础上×a就可以了。
int ksm(int a,int x)//求a^x的快速幂 时间复杂度:O(logx)
{
int ans=1;
while(x)
{
if(x&1) ans=(ans*a);//位运算:x&1==1则x为奇数
a=a*a;
x=x>>1;//位运算:右移一位,即将X除以2
}
return ans;
}
二、分治算法:
定义:将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。
这个算法的三个应用:
1.二分查找:
定义:给定排序数组,查询某个数是否在数组中
算法描述:在查找所要查找的元素时,首先与序列中间的元素进行比较,如果大于这个元素,就在当前序列的后半部分继续查找,如果小于这个元素,就在当前序列的前半部分继续查找,直到找到相同的元素,或者所查找的序列范围为空为止。
bool find(int x)//二分查找x是否在序列z[]中
{
left=0,right=n;
while(left+1!=right)
{
middle=(left+right)>>1;
if(z[middle]>=x) right=middle;
else left=middle;
}
}
还可以用lower_bound和upper_bound函数进行优化,用法详见下:
#include
#include //必须包含的头文件,C++特有的库函数
using namespace std;
int main()
{
int point[5]={1,3,7,7,9};
int ans;
/*两个函数传入的:(数组名,数组名+数组长度,要查找的数字),返回的是一个地址,减去数组名即可得到数字在数组中的下标*/
ans=upper_bound(point,point+5,7)-point;//按从小到大,7最多能插入数组point的哪个位置
printf("%d ",ans);//输出数字在数组中的下标
ans=lower_bound(point,point+5,7)-point;按从小到大,7最少能插入数组point的哪个位置
printf("%d\n",ans);//输出数字在数组中的下标
return 0;
}
/*
output:
4 2
*/
2.归并排序(nlogn):
是分治法的典型应用。
归并过程:
比较a[i]和b[j]的大小,若a[i]≤b[j],则将第一个有序表中的元素a[i]复制到r[k]中,并令i和k分别加上1;
否则,将第二个有序表中的元素b[j]复制到r[k]中,并令j和k分别加上1。
如此循环下去,直到其中一个有序表取完,然后再将另一个有序表中剩余的元素复制到r中从下标k到下标t的单元
归并排序的算法我们通常用递归实现,先把待排序区间[s,t]以中点二分,接着把左边子区间排序,再把右边子区间排序,最后把左区间和右区间用一次归并操作合并成有序的区间[s,t]。
3.快速排序(nlogn):
一般在使用时候直接调用快排函数。
sort(快速排序,是C#特有的,不会退化为n2,比下面三个都要快,一般使用这个)
qsort(最坏情况下为n2,最好是n,期望为nlogn)
merge_sort(归并排序,稳定为nlongn)
heap_sort(堆排序,稳定为nlongn)
三、贪心算法:
算法思想:在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
经典例题:选最大(野题)
给你N个数,要求你选出K个数使他们的和最大。
思路:我们进行K次贪心,每次我们都“贪”剩下的所有数中最大的,选择后拿走,这样就能保证我选的K个数总和最大。做法就是把N个数从大到小排序选择前K个即可。
经典例题:国王游戏(Luogu 1080)
恰逢 H 国国庆,国王邀请 n 位大臣来玩一个有奖游戏。首先,他让每个大臣在左、右手上面分别写下一个整数,国王自己也在左、右手上各写一个整数。然后,让这 n 位大臣排成一排,国王站在队伍的最前面。排好队后,所有的大臣都会获得国王奖赏的若干金币,每位大臣获得的金币数分别是:排在该大臣前面的所有人的左手上的数的乘积除以他自己右手上的数,然后向下取整得到的结果。
国王不希望某一个大臣获得特别多的奖赏,所以他想请你帮他重新安排一下队伍的顺序,使得获得奖赏最多的大臣,所获奖赏尽可能的少。注意,国王的位置始终在队伍的最前面。
思路:此题要用到高精度,考虑最后一个大臣,显然他很可能是金币最多的人。我们要让他的金币尽量的少。之前的大臣左手的数肯定会乘起来,所以我们要使S/A/B尽量的大。(设S是左手所有数的乘积),即让A*B尽量的大。选完最后一个后,我们把他踢掉,然后再找一个最后的大臣。如此往复,相当于就是对A*B排序。
基于交换的证明:假设两个相邻的大臣i,j,而前面的人总乘积为S。
当i在j前面时,设ans1=max(s/i.b,s*i.a/j.b);
当j在i前面时,设ans2=max(s/j.b,s*j.a/i.b);
所以要使得i在j前面,必须要ans1
ZHX给的一个小小的Tip:
当我们做贪心时候不知道按照什么来排序,这时候就可以慢慢试,按照a×b、a+b、ab…这些条件来排序,看看哪个计算出来的结果最符合标答^_^,真。。。是个有用的Tip啊!
四、搜索算法:
1.三种不同的搜索问题:
①最优性问题:例如:有N个数,从中选出K个数满足和最大。
②计数问题:例如:有N个数,从中选出K个数的方案数目有多少。
③方案问题:例如:有N个数,从中选出K个数满足已知条件,输出其中M种方案,一般这第三类问题可以在第二类问题中解决。
2.两种不同的搜索方法:
①BFS(广度优先搜索):目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。
②DFS(深度优先搜索):其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。
3.搜索优化技巧:
①剪枝:当搜索到一个状态时候,发现当前状态不满足条件,就不继续往下搜,而不是等到搜索完毕才判断是否满足条件。
②BFS—meet in the middle:双向搜索(前提是要知道最终状态的样子)
举个栗子:我们有一个满三叉树,深度为k,我们搜索到最后一层的状态数为3k-1,这样要花费的时间非常多。
已知了起始状态和最终状态,要用BFS求出起始到最终所经过的那些状态,其实可以从起始状态往后走k/2个状态(k为初末总状态数),再从最终的N个状态往前走k/2个状态,一定会在中间某个节点相遇,这样联通起了整个状态。
时间、空间复杂度从O(X)变为O(sprt(X))。
③重复结构利用(计数问题):这个我不会~
④卡时(最优化问题):我们在面对求最优值问题的时候,如果找到一个更优的值,更新一下ans的值,但是如果你还没搜索完毕,时间已经快要到达上限了,这时候就会爆0,意思就是要在程序运行时间快要到达题目限制时候,把当前我们找到的ans输出来!这样你就会从“得0分”变成“至少得0分”!超级有用啊啊!!
#include
#include //exit()要用到的库函数
#include //clock()去官网看能不能用,否则就会变成至多零分了……
int t;//程序开始运行时候当前系统时间
void dfs()
{
/*假如题目限制时间为2000ms*/
if(clock()-t>=1995)//程序运行时间=程序执行到此当前系统时间-程序开始系统时间(以ms为单位)
{
shuchujie();//输出答案(保守一点可能需要5ms传入输出函数并输出)
exit(0);//直接销毁程序!
}
else
{
;//继续往下搜
}
}
int main()
{
t=clock();//记录一下当前时间
return 0;
}
⑤数据结构优化:一般不会出到那么难(笑哭脸)
五、奇怪的东西:
越来越有用的东西——读入优化(可以加快读入int的速度):
我们平时从一个文件里面读入一百万个数数据,基本接近一秒了(那还玩个P啊!),读入优化基本思想就是:把读入的数据拆成一个字符一个字符的来读入,因为字符读入比int快~
下面给出了读入优化int的代码,直接调用就可以了噢~当然只限于读入非负整数,如果是负数、小数、科学计数法的话还要加上一些判断。
这个什么原理相信各位看官稍加思考便可看明白。
#include
int getint()
{
char c=' ';
while(c'9') c=getchar();
int x=0;//x为这个数字的前缀
while(c>='0'&&c<='9')
{
x=x*10+c-'0';
c=getchar();
}
return x;
}
int main()
{
int a;
a=getint();//调用读入优化
printf("%d\n",a);
return 0;
}
本文所有内容经作者授权发布,未经允许不得转载!
往期精选内容
(点击标题即可查看)
CCF关于CSP-J/S2019第二轮认证报名的通知
第五届全国青少年创意编程与智能设计大赛获奖名单公示
CSP-J/S认证第一轮初赛S、J组答案
逾10万人参加首次CSP-J/S认证,附CSP-S/J组初赛试题及参考答案
NOI2019笔试题 在线版刷题题库发布!
CSP-J/S 各省负责人联系方式!
CSP-J/S2019认证须知
NOIP2015-2018普及组&提高组初赛试题答案&解析
CCF NOI教材-提高篇面世啦!
CCF发布NOI2020省队选拔规定
学好信竞-浅谈信息学竞赛考场策略及程序测试
CCF推出CSP非专业级别的能力认证,分入门级和提高级!
详细盘点清华姚班 智班,北大 浙大图灵班等多所高校AI专业实力!
大牛云集!清华大学2019年姚班及智班第一届AI本科生名单公布!
清北学堂C++入门公益课视频已上传!
清华北大2019自主招生入选资格考生名单公布
清华大学计算机姚班、人工智能学堂班(智班)选拔方案出炉!
NOI2019获奖规则公布,前50名选手将自动入选国家集训队!
NOI2019仍有高校现场签约 现场招生规则发布(附各省队名单)!
2019年NOI省队选拔规定及NOI2019名额分配方案发布!
2019年全国69所高校保送生拟录取名单公布,清华北大共349人!
CTS2019 获奖名单分析-信息学强省强校排行榜!
APIO2019中国区获奖名单公布,学军中学 杭州二中等表现出色
五大学科竞赛国家队23人大名单出炉(各省,中学分布统计)
IOI2019(国际信息学奥林匹克)中国队名单出炉
最详细解析低分进名校三大途径:自主招生、综合评价、高校专项计划!
2019年保送生资格名单公示
再见,OI-大牛HZW亲笔,分享OI生涯记录,不变的是坚持和热爱!
NOIP复赛知识点简述及复赛算法总结!
重磅!NOIP2初018赛普及组提高组真题及答案发布
NOIP2018提高组试题解析
根据信息学竞赛之路带你了解信息学竞赛流程
全国自主招生高校对各项学科竞赛奖项要求大汇总!签约路径
教育部出台高中新课标,信息学竞赛相关内容被编入必修课程!
从搜狗CEO王小川(信息学金牌),看这二十几年中国奥赛金牌的去向
揭晓高薪专业排行榜,计算机专业薪资最高!哪些专业最具潜力?
一个清华保送生妈妈对竞赛的感受,自主招生家长都要看看!
计算机科学与技术专业全国大学排行榜!
为什么这些孩子初中就能被清华北大签约
(1) 为什么有“编程思维”和数学能力强的人更优秀?(2)清北独家录制NOIP成功者说学习视频!!!
(3)我们为什么要对孩子进行编程教育?
(4)信息学竞赛答家长问题
1.信息学竞赛,你想了解的知识都在这里
2.信息学奥赛(NOIP)初赛学习方法推荐
3.信息学奥赛(NOIP)复赛学习方法推荐
4.大牛为你推荐十本最适合信息学竞赛的书籍
5.信息学奥赛有那么重要吗?
6.参加编程竞赛对实际工作的用处
7.清北学堂独家录制NOIP考试技巧讲座
8.在线编程挑战赛第一名:我是这么学算法的
9.信息学竞赛如何学习及准备攻略!
10.凭什么我得了信息学奥赛国家一等奖
11.榜样 | 北大降200分要这个诸暨天才少年
12.OI金牌教练胡芳:爱和成长的故事
13.信息学竞赛,一个让孩子不需要再去挤独木桥的方向
14.新学期必须了解的学科竞赛与自主招生时间!
15.北大录取生陈代超:在信息学中找到“思维图谱”
16.国务院发文支持编程教育进入中小学,中国人工智能厚积薄发
(1)NOIP报名,申诉,查成绩方式介绍
(2)NOIP复赛考前需要注意的那些事儿!!!
(3)NOIP测评环境,数据提交你都了解吗?请注意这些问题!
(4)关于NOI系列赛编程语言使用限制的规定
关注「信息学竞赛」
看更多信息学趣闻与知识
↓↓↓