【复习】冲击NOIP2016普及组复赛

本文详细介绍了NOIP2016备考所需的多种算法和数据结构知识,包括数学思维题、高精度计算、排序算法、递推递归、分治算法、贪心算法、动态规划等,并对各种算法进行了深入浅出的讲解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    马上就是NOIP2016普及组复赛了,要好好复习一下学过的知识~

    如果你想要看详解,请点击

    1、数学思维题:

    主要是一些考验逻辑思维的题目,比如五户共井问题,是枚举的问题,但是神烦的是不知如何枚举,重点是能不能想到巧妙的办法,OpenJudge上的小学奥数里有很多这种题目,不算太难

<-------------------------------------------------------------------------------------------------->
    2、高精度计算(加,减,乘,除,取余)
    高精度算法还是算比较简单,五种运算难度有所不同,浮点数也会增加难度,试着分析一下:

    (1)加

    加法很简单,最多考虑一下进位……浮点数也不难……只是如果有负数的话需要与减法进行互化

    样例代码:

/*注意:没有考虑负数,小数及前导零*/
/*初始化*/ 
char CA[maxn+5],CB[maxn+5];
int A[maxn+5],B[maxn+5],C[maxn+5];
scanf("%s%s",CA,CB);
int lena=strlen(CA);
int lenb=strlen(CB);
int lenc=max(lena,lenb); 
for(int i=0,j=lena;i<lena&&j>0;i++,j--)
{
	A[j]=CA[i]-'0';
}
for(int i=0,j=lenb;i<lenb&&j>0;i++,j--)
{
	B[j]=CB[i]-'0';
}
/*加法过程*/
for(int i=1;i<=lenc;i++)
{
	C[i]+=A[i]+B[i];
	if(C[i]>10)
	{
		C[i+1]+=C[i]/10;
		C[i]%=10;
		if(i==lenc)
		{
			lenc++;
		}
	}
}
for(int i=lenc;i>=1;i--)
{
	printf("%d",C[i]);
}

    (2)减

    同加法,也很简单,负数要考虑互化

    样例代码:

/*注意:没有考虑和或运算数为负数,小数及前导零*/
/*初始化*/ 
char CA[maxn+5],CB[maxn+5];
int A[maxn+5],B[maxn+5],C[maxn+5];
scanf("%s%s",CA,CB);
int lena=strlen(CA);
int lenb=strlen(CB);
int lenc=max(lena,lenb); 
for(int i=0,j=lena;i<lena&&j>0;i++,j--)
{
	A[j]=CA[i]-'0';
}
for(int i=0,j=lenb;i<lenb&&j>0;i++,j--)
{
	B[j]=CB[i]-'0';
}
/*减法过程*/
for(int i=1;i<=lenc;i++)
{
	C[i]+=A[i]-B[i];
	if(C[i]<0)
	{
		C[i+1]--;
		C[i]+=10;
	}
}
int o=0;
for(int i=lenc;i>=1;i--)
{
	if(o||C[i])
	{
		printf("%d",C[i]);
		o=1;
	}
}

    (3)乘

    整数乘法最为简单,包括负数,只需绝对值相乘即可,但浮点数较为困难,小数部分与整数相乘时,需要特殊处理

    (4)除

    除法偏难,高精度除以低精度还好,高精度除以高精度就有点难,要比较大小,不停去减

    (5)取余

    通过除法除完之后取剩余即可

<-------------------------------------------------------------------------------------------------->
    3、几种排序算法的比较和灵活运用
    排序还算好……主要是三种时间:O(n),O(n^2),O(n*log2 n)

    (1)冒泡,选择

    难度较低,时间较高,数据水的时候可以用用,很方便~

    (2)快排,归并

    快排好像不怎么稳定……但是有sort,所以很方便,最常用!归并排序较难写,但是稳定且能很容易求逆序对,也很不错

    (3)桶排序

    很快的排序,但由于不懂得去一一映射,所以很消耗空间,也无法输出编号神马的,还是不如(至少对我来说——呵呵)快排

<-------------------------------------------------------------------------------------------------->
    4、递推递归题
    这个没什么好讲的,让我们一起巧(bao)妙(li)地递归递推吧!
