算法学习笔记

第一章 基础知识

常见做题结果反馈

  • Accepted: 答案正确, 恭喜你正确通过了这道题目。
  • Wrong Answer: 答案错误, 出现这个错误的原因一般是你的程序实现或思路出现了问题, 或者数据范围边界没有考虑到。
  • Runtime Error: 运行时错误, 出现这个错误的原因一般是数组越界或者递归过深导致栈溢出。
  • Presentation Error: 输出格式错误 , 出现这个错误的原因一般是末尾多了或少了空格, 多了或少了换行
  • Time Limit Exceeded: 程序运行超时, 出现这个错误的原因一般是你的算法不够优秀, 导致程序运行时间过长。
  • Memory Limit Exceeded: 运行内存超限, 出现这个错误的原因一般是你的程序申请太大了空间, 超过了题目规定的空间大小。
  • Compile Error: 编译错误, 这个不用说了吧, 就是你的代码存在语法错误, 检查一下是不是选择错误的语言提交了。
  • Output Limit Exceeded: 输出超限, 程序输出过多的内容, 一般是循环出了问题导致多次输出或者是调试信息忘记删除了。
  • Submitting: 提交中, 请等待题目结果的返回, 由于判题机有性能差异, 所以返回结果的速度也不一样

以上几种结果就是评判系统可能会返回的几种常见结果。 若返回 Accept, 那么你就可以拿到该题所有分数, 如果返回其他结果, 则要根据考试规则来得分,即是根据通过测试点的百分比给分还是只要不是 AC 就得 0 分。

输入输出技巧

  • 输入 int 型变量 scanf("%d", &x);
  • 输入 double 型变量 scanf("%lf", &x); 不用 float 直接 double
  • 输入 char 类型变量 scanf("%c", &x);
  • 输入字符串数组 scanf("%s", s);
  • 输出与输入的表示方式一致 比如printf("%s\n", s);

scanf 输入解析

  • 例:输入日期 2019-10-21
int year, month, day;
scanf("%d-%d-%d", &year, &month, &day);
printf("%d %d %d\n", year, month, day);
  • 例:输入时间 18:21:30
int hour, minute, second;
scanf("%d:%d:%d", &hour, &minute, &second);
printf("%d %d %d\n", hour, minute, second);

scanf 和 gets

输入一行字符串带空格的话,使用 gets, scanf 遇到空格会自动结束

char s[105];
gets(s);//例如输入 how are you?
printf("%s\n", s);

getchar 和 putchar

读入单个字符和输出单个字符, 一般在 scanf 和 gets 中间使用 getchar 用于消除回车’\n’的影响

输出进制转换

int a = 10;
printf("%x\n", a);//小写十六进制输出 答案 a
printf("%X\n", a);//大写十六进制输出 答案 A
printf("%o\n", a);//八进制输出 答案 12

输出增加前置 0

int a = 5;
printf("%02d\n", a);//其中 2 代表宽度 不足的地方用 0 补充
输出结果 05
printf("%04d\n", a);
输出结果 0005

输出保留小数

double a = 3.6;
printf("%.2lf\n", a);//2 表示保留两位小数
输出结果 3.60

有小数输出小数, 没小数输出整数

%g

特别注意: 中文符号和英文符号要对应一致, 一般情况下都用英文符号(如中文逗号, 和英文逗号,)

long long 的使用

很多情况下的计算会超出 int, 比如求 N!,N 比较大的时候int就存不下了,这时候我们就要用 long long。 那么我们怎么去记 int 和 long long 的范围呢, 有一个简单的记法, int 范围-1e9到 1e9, long long 范围-1e18 到 1e18, 这样就容易记了。

long long x;
scanf("%lld", &x);
printf("%lld\n", x);

字符串的ASCII码

字符的 ASCII 码
不要硬记, 直接输出来看

printf("%d\n", 'a');
输出结果 97
printf("%d\n", 'A');
输出结果 65

特别注意: 如果遇到需要 ASCII 码的题目的时候记住 char 字符和 int 值是可以相互转化的。

cin 和 cout

很多时候使用 C++的输入输出写起来更简单, 在应对一些输入输出量不是很大的题目的时候,我们会采用 cin 和 cout 来提高我们的解题速度。

比如求两个数的和

1. #include <iostream>//输入输出函数的头文件
2.
3. int main() {
4. int a, b;
5. cin >> a >> b;
6. cout << a + b;//输出两个数之和
7. }

可以发现, C++的输入输出敲起来更快, 这是我们会使用它来进行混合编程的原因之一。另外, C++的 string 类对于字符串操作很方便, 但是输入输出只能用 cin、 cout。
特别注意: 大家一定平时练习的时候不要排斥混合编程, 即 C 与 C++语法混用, 然后用 C++提交。 这样可以极大的帮助你以更快的速度解决一道你用纯 C 写半天才能解决的题目, 留下充裕的时间去解决更多的题目
友情提示: 当输入或输出格式有特殊要求的时候, cin 和 cout 不方便解决, 那么我们还是使用 scanf 和 printf 来解决问题。 要注意的是 printf 尽量不要和 cout 同时使用, 会发生一些不可控的意外

头文件技巧

万能头文件

1. #include <bits/stdc++.h>
2. using namespace std;

不过要看考试的评测机支不支持, 绝大部分都是支持的。 当然, 我们还可以留一手, 准备一个完整的头文件, 在考试开始前敲上去就行。

#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>
#include <time.h>
#include <algorithm>
#include <iostream>
#include <queue>
#include <stack>
#include <vector>
#include <string>
using namespace std;
int main() {
	return 0;
}

特别注意: 头文件可以多, 但是不能少, 但是有一些头文件是不允许的, 大部分 OJ 为了系统安全性考虑限制了一些特殊的 API, 导致一些头文件不能使用, 比如 windows.h。 当然不同的OJ 的安全策略也不尽相同, 一般不涉及到系统函数的头文件一般都是可以使用的。

数组使用技巧

数组除了可以存储数据以外, 还可以用来进行标记。
例题:

输入 N(N<=100) 个数, 每个数的范围> 0 并且 <= 100, 请将每个不同的数从小到大输出并且输出它对应的个数。
样例输入
8
3 2 2 1 1 4 5 5
样例输出
1 2
2 2
3 1
4 1
5 2

代码如下

#include <bits/stdc++.h>
using namespace std;
int f[105]={0};//注意, 尽量将数组开在全局
int main() {
	int n,x;
	scanf("%d", &n);
	for (int i = 0; i < n; i++) {
		scanf("%d", &x);
		f[x]++;
	}
	for (int i = 0; i <= 100; i++) {
		if (f[i] > 0) printf("%d %d\n", i, f[i]);
	}
	return 0;
	}

在这个程序中, 我们使用一个数组 f 记录每个值得个数, f[i]的值表示 i 这个数有多少个,初始的时候每个值的个数都是 0。
数组的使用不一定从 0 开始, 可以从任意下标开始, 只要我们使用的时候对应上就行。

例:
我们存储地图的时候
####
#.##
##@#
####
假设一个地图是这样的, 我们要用二维字符数组来存储,
我们可以像下面这样做。
#include <bits/stdc++.h>
using namespace std;
char mpt[10][10];
int main() {
	for (int i = 1; i <= 4; i++) {
		scanf("%s", mpt[i] + 1);
/* 不要用下面这种输入方式, 否则会出问题, 
因为回车也算一个 char 字符
	for (int j = 1; j <= 4; j++) {
		scanf("%c", &mpt[i][j]);
	}
*/
	}
	for (int i = 1; i <= 4; i++) {
		for (int j = 1; j <= 4; j++) {
			printf("%c", mpt[i][j]);
		}
		printf("\n");
	}
	return 0;
}

数组还可以嵌套使用
我们将上面那题改进一下

例题:
输入 N(N<=100) 个数, 每个数的范围> 0 并且 <= 100, 请将每个不同的数输出并且输出它
对应的个数。 要求按值出现的次数从小到大排序, 如果多个值有相同的个数, 只用输出值最大
的那个。
样例输入
8 3
2 2 1 1 4 5 5
样例输出
4 1
5 2

代码如下
//这里其实我想使用结构体数组来实现,我在这个代码底部加上

#include <bits/stdc++.h>
using namespace std;
int f[105] = {0};
int p[105] = {0};//p[i]表示有 i 个这样的数的最大值是多少
int main() {
	int n,x;
	scanf("%d", &n);
	for (int i = 0; i < n; i++) {
		scanf("%d", &x);
		f[x]++;
	}
	for (int i = 0; i <= 100; i++) p[f[i]] = i;
	for (int i = 1; i <= 100; i++) {
		if (p[i] > 0) printf("%d %d\n", p[i], i);
	}
	return 0;
}

结构体排序(自己补充的)

方法一: 排序函数写在结构体外

1.写结构体
struct node{
    int k,s;
}p[5005];
2.写排序函数
//这里的return 后面实际上是判断,返回的是TRUE或FALSE
bool cmp1(node x,node y){
    return x.s>y.s;   //定义降序排序(从大到小) 
}
bool cmp2(node x,node y){
    return x.k<y.k;   //定义升序排序(从小到大) 
}
3.开始排序
sort(p,p+n,cmp2); //排序

结构体排序例题:

#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
 
