C++语言基础

目录

编程工具devc++

devc++是一个非常简洁的开发工具,集编辑器和编译器为一体,非常适合初学者使用。

下载地址

https://sourceforge.net/projects/orwelldevcpp/

设置软件为中文

1、点击上方"Tools"->"Environment Options"
image
2、找到"Language",选择“简体中文/Chinese”即可
image

使用方法

1、安装完成后打开软件
image
2、点击左上角“文件”->“新建”->“源代码”
image
3、点击左上角“文件”->“另存为”,将文件保存到本地
image
4、打开刚刚保存的文件,开始编写代码
image
5、编写完代码后,点击“运行”->“编译运行”,即可将代码跑起来
17c5f633666097efdbdfaf301881d148
image

打开调试信息

1、点击上方菜单栏“工具”-“编译选项”
51ecdad287f0340531c129148b414ee0
2、选择“代码生成/优化”-“连接器”-“产生调试信息”,改为Yes
image

添加编译命令

信息学竞赛的编译环境为C++14,开O2,可将命令添加到devc++
命令:-O2 -std=c++14
方法:打开“工具”-“编译选项”,在“编译时加入以下命令”中,添加上述命令
image

顺序结构

1.1 代码框架

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

int main() 
{ 
	return 0;
} 

上述代码为c++程序的主体框架,在写程序前先把这个框架打出来,然后再填充具体内容

#include<bits/stdc++.h>表示包含万能头文件,包含信息奥赛所有功能
using namespace std;声明定义域std
int main() 主函数,c++程序运行时首先进入主函数
return 0;函数的返回值

1.2 程序员的第一段代码“hello, world”

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

int main() 
{ 
	cout << "hello, world" << endl;

	return 0;
} 

代码解释:
cout为输出功能,上述代码表示输出一个字符串"hello, world",endl表示回车

1.2.1 例题:Hello,World!

#include <bits/stdc++.h>	//万能头文件
using namespace std;		//包含定义域

int main()
{
	cout << "Hello,World!" << endl;
	
	return 0;
} 

1.3 变量

在编写程序时,需要输入数据,程序会把这些数据保存到计算机内存当中,而声明一个变量就是在内存中占据一定的空间,对变量的操作就是对内存的操作。

1.3.1 命名规则

①变量名必须以字母或下划线开头,名字中间只能由字母、数字、下划线"_"组成
②变量名在有效范围内必须是唯一的
③变量名不能是关键字

1.3.2 c++中的数据类型

c++中的数据必须有其数据类型,常用的基本数据类型有整形、浮点型、字符型、布尔型。每一种数据类型都有自己的数据范围和在内存中存储大小。

类型 关键字 格式控制符 存储(字节) 范围
短整型 short int %hd 2 \(-2^{15}\)~\(2^{15}-1\)
无符号短整型 unsigned short int %hu 2 \(0\)~\(2^{16}-1\)
整型 int %d 4 \(-2^{31}\)~\(2^{31}-1\)
无符号整型 unsigned int %u 4 \(0\)~\(2^{32}-1\)
长整型 long long %lld 8 \(-2^{63}\)~\(2^{63}-1\)
无符号长整型 unsigned long long %llu 8 \(0\)~\(2^{64}-1\)
单精度浮点型 float %f 4 6~7位有效位数
双精度浮点型 double %lf 8 15~16位有效数
长双精度浮点型 long double %llf 16 18~19位有效数
字符型 char %c 1
布尔型 bool 1 c++有,c无

1.3.3 变量定义

int a;  //定义一个整形变量,可用于存储整数 
double b;//定义一个双精度浮点型变量,可用于存储小数 
char c;//定义一个字符型变量,可用于存储特殊字符 

1.4 输入、输出

cin/cout写法

基本写法

int a, b, c, d;  //定义4个整型变量
cin >> a;  //输入一个变量
cin >> b >> c >> d;  //输入多个变量

cout << a;  //输出一个变量
cout << b << c << d;  //输出多个变量

double d;    //定义一个双精度浮点型,用于存储小数 
cin >> d;    //输入一个小数
cout << d;   //输出一个小数 

char c;    //定义一个字符变量,用于存储特殊字符
cin >> c;  //输入一个特殊字符
cout c;    //输出一个特殊字符

例题

例题1:A+B

#include <bits/stdc++.h>	//万能头文件
using namespace std;		//包含定义域

int main()
{
	int a, b, c;
	
	cin >> a;	//输入a
	cin >> b;	//输入b
	c = a + b;	//运算
	cout << c; 	//输出c
	
	return 0;
} 

例题2:字符三角形

#include <bits/stdc++.h>	//万能头文件
using namespace std;		//包含定义域

int main()
{
	char c;
	cin >> c;
	cout << "  " << c << "  " << endl;
	cout << " " << c << c << c << " " << endl;
	cout << c << c << c << c << c;
	
	return 0;
} 

输入输出流的控制符

控制符 作 用
dec 设置数值的基数为10
hex 设置数值的基数为16
oct 设置数值的基数为8
setfill(c) 设置填充字符c,c可以是字符常量或字符变量
setprecision(n) 设置浮点数的精度为n位。在以一般十进制小数形式输出时,n代表有效数字。在以fixed(固定小数位数)形式和 scientific(指数)形式输出时,n为小数位数
setw(n) 设置字段宽度为n位
setiosflags( ios::fixed) 设置浮点数以固定的小数位数显示
setiosftags( ios::scientific) 设置浮点数以科学记数法(即指数形式)显示
setiosflags( ios::left) 输出数据左对齐
setiosflags( ios::right) 输出数据右对齐
setiosflags( ios::skipws) 忽略前导的空格
setiosflags( ios::uppercase) 数据以十六进制形式输出时字母以大写表示
setiosflags( ios::lowercase) 数据以十六进制形式输出时宇母以小写表示
setiosflags(ios::showpos) 输出正数时给出“+”号

使用方法

int a, b, c;
cout << a << " " << b << " " << c;  //输出三个变量,用空格隔开

//引号内的字符会原样输出,起到提示作用
cout << "a=" << a << "b=" << b << "c=" << c;

cout << a << " " << b << endl;  //endl表示回车

//变量a,b输出最少5个字符,不够左边补空格
cout << setw(5) << a << b << endl;

//输出变量a,保留两位小数 
cout << setprecision(2) << fixed << a << endl;

例题

例题1:保留3位小数的浮点数

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

int main() 
{
	double a;
	
	cin >> a;
	cout << fixed << setprecision(3) << a;
	 
	return 0;
}

scanf/printf写法

scanf

说明: 格式化输入函数,用于按指定格式读取数据
基本语法:
scanf("格式字符串", &变量地址1, &变量地址2, ...)
示例:

int a, b;
scanf("%d%d", &a, &b);	//读入整型数据

double pi;
scanf("%lf", &pi);	//读入双精度浮点型数据

char ch;
scanf("%c", &ch);	//读入字符型数据
scanf(" %c", &ch);	//%c前加空格,跳过前导空白

char s[100];
scanf("%s", s);		//读入字符数组

注意事项:

  • 取地址符&:初字符串数组(数组名本身就是地址,无需&)外,其他变量必须使用&传地址
  • 空白字符:读\(%c\)时会包括空格/换行,若要跳过前导空白,需要在\(%c\)前加空格

printf

说明: 格式化输入函数,用于将数据按指定格式输出
基本语法:
printf("格式字符串", 变量1, 变量2, ...)
示例:

int a, b;
printf("%d %d", a, b);//输出两个整型数据,以空格隔开

double pi;
printf("%lf", pi);	//输出一个双精度浮点型数据
printf("%.2f", pi);	//输出一个浮点数,保留两位小数

char ch;
printf("%c", ch);	//输出一个字符

char s[100];
printf("%s", s);	//输出字符串s

注意事项:
可以通过格式说明符控制精度、宽度等

  • 保留\(n\)位小数:%.nf(如%.2f保留2位)
  • 指定输出宽度:%5d(整数占5个字符宽度,不足补空格)

1.5 运算符

1.5.1 赋值运算符 "="

语法:
变量 = 表达式

int a, b;
a = 10;	//将a的值设为10
b = a;	//将a的值赋给b
a = b = 5;	//将a和b的值都设为5

1.5.2 算术运算符

运算符 描述
+ 把两个操作数相加
- 从第一个操作数中减去第二个操作数
* 把两个操作数相乘
/ 分子除以分母
% 取模运算符,整除后的余数
++ 自增运算符,整数值增加 1
-- 自减运算符,整数值减少 1
int a = 8, b = 5, c = 0;

c = a + b;	//c为13
c = a - b;	//c为3
c = a * b;	//c为40
c = a / b;  //c为1,此处注意8/5为1.6,但c为int,所以只保留整数部分
c = a % 5;	//c为3

a = 8;
a++;		//a为9

a = 8;
c = a++;	//先执行c=a,再执行a++,故c为8,a为9

a = 8;
c = ++a;	//先执行++a, 再执行c=a,故c为9,a为9

a = 8;
a--;	//a为7

a = 8;
c = a--;	//先执行c=a,再执行a--,故c为8,a为7

a = 8;
c = --a;	//先执行--a, 再执行c=a,故c为7,a为7 

1.5.3 例题

例题:计算(a+b)×c的值

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

int main() 
{ 
	int a = 0, b = 0, c = 0, d = 0;
	
	cin >> a >> b >> c;
	d = (a + b) * c;
	cout << d;

	return 0;
}  

1.6 常量

常量就是不能被改变的值

1.6.1 类型

常量可以是整形、浮点型、字符型等

10;				//单独一个整形,常量
10 = 15;		//报错,不能给常量赋值

3.14;			//单独一个浮点型,常量
3.14 = 9.3;		//报错,不能给常量赋值

'#';			//单独一个字符型,常量
'#' = '*';		//报错,不能给字符常量赋值

1.6.1 自定义常量

1、使用const关键字

const int maxn = 100;	//maxn为值为100的常量

maxn = 150;				//报错,不能给常量赋值

int n = 0;
n = maxn;				//正确,可以把常量的值赋给其他变量

2、使用 #define 宏定义

#define PI 3.14		//PI为值为3.14的常量

PI = 1.23;			//报错,不能给常量赋值

double a = 0;
a = PI;				//正确,可以把常量的值赋给其他变量

1.7 自动类型转换

1、一个表达式中出现不同类型间的混合运算,较低类型自动向较高类型转换
①整型类别从低到高:

int -> unsigned int -> long -> unsigned long -> long long -> unsigned long long
int a = 10;
long long b = 5;

a + b;	//a*b的值类型为long long

②浮点型类别从低到高:

float -> double
float a = 3.14;
double b = 5.2;

a * b;	//a*b的值类型为double

③当表达式中有浮点型数据时,所有操作数自动转换为浮点型

int a = 3;
double b = 5.2;

a * b;	//a*b的值类型为double

1.8 强制类型转换

形式为:(类型说明符)(表达式)

int a = 12, b = 5;
double c = 0, d = 0;

c = a / b;	//c的值为2,因为a和b都为整型,所以a/b也为整型,然后再赋值给c

d = (double)a / b;	//d的值为2.4,(double)a强制将a转为浮点型,这样a/b的类型也为浮点型,然后再赋值给d

1.8.1 例题

例题:计算球的体积

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

const double PI = 3.14;
double v = 0, r = 0;

int main() 
{ 
	cin >> r;
	v = 4.0 / 3 * PI * r * r * r;
	cout << fixed << setprecision(2) << v;

	return 0;
}  

选择结构

2.1 运算符

2.1.1 关系运算符

下表显示了 C++ 支持的关系运算符。假设变量 A = 10,变量 B=20,则:

运算符 描述 实例
== 检查两个操作数的值是否相等,如果相等则条件为真。 (A == B) 不为真。
!= 检查两个操作数的值是否相等,如果不相等则条件为真。 (A != B) 为真。
> 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 (A > B) 不为真。
< 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 (A < B) 为真。
>= 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 (A >= B) 不为真。
<= 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 (A <= B) 为真。

2.1.2 逻辑运算符

下表显示了 C++ 支持的关系逻辑运算符。假设布尔变量 A = true,布尔变量 B = false,则:

运算符 描述 实例
&& 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 (A && B) 为假。
|| 称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 (A || B) 为真。
! 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 !(A && B) 为真。

ps:若变量为整型,则0为false,非0为true

2.2 if语句

2.1.1 单分支结构

if(条件表达式)
{
	语句1;
	语句2;
	···
}

功能:如果条件表达式的值为真,即条件成立,花括号中的语句将被顺序执行。否则,花括号中的所有语句将被忽略(不被执行),程序将按顺序从整个选择结构之后的下一条语句继续执行。
说明:格式中的“条件表达式”必须用圆括号括起来,执行语句若只有一条,可省略花括号,两条及以上,必须要有花括号
示例代码:

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

int main() 
{ 
	int a = 15;

	//如果a大于10,输出yes
	if(a > 10)
	{
		cout << "yes" << endl;
	}

	return 0;
} 

2.1.2 双分支结构

//条件表达式为true,执行语句1、语句2
//否则执行语句3、语句4
if(条件表达式)	
{
	语句1;
	语句2;
	···
}
else
{
	语句3;
	语句4;
}

功能:如果条件表达式为真(true),执行语句1、语句2,条件表达式为假(false),执行语句3、语句4
示例代码:

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

int main() 
{ 
	int a = 5;

	//如果a大于10,输出yes,否则输出no
	if(a > 10)
	{
		cout << "yes" << endl;
	}
	else 
	{
		cout << "no" << endl;
	}

	return 0;
}  

2.1.3 多分支结构

