分享

NOIP复赛复习(十二)基础算法巩固与提高

 长沙7喜 2018-04-16

定期推送信息学新闻竞赛自主招生信息学专业知识信息学疑难解答融科教育信息学竞赛培训等诸多优质内容的微信平台,欢迎分享文章给你的朋友或者朋友圈

一、倍增算法:

 

定义:用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]表示:从ii+2j-12j个位置的元素最大值

初始化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表,引自P2O5dalaobloghttps:///P2Oileen/note/816892#应用1-st

for(inta=1;a<=n;a++) f[a][0]=z[a];//z[]为源数据区间数组

for(intj=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不同,都是一个人发明的),但是很难用到这个算法,原因有俩:没遇到这样的题目不会!(笑哭脸),有兴趣可以了解一下。

下面介绍倍增的算法:

定义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(intx=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];//因为上面的循环p1p2只是走到了LCA的下方,这个判断只是处理最后一步:把p1p2往上跳到它的父亲,就是LCA,返回即可

}

return p1;//p1LCA,返回

 

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,intp2)

{

    if(dpeth[p1]

    for(int x=19;x>=0;x--)

    {

    if((2^x)<=depth[p1]-depth[p2])p1=f[p1][x];

    }

}


下面是另一种写法思路就不多讲,YY一下就可以出来的啦~

 

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/2a的乘积的平方,这样就省去一半计算量,如果x是奇数,就在原先的基础上×a就可以了。

 

int pow(int a,int x)//a^x的快速幂时间复杂度:O(logx)

{

    int ans=1;

    while(x)

    {

        if(x&1) ans=(ans*a);//位运算:x&1==1x为奇数

        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_boundupper_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]中,并令ik分别加上1

否则,将第二个有序表中的元素b[j]复制到r[k]中,并令jk分别加上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个即可。


经典例题:国王游戏(Luogu1080

恰逢 H 国国庆,国王邀请 n 位大臣来玩一个有奖游戏。首先,他让每个大臣在左、右手上面分别写下一个整数,国王自己也在左、右手上各写一个整数。然后,让这 n 位大臣排成一排,国王站在队伍的最前面。排好队后,所有的大臣都会获得国王奖赏的若干金币,每位大臣获得的金币数分别是:排在该大臣前面的所有人的左手上的数的乘积除以他自己右手上的数,然后向下取整得到的结果。

国王不希望某一个大臣获得特别多的奖赏,所以他想请你帮他重新安排一下队伍的顺序,使得获得奖赏最多的大臣,所获奖赏尽可能的少。注意,国王的位置始终在队伍的最前面。 

思路:此题要用到高精度,考虑最后一个大臣,显然他很可能是金币最多的人。我们要让他的金币尽量的少。之前的大臣左手的数肯定会乘起来,所以我们要使S/A/B尽量的大。(设S是左手所有数的乘积),即让A*B尽量的大。选完最后一个后,我们把他踢掉,然后再找一个最后的大臣。如此往复,相当于就是对A*B排序。

 

基于交换的证明:假设两个相邻的大臣i,j,而前面的人总乘积为S

ij前面时,设ans1=max(s/i.b,s*i.a/j.b);

ji前面时,设ans2=max(s/j.b,s*j.a/i.b);

所以要使得ij前面,必须要ans1,所以:按照ai*bi排序,条件为x.a*x.b才是正解!

 

Tip

 

当我们做贪心时候不知道按照什么来排序,这时候就可以慢慢试,按照a×ba+bab这些条件来排序,看看哪个计算出来的结果最符合标答^_^,真。。。是个有用的Tip啊!

 

四、搜索算法:

 

1.三种不同的搜索问题:

 

最优性问题:例如:有N个数,从中选出K个数满足和最大。

计数问题:例如:有N个数,从中选出K个数的方案数目有多少。

方案问题:例如:有N个数,从中选出K个数满足已知条件,输出其中M种方案,一般这第三类问题可以在第二类问题中解决。

 

2.两种不同的搜索方法:

 

BFS(广度优先搜索):目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。

DFS(深度优先搜索):其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。

 

3.搜索优化技巧:

 

剪枝:当搜索到一个状态时候,发现当前状态不满足条件,就不继续往下搜,而不是等到搜索完毕才判断是否满足条件。

BFS—meetin 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;

}


另:为感谢各位家长一直以来对融科信息学的信任与支持,在双十二来临之际,特推出双十二底价团购信息学课程,详情点击链接→融科教育双十二同学“惠”,信息学课程底价疯狂抢!(←点击链接,了解活动详情吧)

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多