第一章
const double pi = atan(-1.0);
,尽量使用const
而不是# define
定义常量。%03d
,长度为3,不足的用0填充。- 变量值的交换:
// 引入中间变量
t = a;
a = b;
b = t;
// 不实用中间变量,只适用于数值型
a = a+b;
b = a-b;
a = a-b;
c/c++
中使用的逻辑运算符均为短路运算符。\n, \\, \', \", %%
。- 闰年:
year%4==0&&year%100!=0 || year%400==0
。 - 标准框架:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1000+10;
int haha[maxn];
int main()
{
freopen("in.txt", "r", stdin);
//freopen("out.txt", "w", stdout);
//cout<<"time used:"<<(double)clock()/CLOCKS_PER_SEC;
return 0;
}
第二章
- 数据溢出:
int
:32位,范围为-21亿~21亿
左右。long long
:64为,范围最大。
- 浮点数相等比较:由于浮点数存储有误差,所以不能直接利用比较运算符
==
进行比较,应该使用abs(a-b)<=1e-6
来表示二者接近相等。 - 结果取模:一般是由于计算的结果巨大无法直接表示。在计算中,模运算对
+、-、*
是等效的,对/
是乘以它的逆元。 - 输出运算的时间:
//cout<<"time used:"<<(double)clock()/CLOCKS_PER_SEC;
。 - 变量在定义之后未赋初值,其值是随机的。特别地,它不一定为0。
- 末行或末尾没有空行或换行:引入中间变量判断之。
- 输入输出框架:
// 重定向
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
// 文件读写
FILE *fin, *fout;
fin = fopen("in.txt", "rb");
fout = fopen("out.txt", "wb");
fscanf(fin, "%d", &a);
fprintf(fout, "%d\n", a);
fclose(fin);
fclose(fout);
// 字符数组读写
char haha[100];
sprintf(haha, "%d%d", a,b);
sscanf(haha, "%d%d", &a,&b);
习题
分数化小数
输入正整数a, b, c,输出a/b的小数形式,精确到小数点后c位。
- 格式化输出的特殊用法:
printf("%.*f\n", c, a/b)
。
排列
用1-9组成3个三位数abc,def和ghi,每个数字恰好用一次且abc:def:ghi=1:2:3。输出所有解。
- 常量数组的运用。
sprintf将内容输出到数组
。
#include <bits/stdc++.h>
using namespace std;
int main()
{
char a[]="123456789";
for(int i=123;i<=329;i++){
int x=i,y=2*i,z=3*i;
char buf[20];
sprintf(buf,"%d%d%d",x,y,z);
sort(buf,buf+9);
if(strcmp(buf,a)==0) printf("%d %d %d\n",x,y,z);
}
return 0;
}
第三章
- 定义大数组:比较大的数组的定义尽量声明在全局,这是为了防止栈溢出。
- 数组的拷贝和赋值:由于数组初始定义时,里面的值不一定全为0,必要时需要赋初值和归零。
- 拷贝:
memcpy(b, a, sizeof(a))
,memcpy(b, a, sizeof(int))
等。 - 赋值:
int a[100] = {0}
,memset(a, 0, sizeof(a))
等。
- 拷贝:
ASCII表
:SP:32, '0':48, 'A':65, 'a':97
。- 字符数组常用函数:
strlen(), strcpy, strcmp, strcat, strchr(s, c), strstr(s1, s2)
。其中,strchr, strstr
返回的均是指针,不是位置数值。isalpha, isdigit, isalnum, isupper, islower, toupper, tolower
。
- 移位运算符:左移乘以2,右移除以2。
- 黑盒测试与OJ:
WA(wrong answer), PE(presentation error), TLE(time limit exceeded), RE(runtime error), AC(accept)
。- 除0,栈溢出,数组越界,mian返回值不为0等常见错误。
UVA
,一个辣鸡的OJ网站,严格要求输出格式也是醉了。下面的题目基本基于该OJ。
例题
开灯问题
有n个灯编号为1~n,开始时灯都是关着的。第i个人把编号为i的倍数的灯的开关按下(开着的灯熄灭,关着的灯打开)。一共有k个人,问最后有哪些灯开着。输入n, k,输出所有开着灯的编号。
- 两个状态的交替变化:
a = !a
。 - 控制输出格式,收尾不能有空格或者收尾没有空行等:利用一个
first
变量控制。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 100+10;
bool light[maxn];
int main()
{
//freopen("in.txt", "r", stdin);
int n,k;
cin>>n>>k;
memset(light, 0, sizeof(light));
for(int i=1;i<=k;i++){
for(int j=i;j<=n;j+=i) light[j] = !light[j];
}
bool first=true;
for(int i=1;i<=n;i++){
if(light[i]){
if(first) first=false;
else cout<<" ";
cout<<i;
}
}
//cout<<"time used:"<<(double)clock()/CLOCKS_PER_SEC<<endl;
return 0;
}
蛇形填数
在nxn的矩阵中填入1, 2,…, nxn,要求填成蛇形。输入n,输出填充后的矩阵。例如,当n=4时,填入如下:
10 11 12 1
9 16 13 2
8 15 14 3
7 6 5 4
- 采用先判断下一个空格是否满足填入条件再去,而不是先到空格上再判断是否满足填入条件。
- 事前判断比事后判断所处理的逻辑更为简单,也更符合常理。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 100+10;
int ans[maxn][maxn];
int main()
{
//freopen("in.txt", "r", stdin);
int n;
cin>>n;
memset(ans, 0, sizeof(ans));
int pos=1, all=n*n;
int i=0, j=n-1;
ans[i][j]=pos;
//当前位置已经填好,查看下一个位置是否满足条件
//如果采取已经站在当前位置再去判断位置是否满足,这样逻辑就比较复杂
while(pos<all){
while(i<n-1 && !ans[i+1][j]) ans[++i][j]=++pos;
while(j>0 && !ans[i][j-1]) ans[i][--j]=++pos;
while(i>0 && !ans[i-1][j]) ans[--i][j]=++pos;
while(j<n-1 && !ans[i][j+1]) ans[i][++j]=++pos;
}
for(int i=0;i<n;i++){
for(int j=0;j<n;j++)
cout<<ans[i][j]<<" ";
cout<<endl;
}
//cout<<"time used:"<<(double)clock()/CLOCKS_PER_SEC<<endl;
return 0;
}
竖式问题
找出所有形如abc*de(三位数乘以两位数)的算式,使得在标准的小学列竖式中出现的所有数字均在给定的集合中。输入数字集合(没有空格),输出所有可能的abc和de。
- 使用
sprintf(buf, "%d", x)
将内容输出到数组中,这样可以很方便的处理数据。 - 在字符串中查找字符:
strchr(s, c)
,s.find(c)
。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 100+10;
char buf[maxn];
int main()
{
//freopen("in.txt", "r", stdin);
string s;
cin>>s;
for(int i=111;i<=999;i++){
for(int j=11;j<=99;j++){
int x=i*(j%10), y=i*(j/10), z=i*j;
memset(buf, 0, sizeof(buf));
sprintf(buf, "%d%d%d%d%d", i,j,x,y,z);
int flag=1;
for(int i=strlen(buf)-1;i>=0;i--){
if(s.find(buf[i])==-1) {flag=0;break;}
}
if(flag) printf("%d %d %d\n", i,j,z);
}
}
//cout<<"time used:"<<(double)clock()/CLOCKS_PER_SEC<<endl;
return 0;
}
Tex中的引号
在Tex中,左双引号是“``”,右双引号是“’’”。输入一个正常英文文本,将其引号变换成Tex格式。
- 观察题目类型,是否可以边输入边处理,或者边输入边判断等。
fgetc(fin)
:fgetc(stdin)
为从标准输入输出中读入单个字符,等同于getchar()
。
fgets(buf, maxn, fin)
:fgets(buf, maxn, stdin)
为从标准输入输出中读入完整一行。但是并不等同于gets(buf)
。gets(buf)
由于没有长度限制,会造成内存溢出。在新标准中已经弃用。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 100+10;
char buf[maxn];
int main()
{
freopen("in.txt", "r", stdin);
char c;
bool left=true;
while((c=fgetc(stdin)) != EOF){
if(c == '\"'){
if(left) cout<<"``";
else cout<<"\'\'";
left=!left;
}
else cout<<c;
}
//cout<<"time used:"<<(double)clock()/CLOCKS_PER_SEC<<endl;
return 0;
}
错位字符串(打表技巧)
在键盘上,手指稍不注意就会往右错一位。这样Q变成W,J变成K等。输入一个错位后的字符串,输出正确字符串。注意,输入的一定全为错位后的字符,比如就不会出现A。
- 善于利用常量数组来减少逻辑判断等,可以使得条例清晰,代码简洁。
- 将经常使用的数据保存在常量数组中,方便程序的查找。俗称打表。
- 打表是一种典型的用空间换时间的技巧,一般指将所有可能需要用到的结果事先计算出来,这样后面需要用到时就可以直接查表获得。打表常见的用法有如下几种:
- 在程序中一次性计算出所有需要用到的结果,之后的查询直接取这些结果。这个是最常用到的用法,例如在一个需要查询大量Fibonacci数F(n)的问题中,显然每次从头开始计算是非常耗时的,对Q次查询会产生O(nQ)的时间复杂度;而如果进行预处理,即把所有Fibonacci数预先计算并存在数组中,那么每次查询就只需O(1)的时间复杂度,对Q次查询就值需要O(n+Q)的时间复杂度(其中O(n)是预处理的时间)。
- 在程序B中分一次或多次计算出所有需要用到的结果,手工把结果写在程序A的数组中,然后在程序A中就可以直接使用这些结果。这种用法一般是当程序的一部分过程小号的时间过多,或是没有想到好的算法,因此在另一个程序中使用暴力算法去i出结果,这样就能直接在源程序中使用这些结果。例如对n皇后问题来说,如果使用的算法不够好,就容易超时,而可以在本地用程序计算付出对所有n来说n皇后问题的方案数,然后把算出的结果直接卸载数组中,就可以根据题目输入的n来直接输出结果。
- 对一些感觉不会做的题目,先用暴力程序计算小范围数据的结果,然后找规律,或许就能发现一些“蛛丝马迹”。这种用法在数据范围非常大时候容易用到,因为这样的题目可能不是用直接能想到的算法来解决的,而需要寻找一些规律才能得到结果。
#include <bits/stdc++.h>
using namespace std;
char s[] = "`1234567890-=QWERTYUIOP[]\\ASDFGHJKL;'ZXCVBNM,./";
int main()
{
freopen("in.txt", "r", stdin);
char c;
while((c=fgetc(stdin)) != EOF){
if(c != ' ') cout<<s[strchr(s, c)-s-1];
else cout<<c;
}
return 0;
}
回文串和镜像串
一个字符串:如果翻转后与原串相同则为回文串,例如
abba, madam
等;如果左右镜像后与原串相同则为镜像串,例如2s, 3AIAE
。输入一个字符串,判断其是否为回文串以及镜像串。
所有可能的字符串如下:
AA, B , C , D , E3, F , G , HH, II, JL, K , LJ, MM, N , OO, P , Q , R , S2, TT, UU, VV, WW, XX, YY, Z5, 11, 2S, 3E, 4 , 5Z, 6 , 7 , 88, 9
。
- 需要两个步骤,判断回文和镜像。
- 判断是否为回文串:利用首尾两个指针,依次判断是否相等,知道两个指针重合。
- 判断是否为镜像串:根据已知的条件,构造两个字符数组或者一个字典。
- 多重条件的答案输出,答案常量数组:答案有4种,分别为
都不是、仅回文、仅镜像、回文和镜像
。- 设回文为m,镜像为n。0表示不是,1表示是。
- 所以可以巧妙的根据排列组合和二进制关系可以推出。
都不是、仅回文、仅镜像、回文和镜像
->00, 01, 10, 11
。
#include <bits/stdc++.h>
using namespace std;
char yuan[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789";
char xiang[] = "A---3--HIL-JM-O---2TUVWXY51SE-Z--8-";
// 答案常量数组
string ans[] = {"not a palindrome", "a regular palindrome", "a mirrored string", "a mirrored palindrome"};
int main()
{
freopen("in.txt", "r", stdin);
string s;
while(cin>>s){
short huiwen=1, jingxiang=1;
for(int pos1=0, pos2=s.size()-1;pos1<=pos2;pos1++,pos2--){
if(huiwen && s[pos1]!=s[pos2]) huiwen = 0;
if(jingxiang && xiang[strchr(yuan, s[pos1])-yuan]!=s[pos2]) jingxiang = 0;
}
// 根据条件,选择一个答案输出
cout<<ans[jingxiang*2+huiwen]<<endl;
}
return 0;
}
猜数字游戏的提示
给定答案序列和用户猜测序列,统计有多少数字位置正确(A),有多少数字在两个序列都出现过但位置不对(B)。输入包含多组数据。每组输入第一行为序列长度n,第二行是答案序列,接下来是用户猜测序列。猜测序列全0时表示改组数据结束。n=0时表示输入结束。
- A中位置正确的数字不会再在B中统计。
- 第一种思路就是双重循环遍历,第一遍遍历统计A,并把正确的位置数字排除;第二遍遍历统计B。但是这种方式在数据量较大时容易超时。
- 第二种思路。对于A可以直接统计,但是B却不能直接遍历得到,容易超时。所以对于每个数字(1~9),统计它在答案序列和猜测序列中出现的次数c1和c2,则min(c1, c2)就是该数字对B的贡献,表示该数字在两个序列中都出现的次数,然后减去位置正确的A,剩下的就是都出现了但是位置不正确的次数B。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 10000+100;
int standard[maxn];
int haha[maxn];
int a[10], b[10];
int main()
{
freopen("in.txt", "r", stdin);
int n, cnt=0;
while(cin>>n && n){
printf("Game %d:\n", ++cnt);
// 答案序列
memset(a, 0, sizeof(a));
for(int i=0;i<n;i++){
cin>>standard[i];
a[standard[i]]++;
}
// 用户猜测序列
// bool first=true;
while(1){
memset(b, 0, sizeof(b));
int right[10]={0}, A=0, B=0, sum=0;
for(int i=0;i<n;i++){
cin>>haha[i];
b[haha[i]]++;
if(haha[i] == standard[i]) right[standard[i]]++;
sum += haha[i];
}
if(sum == 0) break;
for(int i=0;i<10;i++){
A += right[i];
B += min(a[i], b[i])-right[i];
}
printf("\t(%d, %d)\n", A,B);
}
}
return 0;
}
生成元
如果x加上x的各个数字之和等于y,则x为y的生成元。输入包含T组数据。每组输入n(1<n<100000),输出n的最小元。无解时输出0。
- 假设需要求n的生成元,显然需要遍历n-1个数。但是这样重复计算量过大,打表。
- 打表的方法只需要一次性枚举100000内所有正整数m,标记以m为生成元的数为y,最后直接查表即可。速度可以大大提升。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 100000+100;
int haha[maxn]={0};
int main()
{
freopen("in.txt", "r", stdin);
for(int i=0;i<=100000;i++){
int x=i, y=i;
while(x){
y += x%10;
x /= 10;
}
if(y<maxn && !haha[y]) haha[y] = i;
}
int t;cin>>t;
while(t--){
int n;cin>>n;
cout<<haha[n]<<endl;
}
return 0;
}
循环序列
给定一个长度为n的环状串,分别从别个位置开始读串,找出字典序最小的表示方式。输入包含T组数据。每组输入一个长度为n的环状DNA串(只包含A, C, T, G)的一种表示方法,输出字典序最小的表示方法。
- 对于环状序列,可以用
i = (i+1)%n
遍历。 - 类比于寻找最小值,将当前最小的和遍历的一次比较即可。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 100+100;
char s[maxn];
bool xiaoyu(int pos1, int pos2, int len)
{
for(int i=0;i<len;i++){
if(s[pos1] != s[pos2])
return s[pos2] < s[pos1];
pos1 = (pos1+1)%len;
pos2 = (pos2+1)%len;
}
return false;
}
int main()
{
freopen("in.txt", "r", stdin);
int t;cin>>t;
while(t--){
cin>>s;
int len=strlen(s), min_pos=0;
for(int pos=1;pos<len;pos++){
if(xiaoyu(min_pos, pos, len))
min_pos = pos;
}
for(int i=0;i<len;i++){
cout<<s[min_pos];
min_pos = (min_pos+1)%len;
}
cout<<endl;
}
return 0;
}
习题
得分
给出一个由O和X组成的字符串(长度为1~80),统计得分。每个O的得分为目前连续O的个数,每个X得分为0。
- 定义一个变量,记录当前连续O的个数,当碰到X时,其值归零。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 80+100;
char haha[maxn];
int main()
{
freopen("in.txt", "r", stdin);
int t;cin>>t;
while(t--){
cin>>haha;
int len=strlen(haha), cur=0, sum=0;
for(int i=0;i<len;i++){
if(haha[i] == 'O'){
cur++;
sum += cur;
}
else cur = 0;
}
cout<<sum<<endl;
}
return 0;
}
分子量
给出分子式(不带括号),求分子量。输入中只包含4中原子(C:12.01, H:1.008, O:16.00, N:14.01,单位g/mol)。例如C6H5OH为94.108g/mol。
- 有点绕弯子,需要先到好的思路才行。
- 根据分子式的特点,每一个原子后面都会有其对应的个数,只不过当个数为1时省略了。
- 所以每次都以一个原子为标准,判断其后面的情况即可:是数字或者是字母与边界。
#include <bits/stdc++.h>
using namespace std;
map<char, float> atom;
int main()
{
freopen("in.txt", "r", stdin);
atom['C'] = 12.01;atom['H'] = 1.008;atom['O'] = 16.00;atom['N'] = 14.01;
int n;cin>>n;
while(n--){
string fenzishi;cin>>fenzishi; // 保存分子式
float sum=0; // 分子量
// 每一个分子,后面都为其个数,只不过当个数为1时,1被省略了
// 每次都以一个原子为标准,判断其后面的情况
for(int pos=0;pos<fenzishi.size();pos++){
char yuansu = fenzishi[pos]; // 当前原子
if(pos+1==fenzishi.size() || isalpha(fenzishi[pos+1])){ // 原子为最后一个或者下一个仍然为原子,故该原子下标为1
sum += atom[yuansu];
}
else{ // 下一个为数字,则表示该原子个数不为1
int num=0;
while(pos+1!=fenzishi.size() && isdigit(fenzishi[pos+1])){
num = num*10+(fenzishi[pos+1]-'0');
pos++;
}
sum += atom[yuansu]*num;
}
}
printf("%.3f\n", sum);
}
return 0;
}
数数字
把前n(n<=10000)个正整数写在一起,数一数0-9各出现了多少次。输入n,输出10个数,分别是0~9的个数。
- 思路很简单,这里用到了
sprintf
将数字输出到字符数组中,避免取各个位的计算。 - 行末不包含多余空格:
for(int i=0;i<=9;i++) printf("%d%c",ans[i],i==9?'\n':' ');
。 - 坑爹的OJ,居然判断末行有无空格,格式要求严格。
#include <bits/stdc++.h>
using namespace std;
int main()
{
freopen("in.txt", "r", stdin);
int ans[10]={0};
char temp[10];
int t;cin>>t;
while(t--){
int n;cin>>n;
memset(ans, 0, sizeof(ans));
for(int i=1;i<=n;i++){
sprintf(temp, "%d", i);
for(int j=strlen(temp)-1;j>=0;j--) ans[temp[j]-'0']++;
}
for(int i=0;i<=9;i++) printf("%d%c",ans[i],i==9?'\n':' ');
}
return 0;
}
周期串
如果一个字符串可以由某个长度k的子串重复多次得到,则称该串为周期串。给定一个字符串(长度不超过80),输出其最小周期。
- 总长度为80,所以可以枚举所有周期。
- 假设当前周期为k,则
k|len
,并且s[i] == s[i%k]
。前者用于优化常数,否则超时。
#include <bits/stdc++.h>
using namespace std;
int main()
{
freopen("in.txt", "r", stdin);
char s[100];
int n;cin>>n;
while(n--){
cin>>s;
int len = strlen(s);
for(int k=1;k<=len;k++){ // 假设周期为k
if(len%k == 0){ // k如果是周期,就要满足k|len,要不然超时。
bool flag=true;
for(int i=0;i<len;i++){
if(s[i] != s[i%k]) {flag = false;break;}
}
if(flag) {cout<<k<<endl;break;}
}
}
if(n) cout<<endl;
}
return 0;
}
谜题
有一个5*5的网格,其中恰好有一个格子是空的,其他格子各有一个字母。一共有4中指令:A,B,L,R,分别表示把空格上、下、左、右的相邻字母移到空格中。输入初始网格和指令序列(以数字0结束),输出指令执行完毕后的网格。如果有非法指令,应输出
This puzzle has no final configuration.
例如,左图中执行ARRBBL0
后,效果如右图所示。