马上就是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)关于思路问题的话没有思路上厕所知道了吗?