//按姓名从小到大排序,姓名一样,按年龄从小到大排序
struct student{
    string name;    //姓名
    int age;        //年龄
    int grade;      //成绩
};
 
//自己定义的排序规则
int comp(const student &s1,const student &s2){
    if(s1.name == s2.name && s1.age != s2.age){
        return s1.age < s2.age;
    }
    else if(s1.name == s2.name && s1.age == s2.age){
        return s1.grade < s2.grade;
    }
    else{
        return s1.name < s2.name;
    }
}
 
int main(){
    student s[100];
    s[0].name = "zhangsan"; s[0].age = 19; s[0].grade = 20;
    s[1].name = "zhangsan"; s[1].age = 18; s[1].grade = 20;
    s[2].name = "lisi";     s[2].age = 20; s[2].grade = 20;
    s[3].name = "zhangsan"; s[3].age = 19; s[3].grade = 2;
    s[4].name = "zhangsan"; s[4].age = 17; s[4].grade = 20;
    s[5].name = "lisi";     s[5].age = 20; s[5].grade = 15;
    sort(s, s + 6, comp);       //左闭右开,所以是对s[0]到s[6]排序
 
    for(int i = 0; i < 6; i++){
        cout<<s[i].name<<" "<<s[i].age<<" "<<s[i].grade<<endl;
    }
 
    return 0;
}

方法二:排序函数写在结构体内(重载‘<’或‘>’号)(同样,return返回的是一个判断结果TRUE或FALSE)

//按姓名从小到大排序,姓名一样,按年龄从小到大排序 
struct student2{
    string name;//姓名 
    int age;//年龄 
    bool operator < (const student2 & s2) const {//符号重载 
        if(name==s2.name){
            return age<s2.age;
        }
        else{
            return name<s2.name;
        }
    }
}; 

这回直接在main函数里sort就行
int main(){
    //结构体数组排序二:符合重载
    student2 s2[100]; 
    s2[0].name="zhangsan";s2[0].age=18;
    s2[1].name="zhangsan";s2[1].age=19;
    s2[2].name="lisi";s2[2].age=20;
    sort(s2,s2+3);//左闭右开,所以是对s[0]到s[2]排序 
    for(int i=0;i<3;i++){
        cout<<s2[i].name<<" "<<s2[i].age<<endl;
    }
    return 0;
}

结构体排序例题:洛谷p1068

审时度势 — 复杂度与是否可做

在做题之前, 我们要先判断这道题是否可做, 对于简单的模拟题, 大家肯定都知道, 我能写出来就是可做, 写不出来就是不可做。 但是对于循环嵌套和算法题, 我们就需要去判断思考自己设计的算法是否可以通过这道题。
不懂复杂度计算的同学去看一下数据结构课程的第一章, 很简单的。
例如: 我们写一个冒泡排序, 它是两个 for 循环, 时间复杂度是O(N^2),那么在 1S 内我们最多可以对多少个数进行冒泡排序呢, N 在 1000 - 3000 之间。一般情况下我们可以默认评测机一秒内可以运行 1e7 条语句, 当然这只是一个大概的估计, 实际上每个服务器的性能不同, 这个值都不同, 但是一般都相差不大, 差一个常数是正常的。
因此, 我们可以这样做一个对应, 下面是时限 1S 的情况

  • O(N) N 最大在 500W 左右
  • O(NlogN) N 最大在 20W 左右
  • O(N^2) N 最大在 2000 左右
  • O(N^2logN) N 最大 700 在左右
  • O(N^3) N 最大在 200 左右
  • O(N^4) N 最大在 50 左右
  • O(2^N) N 最大在 24 左右
  • O(N!) N 最大在 10 左右
    如果是 2S、 3S 对应的乘以 2 和 3 就可以

特殊技巧: 如果发现自己设计的算法不能在题目要求的时限内解决问题, 不要着急, 可以先把这道题留一下, 继续做其他题, 然后看一下排行榜, 有多少人过了这道题, 如果过的人多, 那么说明这道题可能数据比较水, 直接暴力做, 不要怕复杂度的问题, 因为出题人可能偷懒或者失误了导致数据很水。 考研机试的题目数据大部分情况都比较水, 所以不要被复杂度吓唬住了, 后面的章节会教大家面对不会更好的算法那来解决题目的时候, 如何用优雅的技巧水过去

举个简单的例子
题目要求你对 10W 个数进行排序,假设你只会冒泡排序, 但是冒泡排序很明显复杂度太高了, 但是有可能出题人偷懒, 他构造的测试数据最多只有 100 个, 根本没有 10W 个, 那么你就可以用冒泡排序通过这道题。
但是这种情况比较少见, 一般至少都会有一组极限数据, 所以可以先把这道题放着去做其他题, 然后再看看其他人能不能通过, 如果很多人都过了, 那么你就可以暴力试一下。
特别注意: 空间复杂度一般不会限制, 如果遇到了再想办法优化空间。

C++ STL 的使用

C++的算法头文件里有很多很实用的函数, 我们可以直接拿来用。

#include <algorithm>

排序

sort()函数:依次传入三个参数, 要排序区间的起点, 要排序区间的终点+1, 比较函数。 比较函数可以不填, 则默认为从小到大排序。
使用示例

#include <bits/stdc++.h>
using namespace std;
int a[105];
int main() {
	int n;
	scanf("%d", &n);
	for (int i = 0; i < n; i++) {
		scanf("%d", &a[i]);
	}
	sort(a, a+n);
	for (int i = 0; i < n; i++)
		printf("%d ", a[i]);
	return 0;
}

查找

  • lower_bound()函数
  • upper_bound()函数

lower_bound( )和 upper_bound( )都是利用二分查找的方法在一个排好序的数组中进行查找的。

从小到大的排序数组中,

  • lower_bound( begin,end,num): 从数组的 begin 位置到 end-1 位置二分查找第一个大于或等于num的数字, 找到返回该数字的地址, 不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标
  • upper_bound( begin,end,num): 从数组的 begin 位置到 end-1 位置二分查找第一个大于 num的数字, 找到返回该数字的地址, 不存在则返回 end。 通过返回的地址减去起始地址 begin,得到找到数字在数组中的下标。

从大到小的排序数组中, 重载 lower_bound()和 upper_bound()

  • lower_bound( begin,end,num,greater() ):从数组的 begin 位置到 end-1 位置二分查找第一个小于或等于 num 的数字, 找到返回该数字的地址, 不存在则返回 end。 通过返回的地址减去起始地址 begin,得到找到数字在数组中的下标。
  • upper_bound( begin,end,num,greater() ):从数组的 begin 位置到 end-1 位置二分查找第一个小于 num 的数字, 找到返回该数字的地址, 不存在则返回 end。 通过返回的地址减去起始地址 begin,得到找到数字在数组中的下标。

使用示例

#include<bits/stdc++.h>
using namespace std;
int cmp(int a,int b){
	return a>b;
}
int main(){
	int num[6]={1,2,4,7,15,34};
	sort(num,num+6); //按从小到大排序
	int pos1=lower_bound(num,num+6,7)-num;
	//返回数组中第一个大于或等于被查数的值
	int pos2=upper_bound(num,num+6,7)-num;
	//返回数组中第一个大于被查数的值
	cout<<pos1<<" "<<num[pos1]<<endl;
	cout<<pos2<<" "<<num[pos2]<<endl;
	sort(num,num+6,cmp); //按从大到小排序
	int pos3=lower_bound(num,num+6,7,greater<int>())-num;
	//返回数组中第一个小于或等于被查数的值
	int pos4=upper_bound(num,num+6,7,greater<int>())-num;
	//返回数组中第一个小于被查数的值
	cout<<pos3<<" "<<num[pos3]<<endl;
	cout<<pos4<<" "<<num[pos4]<<endl;
	return 0;
}

优先队列

通 过 priority_queue q 来 定 义 一 个 储 存 整 数 的 空 的 priority_queue 。 当 然priority_queue 可以存任何类型的数据, 比如 priority_queue q 等等

示例代码

#include <iostream>
#include <queue>
using namespace std;
int main() {
	priority_queue<int> q;//定义一个优先队列
	q.push(1);//入队
	q.push(2);
	q.push(3);
	while (!q.empty()) {//判读队列不为空
		cout << q.top() << endl;//队首元素
		q.pop();//出队
	}
	return 0;
}

C++的 STL(标准模板库) 是一个非常重要的东西, 可以极大的帮助你更快速的解决题目。

vector

通过vector<int> v 来定义一个储存整数的空的 vector。 当然 vector 可以存任何类型的数据,比如 vector<string> v 等等

示例代码

#include <iostream>
#include <vector>
using namespace std;
int main() {
	vector<int> v;//定义一个空的 vector
	for (int i = 1; i <= 10; ++i) {
		v.push_back(i * i); //加入到 vector 中
	}
	for (int i = 0; i < v.size(); ++i) {
		cout << v[i] << " "; //访问 vecotr 的元素
	}
	cout << endl;
	return 0;
}

queue

通过 queue<int> q 来定义一个储存整数的空的 queue。 当然 queue 可以存任何类型的数据,比如 queue<string> q 等等。

示例代码

