前言
其实从大二开始就在整理有关如何学习C语言以及如何应对程序设计实践(和C语言考试)的经验和相关模板,由于各种原因,这件事情也没有一个很好的进展。前不久邹大佬提起这事儿的时候,突然觉得是应该好好整理一份类似于参考资料的东西了。
我打算先由自己整理出来这份模板,主要面向应对程序设计实践考试的同学。
本文当中可能会存在一些错误和遗漏的东西,还请指正。(email 1278683056@qq.com)
使用这份模板之前,你需要学会最基本的C语言(C++)语法,所以关于语法部分如果还不是很熟悉,这份模板对你而言没有任何帮助。
在信工院程设挂科率奇高的大环境下,我觉得整理出一份适合于入门者使用的模板很有必要,希望能够帮助到大家。
第一章 关于程序设计入门
- 1.online judge
oj指的是在线评测系统,程序设计实践考试在oj上进行,所以首先我们需要对oj有一个大致的了解。
1.1 根据测试,xtuoj 1秒钟大约能够运行3e7次,这一点在避免得到TLE很重要,学会计算时间复杂度和空间复杂度是数据结构课程的内容,在此不赘述。
1.2 介绍几种常见错误的原因,以便于对症下药。
|类型|原因 |解决方案|
|—|–|–|
| WA(答案错误) | 程序输出跟标程输出不一致,算法设计上有错误,或存在逻辑错误 | 改进算法,检查逻辑问题 |
| TLE(超时) | 程序未能在限定时间内结束,算法复杂度过高,或存在死循环 | 检查是否存在死循环,判断算法时间复杂度是否可行,如果确认复杂度可行,有可能是被卡常了 |
|RE(运行错误)|除0,栈溢出,内存访问越界等| ①检查除法运算的地方是不是除0了 ②如果使用了递归算法,判断是不是爆栈了 ③ 下标超过范围,数组开小,会访问越界 |
|MLE(内存超限)|申请的内存超过了题目限制范围,一般是数组开大了,也可能是因为在死循环里不停地申请内存|改进算法,计算空间复杂度|
|PE(格式错误)|答案对了,但是输出格式有问题|仔细检查换行,空格等问题,距离AC很接近了|
在此解释一下何为卡常:
卡常指的是,程序算法本身复杂度符合题目要求,按理说是能够AC的,但可能由于自己代码写了很多不必要的东西,导致超时。当然,不排除会有出题人故意卡常。解决方法是尽量避免不必要的额外运算,另外,在输入输出上能通过使用外挂从而加速运行。外挂会在接下来的模板中给大家贴出。
何为爆栈:
递归层数太多,导致栈溢出。(这类似于死循环,但是程序还没超时就因为爆栈而终止运行了。)如果确实是因为层数太多,也可以手动模拟栈(stack),或者改为队列(queue)。
- 2.分析题型
程设考试一般6题,对于绝大多数人而言,通过2题意味着考试及格,当然也有少部分人可以1题及格。
一:暴力,所谓的签到题
二:执行
三:贪心
四:模拟
五:数据结构
六:图论
七:动态规划
八:数学相关
对于以上题型,一到四项没有什么很好的模板可供参考,更多的是平时的积累和练习,然而在考试时这些题相对后面的题型来说,属于简单题;针对五到八项,接下来我会整理出一些适合的模板。
第二章 数学相关
- 1 素数相关
1.1单个数n的判定,时间复杂度O(sqrt(N))
1 | bool isprime(int n){ |
解释:
素数的因子只有1和它本身,那么如果从1到sqrt(n)都没有数字是n的因子,那么n一定是质数。
可以发现,一个数的所有因子,一定均等地分布在sqrt(n)的左右两边。
比如数字9的因子{1,3,9},左边是{1,3},右边是{3,9}。
1.2素数表,时间复杂度O(N)
1 | const int maxn = 1e5+10; |
解释:
notprime[i]==1表示i不是素数,反之表示i是素数。
对于一个素数a,它的倍数一定都不是素数,所以我们可以对于遇见的每个素数,都把它的倍数标记为非素数,以上代码就是实现这一过程的。
由于i*i可能会溢出,为了避免溢出,j使用long long型。j从i^2开始,因为小于i倍的部分都已经被修改过了,不需要重复修改。
1.3 合数分解(值域为int的)
把一个合数a分解为 a = 1 * p1^x1 * p2^x2 * … *pn^xn 的形式
1 | const int maxn = 1e5; |
解释:
调用这个函数后,n的分解结果存储在p数组和x数组中,表达形式如上述。
如果能够分解出一个质数p,那么循环分解出的p的最高次幂。
最后剩下的“尾巴”如果大于1,说明这个数字一定是个质数。
- 2 最大公约数
gcd和lcm
最大公约数主要用到的是辗转相除法
1 | int gcd(int a,int b){ |
当然我们可以直接使用库函数__gcd(,)它是内部已经实现好了的函数,所以可以省去上面的代码,请注意该函数前面有两条下划线。
至于a和b的最小公倍数,等于a*b/gcd(a,b)
我们可以实现函数:
1 | int lcm(int a,int b){ |
- 3 组合数
3.1组合数打表
对于较小的组合数,我们一般采用打表的方式存储答案,主要有以下两种方法:
1 | int dp[30][30]; |
第一种方法是我喜欢的写法,不过以下第二种方法可能更加方便。
1 | int dp[30][30]; |
以上打表的算法,时间复杂度都是O(n^2)的,所以当复杂度过高时,请使用卢卡斯定理。
另外,根据数据范围调整32位整型和64位整型,如果要求取模,记得每次运算都要取模。
下面我们介绍卢卡斯定理。
3.2 卢卡斯定理
具体原理可以自行百度学习,这里还牵涉到了乘法逆元的知识点,初学者不妨把它当作黑箱子来使用。
1 | typedef long long ll; |
小结:至于其他的数学知识,暂时还没有怎么见过,主要还是素数筛法考来考去。
第二章 数据结构
1 排序
在数据结构里,我们学习了很多种各有特色的排序算法,但在这里我只介绍一种最方便的排序工具:STL里的sort()函数。
什么是STL?这个问题在我另一篇很久没更新的博客上可以找到答案:点我,我是传送门
上面的链接里有很多可用的STL介绍,在此不赘述。
讲到排序,不妨再深入学习一下其他的STL工具。
这里列举可以深入了解的STL工具:
stack
queue
vector
map
set
以及一大堆好用的函数2 双指针尺取法
之所以把这个东西单独拿出来讲,是因为eric多次出过这种类型的题,O(nlogn)过不去,而这个O(n)的算法可以过题。
举个例子:给个序列,序列中每个值都是正数,序列长度为百万级别的,问有多少个区间[L,R],使得区间累加和为k。
朴素做法是对于每个点都以它为起点,暴力扫描一遍,复杂度O(n²)。
可以把朴素做法用二分搜索优化到O(nlogn)。
最快做法如下(这个链接的D题题解就是双指针原题):
1 | ans=0;//答案初始为0 |
- 3 前缀和
前缀和是一种可以用来快速查询区间和的数据结构,常见题型有“区间内素数个数”,“序列区间和”等。
1 | const int maxn = 1e5+10; |
- 4 树状数组
既然前缀和可以处理区间和,那如果需要对单点进行修改操作呢?我们可以借助树状数组来实现。由于这个知识点需要很多前置知识,所以在这里只贴模板。
1 | #define maxn 1000060 |
时间复杂度分析:修改操作和查询操作,复杂度均为O(logn)。
- 5 指针实现二叉树
二叉排序树是一个很经典的数据结构,代码如下:
1 | struct node{ |
戳我,这篇的E题也是一个二叉排序树的考试原题,学会这两题就能掌握BST了
- 6 dfs
我发现其实很多人都不太会深度优先搜索(depth first search),所以贴一个原理和解释吧。
1 | //问题:有一个100*100的迷宫,告诉你起点终点,以及障碍物的位置,问你至少需要几步从起点走到终点。 |
这里贴一个dfs例题题解的链接,里面有完整代码,可作为模板使用。
- 7 bfs
我们可以认为dfs的实现过程像是一个栈,而bfs的实现过程像是一个队列。
以下代码也是以迷宫为原型的bfs1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19struct node{
int x,y,step;
node(int x,int y,int step):x(x),y(y),step(step){}
};
void bfs(){
int vis[100][100]={0};
queue<int> q;
q.push(node(起点坐标));
while(!q.empty()){
node now = q.front();q.pop();
vis[now.x][now.y]=1;
if(now这个点的坐标是终点坐标){ 答案就是now.step; }
if(......)q.push();如果这个点不是障碍物,并且以前没有到达过,那么我们就压入队列,等待访问。
if(......)q.push();
if(......)q.push();
if(......)q.push();
}
如果队列访问空了都没有找到答案,说明无解。
}
按理说,“搜索”应该放在算法一类讲,不过这样分类也无大碍,接下来是一些最基础的搜索原题和代码。
1 | //题意: |
- 8 二分搜索
使用二分的先决条件是,二分对象满足单调性。
比如:有一个序列a[],你想知道x在这个序列中的排名,假使这个序列有序,那么我们就可以对序列进行二分。
1 | const int maxn = 1e5+5; |
三 图论
- 1 建图
在了解这一个知识点之前,你需要前置知识《离散数学.图论》和《数据结构》。
对于稠密图,我们直接用邻接矩阵存储。也就是说,图中有多少个点,我们就开个多大的二维数组对于稀疏图,我们采用邻接表存储,这里有两种存储方式1
2int mp[maxn][maxn];
//那么,从点u到点v的距离就是mp[u][v]了,非常方便。
1.1 vector存图
1.2 前向星
1 | struct edge{ |
- 2 单源最短路之dijkstra
1 | //邻接矩阵版本 |
3 多源最短路之floyd
floyd的过程像是一个动态规划,复杂度O(n3)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23#define MAX 500
#define INFE 1<<20
int N;
int map[MAX][MAX],b[MAX],path[MAX][MAX]; //path[i][j]记录路径
void init(){
int i,j;
for(i=1;i<=N;i++)
for(j=1;j<=N;j++) {
map[i][j]=INFE;
path[i][j]=j;
}
}
void floyd(){
int i,j,k;
for(k=1;k<=N;k++)
for(i=1;i<=N;i++)
for(j=1;j<=N;j++)
if(map[i][j]>map[i][k]+map[k][j]) {
map[i][j]=map[i][k]+map[k][j];
path[i][j]=path[i][k];
}
}
//最后点u和点v的距离就是map[i][j]了,如果map[i][j] == INFE,说明不存在(u,v)的路径。4 最小生成树之克鲁斯卡尔
前置知识:并查集。(不会也没关系,照抄就是了)
最小生成树:给出一个无向图,有n个点和m条边,要你从中选出n-1条边,
使得这个图变成一棵树,并且边权之和最小。
1 | Description |
四 动态规划
动态规划是一个庞大的知识块,它有很多种解决不同类型问题的变形,所以我只贴一些原题的代码。
1303
1 | n(1≤n≤60)个整数组成的序列(a1,a2,…,an),1≤ai≤8。 |
1 | 给一个整数序列{a1,a2,…,an},存在这样的子序列{ai1,ai2,…,aim}∣1≤i1<i2<…<im≤n, |
1 | 思路: |
另外再补充一些简单动态规划模板吧。
最长公共子序列:
对于两个序列,找到他们最长公共子序列的长度
1 | #include <bits/stdc++.h> |
最长上升子序列:
对于一个序列,找到最长上升的子序列长度
1 | #include<iostream> |
01背包:
有一个容量为V的背包,有很多体积不等和价值不等物品,问最多能装多少价值的物品。
1 | #include<iostream> |
五 其他
1 重定向输入输出
1
2freopen("a.in","r",stdin);//从a.in这个文件读取
freopen("a.out","w",stdout);//输出到a.out这个文件2 输入挂,输出挂
用了输入挂输出挂就别用普通的scanf和printf,容易出错。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18inline char nc(){
static char buf[100000],*p1=buf,*p2=buf;
return p1==p2&&(p2=(p1=buf)+fread(buf,1,100000,stdin),p1==p2)?EOF:*p1++;
}
template<class T>
inline void read(T &sum){
char ch=nc();sum=0;
while(!(ch>='0'&&ch<='9'))ch=nc();
while(ch>='0'&&ch<='9')sum=sum*10+ch-48,ch=nc();
}
template<class T>
inline void print(T x){
if(x>9)print(x/10);putchar(x%10+'0');
}
读入一个整数int a 就是 read(a);
输出一个整数int a 就是 print(a);
结语:
2018.12.5更新:
今天把四个主要内容补充完,其中很多代码都是直接手写的,没有检验过正确性,如果有错误,请指出。
部分模板来自网络,部分 原题来自exam。
希望能够帮到那些初学者,以及被eric折磨得死去活来 的无辜群众们。
因为写这个面向的是小朋友嘛,所以作为一个学长,希望你们能够好好学习编程,切身去感受一下算法的魅力,有兴趣的话可以加一下eric带的 那个实验室,
叫什么名字来着?湘潭大学ACM集训队。