if(条件表达1)
{
	语句1;
	语句2;
	···
}
else if(条件表达式2)
{
	语句3;
	语句4;
}
else if(条件表达式3)
{
	语句5;
	语句6;
}
else
{
	语句7;
	语句8;
}

功能:
若条件表达式1为真(true),则执行语句1,语句2。
在条件表达式1为假(false)的情况下,若条件表达式2为真(true),则执行语句3,语句4。
在条件表达式1和2都为假(false)的情况下,若条件表达式3为真(true),则执行语句5,语句6。
在条件表达式1和2和3都为假(false)的情况下,则执行语句7,语句8。

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

int main() 
{ 
	int a = 25;

	if(a < 10)	//a<10
	{
		cout << "a" << endl;
	}
	else if(a > 20) //a>20
	{
		cout << "b" << endl;
	}
	else	//10<=a && a<=20
	{
		cout << "c" << endl;
	}

	return 0;
}  

2.1.4 例题

PJ31 判断数正负

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

int main() 
{ 
	int n;

	cin >> n;
	if(n > 0)
	{
		cout << "positive" << endl;
	}
	else if(n < 0) 
	{
		cout << "negative" << endl;
	}
	else
	{
		cout << "zero" << endl;
	}

	return 0;
}

循环结构

for 循环

格式:

for(控制变量初始化;条件表达式;增量表达式)
{
	语句1;
	语句2;
	...
}

说明:
花括号中的内容为循环体,如果循环体中只有一条语句,花括号可以省略
执行过程:
1、执行“控制变量初始化”,使控制变量获得一个初值
2、判断是否满足“条件表达式”,若满足则执行一遍循环体,然后执行第3步;否则结束循环
3、执行增量表达式,计算出控制变量所得到的新值
4、自动跳转到第2步
5、若条件表达式一直为真,则陷入死循环
格式举例:

for(int i=1; i<=100; i++)	//控制变量从1变到100,增量为1
for(int i=1; i<=100; i+=5)	//控制变量从1变到100,增量为5
for(int i=100; i>=1; i--)	//控制变量从100变到1,增量为-1
for(int i=100; i>=1; i-=5)	//控制变量从100变到1,增量为-5

例题: PJ52 求平均年龄

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

int main() 
{ 
	int n, s = 0, x;
	double a;
	
	cin >> n;
	for(int i=1; i<=n; i++)
	{
		cin >> x;
		s += x;
	}
	a = (double)s / n;
	cout << fixed << setprecision(2) << a << endl;

	return 0;
} 

while 循环

格式:

while(条件表达式)
{
	语句1;
	语句2;
	...
}

说明:
花括号内为循环体
执行过程:
1、判断“条件表达式”是否为真,若为真则执行循环体,否则结束循环
2、重复步骤1,直到条件表达式为假
3、若条件表达式一直为真,则陷入死循环
格式举例:

//输出1~10
int i = 1;
while(i <= 10)
{
	cout << i << endl;
	i++;
}

例题:
\(s=1+2 +3……+n\),当加到第几项时,\(s\)的值会超过\(1000\)

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

int main() 
{
	int n = 0, s = 0;
	while(s <= 1000)
	{
		n++;
		s += n;
	}
	cout << n;	//加到第n项时,s会超过1000

	return 0;
}

do...while 循环

格式:

do
{
	语句1;
	语句2;
	...
}while(条件表达式); 

说明:
花括号内为循环体
执行过程:
1、先执行一遍循环体
2、判断“条件表达式”是否为真,若为真则执行循环体,否则结束循环
2、重复步骤2,直到条件表达式为假
3、若条件表达式一直为真,则陷入死循环

//只输出输出1
int i = 1;
do
{
	cout << i << endl;
	i++;
}while(i > 5);

break、continue、return

1、break:终止当前结构,跳出整个循环,执行该循环后面的代码
示例:

for(int i=1; i<=5; i++)
{
	if(i == 3) //当i=3时,终止整个for循环
	{
		break;
	}
	cout << i << " ";
}
//输出:1 2(i=3时触发break,循环直接结束,不会执行i=3/4/5的迭代)

int i = 1;
while(i <= 5)
{
	if(i == 3) break;
	cout << i << endl;
	i++;
}
//输出1 2(i=3时触发break,循环直接结束)

2、continue:跳过本次循环剩余语句,进入下一次循环
示例:

for(int i=1; i<=5; i++)
{
	if(i == 3)		//当i=3时,跳过本次循环剩余语句,直接进入下一次循环
	{
		continue;
	}
	cout << i << " ";
}
//输出:1 2 4 5(i=3时触发continue,跳过了cout,直接执行i++进入下一次循环)

int i = 1;
while(i <= 5)
{
	if(i == 3) 
	{
		i++;
		continue;
	}
	cout << i << endl;
	i++;
}
//输出:1 2 4 5(i=3时触发continue,跳过了cout)

3、return:用于退出函数,具体作用见“函数”章节
在main中return 0;就是退出主函数,即退出整个程序

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

int main() 
{  
	//该段代码只输出1 2
	//在i=3的时候,执行return 0,程序就结束了
	for(int i=1; i<=5; i++)
	{
		if(i == 3) return 0;
		cout << i << " ";
	}
	cout << "hello world" << endl;

	return 0;
}   

循环嵌套

顾名思义,在循环中嵌套循环
例题:
PJ77 【循环结构】阶乘和

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

int main() 
{ 
	int n = 0;
	long long sum = 0, a = 0; 
	
	cin >> n;
	for(int i=1; i<=n; i++)
	{
		a = 1;
		for(int j=1; j<=i; j++)
		{
			a *= j;
		}
		sum += a;
	}
	cout << sum;

	return 0;
} 

数组

数组是存放相同类型对象的容器,数组中存放的对象没有名字,而是要通过其所在的下标访问。

  • 数组的大小是固定的,不能随便改变数组的长度
  • 一个数组的所有元素在内存中的存储位置是连续的

一维数组

数组的定义

格式: 数据类型 数组名[常量表达式]
例子:

int a[10];		//定义能存储10个整型变量的数组,下标是:0~9
char c[100];	//定义能存储100个字符变量的数组,下标是:0~99
double f[5];	//定义能存储5个双精度实数变量的数组,下标是:0~4

说明:
1、数组名的命名规则与变量名的命名规则一致
2、常量表达式表示数组元素的个数。可以是常量和符号常量,不能是变量
3、数组的长度必须是整数
错误示例:

int n;
cin >> n;
int a[n];	//不能用变量n定义数组

数组的初始化

数组的初始化可以在定义时一并完成,有多种形式
1、顺序指定全部元素的初始值

int a[5] = {1, 2, 3, 4, 5};

2、顺序指定部分元素的初始值

int a[10] = {0, 1, 2, 3, 4};	//该方法仅对数组的前5个元素依次进行初始化,其余值为0

3、对数组元素全部初始化为0,可以简写为:{}或{0}

int a[5] = {};

4、初始化时不指定数组元素的个数(不常用)

int a[] = {1, 2, 3}; 	//该方法会定义一个长度为3的数组,每个元素初始值依次为1,2,3

注意: 初始化时,{}里面的元素个数不能超过数组的大小

数组元素的引用

格式: 数组名[整型表达式]
规定:
1、下标可以是任意值为整型的表达式,该表达式里可以包含变量和函数调用。
2、下标的范围是0到数组的最大长度减1,引用时,下标值应在数组合法的下标值范围内。例如定义数组int a[3],则元素分别为a[0],a[1],a[2]
3、C语言只能逐个引用数组元素,而不能一次引用整个数组
4、数组元素可以像同类型的普通变量那样使用,对其进行赋值和运算的操作,和普通变量完全相同。例如:a[5]=34,实现了给a[5]赋值为34
例如:

a[5]		//引用a数组下标为5的元素
a[i+1]		//先计算表达式i+1的值,再引用相应值作为下标的元素
a[++j]		//先计算表达式++j的值,再引用相应值作为下标的元素

数组的输入和输出

  • C语言规定,对数组的使用只能逐个引用数组元素,不能一次引用整个数组。同样,对数组的输入输出也是一次对每个元素进行的,不可整体输入输出。
  • 数组往往和循环一起结合使用,通常用循环里面的控制变量来作为数组的下标,对数组的每个元素进行引用
    例如:输入\(n\)个整数,并将他们输出
int n, a[20];
cin >> n;
for(int i=0; i<n; i++)
{
	cin >> a[i];
}
for(int i=0; i<n; i++)
{
	cout << a[i] << " ";
}

例题

PJ86 与指定数字相同的数的个数

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

int main() 
{ 
	int n, m, a[110], ans = 0;
	
	cin >> n;
	for(int i=1; i<=n; i++)
	{
		cin >> a[i];
	}
	cin >> m;
	for(int i=1; i<=n; i++)
	{
		if(a[i] == m) ans++;
	}
	cout << ans;

	return 0;
} 

二维数组

形象的来说,一维数组是一条线,二维数组则是有很多条线组成的面。
注意:在内存存储中,一维数组和二维数组的地址都是连续的

二维数组的定义

格式: 数据类型 数组名[常量表达式1][常量表达式2]

int a[10][5];		//定义一个10行5列的存储整型变量的二维数组
double b[20][9];	//定义一个20行9列的存储双精度浮点型变量的二维数组
char c[100][120];	//定义一个100行120列的字符变量二维数组

一维数组和二维数组直观上的区别:
一维数组:
image
二维数组:
image

二维数组的初始化

数组的初始化可以在定义时一并完成,有多种形式:
1、顺序指定全部元素的初始值(每一行数据单独写在一个花括号里,中间以逗号隔开)

int a[3][2] = {{1, 2}, {3, 4}, {5, 6}};

2、顺序指定部分元素的初始值

int x[10][2] = {{0, 1}, {2}, {3, 4}};	//该方法仅对数组对应位置的元素依次进行初始化,其余值为0

3、对数组元素全部初始化为0,可以简写为:{}或{0}

int a[5][3] = {};	//将数组a的15个元素都初始化为0

4、初始化时不指定行数,但必须指定列数

int a[][2] = {{1, 2}, {3, 4}};	//该方法会定义一个2行2列的数组,每个元素的初始值依次为1,2,3,4

注意: 初始化时,{}里面的元素个数不能超过数组的大小

二维数组的引用

格式: 数组名[行下标][列下标]
规定:
1、与一维数组一致
2、定义数组int a[3][3],则元素分别为:
a[0][0], a[0][1], a[0][2]
a[1][0], a[1][1], a[1][2]
a[2][0], a[2][1], a[2][2]
例如:

a[5][10];		//引用下标为5和10的元素
a[i+1][j-1];	//先计算i+1和j-1再引用
a[++j];			//先计算++j,再引用

二维数组的输入和输出

例如: 输入n行m列的整数,并将他们输出

int n, m, a[20][20];

scanf("%d%d", &n, &m);
for(int i=1; i<=n; i++)
{
	for(int j=1; j<=m; j++)
	{
		cin >> a[i][j];
	}
}
for(int i=1; i<=n; i++)
{
	for(int j=1; j<=m; j++)
	{
		cout << a[i][j] << " ";
	}
	cout << endl;
}

例题

PJ95 杨辉三角

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

/*
杨辉三角转化为二维数组可这样直观表示
0 0 0 0 0 0 0
0 1 0 0 0 0 0
0 1 1 0 0 0 0
0 1 2 1 0 0 0
0 1 3 3 1 0 0
0 1 4 6 4 1 0
...

找规律:
①a[1][1]固定为1
②其他位置的值可认为是其正上方的值加其左上方的值
即a[i][j] = a[i-1][j] + a[i-1][j-1]
③杨辉三角中,第i行共有i个数
*/

int main() 
{ 
	int n;
	
	//针对输入不确定个数的值,这样处理
	//cin >> n表示读入一个数,成功返回true,失败返回false
	//while(cin >> n)表示一直读取数据,直到读取失败(即读完)
	while(cin >> n)	
	{
		int a[40][40] = {};
		
		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++)
			{
				cout << a[i][j] << " ";
			}
			cout << endl;
		}
		
		cout << endl;
	}

	return 0;
} 

字符数组

定义

格式:char 数组名[常量表达式]
例如:char sz[100];

初始化

1、用字符初始化

char sz[6] = {'a', 'b', 'c', 'd', 'e'};

从首元素开始赋值,剩余元素默认为空字符(也叫结束符,用'\0'表示,其ASCII码为0)
2、用字符串常量初始化

char sz1[5] = {"abcd"};
char sz2[5] = "abcd";

字符串常量的长度不能超过数组的长度,其中的每个字符依次为数组的每个元素进行初始化,剩余的元素用空字符'\0'补全

输入输出

输入

1、cin语句
格式:cin >> 字符数组名,读入字符串,遇空格、回车结束。

char s[100];
cin >> s;

//输入:abc def
//输出:abc
//因为cin遇到空格即结束

2、scanf语句
格式:
scanf("%s", 字符数组名称),读取一个字符串
scanf("%s%s%s", 字符数组名称1, 字符数组名称2, 字符数组名称3),读取多个字符串

char s1[100], s2[100], s3[100];
scanf("%s%s%s", s1, s2, s3);

//输入 Let us go
//s1为"Let",s2为"us",s3为"go"

3、cin.getline()
格式:
cin.getline(字符数组名,字符个数)
cin.getline(字符数组名,字符个数,结束符)
此函数会一次读取多个字符(包括空格、tab),直到读满 “字符个数”-1 个字符。
若不指定结束符,则默认以回车或EOF作为结束符。
若指定结束符,则以结束符或EOF作为结束符(可以读入回车)。

char s[100];
cin.getline(s, 3);	//读取前2个字符,若输入少于2,则遇到回车或EOF截止

//输入abcde
//s为ab