#include <iostream>
#include <queue>
using namespace std;
int main() {
	queue<int> q;//定义一个队列
	q.push(1);//入队
	q.push(2);
	q.push(3);
	while (!q.empty()) {//当队列不为空
		cout << q.front() << endl;//取出队首元素
		q.pop();//出队
	}
	return 0;
}

stack

通过 stack<int> S 来定义一个全局栈来储存整数的空的 stack。 当然 stack 可以存任何类型的数据, 比如 stack<string> S 等等。

示例代码

#include <iostream>
#include <stack>
using namespace std;
stack<int> S;//定义一个栈
int main() {
	S.push(1);//入栈
	S.push(10);
	S.push(7);
	while (!S.empty()) {//当栈不为空
		cout << S.top() << endl;//输出栈顶元素
		S.pop();//出栈
	}
	return 0;
}

map

通过 map<string, int> dict 来定义一个 key:value 映射关系的空的 map。 当然 map 可以存任何类型的数据, 比如 map<int, int> m 等等。

示例代码

#include <iostream>
#include <string>
#include <map>
using namespace std;
int main() {
	map<string, int> dict;//定义一个 map
	dict["Tom"] = 1;//定义映射关系
	dict["Jone"] = 2;
	dict["Mary"] = 1;
	if (dict.count("Mary")) {//查找 map
		cout << "Mary is in class " << dict["Mary"];
	}
//使用迭代器遍历 map 的 key 和 value
	for (map<string, int>::iterator it = dict.begin(); it != dict.end(); ++it) {
		cout << it->first << " is in class " << it->second << endl;
	}
	dict.clear();//清空 map
	return 0;
}

set

通过 set<string> country 来定义一个储存字符串的空的 set。 当然 set 可以存任何类型的数据, 比如 set<int> s 等等。

示例代码

#include <iostream>
#include <set>
using namespace std;
int main() {
	set<string> country;//定义一个存放 string 的集合
	country.insert("China");//插入操作
	country.insert("America");
	country.insert("France");
	set<string>::iterator it;
	//使用迭代器遍历集合元素
	for (it = country.begin(); it != country.end(); ++it) {
		cout << * it << " ";
	}
	cout << endl;
	country.erase("American");//删除集合内的元素
	country.erase("England");
	if (country.count("China")) {//统计元素个数
		cout << "China in country." << endl;
	}
	country.clear();//清空集合
	return 0;
}

这里应该补充一个c++的迭代器的知识,一会整理完后,我补充在下面

多组输入的问题

对有的题目来说, 可能需要多组输入。
多组输入是什么意思呢? 一般的题目我们输入一组数据, 然后直接输出程序就结束了, 但是多组输入的话要求我们可以循环输入输出结果。

例题:

输入两个数, 输出两个数的和, 要求多组输入。
样例输入
1 2
3 7
10 24
样例输出
3
10
34

C 循环读入代码如下

#include <bits/stdc++.h>
using namespace std;
int main() {
	int a, b;
	while (scanf("%d%d", &a, &b) != EOF) {
		printf("%d\n", a+b);
	}
	return 0;
}

特别注意: 不能使用 while(1)这样死循环, !=EOF 的意思一直读取到文件末尾( End of file).另外, 多组输入一定要注意初始化问题, 数组和变量的初始化要放在 while 循环内, 否则上一次的运算的结果会影响当前的结果。

C++循环读入代码如下

#include <bits/stdc++.h>
using namespace std;
int main() {
	int a, b;
	  while (cin >> a >> b) {
7. cout << a + b << endl;
8. }
9. return 0;
10. }

Java 循环读入代码如下

Scanner stdin = new Scanner(System.in);
while (stdin.hasNext()) {
	String s = stdin.next();
	int n = stdin.nextInt();
	double b = stdin.nextDouble();
}

Python 循环读入代码如下

while True:
	try:
		a, b = map(int, input().split())
		c = a+b
		print(c)
	except: #读到文件末尾抛出异常结束循环
		break

C++迭代器(自己有空补充上)

C++之迭代器(Iterator)篇
C++迭代器(STL迭代器)iterator详解
总的来说,迭代器是STL的指针。(懂c语言的指针很重要。),并且++i比i++实现的速度要快,所以迭代器里一般都用++i。

要访问顺序容器和关联容器中的元素,需要通过“迭代器(iterator)”进行。迭代器是一个变量,相当于容器和操纵容器的算法之间的中介。迭代器可以指向容器中的某个元素,通过迭代器就可以读写它指向的元素。从这一点上看,迭代器和指针类似。

迭代器按照定义方式分成以下四种。

  1. 正向迭代器,定义方法如下:

容器类名::iterator 迭代器名;

  1. 常量正向迭代器,定义方法如下:

容器类名::const_iterator 迭代器名;

  1. 反向迭代器,定义方法如下:

容器类名::reverse_iterator 迭代器名;

  1. 常量反向迭代器,定义方法如下:

容器类名::const_reverse_iterator 迭代器名;

迭代器用法示例

通过迭代器可以读取它指向的元素,*迭代器名就表示迭代器指向的元素。通过非常量迭代器还能修改其指向的元素。

迭代器都可以进行++操作。反向迭代器和正向迭代器的区别在于:
对正向迭代器进行++操作时,迭代器会指向容器中的后一个元素;
而对反向迭代器进行++操作时,迭代器会指向容器中的前一个元素。

下面的程序演示了如何通过迭代器遍历一个 vector 容器中的所有元素。

#include <iostream>
#include <vector>
using namespace std;
int main()
{
    vector<int> v;  //v是存放int类型变量的可变长数组,开始时没有元素
    for (int n = 0; n<5; ++n)
        v.push_back(n);  //push_back成员函数在vector容器尾部添加一个元素
    vector<int>::iterator i;  //定义正向迭代器
    for (i = v.begin(); i != v.end(); ++i) {  //用迭代器遍历容器
        cout << *i << " ";  //*i 就是迭代器i指向的元素
        *i *= 2;  //每个元素变为原来的2倍
    }
    cout << endl;
    //用反向迭代器遍历容器
    for (vector<int>::reverse_iterator j = v.rbegin(); j != v.rend(); ++j)
        cout << *j << " ";
    return 0;
}

程序的输出结果是:
0 1 2 3 4
8 6 4 2 0

第 6 行,vector 容器有多个构造函数,如果用无参构造函数初始化,则容器一开始是空的。

第 10 行,begin 成员函数返回指向容器中第一个元素的迭代器。++i 使得 i 指向容器中的下一个元素。end 成员函数返回的不是指向最后一个元素的迭代器,而是指向最后一个元素后面的位置的迭代器,因此循环的终止条件是i != v.end()。

第 16 行定义了反向迭代器用以遍历容器。反向迭代器进行++操作后,会指向容器中的上一个元素。rbegin 成员函数返回指向容器中最后一个元素的迭代器,rend 成员函数返回指向容器中第一个元素前面的位置的迭代器,因此本循环实际上是从后往前遍历整个数组。(因为是反向的,所以使用的是rbegin和rend)

如果迭代器指向了容器中最后一个元素的后面或第一个元素的前面,再通过该迭代器访问元素,就有可能导致程序崩溃,这和访问 NULL 或未初始化的指针指向的地方类似。

第 10 行和第 16 行,写++i、++j相比于写i++、j++,程序的执行速度更快。回顾++被重载成前置和后置运算符的例子如下:

CDemo CDemo::operator++ ()
{  //前置++
    ++n;
    return *this;
}
CDemo CDemo::operator ++(int k)
{  //后置++
    CDemo tmp(*this);  //记录修改前的对象
    n++;
    return tmp;  //返回修改前的对象
}

以上是从刚才提供的两个网址上摘抄的一些部分,我足够用了,不够的时候再看一下这章的那两个网址就可以🐕🐕🐕

第二章 入门经典

简单模拟

有一类很常见的题型叫做简单模拟。 顾名思义, 就是不需要去考虑什么算法,直接按照题目的意思进行模拟计算就行。

促销计算
题目描述:
某百货公司为了促销, 采用购物打折的优惠方法, 每位顾客一次购物: 在 1000 元以上者, 按
9.5 折优惠; 在 2000 以上者, 按 9 折优惠; 在 3000 以上者, 按 8.5 折优惠; 在 5000 以上者,
按 8 折优惠; 编写程序, 购物款数, 计算并输出优惠价。
输入样例#:
850
1230
5000
3560
输出样例#:
discount=1,pay=850
discount=0.95,pay=1168.5
discount=0.8,pay=4000
discount=0.85,pay=3026
题目来源:
DreamJudge 1091

解题分析: 根据题目的意思, 我们知道就是按照题意去进行打折优惠的计算, 只需要判断输入的数值在哪个区间该用什么优惠去计算就好了。

参考代码

#include <bits/stdc++.h>//万能头文件
using namespace std;
int main() {
	double a;
	scanf("%lf", &a);
	//使用%g 可以自动去掉小数点后多余的 0 如果是整数则显示整数
	if (a < 1000) printf("discount=1,pay=%g\n", a);
	if (a >= 1000 && a < 2000) printf("discount=0.95,pay=%g\n", a*0.95);
	if (a >= 2000 && a < 3000) printf("discount=0.9,pay=%g\n", a*0.9);
	if (a >= 3000 && a < 5000) printf("discount=0.85,pay=%g\n", a*0.85);
	if (a >= 5000) printf("discount=0.8,pay=%g\n", a*0.8);
	return 0;
}