<-------------------------------------------------------------------------------------------------->
    5、分治算法(归并排序,求逆序对,二分思想的灵活运用)
    没有学得很好,所以重点复习

    (1)归并

    归并作为二分排序之一,是分治的典型运用(当然快排也是),可以很方便地求逆序对!方法也简单,在归并时,一旦有交换情况,交换时,应该逆序对增加,增加多少呢?正常序列还有多少加多少……归并过程如下:

    ①判断能否分为两部分,如果可以分成两部分,进行第二步

    ②分成左半部分和右半部分,分别进行排序,然后进行第三步

    ③合并左右两部分,每遇到右半部分最小数小于左半部分最小数,则左半部分还有几个数,逆序对总数增加几(因为最小的都要大与别人,后面的肯定也不会小),之后进入第四步

    ④将合并结果保存,返回
<-------------------------------------------------------------------------------------------------->
    6、贪心算法

    贪心很简单,只要找到贪心思路便没有问题了……(不要打我)好吧by the way,按关键字排序可以省很多事儿(见第3章)
<-------------------------------------------------------------------------------------------------->
    7、动态规划(几种背包问题,最长不下降序列,石子归并,最长公共子序列)
    动态规划有很多难题,要好好复习

    (1)背包问题

    ①01背包

    很简单,就不啰嗦了~只需要双重循环即可,空间复杂度也不高

    样例代码:

for(int i=1;i<=n;i++)
{
	for(int j=m;j>=w[i];j--)
	{
		f[j]=max(f[j],f[j-w[i]]+v[i]);
	}
}

    ②完全背包

    01背包稍加修改,即可将其转化为完全背包(循环方式改变)

    样例代码:

for(int i=1;i<=n;i++)
{
	for(int j=w[i];j<=m;j++)
	{
		f[j]=max(f[j],f[j-w[i]]+v[i]);
	}
}

    可以看出,01背包与完全背包的区别是循环方式,因为完全背包可以无数次装的特性,所以顺推,使得一种物品可以随意装多少件都可以,巧妙地减少了循环次数

    ③多重背包

    将多重背包转化为01背包即可,通过二进制方法,如:最多拿3件,则分成1件和2件;最多拿10件,则分成1件,2件,4件和3件(因为其他的任何件数均可通过这些件数的组合达到,节省了很多空间)

    样例代码:

/*初始化*/
int k=1,x; 
for(int i=1;i<=n;i++)
{
	for(x=1;x*2<=p[i];x*=2)
	{
		nw[k]=x*w[i];
		nv[k]=x*v[i];
		k++;
	}
	if(x<p[i])
	{
		nw[k]=(p[i]-x)*w[i];
		nv[k]=(p[i]-x)*v[i];
		k++;
	}
}
/*背包*/ 
for(int i=1;i<=k;i++)
{
	for(int j=m;j>=nw[i];j--)
	{
		f[j]=max(f[j],f[j-nw[i]]+nv[i]);
	}
}

    看吧!转化为01背包还是很简单吧!

    ④混合背包

    由于01和完全只是循环方式是顺序与逆序的不同,所以if即可,多重则转化为01,It's so easy!

    样例代码:

    为什么你不自己去写?You're lazy!

    ⑤分组背包

    这种问题较为恶心,选择方案很多——选择某一组中的一个或一个也不选,但是只不过是三重循环而已,并没有太难

    (2)最长不下降序列

    这个问题还是比较简单,用经典的O(n^2)方法,依次尝试以i号点为结尾,往前寻找更小的,接上去,保留最大值即可,拦截导弹也是一样的方法,有很多类似的题目可以通过这个拓展而来(如OpenJudge上的2.6动态规划上的怪盗基德的滑翔翼之类)

    (3)石子归并

    很好的一道题目,但同样的恶心,开始毫无思路,但后来想到了办法——

    f[1][n]=2*min{f[1][n-1]+f[n][n],f[1][n-2]+f[n-1][n]……,f[1][1]+f[2][n]}

    即:把1~n排好相当于把1~n-1排好再与n排好或者把1~n-2排好再与n-1~n排好相结合……这样不断比较,最后*2(原始时间+合并额外用时)

    (4)最长公共子序列

    有些复杂,但可以看出,我们可以不停的比较,用一个二维数组(如C[maxn][maxn])来保存最优方案

    C[i][j]表示匹配到s1的第i个字符,s2的第二个字符能得到的最优方案,如果s1[i]=s2[j],那么C[i][j]=C[i-1][j-1]+1(即匹配到i-1个字符和j-1个字符的最大值+1),如果不等,那么就等于max{C[i-1][j],C[i][j-1]}(即匹配到i与j-1和i-1和j的最大值

<-------------------------------------------------------------------------------------------------->

    8、队列,堆栈,树型结构的典型问题

    (1)队列

    bfs就靠它了!先进先出使得很容易找到无权图的最短路,运用各种方法可以完成各种迷宫问题(找钥匙什么的完全不在话下),是居家旅行必备用品,而且可以用数组模拟,还能变成循环队列,灵活性更强!(当然,优先队列还可以解决有权值的图,见11章)

    操作:

    头文件:#include<queue>+std库

    定义队列:queue<...(结构:如int、double,也可以是结构体node)>que(自己取队列的名字);(定义一个队列)

    入队:que.push(a(任何符合队列结构的元素均可));(在队尾新增一个元素a)

    出队:que.pop();(删除队首元素)

    访问队首:que.front();(返回队首元素)

    访问队尾:que.back();(返回队尾元素)

    队列是否为空:que.empty();(队列为空返回TRUE,否则返回FALSE)

    队列长度:que.size();(返回队列长度)

    PS:在作为循环条件时,que.size()与!que.empty()效果一样,但还是用!que.empty()较好,说不出来为什么,反正总感觉算长度要慢一些……

    (2)堆栈

    ①堆

    一种保存树的方法,可以很轻松的添加新元素并保持优先顺序,优先队列也是通过这个原理来的(参见11章),堆的样子如下图:

                       

    如图,每一个子节点都比他的父亲小,否则就交换,虽然不能保证整棵树是按最小方式排列,但可以保证第一个肯定是最小的,要排序也很简单,拿走根节点,将最后一个儿子放在根节点的位置,与两个儿子比较,将较小者与之交换,交换后继续比较,一直到两个儿子都比自己大为止。大根堆与小根堆差不多,也是同样的。堆排序没有快排方便,没有归并排序那样可以寻找逆序对,也没有桶排序快,但它可以很快地在有序序列中加入一个新元素,使它仍然保持有序,在一些情况下很方便(经典题目:合并果子)

     操作(通过优先队列实现):

     头文件:#include<queue>+std库

     定义一个堆:priority_queue<vector<...(结构类型:如int、double,也可以是结构体node)>,...(跟前面一样的结构),greater<int>(或less<int>或自己定义的对比方式)(这里有个空格!!!)>que(堆名);

     入堆新元素:que.push(x);

     出堆堆首:que.pop();

     返回堆首元素:que.top(注意不是front!by the way,优先队列没有尾元素)();

     PS:堆里的元素会按优先方案自动排好序,greater是最小的放前面,less或不写是最大的放前面

    (2)栈

    你造吗?递归是通过栈实现的!栈就像一个网球筒,先把一堆球放进去,先拿出来的是最后一个放进去的,函数的递归莫不如此,先调用的后返回,最后调用的却最先返回。通过这种方式,可以解决许多问题,经典的要数括号匹配。比如说{(<>[])}这样一个括号串,我们遇到左括号则将它入栈,遇到右括号则将它与栈顶括号比较,如果可以匹配,将栈顶弹出,如果不匹配或匹配完了栈不为空,即匹配失败(经典题目:括号匹配)

    栈的示意图:




    操作:(我才不是粘贴狗!)

    头文件:#include<stack>+std库

    定义栈:stack<...(结构:如int、double,也可以是结构体node)>sta(自己取栈的名字);(定义一个队列)

    入队:sta.push(a(任何符合栈结构的元素均可));(在栈顶新增一个元素a)

    出队:sat.pop();(删除栈顶元素)

    访问栈顶:sta.top();(返回栈顶元素)

    栈顶是否为空:sta.empty();(栈为空返回TRUE,否则返回FALSE)

    栈的长度:sta.size();(返回栈的长度)


    (3)树

     树是什么?就是全部连通无回路的一张图!或者这么说吧,用n-1条边连接n个点,使两点之间一定有一条路可走的图,便是树。二叉树,是我们最常讨论的特殊树,即每一个节点要么有1个儿子,要么有2个儿子,要么没有儿子。而在二叉树中,又有两种特殊的树——满二叉树和完全二叉树。满二叉树是最简单的二叉树,即只有最后一层没有儿子,其他全部层数必须有两个儿子,看起来很“满”。完全二叉树则是比满二叉树要不完美一点,即除了最后一层没有儿子,倒数第二层可有任意数量的儿子,其他层数必须全部都要有两个儿子,而且倒数第二层的儿子必须是连续的,就是说某一个只有一个儿子,那么后面的每一个节点就不能有任何儿子,同样如果有一个节点没有儿子,后面的节点也不能有任何儿子,即:

     

    看出来没有?如果你还是不懂,那么我用另一种解释吧!——如果将一棵二叉树装进数组中,根结点下标为1,下标为n的节点的左儿子下标为2n,右儿子为2n+1,那么完全二叉树的编号必须连续。其实满二叉树也是一颗完全二叉树,包括前面提到的堆。树一般来说不是来方便我们的,是来坑我们的,什么扩展树、FBI树、树的计数(给定先序遍历和后序遍历,求可能性的和,卡了很久)、树的遍历、建造树,恶心得不亦乐乎,不过还是陪伴我们走过了那段艰苦的岁月,可以去拥抱死亡哦~(经典题目:上面不是说了么……)

<-------------------------------------------------------------------------------------------------->

    9、DFS,BFS(记忆化搜索,高效剪枝)
    好吧,仔细回味一下(暴力的)搜索,其实也是很复杂的

    (1)DFS

    深搜绝对是骗分一把手,尝试每一种可能,不撞南墙不回头,一口气冲到底,撞出了一个包,没关系,挥手自兹去,换个方向继续撞,实在不行飞回去,就是这么潇(zhi)洒(zhang)。但是许多规模较小的题目都可以骗骗分~但是迷宫问题一般都不会用到它(这么慢的速度敢用么),那我们如果遇到迷宫怎么办?参见bfs

    (2)BFS

    迷宫问题核心算法!运用数组或队列实现。与dfs不同的是,bfs的扩展没有dfs方便,但第一个找到的解绝对是最优解!看看以下图例:

                          

    看出来了吗?每次将当前点的四个方向探索入队,然后不继续深究,查看队列下一个元素即可,一直到终点,即是最优解。迷宫问题大哥大!

    10、最短路径(Floyd,Dijkstra,Bellman-Ford,SPFA),最小生成树(kruscal,prim),并查集
    对于图论然而并没有什么可以说的……几种算法各有千秋

    (1)最短路

    Floyd虽然耗费时间长,但是功能齐全,可以解决负环、有限制等等其他算法很伤脑筋的题目,而且代码简练,用if语句写而不用max核心代码都只有5行

    Dijkstra的算法较Floyd和Bellman-Ford都要快,也比SPFA代码简单,但是有时候并不能很好地解决问题,常见的情况是负边权导致贪心失败……但是它的速度很可观,大部分最短路都可以解决

    Bellman-Ford,相较于Dijkstra,除了稀疏图完全只能败下阵来。完全图时甚至和Floyd相当,所以很少被使用到题目中

    SPFA比其他算法都要快速,可以说是极速。但是利用队列实现的它代码很复杂,如果其他算法也可以完成的任务还是尽量不要使用SPFA,徒增代码长度

    (2)最小生成树

    话说prim我真的没用过,我平常一般都是用kruscal,这么简单的东西还需要直说么?一道排序就可以很轻松地使用并查集解决最小生成树了。一道道增加线段,将开始和结束节点划分到同一个集合,直到所有节点都到了一个集合即可


    11、字符函数,结构体内联函数,重载运算符,大根堆,小根堆,优先队列


    12、高级数据结构(树状数组、线段树、平衡树)

<-------------------------------------------------------------------------------------------------->

暂时写到这里啦~慢慢会更加完整的

<-------------------------------------------------------------------------------------------------->

    13、问题总结

    1)没有思考好就动笔键盘,导致写完之后调试很久,浪费时间

    2)喜欢编数据动态查错,不爱静态查错,容易有潜在错误

    3)遇到问题脑中只有两个字:回(bao)溯(sou),应该根据题目的数据,思考什么方式更好:

    20:强行递归都不用怕!

    100:Floyed,三重循环,搜索(dfs,bfs)

    1000~5000:双重循环,冒泡,选择

    10000~20000:DP,快排,归并,优先队列

    100000~1000000:贪心,数学方法(追击相遇,辗转相除等)

    100000…(以下省略n个0)000:高精度

    4)有时容易犯一些小(ruo zhi xing)错误:

    如:freopen写错、装成n个文件夹而交错文件夹、调试后freopen的注释号没有删除……

    小贴士:

   1)在时间、空间及正确性允许的条件下,越简单好理解的算法越好(比如dfs,Floyed),不要作死强行优化,越有把握,越简洁的代码越易查错(只有七八十行查错还不容易?至于160+……)

   2)打代码时多写写注释,避免头脑短路的情况发生,理解自己的变量是什么意思,有什么用,千万不要用混了,那样很容易出错

   3)关于思路问题的话没有思路上厕所知道了吗?

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值