//输入a
//s为a
char s[100];
cin.getline(s, 3, 'b');	//读取前2个字符,若存在'b'则读到'b'截止,否则读满2个字符或遇到EOF为止

//输入abcde
//s为a

//输入a\nb		\n为回车
//输出a\n

输出

1、cout语句
格式:cout << 字符串名

char s[100];
cout << s;

2、printf语句
格式:printf("%s", 字符数组名)
说明:输出内容遇到结束符'\0'才会结束

char s[100];
printf("%s", s);

3、puts函数
格式:puts(字符数组名);
说明:
①puts函数输出一个字符串和一个换行符
②printf("%s\n", s)和puts(s)是等价的

char s[100];
puts(s);

字符串处理函数

1、strcat(dst, src),将src连接到dst后面

char s1[100] = "hello", s2[100] = "world";
strcat(s1, s2);
cout << s1;		//输出helloworld

2、strncat(dst, src, num),将src的前num个字符连接到dst的后面

char s1[100] = "hello", s2[100] = "world";
strncat(s1, s2, 3);
cout << s1;		//输出hellowor

3、strcpy(dst, src),将src复制到dst

char s1[100] = "hello", s2[100] = "world";
strcpy(s1, s2);
cout << s1;		//输出world

4、strncpy(dst, src, num),将src的前num个字符复制到dst
注意:该函数只单纯复制字符,不会在后面添加终止符

char s1[100] = "hello", s2[100] = "world";
strncpy(s1, s2, 3);
cout << s1;		//输出worlo

5、int strcmp(str1, str2),比较字符串str1和str2的大小
说明:
从两个字符串的第一个字符开始比较其字符的ASCII,如果同一位置字符相同,接着比较后面的字符,直到不同为止
str1<str2:返回负数
str1=str2:返回0
str1>str2:返回正数

int a = 0, b = 0, c = 0;

a = strcmp("abc", "def");
b = strcmp("abc", "abc");
c = strcmp("abc", "ab");

cout << a << endl;		//a<0
cout << b << endl;		//b=0
cout << c << endl;		//c>0

6、int strncmp(str1, str2, num),比较字符串str1和str2前num个字符的大小

int a = 0, b = 0, c = 0;
char s1[100] = "abc", s2[100] = "abcdef";

a = strncmp(s1, s2, 5);
b = strncmp(s1, s2, 3);
c = strncmp("abcdef", "abc", 5);

cout << a << endl;		//a<0
cout << b << endl;		//b=0
cout << c << endl;		//c>0

7、strlen(str),返回字符数组的长度,不包括终止符

char s[100] = "abc";
int len = 0;

len = strlen(s);
cout << len;	//len=3

8、char *strstr(str1, str2),判断str2是否是str1的子串
说明:
①如果str2是str1的一个子串,返回一个指向str2在str1中首次出现的位置
②如果str2不是str1的一个子串,返回空指针NULL

char s[100] = "abcdef"; 

if(strstr(s, "cd") != NULL)
{
	cout << "yes";
}
else 
{
	cout << "no";
}
char s[100] = "abcdef";
int pos = 0;

char *p = strstr(s, "cd");
if(p != NULL)
{
	pos = p - s;
	cout << pos;	//输出2,即cd在abcdef中首次出现的位置,下标从0开始
}

例题

ybt1138 将字符串中的小写字母转换成大写字母

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

char s[110];

int main()
{ 
	int len = 0;

	cin.getline(s, 110);	//输入字符串包含空格 
	
	len = strlen(s);
	for(int i=0; i<len; i++)
	{
		if(s[i] >= 'a' && s[i] <= 'z')	//小写字母 
		{
			printf("%c", s[i] - 'a' + 'A');	//转大写 
		}
		else 
		{
			printf("%c", s[i]);
		}
	}
 
	return 0;
} 

函数

函数的概念与意义

\(c++\)中,函数 是一段封装了特定功能的可重用的代码块,通过函数名被调用。
核心价值:

  • 代码复用:避免重复编写相同逻辑,降低冗余
  • 模块化:将复杂问题拆解为多个小功能,便于维护和调试
  • 可读性:通过函数名直观理解代码功能,提升程序清晰度
    例如: 计算两个数的和可以封装为\(add\)函数,后续无需重复编写加法逻辑,直接调用即可。

函数的定义与声明

函数的定义:实现功能的具体代码

函数定义的完整结构如下:

返回值类型 函数名(参数列表)
{
	//函数体:实现功能的语句
	return 返回值;		//若返回值类型为void,可省略return
}

说明:

  • 返回值类型:函数执行后返回的数据类型(如:int/double/void等)。void表示无返回值
  • 函数名:遵循标识符命名规则(字母、数字、下划线,首字符非数字)
  • 参数列表:函数接收的输入数据,格式为"类型 参数名",多个参数用逗号分隔(无参数时写())
  • 函数体:用{}包裹的语句块,实现具体功能
  • return 语句:将结果返回给调用者,若返回值类型为void,可省略(或仅写return;),只要执行到return,则代表该函数结束
    示例:
//1、无参数、无返回值(void)
void p()
{
	printf("hello world");
}

//2、有参数、有返回值(int)
int myadd(int a, int b)
{
	int s = a + b;
	return s;
}

//3、多参数、返回值为double
double ave(double a, double b)
{
	return (a + b) / 2;
}

函数声明:告诉编译器函数的存在

函数定义在调用位置,之前:不用声明;之后:需要声明
格式: 仅保留函数结构,省略函数体,以分号结尾
即:返回值类型 函数名(参数列表);
示例:

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

//函数声明(告知编译器:存在一个add函数)
int myadd(int a, int b);
  
int main() 
{  
	int x = 1, y = 1, z = 0;
	
	z = myadd(x, y);		//调用add函数
	printf("%d", z);	//输出2
	 
	return 0;
} 

//函数定义
int myadd(int a, int b)
{
	return a + b;
}

说明:

  • 函数声明时参数名可省略(仅需类型),如int add(int, int);,但保留参数名更容易理解
  • 声明需与定义的“返回值类型、函数名、参数列表”完全一致(否则编译错误)

函数的调用与参数传递

函数调用是执行函数功能的过程,需传递实际参数(实参)给形式参数(形参)

函数调用的格式

格式:

  • 函数名(实参列表); //无返回值时
  • 返回值类型 变量 = 函数名(实参列表) //有返回值时
    示例:
#include <bits/stdc++.h>
using namespace std;

void p()
{
	printf("hello world");
}

int myadd(int a, int b)
{
	int s = a + b;
	return s;
}
 
int main() 
{  
	int x = 1, y = 1, z = 0;
	
	z = myadd(x, y);	//调用有参函数,接收返回值
	printf("%d\n", z);

	p();	//调用无参函数
	 
	return 0;
}  

实参与形参的关系

  • 形参:函数定义时的参数(如add(int a, int b)中的a.b),仅在函数体内有效(局部变量)
  • 实参:调用函数时传递的具体值(如add(x, y)中的x.y),可以是常量、变量或表达式
  • 传递规则:实参与形参的类型、个数、顺序必须一一对应,否则编译错误

参数传递方式:值传递(默认)

c++默认采用值传递:将实参的“值副本”传递给形参,函数内修改形参不会影响实参。
示例: 值传递的特性

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

void myswap(int x, int y)
{
	int t = 0;
	t = x;
	x = y;
	y = t;
	printf("x=%d, y=%d\n", x, y);
}
 
int main() 
{  
	int a = 3, b = 5;
	 
	myswap(a, b);
	printf("a=%d, b=%d\n", a, b);
	 
	return 0;
} 

结论: 值传递中,形参与实参是独立变量,修改形参不影响实参

局部变量与全局变量

局部变量

定义位置: 在函数内部、代码块(如\(if/for/while\)\({}\)内)或函数参数列表中定义的变量。
作用域: 仅在定义他的函数或代码块内部有效,出了这个范围就无法访问。
生命周期: 从进入作用域(如函数被调用、代码块开始执行)时创建,离开作用域(如函数执行结束、代码块执行完毕)时自动销毁。
初始化: 若未手动初始化,局部变量的值是未定义的(即随机值),使用时可能导致程序异常。

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

void test()
{
	int a = 10;	//局部变量(函数内部)
	if(a > 5)
	{
		int b = 20;	//局部变量(if代码块内部)
		printf("b = %d\n", b);	//有效:在if块内
	}
	//printf("b = %d\n", b);	//错误:出了if块,b已销毁
	printf("a = %d\n", a);	//有效:在test函数内
}

int main()
{ 
	test();
	//printf("a = %d\n", a);	//错误:a是test函数的局部变量,main中不可访问

	return 0;
} 

全局变量

定义位置: 在所有函数和代码块之外(通常在文件顶部)定义的变量。
作用域: 从定义位置开始,整个程序文件内有效。
生命周期: 从程序启动(main函数执行前)创建,直到程序结束(main函数执行完毕)才销毁。
初始化: 若未手动初始化,会被编译器默认初始化为0.

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

int a = 100;	//全局变量(函数外部)

void func1()
{
	printf("func1: a=%d\n", a);	//有效
}

void func2()
{
	a = 200;	//修改全部变量
	printf("func2: a=%d\n", a);
}

int main()
{ 
	func1();	//输出:100
	func2();	//输出:200
	printf("main: a=%d\n", a);	//输出:200(已被func2修改)

	return 0;
} 

屏蔽规则

当局部变量与全局变量同名时,局部变量会屏蔽全局变量(优先访问局部变量)
ps:自己写代码时应避免同名

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

int a = 10;	//全局变量

int main()
{ 
	a = 20;	//局部变量(与全局变量同名)
	printf("a = %d\n", a);	//输出20(局部变量屏蔽全局变量)

	return 0;
} 

例题

PJ132 求阶乘和

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

long long jc(int k)
{
	long long ret = 1;
	for(int i=1; i<=k; i++)
	{
		ret *= i;
	}
	return ret;
}
 
int main() 
{  
	int n = 0;
	long long ans = 0;
	
	scanf("%d", &n);
	for(int i=1; i<=n; i++)
	{
		ans += jc(i);
	}
	printf("%lld", ans);

	return 0;
}  

递归

定义:
在数学和计算机科学中是指在函数的定义中使用函数自身的方法,在计算机科学中还额外指一种通过重复将问题分解为同类的子问题而解决问题的方法。
简而言之:函数直接或间接的调用自身,称为递归。适用于解决“可拆解为同类子问题”的场景。
递归的两个必要条件:
1、终止条件: 当满足某条件时停止递归(避免无限循环)
2、递归关系: 将原问题拆解为规模更小的子问题
递归的调用过程:
image
1、当参数为\(n\)时,调用一次函数,占用一段内存,然后执行参数\(n-1\),再调用一次函数,占用一段内存,直到参数到达结束条件。
2、当最后一次函数执行完毕后,将所需要的结果返回上一层函数,直到返回\(n\)时,得到最终结果,结束递归。
为什么要写递归:
1、结构清晰,可读性强
2、练习分析问题的结构。当发现问题可以被分解成相同结构的小问题时,递归写多了就能敏锐发现这个特点,进而高效解决问题
缺点:
在程序执行中,递归是利用堆栈来实现的。每当进入一个函数调用,栈就会增加一层栈帧,每次函数返回,栈就会减少一层栈帧。
而栈不是无限大的,当递归层数过多时,就会造成栈溢出的后果。
写递归的要点:
明白一个函数的作用并相信它能完成这个任务,千万不要跳进这个函数里面企图探究更多细节。
否则就会陷入无穷的细节无法自拔,人脑能压几个栈啊。
例题:
PJ189 【递归】求f(x,n)

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

int n = 0;
double x = 0;

double f(double x, int n)
{
	if(n == 1) return sqrt(1+x);
	return sqrt(n+f(x, n-1));
}

int main()
{
	scanf("%lf%d", &x, &n);
	printf("%.2lf\n", f(x, n));
	
	return 0;
} 

结构体

概念

概念: 结构体是一种用户自定义的数据类型,用于将多个不同类型的数据(成员)组合成一个整体,以实现一个“实体”的属性。
例如:
描述一个“学生”,需要包含姓名(字符数组)、年龄(整数)、成绩(浮点数)等信息。
核心价值:
将关联的数据“打包”,使代码更符合现实世界的实体描述,提升可读性和维护性。

定义与初始化

结构体的定义

格式:

struct 结构体名
{
	成员类型1 成员名1;	//成员可以是基本类型、数组、指针、其他结构体等
	成员类型2 成员名2;
	//... 更多成员
};	//注意末尾的分号

说明:

  • 结构体名:遵循标识符命名规则
  • 成员:结构体包含的“属性”,每个成员有自己的类型和名称
  • 结构体定义本身不分配内存,仅声明一种新的数据类型
    示例:
struct Student
{
	int id;			//学号(整数)
	char name[20];	//姓名(字符数组)
	double score;	//成绩(浮点数)
};

结构体变量的定义与初始化

定义

1、先定义结构体,再定义变量

struct Student
{
	int id;			//学号(整数)
	char name[20];	//姓名(字符数组)
	double score;	//成绩(浮点数)
};
Student stu1, stu2;	//定义两个结构体变量

2、定义结构体的同时定义变量

struct Student
{
	int id;			//学号(整数)
	char name[20];	//姓名(字符数组)
	double score;	//成绩(浮点数)
}stu1, stu2; //定义两个结构体变量 

初始化

定义时初始化

struct Student
{
	int id;			//学号(整数)
	char name[20];	//姓名(字符数组)
	double score;	//成绩(浮点数)
};
Student stu1 = {1, "zhangsan", 80.5};	//依次为成员赋值
Student stu2 = {};	//初始化所有成员为0

成员访问

