前言
其实从大二开始就在整理有关如何学习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))
bool isprime(int n){
if(n<2)return 0;
if(n<4)return 1;
for(int i=2;i*i<=n;i++){
if(n%i==0)return 0;
}
return 1;
}
解释:
素数的因子只有1和它本身,那么如果从1到sqrt(n)都没有数字是n的因子,那么n一定是质数。
可以发现,一个数的所有因子,一定均等地分布在sqrt(n)的左右两边。
比如数字9的因子{1,3,9},左边是{1,3},右边是{3,9}。
1.2素数表,时间复杂度O(N)
const int maxn = 1e5+10;
bool notprime[maxn];
void getprime(){
notprime[0]=notprime[1]=1;
for(int i=2;i<maxn;i++){
if(notprime[i]==0){
for(long long j=1LL*i*i;j<maxn;j+=i){
notprime[j]=1;
}
}
}
}
解释:
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 的形式
const int maxn = 1e5;
int p[100],x[100];
void getheshu(int n){
int cnt=0;
for(int i=2;i<maxn&&n>1;i++){
if(n%i==0){
p[++cnt]=i;
while(n%i==0){
n/=i;
x[cnt]++;
}
}
}
if(n>1){
p[++cnt]=n;
x[cnt]=1;
}
}
解释:
调用这个函数后,n的分解结果存储在p数组和x数组中,表达形式如上述。
如果能够分解出一个质数p,那么循环分解出的p的最高次幂。
最后剩下的“尾巴”如果大于1,说明这个数字一定是个质数。
- 2 最大公约数
gcd和lcm
最大公约数主要用到的是辗转相除法
int gcd(int a,int b){
int c;
while(b){
c=a;
a=b;
b=t%b;
}return a;
}
当然我们可以直接使用库函数__gcd(,)它是内部已经实现好了的函数,所以可以省去上面的代码,请注意该函数前面有两条下划线。
至于a和b的最小公倍数,等于a*b/gcd(a,b)
我们可以实现函数:
int lcm(int a,int b){
return 1LL*a*b/gcd(a,b);//避免32位整型溢出
}
- 3 组合数
3.1组合数打表
对于较小的组合数,我们一般采用打表的方式存储答案,主要有以下两种方法:
int dp[30][30];
for(int i=0;i<30;i++){
dp[i][0]=dp[0][i]=1;
}
for(int i=1;i<30;i++){
for(int j=1;j<30;j++){
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
/**解释:dp[i][j]表示从i+j个物品中选择i个物品,不选择j个物品,
那么它可以由dp[i-1][j]和dp[i][j-1]转移得到,满足加法定理。
C(n,k)对应dp[n-k][k]
**/
第一种方法是我喜欢的写法,不过以下第二种方法可能更加方便。
int dp[30][30];
for(int i=0;i<30;i++){
c[i][0]=c[i][i]=1;
for(int j=1;j<i;j++){
dp[i][j]=dp[i-1][j]+dp[i-1][j-1];
}
}
/**解释:这种写法的C(n,k)对应的值是dp[n][k]
**/
以上打表的算法,时间复杂度都是O(n^2)的,所以当复杂度过高时,请使用卢卡斯定理。
另外,根据数据范围调整32位整型和64位整型,如果要求取模,记得每次运算都要取模。
下面我们介绍卢卡斯定理。
3.2 卢卡斯定理
具体原理可以自行百度学习,这里还牵涉到了乘法逆元的知识点,初学者不妨把它当作黑箱子来使用。
typedef long long ll;
const int maxn = 1e5+10;
const int mod = 1e9+7;
ll qpow(ll a,ll n){
ll ret=1;
while(n){
if(n&1)ret=ret*a%mod;
a=a*a%mod;
n>>=1;
}
return ret;
}
//