这道题应该是多考虑了万一出现小数的情况。

题型总结
简单模拟这类题目在考试中很常见, 属于送分签到的题目。 所有的考生, 注意了, 这类题必须会做。
对于简单模拟这一类的题目, 怎么去练习提高呢?很简单, 在 DreamJudge上多做题就行了。 那么要达到什么样的标准呢?如果你想拿高分甚至满分, 平时训练的时候, 这类题尽量要在 8 分钟内解决。如果你只是想拿个还不错的成绩, 这类题 AC 的时间尽量不要超过 15 分钟, 一定要记住, 最坏情况不能超过 20 分钟, 如果超过了, 说明你平时做的题还是太少了。
在考试的过程中, 大多数考生都会紧张, 有些考生甚至会手抖, 导致敲多某个字母, 然后又调试半天, 找半天错, 会导致比平时解决同样难度的问题时长多一倍甚至更多, 所以平时就要注意, 做题千万不能太慢了, 不然没有足够的时间来解决其他的题目哦。

练习题目

DreamJudge 1133 求 1 到 n 的和
DreamJudge 1043 计算 Sn
DreamJudge 1040 利润提成
DreamJudge 1722 身份证校验

进制转换类问题

进制转换类的题目在绝大多数学校都是必考题目之一, 这类题目的既基础又灵活, 能看出学生的编程功底, 所以这类题目一定要掌握。
总的来说, 跟进制相关的题目可以分为以下几种题型

  1. 反序数: 输入一个整数如 123, 将其转换为反序之后的整数 321
  2. 10 进制转 2 进制: 将一个 10 进制整数转化为一个 2 进制的整数。例如: 7 转换为 111
  3. 10 进制转 16 进制: 将一个 10 进制整数转化为一个 16 进制的整数。例如: 10 转换为 A
  4. 10 进制转 x 进制: 将一个 10 进制整数转化为一个 x 进制的整数。解析: 这是前面两个的一种通解, 如果会前面两种那么这个自然也触类旁通。
  5. x 进制转 10 进制: 将一个 x 进制整数转化为一个 10 进制的整数。解析: 这是上一种情况的反例, 看代码之后相信也能容易理解。
  6. x 进制转 y 进制: 将一个 x 进制整数转化为一个 y 进制的整数。解析: 遇到这种情况, 可以拆解为 x 先转为 10 进制, 然后再将 10 进制转为 y 进制。
  7. 字符串转浮点数。例如: 有一串字符串 31.25 将其转换为一个浮点数, 可以先转整数部分, 再转小数部分, 最后相加即可。
  8. 浮点数转字符串。例如: 有一个浮点数 23.45 将其转换为一个字符串进行存储, 可以将整数和小数拆开再合并成一个字符串。
  9. 字符串转整型和整形转字符串。解析: 直接用 atoi 函数和 itoa 函数即可。
    //事实上,atoi函数与itoa函数尽量不要去使用,如果存在空格或者存在一些别的符号,使用这个函数可能会出问题。如果出问题的话改的时候不好改,还是自己写比较好

反序数代码

//比如123转成321,每次对123取余取到个位数,然后加到ans上,第一次ans=1,然后ans乘10,加上2,ans就是21,再然后ans乘上10再加3,就是321

#include <stdio.h>
int main() {
	int n;
	 scanf("%d", &n);
6. int ans = 0;//将反序之后的答案存在这里
7. while (n > 0) {//将 n 逐位分解
8. ans *= 10;
9. ans += (n % 10);
10. n /= 10;
11. }
12. printf("%d\n", ans);
13. return 0;
14. } 

10进制转 x 进制代码(x 小于 10 的情况)

//例如10进制转换成2进制。7转换成111
//由上面的反序数代码(不断模10然后除10),我们可以推广到进制转x进制,不断模x然后除x
//底下的代码是用数组做的。数组保存好之后反序输出。
//cnt[i++]=5,等价与cnt[i]=5;i++;
//实际上这里cnt[i++]就相当于,7转成111之后每次让余数乘以2(但是二进制下乘2,我们语言里却不好实现二进制乘法,所以用字符串数组的下标的移动来实现这个类似的操作)
//自己纸上实现一下用7(10进制)转换成111(2进制),用7(10进制)转换成13(4进制)来试试(转换过程与转成2进制类似)。 实现了这两个转换,就明白了。

#include <stdio.h>
int main() {
	int n, x;
	int s[105];
	//输入 10 进制 n 和 要转换的进制 x
	scanf("%d%d", &n, &x);
	int cnt = 0;//数组下标
	while (n > 0) {//将 n 逐位分解
		int w = (n % x);
		s[cnt++] = w;
		n /= x;
	}
	//反序输出
	for (int i = cnt - 1; i >= 0; i--) {
		printf("%d", s[i]);
	}
	printf("\n");
	return 0;
}

10 进制转 x 进制代码( 通用版)

#include <stdio.h>
int main() {
	int n, x;
	char s[105];//十进制以上有字符, 所以用 char 存储
	//输入 10 进制 n 和 要转换的进制 x
	scanf("%d%d", &n, &x);
	int cnt = 0;//数组下标
	while (n > 0) {//将 n 逐位分解
		int w = (n % x);
		if (w < 10) s[cnt++] = w + '0';//变成字符需要加'0'
		else s[cnt++] = (w - 10) + 'A';//如果转换为小写则加'a'
		//如果大于 10 则从 A 字符开始
		n /= x;
	}
	//反序输出
	for (int i = cnt - 1; i >= 0; i--) {
		printf("%c", s[i]);
	}
	printf("\n");
	return 0;
}

x 进制转 10 进制( x 为 2 时)

#include <stdio.h>
#include <string.h>
int main() {
	char s[105];
	//输入二进制字符串
	scanf("%s", &s);
	int ans = 0;//
	int len = strlen(s);
	for (int i = 0; i < len; i++) {
		if (s[i] == '0') {
			ans = ans * 2;
		}
		else {
			ans = ans * 2 + 1;
		}
	}
	printf("%d\n", ans);
	return 0;
}

x 进制转 10 进制( 通用版)

#include <stdio.h>
#include <string.h>
int main() {
	char s[105];
	int x;
	//输入 X 进制字符串 和 代表的进制 x
	scanf("%s%d", &s, &x);
	int ans = 0;//
	int len = strlen(s);
	for (int i = 0; i < len; i++) {
		ans = ans * x;
		if (s[i] >= '0' && s[i] <= '9') ans += (s[i] - '0');
		else ans += (s[i] - 'A') + 10;
	}
	printf("%d\n", ans);
	return 0;
}

x 进制转 y 进制( 通用版)

//最终目标,x进制转y进制,这里是输入二进制字符串和代表的进制x,以及要转换的进制y
//这里加了一层判断,即判断数属于0~9还是数属于字母
// 这样第一个for循环里就成功的把一个x进制的数转换成一个十进制的数,然后后面再把这个十进制的数转换成y进制的数
//这里第一个for循环,我们想一下十六进制转十进制的过程,我们就懂了

#include <stdio.h>
#include <string.h>
int main() {
	char s[105];
	int x, y;
	//输入二进制字符串 和 代表的进制 x 以及要转换的进制 y
	scanf("%s%d%d", &s, &x, &y);
	int ans = 0;
	int len = strlen(s);
	for (int i = 0; i < len; i++) {
		ans = ans * x;
		if (s[i] >= '0' && s[i] <= '9') ans += (s[i] - '0');
		else ans += (s[i] - 'A') + 10;
	}
	char out[105];
	int cnt = 0;
	while (ans > 0) {
		int w = (ans % y);
		if (w < 10) out[cnt++] = w + '0';
		else out[cnt++] = (w-10) + 'A';
		ans /= y;
	}
	for (int i = cnt - 1; i >= 0; i--) {
		printf("%c", out[i]);
	}
	printf("\n");
	return 0;
}

还有一类进制转换的题目是大数的进制转换, 建议同学们学完第四章高精度问题, 学会大数的加减乘除法再看下面这类题

进制转换

题目描述:
将一个长度最多为 30 位数字的十进制非负整数转换为二进制数输出
输入描述:
多组数据, 每行为一个长度不超过 30 位的十进制非负整数。
(注意是 10 进制数字的个数可能有 30 个, 而非 30bits 的整数)
输出描述:
每行输出对应的二进制数。
输入样例#:
0
1
3
8
输出样例#:
0
1
11
1000
题目来源:
DreamJudge 1178

题目解析: 这个题和一般的 10 进制转二进制的区别在于它的数是一个很大的整数。 对于一个很大的数我们做法和普通的 10 进制转二进制是一样的, 就是不断的%2 然后除/2, 唯一区别在于要用大数进行模拟。

参考代码

#include<bits/stdc++.h>
using namespace std;
//十进制转二进制
char s[40], buf[200];
int main(){
	int num[40];
	while (scanf("%s", s) != EOF){
		int len = strlen(s);
		for (int i = 0; i < len; i++){//字符串转成 int 数组
			num[i] = s[i] - '0';
		}
		int i = 0, len_str = 0;
		while (i < len){//除 2 取余法
			buf[len_str++] = num[len - 1] % 2 + '0';//余数
			// 大数除法,更新 num[]数组
			int c = 0;
			for (int j = i; j < len; j++){
				int tmp = num[j];
				num[j] = (num[j] + c) / 2;//高位除 2(数的高位对应数组低位
				if (tmp % 2 == 1){//判断 tmp 是否为奇数
					c = 10;// 若 tmp 为奇数, 则该位必有余数 10
				}
				else c = 0;
			}
			if (num[i] == 0) i++;//高位变为 0
		}
		for (int j = len_str - 1; j >= 0; j--){
			printf("%c", buf[j]);
		}
		printf("\n");
	}
	return 0;
}