用点运算符(.)

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

struct Student
{
	int id;			//学号(整数)
	char name[20];	//姓名(字符数组)
	double score;	//成绩(浮点数)
};
Student stu1 = {1, "zhangsan", 80.5};	//依次为成员赋值 
Student stu2 = {};	//初始化所有成员为0
 
int main() 
{  
	//读取值
	printf("%d\n%s\n%lf\n", stu1.id, stu1.name, stu1.score);
	
	//赋值
	stu2.id = 2;
	strcpy(stu2.name, "lisi");
	stu2.score = 100;
	printf("%d\n%s\n%lf\n", stu2.id, stu2.name, stu2.score);
	
	//相同类型的变量可以直接赋值 
	stu2 = stu1;
	printf("%d\n%s\n%lf\n", stu2.id, stu2.name, stu2.score);

	return 0;
}  

内存分配机制

注意: 定义一个结构体类型时不分配内存,只有定义结构体变量时才分配内存
原则: 结构体的总大小为其最宽基本类型成员大小的整数倍。

struct Student
{
	char a; 
	int b;
	char c;
}stu;
//stu在内存中占12个字节
//int b占4个字节,所以char a后面会自动补3个字节,char c后面也会自动补3个字节
struct Student
{
	char a; 
	char c;
	int b; 
}stu;
//stu在内存中占8个字节
//int b占4个字节,char a和char c各占一个字节,后面自动补充2个字节

提示:
在竞赛阶段,为了节约内存,在组织数据结构的数据成员的时候,可以将相同类型的成员放在一起,这样就减少了编译器为了对齐而添加的填充字符。
补充:
sizeof 是c++中的一个运算符(非函数),用于计算变量、数据类型在内存中所占的字节数
示例:

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

struct Student
{
	char a; 
	int b;
	char c;
}stu;
int s[10];
 
int main() 
{  
	int x = sizeof(stu);	//计算stu所占内存
	int y = sizeof(int);	//计算int类型所占内存
	int z = sizeof(s);		//计算数组s所占内存
	
	//x=12,y=4,z=40
	printf("x=%d,y=%d,z=%d\n", x, y, z);

	return 0;
}  

运算符重载

结构体是构造类型,无法直接进行逻辑运算、四则运算等,但我们可以对相应的运算符进行重载。
1、作为成员变量重载
格式:

bool operator 运算符(const 结构体类型名 &参数) const
{
	//条件表达式
}

说明:
1、operator为关键字
2、第一个const表示形参为只读
3、第二个const表示不允许修改成员变量,固定格式
示例:

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

struct Student
{
	int id;			//学号(整数)
	char name[20];	//姓名(字符数组)
	double score;	//成绩(浮点数)

	//重载运算符<
	bool operator < (const Student &s) const
	{
		//id表示本身的成员变量
		//s.id表示参数的成员变量
		return id < s.id;
	}
}stu1, stu2;

 
int main() 
{  
	stu1.id = 1;
	strcpy(stu1.name, "zhangsan");
	stu1.score = 100;
	
	stu2.id = 2;
	strcpy(stu2.name, "lisi");
	stu2.score = 80;
	
	//结果输出yes
	if(stu1 < stu2) printf("yes");
	else printf("no");

	return 0;
}  

2、全局重载
格式:

bool operator 运算符 (const 结构体类型名 &参数1, const 结构体类型名 &参数2)
{
	//条件表达式
}

示例:

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

struct Student
{
	int id;			//学号(整数)
	char name[20];	//姓名(字符数组)
	double score;	//成绩(浮点数) 
}stu1, stu2;

bool operator < (const Student &a, const Student &b)
{
	return a.id < b.id;
}
 
int main() 
{  
	stu1.id = 1;
	strcpy(stu1.name, "zhangsan");
	stu1.score = 100;
	
	stu2.id = 2;
	strcpy(stu2.name, "lisi");
	stu2.score = 80; 
	
	//结果输出yes
	if(stu1 < stu2) printf("yes");
	else printf("no");

	return 0;
}  

标准模板库(STL)

C++标准模板库(Standard Template Library,简称 STL)是\(C++\)标准库的核心组成部分,它提供了一套通用的、可复用的模板类和函数。

sort函数

STL提供的\(sort\)函数是基于模板实现的高效排序工具,支持多种类型,且可自定义排序规则。
格式:
\(sort\)(起始位置,起始位置+长度)
\(sort\)(起始位置,起始位置+长度,自定义比较规则)
说明:
①自定义比较规则,可省略,如果省略,默认按升序排列
\(sort\)默认通过元素的\(<\)(小于号)运算符比较大小,支持任意可比较的数据类型(如:\(int\)\(double\)等)

默认升序排序

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

int main() 
{   
	//对3, 2, 9, 1, 5进行升序输出
	int a[10] = {0, 3, 2, 9, 1, 5};
	
	//a+1为起始位置,a+1+5为起始位置+长度
	//没有自定义比较规则,所以默认升序
	sort(a+1, a+1+5);
	//输出结果为:1 2 3 5 9
	for(int i=1; i<=5; i++)
	{
		printf("%d ", a[i]);
	}

	return 0;
} 

自定义排序规则

需要自定义自定义比较规则(\(cmp\)),\(cmp\)函数返回值为\(bool\),表示“是否让第一个参数排在第二个参数前面”
1、对整数数组排序

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

bool cmp(int x, int y)
{
	return x > y;
}

int main() 
{   
	//对3, 2, 9, 1, 5进行降序输出
	int a[10] = {0, 3, 2, 9, 1, 5};
	 
	//cmp为自定义比较规则
	sort(a+1, a+1+5, cmp);
	//输出结果为:9 5 3 2 1
	for(int i=1; i<=5; i++)
	{
		printf("%d ", a[i]);
	}

	return 0;
}   

2、对结构体排序
2.1 对结构体重载\(<\)(小于号)运算符

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

struct node 
{
	int id;		//学号
	int score;	//分数
	
	bool operator < (const node &nd) const
	{
		return id < nd.id;
	}
}a[10];

int main() 
{   
	a[1].id = 1000;
	a[1].score = 90;
	a[2].id = 3239;
	a[2].score = 88;
	a[3].id = 2390;
	a[3].score = 95;
	a[4].id = 7231;
	a[4].score = 84;
	a[5].id = 1005;
	a[5].score = 95;
	
	sort(a+1, a+1+5);
	for(int i=1; i<=5; i++)
	{
		printf("%d %d\n", a[i].id, a[i].score);
	}
	//输出结果:
	//1005 95
	//2390 95
	//1000 90
	//3239 88
	//7231 84

	return 0;
}    

2.2 自定义自定义比较规则(\(cmp\)

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

struct node 
{
	int id;		//学号
	int score;	//分数
}a[10];

//自定义规则,按分数从高到低
//若分数相同,则按学号从低到高
bool cmp(node nd1, node nd2)
{
	if(nd1.score == nd2.score) return nd1.id < nd2.id;
	return nd1.score > nd2.score;
}

int main() 
{   
	a[1].id = 1000;
	a[1].score = 90;
	a[2].id = 3239;
	a[2].score = 88;
	a[3].id = 2390;
	a[3].score = 95;
	a[4].id = 7231;
	a[4].score = 84;
	a[5].id = 1005;
	a[5].score = 95;
	
	sort(a+1, a+1+5, cmp);
	for(int i=1; i<=5; i++)
	{
		printf("%d %d\n", a[i].id, a[i].score);
	}
	//输出结果:
	//1005 95
	//2390 95
	//1000 90
	//3239 88
	//7231 84

	return 0;
}   

string字符串

\(string\)\(c++\)标准库提供的字符串类,用于便捷的管理和操作字符串。

string的定义与初始化

string s1 = "hello";	//将s1初始化为hello
string s2("hello");		//将s2初始化为hello
//string s3(n, c);		//将s3初始化为n个字符c
string s3(5, 'a');		//s3的值为aaaaa

string的输入与输出

\(string\)的输入与输出需要使用\(cin\)\(cout\),不能用\(scanf\)\(printf\)

输入

1、cin
会忽略开头的制表符、换行符、空格,当再次碰到空白字符就停止(并不会读取空字符)

string s1;

cin >> s1;
cout << s1;
//输入hello world,输出hello,因为读入时,碰到中间的空格,就停止了

2、getline()
格式:getline(cin, s); //cin指的是读入流,一般情况下直接写cin即可,s是存储字符串的变量
getline会读入整个句子(连空格一起读入)

string s1;

//输入hello world,输出hello world
//因为连中间空格一起读入了
getline(cin, s1);
cout << s1;

输出

直接用\(cout\)输出即可

string常用操作

操作 含义
s.empty() 如果s为空串,则返回 true,否则返回 false
s.size() 返回s中字符的个数,与s.length()用法一致
s[i] s中下标为i的字符,下标从 0 开始计数(可对其进行读写操作)
s1 = s2 把 s1 内容替换为 s2 的副本,即赋值
s3 = s1 + s2 把 s2 连到 s1 之后生成一个新字符串,赋值给 s3
s1==s2 比较 s1 与 s2 的内容,相等则返回 true,否则返回 false
!=, <, >, <=, >= 按照字典序比较两个字符串,例如: if (s1 > s2)
s.insert(pos,s2) 在 s 下标为 pos 的元素插入 string 类型 s2
s.substr(pos, len) 返回一个 string 类型,它包含 s 中下标为 pos 起的连续 len 个字符构成的子串
s.erase(pos, len) 删除s中下标为 pos 开始的 len 个字符
s.replace(pos, len, s2) 将 s 中下标为 pos 开始的连续 len 个字符替换为 s2
s.find(s2, pos) 返回在 s 中以 pos 下标起查找 s2 第一次出现的位置,如果未找到,返回 string::npos(也可以看成是有符号整型的 -1)
s.c_str() 返回一个与 s 字面值相同的 C 语言风格的字符串(可以先简单理解为字符数组)
示例:
#include <bits/stdc++.h>
using namespace std;
 
int main() 
{   
	string s1;
	
	s1 = "abcde";
	printf("%d\n", s1.size());	//输出5
	
	s1[2] = 'f';
	cout << s1 << endl;	//输出abfde
	
	s1 = "abcde";
	s1.insert(1, "xyz");
	cout << s1 << endl;	//输出axyzbcde
	
	s1 = "abcde";
	string s2 = s1.substr(1, 2);
	cout << s2 << endl;	//输出bc
	
	s1 = "abcde";
	s1.erase(1, 2);
	cout << s1 << endl;	//输出ade
	
	s1 = "abcde";
	s1.replace(1, 3, "xy");
	cout << s1 << endl;	//输出axye

	s1 = "abcde";
	int x = s1.find("cd", 0);
	cout << x << endl; 	//输出2
	x = s1.find("ab", 3);
	cout << x << endl;	//输出-1

	return 0;
}   

vector容器

定义与本质

  • Vector是C++ STL中的动态数组,支持元素的动态扩容和随机访问
  • 底层通过连续内存存储元素,类似数组,但可以自动管理内存

初始化

vector<int> a;			//空的容器
vector<int> b(5);		//长度为5,默认值为0
vector<int> c(5, 3);	//长度为5,初始值3

常用操作

常用操作

函数 功能描述 时间复杂度
[] 通过下标访问元素,a[i] O(1)
front() 访问首元素 O(1)
back() 访问尾元素 O(1)
push_back(x) 在尾部添加元素 O(1) 均摊
pop_back() 删除尾部元素 O(1)
clear() 清空所有元素 O(n)
empty() 返回一个\(bool\)值,判断是否为空 O(1)
size() 返回当前元素个数 O(1)
capacity() 返回容器的容量,即当前已为多少个元素分配了空间 O(1)
reserve(n) 预分配至少\(n\)个元素的空间,避免频繁扩容 O(n)
resize(n) 调整大小为\(n\),新增元素默认初始化 O(n)
#include <bits/stdc++.h>
using namespace std;
   
int main() 
{    
	vector<char> v;
	
	printf("%d\n", v.size());	//输出0
	
	v.push_back('a');
	v.push_back('b');
	v.push_back('c');
	v.push_back('d');
	v.push_back('e');
	
	printf("%c %c\n", v.front(), v.back());	//输出a e
	
	printf("%d\n", v.size());	//输出5
	
	//遍历容器元素
	for(int i=0; i<v.size(); i++)	
	{
		printf("%c ", v[i]);
	}
	printf("\n");

	v.resize(10);
	printf("%d\n", v.size()); 	//输出10
 	 
 	//遍历容器元素
	for(char c : v)
	{
		printf("%c ", c);
	}
 
	return 0;
}    

pair

定义

  • \(pair\)用于存储两个不同或相同类型的数据
  • 本质是将两个变量“打包”成一个整体,方便作为单个变量传递

声明与初始化

1、格式:\(pair\)<类型1,类型2> 变量名
2、初始化:

初始化方式 代码示例 说明
默认初始化 pair<int, string> p; first和second为对应类型的默认值(int为0,string为空)
构造函数初始化 pair<int, string> p(101, "Alice"); 直接传入两个值,顺序对应first、second
make_pair 函数 pair<int, string> p; p = make_pair(101, "Alice"); 先定义,再赋值
列表初始化(C++11+) pair<int, string> p = {101, "Alice"}; 简洁直观,奥赛中最常用
//构造函数初始化
pair<int, string> p1(101, "Alice");

//make_pair初始化
pair<int, string> p2;
p2 = make_pair(102, "Bob");

//列表初始化
pair<int, string> p3 = {103, "charlie"};

赋值操作与成员访问

整体赋值: 两个\(pair\)类型完全匹配时,可直接用=赋值
成员单独赋值: 通过\(first\)\(second\)直接修改单个成员

  • first: 访问第一个元素(对应模板的“类型1”)
  • second: 访问第二个元素(对应模板的“类型2”)
#include <bits/stdc++.h>
using namespace std;
   
int main() 
{    
	pair<int, string> p1(101, "Alice");
	pair<int, string> p2;
	
	p2 = p1;
	cout << p2.first << endl;
	cout << p2.second << endl;
 
 	p2.first = 102;
 	p2.second = "Bob";
 	cout << p2.first << endl;
	cout << p2.second << endl;
 	 
	return 0;
} 

赋值操作与成员访问

\(pair\)支持比较运算和交换操作,这是其在排序、容器中应用的核心基础

比较运算

\(pair\)重载了\(<、<=、>、>=、==、!=\)运算符,默认比较规则为“先比first,再比second”,即:优先按第一关键字排序,再按第二关键字排序

#include <bits/stdc++.h>
using namespace std;
   
int main() 
{    
    pair<int, int> p1 = {2, 3};
    pair<int, int> p2 = {2, 5};
    pair<int, int> p3 = {1, 10};
    
    // 比较结果
    cout << (p1 < p2) << endl;  // 1(true:first相同,p1.second=3 < p2.second=5)
    cout << (p1 > p3) << endl;  // 1(true:p1.first=2 > p3.first=1)
    cout << (p2 == p3) << endl; // 0(false:first和second均不同)
 	 
	return 0;
}

交换操作

使用\(swap()\)函数交换两个类型完全相同的\(pair\)的所有成员(first和second同时交换)

#include <bits/stdc++.h>
using namespace std;
   
int main() 
{    
	pair<int, string> p1 = {101, "Alice"};
	pair<int, string> p2 = {102, "Bob"};
	
	p1.swap(p2);	//交换p1和p2
	cout << p1.first << " " << p1.second << endl;	//102 Bob
	cout << p2.first << " " << p2.second << endl;	//101 Alice
 	 
	return 0;
}   

高精度

高精度计算,也被称作大整数计算,运用了一些算法结构来支持更大整数间的运算(数字大小超过语言内建整型(\(int\) ,\(long long\)))。

反转存储

在平常的实现中,高精度数字利用字符串表示,每一个字符表示数字的一个十进制位。因此,可以说,高精度数值计算实际上是一种特别的字符串处理。
读入字符串时,数字最高位在字符串首(下标小的位置)。但习惯上,下标最小的位置存放的是数字的最低位,即存储反转的字符串。
这么做的原因在于,数字的长度可能发生变化,但我们希望同样权值位始终保持对齐(例如,希望所有的个位都在下标[1],所有的十位都在下标[2]...)。
同时,加、减、乘的运算一般都从个位开始进行(回想小学的竖式运算)。

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

const int maxn = 3000;

char s1[maxn] = {}, a[maxn] = {};

int main()
{  
	int len = 0;
	scanf("%s", s1);
	
	//将字符串s1反转存储到a数组
	len = strlen(s1);
	a[0] = len;	//a[0]存储整个字符串的长度
	for(int i=1; i<=len; i++)	
	{
		a[i] = s1[len - i];	//反转存储
	}
	
	//将反转存储的数组进行输出
	for(int i=a[0]; i>=1; i--)
	{
		printf("%c", a[i]);
	}

	return 0;
} 

高精加法

高精度加法,其实就是竖式加法。
image

也就是从最低位开始,将两个加数对应位置上的数码相加,并判断是否达到或超过10。
如果达到,那么处理进位:
将更高一位的结果上增加1,当前位的结果减少10。

//高精+高精
void GaddG(char s1[], char s2[], int c[])
{
	int a[maxn] = {}, b[maxn] = {};
	int len1 = strlen(s1);
	int len2 = strlen(s2);
	
	//反转存储
	for(int i=1; i<=len1; i++) a[i] = s1[len1-i] - '0';
	for(int i=1; i<=len2; i++) b[i] = s2[len2-i] - '0';
	
	int len3 = max(len1, len2);
	for(int i=1; i<=len3; i++)
	{
		c[i] += a[i] + b[i];
		c[i+1] = c[i] / 10;	//进位
		c[i] %= 10;	//余数即为当前位
	}
	if(c[len3+1] > 0) len3++;
	c[0] = len3;
}

高精减法

高精度减法,也就是竖式减法
image

从个位起逐位相减,遇到负的情况则向上一位借1。整体思路与加法完全一致。

//高精 - 高精
void GjianG(char s1[], char s2[], int c[])
{
	int a[maxn] = {}, b[maxn] = {};
	int len1 = strlen(s1);
	int len2 = strlen(s2);
	
	//首先判断结果的正负
	if(len1<len2 || (len1==len2 && strcmp(s1, s2)<0))
	{
		printf("-");
		swap(s1, s2);
	}
	
	//反转存储
	len1 = strlen(s1);
	len2 = strlen(s2);
	for(int i=1; i<=len1; i++) a[i] = s1[len1-i] - '0';
	for(int i=1; i<=len2; i++) b[i] = s2[len2-i] - '0';
	
	int len3 = max(len1, len2);
	for(int i=1; i<=len3; i++)
	{
		c[i] += a[i] - b[i];
		if(c[i] < 0)
		{
			c[i+1]--;	//向高位借1
			c[i] += 10;	//当前为加10
		}
	}
	//删除前导0,注意这里至少要保留1位
	//当结果为0时,也要输出0
	while(len3>1 && c[len3]==0) len3--;	 
	c[0] = len3;
}

高精乘法

高精度乘法,也是竖式乘法

              a₄  a₃    a₂    a₁
      *           b₃    b₂    b₁
    —————————————————————————————
            C₄'   C₃'   C₂'   C₁'
      C₅''  C₄''  C₃''  C₂''
C₆''' C₅''' C₄''' C₃'''
—————————————————————————————————
C₆    C₅    C₄    C₃    C₂     C₁

\(c_i\) = \(c_i\)' + \(c_i\)'' + \(c_i\)'''
\(c_3\)为例:
\(c_3\) = \(c_3\)' + \(c_3\)'' + \(c_3\)'''
\(c_3\)' = \(a_3\) * \(b_1\)
\(c_3\)'' = \(a_2\) * \(b_2\)
\(c_3\)''' = \(a_1\) * \(b_3\)
观察规律可知:\(a_i\) * \(b_j\)的值都累加到了\(c_{i+j-1}\),进位到了\(c_{i+j}\)

//高精乘高精
void GchengG(char s1[], char s2[], int c[])
{
	int a[maxn] = {}, b[maxn] = {};
	int len1 = strlen(s1);
	int len2 = strlen(s2);
	
	//反转存储
	for(int i=1; i<=len1; i++) a[i] = s1[len1-i] - '0';
	for(int i=1; i<=len2; i++) b[i] = s2[len2-i] - '0';
	
	for(int i=1; i<=len1; i++)
	{
		for(int j=1; j<=len2; j++)
		{
			c[i+j-1] += a[i] * b[j];	//a[i] * b[j]的值累加到c[i+j-1]
			c[i+j] += c[i+j-1] / 10;	//向c[i+j]进位
			c[i+j-1] %= 10;
		}
	}
	
	//删除前导0
	int len3 = len1 + len2;
	while(len3>1 && c[len3]==0) len3--;
	c[0] = len3;
}

高精除法

高精除低精

竖式除法
image

除法运算不要反转存储,因为除法运算是从高位开始除。
从最高位开始除,相除后的结果保存在该位,余数*10后,累加到下一位再继续除

//高精除低精
//该代码只能处理可以整除的情况
void GchuD(char s1[], int b, int c[])
{
	int a[maxn] = {};
	int t[maxn] = {};
	int len1 = strlen(s1);
	//正向存储
	for(int i=1; i<=len1; i++) a[i] = s1[i-1] - '0';
	
	//从高位开始除
	int x = 0;
	for(int i=1; i<=len1; i++)
	{
		t[i] = (x*10+a[i]) / b;
		x = (x*10+a[i]) % b;
	}
	
	//前导可能为0,st记录第一个非0的位置
	int st = 1;	
	while(t[st]==0 && st<len1) st++;
	//将结果保存到c数组
	c[0] = 0;
	for(int i=st; i<=len1; i++) c[++c[0]] = t[i];
}

高精除高精

高精除高精依然使用竖式长除法
image

竖式长除法实际上可以看作一个逐次减法的过程。
例如上图中商数十位的计算可以这样理解:将\(45\)减去三次\(12\)后变得小于\(12\),不能再减,故此位为\(3\)
因此可以把除法转变为多次减法,减完后剩余的数即为余数。

//比较字符串a[k, k+len-1]和b的大小
//即a从下标k开始,长度为len
//a>=b返回true,a<b返回false
bool compare(int a[], int b[], int k, int len)
{
	if(a[k+len] > 0) return true;	//字符串a上面那位还有值,则一定比b大
	for(int i=len-1; i>=0; i--)
	{
		if(a[k+i] > b[i+1]) return true;		//明确a>b
		else if(a[k+i] < b[i+1]) return false;	//明确a<b
	}
	return true;	//每一位都相等,则表示a和b相等
}

//高精除高精
//运算成功返回true,失败返回false
bool GchuG(char s1[], char s2[], int c[], int d[])
{
	int a[maxn] = {}, b[maxn] = {};
	int len1 = strlen(s1);
	int len2 = strlen(s2);
	//除数为0
	if(len2==0 && s2[0]==0) return false;
	
	//反向存储
	for(int i=1; i<=len1; i++) a[i] = s1[len1-i] - '0';
	for(int i=1; i<=len2; i++) b[i] = s2[len2-i] - '0';
	
	//从a[len1-len2+1,len1]开始减
	for(int i=len1-len2+1; i>=1; i--)
	{
		//只要a[i, i+len2-1]大于等于b,就不断地减b
		while(compare(a, b, i, len2))	
		{
			//高精减
			for(int j=0; j<len2; j++)
			{
				a[i+j] -= b[j+1];
				if(a[i+j] < 0)
				{
					a[i+j+1] -= 1;
					a[i+j] += 10;
				}
			}
			c[i] += 1;
		}
	}
	//c保存的是商,此处是计算商的长度
	for(int i=len1-len2+1; i>=1; i--)
	{
		if(c[i] != 0)
		{
			c[0] = i;
			break;
		}
	}
	
	//a中剩下的数字即为余数,复制到r中
	for(int i=len1; i>=1; i--)
	{
		if(a[i] != 0)
		{
			r[i] = a[i];
			if(r[0] == 0) r[0] = i;	//计算余数的长度
		}
	}
	if(r[0]==0) r[0] = 1;
	
	return 0;
} 

四则运算

完整的四则运算代码,高精加、高精减、高精乘、高精除低精、高精除高精

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

const int maxn = 3000;
int ans[maxn] = {}, r[maxn] = {};

//高精 + 高精
void GaddG(char s1[], char s2[], int c[])
{
	int a[maxn] = {}, b[maxn] = {};
	int len1 = strlen(s1);
	int len2 = strlen(s2);
	
	//反转存储
	for(int i=1; i<=len1; i++) a[i] = s1[len1-i] - '0';
	for(int i=1; i<=len2; i++) b[i] = s2[len2-i] - '0';
	
	int len3 = max(len1, len2);
	for(int i=1; i<=len3; i++)
	{
		c[i] += a[i] + b[i];
		c[i+1] = c[i] / 10;	//进位
		c[i] %= 10;	//余数即为当前位
	}
	if(c[len3+1] > 0) len3++;
	c[0] = len3;
}

//高精 - 高精
void GjianG(char s1[], char s2[], int c[])
{
	int a[maxn] = {}, b[maxn] = {};
	int len1 = strlen(s1);
	int len2 = strlen(s2);
	
	//首先判断结果的正负
	if(len1<len2 || (len1==len2 && strcmp(s1, s2)<0))
	{
		printf("-");
		swap(s1, s2);
	}
	
	//反转存储
	len1 = strlen(s1);
	len2 = strlen(s2);
	for(int i=1; i<=len1; i++) a[i] = s1[len1-i] - '0';
	for(int i=1; i<=len2; i++) b[i] = s2[len2-i] - '0';
	
	int len3 = max(len1, len2);
	for(int i=1; i<=len3; i++)
	{
		c[i] += a[i] - b[i];
		if(c[i] < 0)
		{
			c[i+1]--;	//向高位借1
			c[i] += 10;	//当前为加10
		}
	}
	//删除前导0,注意这里至少要保留1位
	//当结果为0时,也要输出0
	while(len3>1 && c[len3]==0) len3--;	 
	c[0] = len3;
}

//高精乘高精 
void GchengG(char s1[], char s2[], int c[])
{
	int a[maxn] = {}, b[maxn] = {};
	int len1 = strlen(s1);
	int len2 = strlen(s2);
	
	//反转存储
	for(int i=1; i<=len1; i++) a[i] = s1[len1-i] - '0';
	for(int i=1; i<=len2; i++) b[i] = s2[len2-i] - '0';
	
	for(int i=1; i<=len1; i++)
	{
		for(int j=1; j<=len2; j++)
		{
			c[i+j-1] += a[i] * b[j];	//a[i] * b[j]的值累加到c[i+j-1]
			c[i+j] += c[i+j-1] / 10;	//向c[i+j]进位
			c[i+j-1] %= 10;
		}
	}
	
	//删除前导0
	int len3 = len1 + len2;
	while(len3>1 && c[len3]==0) len3--;
	c[0] = len3;
}

//高精除低精
//该代码只能处理可以整除的情况
void GchuD(char s1[], int b, int c[])
{
	int a[maxn] = {};
	int t[maxn] = {};
	int len1 = strlen(s1);
	//正向存储
	for(int i=1; i<=len1; i++) a[i] = s1[i-1] - '0';
	
	//从高位开始除
	int x = 0;
	for(int i=1; i<=len1; i++)
	{
		t[i] = (x*10+a[i]) / b;
		x = (x*10+a[i]) % b;
	}
	
	//前导可能为0,st记录第一个非0的位置
	int st = 1;	
	while(t[st]==0 && st<len1) st++;
	//将结果保存到c数组
	c[0] = 0;
	for(int i=st; i<=len1; i++) c[++c[0]] = t[i];
}

//比较字符串a[k, k+len-1]和b的大小
//即a从下标k开始,长度为len
//a>=b返回true,a<b返回false
bool compare(int a[], int b[], int k, int len)
{
	if(a[k+len] > 0) return true;	//字符串a上面那位还有值,则一定比b大
	for(int i=len-1; i>=0; i--)
	{
		if(a[k+i] > b[i+1]) return true;		//明确a>b
		else if(a[k+i] < b[i+1]) return false;	//明确a<b
	}
	return true;	//每一位都相等,则表示a和b相等
}

//高精除高精
//运算成功返回true,失败返回false
bool GchuG(char s1[], char s2[], int c[], int d[])
{
	int a[maxn] = {}, b[maxn] = {};
	int len1 = strlen(s1);
	int len2 = strlen(s2);
	//除数为0
	if(len2==0 && s2[0]==0) return false;
	
	//反向存储
	for(int i=1; i<=len1; i++) a[i] = s1[len1-i] - '0';
	for(int i=1; i<=len2; i++) b[i] = s2[len2-i] - '0';
	
	//从a[len1-len2+1,len1]开始减
	for(int i=len1-len2+1; i>=1; i--)
	{
		//只要a[i, i+len2-1]大于等于b,就不断地减b
		while(compare(a, b, i, len2))	
		{
			//高精减
			for(int j=0; j<len2; j++)
			{
				a[i+j] -= b[j+1];
				if(a[i+j] < 0)
				{
					a[i+j+1] -= 1;
					a[i+j] += 10;
				}
			}
			c[i] += 1;
		}
	}
	//c保存的是商,此处是计算商的长度
	for(int i=len1-len2+1; i>=1; i--)
	{
		if(c[i] != 0)
		{
			c[0] = i;
			break;
		}
	}
	
	//a中剩下的数字即为余数,复制到r中
	for(int i=len1; i>=1; i--)
	{
		if(a[i] != 0)
		{
			r[i] = a[i];
			if(r[0] == 0) r[0] = i;	//计算余数的长度
		}
	}
	if(r[0]==0) r[0] = 1;
	
	return 0;
}

int main()
{
	char s1[maxn], s2[maxn];
	
//	scanf("%s%s", s1, s2);
//	GaddG(s1, s2, ans);
//	for(int i=ans[0]; i>=1; i--)
//	{
//		printf("%d", ans[i]);
//	}

//	scanf("%s%s", s1, s2);
//	GjianG(s1, s2, ans);
//	for(int i=ans[0]; i>=1; i--)
//	{
//		printf("%d", ans[i]);
//	}	

//	scanf("%s%s", s1, s2);
//	GchengG(s1, s2, ans);
//	for(int i=ans[0]; i>=1; i--)
//	{
//		printf("%d", ans[i]);
//	}	

//	int x = 0;
//	scanf("%s%d", s1, &x);
//	GchuD(s1, x, ans);
//	for(int i=1; i<=ans[0]; i++)
//	{
//		printf("%d", ans[i]);
//	}

	scanf("%s%s", s1, s2);
	GchuG(s1, s2, ans, r);
	for(int i=ans[0]; i>=1; i--)
	{
		printf("%d", ans[i]);
	}	
	printf("\n");
	//余数
	for(int i=r[0]; i>=1; i--)
	{
		printf("%d", r[i]);
	}

    return 0;
} 

压位高精度

引入:
在一般的高精度加法、减法、乘法运算中,我们都是将参与运算的数拆分成一个个单独的数码进行运算。
例如计算\(8192*42\)时,如果按照高精度乘高精度的计算方式,我们实际上算的是\((8000+100+90+2)*(40+2)\)
在位数较多的时候,拆分出的数也很多,高精度运算的效率就会下降。
优化:
注意到拆分数字的方式并不影响最终的结果,因此我们可以将若干个数码进行合并。
过程:
还是以上面的例子为例,如果我们每两位拆分一个数,我们可以拆分成\((8100+92)*42\).
这样的拆分不影响最终结果,但是因为拆分出的数字变少了,计算效率也就提升了。
进位制:
从进位制的角度理解这一过程,我们通过在较大的进位制(上面每两位拆分一个数,可以认为是在\(100\)进制下进行运算)下进行运算,从而达到减少参与运算的数字的位数,提升运算效率的目的。
压位高精加法:

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

const int maxn = 3000;
int ans[maxn] = {}, r[maxn] = {};

//压位高精度(百进制)
//高精 + 高精
void GaddG(char s1[], char s2[], int c[])
{
	int a[maxn] = {}, b[maxn] = {};		//十进制
	int a1[maxn] = {}, b1[maxn] = {};	//百进制
	int len1 = strlen(s1);
	int len2 = strlen(s2); 
	
	//反转存储(十进制)
	for(int i=1; i<=len1; i++) a[i] = s1[len1-i] - '0';
	for(int i=1; i<=len2; i++) b[i] = s2[len2-i] - '0';
	
	//反转存储(百进制)
	len1 = (len1 + 1) / 2;
	len2 = (len2 + 1) / 2;
	for(int i=1; i<=len1; i++) a1[i] = a[1+2*(i-1)+1] * 10 + a[1+2*(i-1)];
	for(int i=1; i<=len2; i++) b1[i] = b[1+2*(i-1)+1] * 10 + b[1+2*(i-1)];
	  
	//百进制格式下进行加法
	int len3 = max(len1, len2);
	for(int i=1; i<=len3; i++)
	{
		c[i] += a1[i] + b1[i];
		c[i+1] = c[i] / 100;	//进位
		c[i] %= 100;	//余数即为当前位
	}
	if(c[len3+1] > 0) len3++;
	c[0] = len3;
}

int main()
{
	char s1[maxn], s2[maxn];
	
	scanf("%s%s", s1, s2);
	GaddG(s1, s2, ans);
	//按百进制输出
	printf("%d", ans[ans[0]]);
	for(int i=ans[0]-1; i>=1; i--)
	{
		printf("%02d", ans[i]);
	}

    return 0;
}

栈和队列

一些概念

栈是OI中常用的一种线性数据结构。
注意: 本文主要讲的是栈这种数据结构,而非程序运行时的系统栈/栈空间。
特点: 栈的修改与访问是按照后进先出的原则进行的,因此栈通常被称为是后进先出表,简称LIFO表。
举例来讲:
可以把食堂里洗净的一摞碗看做一个栈,通常情况下,最先洗净的碗总是放在最底下,后洗净的碗总是摞在最顶上。
而使用时,却是从顶上拿取,也就是说,后洗的先取用,后摞上的先取用。
如果我们把洗净的碗“摞上”称为进栈,把“取用碗”称为出栈,那么,上例的特点是:后进栈的先出栈。
用图形来表示:
image
常见运算:

  • 进栈:往栈顶加入一个元素
  • 出栈:删除栈顶元素
  • 读栈:查看当前的栈顶元素
  • 建栈:在使用栈之前,首先需要建立一个空栈
  • 测试栈:使用栈的过程中,不断测试栈是否为空或已满

用数组模拟栈

	//st表示栈,top为栈顶元素下标
	int st[maxn] = {}, top = 0;
	
	//进栈
	st[++top] = x;
	//读栈
	int y = st[top];
	//出栈
	if(top) top--;
	//清空栈
	top = 0;

STL中的栈

	stack<int> s;
	
	s.empty();	//如果栈为空,返回true,否则返回false
	s.size();	//返回栈中元素的个数
	s.pop();	//删除栈顶元素但不返回其值
	s.top();	//返回栈顶的元素,但不删除该元素
	s.push();	//在栈顶压入新元素

例题:

B3614 【模板】栈
数组模拟实现:

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

const int maxn = 1e6 + 10;

int n = 0;
long long st[maxn] = {}, top = 0;

int main()
{
	string s1;
	int x = 0;
	
	while(scanf("%d", &n) != EOF)
	{
		top = 0;	//清空栈
		for(int i=1; i<=n; i++)
		{
			cin >> s1;
			if(s1 == "push") 
			{
				cin >> x;
				st[++top] = x;	//进栈
			}
			if(s1 == "pop")
			{
				if(top > 0) top--;	//出栈
				else cout << "Empty" << endl;
			} 
			if(s1 == "query") 
			{
				if(top > 0) cout << st[top] << endl;//读栈
				else cout << "Anguei!" << endl;
			}
			if(s1 == "size") cout << top << endl; //栈的大小
		}		
	}
 
    return 0;
} 

STL实现:

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

int n = 0;
stack<long long> st; 

int main()
{
	string s1;
	int x = 0;
	
	while(cin >> n)
	{
		while(st.size()) st.pop();
		
		while(n--)
		{
			cin >> s1;
			if(s1 == "push")
			{
				cin >> x;
				st.push(x);
			}
			if(s1 == "pop")
			{
				if(st.size() == 0) cout << "Empty" << endl;
				else st.pop();
			}
			if(s1 == "query")
			{
				if(st.size() == 0) cout << "Anguei!" << endl;
				else cout << st.top() << endl;
			}
			if(s1 == "size") cout << st.size() << endl;			
		} 
	}
 
    return 0;
} 

队列

一些概念

队列: 是一种具有先进入队列的元素一定先出队列性质的表。由于该性质,队列通常也被成为先进先出表,简称FIFO表。
举例来讲:
就像排队买东西,排在前面的人先买完东西后离开队伍(删除),而后来的人总是排在队伍末尾(插入)。
通常把队列的删除和插入分别称为出队入队
用图形来表示:
image
常见运算:

  • 入队:将x接到队列的末端
  • 出队:弹出队列的第一个元素,注意,并不会返回被弹出元素的值
  • 访问队首元素:返回最早被压入队列的元素
  • 访问队尾元素:返回最后被压入队列的元素
  • 判断是否为空
  • 访问队列中的元素个数

用数组模拟队列

	int q[maxn] = {}, head = 1, tail = 0;

	q[++tail] = x;	//插入元素
	head++;			//删除元素
	x = q[head];	//访问队首
	x = q[tail];	//访问队尾
	
	//清空队列
	head = 1;
	tail = 0;
	
	head > tail;	//队列为空
	x = tail - head + 1;	//队列中的元素个数	

STL中的队列

	queue<int> q;
	
	q.front();		//返回队首元素
	q.back();		//返回队尾元素
	q.push();		//在队尾插入元素
	q.pop();		//弹出队首元素
	q.empty();		//队列是否为空
	q.size();		//返回队列中元素的个数
	
	//queue赋值运算符:=
	queue<int> q1, q2;
	q1 = q2;		//将q2赋值给q1

例题:

B3616 【模板】队列
数组模拟实现:

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

const int maxn = 10010;

int n = 0;
int q[maxn] = {}, head = 1, tail = 0;

int main()
{
	int op = 0, x = 0;
	
	scanf("%d", &n);
	for(int i=1; i<=n; i++)
	{
		scanf("%d", &op);
		if(op == 1) //将元素 x 加入队列
		{
			scanf("%d", &x);
			q[++tail] = x;
		}
		if(op == 2) //将队首弹出队列
		{
			if(head > tail) printf("ERR_CANNOT_POP\n");
			else head++;
		}
		if(op == 3) //查询队首
		{
			if(head > tail) printf("ERR_CANNOT_QUERY\n");
			else printf("%d\n", q[head]);
		}
		if(op == 4) //查询队列内元素个数
		{
			printf("%d\n", tail - head + 1);
		}
	}
 
    return 0;
} 

STL实现:

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

const int maxn = 10010;

int n = 0;
queue<int> q;

int main()
{
	int op = 0, x = 0;
	
	scanf("%d", &n);
	for(int i=1; i<=n; i++)
	{
		scanf("%d", &op);
		if(op == 1) //将元素 x 加入队列
		{
			scanf("%d", &x);
			q.push(x);	
		} 
		if(op == 2) //将队首弹出队列
		{
			if(q.size() == 0) printf("ERR_CANNOT_POP\n");
			else q.pop();
		}
		if(op == 3) //查询队首
		{
			if(q.size() == 0) printf("ERR_CANNOT_QUERY\n");
			else printf("%d\n", q.front());
		}
		if(op == 4) //查询队列内元素个数
		{
			printf("%d\n", q.size());
		}
	}
 
    return 0;
} 

搜索和回溯

说明

搜索与回溯是计算机解题中常用的算法,很多问题无法根据某种确定的计算法则来求解,可以利用搜索与回溯的技术求解。
回溯是搜索算法中的一种控制策略。
基本思想:
为了求得问题的解,先选择某一种可能情况向前探索,在探索过程中,一旦发现原来的选择时错误的,就退回一步重新选择,继续向前探索,如此反复进行,直至得到解或证明无解。
比如:迷宫问题
进入迷宫后,先随意选择一个前进方向,一步步向前试探前进,如果碰到死胡同,说明前进方向已无路可走,这时,首先看其他方向是否还有路可走,如果有路可走,则沿该方向再向前试探;如果已无路可走,则返回一步,再看其他方向是否还有路可走;如果有路可走,则沿该方向再向前试探。
按此原则不断搜索回溯再搜索,直到找到新的出路或从原路返回入口处无解为止。
具体例子:
在下图中找一条A到B的路线
image

从A到B的路线:A->4->6->B

框架

框架一:

int search(int k)
{
	for(int i=1; i<=算符种数; i++)
	{
		if(满足条件)
		{
			保存结果;
			if(到目的地) 输出解;
			else search(k + 1);
			
			恢复:保存结果之前的状态{回溯一步}
		}
	}
}

框架二:

int search(int k)
{
	if(到目的地) 
	{
		输出解;	
		return 0;
	}
	
	for(int i=1; i<=算符种数; i++)
	{
		if(满足条件)
		{
			保存结果; 
			search(k + 1);
			
			恢复:保存结果之前的状态{回溯一步}
		}
	}
}

例题

P198. 全排列问题

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

int n = 0;
int a[15] = {}, v[15] = {};

//给第x位填数
void search(int x)
{
	for(int i=1; i<=n; i++)
	{
		if(v[i] == 1) continue;	//i已被使用,跳过
		
		v[i] = 1;	//标记i已使用
		a[x] = i;	//第x位赋上值i
		if(x == n)	//已完成全部赋值
		{
			for(int j=1; j<=n; j++)
			{
				printf("%5d", a[j]);
			}
			printf("\n");
		}
		
		search(x + 1); 	//搜索
		v[i] = 0;		//回溯,只需要标记i未用即可,a[x]不需要
	}
}

int main()
{
	scanf("%d", &n);
	
	search(1);	//从第1位开始搜索
 
    return 0;
} 

分治算法

二分法

二分查找

定义:
二分查找,也叫折半搜索、对数搜索,是用来在一个有序数组中查找某一元素的算法。
过程:
以在一个升序数组中查找一个数为例。
每次比较数组当前部分的中间元素,如果中间元素大于等于所查找的值同理,只需到左侧查找;如果中间元素小于所查找的值,那么左侧的只会更小,不会有所查找的元素,只需到右侧查找。
时间复杂度:
平均时间复杂度和最坏时间复杂度均为\(O(log(n))\)
因为在二分搜索过程中,算法每次都把查询的区间减半,所以对于一个长度为\(n\)的数组,至多会进行\(O(log(n))\)次查找。
代码模板:

bool check(int x)  //检查x是否满足某种性质

int l = 1, r = n, ans = 0;
while(l <= r)
{
  int mid = (l+r)>>1;
  if(check(mid)) { ans = mid; r = mid - 1; }
  else l = mid + 1;
}
printf("%d", ans); 

例题: PJ835 二分查找

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

const int maxn = 1e6 + 10;

int n = 0;
int a[maxn] = {};

bool check(int pos, int x)
{
	return a[pos] >= x;
}

int main()
{ 
	int x = 0, ans = 0;

	scanf("%d", &n);
	for(int i=1; i<=n; i++) scanf("%d", &a[i]);
	scanf("%d", &x);
		
	int l = 1, r = n;
	while(l <= r)
	{
		int mid = (l + r) >> 1;
		if(check(mid, x)) { ans = mid; r = mid - 1; } 
		else { l = mid + 1; }
	}
	if(a[ans] == x) printf("%d ", ans);
	else printf("-1 "); 
 
    return 0;
} 

STL的二分查找

c++标准库中的两个函数
lower_bound: 查找首个大于等于给定值的元素
upper_bound: 查找首个大于给定值的元素
例题: PJ835 二分查找

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

const int maxn = 1e6 + 10;

int n = 0;
int a[maxn] = {};
 
int main()
{ 
	int x = 0, ans = 0;

	scanf("%d", &n);
	for(int i=1; i<=n; i++) scanf("%d", &a[i]);
	scanf("%d", &x);
		
	ans = lower_bound(a+1, a+1+n, x) - a;

	if(a[ans] == x) printf("%d ", ans);
	else printf("-1 "); 
 
    return 0;
} 

最大值最小化

概念:
如果一个数组中的左侧或者右侧都满足某一种条件,而另一侧都不满足这种条件,也可以看作是一种有序。
换言之,二分查找可以用来查找满足某种条件的最大(最小)的值。
思想:
求满足某种条件的最大值的最小可能情况(最大值最小化)。
暴力: 从小到大枚举这个作为答案的最大值,然后去判断是否合法。
优化做法:
前提: 答案具有单调性
条件:

  • 答案在一个固定区间内
  • 可能查找一个符合条件的值不是很容易,但要求能比较容易得判断某个值是否是符合条件的
  • 可行解对于区间满足一定的单调性,换言之,如果\(x\)是符合条件的,那么有\(x+1\)或者\(x-1\)也符合条件。

例题: PJ210 [NOIP2015]跳石头

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

const int maxn = 50010;
int len = 0, n = 0, m = 0;
int d[maxn] = {};

bool check(int x)
{
	//假设站在i位置,往j跳
	int cnt = 0, i = 0, j = 1;
	while(i<n+1 && j<=n+1)
	{
		if(d[j] - d[i] < x) //i到j之间的距离小于x
		{
			if(j == n+1) //j为终点,则将i移走
			{
				cnt++;	
				break;
			}
			//j不是终点,则将j移走
			cnt++;	
			j++;	
		}
		//i往前跳一步
		else 
		{
			i = j; 
			j++;
		}
	}
	if(cnt <= m) return true;
	return false;
}

int main()
{
	int x = 0;
	scanf("%d%d%d", &len, &n, &m);
	for(int i=1; i<=n; i++) scanf("%d", &d[i]);
	d[n+1] = len;
	
	int l = 0, r = len, ans = 0;
	while(l <= r)
	{
		int mid = (l + r) >> 1;
		if(check(mid)) { ans = mid; l = mid + 1; }
		else r = mid - 1;
	}
	printf("%d", ans);
	
	return 0;
} 

浮点数二分

一般如果题目要求精度为小数点后\(n\)位,则运算时,保存到\(n+2\)位即可。
即:题目要求精确到小数点后\(2\)位,运算时,保存到小数点后\(4\)位即可。
代码模板:

bool check(double x)  //检查x是否满足某种性质

double l = 1, r = n;
while(l + eps < r)
{
  double mid = (l+r)/2;
  if(check(mid)) r = mid; 
  else l = mid;
}
printf("%lf", l);

//或者写成这样
double l = 1, r = n;
for(int i=1; i<=100; i++)
{
  double mid = (l+r)/2;
  if(check(mid)) r = mid; 
  else l = mid;
}
printf("%lf", l); 

例题: PJ215 一元三次方程求解

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

double a = 0, b = 0, c = 0, d = 0;

double f(double x)
{
	return a*x*x*x + b*x*x + c*x + d;
}

int main()
{
	int cnt = 0;
	scanf("%lf%lf%lf%lf", &a, &b, &c, &d);
	for(double i=-100; i<100; i++)
	{
		double y1 = f(i);
		double y2 = f(i+1);
		//先判断边界是不是答案
		if(f(i) == 0) 
		{
			printf("%.2lf ", i);
			cnt++;
		}
		//如果y1*y1<0,表示f(i)和f(i+1)异号,则i到i+1之间一定有答案
		else if(y1 * y2 < 0)
		{
			double l = i, r = i + 1, ans = 0;
			while(l + 0.0001 < r )
			{
				double mid = (l+r)/2;
				if(f(mid)*f(r) < 0) l = mid;
				else r = mid;
			}
			printf("%.2lf ", l);
			cnt++;
		}
		
		if(cnt == 3) return 0;
	}
	//前面没有判断-100,如果cnt==2,则表示-100一定是答案
	if(cnt == 2) printf("%.2lf", 100);
	
	return 0;
} 

逆序数

逆序: 在一个排列中,如果某一个较大的数排在某一个较小的数前面,就说这两个数构成一个逆序或反序。
逆序数: 在一个排列里出现的逆序的总个数,叫做逆序数。
排列的逆序数是它恢复成正序序列所需要做相邻对换的最少次数。
解法: 求解逆序数的算法,可以使用归并排序树状数组,时间复杂度均为\(O(nlogn)\)。此处只讲解归并排序法。

归并排序

原理: 归并排序基于分治思想将数组分段排序后再合并,合并是核心
时间复杂度:
时间复杂度在最优、最坏与平均情况下均为\(O(nlogn)\),空间复杂度为\(O(n)\)
稳定性: 归并排序是一种稳定排序算法
分段:
1、当数组长度为1时,不用再分解
2、当数组长度大于1时,将该数组分为两段,通常分为尽量等长的两段(\(\text{mid} = \left\lfloor \frac{l + r}{2} \right\rfloor\)
image

合并:
归并排序最核心的部分是合并过程:将两个有序的数组\(a[i]\)\(b[j]\)合并为一个有序数组\(c[k]\)
从左往右枚举\(a[i]\)\(b[j]\),找出最小的值并放入数组\(c[k]\);重复上述过程直到\(a[i]\)\(b[j]\)有一个为空时,将另一个数组剩下的元素放入\(c[k]\)
为保证排序的稳定性,前段首元素小于或等于后段首元素时(\(a[i]<=b[j]\)),而非小于时(\(a[i]<b[j]\))就要作为最小值放入\(c[k]\)
image

代码模板:

void merge_sort(int q[], int l, int r)
{
    if (l >= r) return;

	//不断的细分,直到只有一个元素
    int mid = l + r >> 1;
    merge_sort(q, l, mid);
    merge_sort(q, mid + 1, r);

	//回溯时进行合并
	//将[l, r]这段区间分为[l, mid]和[mid+1, r]两部分,边比较边放入临时数组
    int k = 0, i = l, j = mid + 1;
    while(i<=mid && j<=r)
    {
        if(q[i] <= q[j]) tmp[k++] = q[i++];
        else tmp[k++] = q[j++];
    }

	//将前mid前或mid后的剩余元素加入临时数组
    while(i <= mid) tmp[k++] = q[i++];
    while(j <= r) tmp[k++] = q[j++];

	//将临时数组中的元素,放到原数组,位置是对应的,相当于对这一段进行了排序
    for (i=l, j=0; i<=r; i++) q[i] = tmp[j++];
} 

逆序对

逆序对\(i<j\)\(a_i>a_j\)的有序数对\((i, j)\)。排序后的数组无逆序对。
求逆序对:
归并排序的合并操作中,每次后段首元素被作为当前最小值取出时,前段剩余元素个数之和即是逆序对,将合并过程中产生的所有逆序对个数相加,最后就是总的逆序对的数量。

void merge_sort(int q[], int l, int r)
{
	if(l >= r) return;
	
	int mid = (l + r) >> 1;
	merge_sort(q, l, mid);
	merge_sort(q, mid+1, r);
	
	int k = 0, i = l, j = mid + 1;
	while(i<=mid && j<=r)
	{
		if(q[i] <= q[j]) tmp[++k] = q[i++];
		else 
		{
			tmp[++k] = q[j++];	
			ans += mid + 1 - i;	//计算逆序对的数量
		}
	}
	
	while(i <= mid) tmp[++k] = q[i++];	
	while(j <= r) tmp[++k] = q[j++];
	
	for(i=l, j=1; i<=r; i++) q[i] = tmp[j++];
}

例题

PJ213 逆序对个数

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

const int maxn = 3e6+10;

int n = 0;
int a[maxn] = {}, tmp[maxn] = {};
long long ans = 0;

void merge_sort(int q[], int l, int r)
{
	if(l >= r) return;
	
	int mid = (l + r) >> 1;
	merge_sort(q, l, mid);
	merge_sort(q, mid+1, r);
	
	int k = 0, i = l, j = mid + 1;
	while(i<=mid && j<=r)
	{
		if(q[i] <= q[j]) tmp[++k] = q[i++];
		else 
		{
			tmp[++k] = q[j++];	
			ans += mid + 1 - i;	//计算逆序对的数量
		}
	}
	
	while(i <= mid) tmp[++k] = q[i++];	
	while(j <= r) tmp[++k] = q[j++];
	
	for(i=l, j=1; i<=r; i++) q[i] = tmp[j++];
}

int main()
{
	scanf("%d", &n);
	for(int i=1; i<=n; i++) scanf("%d", &a[i]);
	
	merge_sort(a, 1, n);
	
	printf("%lld", ans);

	return 0;
} 

快速幂取模

前置推导

处理形如\(a^b\ \%\ c\ =\ ?\)的问题
前置知识:
公式一:
\((a*b)\%c == ((a\%c)*(b\%c))\%c\)
\((a*b)\%c ==((a\%c)*b)\%c\)
公式二:
\(a^b\%c == (a\%c)^b\%c\)
注:公式二可由公式一推导得出
\((a^b)\%c == (a*a*a...)\%c\)
\(==((a\%c)*(a\%c)*(a\%c)...)\%c\)
\(==((a\%c)^b)\%c\)
算法1:

int ans = 1;
for(int i=1; i<=b; i++)
{
	ans = ans * a;
}
ans = ans % c;

时间复杂度:\(O(b)\)
问题:如果\(a\)\(b\)过大,很容易就会溢出
算法2:
根据公式一优化:

int ans = 1;
a = a % c;	//缩小a的值
for(int i=1; i<=b; i++)
{
	ans = ans * a;
}
ans = ans % c;

算法3:
既然相乘再取余与取余再相乘保持余数不变,那么新算法得的ans也可以进行取余,在算法2的基础上进行改进

int ans = 1;
a = a % c;	//缩小a的值
for(int i=1; i<=b; i++)
{
	ans = (ans * a) % c;
}
ans = ans % c;

时间复杂度:\(O(b)\)
问题:如果\(b\)过大,可能超时
算法4:
根据公式二优化,考虑\(b\)的奇偶性
1、b为偶数
\(a^b\%c == ((a^2)^{b/2})\%c == ((a^2\%c)^{b/2})\%c\)
2、b为奇数
\(a^b\%c == (a*(a^2)^{b/2})\%c == (a*(a^2\%c)^{b/2})\%c\)
\(k=a^2\%c\),那么
\(b\)为偶数:求\(k^{b/2}\%c\)即可
\(b\)为奇数:求\((a*k^{b/2}\%c)\%c\)即可

int ans = 1;
a = a % c;	//缩小a的值
//如果b为奇数,要多求一步,可以提前算到ans中
if(b % 2 == 1) ans = (ans * a) % c;
k = (a * a) % c;
for(int i=1; i<=b/2; i++)
{
	ans = (ans * k) % c;
}
ans = ans % c;

时间复杂度:\(O(b/2)\)
问题:如果\(b\)过大,依然可能超时
算法5:
通过算法4,直接将时间复杂度由\(O(b)\)优化到\(O(b/2)\)
当令\(k=(a*a)\%c\)时,所求的结果变为了\(k^{b/2}\%c\)
此时令\(a=k,b=b/2\),则又变成了求\(a^{b}\%c\),但此时的b相对于最初已经缩小了一半,而这个过程可以迭代下去。
\(b==0\)时,所有的因子都已经相乘,算法结束。
于是便可以在\(O(logb)\)的时间内完成了,便有了最终算法

int ans = 1;
a = a % c;	//缩小a的值
while(b > 0)
{
	if(b % 2 == 1) ans = (ans * a) % c;
	b = b / 2;
	a = (a * a) % c;
}

蒙哥马利算法

蒙哥马利快速幂取模算法,简单漂亮
有彼得·蒙德马利在1985年提出

//求a^b%c
int Montgomery(int a, int b, int c)
{
	int ans = 1;
	a = a % c;
	while(b > 0)
	{
		//位运算
		//b&1==1表示b为奇数,为0表示为偶数
		if(b & 1) ans = (ans * a) % c;	//奇数,补一项
		b = b >> 1;
		a = (a * a) % c;
	}
	return ans;
}

例题

PJ219 取余运算

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

typedef long long ll;

ll montgomery(ll a, ll b, ll c)
{
	ll ans = 1;
	a = a % c;
	while(b > 0)
	{
		if(b & 1) ans = (ans * a) % c;
		b = b >> 1;
		a = a * a % c;
	}
	
	return ans;
}

int main()
{
	ll a = 0, b = 0, c = 0;
	scanf("%lld%lld%lld", &a, &b, &c);
	printf("%lld^%lld mod %lld=%lld", a, b, c, montgomery(a, b, c));
	
	return 0;
} 

贪心算法

优先队列

特性

  • 普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。
  • 优先队列具有队列的所有特性,包括队列的基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的。
  • 优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。
  • 优先队列具有最高级先出的行为特征

基本操作

定义:
原型: priotity_queue<Type,Container,Functional>
Type:数据类型
Container:容器类型(STL里面默认用的是vector)
Functional:比较的方式
当需要用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,默认是大根堆

//大根堆
priority_queue<int> q1;
//大根堆,与上面的q1等价
priority_queue<int, vector<int>, less<int>> q2;
//小根堆
priority_queue<int, vector<int>, greater<int>> q3;

支持的操作:

  • top 访问队头元素(\(O(1)\))
  • empty 队列是否为空(\(O(1)\))
  • size 返回队列内元素个数(\(O(1)\))
  • push 插入元素到队尾(并排序)(\(O(logn)\))
  • pop 弹出队头元素,此时优先队列不能为空(\(O(logn)\))
  • swap 交换内容
  • emplace 原地构造一个元素并插入队列

基本类型的优先队列

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

//大根堆
priority_queue<int> q1;   
//小根堆
priority_queue<int, vector<int>, greater<int>> q2;
priority_queue<string> q3;

int main()
{ 
	for(int i=1; i<=5; i++)
	{
		q1.push(i);
		q2.push(i);
	}
	
	//该循环输出5 4 3 2 1
	while(!q1.empty())
	{
		printf("%d ", q1.top());
		q1.pop();
	}
	printf("\n");
	
	//该循环输出1 2 3 4 5
	while(!q2.empty())
	{
		printf("%d ", q2.top());
		q2.pop();
	}
	printf("\n");
	
	q3.push("abc");
	q3.push("abcd");
	q3.push("cbd");
	
	//该循环输出cbd abcd abc
	while(!q3.empty())
	{
		cout << q3.top() << " ";
		q3.pop();
	}
	printf("\n");

	return 0; 
}  

pair类型的优先队列

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

//最后一个>前往往要加个空格
//防止编辑器认为是>>
priority_queue<pair<int, int> > q1;    

int main()
{ 
	q1.push(make_pair(1, 2));
	q1.push(make_pair(1, 3));
	q1.push(make_pair(2, 5));
	
	while(!q1.empty())
	{
		printf("%d %d\n", q1.top().first, q1.top().second);
		q1.pop();
	}
	//输出:
	//2 5
	//1 3
	//1 2

	return 0; 
} 

自定义类型的优先队列

自定义类型的优先级是基于小于号比较的,所以只需要重载小于号即可。

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

struct student
{
	int age;		//年龄
	string name;	//姓名
	
	//年龄由小到大,如果年龄相同,则按姓名字典序由大到小
	bool operator < (const student &stu) const
	{
		if(age == stu.age) return name > stu.name;
		return age < stu.age;
	}
}stu1, stu2, stu3;
 
priority_queue<student> q1;    

int main()
{ 
	stu1.age = 15;
	stu1.name = "zhangsan";
	stu2.age = 15;
	stu2.name = "lisi";
	stu3.age = 16;
	stu3.name = "wangwu"; 
	q1.push(stu1);
	q1.push(stu2);
	q1.push(stu3);	
	
	while(!q1.empty())
	{
		cout << q1.top().age << " " << q1.top().name << endl; 
		q1.pop();
	}
	//输出:
	//16 wangwu
	//15 lisi
	//15 zhangsan

	return 0; 
}  

基础贪心

理论基础

引入:
贪心算法是用计算机来模拟一个贪心的人做出决策的过程。这个人十分贪婪,每一步行动总是按某种指标选取最优的操作。而且他目光短浅,总是只看眼前,并不考虑以后可能造成的影响。
因此,并不是所有的时候贪心法都能获得最优解,所以一般使用贪心法的时候,都要确保自己能证明其正确性。
适用范围:
贪心算法在有最优子结构的问题中尤为有效。
最优子结构的意思是问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。
证明:
贪心算法有两种证明方法:反证法和归纳法。一般情况下,一道题只会用到其中的一种方法来证明。
1、反证法:如果交换方案中任意两个元素/相邻的两个元素后,答案不会变的更好,那么可以推定目前的解已经是最优解了。
2、归纳法:先算得出边界情况(例如\(n=1\))的最优解\(F_1\),然后再证明:对于每个\(n\)\(F_{n+1}\)都可以由\(f_n\)推导出结果。

例题

混合奶牛

贪心策略: 先买单价便宜的牛奶,再买贵的,直到买够所需数量

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

struct Node
{
	int p, a;
	
	bool operator < (const Node &nd) const
	{
		return p > nd.p;
	}
};
int n = 0, m = 0;
priority_queue<Node> q;

int main()
{
	scanf("%d%d", &n, &m);
	for(int i=1; i<=m; i++)
	{
		Node nd;
		scanf("%d%d", &nd.p, &nd.a);
		q.push(nd);
	}

	int t = 0, ans = 0;
	while(t<n)
	{
		Node nd = q.top();
		q.pop();
		if(n-t<=nd.a) 
		{
			ans += nd.p * (n-t);
			break;
		}
		else
		{
			ans += nd.p * nd.a;
			t += nd.a;
		}
	}
	printf("%d", ans);

	return 0;
}  

纪念品分组

贪心策略: 判断价格最大的物品和价格最小的物品价格的和是否超过了w,如果超过了,则价格最大物品单独一组,没超过则价格最大和最小一组
合理性证明:
1、思考:价值最大的物品有没有必要再跟价格第二小和价格第三小或之前的再组合
2、没必要
3、比如\(p_1<p_2<p_3<p_4<p_5\),如果\(p_5\)\(p_2\)组合为一组,那么\(p_1\)\(p_2\)都可以和其他任何物品组合为一组,即选\(p_1\)\(p_2\)都一样

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

int w = 0, n = 0;
int a[30010] = {};
int ans = 0;

//先对数组从小到大排序,如果最大+最小<=w,则分一组,否则最大单独一组

int main()
{
	scanf("%d%d", &w, &n);
	for(int i=1; i<=n; i++) scanf("%d", &a[i]);
	
	sort(a+1, a+1+n);
	
	int l=1, r = n;
	while(l<=r)
	{
		if(a[r]+a[l] <= w) l++;
		
		r--;
		ans++;
	}
	printf("%d", ans);
	
	return 0;
} 

反悔贪心

思想

思路是无论当前的选项是否最优都接受,然后进行比较,如果选择之后不是最优了,则反悔,舍弃掉这个选项;否则,正式接受。如此往复。

例题

[Usaco2009 Open]工作安排Job
思路:
1、将任务按截止时间由小到大进行排序,先做截止时间靠前的任务,这样可以保证尽量多做任务
2、用小根堆维护已做的所有任务,\(q.top\)就是已做的任务中,最小利润;\(q.size\)就是已做任务的数量
2、当遍历到第\(i\)个任务时,假如该任务截止时间为\(ndlist[i].d\)
3、如果当前已做的任务量\(q.size<ndlist[i].d\),说明这个任务直接就可以做
4、【反悔】如果任务量\(q.size>=ndlist[i].d\),那么就要把之前所做的所有任务中,利润最小的任务删掉,换成第\(i\)个任务,这样可以使总利润最大

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

int n = 0;
struct Node
{
	int d, p;
}ndlist[100010];
//q表示当前利润最小的工作
priority_queue<int, vector<int>, greater<int>> q;
long long ans = 0;

bool cmp(Node &nd1, Node &nd2)
{
	return nd1.d < nd2.d;
}

int main()
{
	scanf("%d", &n);
	for(int i=1; i<=n; i++)
	{
		scanf("%d%d", &ndlist[i].d, &ndlist[i].p);
	}
	
	sort(ndlist+1, ndlist+1+n, cmp);
	
	for(int i=1; i<=n; i++)
	{
		//如果当前工作截止日期>所做工作总的数量
		//说明直接做这件事就可以
		if(ndlist[i].d > q.size())
		{
			q.push(ndlist[i].p);
			ans += ndlist[i].p;
		}
		else
		{
			//找到这个截止日期前利润最小的工作
			//如果当前工作利润大,则把利润最小的工作删去
			if(ndlist[i].p > q.top())
			{
				ans += ndlist[i].p - q.top();
				q.pop();
				q.push(ndlist[i].p);
				
			}
		}
	}
	printf("%lld", ans);
	
	return 0;
}  

树基础

引入

图论中的树和现实生活中的树长得一样,只不过我们习惯于处理问题的时候把树根放到上方来考虑。这种数据结构看起来像是一个倒挂的树,因此得名。

一些概念

树的概念:
1、树是一种常见的非线性的数据结构
2、树是\(n(n>0)\)个节点的有限集合,这个集合满足一下条件:
①有且仅有一个节点没有前驱(父亲节点),该节点称为树的根
②除根外,其余的每个结点都有且仅有一个父节点
③除根外,每一个节点都通过唯一的路径连到根上。这条路径由根开始,而末端就在该节点上,且除根以外,路径上的每一个结点都是前一个结点的后驱(儿子节点)
父亲: 对于除根以外的每个结点,定义为从该结点到根路径上的第二个结点。根结点没有父结点。
祖先: 一个结点到根结点的路径上,除了它本身外的结点。根结点的祖先集合为空。
根结点: 没有前驱的结点。在树中有且仅有一个根结点。根结点到每一个结点的路径是唯一的
叶节点: 没有后驱的结点称为树叶。由树的定义可知,树叶本身也是其父结点的子树。
子结点: 如果\(u\)\(v\)的父亲,那么\(v\)\(u\)的子结点。子结点的顺序一般不加以区分,二叉树是一个例外。
结点的度: 一个结点的子树数目称为该结点的度
树的度: 所有结点中最大的度称为该树的度
image
上图中,A结点的度为3,B结点的度为1,C结点的度为2,等等。树的度为3
结点的深度: 到根结点的路径上的边数
树的深度(高度): 所有节点的深度的最大值
image
上图中,A的深度为0,BCD的深度为1,EFG的深度为2,H的深度为3。树的深度为3
兄弟: 同一个父亲的多个子结点互为兄弟
后代: 子结点和子结点的后代
image
子树: 删掉与父亲相连的边后,该结点所在的子图。
image
特殊的树:
链: 满足与任一结点相连的边不超过\(2\)条的树称为链
image
菊花/星星: 满足存在u使得所有除u以外节点均与u相连的数称为菊花
image
二叉树: 每个结点最多只有两个儿子(子结点)的有根树称为二叉树。
常常对两个子结点的顺序加以区分,分别称之为左子结点和右子节点。
大多数情况下,二叉树一词均指有根二叉树。
image
完整二叉树: 每个结点的子结点数量均为0或者2的二叉树。换言之,每个结点或者是树叶,或者左右子树均非空
image
完全二叉树: 只有最下面两层结点的度数可以小于2,且最下面一层的结点都集中在该层最左边的连续位置上。
image
完美二叉树: 所有叶节点的深度均相同,且所有非叶节点的子节点数量均为2的二叉树称为完美二叉树
\(OIers\)所说的满二叉树多指完美二叉树
image

posted @ 2025-09-09 14:30  毛竹259  阅读(305)  评论(0)    收藏  举报