题型总结
这类题目任他千变万化, 本质上都是不变的。 就是数位的拆解与合并, 拆解很明显就是两步, 先取模然后除取整, 合并就是先乘后加。 只要掌握了以上的几类变化, 不管题目如何变化,你都立于了不败之地。

题目练习

DreamJudge 1454 反序数
DreamJudge 1259 进制转换 2
DreamJudge 1176 十进制和二进制
DreamJudge 1380 二进制数
DreamJudge 1417 八进制
DreamJudge 1422 进制转换 3
DreamJudge 1097 负二进制

排版类问题

排版类问题也是机试中经常出现的题目, 这类题目主要考验考生对代码的掌控程度。 表面上看起来很简单, 但是对于大部分没有认真研究过的同学来学, 这些题可能会搞半天才能搞出来。

总的来说, 排版类的题目可以以下几种题型为代表。

  1. 输出字符棱形 DreamJudge 1473
    这类题目的变形可以是输出长方形、 三角形、 梯形等形状。
  2. 旋转数字输出
  3. 矩阵顺/逆指针旋转
  4. 矩阵翻转
    这类题目的变形可以是轴对称翻转、 中心对称翻转等。
  5. 杨辉三角形
  6. 2048 问题

以上, 我们选择其中输出字符棱形和杨辉三角形进行详细讲解, 其他题型我们给出解题思路以及题目编号, 大家可以在本节后面的练习题目里找到并完成。

字符棱形

 题目描述:
输入一个整数 n 表示棱形的对角半长度, 请你用*把这个棱形画出来。
输入: 3
输出:
  *
 ***
*****
 ***
  *
输入样例#:
1 
输出样例#:
* 
题目来源:
DreamJudge 1473

解题分析: 对于这类题目, 我们可以将它进行分解。 从中间切开, 上面一个三角形, 下面一个三角形。 那么问题就转化为了如何输出三角形, 我们可以利用两个 for 循环控制来输出三角形。
参考代码

#include <stdio.h>
int main() {
	int n;
	scanf("%d", &n);
	//上三角
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n - i; j++) {
			printf(" ");
		}
		for (int j = n - i + 1; j < n + i; j++) {
			printf("*");
		}
		printf("\n");
	}
	//下三角 下三角只需要将上三角反过来输出就行
	for (int i = n - 1; i >= 1; i--) {
		for (int j = 1; j <= n - i; j++) {
			printf(" ");
		}
		for (int j = n - i + 1; j < n + i; j++) {
			printf("*");
		}
		printf("\n");
	}
	return 0;
}

杨辉三角形
题目描述:
提到杨辉三角形.大家应该都很熟悉.这是我国宋朝数学家杨辉在公元 1261 年著书《详解九章算法》 提出的。 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 1 5 10 10 5 1 1 6 15 20 15 6 1 我们不难看出其规律: S1: 这些数排列的形状像等腰三角形, 两腰上的数都是 1 S2: 从右往左斜着看,
第一列是 1, 1, 1, 1, 1, 1, 1; 第二列是, 1, 2, 3, 4, 5, 6; 第三列是 1, 3, 6, 10, 15;第四列是 1, 4, 10, 20; 第五列是 1, 5, 15; 第六列是 1, 6……。 从左往右斜着看, 第一
列是 1, 1, 1, 1, 1, 1, 1; 第二列是 1, 2, 3, 4, 5, 6……和前面的看法一样。 我发现这个数列是左右对称的。 S3: 上面两个数之和就是下面的一行的数。 S4: 这行数是第几行, 就是第二个数加一。 …… 现在要求输入你想输出杨辉三角形的行数 n; 输出杨辉三角形的前 n 行.
输入描述:
输入你想输出杨辉三角形的行数 n(n<=20);当输入 0 时程序结束.
输出描述:
对于每一个输入的数,输出其要求的三角形.每两个输出数中间有一个空格.每输完一个三角形换行.
输入样例#:
5
输出样例#:
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
题目来源:
DreamJudge 1062
杨辉三角

解题分析: 这是一道特别经典的题目, 我们只需要按照题意用二维数组去计算即可。 对于任意一个数 a[i][j], 都有 a[i][j] = a[i-1][j] + a[i-1][j-1];

参考代码

#include <stdio.h>
	int main() {
		int a[21][21] = {0};//数组里的所有值初始化为 0
		int n;
		while (scanf("%d", &n) != EOF) {
			if (n == 0) break;
			a[1][1] = 1;
			for (int i = 2; i <= n; i++) {
				for (int j = 1; j <= i; j++) {
					a[i][j] = a[i-1][j] + a[i-1][j-1];
				}
			}
			for (int i = 1; i <= n; i++) {
				for (int j = 1; j <= i; j++) {
					printf("%d ", a[i][j]);
				}
				printf("\n");
			}
		}
	return 0;
}

题型总结: 这类题目尽量在平时练习, 解法主要就是把一个大问题进行分解, 一部分一部分的实现。 在考试的时候遇到, 千万不要急, 将问题进行分解, 找到其中的规律, 然后再写出来。
当然, 如果平时就有练习, 那就不用担心了。

练习题目
DreamJudge 1392 杨辉三角形 - 西北工业大学
DreamJudge 1377 旋转矩 - 北航
DreamJudge 1216 旋转方阵
DreamJudge 1221 旋转矩阵
DreamJudge 1472 2048 游戏

日期类问题

日期类的题目也是常考的题目, 这类题目一般都为以下几种考法。

  1. 判断某年是否为闰年
  2. 某年某月某日是星期几
    变形问法: 某日期到某日期之间有多少天
  3. 某天之后 x 天是几月几日
  4. 10:15 分之后 x 分钟是几点几分
    变形问法: 某点到某点之间有多少分或多少秒

注意输入时候一般用 scanf 解析输入值
如: 2019-11-8 2019-11-08 2019/11/8 10:10

int year, month, day;
scanf("%d-%d-%d", &year, &month, &day);
scanf("%d/%d/%d", &year, &month, &day);
int hour, minute;
scanf("%d:%d", &hour, &minute);

日期
题目描述:
定义一个结构体变量(包括年、 月、 日) , 编程序, 要求输入年月日,计算并输出该日在本年中第几天。
输入描述:
输输入三个整数(并且三个整数是合理的,既比如当输入月份的时候应该在 1 至 12 之间, 不应该超过这个范围)否则输出 Input error!
输出描述:
输出一个整数.既输入的日期是本月的第几天。
输入样例#:
1985 1 20
2006 3 12
输出样例#:
20
71
题目来源:
DreamJudge 1051

解题分析: 这个题目的考点在于两个地方, 一个是每个月的天数都不一样, 另一个是 2 月如果是闰年则多一天, 最后我们还要判断输入的日期是否存在, 如果不存在则输出 Input error!

参考代码

#include <stdio.h>
struct node {
	int year, month, day;
}p;
int f[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int main() {
	while (scanf("%d%d%d", &p.year, &p.month, &p.day) != EOF) {
		//判断是否闰年
		if ((p.year%400== 0)||(p.year%4==0)&&(p.year%100!=0)) {
			f[2] = 29;
		}
		else f[2] = 28;
		int flag = 0;
		//判断月份输入是否合法
		if (p.month < 1 || p.month > 12) flag = 1;
		//判断天的输入是否合法	
		if (p.day < 0 || p.day > f[p.month]) flag = 1;
		if (flag) {
			printf("Input error!\n");
			continue;
		}
		int ans = p.day;
		for (int i = 1; i < p.month; i++) {
			ans += f[i];
		}
		printf("%d\n", ans);
	}
	return 0;
}

题型总结
日期类的题目就是要特别注意闰年的判断, 这些题目一般都是考察代码细节的把握, 时间类的

题目注意时间的转换, 1 天=24 小时, 1 小时=60 分, 1 分=60 秒。
特别注意: 一天之内时针和分针会重合 22 次, 而不是 24 次。

练习题目
DreamJudge 1011 日期
DreamJudge 1290 日期差值
DreamJudge 1410 打印日期
DreamJudge 1437 日期类
DreamJudge 1446 日期累加
DreamJudge 1053 偷菜时间表

字符串类问题

字符串类的问题也是各个院校必考的题型之一, 基本上有以下这些考点:

  1. 统计字符个数
  2. 单词首字母大写
  3. 统计子串出现次数
    解析: 考察大家基础的字符串遍历能力。
  4. 文本加密/解密
    解析: 通过循环往后移动 x 位或直接给一个映射表是比较常见的考法。
  5. 文本中的单词反序
    解析: 灵活使用 string 可以秒杀这类题目, 当然也可以用字符串一步步解析。
  6. 删除字符串(大小写模糊)
    解析: 如果大小写不模糊, 那么就是直接找到之后删除。 大小写模糊的话, 只是多一个判断。

加密算法
题目描述:
编写加密程序, 加密规则为: 将所有字母转化为该字母后的第三个字母, 即 A->D、 B->E、C->F、 …、 Y->B、 Z->C。 小写字母同上, 其他字符不做转化。 输入任意字符串, 输出加密
后的结果。
例如: 输入"I love 007", 输出"L oryh 007"
输入描述:
输入一行字符串, 长度小于 100。
输出描述:
输出加密之后的结果。
输入样例#:
I love 007
输出样例#:
L oryh 007
题目来源:
DreamJudge 1014

题目解析: 这是一道很常见的加解密考法, 往后移动 3 位是这道题的核心, 我们只需要按照题意将大写字母、 小写字母、 和其他分开进行处理就可以, 具体看代码。

参考代码

#include <stdio.h>
#include <string.h>
int main() {
	char s[105];
	gets(s);//输入一行文本用 gets
	int len = strlen(s);
	for (int i = 0; i < len; i++) {
		if (s[i] >= 'A' && s[i] <= 'Z') {
			s[i] += 3;
			if (s[i] > 'Z') s[i] -= 26;//溢出循环
		}
		else if (s[i] >= 'a' && s[i] <= 'z') {
			s[i] += 3;
			if (s[i] > 'z') s[i] -= 26;//溢出循环
		}
		else {
			continue;
		}
	}
	puts(s);
	return 0;
}

练习题目
DreamJudge 1012 字符移动
DreamJudge 1292 字母统计
DreamJudge 1240 首字母大写
DreamJudge 1394 统计单词
DreamJudge 1027 删除字符串 2

排序类问题

排序类的问题基本上是每个学校必考的知识点, 所以它的重要性不言而喻。如果你在网上一查, 或者看数据结构书, 十几种排序算法可以把你吓的魂不守舍。 表面上看各种排序都有其各自的特点, 那是不是我们需要掌握每一种排序呢?答案自然是否定的。 我们一种排序也不需要掌握, 你需要会用一个 sort 函数就可以了,正所谓一个 sort 走天下。
sort 函数本质上是封装了快速排序, 但是它做了一些优化, 所以你只管用它就行了。
复杂度为: nlogn
所以 sort 可以对最大 30W 个左右的元素进行排序, 可以应对考研机试中的 99.9%的情况。

sort 函数的用法

sort()函数:依次传入三个参数, 要排序区间的起点, 要排序区间的终点+1, 比较函数。 比较函数可以不填, 则默认为从小到大排序。

sort 函数两个常见的应用场景

  1. 自定义函数排序
  2. 多级排序

成绩排序
题目描述:
输入任意(用户, 成绩) 序列, 可以获得成绩从高到低或从低到高的排列,相同成绩都按先录入排列在前的规则处理。
示例:
jack 70
peter 96
Tom 70
smith 67
从高到低 成绩
peter 96
jack 70
Tom 70
smith 67
从低到高
smith 67
jack 70
Tom 70
peter 96
输入描述:
输入多行, 先输入要排序的人的个数, 然后输入排序方法 0( 降序) 或者 1( 升序) 再分别输入他们的名字和成绩, 以一个空格隔开
输出描述:
按照指定方式输出名字和成绩, 名字和成绩之间以一个空格隔开
输入样例#:
3 0
fang 90
yang 50
ning 70
输出样例#:
fang 90
ning 70
yang 50
题目来源:
DreamJudge 1151

题目解析: 这题唯一的一个考点在于稳定排序, sort 排序是不稳定的, 排序之后相对次序有可能发生改变。 解决这个问题有两个方法, 一个是用 stable_sort 函数, 它的用法和 sort 一样, 但是它是稳定的, 所以如果我们遇到有稳定的需求的排序时, 可以用它。 另一个方法是给每一个输入增加一个递增的下标, 然后二级排序, 当值相同时, 下标小的排在前面(我理解的是在结构体排序,写cmp函数的时候,保持稳定性。就是用<来跳转而不是<=来跳转)

参考代码(稳定排序)

#include <bits/stdc++.h>
using namespace std;
struct Student {
	string name;
	int grade;
}stu[1005];
//从大到小排序
bool compareDesc(Student a,Student b) {
	return a.grade > b.grade;
}
//从小到大排序
bool compareAsc(Student a,Student b) {
	return a.grade < b.grade;
}
int main() {
	int n,order;
	while(cin>>n) {
		cin>>order;
		for(int i=0;i<n;i++) {
			cin>>stu[i].name>>stu[i].grade;
		}
		if(order==0)
			stable_sort(stu,stu+n,compareDesc);
		else
			stable_sort(stu,stu+n,compareAsc);
		for(int i=0;i<n;i++) {
			cout<<stu[i].name<<" "<<stu[i].grade<<endl;
		}
	}
	return 0;
}

参考代码(标记 id)

#include <bits/stdc++.h>
using namespace std;
struct Student {
	string name;
	int grade, id;
}stu[1005];
//从大到小排序
bool compareDesc(Student a,Student b) {
	if (a.grade == b.grade) return a.id < b.id;
	return a.grade > b.grade;
}
//从小到大排序
bool compareAsc(Student a,Student b) {
	if (a.grade == b.grade) return a.id < b.id;
	return a.grade < b.grade;
}
int main() {
	int n,order;
	while(cin>>n) {
		cin>>order;
		for(int i=0;i<n;i++) {
			cin>>stu[i].name>>stu[i].grade;
			stu[i].id = i;//通过标记 ID 进行判断
		}
		if(order==0)
			sort(stu,stu+n,compareDesc);
		else
			sort(stu,stu+n,compareAsc);
		for(int i=0;i<n;i++) {
			cout<<stu[i].name<<" "<<stu[i].grade<<endl;
		}
	}
	return 0;
}

排序
题目描述:
输入 n 个数进行排序, 要求先按奇偶后按从小到大的顺序排序。
输入描述:
第一行输入一个整数 n, 表示总共有多少个数, n<=1000。
第二行输入 n 个整数, 用空格隔开。
输出描述:
输出排序之后的结果。
输入样例#:
8
1 2 3 4 5 6 7 8
输出样例#:
1 3 5 7 2 4 6 8
题目来源:
DreamJudge 1010

题目解析: 题目要求我们按照奇数在前偶数在后的排序方法, 同为奇数或同为偶数再从小到大排序。 我们有两种简便的方法可以解决这个问题, 其一是我们将奇数和偶数分离开来, 然后分别排好序, 再合并在一起。 其二是使用 sort 进行二级排序, 这里我们采用第二种方法进行演示。

参考代码

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. bool cmp(int a,int b){
  4. if(a % 2 == b % 2)//如果同奇同偶
  5. return a < b;//直接从小到大排序
  6. else//如果奇偶性不同
  7. return (a%2) > (b%2);//奇数在偶数前
    1. }
  8. int main() {
  9. int n;
  10. int a[1005] = {0};
  11. cin >> n;
  12. for (int i = 0; i < n; i++) {
  13. cin >> a[i];
  14. }
  15. sort(a, a+n, cmp);
  16. for(int i = 0; i < n; i++) {
  17. cout << a[i] << " ";
  18. }
  19. cout << endl;
  20. return 0;
  21. }

小结: 由上面可以看出, 只要我们掌握好 sort 的用法, 不管什么样的花里胡哨的排序, 我们
都可以一力破之。

一些特殊的排序题

1、 如果题目给的数据量很大, 上百万的数据要排序, 但是值的区间范围很小, 比如值最大只有 10 万, 或者值的范围在 1000W 到 1010W 之间, 对于这种情况, 我们可以采用空间换时间的计数排序。
2、 字符串的字典序排序是一个常见的问题, 需要掌握, 也是用 sort。
下面两种情况了解即可, 追求满分的同学需要掌握
3、 如果题目给你一个数的序列, 要你求逆序数对有多少, 这是一个经典的问题, 解法是在归并排序合并是进行统计, 复杂度可以达到 nlogn。 如果数据量小, 直接冒泡排序即可。
4、 如果题目让你求 top10, 即最大或最小的 10 个数, 如果数据量很大, 建议使用选择排序, 也就是一个一个找, 这样复杂度比全部元素排序要低。
5、 如果题目给的数据量有几百万, 让你从中找出第 K 大的元素, 这时候 sort 是会超时的。解法是利用快速排序的划分的性质, 进入到其中一个分支继续寻找。

以上都是一些数据很特殊且数据量非常大的情况下的解决方案

由于部分同学只能用纯 C 语言机试, 这里给出纯 C 语言实现的排序代码模板

普通排序(适合 n 在 2000 以内的题目)

#include <stdio.h>
const int maxn = 1005;
int a[maxn];
int main() {
	int n;
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
	for (int i = 1; i <= n; i++) {//两个 for 都是 1 到 n 方便好记
		for (int j = 1; j < n ;j++) {
			if (a[j] > a[j + 1]) {//交换 a[j]和 a[j+1]
				int temp = a[j];
				a[j] = a[j+1];
				a[j+1] = temp;
			}
		}
	}
	for (int i = 1; i <= n; i++) {
		printf("%d ", a[i]);
	}
	printf("\n");
	return 0;
}

快速排序(适合 n 在 50W 以内的题目)

  1. #include <stdio.h>
  2. const int maxn = 100005;
  3. int a[maxn];
  4. //快速排序
  5. void Quick_Sort(int l, int r) {
  6. if(l >= r) return;
  7. int i = l,j = r,x = a[l];
  8. while (i < j) {
  9. while (i < j && a[j] >= x) j–;
  10. if (i < j) a[i++] = a[j];
  11. while (i < j && a[i] < x) i++;
  12. if (i < j) a[j–] = a[i];
  13. }
  14. a[i] = x;
  15. Quick_Sort(l, i - 1);
  16. Quick_Sort(i + 1, r);
  17. }
  18. int main() {
  19. int n;
  20. scanf("%d", &n);
  21. for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
  22. Quick_Sort(1, n);//传入左边界下标和右边界下标
  23. for (int i = 1; i <= n; i++) {
  24. printf("%d ", a[i]);
  25. }
  26. printf("\n");
  27. return 0;
  28. }

练习题目
DreamJudge 1106 排序 2
DreamJudge 1159 成绩排序 2.0
DreamJudge 1217 国名排序
DreamJudge 1227 日志排序
DreamJudge 1248 整数奇偶排序
DreamJudge 1254 字符串排序
DreamJudge 1255 字符串排序 2
DreamJudge 1261 字符串排序 3
DreamJudge 1294 后缀子串排序
DreamJudge 1310 奥运排序问题
DreamJudge 1338 EXCEL 排序
DreamJudge 1360 字符串内排序
DreamJudge 1399 排序 - 华科
DreamJudge 1400 特殊排序
DreamJudge 1404 成绩排序 - 华科
DreamJudge 1412 大整数排序
DreamJudge 1817 成绩再次排序
DreamJudge 1798 数组排序

查找类问题

查找是一类我们必须掌握的算法, 它不仅会在题目中直接考察, 同时也可能是其他算法中的重要组成部分。 本章中介绍的查找类问题都是单独的基础查找问题, 对于这类基础查找的问题, 我们应该将它完全掌握。

查找类题目一般有以下几种考点

  1. 数字查找: 给你一堆数字, 让你在其中查找 x 是否存在
    题目变形: 如果 x 存在, 请输出有几个。
  2. 字符串查找: 给你很多个字符串, 让你在其中查找字符串 s 是否存在

顺序查找就不说了, 这个大家会。
什么时候不能用顺序查找呢?
很明显, 当满足下面这种情况的时候

  1. 数据量特别大的时候, 比如有 10W 个元素。
  2. 查询次数很多的时候, 比如要查询 10W 次。

遇到这类题大多数人的想法是先 sort 排序, 然后二分查找, 这是一个很常规的解决这类问题的方法。
但是, 我们不推荐你这么做, 我们有更简单易用且快速的方法。 我们推荐你了解并使用map 容器。
前面介绍过 map, 它是 STL 的一种关联式容器, 它的底层是红黑树实现的, 也就意味着它的插入和查找操作都是 log 级别的。相信每一个用过 map 的同学, 都会情不自禁的说一句, map 真香!

查找学生信息 2
题目描述:
输入 N 个学生的信息, 然后进行查询。
输入描述:
输入的第一行为 N, 即学生的个数(N<=1000)
接下来的 N 行包括 N 个学生的信息, 信息格式如下:
01 李江 男 21
02 刘唐 男 23
03 张军 男 19
04 王娜 女 19
然后输入一个 M(M<=10000),接下来会有 M 行, 代表 M 次查询, 每行输入一个学号, 格式如下:
02
03
01
04
输出描述:
输出 M 行, 每行包括一个对应于查询的学生的信息。
如果没有对应的学生信息, 则输出“No Answer!”
输入样例#:
4
01 李江 男 21
02 刘唐 男 23
03 张军 男 19
04 王娜 女 19
5
02
03
01
04
03
输出样例#:
02 刘唐 男 23
03 张军 男 19
01 李江 男 21
04 王娜 女 19
03 张军 男 19
题目来源:
DreamJudge 1476

题目解析: 对于这类查询量大的题目, 我们有两种方法来解决这个问题。 第一是将学号先排好序, 然后使用二分查找, 但是很多同学写二分的时候容易出现问题, 而且代码量也比较大, 我们不推荐这种做法。 推荐大家使用 map 来解决这类问题, 基本上 map 可以通过 99.9%的这类题目

参考代码

#include <bits/stdc++.h>
using namespace std;
struct node{
	string num;
	string name;
	string sex;
	int age;
};
int main(){
	int n,q;
	map<string, node> M;//定义一个 map 映射
	while(scanf("%d", &n)!=EOF){
		for(int i=0;i<n;i++){
			node tmp;
			cin>>tmp.num>>tmp.name>>tmp.sex>>tmp.age;
			M[tmp.num] = tmp;//将学号指向对应的结构体
		}
		scanf("%d", &q);
		for(int i=0;i<q;i++){
			string num;
			cin>>num;
			if((M.find(num))!=M.end())//find 查找 如果找不到则返回末尾
				cout<<M[num].num<<" "<<M[num].name<<" "<<M[num].sex<<" "<<M[num].age<<endl;
			else
				cout<<"No Answer!"<<endl;
		}
	}
	return 0;
}

可以发现, 用 map 解决这个题目的时候, 不用去考虑字符串排序的问题, 也不用想二分查找会不会写出问题, 直接用 map, 所有的烦恼都没有了, 而且它的复杂度和二分查找是一个量级的。

上面讲的是一类静态查找的问题, 实际中为了增加思维难度或代码难度, 会经过一定的改变变成动态查找问题。

动态查找问题
题目描述:
有 n 个整数的集合, 想让你从中找出 x 是否存在。
输入描述:
第一行输入一个正整数 n(n < 100000)
第二行输入 n 个正整数, 用空格隔开。
第三行输入一个正整数 q(q<100000) , 表示查询次数。
接下来输入 q 行, 每行一个正整数 x, 查询 x 是否存在。
输出描述:
如果 x 存在, 请输出 find, 如果不存在, 请输出 no, 并将 x 加入到集合中。
输入样例#:
5
1 2 3 4 5
3
6
6
3
输出样例#:
no
find
find
题目来源:
DreamJudge 1477

题目解析: 通过分析题目我们可以发现, 这道题有一个特点就是, 数的集合在不断的改变。 如果我们用先排序再二分的方法就会遇到困难, 因为加入新的数的时候我们需要去移动多次数组, 才能将数插入进去, 最坏情况每次插入都是 O(n)的复杂度, 这是无法接受的。 当然也不是说就不能用这样的方法来解决了, 可以用离线的方法来解决这个问题, 但是这样做太复杂,不适合在考试中使用。 那么我们考虑用 map 来解决这个问题。

参考代码

#include <bits/stdc++.h>
using namespace std;
int main(){
	int n,q,x;
	map<int, int> M;//定义一个 map 映射
	scanf("%d", &n);
	for (int i = 0; i < n; i++) {
		scanf("%d", &x);
		M[x]++;//记录集合中 x 有多少个
	}
	scanf("%d", &q);
	for (int i = 0; i < q; i++) {
		scanf("%d", &x);
		if (M[x] == 0) {//如果 x 的个数为 0
			printf("no\n");
			M[x]++;//将 x 加入到集合中
		}
		else printf("find\n");
	}
	return 0;
}

看了上面的代码, 是不是发现用 map 来解决问题真是超级简单。 所以学会灵活使用 map将能极大的拉近你和大佬之间的距离, 我们一起来学 map 吧!

当然不是说二分查找就没用了, 我们也需要了解二分查找的原理, 只不过。 二分的前提是单调性, 只要满足单调性就可以二分, 不论是单个元素还是连续区间。 下面我们也给出一个基本的二分查找代码, 供大家参考。

#include <bits/stdc++.h>
using namespace std;
int a[10005];
int main(){
	int n,x;
	scanf("%d", &n);//输入 n 个数
	for (int i = 1; i <= n; i++) {
		scanf("%d", &a[i]);
	}
	sort(a+1, a+1+n);//排序保持单调性
	scanf("%d", &x);//要查找的数 x
	int l = 1, r = n;
	while (l <= r) {
		int mid = (l + r) / 2;
		if (a[mid] == x) {
			printf("find\n");
			return 0;
		}
		if (a[mid] > x) {//如果 x 比中间数小
		r = mid - 1;//说明在左区间
	}
	else l = mid + 1;//否则在右区间内
	}
	printf("not find\n");
	return 0;
}

注: 由于部分同学只能用纯 C 语言机试, 这里给出纯 C 语言实现查找的方法。普通查找即顺序查找,相信同学们都会。二分查找即将上面的 sort 排序改为上一节纯 C 语言实现的快速排序即可。

练习题目
DreamJudge 1177 查找学生信息
DreamJudge 1388 查找 1
DreamJudge 1387 查找 - 北邮
DreamJudge 1383 查找第 K 小数

贪心类问题

贪心类问题是很常见的考点, 贪心算法更重要的是一种贪心的思想, 它追求的是当前最优解, 从而得到全局最优解。 贪心类问题基本上算是必考题型之一, 它能更好的考察出学生的思维能力以及对问题的分析能力, 很多学校的出题人都非常爱出贪心类的题目。

贪心算法是指在对问题求解时, 总是做出在当前看来是最好的选择。 也就是说, 不从整体最优上加以考虑, 只做出在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解, 关键是贪心策略的选择, 选择的贪心策略必须具备无后效性, 即某个状态以前的过程不会影响以后的状态, 只与当前状态有关。贪心可以很简单, 简单到让所有人一眼就能看出来该怎么做。 贪心也可以很难, 难到让你没办法去证明这样贪心的正确性。 所以要想解决贪心这类问题, 主要还是看你的悟性, 看你对题目的分析能力如何, 下面我们举例说明。

例子 1: 地上有 3 张纸币, 分别是 5 元、 1 元和 10 元, 问你只能拿一张, 最多能拿多少钱?
解析: 很明显, 10 元。
例子 2: 地上有 n 张纸币, 有 1 元的, 有 5 元的, 还要 10 元的, 问你只能拿一张, 最多能拿多少钱?
解析: 很明显, 还是 10 元。
例子 3: 地上有很多纸币, 有 a 张 1 元的, 有 b 张 5 元的, 还要 c 张 10 元的, 问你从中拿 x张, 最多能拿多少钱?
解析: 大家应该都能想到, 肯定是优先拿 10 元的, 如果 10 元的拿完了, 再拿 5 元的, 最后才会拿 1 元的。 这就是贪心的思想, 所以贪心其实是很容易想到的。
例子 4: 有 n 个整数构成的集合, 现在从中拿出 x 个数来, 问他们的和最大能是多少?
解析: 相信大家都能想到, 优先拿大的, 从大到小一个个拿, 这样组成的和最大。 那么在解决这个问题之前, 我们需要先排序, 从大到小的排好序, 然后将前 x 个数的和累加起来就是答案。

从上面几个例子中, 相信大家对贪心已经有了初步的了解。 我们使用贪心的时候, 往往需要先
按照某个特性先排好序, 也就是说贪心一般和 sort 一起使用。

喝饮料
题目描述:
商店里有 n 中饮料, 第 i 种饮料有 mi 毫升, 价格为 wi。
小明现在手里有 x 元, 他想吃尽量多的饮料, 于是向你寻求帮助, 怎么样买才能吃的最多。
请注意, 每一种饮料都可以只买一部分。
输入描述:
有多组测试数据。
第一行输入两个非负整数 x 和 n。
接下来 n 行, 每行输入两个整数, 分别为 mi 和 wi。
所有数据都不大于 1000。
x 和 n 都为-1 时程序结束。
输出描述:
请输出小明最多能喝到多少毫升的饮料, 结果保留三位小数。
输入样例#:
233 6
6 1
23 66
32 23
66 66
1 5
8 5
-1 -1
输出样例#:
136.000
题目来源:
DreamJudge 1478

题目解析: 通过分析之后我们可以发现, 小明想要喝尽量多的饮料的话, 肯定优先选择性价比最高的饮料喝, 也就是说 1 毫升的价格最低的饮料先喝, 那么我们就需要去比较, 每种饮料 1毫升的价格是多少。 然后按照这个单价从低到高依次排序, 然后一个一个往后喝, 这样可以保证小明能喝到最多的饮料。
参考代码

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. struct node {
  4. double w, m;
  5. }p[1005];
  6. bool cmp(node a, node b) {
  7. //按照每毫升的价格从低到高排序
  8. return a.w/a.m < b.w/b.m;
  9. }
  10. int main(){
  11. int n,x;
  12. while (scanf("%d%d", &x, &n) != EOF) {
  13. if (x == -1 && n == -1) break;
  14. for (int i = 1; i <= n; i++) {
  15. scanf("%lf%lf", &p[i].m, &p[i].w);
  16. }
  17. sort(p+1, p+1+n, cmp);
  18. double ans = 0;
  19. for (int i = 1; i <= n; i++) {
  20. if (x >= p[i].w) {//如果剩余的钱能全买
  21. ans += p[i].m;
  22. x -= p[i].w;
  23. }
  24. else {//如果剩余的钱买不完这种饮料
  25. ans += (p[i].m*x/p[i].w);
  26. break;//到这里 x 已经为 0 了
  27. }
  28. }
  29. printf("%.3lf\n", ans);
  30. }
  31. return 0;
  32. }

解题的通用步骤

  1. 建立数学模型来描述问题;
  2. 把求解的问题分成若干个子问题;
  3. 对每一子问题求解, 得到子问题的局部最优解;
  4. 把子问题的局部最优解合成原来问题的一个解。

题型总结
贪心问题在很多机试难度低的学校, 可以成为压轴题, 也就是通过人数最少的题目。 在机试难度高的学校也是中等难度及以上的题目, 为什么明明贪心看起来这么容易的题目, 却成为大多数学生过不去的坎呢? 原因有二, 一是很多同学根本就没有想到这个题目应该用贪心算法, 没能将题目抽象成数学模型来分析, 简单说就是没有读懂题目隐藏的意思。 二是读懂题了, 知道应该是贪心算法解这个题目, 但是排序的特征点却没有找准, 因为不是所有题目都是这么明显的看出来从小到大排序, 有的题目可能隐藏的更深, 但是这种难度的贪心不常见。 所以机试中的贪心题, 你要你反应过来这是一个贪心, 99%的情况下都能解决。

练习题目
DreamJudge 1307 组队刷题
DreamJudge 1347 To Fill or Not to Fill

链表类问题

链表类问题属于选读章节, 对于使用 OJ 测评的院校的同学来说, 这类问题可以用数组来实现, 没有必要用链表去实现, 写起来慢不说, 还容易出错, 所以我们一般都直接用数组来实现, 反正最后 OJ 能 AC 就行, 建议这类同学跳过本节或仅做了解即可。 但是对于非 OJ 测评的院校来说, 链表类问题可以说是必考的题型。

一般来说有以下三种常见考点

  1. 猴子报数
    解析: 循环链表建立之后, 按照题意删除节点。
  2. 两个有序链表合并为一个
    解析: 这个和两个有序数组合并为一个有序数组原理一样。
  3. 链表排序
    解析: 使用冒泡排序进行链表排序, 因为冒泡排序是相邻两个元素进行比较交换, 适合链表。

猴子报数
题目描述:
n 个猴子围坐一圈并按照顺时针方向从 1 到 n 编号, 从第 s 个猴子开始进行 1 到 m 的报数, 报数到第 m 的猴子退出报数, 从紧挨它的下一个猴子重新开始 1 到 m 的报数, 如此进行下去直到所有的猴子都退出为止。 求给出这 n 个猴子的退出的顺序表。
输入描述:
有做组测试数据. 每一组数据有两行, 第一行输入 n(表示猴子的总数最多为 100) 第二行输入数据 s(从第 s 个猴子开始报数)和数据 m(第 m 个猴子退出报数). 当输入 0 0 0 时表示程序结束.
输出描述:
每组数据的输出结果为一行, 中间用逗号间隔。
输入样例#:
10
2 5
5
2 3
0
0 0
输出样例#:
6,1,7,3,10,9,2,5,8,4
4,2,1,3,5
题目来源:
DreamJudge 1081

题目解析: 我们需要创建一个首尾相连的循环链表, 然后先走 s 步, 再开始循环遍历链表, 每走 m 步删除一个节点, 知道链表中只能下一个节点时结束循环。 只能一个节点的判断条件是,它的下一个指针指向的是它, 说明它自循环了。

参考代码

#include <stdio.h>
#include <malloc.h>
struct node {
	int num;
	struct node *next;
};
int n, s, m;
//创建循环链表
struct node* create() {
	struct node *head, *now, *pre;
	for (int i = 1; i <= n; i++) {
		now = (struct node *)malloc(sizeof(node));
		if (i == 1) {
			head = now;
			pre = now;
		}	
		now->num = i;
		now->next = head;
		pre->next = now;
		pre = now;
	}
	return head;
};
//按照题目要求输出
void print(struct node *head) {
	struct node *p, *pre;//pre 是前一个节点
	p = head;
	s -= 1;//因为起点在第一个 所以要少走一步
	while (s--) {//先走 s 步
		pre = p;
		p = p->next;
	}
	int i = 1;
	while (p != NULL) {
		if (p == p->next) {//只剩最后一个
			printf("%d\n", p->num);
			break;
		}
		if (i % m == 0) {//找到第 m 个
			printf("%d,", p->num);//输出它
			pre->next = p->next;//删除它
		}
		else pre = p;//这里一定要用 else 如果是删除的话 pre 不变
		p = p->next;
		i++;
	}
}
int main(){
	while (scanf("%d%d%d", &n, &s, &m) != EOF) {
		if (n==0&&s==0&&m==0) break;
		struct node *head;
		head = create();
		print(head);
	}
	return 0;
} 

练习题目
DreamJudge 1015 单链表
DreamJudge 1018 击鼓传花
DreamJudge 1025 合并链表
DreamJudge 1405 遍历链表

训练的题目

这里
考虑新开一个帖子,然后把训练的题目和想法都发到那个帖子里,有空整一个

day 5: 这节课我觉得没讲通C++的STL,但训练题目用到了map,要自己深入学习一下C++STL
STL教程:C++ STL快速入门(非常详细)
C++ STL map容器详解
dreamjudge1204
dreamjudge1205 这道题是string和map的使用,都学习完以后就可以实现(不难)

番外

ASCII码对照表

ASCII

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值