考研算法辅导课【合集】

文章目录

讲义

AcWing《考研算法辅导课》讲义
考纲:
一、线性表
(一)线性表的定义和基本操作
(二)线性表的实现
1. 顺序存储
2. 链式存储
3. 线性表的应用
二、栈、队列和数组
(一)栈和队列的基本概念
(二)栈和队列的顺序存储结构
(三)栈和队列的链式存储结构
(四)栈和队列的应用
(五)特殊矩阵的存储和压缩
三、树与二叉树
(一)树的基本概念
(二)二叉树
1. 二叉树的定义及其主要特征
2. 二叉树的顺序存储结构和链式存储结构
3. 二叉树的遍历
4. 线索二叉树的基本概念和构造
(三)树、森林
1. 树的存储结构
2. 森林与二叉树的转换
3. 树和森林的遍历
(四)树与二叉树的应用
1. 二叉排序树
2. 平衡二叉树
3. 哈夫曼(Huffman)树的哈弗曼编码
四、图
(一)图的基本概念
(二)图的存储及基本操作
1. 邻接矩阵法
2. 邻接表法
3. 邻接多重表、十字链表
(三)图的遍历
1. 深度优先搜索
2. 广度优先搜索
(四)图的基本应用
1. 最小(代价)生成树
2. 最短路径
3. 拓扑排序
4. 关键路径
五、查找
(一)查找的基本概念
(二)顺序查找法
(三)分块查找法
(四)折半查找法
(五)B树及其基本操作、B+树及其基本概念
(六)散列(Hash)表
(七)字符串模式匹配(KMP)
(八)查找算法的分析及应用
六、排序
(一)排序的基本概念
(二)插入排序
1. 直接插入排序
2. 折半插入排序
(三)起泡排序(bubble sort)
(四)简单选择排序
(五)希尔排序(shell sort)
(六)快速排序
(七)堆排序
(八)二路归并排序(merge sort)
(九)基数排序
(十)外部排序
(十一)各种内部排序算法的比较
(十二)排序算法的应用




-------------------------------------------------------------------------------------

第1讲 时间复杂度、矩阵展开
一、时间、空间复杂度
	只考虑次数,不考虑常数。常见复杂度有:O(1)、O(n)、O(sqrt(n))、O(n^k)、O(logn)、O(nlogn)
	考题:2011-1、2012-1、2013-1、2014-1、2017-1、2019-1
二、矩阵展开
	矩阵的按行展开、按列展开,展开后下标从0开始。
	考题:2016-4、2018-3、2020-1

-------------------------------------------------------------------------------------

第2讲 线性表
1. 将具有线性关系的数据存储到计算机中所使用的存储结构称为线性表。
2. 对于线性表中的数据来说,位于当前数据之前的数据统称为“前趋元素”,前边紧挨着的数据称为“直接前趋”;同样,后边的数据统称为“后继元素”,后边紧挨着的数据称为“直接后继”。
3. 线性表的分类
	(1) 数据元素在内存中集中存储,采用顺序表示结构,简称“顺序存储”;
		例如:数组
	(2) 数据元素在内存中分散存储,采用链式表示结构,简称“链式存储”。
		例如:单链表、双链表、循环单(双)链表
4. 不同实现方式的时间复杂度(不要硬背结论、要从实现方式入手分情况讨论,下述为特定情况下的时间复杂度)
	(1) 数组:随机索引O(1)、插入O(n)、删除O(n)
	(2) 单链表:查找某一元素O(n)、插入O(1)、删除O(n)
	(3) 双链表:查找某一元素O(n)、插入O(1)、删除O(1)
5. 考题:2016-1、2016-2、2012-42、2015-41、2019-41
6. 押题:AcWing 34、AcWing 1451

-------------------------------------------------------------------------------------

第3讲 栈与队列
1. 栈和队列的基本概念
2. 栈和队列的顺序存储结构
	(1) 栈:栈顶元素位置:指向最后一个元素、指向最后一个元素的下一个位置
	(2) 队列:一般采用循环队列。
		(a) 队头元素位置:指向第一个元素、指向第一个元素的前一个位置。
		(b) 队尾元素位置:指向队尾元素、指向队尾元素的下一个位置。
3. 栈和队列的链式存储结构
4. 栈和队列的应用
	(1) 栈的应用:表达式求值(中缀表达式转后缀表达式、括号匹配)、DFS
	(2) 队列的应用:BFS
5. 考题:2011-2、2011-3、2012-2、2013-2、2014-2、2014-3、2015-1、2016-3、2017-2、2018-1、2018-2、2019-42、2020-2
6. 押题:AcWing 3302

-------------------------------------------------------------------------------------

第4讲 树的基本概念、二叉树、树和森林
1. 树的基本概念
	(1) 树是由根节点和若干颗子树构成的。树是由一个集合以及在该集合上定义的一种关系构成的。集合中的元素称为树的节点,所定义的关系称为父子关系。父子关系在树的节点之间建立了一个层次结构。在这种层次结构中有一个节点具有特殊的地位,这个节点称为该树的根节点,或称为树根。
	(2) 空集合也是树,称为空树。空树中没有节点;
	(3) 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;
	(4) 节点的度:一个节点含有的子节点的个数称为该节点的度;
	(5) 叶节点或终端节点:度为0的节点称为叶节点;
	(6) 非终端节点或分支节点:度不为0的节点;
	(7) 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
	(8) 兄弟节点:具有相同父节点的节点互称为兄弟节点;
	(9) 树的度:一棵树中,最大的节点的度称为树的度;
	(10) 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
	(11) 树的高度或深度:树中节点的最大层次;
	(12) 节点的祖先:从根到该节点所经分支上的所有节点;
	(13) 子孙:以某节点为根的子树中任一节点都称为该节点的子孙;
	(14) 森林:由棵互不相交的树的集合称为森林。
2. 二叉树
	(1) 二叉树的定义及其主要特征
		a. 二叉树的基本形态:空二叉树、单节点二叉树、左子树、右子树
		b. 性质:
			[1] 在非空二叉树中,第i层上至多有2^(i-1) 个结点。
			[2] 深度为k的二叉树至多有2^k - 1个结点
			[3] 对任何一棵二叉树,若其叶子结点数为n0,度为2的结点数为n2,则n0 = n2 + 1[4] n个结点的完全二叉树深度为:log2(n)向下取整 + 1
			[5] 二叉树的堆式存储: 节点p的左儿子:2x,右儿子:2x+1
		c. 两种特殊的二叉树
			[1] 满二叉树:一颗深度为k且有2^k-1个结点的二叉树
			[2] 如果深度为k,有n个结点的二叉树,当且仅当其每个结点都与深度为k的满二叉树中编号从1到n的结点一一对应,该二叉树称为完全二叉树
	(2) 二叉树的顺序存储结构和链式存储结构
	(3) 二叉树的遍历
		a. 前序遍历
		b. 中序遍历
		c. 后序遍历
		d. 根据前序 + 中序重建二叉树(AcWing 18)
	(4) 线索二叉树的基本概念和构造
		对二叉树节点的指针域做如下规定:
			a. 若节点有左孩子,则Lchild指向左孩子,否则指向直接前驱;右孩子同理;
			b. 增加两个标志域,Ltag表示指向的是子节点还是前驱;Rtag同理
			c. 指向前驱和后继的指针叫做线索。按照某种次序遍历,加上线索的二叉树称之为线索二叉树
3. 树、森林
	(1) 树的存储结构
		a. 只存父节点
		b. 邻接表存储所有子节点
		c. 左儿子右兄弟
	(2) 森林F与二叉树T的转换
		a. 原树中叶子节点数 = 转换后的树中有右儿子的节点数 + 1
		b. F的前序遍历就是T的前序遍历
		c. F的后序遍历就是T的中序遍历
	(3) 树和森林的遍历
		a. 前序遍历
		b. 后序遍历
4. 考题:2011-4、2011-5、2011-6、2012-3、2013-5、2014-4、2014-5、2014-41、2015-2、2016-5、2016-42、2017-4、2017-5、2018-4、2019-2、2020-3、2020-4
5. 押题:AcWing 18、AcWing 19

-------------------------------------------------------------------------------------

第5讲 二叉排序树、平衡树、表达式树
1. 二叉排序树
2. 平衡树——AVL
	(1) 定义:满足如下条件的树:
		a. 是二叉查找树
		b. 每个节点的左子树和右子树的高度差最多为1
	(2) 平衡因子:一个结点的左子树的高度减去右子树的高度,可取-1、0、1三种值
	(3) 平衡操作
3. 表达式树
4. 考题:2011-7、2012-4、2013-3(PDF中的分析有误,以上课讲解为准)、2013-6、2015-4、2018-6、2019-4、2019-6、2020-5、2017-41

-------------------------------------------------------------------------------------

第6讲 Huffman编码和Huffman树
1. Huffman编码和Huffman树
	(1) Huffman编码
		a. 前缀编码: 是指对字符集进行编码时,要求字符集中任一字符的编码都不是其它字符的编码的前缀。
		b. 树的带权路径长度(WPL)
		c. 构造过程
	(2) Huffman树
	(3) 应用
2. 考题:2012-41、2013-4、2014-6、2015-3、2017-6、2018-5、2019-3、2020-42

-------------------------------------------------------------------------------------

第7讲 图的基本概念、存储、遍历、拓扑排序
1. 图的基本概念
	(1) 有向图、无向图
	(2) 度数(出度、入度)
	(3) 简单图:不存在顶点到其自身的边,且同一条边不重复出现
	(4) 路径、环、简单路径
	(5) 无向完全图:任意两个顶点之间都存在边,有n个顶点的无向完全图有 n × (n - 1) / 2条边
	(6) 有向完全图:任意两个顶点之间都存在方向护卫相反的两条弧,有n个顶点的无向完全图有 n × (n - 1) 条弧
	(7) 稀疏图&稠密图:有很少条边或弧的图称为稀疏图,反之称为稠密图,相对的概念。
2. 图的存储及基本操作
	(1) 邻接矩阵:适用于稠密图,可存有向图、无向图。常用。空间复杂度:O(n^2)。无法存重边。
	(2) 邻接表:适用于稀疏图,可存有向图、无向图。常用。空间复杂度:O(n + m)。可存重边。
	(3) 邻接多重表,适用于稀疏图,可存无向图。不常用。空间复杂度:O(n + m)。可存重边。
	(4) 十字链表,适用于稀疏图,可存有向图、无向图。不常用。空间复杂度:O(n + m)。无法存重边
	(5) 三元组表,适用于稀疏图,可存有向图,无向图。常用于Bellman-Ford算法、Kruskal算法。空间复杂度:O(m)。可存重边。
3. 图的遍历
	(1) 深度优先搜索。邻接表存储的时间复杂度:O(n + m)。邻接矩阵存储的时间复杂度:O(n^2)
	(2) 广度优先搜索。邻接表存储的时间复杂度:O(n + m)。邻接矩阵存储的时间复杂度:O(n^2)
4. 拓扑排序
5. 考题:2011-8、2012-5、2012-6、2013-7、2013-8、2014-7、2015-5、2016-6、2016-7、2017-3、2017-7、2018-7、2020-6

-------------------------------------------------------------------------------------

第8讲 最小生成树、最短路、关键路径
1. 最小生成树
	(1) Prim
	(2) Kruskal
2. 最短路
	(1) 单源最短路 Dijkstra
	(2) 多源汇最短路 Floyd
3. 关键路径
4. 考题:2011-41、2012-7、2012-8、2013-9、2015-6、2015-42、2016-8、2017-42、2018-42、2019-5、2020-7、2020-8

-------------------------------------------------------------------------------------

第9讲 基本概念、顺序、折半、分块查找法、B/B+树
1. 查找的基本概念
	(1) 平均查找长度 ASL = 每个元素 查找概率 * 找到第i个元素需要进行的比较次数 的和。
	(2) 决策树(判定树)
2. 顺序查找法
	(1) 一般线性表的顺序查找
		a. 若每个元素查找概率相同,则 ASL(成功) = (1 + 2 + ... + n) / n = (n + 1) / 2
		b. ASL(失败) = n或n+1,取决于代码写法。
	(2) 有序表的顺序查找
		a. 若每个元素查找概率相同,则 ASL(成功) = (1 + 2 + ... + n) / n = (n + 1) / 2
		b. ASL(失败) = (1 + 2 + ... + n + n) / (n + 1) = n / 2 + n / (n + 1)
3. 折半查找法
	(1) ASL = log(n + 1) - 1
4. 分块查找法
	设共n个元素,每块s个元素,共b = n / s块。块内无序,块间有序。
	(1) 顺序查找确定块:ASL(成功) = (s^2 + 2s + n) / (2s),s = sqrt(n)时取最小值
	(2) 二分查找确定块:log(n/s + 1) + (s - 1)/2
5. B树及其基本操作、B+树及其基本概念
	(1) B树
		[1] m阶B树,每个节点最多有m个孩子。
		[2] 每个节点最多有m-1个关键字(可以存有的键值对)。
		[3] 根节点最少可以只有1个关键字。
		[4] 非根节点至少有m/2个关键字。
		[5] 每个节点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它。
		[6] 所有叶子节点都位于同一层,或者说根节点到每个叶子节点的长度都相同。
		[7] 每个节点都存有索引和数据,也就是对应的key和value。
		[8] 所以,根节点的关键字数量范围:1 <= k <= m-1,非根节点的关键字数量范围:m/2 <= k <= m-1。
	(2) B+树
		[1] B+跟B树不同B+树的非叶子节点不保存关键字记录的指针,只进行数据索引,这样使得B+树每个非叶子节点所能保存的关键字大大增加;
		[2] B+树叶子节点保存了父节点的所有关键字记录的指针,所有数据地址必须要到叶子节点才能获取到。所以每次数据查询的次数都一样;
		[3] B+树叶子节点的关键字从小到大有序排列,左边结尾数据都会保存右边节点开始数据的指针。
	(3) 参考链接:https://blog.csdn.net/Fmuma/article/details/80287924
6. 考题:2011-42、2012-9、2013-10、2013-42、2014-9、2015-7、2016-9、2016-10、2017-8、2017-9、2018-8、2020-10

-------------------------------------------------------------------------------------

第10讲 散列(Hash)表、字符串模式匹配(KMP)
1. 散列(Hash)表
	(1) 负载因子
	(2) 哈希函数
		[1] 除余法 h(x) = x % M
		[2] 乘余取整法 h(x) = floor(n * (A * x的小数部分))
		[3] 平方取中法 先平方,然后取中间几位
		[4] 基数转换法 换成其他进制,然后取其中几位
		[5] ELFhash字符串
	(3) 解决冲突的方式
		[1] 开散列方法(拉链法)
		[2] 闭散列方法(开放寻址法)
			聚集和二级聚集
			a. 线性探查法 d(i) = (d(0) + i * c) % M。易产生聚集问题。
			b. 二次探查法。易产生二级聚集问题。
				d(2i - 1) = (d(0) + i^2) % M
				d(2i) = (d(0) - d ^2) % M
			c. 随机探查法。易产生二级聚集问题。
			d. 双散列探查法
2. 字符串模式匹配(KMP)
3. 考题:2011-9、2014-8、2015-8、2018-9、2018-41、2019-8、2019-9

-------------------------------------------------------------------------------------

第11讲 基本概念、插入、冒泡、选择、希尔、快速、堆、归并排序
1. 排序的基本概念
	(1) 内排序和外排序
	(2) 算法的稳定性
2. 插入排序
	(1) 直接插入排序
		a. 时间复杂度
			[1] 最好情况:O(n)
			[2] 平均情况:O(n^2)
			[3] 最坏情况:O(n^2)
		b. 辅助空间复杂度
			O(1)
		c. 稳定
	(2) 折半插入排序
		a. 时间复杂度
			[1] 最好情况:O(n)
			[2] 平均情况:O(n^2)
			[3] 最坏情况:O(n^2)
		b. 辅助空间复杂度
			O(1)
		c. 稳定
3. 冒泡排序(bubble sort)
	(1) 时间复杂度
		a. 最好情况:O(n)
		b. 平均情况:O(n^2)
		c. 最坏情况:O(n^2)
	(2) 空间复杂度
		O(1)
	(3) 稳定
4. 简单选择排序
	(1) 时间复杂度
		a. 最好情况:O(n^2)
		b. 平均情况:O(n^2)
		c. 最坏情况:O(n^2)
	(2) 空间复杂度
		O(1)
	(3) 不稳定
5. 希尔排序(shell sort)
	(1) 时间复杂度
		O(n^(3/2))
	(2) 空间复杂度
		O(1)
	(3) 不稳定
6. 快速排序
	(1) 时间复杂度
		a. 最好情况:O(nlogn)
		b. 平均情况:O(nlogn)
		c. 最坏情况:O(n^2)
	(2) 空间复杂度
		O(logn)
	(3) 不稳定
7. 堆排序
	(1) 时间复杂度
		a. 最好情况:O(nlogn)
		b. 平均情况:O(nlogn)
		c. 最坏情况:O(nlogn)
	(2) 空间复杂度
		O(logn)
	(3) 不稳定
8. 二路归并排序(merge sort)
	(1) 时间复杂度
		a. 最好情况:O(nlogn)
		b. 平均情况:O(nlogn)
		c. 最坏情况:O(nlogn)
	(2) 空间复杂度
		O(n)
	(3) 稳定

-------------------------------------------------------------------------------------

第12讲 桶排序、基数排序、外部排序
1. 桶排序
	(1) 时间复杂度
		a. 最好情况:O(n + m)
		b. 平均情况:O(n + m)
		c. 最坏情况:O(n + m)
	(2) 空间复杂度
		O(n + m)
	(3) 稳定
2. 基数排序
	(1) 时间复杂度
		a. 最好情况:O(d(n + r))
		b. 平均情况:O(d(n + r))
		c. 最坏情况:O(d(n + r))
	(2) 空间复杂度
		O(n + r)
	(3) 稳定
3. 外部排序
	(1) 置换选择排序
	(2) 归并排序
		a. 胜者树
		b. 败者树
		c. huffman树
4. 考题:2011-10、2011-11、2012-10、2012-11、2013-11、2014-10、2014-11、2015-9、2015-10、2015-11、2016-11、2016-43、2017-10、2017-11、2018-10、2018-11、2019-7、2019-10、2019-11、2020-9、2020-11、2020-41


-------------------------------------------------------------------------------------


第21讲 红黑树和并查集
1. 红黑树
	1.1 定义
		(1) 结点是红色或黑色。
		(2) 根结点是黑色。
		(3) 所有叶子都是黑色。(叶子是NIL结点[NULL空结点](4) 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
		(5) 从任一节结点其每个叶子的所有路径都包含相同数目的黑色结点。
	1.2 性质
		从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。
	1.3 平衡操作
		1.3.1 插入
			1.3.1.1 被插入的节点是根节点。
				直接把此节点涂为黑色。
			1.3.1.2 被插入的节点的父节点是黑色。
				什么也不需要做。
			1.3.1.3 被插入的节点的父节点是红色。
				[1] 当前节点的祖父节点的另一个子节点(叔叔节点)也是红色。
					(1) 将“父节点”设为黑色。
					(2) 将“叔叔节点”设为黑色。
					(3) 将“祖父节点”设为“红色”。
					(4) 将“祖父节点”设为“当前节点”(红色节点);即,之后继续对“当前节点”进行操作。
				[2] 叔叔节点是黑色,且当前节点是其父节点的右孩子
					(1) 将“父节点”作为“新的当前节点”。
					(2) 以“新的当前节点”为支点进行左旋。
				[3] 叔叔节点是黑色,且当前节点是其父节点的左孩子
					(1) 将“父节点”设为“黑色”。
					(2) 将“祖父节点”设为“红色”。
					(3) 以“祖父节点”为支点进行右旋。
		1.3.2 删除
			1.3.2.1 x指向一个"红+黑"节点。
				将x设为一个"黑"节点即可。
			1.3.2.2 x指向根。
				将x设为一个"黑"节点即可。
			1.3.2.3 
				[1] x的兄弟节点是红色。
					(1) 将x的兄弟节点设为“黑色”。
					(2) 将x的父节点设为“红色”。
					(3) 对x的父节点进行左旋。
					(4) 左旋后,重新设置x的兄弟节点。
				[2] x的兄弟节点是黑色,x的兄弟节点的两个孩子都是黑色。
					(1) 将x的兄弟节点设为“红色”。
					(2) 设置“x的父节点”为“新的x节点”。
				[3] x的兄弟节点是黑色;x的兄弟节点的左孩子是红色,右孩子是黑色的。
					(1) 将x兄弟节点的左孩子设为“黑色”。
					(2) 将x兄弟节点设为“红色”。
					(3) 对x的兄弟节点进行右旋。
					(4) 右旋后,重新设置x的兄弟节点。
				[4] x的兄弟节点是黑色;x的兄弟节点的右孩子是红色的,x的兄弟节点的左孩子任意颜色。
					(1) 将x父节点颜色 赋值给 x的兄弟节点。
					(2) 将x父节点设为“黑色”。
					(3) 将x兄弟节点的右子节设为“黑色”。
					(4) 对x的父节点进行左旋。
			
					
2. 并查集
	2.1 定义
		用森林维护集合关系,支持合并、查询等操作。
	2.2 优化
		(1) 路径压缩
		(2) 按秩合并
		

1.排序与进位制

3375. 成绩排序

给定学生的成绩单,成绩单中包含每个学生的姓名和分数,请按照要求将成绩单按成绩从高到低或从低到高的顺序进行重新排列。

对于成绩相同的学生,无论以哪种顺序排列,都要按照原始成绩单中靠前的学生排列在前的规则处理。

输入格式
第一行包含整数 N,表示学生个数。

第二行包含一个整数 0 或 1,表示排序规则,0 表示从高到低,1 表示从低到高。

接下来 N 行,每行描述一个学生的信息,包含一个长度不超过 10 的小写字母构成的字符串表示姓名以及一个范围在 0∼100 的整数表示分数。

输出格式
输出重新排序后的成绩单。

每行输出一个学生的姓名和成绩,用单个空格隔开。

数据范围
1≤N≤1000
输入样例1:
4
0
jack 70
peter 96
Tom 70
smith 67
输出样例1:
peter 96
jack 70
Tom 70
smith 67
输入样例2:
4
1
jack 70
peter 96
Tom 70
smith 67
输出样例2:
smith 67
jack 70
Tom 70
peter 96

	此题需要保持稳定排序【实则可为多关键字排序hh】
		stable_sort(); //归并实现,稳定排序   
		【 若sort用多关键字排序,值相同时比较下标 -->也可以变成稳定排序  】 
		greater<类型名>()  :指代用大于号'>'  从大到小排序 
		const是好习惯(也可以不加)第一个是不改变t,第二个是不改变Person内的成员变量
		【应该排在t的前面,返回true

stable_sort
稳定排序:相同元素的相对顺序不发生编号

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;

const int N = 1010;
int n,m;

struct Person
{
	string name;
	int score;
	
    bool operator< (const Person &t)const   //记:xx应该排在t的前面,返回true
	{
		return score < t.score;
	}
	
	bool operator> (const Person &t)const 
	{
		return score > t.score;
	}
	
}q[N];

int main() {
	
	cin >> n >> m;
	for(int i = 0;i < n;i++)
	{
		cin >> q[i].name >> q[i].score;
	}
	
	if(!m) stable_sort(q, q + n , greater<Person>() ) ; //m == 0按从大到小排序 
	else stable_sort(q , q + n);  
	
	for(int i = 0;i < n;i++)
		cout << q[i].name <<" "<< q[i].score << endl;//string
	  
	return 0;
}

sort

#include <iostream>
#include<cstring>
#include<algorithm>
using namespace std;

const int N = 1010;

int n , m;
struct Person
{
	string name;
	int score;
	int id;
		
	bool operator< (const Person &t) const 	  //operator<重载小于号
	{
		if(score != t.score) return score < t.score;
		return id < t.id; //score相同,用id排序 [无论从大到小还是从小到大,score相同时id小的在前]   
	}
	bool operator> (const Person &t) const 		
	{
		if(score != t.score) return score > t.score;
		return id < t.id;   //score相同,用id排序 [无论从大到小还是从小到大,score相同时id小的在前]   
	}
	
}q[N]; 

int main() {
	
	cin >> n >> m;
	for(int i = 0;i < n;i++)
	{
		cin >> q[i].name >> q[i].score;
		q[i].id = i;
	}
	
	if(!m) sort(q, q + n , greater<Person>() ) ; //m == 0按从大到小排序 
	else sort(q , q + n);  
	
	for(int i = 0;i < n;i++) 
		cout << q[i].name <<" "<< q[i].score << endl;//string
	  
	return 0;
}

比较函数cmp

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

const int N = 1010;
int n, m;

struct Person
{
	string name;
	int score;

}q[N];

bool cmp1(Person x, Person y)
{
    return x.score < y.score;
}

bool cmp2(Person x, Person y)
{
    return x.score > y.score;
}

int main() {
	
	cin >> n >> m;
	for(int i = 0;i < n;i++)
		cin >> q[i].name >> q[i].score;
	
	if(!m) stable_sort(q, q + n , cmp2) ; //m == 0按从大到小排序 
	else stable_sort(q , q + n, cmp1);  
	
	for(int i = 0;i < n;i++) 
		cout << q[i].name <<" "<< q[i].score << endl; //string
	  
	return 0;
}

加上id-双关键字排序【最简版】

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;

const int N = 1010;
int n,m;

struct Stu
{
	string name;
	int score;
	int id; //多关键字排序

}s[N];


bool cmp(Stu x, Stu y) //自定义规则-多关键字排序
{
    if(m == 0)//m == 0按从大到小排序 
    {
        if(x.score == y.score) return x.id < y.id;
        return x.score > y.score; //从大到小
    }
    else 
    {
        if(x.score == y.score) return x.id < y.id;
        return x.score < y.score; //从小到大
    }
}

int main() {
	
	cin >> n >> m;
	for(int i = 0;i < n;i++)
		cin >> s[i].name >> s[i].score, s[i].id = i; //注意按顺序初始化id
	
	sort(s, s + n, cmp);
	
	for(int i = 0;i < n;i++)
		cout << s[i].name <<" "<< s[i].score << endl;
	  
	return 0;
}

3376. 成绩排序2

给定学生的成绩单,成绩单中包含每个学生的学号和分数,请将成绩单按成绩从低到高的顺序重新排序。

如果学生的成绩相同,则按照学号从小到大的顺序进行排序。

输入格式
第一行包含整数 N,表示学生数量。

接下来 N 行,每行包含两个整数 p 和 q,表示一个学生的学号和成绩。

学生的学号各不相同。

输出格式
输出重新排序后的成绩单。

每行输出一个学生的学号和成绩,用单个空格隔开。

数据范围
1≤N≤100,
1≤p≤100,
0≤q≤100
输入样例:
3
1 90
2 87
3 92
输出样例:
2 87
1 90
3 92

简记:按从小到大多关键字排序 ,结构体 + 重载小于号    
#include<iostream>
#include<algorithm>
using namespace std;

const int N = 1010;
int n;
struct Person
{
	int id;
	int score;
	
	bool operator< (const Person &t)const
	{
		if(score != t.score) return score < t.score;
		return id < t.id;
	}
}q[N];
 

int main()
{
	cin >> n;
	for(int i = 0;i < n;i++) cin >> q[i].id >> q[i].score ;
	
	sort(q , q + n);	
	
	for(int i = 0;i < n;i++) cout << q[i].id << " " << q[i].score << endl;
	
	return 0;
}
 

奇特版:

#include <bits/stdc++.h>

#define x first
#define y second

using namespace std;

typedef pair<string, int> PSI;

const int N = 1010;

int n, op;
PSI stu[N];

bool cmp1(PSI stu1, PSI stu2)
{
    return stu1.y < stu2.y;    
}

bool cmp2(PSI stu1, PSI stu2)
{
    return stu1.y > stu2.y;
}

int main()
{
    cin >> n >> op;

    for(int i = 0; i < n; i ++) cin >> stu[i].x >> stu[i].y;

    /*stable_sort:稳定排序 VS sort:非稳定排序*/
    if(op == 1) stable_sort(stu, stu + n, cmp1);
    else stable_sort(stu, stu + n, cmp2);

    for(int i = 0; i < n; i ++) cout << stu[i].x << ' ' << stu[i].y << endl;

    return 0;
}


//作者:我呼吸了 链接:https://www.acwing.com/solution/content/131382/

3373. 进制转换

将一个长度最多为 30 位数字的十进制非负整数转换为二进制数输出。

输入格式
输入包含多组测试数据。

每组测试数据占一行,包含一个长度不超过 30 位的十进制非负整数。

输出格式
每组数据输出一个结果,占一行,为输入对应的二进制数。

数据范围
输入最多包含 100 组测试数据。

输入样例:
0
1
3
8
输出样例:
0
1
11
1000

简记:除基取余法+  高精度除法 : 10进制除其他进制 
		while(A.size())  //除基取余法                          
		{
			res += to_string(A[0] % 2);    //to_string函数   
			A = div(A,2);  	
		}	
#include<iostream>
#include<algorithm>
#include<string>
#include<vector>
using namespace std;

vector<int> div(vector<int> A , int b) //高精度除法模板-余数r不需要计算
{
	vector<int> C;
	for(int i = A.size() - 1,r = 0; i >= 0; i--)
	{
		r = r * 10 + A[i]; //上一高位的余数+当前位 组合成的数 
		C.push_back(r / b);  //存每位的商 
		r %= b;  //r为余数 
	}
	reverse(C.begin() , C.end()); //余数逆序  翻转为顺序 
	while(C.size() && C.back() == 0) C.pop_back();	  //若非0[C.size() > 1]且除的过程中有前导0(删去前导0高位)
	return C; 
} 

int main()
{
	string s; //字符串输入 
	while(cin >> s)
	{
		vector<int> A;
		for(int i = 0;i < s.size();i ++)
			A.push_back(s[s.size() - i - 1] - '0'); //逆序放入
		
		string res;
		if(s == "0") res = "0"; //为0
		else 
		{
			while(A.size())  //除基取余法                          
			{
				res += to_string(A[0] % 2);    //to_string函数   
				A = div(A,2);//数组除法 高精度模板   	
			}	
		}	

	reverse(res.begin() , res.end());
	cout << res << endl;
	}
	
	return 0;
}



//什么大佬!  好吧:模板罢了
while True:
    try:
        number = int(input())
        list1 = []
        if number == 0:
            print(number,end ='')
        while number != 0:
            remainder = number % 2
            number //= 2
            list1.append(str(remainder))
        list2 = list(reversed(list1))
        print("".join(list2))
    except:
        break


//作者:清风无忧 链接:https://www.acwing.com/solution/content/54733/





#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

bool judge(vector<int> &a){
    for(auto x:a){
        if(x>0) return true;
    }
    return false;
}

vector<int> div(vector<int> &A,int &r)  // A / b = C ... r, A >= 0, b > 0
{
    int b=2;
    vector<int> C;
    for (int i = A.size() - 1; i >= 0; i -- )
    {
        r = r * 10 + A[i];
        C.push_back(r / b);
        r %= b;
    }
    reverse(C.begin(), C.end());
    while (C.size() > 1 && C.back() == 0) C.pop_back();
    return C;
}


string solve(string a){
    string b="";
    vector<int> s;
    for(int i=a.size()-1;i>=0;i--) s.push_back(a[i]-'0');

    while(judge(s)){
        int r=0;
        s=div(s,r);
        b+=char(r+'0');
    }
    reverse(b.begin(),b.end());
    if(b=="") return "0";
    return b;
}

int main(){
    string a;
    while(cin>>a){
        cout<<solve(a)<<endl;
    }
    return 0;
}


//作者:Tilbur 链接:https://www.acwing.com/solution/content/55099/




3374. 进制转换2

将 M 进制的数 X 转换为 N 进制的数输出。

输入格式
第一行包括两个整数:M 和 N。

第二行包含一个数 X,X 是 M 进制的数,现在要求你将 M 进制的数 X 转换成 N 进制的数输出。

输出格式
共一行,输出 X 的 N 进制表示。

数据范围
2≤N,M≤36,
X 最多包含 100 位。
在输入中,当某一位数字的值大于 10(十进制下)时,我们用大写字母 A∼Z,分别表示(十进制下的)数值 10∼35。
在输出中,当某一位数字的值大于 10(十进制下)时,我们用小写字母 a∼z,分别表示(十进制下的)数值 10∼35。

输入样例:
10 2
11
输出样例:
1011

M进制转换为N进制
M进制的高精度除法 + 除基取余法【模块化】
思路:m进制转10进再转n进制,但采取逐位转换 制(注意 " / " "/" "/" " % " "\%" "%"计算规则都是10进制)
【否则分步转换①m->10 + ②10->n需要高精度add,mul,div】

函数分解版

#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

int n, m;
string s;
vector<int> A;

char get(int x) //int整数(偏移量) ---> char进制数(逐位)
{
    if(x <= 9) return x + '0';
    return x - 10 + 'a'; //输出要求小写a-z  【a/A偏移量 == 10】
}

int get_num(char c) //char进制数(逐位) ---> int整数(偏移量)
{
    if(c >= '0' && c <= '9') return c - '0';
    return c - 'A' + 10; //【题目输入大于10进制为A-Z】 
}

// M进制数除法:对应秦九韶: x = x * m + A[i];
int div(vector<int> &A, int n, int r) 
{
    vector<int> C;
    for(int i = A.size() - 1; i >= 0; i --)  //[秦九韶]M进制版
    {
        r = r * m + A[i];    // 【转10进制】 上一位余数乘 m进制 加当前位         
        C.push_back(r / n); // 【转n进制】  放入本位商                    
        r = r % n; //余数r
    }

    reverse(C.begin(), C.end()); //翻转
    while(C.size() && C.back() == 0) C.pop_back(); //C.size() > 1会TLE。。。
    A = C;  //最终结果C赋值给A

    return r;
}

int main()
{
    cin >> m >> n >> s; //M进制转N进制, M进制数为s
    for(int i = s.size() - 1; i >= 0; i --) A.push_back(get_num(s[i]));

    int r = 0; //余数                         
    string res = "";
    while(A.size())  //除基取余法[余数逆序拼接——进制数]
    {
        r = div(A, n, 0); //每轮初始余数0, r为每位的余数
        res = get(r) + res; //逆序存放
        // print(A);调试
    }

    cout << res << endl;

    return 0;
}

// void print(vector<int> &A)//调试
// {
//     for(int i = A.size() - 1;i >= 0;i --) cout << A[i];
//     cout << endl;
// }

main简化版

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

int main()
{
    int a, b;
    string s;
    cin >> a >> b >> s;  //a进制的数s转换成b进制   (s指用字符串保存) 
    vector<int> A;
    for (int i = 0; i < s.size(); i ++ )
    {
        char c = s[s.size() - 1 - i];    //大于十的进制(字母表示) --> 转换成数值
        if (c >= 'A') A.push_back(c - 'A' + 10);  //ASCLL  48-57 :0-9  , 65-97  A->Z ->a
        else A.push_back(c - '0');
    }
    string res;
    if (s == "0") res = "0"; //为0输出0
    else
    {
        while (A.size())      //循环高精度除法过程 ,但是为A进制除其他进制【原为10进制除其他进制】
        {
            int r = 0;
            for (int i = A.size() - 1; i >= 0; i -- )  
            {
                A[i] += r * a;  //!唯一区别 a进制的余数*a + 当前位
                r = A[i] % b;
                A[i] /= b;
            }
            while (A.size() && A.back() == 0) A.pop_back();
            if (r < 10) res += to_string(r);   
            else res += r - 10 + 'a';
        }
        reverse(res.begin(), res.end());
    }
    cout << res << endl;

    return 0;
}

2.链表、日期问题

链表不要背代码:按步骤画图翻译成代码
单链表:

简记:
	Node() : next(NULL) { }  // 构造函数
	Node(int _val) :val(_val) ,next(NULL) { } 
	void print(Node* head)
			for(auto p = head ; p ;p = p->next)

#include<iostream>

using namespace std;

struct Node
{
	int val;
	Node * next;
	
	//构造函数:更好写代码
	Node() : next(NULL) { } 
	Node(int _val) :val(_val) ,next(NULL) { }  //new Node创建新结点:传入参数_val ,赋值给val , 同时next = NULL 
	  
};

void print(Node* head)
{
	for(auto p = head ; p ;p = p->next)
		printf("%d ",p->val);
	puts("");
}

int main()
{
	Node* head = new Node(1); 
	auto a = new Node(2);
	head->next = a;
	
	auto b = new Node(3);
	a->next = b; 
	
	auto c = new Node(4);  //将4插入到1(头结点)后面 
	c->next = head->next;//注意顺序不能交换  !!! 
	head->next = c;
	
	
	
	
	
	//删除头结点  ,先存下来   !!!一定要释放删除元素 ,否则扣分!!! 
	auto p = head; 
	head = head->next;
	delete p; //释放内存 
	
	print(head);
	
	return 0;
 } 
 

循环双链表:
【不要死记硬背 ,用图翻译代码】

简记:Node() : prev(NULL) ,next(NULL) { } 
 	  Node(int _val) :val(_val) ,prev(NULL) ,next(NULL) { }  //new Node创建新结点:传入参数_val ,赋值给val , 同时next = NULL 
	  void print(Node* head) 
			for(auto p = head->next; p != head ;p = p->next) 
		Node *head  = new Node() , *tail = head;   构造循环双链表简单【仅需tail与head指针指向同一个点】;new头结点可以不赋值 
		head->next = tail , tail->prev = head ; 初始空,头尾指针互指

//双链表 
#include<iostream>

using namespace std;

struct Node
{
	int val;
	Node *prev , * next;  //双链表 
	
	//构造函数
	Node() : prev(NULL) ,next(NULL) { } 
	Node(int _val) :val(_val) ,prev(NULL) ,next(NULL) { }  //new Node创建新结点:传入参数_val ,赋值给val , 同时next = NULL 
	  
}; 

void print(Node* head) //循环双链表打印 
{
	for(auto p = head->next; p != head ;p = p->next)  //next哨兵,不存元素 , 从next开始走,走回next停止 【循环双链表】 
		printf("%d ",p->val);
	puts("");
}

int main()
{
	//初始化双链表  Node*类型指针 [哨兵,左右护法,不会用到值,判断边界]
	Node *head  = new Node() , *tail = head;   //构造循环双链表简单【仅需tail与head指针指向同一个点】;new头结点可以不赋值 ,但编译器问题NULL = 13703728 
	head->next = tail , tail->prev = head ; //空,头尾指针互指 
	
	//print(head);
	
	auto a = new Node(1); //加入新结点    [头插法不要用tail,很多节点后在十万八千里]
	a->next = head->next , a->prev = head;    //顺序!! 【先把新结点的指针连好,再把头尾指向新结点】 
	head->next = a , tail->prev = a; 
	
	//print(head);
	
	auto b = new Node(2);
	b->next = a->next , b->prev = a;  //b->a , b->a->next  [插入a后面]   
	a->next = b, head->next = a;  //双向互指  a更改前后驱
	
	print(head); // 1 2
	
	//删除b 【用b指针】 
	b->prev->next = b->next;  //[前驱的后指针指向后后,后驱的前指针->prev指向前前,]
	b->next->prev = b->prev;
	delete b; 
	
	print(head); // 1
	
	auto c = new Node(3); //c插入a的前面 (头插法)  [此时:head->next = a , a->prev = head;]
	c->next = a , c->prev = head; 
	head->next = c, a->prev = c; 
	
	print(head); // 3 1
	
	return 0;
 } 



66. 两个链表的第一个公共结点

输入两个链表,找出它们的第一个公共结点。

当不存在公共节点时,返回空节点。

数据范围
链表长度 [1,2000]。

样例

给出两个链表如下所示:
A:        a1 → a2
                   ↘
                     c1 → c2 → c3
                   ↗            
B:     b1 → b2 → b3

输出第一个公共节点c1

sizeof(a) == sizeof(b) 【那么走到sizeof a 的长度处就相遇了】
或sizeof(a) != sizeof(b) 【a走 a->c->b (接着往b走) , b走 b->c->a (接着往a走) ,走的长度都为 a+b+c】
发现均第一次相遇(p,q指针指向值相等)在公共节点【得证】

//y总
     【注释为数据结构定义,考研要写】
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *findFirstCommonNode(ListNode *headA, ListNode *headB) {
        auto p = headA, q = headB; 
        while (p != q) { //直到p == q 停止
            p = p ? p->next : headB; //p非空往下走,否则(走到末尾)再往B走
            q = q ? q->next : headA; 
        }
        return p;
    }
};


//作者:yxc 链接:https://www.acwing.com/activity/content/code/content/1405427/




//方法1:不同部分为a, 和b,公共部分为c;a + c + b = b + c + a;让两个一起走,a走到头就转向b, b走到头转向a,则在公共部分相遇。
//yyds 看成走路线相遇在公共点
class Solution {
public:
    ListNode *findFirstCommonNode(ListNode *headA, ListNode *headB) {
        auto p = headA, q = headB;
        while(p != q) {
            if(p) p = p->next;
            else p = headB;
            if (q) q = q->next;
            else q = headA;
        }
        return p;
    }
};


//方法2:先计算出两个链表的长度,可以让比较长的先走两个链表长度之差的步数,两个再一起走就同时到公共节点。


class Solution {
public:
    ListNode *findFirstCommonNode(ListNode *headA, ListNode *headB) {
        auto p = headA, q = headB;
        int la = 0, lb = 0;
        for (auto t = headA; t; t = t->next) la ++;
        for (auto t = headB; t; t = t->next) lb ++;
        int k = la - lb;
        if (la < lb) {
            p = headB, q = headA;
            k = lb - la;
        }
        while(k --) {
            p = p->next;
        }
        while(p) {
            if (p == q) return p;
            p = p->next;
            q = q->next;
        }
        return nullptr;
    }
};

3756. 筛选链表

一个单链表中有 m 个结点,每个结点上的元素的绝对值不超过 n。

现在,对于链表中元素的绝对值相等的结点,仅保留第一次出现的结点而删除其余绝对值相等的结点。

请输出筛选后的新链表。

例如,单链表 21 -> -15 -> -15 -> -7 -> 15,在进行筛选和删除后,变为 21 -> -15 -> -7。

输入样例:
输入:21->-15->-15->-7->15

输出:21->-15->-7
数据范围
1≤m≤1000,
1≤n≤10000

/**
 * Definition for singly-linked list. //单链表的定义
 * struct ListNode { //结构体
 *     int val;
 *     ListNode *next;          
 *     ListNode(int x) : val(x), next(NULL) {}             //构造函数
 * };
 */
class Solution {
public:
    ListNode* filterList(ListNode* head) {
         bool st[10001] = {};//局部变量初始值
        st[abs(head->val)] = true; //绝对值标记访问
        for (auto p = head; p->next;) {
            int x = abs(p->next->val);//y总简写
            if (st[x]) {//有访问过的,就删除节点     (这样释放不了节点 p->next = p->next->next ; 所以要用q指针释放) 
                auto q = p->next;
                p->next = q->next;
                delete q;
            } else {
                p = p->next; //继续遍历,标记
                st[x] = true;
            }
        }
        return head;
    }
};
 

3757. 重排链表

一个包含 n 个元素的线性链表 L=(a1,a2,…,an−2,an−1,an)。

现在要对其中的结点进行重新排序,得到一个新链表 L′=(a1,an,a2,an−1,a3,an−2…)

样例1:
输入:1->2->3->4

输出:1->4->2->3
样例2:
输入:1->2->3->4->5

输出:1->5->2->4->3

数据范围
1≤n≤1000,
1≤ai≤10000
需再看和理解:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    void rearrangedList(ListNode* head) {
        if (!head->next) return;
        int n = 0;
        for (auto p = head; p; p = p->next) n ++ ;
        int left = (n + 1) / 2;  // 前半段的节点数    【对半分】    
        auto a = head;
        for (int i = 0; i < left - 1; i ++ ) a = a->next;    
        auto b = a->next, c = b->next;
        
        a->next = b->next = NULL; // 反转后半部分的链表
        while (c) {
            auto p = c->next;
            c->next = b;
            b = c, c = p;
        }
        // 将前后两半部分的链表相结合
        for (auto p = head, q = b; q;) {
            auto o = q->next;
            q->next = p->next;
            p->next = q;
            p = p->next->next;
            q = o;
        }
    }
};
 



/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    void rearrangedList(ListNode* head) {
        if (!head->next) return;

        // 要对链表进行分段,那怎么求链表长度呢?答: 遍历一遍
        int len = 0;
        for (auto i = head; i; i = i->next) len ++;
        int left = (len + 1) / 2;

        // 开始分段,目标:需要找到第二段头指针
        auto a = head;
        for (int i = 1; i < left; i ++) a = a->next;
        auto b  = a->next;

        // 反转链表,要设置前后两个指针bc,因为要把箭头方向改变,故而还得需要有个tmp来存储一下c->next
        auto c = b->next;
        a->next = b->next = NULL;   // 两段末尾处要拿空指针堵上
        while (c)
        {
            auto tmp = c->next; // 为什么tmp不是存储c呢?
            // 因为反转之后才需要bc往前移动嘛,而c指针有2个next,所以要提前把bc应该移动到的指针存下来
            c->next = b;
            b = c;
            c = tmp;
        }

        // 合并链表
        for (auto p = head, q = b; q;)
        {
            auto u = p->next, v = q->next;
            q->next = u;
            p->next = q;
            // q->next = u; // 插入元素,因为把p、q后面指针保存下来了,
            // 也就不必在插入的时候需要先连后面的链,再连前面的链
            p = u, q = v;
        }
    }
};
 

3607. 打印日期

给出年份 y 和一年中的第 d 天,算出第 d 天是几月几号。

输入格式
输入包含多组测试数据。

每组数据占一行,包含两个整数 y 和 d。

输出格式
每组数据输出一行一个结果,格式为 yyyy-mm-dd。

数据范围
输入最多包含 100 组数据,
1≤y≤3000,
1≤d≤366,
数据保证合法。

输入样例:
2000 3
2000 31
2000 40
2000 60
2000 61
2001 60
输出样例:
2000-01-03
2000-01-31
2000-02-09
2000-02-29
2000-03-01
2001-03-01

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int months[] = {0,31,28,31,30,31,30,31,31,30,31,30,31};

int is_leap(int year) //二月是否加1  :闰年返回1 , 平年返回0
{
    if( year % 4 == 0 && year % 100  || year % 400 == 0)  
        return 1;
    return 0;
}

int get_days(int y,int m) 
{
   if(m == 2)return months[m] + is_leap(y); //若2月,需判断闰年
   return months[m];
}



int main()
{
    int y,s;
    while(cin >> y >> s)
    {
        int m = 1,d = 1;
        s --;
        while(s --)
        {
            if(++ d > get_days(y,m))   //超过归一【类似取余:平年d %= 365或闰年d %= 366 】
            {
                d = 1;  
                if(++m > 12)
                {
                    m = 1;
                    y ++ ;
                }
            }
        }
        printf("%04d-%02d-%02d\n",y,m,d);        
    }
    
    return 0;
}





抽风版:

#include<bits/stdc++.h>
using namespace std;
int a[13]={0,31,28,31,30,31,30,31,31,30,31,30,31};
int main()
{
    int y,d;
    while(cin>>y>>d){
        if(y%4==0&&y%100!=0||y%400==0)a[2]=29;else a[2]=28;
        int m=0;
        while(d>0){
            if(d-a[m]<=0)break;
            else{
                d-=a[m];
                m++;
            }
        }
        printf("%04d-%02d-%02d\n",y,m,d);
    }
}


//作者:垫底抽風 链接:https://www.acwing.com/solution/content/132761/

3573. 日期累加

设计一个程序能计算一个日期加上若干天后是什么日期。

输入格式
第一行包含整数 T,表示共有 T 组测试数据。

每组数据占一行,包含四个整数 y,m,d,a,分别表示给定日期的年、月、日和累加的天数。

输出格式
每组数据输出一行,一个结果,每行按 yyyy-mm-dd 的格式输出。

数据范围
1≤T≤1000
1000≤y≤3000,
1≤m≤12,
1≤d≤31,
1≤a≤106,
保证输入日期合法。

输入样例:
1
2008 2 3 100
输出样例:
2008-05-13

get_days() 特判二月份:若 闰年 d += month[2] + is_leap(y);

//s-- //1号也算1天 
//背is_leap(y)  平年: return 0 , 闰年 :return 1	
//get_days()  特判二月份:若 闰年 d +=  month[2] + is_leap(y);
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

//平年每月的天数
const int months[] = {
  0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
};

//判断是否是闰年
bool is_leap(int y)
{
    if(y % 400 == 0 || (y % 100 && y % 4 == 0)) return true;
    return false;
}

//获得y年第m月的天数
int get_days(int y, int m)
{
    if(m == 2) return months[m] + is_leap(y);
    return months[m];
}

//获得y年第m月--y + 1年第m月的的天数
int get_year_days(int y, int m)
{
    if(m <= 2) return (365 + is_leap(y));
    return (365 + is_leap(y + 1));
}

int main()
{
    int T;
    cin >> T;
    while (T -- )
    {
        int y, m, d, a;
        cin >> y >> m >> d >> a;
        if (m == 2 && d == 29) a --, m = 3, d = 1;
        while (a > get_year_days(y, m))
        {
            a -= get_year_days(y, m);
            y ++ ;
        }
        while (a -- )
        {
            if ( ++ d > get_days(y, m))
            {
                d = 1;
                if ( ++ m > 12)
                {
                    m = 1;
                    y ++ ;
                }
            }
        }
        printf("%04d-%02d-%02d\n", y, m, d);
    }

    return 0;
} 

3.表达式求值

3302. 表达式求值

给定一个表达式,其中运算符仅包含 +,-,*,/(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。

注意:

数据保证给定的表达式合法。
题目保证符号 - 只作为减号出现,不会作为负号出现,例如,-1+2,(2+2)*(-(1+1)+2) 之类表达式均不会出现。
题目保证表达式中所有数字均为正整数。
题目保证表达式在中间计算过程以及结果中,均不超过 231−1。
题目中的整除是指向 0 取整,也就是说对于大于 0 的结果向下取整,例如 5/3=1,对于小于 0 的结果向上取整,例如 5/(1−4)=−1。
C++和Java中的整除默认是向零取整;Python中的整除//默认向下取整,因此Python的eval()函数中的整除也是向下取整,在本题中不能直接使用。
输入格式
共一行,为给定表达式。

输出格式
共一行,为表达式的结果。

数据范围
表达式的长度不超过 105

输入样例:
(2+2)*(1+1)
输出样例:
8

表达式求值【正常数学计算表达式书写顺序】

简记 : eval计算函数 , op,num栈 , 定义优先级hash表,'(‘与’)'特判 :重点优先级分支处理
最后处理剩余运算符,最终结果存在num栈顶

易错eval()第二个操作数b先出栈,第一个操作数a后出栈



#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_map>//三个参数key,value,类(一般省略)
#include <stack>

using namespace std;

stack<char> op;//运算符栈
stack<int> num;//数值栈

void eval()//计算函数 【弹栈op一个c,num两个a,b: 操作数a 运算符c 操作数b  根据运算符判断运算方式 计算结果x存回数值栈 】
{
	auto b = num.top(); num.pop();//易错点:第二个操作数先出栈 
    auto a = num.top(); num.pop();//易错点:第一个操作数后出栈 
    auto c = op.top(); op.pop();//运算符

    int x;//存放结果
    if (c == '+') x = a + b;
    else if (c == '-') x = a - b;
    else if (c == '*') x = a * b;
    else x = a / b;
    num.push(x);//计算结果存回数值栈
}

int main()
{
    string s;//字符串
    cin >> s;

    unordered_map<char, int> pr{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};//运算符优先级 (key,value),key唯一标识,value值代表运算优先级大小
    for (int i = 0; i < s.size(); i ++ )
    {
        if (isdigit(s[i]))//判断是否为数字
        {
            int j = i, x = 0;//j指针记录读取位置 ,x辅助存数值   
            while (j < s.size() && isdigit(s[j]))
                x = x * 10 + s[j ++ ] - '0';//字符串转数字
            num.push(x);//计算完放入num数字栈
            i = j - 1;//i移动到j : i先移动到j-1, 最后执行i++ == j       
        } //括号特殊,遇到左括号直接入栈,遇到右括号计算括号里面的
        else if (s[i] == '(') op.push(s[i]);//读到左括号无优先级 ,直接放入操作栈op
        else if (s[i] == ')')//读到右括号,运算调用eval函数计算 , 不断弹栈直到左括号结束 ,再弹出左括号
        {
            while (op.top() != '(') eval();
            op.pop();
        }
        else
        {
            while (op.size() && op.top() != '(' && pr[op.top()] >= pr[s[i]]) //op栈还有操作符且不是左括号 && op运算符优先级 >= 读到的操作符
                eval();//弹一个op栈操作符 ,计算    //待入栈运算符优先级低,则先计算
            op.push(s[i]);//操作符入栈
        }
    }

    while (op.size()) eval();//最后运算到没有操作符,得出表达式运算结果由eval存回num栈顶
    cout << num.top() << endl;//调用栈顶

    return 0;
}

补充(非此题):
中缀表达式转后缀表达式:




#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_map>
#include <stack>

using namespace std;

stack<char> op;

void eval()
{
    auto c = op.top(); op.pop();
    cout << c << ' ';
}

int main()
{
    string s;
    cin >> s;

    unordered_map<char, int> pr{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};
    for (int i = 0; i < s.size(); i ++ )
    {
        if (isdigit(s[i]))
        {
            int j = i, x = 0;
            while (j < s.size() && isdigit(s[j]))
                x = x * 10 + s[j ++ ] - '0';
            cout << x << ' ';
            i = j - 1;
        }
        else if (s[i] == '(') op.push(s[i]);
        else if (s[i] == ')')
        {
            while (op.top() != '(') eval();
            op.pop();
        }
        else
        {
            while (op.size() && op.top() != '(' && pr[op.top()] >= pr[s[i]])
                eval();
            op.push(s[i]);
        }
    }

    while (op.size()) eval();

    return 0;
} 

4.树的遍历

在这里插入图片描述每次小三角看(每三个数根和它的左右孩子遍历完,才递归遍历其他)【做前序+中序也是每三个数字看:一个为根两个为孩子】

在这里插入图片描述十字链表

my_note
满二叉树(除叶子节点,父节点均有左右孩子   【数学归纳法,证明:第k层有 2^k-1^ 节点 ,所有节点 2^0^ +... + 2^k-1^ = 2^k^ - 1】 
完全二叉树 (所有节点编号是连续的,除最后一行不一定,上一行必为满二叉树 

点数 = n0 + n1 + n2 (度数为0 、1 、2)    
边数 = 点数 - 1  =  0 * n0 + 1 * n1 + 2 * n2         【度数为x的点有x条边】 
叶子节点n0 = n2 + 1     【度这里等效:左右孩子数量】 完全二叉树n1 为1或0

节点编号n的高度:log2^n  - 1 

前序 :根左右 
中序 :左根右 
后序 :左右根 

**每次小三角看(每三个数根和它的左右孩子遍历完,才递归遍历其他)** 


前序遍历 : 根 左子树前序 右子树前序   
中序遍历 : 左子树中序 根 右子树中序 

上机题:给前序和中序,输出二叉树的层次遍历序列 
unordered_map<int,int> position;
 
①并查集:特殊的树(只存父节点) 
②邻接表: 每个点开一条链表(存左右孩子) 存所有子节点 
③邻接多重表:(无向图优化,找某一条边a->b的反向边b->a,但考试不写这个代码,又臭又长) 

十字链表   
三元组表  : 点 -> 点 + 边权值 
 
树->二叉树转化 : 左儿子右兄弟   【二叉树节点的左孩子为孩子节点,右孩子为兄弟节点】 

补:所有父节点均有左右孩子 :前序+后序也可以构造出二叉树  

// 要从代码来看学习思路,不要太笼统的从描述看(有歧义很难记清除) 


满足题目要求构造,越特殊越简单越好 

线索二叉树:考的都是树的遍历 :按照遍历顺序的前驱 、后继 


做题 
找关系证明: 点与度 , 边与度 , 点数 = 边数 + 1 

用dfs代码短
WPL : ∑ 叶子结点的权值 * 深度


/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */

//WPL : ∑ 叶子结点的权值 * 深度
class Solution {
public:
    int dfs(TreeNode* root, int depth) {
        if (!root) return 0;//空
        if (!root->left && !root->right) return root->val * depth;//叶子节点
        return dfs(root->left, depth + 1) + dfs(root->right, depth + 1);
    }//判断完每层情况左右递归下一层,加和左右递归值得WPL

    int pathSum(TreeNode* root) {
        return dfs(root, 0);
    }
};  

3766. 二叉树的带权路径长度

二叉树的带权路径长度(WPL)是二叉树中所有叶结点的带权路径长度之和,也就是每个叶结点的深度与权值之积的总和。

给定一棵二叉树 T,请你计算并输出它的 WPL。

注意,根节点的深度为 0。

样例
输入:二叉树[8, 12, 2, null, null, 6, 4, null, null, null, null]如下图所示:
    8
   / \
  12  2
     / \
    6   4

输出:32

数据范围
二叉树结点数量不超过 1000。
每个结点的权值均为不超过 100 的非负整数。

(WPL)是二叉树中所有叶结点的带权路径长度之和,也就是每个叶结点的深度与权值之积的总和
WPL : ∑ 叶子结点的权值 * 深度

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int dfs(TreeNode* root, int depth) {
        if (!root) return 0;//为空, 值为0 
        if (!root->left && !root->right) return root->val * depth;//叶子节点返回对应的WPL 
        return dfs(root->left, depth + 1) + dfs(root->right, depth + 1);//每次往下递归一层 depth++ 
    }//判断完每层情况左右递归下一层,加和左右递归值得WPL

    int pathSum(TreeNode* root) {
        return dfs(root, 0);
    }
};

18. 重建二叉树

输入一棵二叉树前序遍历和中序遍历的结果,请重建该二叉树。

注意:

二叉树中每个节点的值都互不相同;
输入的前序遍历和中序遍历一定合法;
数据范围
树中节点数量范围 [0,100]。

样例
给定:
前序遍历是:[3, 9, 20, 15, 7]
中序遍历是:[9, 3, 15, 20, 7]

返回:[3, 9, 20, null, null, 15, 7, null, null, null, null]

返回的二叉树如下所示:
    3
   / \
  9  20
    /  \
   15   7

在这里插入图片描述

已知前序遍历与中序遍历还原二叉树

推导build函数 :[a,b]与[x,y]     【如图k为根在中序遍历中的位置】
	前缀中间子树的根位置:a+1 ~ ? == x ~ k - 1  [长度相等]
		? - (a+1) = (k -1) - x      【?:根的儿子作为根在前序遍历中的位置】
	前半段:
		?  == (a + 1) + (k - 1) - x  ===>[a,b]-->[a,(a + 1) + (k - 1) - x ]
		[x,y] ⇒ [x,k - 1]
	后半段:
		[(a + 1) + (k - 1) - x + 1,b]  
		[k+1,y]

题目所给形参已经引用了中序和前序遍历序列(给定形参填充函数,不用管输入输出)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    unordered_map<int, int> pos;
    vector<int> preorder, inorder;//根据前序遍历和中序遍历还原二叉树

    TreeNode* build(int a, int b, int x, int y) {//前序[a,b] ,中序[x,y]
        if (a > b) return NULL;
        auto root = new TreeNode(preorder[a]);
        int k = pos[root->val];
        root->left = build(a + 1, a + 1 + k - 1 - x, x, k - 1);//递归位置:前序左与中序左长度相等 推导 
        root->right = build(a + 1 + k - 1 - x + 1, b, k + 1, y);
        return root;//返回根的位置
    }
	 //题目所给形参已经引用了中序和前序遍历序列(给定形参填充函数,不用管输入输出)
    TreeNode* buildTree(vector<int>& _preorder, vector<int>& _inorder) {
        preorder = _preorder, inorder = _inorder;
        int n = inorder.size();//结点个数
        for (int i = 0; i < n; i ++ ) pos[inorder[i]] = i;//记录每个点在中序遍历中的位置
        return build(0, n - 1, 0, n - 1);
    }
};

/*
 * @desc: 树的遍历+DFS递归+哈希表 - 重建二叉树。

【知识点】
    - DFS;递归;树的遍历;哈希表。

【本题思路】
    递归建立整棵二叉树:先递归创建左右子树,然后创建根节点,并让指针指向两棵子树。
    (1)先利用前序遍历找根节点:前序遍历的第一个数,就是根结点的值。
    (2)在中序遍历序列中找到当前根结点的位置 k,则在序列中 k 的左边是左子树的中序遍历序列(in_left, in_left + k - 1),右边是右子树的中序遍历序列(in_left + k + 1, in_right)。
    (3)假设左子树的中序遍历的长度是 l,则在前序遍历中,根结点后面的 l 个数就是左子树的前序遍历序列(pre_left + 1, pre_left + k),剩下的数是右子树的前序遍历序列(pre_left + k + 1, pre_right)。
    (4)获得左右子树的前序遍历序列和中序遍历序列后,我们可以先递归创建出左右子树,然后再创建根结点。

【时间复杂度】
    - T(n) = O(n)。

【注意点】
    (1)左子树的中序遍历序列(in_left, in_left + k - 1),右子树的中序遍历序列(in_left + k + 1, in_right)。
    (2)注意左右子树的中序遍历序列中不包括第 in_left + k 个,这是因为这个结点就是当前的根结点。
    (3)左子树的前序遍历序列(pre_left + 1, pre_left + k),右子树的前序遍历序列(pre_left + k + 1, pre_right)。
    (4)注意左右子树的前序遍历序列中不包括第 pre_left 个,这是因为这个结点就是当前的根结点。

【重点文章】
    - 代码: https://www.acwing.com/activity/content/code/content/1466482/
    - 题解: https://www.acwing.com/solution/content/706/ (y总题解,时间复杂度分析)

【变量说明】
    - pos:       记录每个结点在中序遍历序列中的位置。
    - pre:       题目输入数据,表示当前二叉树的前序遍历序列。
    - in:        题目输入数据,表示当前二叉树的中序遍历序列。
    - k:         表示当前根结点在所给的中序遍历范围区间内的位置(分割左右子树)。
    - pre_left:  表示当前根结点所代表的树在前序遍历序列中对应范围的起点位置。
    - pre_right: 表示当前根结点所代表的树在前序遍历序列中对应范围的终点位置。
    - in_left:   表示当前根结点所代表的树在中序遍历序列中对应范围的起点位置。
    - in_right:  表示当前根结点所代表的树在中序遍历序列中对应范围的终点位置。

 * @author: Zzay
 * @time: 2022/05/19 16:20
 */
/**
* Definition for a binary tree node.
* struct TreeNode {
*     int val;
*     TreeNode *left;
*     TreeNode *right; 5555 
*     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {

public:
   // 记录每个结点在中序遍历序列中的位置
   unordered_map<int, int> pos;

   // DFS递归遍历二叉树
   TreeNode* dfs(vector<int>& pre, vector<int>& in, int pre_left, int pre_right, int in_left, int in_right) {
       // 注意递归结束条件
       if (pre_left > pre_right) {
           return NULL;
       }
       // k 表示当前根结点在所给的中序遍历范围区间内的位置(分割左右子树)
       int k = pos[pre[pre_left]] - in_left;
       // 左子树的中序遍历序列(in_left ~ in_left + k - 1),右子树的中序遍历序列(in_left + k + 1 ~ in_right)
       // 左子树的前序遍历序列(pre_left + 1 ~ pre_left + k),右子树的前序遍历序列(pre_left + k + 1, pre_right)
       TreeNode* root = new TreeNode(pre[pre_left]);
       root->left = dfs(pre, in, pre_left + 1, pre_left + k, in_left, in_left + k - 1);
       root->right = dfs(pre, in, pre_left + k + 1, pre_right, in_left + k + 1, in_right);
       return root;
   }

   // 根据前序遍历序列和中序遍历序列,构建二叉树
   TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
       // 记录每个结点在中序遍历序列中的位置
       int n = preorder.size();
       for (int i = 0; i < n; i++) {
           pos[inorder[i]] = i;
       }
       return dfs(preorder, inorder, 0, n - 1, 0, n - 1);
   }
};


//作者:Zzay 链接:https://www.acwing.com/file_system/file/content/whole/index/content/5684403/

好题解:
①https://www.acwing.com/file_system/file/content/whole/index/content/5684403/
②y总前期:https://www.acwing.com/solution/content/706/

y总前期

(递归) O(n)
递归建立整棵二叉树:先递归创建左右子树,然后创建根节点,并让指针指向两棵子树。

具体步骤如下:

先利用前序遍历找根节点:前序遍历的第一个数,就是根节点的值;
在中序遍历中找到根节点的位置 k,则 k 左边是左子树的中序遍历,右边是右子树的中序遍历;
假设左子树的中序遍历的长度是 l,则在前序遍历中,根节点后面的 l 个数,是左子树的前序遍历,剩下的数是右子树的前序遍历;
有了左右子树的前序遍历和中序遍历,我们可以先递归创建出左右子树,然后再创建根节点;
时间复杂度分析
我们在初始化时,用哈希表(unordered_map<int,int>)记录每个值在中序遍历中的位置,这样我们在递归到每个节点时,在中序遍历中查找根节点位置的操作,只需要 O(1) 的时间。此时,创建每个节点需要的时间是 O(1),所以总时间复杂度是 O(n)/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:

    unordered_map<int,int> pos;

    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int n = preorder.size();
        for (int i = 0; i < n; i ++ )
            pos[inorder[i]] = i;
        return dfs(preorder, inorder, 0, n - 1, 0, n - 1);
    }

    TreeNode* dfs(vector<int>&pre, vector<int>&in, int pl, int pr, int il, int ir)
    {
        if (pl > pr) return NULL;
        int k = pos[pre[pl]] - il;
        TreeNode* root = new TreeNode(pre[pl]);
        root->left = dfs(pre, in, pl + 1, pl + k, il, il + k - 1);
        root->right = dfs(pre, in, pl + k + 1, pr, il + k + 1, ir);
        return root;
    }
};


//作者:yxc 链接:https://www.acwing.com/solution/content/706/

5. 二叉搜索树与表达式树

3765. 表达式树

请设计一个算法,将给定的表达式树(二叉树)转换为等价的中缀表达式(通过括号反映操作符的计算次序)并输出。

例如,当下列两棵表达式树作为算法的输入时:

在这里插入图片描述

输出的等价中缀表达式分别为 (a+b)(c(-d)) 和 (a*b)+(-(c-d))。

注意:

树中至少包含一个运算符。
当运算符是负号时,左儿子为空,右儿子为需要取反的表达式。
树中所有叶节点的值均为非负整数。

样例:
输入:二叉树[+, 12, *, null, null, 6, 4, null, null, null, null]如下图所示:
    +
   / \
  12  *
     / \
    6   4
    
输出:12+(6*4)

数据范围
给定二叉树的非空结点数量保证不超过 1000。
给定二叉树保证能够转化为合法的中缀表达式。

再改看

O(n2)写法:


/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     string val;
 *     TreeNode *left;
 *     TreeNode *right;
 * };
 */
class Solution {
public:
    string dfs(TreeNode* root) {
        if (!root) return "";
        if (!root->left && !root->right) return root->val;
        return '(' + dfs(root->left) + root->val + dfs(root->right) + ')';
    }

    string expressionTree(TreeNode* root) {
        return dfs(root->left) + root->val + dfs(root->right);
    }
};

因为return 会复制一遍,则 1 + 2+ 3 + … + n ,达到O(n2)
O(n)优化写法【取ans分别+=赋值,不用return】





/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     string val;
 *     TreeNode *left;
 *     TreeNode *right;
 * };
 */
class Solution {
public:
    string ans;

    void dfs(TreeNode* root) {
        if (!root) return;
        if (!root->left && !root->right) ans += root->val;
        else
        {
            ans += '(';
            dfs(root->left);
            ans += root->val;
            dfs(root->right);
            ans += ')';
        }
    }

    string expressionTree(TreeNode* root) {
        dfs(root->left), ans += root->val, dfs(root->right);
        return ans;
    }
};


//作者:yxc 链接:https://www.acwing.com/activity/content/code/content/1476695/

3786. 二叉排序树

你需要写一种数据结构,来维护一些数,其中需要提供以下操作:

插入数值 x。
删除数值 x。
输出数值 x 的前驱(前驱定义为现有所有数中小于 x 的最大的数)。
输出数值 x 的后继(后继定义为现有所有数中大于 x 的最小的数)。
题目保证:

操作 1 插入的数值各不相同。
操作 2 删除的数值一定存在。
操作 3 和 4 的结果一定存在。
输入格式
第一行包含整数 n,表示共有 n 个操作命令。

接下来 n 行,每行包含两个整数 opt 和 x,表示操作序号和操作数值。

输出格式
对于操作 3,4,每行输出一个操作结果。

数据范围
1≤n≤2000,
−10000≤x≤10000
输入样例:
6
1 1
1 3
1 5
3 4
2 3
4 2
输出样例:
3
5

左旋、右旋不改变中序遍历,且互逆
背过:①最小不平衡树,从下往上找,找到第一个不平衡的子树 ②按照类型规则旋转
四种类型 :LL、RR、LR、RL
在这里插入图片描述
看图的感悟:LR:左C后变LL,再右旋A ;RL:右C后变RR,再左旋A ;
手写二叉排序树【前驱和后继为遍历序列左右相邻元素】
先序【根左右】、中序【左根右】、后序【左右根】 [小三角判断]

insert(TreeNode* &root, int x) 递归查找左<根<右
remove(TreeNode* &root, int x) 先找再分类删
get_pre(TreeNode* root, int x) 寻找前驱【找小于的最大值,不存在用-INF负无穷】
get_suc(TreeNode* root, int x) 寻找后继   【找大于的最小值,不存在用INF正无穷】
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int INF = 1e8;

struct TreeNode
{
    int val;
    TreeNode *left, *right;//记住定义指针变量,每个都要加*
    TreeNode(int _val): val(_val), left(NULL), right(NULL) {}
}*root;

void insert(TreeNode* &root, int x)//改变指针所指的值 取引用 
{
    if (!root) root = new TreeNode(x);//【找到位置后,指针指向new的新结点】
    else if (x < root->val) insert(root->left, x);//小于父结点值,放左子树,递归判断下一次插入
    else insert(root->right, x);//大于父结点值,放右子树,递归判断下一次插入
}

void remove(TreeNode* &root, int x)//先找再分类删【左右子树是否为空分四类】 
{
    if (!root) return;//树为空,删除的数不存在 【注意判空分支是提前判断 没有if-else关系】 
    if (x < root->val) remove(root->left, x);//x小于根结点,x在左子树,去左子树删
    else if (x > root->val) remove(root->right, x);//x大于根结点值,x在右子树,
    else//找到x后:分类删除
    {
        if (!root->left && !root->right) root = NULL; //如果没有左右孩子,则为叶子结点,直接删除【root指针指向NULL】
        else if (!root->left) root = root->right;//如果左子树为空,把右子树填上来   ,往右找最右侧【取最大值】,
        else if (!root->right) root = root->left;//如果右子树为空,把左子树填上来 
        else//如果左右子树都有 【则把左子树最右侧的结点值替换删除结点,然后删除最右侧结点】
        {
            auto p = root->left; //替换法删除【保持左<父<右 && 中序遍历不变】
            while (p->right) p = p->right;//找左子树最右侧最大值,替换父结点,再删除最右侧结点
            root->val = p->val;
            remove(root->left, p->val);//删除左子树的最右侧结点,值为p->val
        }
    }
}

int get_pre(TreeNode* root, int x)//寻找前驱【找小于的最大值,不存在用-INF负无穷】 【查找不需要修改,不用加引用】 
{
    if (!root) return -INF;//找最大值,没有答案用-INF(负无穷) 
    if (root->val >= x) return get_pre(root->left, x);//值大于x,返回左子树的查找结果 
    return max(root->val, get_pre(root->right, x));//否则返回根的结果和右子树的值的max(看根有没有右子树,有则前驱为右子树的最右侧)
}

int get_suc(TreeNode* root, int x)//寻找后继   【找大于的最小值,不存在用INF正无穷】
{
    if (!root) return INF;//找最小值,没有答案用正无穷 
    if (root->val <= x) return get_suc(root->right, x); //值大于,往右子树找
    return min(root->val, get_suc(root->left, x));//返回根与其左子树min(若根有左子树则左子树作为后继,min=左子树)
}

int main()//若输出就是中序遍历
{
    int n;
    cin >> n;
    while (n -- )
    {
        int t, x;
        cin >> t >> x;
        if (t == 1) insert(root, x);
        else if (t == 2) remove(root, x);
        else if (t == 3) cout << get_pre(root, x) << endl;
        else cout << get_suc(root, x) << endl;
    }

    return 0;
} 

6. Huffman树

148. 合并果子

在一个果园里,达达已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。

达达决定把所有的果子合成一堆。

每一次合并,达达可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。

可以看出,所有的果子经过 n−1 次合并之后,就只剩下一堆了。

达达在合并果子时总共消耗的体力等于每次合并所耗体力之和。

因为还要花大力气把这些果子搬回家,所以达达在合并果子时要尽可能地节省体力。

假定每个果子重量都为 1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使达达耗费的体力最少,并输出这个最小的体力耗费值。

例如有 3 种果子,数目依次为 1,2,9。

可以先将 1、2 堆合并,新堆数目为 3,耗费体力为 3。

接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 12,耗费体力为 12。

所以达达总共耗费体力=3+12=15。

可以证明 15 为最小的体力耗费值。

输入格式
输入包括两行,第一行是一个整数 n,表示果子的种类数。

第二行包含 n 个整数,用空格分隔,第 i 个整数 ai 是第 i 种果子的数目。

输出格式
输出包括一行,这一行只包含一个整数,也就是最小的体力耗费值。

输入数据保证这个值小于 231。

数据范围
1≤n≤10000,
1≤ai≤20000
输入样例:
3
1 2 9
输出样例:
15

小根堆【优先队列】此题允许任意两堆合并

#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;

int main()
{
    int n;
    scanf("%d", &n);

    priority_queue<int, vector<int>, greater<int>> heap;
    while (n -- )
    {
        int x;
        scanf("%d", &x);
        heap.push(x);
    }

    int res = 0;
    while (heap.size() > 1)
    {
        int a = heap.top(); heap.pop(); //调用函数之间只能用分号隔开 
        int b = heap.top(); heap.pop();
        res += a + b;
        heap.push(a + b);
    }

    printf("%d\n", res);
    return 0;
} 

149. 荷马史诗

追逐影子的人,自己就是影子。 ——荷马

达达最近迷上了文学。

她喜欢在一个慵懒的午后,细细地品上一杯卡布奇诺,静静地阅读她爱不释手的《荷马史诗》。

但是由《奥德赛》和《伊利亚特》组成的鸿篇巨制《荷马史诗》实在是太长了,达达想通过一种编码方式使得它变得短一些。

一部《荷马史诗》中有 n 种不同的单词,从 1 到 n 进行编号。其中第 i 种单词出现的总次数为 wi。

达达想要用 k 进制串 si 来替换第 i 种单词,使得其满足如下要求:

对于任意的 1≤i,j≤n,i≠j,都有:si 不是 sj 的前缀。

现在达达想要知道,如何选择 si,才能使替换以后得到的新的《荷马史诗》长度最小。

在确保总长度最小的情况下,达达还想知道最长的 si 的最短长度是多少?

一个字符串被称为 k 进制字符串,当且仅当它的每个字符是 0 到 k−1 之间(包括 0 和 k−1)的整数。

字符串 Str1 被称为字符串 Str2 的前缀,当且仅当:存在 1≤t≤m,使得 Str1=Str2[1…t]。

其中,m 是字符串 Str2 的长度,Str2[1…t] 表示 Str2 的前 t 个字符组成的字符串。

注意:请使用 64 位整数进行输入输出、储存和计算。

输入格式
输入文件的第 1 行包含 2 个正整数 n,k,中间用单个空格隔开,表示共有 n 种单词,需要使用 k 进制字符串进行替换。

第 2∼n+1 行:第 i+1 行包含 1 个非负整数 wi,表示第 i 种单词的出现次数。

输出格式
输出文件包括 2 行。

第 1 行输出 1 个整数,为《荷马史诗》经过重新编码以后的最短长度。

第 2 行输出 1 个整数,为保证最短总长度的情况下,最长字符串 si 的最短长度。

数据范围
2≤n≤100000,
2≤k≤9
1≤wi≤1012
输入样例:
4 2
1
1
2
2
输出样例:
12
2

只允许相邻两堆合并【石子合并】
此题难度系数大(NOI国赛)但会了就简单

二叉树每次合并最小两个结点,
此题k叉树选取k个最小值结点合并,且相同值的选取深度更小的(双重贪心)【多关键字排序】

贪心题特点:代码短,实际做不需要证明,但要想清楚

问算法思想,为什么最好?答:根据Huffman思想

在这里插入图片描述
min(WPL)+min(dpeth) 双贪心-k叉树构造

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

#define x first//pair取元素时代码化简
#define y second

using namespace std;

typedef long long LL;
typedef pair<LL, int> PLI;//双关键字排序 (LL weight, int depth)   

int main()
{
    int n, k;
    scanf("%d%d", &n, &k);
    priority_queue<PLI, vector<PLI>, greater<PLI>> heap;//创建双关键字类型的小根堆
    while (n -- )
    {
        LL w;
        scanf("%lld", &w);
        heap.push({w, 0});
    }
    //每次选取k个点合并成1个点,即减少k-1个点,且最后要剩下一个点(初始n个点总共需减少n-1个点) 
    //需添加点{0, 0}至能整除 : (n - 1) % (k - 1) == 0停止 (不等于0循环执行)  【初始n = heap.size()】
    while ((heap.size() - 1) % (k - 1)) heap.push({0, 0});

    LL res = 0;
    while (heap.size() > 1) //Huffman-k叉树-双关键字 合并
    {
        LL s = 0;
        int depth = 0;
        for (int i = 0; i < k; i ++ )//每轮选取k个最小点
        {
            auto t = heap.top();
            heap.pop();
            s += t.x, depth = max(depth, t.y);//贪心, 高度取所有子结点的namax 
        }
        heap.push({s, depth + 1});//前k个min合并的新的结点加入点集, 合并后高度 = max子结点高度+1
        res += s;//记录WPL 
    }

    printf("%lld\n%d\n", res, heap.top().y);//输出WPL 和 max-depth(只剩一个点即最终depth)
    return 0;
}


7. 拓扑排序

848. 有向图的拓扑序列

给定一个 n 个点 m 条边的有向图,点的编号是 1 到 n,图中可能存在重边和自环。

请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 −1。

若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x 在 A 中都出现在 y 之前,则称 A 是该图的一个拓扑序列。

输入格式
第一行包含两个整数 n 和 m。

接下来 m 行,每行包含两个整数 x 和 y,表示存在一条从点 x 到点 y 的有向边 (x,y)。

输出格式
共一行,如果存在拓扑序列,则输出任意一个合法的拓扑序列即可。

否则输出 −1。

数据范围
1≤n,m≤105
输入样例:
3 3
1 2
2 3
1 3
输出样例:
1 2 3
(或1 3 2)

数组模拟链表:


#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue> 
using namespace std;

const int N = 100010 , M = 100010;

int n,m; //n个点m条边
struct Node
{
	int id;
	Node* next;
	Node(int _id) : id(_id),next(NULL){} 
} *head[N];
int st[N] ;

void add(int a,int b)  //单链表插入 (头插法) 但是存下标 
{
	auto p = new Node(b);
	p->next = head[a];
	head[a] = p;   //存下标 
}

void bfs()
{ 
	queue<int> q;
	q.push(1);  //1号节点入队
	st[1] = true;  //标记 
	
	while(q.size())
	{
		auto t = q.front();
		q.pop();
		
		printf("%d ",t); //ans可以先用[输入队列]:因为队列先进先出 ,输入队列和输出队列是一样的 
		for(auto p = head[t]; p ;p = p->next)//枚举所有的邻接点
		{
			int j = p->id;
			if(!st[j])//如果这个点没被搜过,搜一下 
			{
				st[j] = true;
				q.push(j); 
			}
		}
	}
	
}


int main()
{
	scanf("%d%d",&n,&m);
	while(m --)
	{
		int a,b; 
		scanf("%d%d",&a,&b);
		add(a,b);
	}
	
	bfs(); 

	return 0;
} 


链表写法14行 ,数组写法6行
上机就用链式前向星:【数组邻接表】




#include<iostream>
#include<algorithm>
#include<queue> 
using namespace std;

const int N = 100010 , M = 100010;

int n,m; //n个点m条边

struct Node
{
	int id;
	Node* next;
	Node(int _id) : id(_id),next(NULL){} 
}*head[N];


void add(int a,int b)  //单链表插入 (头插法) 但是存下标 
{
	auto p = new Node(b);
	p->next = head[a];
	head[a] = p;   //存下标 
}

int st[N],q[N],top;
bool dfs(int u)
{
	st[u] = 1;
	
	for(auto p = head[u]; p ;p = p->next)
	{
		int j = p->id;
		if(!st[j])
		{
			if(!dfs(j))return false;  //【有环路】j的dfs返回false说明子序列有环,不是拓扑序列  
		}
		else if (st[j] == 1) return false; //已经被遍历过,再遍历就有环 
	}
	
	q[top ++] = u;  //存拓扑序列【dfs最后一个才返回,此处逆序】 
	
	st[u] = 2; //2表示已遍历且验证无环
	return true;
}



bool topsort()
{
	for(int i = 1;i <= n;i++)
		if(!st[i] && !dfs(i)) //都遍历过 且 没有环
			return false;
	return true;	
} 

int main()
{
	scanf("%d%d",&n,&m);
	while(m --)
	{
		int a,b; 
		scanf("%d%d",&a,&b);
		add(a,b);
	}
	
	if(!topsort())puts("-1");
	else
	{
	    for(int i = n - 1;i >= 0;i --)   //i++无限循环出现: Segmentation Fault   
	        printf("%d ",q[i]);
	}
        
	return 0;
} 


//BFS课后自己看


//int st[N];  0还没搜过,直接搜  1正在搜  2遍历完 

//dfs递归是到最后才返回,所以序列是倒着的,也可以每次true就输出(就变成正序的)

/*
DP本质递推,都可以看做最短路问题,无后效性

上机最常用还是宽搜!!!【深搜dfs也可以,有的OJ网站没有把栈空间放开,递归爆栈】
*/


#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;

const int N=100010;
int h[N],e[N],ne[N],idx;
int n,m;
int q[N],d[N];//q表示队列,d表示点的入度

void add(int a,int b)//e[] = b存点下标
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

bool topsort()
{
    int hh = 0,tt = -1;
    for(int i = 1;i <= n;i ++)
     if(!d[i]) //入度 == 0 
     	q[++tt] = i;//将入度为零的点入队
     	
    while(hh <= tt)
    {
        int t = q[hh++];
        for(int i = h[t]; i != -1;i = ne[i])
        {
            int j = e[i];
            d[j] --;//删除点t指向点j的边
            if(d[j] == 0)//如果点j的入度为零了,就将点j入队
            	q[++tt] = j;
        }
    }
    return tt == n - 1;
    //表示如果n个点都入队了话,那么该图为拓扑图,返回true,否则返回false
}


int main()
{
    cin >> n >> m;
    memset(h, -1, sizeof h);//如果程序时间溢出,就是没有加上这一句
    for(int i = 0;i < m; i++)
    {
        int a,b;
        scanf("%d%d", &a, &b);
        add(a, b);//因为是a指向b,所以b点的入度要加1
        d[b]++;
    }
    if(topsort()) 
    {
        for(int i = 0; i < n; i++)
        printf("%d ", q[i]);
        //经上方循环可以发现队列中的点的次序就是拓扑序列
        //注:拓扑序列的答案并不唯一,可以从解析中找到解释
        puts("");
    }
    else puts("-1");

    return 0;
}
//作者:E.lena 链接:https://www.acwing.com/solution/content/21908/

8. 最小生成树、最短路

858. Prim算法求最小生成树

给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。

求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。

给定一张边带权的无向图 G=(V,E),其中 V 表示图中点的集合,E 表示图中边的集合,n=|V|,m=|E|。

由 V 中的全部 n 个顶点和 E 中 n−1 条边构成的无向连通子图被称为 G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G 的最小生成树。

输入格式
第一行包含两个整数 n 和 m。

接下来 m 行,每行包含三个整数 u,v,w,表示点 u 和点 v 之间存在一条权值为 w 的边。

输出格式
共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。

数据范围
1≤n≤500,
1≤m≤105,
图中涉及边的边权的绝对值均不超过 10000。

输入样例:
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例:
6

海绵宝宝不错呀hhh

朴素版prim: O(n2)

无向图g[a][b] = g[b][a] = min(c,g[a][b]);
dist计算两点最小边权,res累加返回答案

#include<iostream>
#include<algorithm> 
#include<cstring> 
using namespace std;

const int N = 510,M = 100010,INF = 0x3f3f3f3f;

int n,m;
int g[N][N],dist[N];//邻接矩阵 
bool st[N];

int prim()//每次从邻接点选取最小距离加入集合
{
    memset(dist , 0x3f, sizeof dist);
    dist[1] = 0;
    int res = 0;//边权和 
    for(int i = 0;i < n ;i ++ )
    {
        int t = -1; //t表示为当前边权最小的点 
        for(int j = 1;j <= n; j ++)
            if(!st[j] && (t == -1 || dist[t] >dist[j]))   //初始起点t == -1 || 加入邻接边最小权重
                t = j;
        if(dist[t] == INF) return INF;   //【发现t没被更新】t节点不可达,无法构造最小生成树,提前剪枝    
        st[t] = true;//t= j ,加入点j
        res +=  dist[t]; //res += 每条边的长度
        for(int j = 1;j <= n;j ++) //用新加的点,更新其他路径 
            dist[j] = min(dist[j] , g[t][j]); // min(原来存的两点边权,新加入的最小点t=j到此点([1,n]中的点)的距离) 【dist则为每条边的长度】
    }
    return res; 
}

int main()
{
    scanf("%d%d",&n,&m);
    memset(g,0x3f,sizeof g);
    while(m --)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c); //a->b :value = c 
        g[a][b] = g[b][a] = min( g[a][b] , c); // 初始【无向图】 ,重边取最小
    }

    int res = prim();
    if(res == INF) puts("impossible");
    else printf("%d\n",res);

    return 0; 
}



Kruskal算法 O(mlogm)

贪心按边权从小到大加入边,并查集判断点是否在集合中,不在的加入并查集



#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N = 510 	, 	M = 100010;
int n,m;

struct Edge
{
	int a,b,c; //a->b : value = c 
	bool operator< (const Edge &t)const 
	{
		return c < t.c;
	}
}e[M];
int p[N];

int find(int x)
{
	if(p[x] != x) p[x] = find(p[x]);
	return p[x];
}

int main()
{
	scanf("%d%d",&n,&m);//n个点m条边 
	for(int i = 0;i < m;i ++)
		scanf("%d%d%d",&e[i].a,&e[i].b,&e[i].c);//输入每条边
	sort(e , e + m);//先排好序按权值大小(重载小于号) [贪心思想每次选取未加入的点的最小边权] 
	
	for(int i = 1;i <= n;i++) p[i] = i;//初始化并查集,维护点的集合 
			
	int res = 0,cnt = n; //统计n个点之间的最小边权和(最小生成树)  [若不是点都联通,则不可能]
	for(int i = 0;i < m;i++)    //枚举所有边	 
	{
		int  a = e[i].a ,b = e[i].b , c =  e[i].c;//简化赋值 
		if(find(a) != find(b))//加入集合过程计算最小生成树的值 
		{
			res += c; 
			cnt --;
			p[find(a)] = find(b);
		}
	}

	if(cnt > 1) puts("impossple");//无法生成最小生成树 
	else printf("%d\n",res); 
	
	return 0;
}



849. Dijkstra求最短路 I

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。

输入格式
第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。

如果路径不存在,则输出 −1。

数据范围
1≤n≤500,
1≤m≤105,
图中涉及边长均不超过10000。

输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3


#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 510, M = 100010, INF = 0x3f3f3f3f;

int n, m;//点数,边数 
int g[N][N], dist[N];
bool st[N];

int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);//初始全部正无穷
    dist[1] = 0;//起点距离为0【点下标从1开始】
    for (int i = 0; i < n; i ++ )
    {
        int t = -1;//路径经过的点下标  
        for (int j = 1; j <= n; j ++ ) //点下标从1开始
            if (!st[j] && (t == -1 || dist[t] > dist[j])) // 开始第一个或者路径更短 
                t = j;
        st[t] = true;
        for (int j = 1; j <= n; j ++ )//每新加入一个点后更新所有路径 [三角形判断]
            dist[j] = min(dist[j], dist[t] + g[t][j]);
    }
    return dist[n];//迭代状态最后放到走了第n次,经过n个点 
}

int main()
{
    scanf("%d%d", &n, &m);
    memset(g, 0x3f, sizeof g);
    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        g[a][b] = min(g[a][b], c);//有重边取最小 
    }

    int res = dijkstra();
    if (res == INF) puts("-1");//无法构造最小生成树(不能经过所有点,dist[n]没有被更新,为INF) 
    else printf("%d\n", res);

    return 0;
}


854. Floyd求最短路

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,边权可能为负数。

再给定 k 个询问,每个询问包含两个整数 x 和 y,表示查询从点 x 到点 y 的最短距离,如果路径不存在,则输出 impossible。

数据保证图中不存在负权回路。

输入格式
第一行包含三个整数 n,m,k。

接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

接下来 k 行,每行包含两个整数 x,y,表示询问点 x 到点 y 的最短距离。

输出格式
共 k 行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出 impossible。

数据范围
1≤n≤200,
1≤k≤n2
1≤m≤20000,
图中涉及边长绝对值均不超过 10000。

输入样例:
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
输出样例:
impossible
1

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 210, INF = 0x3f3f3f3f;

int n, m, Q;
int d[N][N];

int main()
{
    scanf("%d%d%d", &n, &m, &Q);
    memset(d, 0x3f, sizeof d);
    for (int i = 1; i <= n; i ++ ) d[i][i] = 0;

    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        d[a][b] = min(d[a][b], c);
    }

     for (int k = 1; k <= n; k ++ )//加入中间点k , 在i->k->j 与 原有i->j之间比较 
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);

    while (Q -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        int c = d[a][b];
        if (c > INF / 2) puts("impossible");
        else printf("%d\n", c);
    }

    return 0;
} 

9. 基本概念、顺序、折半、分块查找法、B/B+树 【没有习题】

10. 哈希表和KMP

840. 模拟散列表

维护一个集合,支持如下几种操作:

I x,插入一个数 x;
Q x,询问数 x 是否在集合中出现过;
现在要进行 N 次操作,对于每个询问操作输出对应的结果。

输入格式
第一行包含整数 N,表示操作数量。

接下来 N 行,每行包含一个操作指令,操作指令为 I x,Q x 中的一种。

输出格式
对于每个询问指令 Q x,输出一个询问结果,如果 x 在集合中出现过,则输出 Yes,否则输出 No。

每个结果占一行。

数据范围
1≤N≤105
−109≤x≤109
输入样例:
5
I 1
I 2
I 3
Q 2
Q 5
输出样例:
Yes
No

闭散列方法(开放寻址法):





#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 200003, null = 0x3f3f3f3f;

int n;
int h[N];

int find(int x) //开发寻址法查找
{
    int t = (x % N + N) % N;
    while (h[t] != null && h[t] != x) //不为空且值相等
        t = (t + 1) % N;
    return t;//找不到x值,最终落在未赋值的null = 0x3f3f3f3f
}

int main()
{
    memset(h, 0x3f, sizeof h);//开发寻址法,初始最大值,null定义为0x3f3f3f3f
    scanf("%d", &n);
    while (n -- )
    {
        char op[2];
        int x;
        scanf("%s%d", op, &x);   //'*'取字符串单个字符(bushi),直接op打出全部内容 
        if (*op == 'I') h[find(x)] = x;//find找到位置且赋值  
        else
        {
            if (h[find(x)] == null) puts("No");
            else puts("Yes");
        }
    }

    return 0;
}



开散列方法(拉链法):

数组模拟链表+find函数–>insert函数
int t = (x % N + N) % N ; //负数整数都转化成为正数【c++取模负数仍为负数】

深度解析数组模拟链表:
e数组存放每个idx下标值,
ne数组存放每个下标指向的下一个下标,add中指向
h数组仅存放最后添加的节点下标【当做每个下标为链表的初始起点】


#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 200003;//除余法需要先找离N最大范围最近的质数【试除法,口算也行】

int n;
int h[N], e[N], ne[N], idx;

bool find(int x)//查找t=x的hash值映射的h[t]单链表是否存在x
{
    int t = (x % N + N) % N;//除余法
    for (int i = h[t]; ~i; i = ne[i])//遍历映射hash值t所在h[t]是否存在x,~i等效 i != -1
        if (e[i] == x)
            return true;
    return false;
}

void add(int a,int b )//链表头插法  : 添加一条边 a->b 
{
	e[idx] =  b , ne[idx] = h[a] , h[a] = idx ++;
}

void insert(int x)
{
	if(find(x)) return ;
	int t = (x % N  + N) % N ; //负数整数都转化成为正数【c++取模负数仍为负数】
	add(t,x);
}
int main()
{
	memset(h , -1 , sizeof h);//二进制全为1  ,遍历邻接表 i = h[t]; ~i 按位取反二进制每位全部为0
	scanf("%d",&n);
	while(n --)
	{
		char op[2];
		int x;
		scanf("%s%d",op,&x);
		if(*op == 'I' ) insert(x);//*取字符串单个字符(bushi),直接op打出全部内容
		else 
		{
			if(find(x)) puts("Yes");
			else puts("No");
		}
	}
	return 0;
}



3820. 未出现过的最小正整数

给定一个长度为 n 的整数数组,请你找出未在数组中出现过的最小正整数。

样例
输入1:[-5, 3, 2, 3]

输出1:1

输入2:[1, 2, 3]

输出2:4

数据范围
1≤n≤105,
数组中元素的取值范围 [−109,109]。

每次填n位,若[1,n]都被填满,最小未填值为n+1
如果[1,n]有缺失,则答案在[1,n]中,即只需要管n + 1个数

class Solution {
public:
    int findMissMin(vector<int>& nums) {   //每次填n位,若1~n都被填满 ,最小未填值为n+1,如果1~n有缺失,则答案在1~n中,即只需要管n + 1个数
        int n = nums.size();
        vector<bool> hash(n + 1); //下标从1开始
        for (int x: nums)
            if (x >= 1 && x <= n)//只需要管[1,n]标记即可(比如四个数但没有全部落在[1,n],则[1,n]必有空缺,必有答案)
                hash[x] = true;//标记
        for (int i = 1; i <= n; i ++ )
            if (!hash[i])//找到答案
                return i;
        return n + 1;//没有return i 说明[1,n]全部填满 ,则最小未填值为n+1
    }
};


831. KMP字符串

给定一个字符串 S,以及一个模式串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。

模式串 P 在字符串 S 中多次作为子串出现。

求出模式串 P 在字符串 S 中所有出现的位置的起始下标。

输入格式
第一行输入整数 N,表示字符串 P 的长度。

第二行输入字符串 P。

第三行输入整数 M,表示字符串 S 的长度。

第四行输入字符串 S。

输出格式
共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。

数据范围
1≤N≤105
1≤M≤106
输入样例:
3
aba
5
ababa
输出样例:
0 2

算法思想

朴素 - 暴力 每个m的按顺序选n个匹配 模式串(n) ,最坏O(nm)

KMP优化:
匹配失败时,主串S不回溯,(最多前进一格s++)
检查next数组,(模式串自身的前后缀字符串相等长度,指针回溯到next位置(前后缀字符字符相等))

第next[j]:第j位不同时回溯到第next[j]位开始(0-next[j]位都匹配成功),从下一位j+1开始比较,j++,S主串的i++
next[1]=0,next[2]=1

nextval:匹配失败时改为next[j]但会有相同则必定失败需继续回溯,
则nextval[j]即为失败回溯的终点的位置,
(即值回溯一次之后跳过中间查找过程优化,并查集的路径压缩也是如此)

佬的思路详解

先死记:双指针+初始化next数组
next[i]表示:前i个字符构成的字符串中,前缀与后缀相等的最长的长度
下标从1开始 ,next[1] = 0;不用求,所以 i = 2开始自身匹配,j = 0相当于后缀直接从最长的开始找
**但下标用 i 和 j+1 ** 【j同时用来表示每个最长前后缀长度和已经匹配的长度,所以初始是0

考研两种考法:手写或问下标(若从0开始,下标减1即可)
考研手写判断:先找出前缀对应长度后缀,再判断

八股文考法比较次数这里的算法会多因为当j!= 0时会多判断p[i]与p[j+1],但是量级不变,无所谓


#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010, M = 1000010;

int n, m;
char p[N], s[M];//p模式串十万 , s主串百万
int ne[N];

int main()
{
    scanf("%d%s", &n, p + 1);//从1开始
    scanf("%d%s", &m, s + 1);

    for (int i = 2, j = 0; i <= n; i ++ )//模式串自身匹配,构造ne[i] 
    {	
        while (j && p[i] != p[j + 1]) j = ne[j];//j为0就到起点,不能回溯了 ,不相同时j=ne[j]回溯
        if (p[i] == p[j + 1]) j ++ ;
        ne[i] = j;//相同前后缀的长度
    }

    for (int i = 1, j = 0; i <= m; i ++ )//主串长度m与模式串匹配【简记:判读语句中变化仅前部分p换成s】
    {
        while (j && s[i] != p[j + 1]) j = ne[j];
        if (s[i] == p[j + 1]) j ++ ;
        if (j == n) printf("%d ", i - n);//输出所有出现位置的起始下标(从0开始,不用i-n+1)
    }
    return 0;
}

kmp超全

11. 排序

785. 快速排序

给定你一个长度为 n 的整数数列。

请你使用快速排序对这个数列按照从小到大进行排序。

并将排好序的数列按顺序输出。

输入格式
输入共两行,第一行包含整数 n。

第二行包含 n 个整数(所有整数均在 1∼109 范围内),表示整个数列。

输出格式
输出共一行,包含 n 个整数,表示排好序的数列。

数据范围
1≤n≤100000
输入样例:
5
3 1 2 4 5
输出样例:
1 2 3 4 5

记讲义的复杂度[最优,平均,最差](不需要会推导)
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1000010; //开越大桶排序过越多样例

int n;
int q[N];

void insert_sort()  //直接插入排序  [一个一个插入,插入到前面,要把数组整体后移] 
{
	for(int i = 1;i < n; i++)//把每个数插入到排序位置上 
	{//q[i]存t   【形象类比: 斗地主整理牌】 
		int t = q[i] , j = i;//加入新的元素q[i],遍历前i个元素,把q[i]排序到对应位置上 
		while(j && q[j - 1] > t)  //j从i开始从已经确定的尾部开始往前遍历: 所以第一个用j- 1, 找到第一个 
		{
			q[j] = q[j - 1];
			j --;	
		} 
		q[j] = t;	
	}	
}
int main()
{
	for(int i = 0;i < 10 ;i ++) scanf("%d ",&q[i]);
	//merge_sort(q,0,9); 
	insert_sort();
	
	for(int i = 0;i < 10 ;i ++) printf("%d ",q[i]);
	
	return 0;
}

insert_sort直接插入


void insert_sort()  //直接插入排序  [一个一个插入,插入到前面,要把数组整体后移] 
{
	for(int i = 1;i < n; i++)//把每个数插入到排序位置上 
	{//q[i]存t   【形象类比: 斗地主整理牌】 
		int t = q[i] , j = i;//加入新的元素q[i],遍历前i个元素,把q[i]排序到对应位置上 
		while(j && q[j - 1] > t)  //j从i开始从已经确定的尾部开始往前遍历: 所以第一个用j- 1, 找到第一个 
		{
			q[j] = q[j - 1];
			j --;	
		} 
		q[j] = t;	
	}	
}

binary_search_insert_sort折半插入

特判全有序 + 边界定义分支 + 二分找大于t + (插排)找到后for往前移动+再把第t填上q[r] = t;【l或者r都行】
[斗地主拿牌排序]



//[斗地主拿牌排序]
void binary_search_insert_sort()//折半插入排序
{
	
	for(int i = 1;i < n;i++)
	{
		//把q[i]插入到已经排序的正确位置【二分搜索位置】 
		if(q[i-1] < q[i])continue;//排序正确此趟已确定 
		int t = q[i],l = 0 , r = i - 1;// [0,i-1]已经排好序,二分查找插入 
		while(l < r)
		{
			int mid = l + r >> 1;
			if(q[mid] > t)r = mid;//已排序的元素大于t,则正确位置在左边 
			else l = mid + 1;
		}
		for (int j = i - 1; j >= r; j -- ) //腾出第i大的位置,[r,i-1]前面排好序的元素比t大的往后移动  [插入排序]   
            q[j + 1] = q[j];
        q[l] = t;//把当前元素放到r的位置上【l或者r都行】 	
	} 
}

bubble_sort冒泡



//最坏情况:逆序对数量
void bubble_sort()  // 冒泡排序
{
    for (int i = 0; i < n - 1; i ++ ) //第i次把第i个位置放上正确的元素
    {
        bool has_swap = false; //优化:没有交换则说明已经有序
        for (int j = n - 1; j > i; j -- ) //交换过程从后往前【从底下往上"冒出"】
            if (q[j] < q[j - 1])
            {
                swap(q[j], q[j - 1]);
                has_swap = true; 
            }
        if (!has_swap) break;//(没有交换)已经有序,剪枝
    }
}

select_sort选择排序


//最慢且【不稳定】(冒泡)
void select_sort()  // 简单选择排序   【每次循环:把后面没有确定位置的元素中选取最小值,放到确定位置】
{
    for (int i = 0; i < n - 1; i ++ ) //每轮确定一个最小值
    {
        int k = i;
        for (int j = i + 1; j < n; j ++ ) //每轮用k找到第i小的值(未确定元素i+1开始到n的最小值)
        {
        	 if (q[j] < q[k]) k = j;
        }
        swap(q[i], q[k]);
    }
}

//所有不稳定都可以改成稳定【但是笔试经常考这个概念,所以要注意】

shell_sort希尔排序


//每组内的下标是等差数列
//c++中的sort是快排+插排 【当排序到<28时改为插入排序】
void shell_sort()  // 希尔排序【分组的插入排序】 不稳定(间隔d的分为一组)
{
    for (int d = n / 3; d; d = d == 2 ? 1 : d / 3) //特判2,等于2就用1,(最后要用1,而2时d/3=0,所以特判) 【d为分组跨度(此时初始每组3个元素)】
    {
        for (int start = 0; start < d; start ++ )//分为d组,每组成员之间间隔为d,组内插入排序
        {
            for (int i = start + d; i < n; i += d)//比较一组 
            {
                int t = q[i], j = i;//j存同一组内小的下标 
                while (d != 0 && q[j - d] > t) //d != 0时(j > start) && 做以下标间隔d的插入排序
                {
                    q[j] = q[j - d];
                    j -= d;
                }
                q[j] = t; 
            }
        }
    }
}


//科学家证明过 用除3分组O(n^(3/2)) 
void shell_sort() //用除2,分组 O(n^2) 
{
	for(int d = n / 2; d ;d /= 2 ) //分为d组,每组成员之间间隔为d,组内插入排序 
	{
		for(int start = 0;start < d; start ++)//比较d组   
		{
			for(int i = start + d ;i < n;i += d)//比较一组 
			{
				int t = q[i] , j = i; //j存同一组内小的下标 
				while(j > start && q[j - d] > t) //相隔为d的分为一组比较 
				{
					q[j] = q[j - d];//后移,下标小的赋值给下标大的
					j -= d;
				}
				q[j] = t;
			}
		}
	}
}

heap_sort堆排序

【下标1开始】 大根堆顶最大
(出堆元素排最后从小到大排序,大的先放最后)

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;
int q[N], sz, w[N];


void down(int u)//递归 
{
    int t = u;
    if (u * 2 <= sz && q[u * 2] > q[t]) t = u * 2; //如果左儿子在界内且大于根节点 
    if (u * 2 + 1 <= sz && q[u * 2 + 1] > q[t]) t = u * 2 + 1;//如果右儿子在界内且大于根节点

    if (u != t)  
    {
        swap(q[u], q[t]);
        down(t);//递归往下调整好 
    }
}

//大根堆:满足根节点的值大于等于子节点的二叉树,子节点的分支也均满足。

void heap_sort()  // 堆排序,下标一定要从1开始!  main函数的读入和输出也从1开始
{
    sz = n;//维护size,堆有多少个元素   【y总翻车,sz没有开全局更新】
    for (int i = n / 2; i; i -- ) down(i); //最后一个节点的父节点开始 
	//i为循环次数而已 ,多一次sz = 1 自己与自己交换也不影响  
    for (int i = 0; i < n - 1; i ++ )//出堆共n-1次,剩下一个最小在正确位置上
    {
        swap(q[1], q[sz]);//【大根堆,堆顶最大:从小到大】堆顶与最后结点交换,放在编号最大的位置,出堆 
        sz -- ;//出堆 【只剩下sz个元素需要排序 
        down(1);//从堆顶往下调整 
    }
}

int main()//【下标从1开始读入!!!】
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &q[i]);
    heap_sort(); //下标一定要从1开始
    for (int i = 1; i <= n; i ++ ) printf("%d ", q[i]);
    return 0;
}



quick_sort快速排序


void quick_sort(int l, int r)  // 快速排序
{
    if (l >= r) return;
    int i = l - 1, j = r + 1, x = q[(l + r) / 2];
    while (i < j)
    {
        do i ++ ; while (q[i] < x);
        do j -- ; while (q[j] > x);
        if (i < j) swap(q[i], q[j]);
    }
    quick_sort(l, j);
    quick_sort(j + 1, r);
}

merge_sort归并排序

int w[N];
void merge_sort(int l, int r)
{
    if (l >= r) return;
    int mid = l + r >> 1;
    merge_sort(l, mid), merge_sort(mid + 1, r);
    int i = l, j = mid + 1, k = 0;
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) w[k ++ ] = q[i ++ ]; //等于的最好放前面【保持稳定性】
        else w[k ++ ] = q[j ++ ];
    while (i <= mid) w[k ++ ] = q[i ++ ];
    while (j <= r) w[k ++ ] = q[j ++ ];
    for (i = l, j = 0; j < k; i ++, j ++ ) q[i] = w[j];
}

可以j < k 也可以 i <= r :

int tmp[N];//可以换成w[N] , 但是tmp写习惯了也快
void merge_sort(int l,int r)
{
	if(l >= r)return;
	int mid = l + r >> 1;
	merge_sort(l,mid),merge_sort(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++];
	
	while(i <= mid)tmp[k++] = q[i++];
	while(j <= r)tmp[k++] = q[j++];
	
	for(int i = l,j = 0;j < k;j++,i++)q[i] = tmp[j]; //j < k 等效(i <= r) : [l,j]段
}



int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);

    // insert_sort();
    // binary_search_insert_sort();
    // bubble_sort();
    // select_sort();
    // shell_sort();
    // quick_sort(0, n - 1);
    // heap_sort();
    merge_sort(0, n - 1);


    for (int i = 0; i < n; i ++ ) printf("%d ", q[i]);
    return 0;
} 

bucket_sort桶排序(计数排序)

简记:小于a[i]的元素个数-在a[i]右边的元素个数=a[i]要移动的距离

思想:【统计相对左边比自身小的个数,把元素放到正确的位置上】
多个相同值为x元素 【假设具有稳定性】,最后一个为可以为 S x − 1 + n 表示相同值的第 n 个 S_{x-1} + n表示相同值的第n个 Sx1+n表示相同值的第n
实际写成 S x − 1 S_x - 1 Sx1:表示把最后一个x放到下一个比x大的值的元素的前面一个位置【因此从后往前摆放】
前缀和s[i]: 开n个桶计算比左边自身小的个数作为a[i],计算a[i]的前缀和
桶里有多少元素:每个桶表示小于值1-5

发现好像仅仅能实现值相邻的元素排序 , 如1 2 3 4 100 ,则在最后w[i]赋值不到100的位置


#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1000010; //开越大桶排序过越多样例

int n;
int q[N], w[N], s[N];//w写入防覆盖辅助数组,s表示等于下标值的元素个数

void bucket_sort()//桶排序(计数排序)    y总图:类似二叉树 划分 < 和 >=
{
    for (int i = 0; i < n; i ++ ) s[q[i]] ++ ; //统计每个桶里有多少个的元素【先把值相同的放在一个桶】
    for (int i = 1; i < N; i ++ ) s[i] += s[i - 1]; //计算左边比自身小的个数a[i]的前缀和s[i]  【注意开到题目要求的N个桶】比s[i]左边小的元素数量即为s[i-1] 【用到s[i-1]从一开始!】
    for (int i = n - 1; i >= 0; i -- ) w[ -- s[q[i]]] = q[i]; //从后往前! 将每个元素放在对应的位置上 [先往前移动,s同时桶的元素个数也就少一,同时也让相同值的元素放到了不同位置上]  
    for (int i = 0; i < n; i ++ ) q[i] = w[i]; //写到另一个数组,读写数组w[N],防止读写冲突【w为辅助数组,防止排序过程中覆盖】
}

radix_sort基数排序【桶排序优化】

简记: 初始化,下标用到j-1,从1开始 , j = n -1从后往前填位置 ,最后读回 
取对应轮的位数:q[j] / radix % r 

多关键字排序:拆成每位比较排序,位数决定排序轮数
d次桶排序【在前面位数排好序的情况下,每次仅当前位排好序】【具有稳定性


//高位到低位需要递归,低位到高位不需要  

void radix_sort(int d, int r)//d:关键字的数量(位数) ,r:进制位基数
{
    int radix = 1;//个位开始 /radix % r进制 取对应第d轮位置的元素值
    for (int i = 1; i <= d; i ++ ) //枚举所有关键字【d次桶排序】
    {
        for (int j = 0; j < r; j ++ ) s[j] = 0; //首先初始所有桶
        for (int j = 0; j < n; j ++ ) s[q[j] / radix % r] ++ ; //统计下桶里有多少个元素
        for (int j = 1; j < r; j ++ ) s[j] += s[j - 1]; //求前缀和
        for (int j = n - 1; j >= 0; j -- ) w[ -- s[q[j] / radix % r]] = q[j];//每个元素从后往前摆放,放到对应的位置上
        for (int j = 0; j < n; j ++ ) q[j] = w[j];
        radix *= r;//做完个位做十位、再百位、再千位......
    }
}

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);

    // bucket_sort();
    radix_sort(10, 10);

    for (int i = 0; i < n; i ++ ) printf("%d ", q[i]);

    return 0;
}


外部排序讲解

多路归并:多个指针选取最小加到tmp数组,如果共m路,每次k路归并,则时间复杂度 O ( l o g k m ) O(log_k^m) O(logkm)

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

胜者树省略了调整操作,更快
改进:败者树【少访问一半的结点】

12. 排序、多路归并和摩尔投票法


选择题:
能不能优化:折半插入和直接插入的不同  
快排:教材上的是可以保证第i次可以把元素放到i位置上 [y总的是短的优化版]   
【反过来:第i趟排序就有i个元素在正确的位置上(指左边比它小,右边比它大)】 
堆排序也可以每次第i位正确放置 
第几次排就当几位数看 :基数排序 

数组改用链表会变慢的是含有随机索引操作[一定需要下标,间隔一个可以不用下标] : 数组O(1) , 链表遍历O(n) 

y总带入答案测试,不对排除

堆:每次小三角(三个元素)往上递归比较 

1603. 整数集合划分

给定一个包含 N 个正整数的集合,请你将它划分为两个集合 A1 和 A2,其中 A1 包含 n1 个元素,A2 包含 n2 个元素。

集合中可以包含相同元素。

用 S1 表示集合 A1 内所有元素之和,S2 表示集合 A2 内所有元素之和。

请你妥善划分,使得 |n1−n2| 尽可能小,并在此基础上 |S1−S2| 尽可能大。

输入格式
第一行包含整数 N。

第二行包含 N 个正整数。

输出格式
在一行中输出 |n1−n2| 和 |S1−S2|,两数之间空格隔开。

数据范围
2≤N≤105,
保证集合中各元素以及所有元素之和小于 231

输入样例1:
10
23 8 10 99 46 2333 46 1 666 555
输出样例1:
0 3611
输入样例2:
13
110 79 218 69 3721 100 29 135 2 6 13 5188 85
输出样例2:
1 9359
n为偶数对半分, n为奇数:下标[0,n/2 - 1]前半段少一个n/2,n]都是后半段
前后半段数量固定,后半段值应尽可能多地选取最大



#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;
int w[N];

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", &w[i]);
    sort(w, w + n);

    int s1 = 0, s2 = 0;
    for (int i = 0; i < n / 2; i ++ ) s1 += w[i];
    for (int i = n / 2; i < n; i ++ ) s2 += w[i];

    printf("%d %d\n", n % 2, s2 - s1);//n偶数相等,奇数相差1

    return 0;
}

3874. 三元组的最小距离

定义三元组 (a,b,c)(a,b,c 均为整数)的距离 D=|a−b|+|b−c|+|c−a|。

给定 3 个非空整数集合 S1,S2,S3,按升序分别存储在 3 个数组中。

请设计一个尽可能高效的算法,计算并输出所有可能的三元组 (a,b,c)(a∈S1,b∈S2,c∈S3)中的最小距离。

例如 S1={−1,0,9},S2={−25,−10,10,11},S3={2,9,17,30,41} 则最小距离为 2,相应的三元组为 (9,10,9)。

输入格式
第一行包含三个整数 l,m,n,分别表示 S1,S2,S3 的长度。

第二行包含 l 个整数,表示 S1 中的所有元素。

第三行包含 m 个整数,表示 S2 中的所有元素。

第四行包含 n 个整数,表示 S3 中的所有元素。

以上三个数组中的元素都是按升序顺序给出的。

输出格式
输出三元组的最小距离。

数据范围
1≤l,m,n≤105,
所有数组元素的取值范围 [−109,109]。

输入样例:
3 4 5
-1 0 9
-25 -10 10 11
2 9 17 30 41
输出样例:
2

贪心[数组已排序]

我の理解:
假设已经k路归并, 以a[i]为左端点,需找到最小的区间能包含ABC中的数[找到B后面第一个A和C],则最小
过程用指针遍历三个数组比较大小,则不用标记组别 [A, B, C集合是有序的, 可以直接从小到大遍历]
【最优解与三个数组元素的顺序没有关系,仅看数值大小就行】,如都取最优解, 则[都为最小值取下标最小,指针指向位置不变]
数据范围max - min > int 则res用LL

暴力法链接

佬の理解:

由D的表达式可以看出, D = 2 * [max(a, b, c) - min(a, b, c)]; (数轴上比较容易看到)
要让对于一个给定的abc, 要让D尽量小, 可以让最小值尽量大或者最大值尽量小;
S1, S2, S3集合是有序的, 因此可以从小到达开始归并;
每次让最小值变大并记录res的最小值即可;

//分析过程归并,但是由于多个最小值,取ABC任意一个当最小都能得到最优解,代码实现为三指针
//由规律发现:B当做左端点,AC下一个元素就是第一个大于B的元素,当做右端点
//最终距离是左右端点距离的两倍   ==> 2 * (c - a)

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 100010;

int l, m, n;
int a[N], b[N], c[N];

int main()
{
    scanf("%d%d%d", &l, &m, &n);
    for (int i = 0; i < l; i ++ ) scanf("%d", &a[i]);
    for (int i = 0; i < m; i ++ ) scanf("%d", &b[i]);
    for (int i = 0; i < n; i ++ ) scanf("%d", &c[i]);

    LL res = 1e18;
    for (int i = 0, j = 0, k = 0; i < l && j < m && k < n;)
    {
        int x = a[i], y = b[j], z = c[k];
        res = min(res, (LL)max(max(x, y), z) - min(min(x, y), z)); //找min(max-min)
        if (x <= y && x <= z) i ++ ;//x最小,后移
        else if (y <= x && y <= z) j ++ ;//y最小,后移
        else k ++ ;//z最小,后移
    }

    printf("%lld\n", res * 2);
    return 0;
}

52. 数组中出现次数超过一半的数字

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。

假设数组非空,并且一定存在满足条件的数字。

思考题:

假设要求只能使用 O(n) 的时间和额外 O(1) 的空间,该怎么做呢?
数据范围
数组长度 [1,1000]。

样例
输入:[1,2,1,1,3]

回忆比创造简单一万倍!【所以平时先练习到相似的题目是制胜关键】

简记:val存最多的数,计数相同cnt++,不同cnt--,
减到0,value换成其它数,最终剩下的数就是次数最多的

摩尔投票法
此题:同元素数量相加,不同元素cnt- -, 初始和过程中cnt = 0代表没有元素,下一个元素读取直接赋值,同时cnt++



class Solution {
public:
    int moreThanHalfNum_Solution(vector<int>& nums) {
        int cnt = 0, val;
        for (auto x: nums) {
            if (!cnt) val = x, cnt ++ ;
            else if (x == val) cnt ++ ;
            else cnt -- ;
        }
        /*【此题保证有解】若出现无解:则最多的元素个数不超过总数的一半
        cnt = 0;
        for(auto x: nums)
            if(x == val) cnt ++;
        if(cnt < nums.size() / 2) return -1;
        */
        return val;
    }
};

13. DFS

3429. 全排列

给定一个由不同的小写字母组成的字符串,输出这个字符串的所有全排列。

我们假设对于小写字母有 a<b<…<y<z,而且给定的字符串中的字母已经按照从小到大的顺序排列。

输入格式
输入只有一行,是一个由不同的小写字母组成的字符串,已知字符串的长度在 1 到 6 之间。

输出格式
输出这个字符串的所有排列方式,每行一个排列。

要求字母序比较小的排列在前面。

字母序如下定义:

已知 S=s1s2…sk,T=t1t2…tk,则 S<T 等价于,存在 p(1≤p≤k),使得 s1=t1,s2=t2,…,sp−1=tp−1,sp<tp 成立。

数据范围
字符串的长度在 1 到 6 之间

输入样例:
abc
输出样例:
abc
acb
bac
bca
cab
cba

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 10;

char str[N],path[N];
bool st[N];
int n;
void dfs(int u)
{
  	//if (u == n) cout << path << endl;
    if(u == n)  puts(path);
   
        
    for(int i = 0; i < n ; i++)
    {
        if(!st[i])
        {
            path[u] = str[i];
            st[i] = true;
            dfs(u + 1);
            st[i] = false;
        }
    }
}

int main()
{
    cin >> str;
    n = strlen(str);
    dfs(0);
    return 0;
}

3472. 八皇后

会下国际象棋的人都很清楚:皇后可以在横、竖、斜线上不限步数地吃掉其他棋子。

如何将 8 个皇后放在棋盘上(有 8×8 个方格),使它们谁也不能被吃掉!

这就是著名的八皇后问题。

对于某个满足要求的 8 皇后的摆放方法,定义一个皇后串 a 与之对应,即 a=b1b2…b8,其中 bi 为相应摆法中第 i 行皇后所处的列数。

已经知道 8 皇后问题一共有 92 组解(即 92 个不同的皇后串)。

给出一个数 b,要求输出第 b 个串。

串的比较是这样的:皇后串 x 置于皇后串 y 之前,当且仅当将 x 视为整数时比 y 小。

输入格式
第一行包含整数 n,表示共有 n 组测试数据。

每组测试数据占 1 行,包括一个正整数 b。

输出格式
输出有 n 行,每行输出对应一个输入。

输出应是一个正整数,是对应于 b 的皇后串。

数据范围
1≤b≤92
输入样例:
2
1
92
输出样例:
15863724
84136275

对角线记为y=x+b 与 y = -x+b
简记: if (!col[i] && !dg[u + i] && !udg[8-u+i]) 若n皇后8改用n

取第n种方案
i f ( − − n = = 0 ) if ( -- n == 0) if(n==0)

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 20;//用到反对角线2*n - 1 开20

int n;
bool col[N], dg[N], udg[N];
int path[N];

bool dfs(int u)
{
    if (u == 8) 
    {
        if ( -- n == 0) //【写法】【语法:每轮--n, 判断是否为对应第n种方案 】
        {
            for (int i = 0; i < 8; i ++ ) //输出
                cout << path[i];
            cout << endl;
            return true;
        }
        return false;
    }

    for (int i = 0; i < 8; i ++ )
        if (!col[i] && !dg[u + i] && !udg[u - i + 8])
        {
            col[i] = dg[u + i] = udg[u - i + 8] = true;
            path[u] = i + 1; //输出的下标从1开始
            if (dfs(u + 1)) return true;
            col[i] = dg[u + i] = udg[u - i + 8] = false;
        }
    return false;
}

int main()
{
    int T;
    cin >> T;
    while (T -- )
    {
        cin >> n;
        memset(col, 0, sizeof col);
        memset(dg, 0, sizeof dg);
        memset(udg, 0, sizeof udg);
        dfs(0);
    }
    return 0;
}

n-皇后——算法基础课模板题

14. 模拟、递推、BFS

3379. 反序输出

输入任意 4 个字符(如:abcd), 并按反序输出(如:dcba)。

输入格式
输入包含多组测试数据。

每组数据占一行,包含四个字符(大小写字母)。

输出格式
对于每组输入,输出一行反序后的字符串。

数据范围
输入最多包含 100 组数据。

输入样例:
Upin
cvYj
WJpw
cXOA
输出样例:
nipU
jYvc
wpJW
AOXc

algorithm库 :reverse + 参数string : [s.begin() , s.end()]


#include<iostream>
#include<string>
#include<algorithm>//reverse函数

using namespace std;

string s;
int main()
{
    while(cin >> s)
    {
        reverse(s.begin(),s.end());// string : [begin , end]
        cout << s << endl;
    }
    
    return 0;
}

3390. 特殊乘法

给定一个 n 位整数 A,各位从高到低依次为 a1,a2,…,an。

给定一个 m 位整数 B,各位从高到低依次位 b1,b2,…,bm。

给定一种特殊乘法,不妨用 ⊗ 来表示,我们规定:
A ⊗ B = ∑ i = 1 n ∑ j = 1 m a i × b j A⊗B=∑_{i=1}^n∑_{j=1}^ma_i×b_j AB=i=1nj=1mai×bj
例如,123⊗45=1×4+1×5+2×4+2×5+3×4+3×5。

对于给定的 A 和 B,请你计算并输出 A⊗B 的值。

输入格式
两个整数 A 和 B。

输出格式
一个整数,表示 A⊗B 的值。

数据范围
1≤A,B≤109
输入样例:
123 45
输出样例:
54

∑a每位*b每位

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

string a,b;
int res;
int main()
{
    cin >> a >> b;
    
    for(auto i : a) //也可用x ,y
        for(auto j : b) 
            res += (i - '0') * (j - '0');
    
    cout << res << endl;        
            
    return 0;
}

3397. 众数

给定一个整数序列,其中包含 n 个非负整数,其中的每个整数都恰好有 m 位,从最低位到最高位,依次编号为 1∼m 位。

现在,请你统计该序列各个位的众数。

第 i 位的众数是指,在给定的 n 个整数的第 i 位上,出现次数最多的最小数字。

输入格式
第一行包含两个整数 n 和 m。

第二行包含 n 个非负整数,表示给定的整数序列。

输出格式
共 m 行,第 i 行输出第 i 位的众数。

数据范围
1≤n≤105,
1≤m≤6
输入样例:
3 2
31 32 30
输出样例:
0
3

取位数++,从小到大枚举
判断第m位上数量最多的数,0->9从小到大,满足如果数量一样,则取小的


#include <iostream>

using namespace std;

int s[6][10];

int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    while (n -- )//n个数
    {
        int x;
        scanf("%d", &x);
        for (int i = 0; i < m; i ++ )//m位
        {
            s[i][x % 10] ++ ;//取位数,对应值数量++
            x /= 10;
        }
    }

    for (int i = 0; i < m; i ++ )//判断第m位上数量最多的数,0->9从小到大,满足如果数量一样,则取小的
    {
        int k = 0;
        for (int j = 0; j < 10; j ++ )
            if (s[i][k] < s[i][j])
                k = j;
        printf("%d\n", k);
    }

    return 0;
}

3426. 糖果分享游戏

一些学生围坐一圈,中间站着他们的老师,所有人都面向老师。

他们要玩一个有关糖果分享的游戏。

每个学生最开始都有一定数量的糖果(保证一定是偶数)。

每轮游戏的进程为:

老师吹起哨声,所有学生同时拿出自己一半数量的糖果,递给右边相邻的同学。
传递完成后,所有拥有奇数数量糖果的同学都将再得到一颗糖果。
游戏将不断进行,直到所有学生拥有的糖果数量均相等为止。

现在,给定所有学生的初始糖果数量,请确定游戏进行的总轮次数以及游戏结束后每个学生的糖果数量。

输入格式
输入可能包含多组数据。

每组数据第一行包含整数 N,表示学生数量。

接下来 N 行,以逆时针方向描述每个学生的初始糖果数量,每行包含一个整数。

当输入一行 N=0 时,表示输入结束。

输出格式
每组数据输出一个结果,占一行。

首先输出游戏总轮次,然后输出游戏结束后每个人的糖果数量。

游戏一定会在有限轮次内结束,原因如下:

设每轮游戏开始前,拥有最多糖果的人的糖果数量为 max,拥有最少糖果的人的糖果数量为 min,那么:

每轮过后,max 的值都不会增加。
每轮过后,min 的值都不会减少。
某轮开始前,拥有糖果数量大于 min 的所有人在该轮结束后,拥有的糖果数量也一定大于该轮开始前的 min。
某轮开始前,如果 min 和 max 不相等,那么至少一个拥有 min 个糖果的人在该轮结束后,拥有糖果数量会增加。
数据范围
1≤N≤100,
每个学生的初始糖果数量不超过 100,且一定是偶数。
每个输入最多包含 100 组数据。

输入样例:
6
36
2
2
2
2
2
11
22
20
18
16
14
12
10
8
6
4
2
4
2
4
6
8
0
输出样例:
15 14
17 22
4 8

技巧避免取模负数 +n , 则前一位同学下标(x-1 + n ) % n
q[(i + n - 1) % n]为了处理0号同学上一位下标为n-1



#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 110;

int n;
int q[N], w[N];

int main()
{
    while (cin >> n, n)//学生数量:n行每个学生的初始糖果数量,读到0结束
    {
        for (int i = 0; i < n; i ++ ) cin >> q[i];

        int cnt = 0;//轮数
        while (true)
        {
            bool is_same = true;
            for (int i = 1; i < n; i ++ )//判断糖果数量是否全部相等
                if (q[i] != q[0])//不等继续
                    is_same = false;
            if (is_same) break;//相等结束
            cnt ++ ;//记录交换轮数
            for (int i = 0; i < n; i ++ )     //本身还要再拿出一半和收到的一半,所以推导成此式,q[(i + n - 1) % n]为了处理0号同学上一位下标为n-1
                w[i] = (q[i] + q[(i + n - 1) % n]) / 2;//不相等再次按规则分配【相邻两个平均分】,圈的环绕下标写法,w辅助,改完后写入q
            for (int i = 0; i < n; i ++ )
            {
                q[i] = w[i];
                if (q[i] % 2) q[i] ++ ;//每轮交换完后,所有拥有奇数数量糖果的同学都将再得到一颗糖果
            }
        }

        printf("%d %d\n", cnt, q[0]);//轮数,平均值
    }

    return 0;
}

递推:

3392. 递推数列

给定 a 0 , a 1 ,以及 a n = p × a n − 1 + q × a n − 2 中的 p , q 。 给定 a_0,a_1,以及 a_n=p×a_{n−1}+q×a{n−2} 中的 p,q。 给定a0,a1,以及an=p×an1+q×an2中的p,q

这里 n≥2。

求第 k 个数 a k a_k ak 对 10000 的模。

输入格式
输入包括 5 个整数: a 0 、 a 1 、 p 、 q 、 k 。 a_0、a_1、p、q、k。 a0a1pqk

输出格式
第 k 个数 a k a_k ak 对 10000 的模。

数据范围
1 ≤ a 0 , a 1 , p , q , k ≤ 10000 1≤a_0,a_1,p,q,k≤10000 1a0,a1,p,q,k10000
输入样例:
20 1 1 14 5
输出样例:
8359

#include <iostream>

using namespace std;

const int N = 10010, MOD = 10000;

int a[N];

int main()
{
    int p, q, k;
    cin >> a[0] >> a[1] >> p >> q >> k;
    for (int i = 2; i <= k; i ++ )
        a[i] = (p * a[i - 1] + q * a[i - 2]) % MOD;//依题意
    cout << a[k] % MOD << endl;

    return 0;
}

3433. 吃糖果

名名的妈妈从外地出差回来,带了一盒好吃又精美的巧克力给名名(盒内共有 N 块巧克力)。

妈妈告诉名名每天可以吃一块或者两块巧克力。

假设名名每天都吃巧克力,问名名共有多少种不同的吃完巧克力的方案。

例如:如果 N=1,则名名第 1 天就吃掉它,共有 1 种方案;如果 N=2,则名名可以第 1 天吃 1 块,第 2 天吃 1 块,也可以第 1 天吃 2 块,共有 2 种方案;如果 N=3,则名名第 1 天可以吃 1 块,剩 2 块,也可以第 1 天吃 2 块剩 1 块,所以名名共有 2+1=3 种方案;如果 N=4,则名名可以第 1 天吃 1 块,剩 3 块,也可以第 1 天吃 2 块,剩 2 块,共有 3+2=5 种方案。

现在给定 N,请你写程序求出名名吃巧克力的方案数目。

输入格式
输入只有 1 行,即整数 N。

输出格式
输出名名吃巧克力的方案数目。

数据范围
1≤N≤20
输入样例:
4
输出样例:
5

递推

#include<iostream>

using namespace std;

const int N = 30;

int n;
int a[N];

int main()
{
    cin >> n;
    a[0] = a[1] = 1;
    for(int i = 2;i <= n;i++)a[i] = a[i - 1] + a[i - 2];//推导由最后一天吃1或2,则由前一天a[n-1]和a[n-2]转移到a[n]

    cout << a[n] << endl;

    return 0;
}

BFS:

3385. 玛雅人的密码

玛雅人有一种密码,如果字符串中出现连续的 2012 四个数字就能解开密码。

给定一个长度为 N 的字符串,该字符串中只含有 0,1,2 三种数字。

可以对该字符串进行移位操作,每次操作可选取相邻的两个数字交换彼此位置。

请问这个字符串要移位几次才能解开密码。

例如 02120 经过一次移位,可以得到 20120,01220,02210,02102,其中 20120 符合要求,因此输出为 1。

如果无论移位多少次都解不开密码,输出 −1。

输入格式
第一行包含一个整数 N,表示字符串的长度。

第二行包含一个由 0,1,2 组成的,长度为 N 的字符串。

输出格式
若可以解出密码,则输出最少的移位次数;否则输出 −1。

数据范围
2≤N≤13
输入样例:
5
02120
输出样例:
1

最小次数 :BFS


#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <unordered_map>

using namespace std;

int n;

int bfs(string start)//初始状态
{
    queue<string> q;//较为固定BFS写法
    unordered_map<string, int> dist;// dist[string t] = int 步数
    dist[start] = 0;
    q.push(start);

    while (q.size())//遍历队列
    {
        auto t = q.front();
        q.pop();

        for (int i = 0; i < n; i ++ )
            if (t.substr(i, 4) == "2012")//调用类函数子串从i开始,取4位
                return dist[t];//匹配成功,返回

        for (int i = 1; i < n; i ++ )
        {
            string r = t;//r辅助
            swap(r[i], r[i - 1]);//依题意交换
            if (!dist.count(r))//当前的状态能否被扩展出来(0表示还没有从此处判断成功),还没有找到则从r继续,步数++
            {
                dist[r] = dist[t] + 1;//string t转化到string r ,步数++
                q.push(r);//放入交换后的string r状态,继续判断
            }
        }
    }
    return -1;//不行返回-1
}

int main()
{
    string start;
    cin >> n >> start;
    cout << bfs(start) << endl;
    return 0;
}

3402. 等差数列

有一个特殊的 n 行 m 列的矩阵 Aij,每个元素都是正整数,每一行和每一列都是独立的等差数列。

在某一次故障中,这个矩阵的某些元素的真实值丢失了,被重置为 0。

现在需要你想办法恢复这些元素,并且按照行号和列号从小到大的顺序(行号为第一关键字,列号为第二关键字,从小到大)输出能够恢复的元素。

输入格式
输入的第一行包含两个正整数 n 和 m。

接下来 n 行,每行 m 个整数,表示整个矩阵,如果 Aij 大于 0,则表示真实值存在的元素。如果 Aij 等于 0,表示真实值丢失的元素。

输出格式
输出若干行,表示所有能够恢复的元素。每行三个整数 i,j,x,表示 Aij 的真实值是 x。

矩阵左上角的元素为 A11,右下角的元素为 Anm。

数据范围
1 ≤ n , m ≤ 1 0 3 1≤n,m≤10^3 1n,m103
1 ≤ A i j ≤ 1 0 9 1≤A_{ij}≤10^9 1Aij109
输入样例:
3 4
1 2 0 0
0 0 0 0
3 0 0 0
输出样例:
1 3 3
1 4 4
2 1 2
样例解释
可以恢复 3 个元素,A13 的真实值是 3,A14 的真实值是 4,A21 的真实值是 2。

BFS还需努力,orz

还需努力,orz
//存在2个非0,已知信息>= 2,此行(列)可恢复:知道a,d,每次恢复完需要重新判断是否>=两个不为0【动态迭代,满足此条件加入集合】
#include <iostream>
#include <cstring>
#include <algorithm>

#define x first
#define y second

using namespace std;

typedef pair<int, int> PII;

const int N = 1010, M = N * 2;

int n, m;
int row[N], col[N];//表示每行(列)有多少个非0元素
int q[M], hh, tt = -1;
bool st[M];// 行:[0,n-1] , 列:[n,n+m-1]
PII ans[N * N];
int top;
int g[N][N];

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < m; j ++ )
        {
            scanf("%d", &g[i][j]);
            if (g[i][j])//对应行列,元素数量++
            {
                row[i] ++ ;
                col[j] ++ ;
            }
        }

    for (int i = 0; i < n; i ++ )
        if (row[i] >= 2 && row[i] < m)//如果已经等于m,说明已经全部填满了,不用填了 
        {
            q[ ++ tt] = i;
            st[i] = true;
        }
    for (int i = 0; i < m; i ++ )
        if (col[i] >= 2 && col[i] < n)
        {
            q[ ++ tt] = i + n;
            st[i + n] = true;
        }

    while (hh <= tt)
    {
        auto t = q[hh ++ ];
        if (t < n)  // 行
        {
            PII p[2];
            int cnt = 0;
            for (int i = 0; i < m; i ++ )
                if (g[t][i])//行存在信息,若非0位数>=2,执行bfs
                {
                    p[cnt ++ ] = {i, g[t][i]};//存入t行i列 ,值g[t][i]
                    if (cnt == 2) break;
                }
            int d = (p[1].y - p[0].y) / (p[1].x - p[0].x);//求公差
            int a = p[1].y - d * p[1].x;//求首项

            for (int i = 0; i < m; i ++ )
                if (!g[t][i])
                {
                    g[t][i] = a + d * i;
                    ans[top ++ ] = {t, i};
                    col[i] ++ ;//动态更新列的非0信息个数
                    if (col[i] >= 2 && col[i] < m && !st[i + n])//若有新的列已知信息非0位 >= 2 , 更新标记,加入答案队列
                    {
                        q[ ++ tt] = i + n; 
                        st[i + n] = true;
                    }
                }
        }
        else  // 列
        {
            t -= n;//列 : st中[n,n+m-1] 对应col中[0,m-1]
            PII p[2];
            int cnt = 0;
            for (int i = 0; i < n; i ++ ) //从行转列代码 :换n、row、g[i][t]   第i列t行
                if (g[i][t])
                {
                    p[cnt ++ ] = {i, g[i][t]};
                    if (cnt == 2) break;
                }
            int d = (p[1].y - p[0].y) / (p[1].x - p[0].x);
            int a = p[1].y - d * p[1].x;
            for (int i = 0; i < n; i ++ )
                if (!g[i][t])
                {
                    g[i][t] = a + d * i;
                    ans[top ++ ] = {i, t};
                    row[i] ++ ;
                    if (row[i] >= 2 && row[i] < n && !st[i])
                    {
                        q[ ++ tt] = i;
                        st[i] = true;
                    }
                }
        }
    }

    sort(ans, ans + top);//pair默认按first排序
    for (int i = 0; i < top; i ++ )
    {
        auto& p = ans[i];//;类似指针取地址,输出答案二元组 {x,y} 【从0开始,答案输出:偏移p.x+1,p.y+1】
        printf("%d %d %d\n", p.x + 1, p.y + 1, g[p.x][p.y]);//对应值已经修改
    }

    return 0;
}

15. 字符串处理、递归和背包问题

字符串处理:

3439. 首字母大写

对一个字符串中的所有单词,如果单词的首字母不是大写字母,则把单词的首字母变成大写字母。

在字符串中,单词之间通过空格(不一定单个)分隔。

输入格式
一行,一个长度不超过 100 的字符串(中间可能包含空格)。

输出格式
一行,输出转换后的字符串。

输入样例:
if so, you already have a google account. you can sign in on the right.
输出样例:
If So, You Already Have A Google Account. You Can Sign In On The Right.
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;



int main()
{
    string s;
    getline(cin,s);
    
    for(int i = 0;i < s.size();i++)
        if(!i || s[i - 1] == ' ')  //句首第一个,或者句中前一个为空格
            cout << (char)toupper(s[i]); 
        else cout << s[i];   
 
    return 0;
}

3406. 日志排序

有一个网络日志,记录了网络中计算任务的执行情况,每个计算任务对应一条如下形式的日志记录:

hs_10000_p 2007-01-17 19:22:53,315 253.035(s)

其中 hs_10000_p 是计算任务的名称,2007-01-17 19:22:53,315 是计算任务开始执行的时间“年-月-日 时:分:秒,毫秒”,253.035(s) 是计算任务消耗的时间(以秒计)

请你写一个程序,对日志中记录计算任务进行排序。

时间消耗少的计算任务排在前面,时间消耗多的计算任务排在后面。

如果两个计算任务消耗的时间相同,则将开始执行时间早的计算任务排在前面。

输入格式
日志中每个记录是一个字符串,每个字符串占一行。最后一行为空行,表示日志结束。

计算任务名称的长度不超过 10,开始执行时间的格式是 YYYY-MM-DD HH:MM:SS,MMM,消耗时间小数点后有三位数字。

计算任务名称与任务开始时间、消耗时间之间以一个或多个空格隔开。

输出格式
排序好的日志记录。每个记录的字符串各占一行。

输入的格式与输入保持一致,输入包括几个空格,你的输出中也应该包含同样多的空格。

数据范围
日志中最多可能有 10000 条记录。
保证不存在开始执行时间和计算任务消耗时间都相同的任务。
开始执行时间保证合法,任务消耗时间不超过 10000。

输入样例:
hs_10000_p 2007-01-17 19:22:53,315 253.035(s)
hs_10001_p 2007-01-17 19:22:53,315 253.846(s)
hs_10002_m 2007-01-17 19:22:53,315 129.574(s)
hs_10002_p 2007-01-17 19:22:53,315 262.531(s)
hs_10003_m 2007-01-17 19:22:53,318 126.622(s)
hs_10003_p 2007-01-17 19:22:53,318 136.962(s)
hs_10005_m 2007-01-17 19:22:53,318 130.487(s)
hs_10005_p 2007-01-17 19:22:53,318 253.035(s)
hs_10006_m 2007-01-17 19:22:53,318 248.548(s)
hs_10006_p 2007-01-17 19:25:23,367 3146.827(s)

输出样例:
hs_10003_m 2007-01-17 19:22:53,318 126.622(s)
hs_10002_m 2007-01-17 19:22:53,315 129.574(s)
hs_10005_m 2007-01-17 19:22:53,318 130.487(s)
hs_10003_p 2007-01-17 19:22:53,318 136.962(s)
hs_10006_m 2007-01-17 19:22:53,318 248.548(s)
hs_10000_p 2007-01-17 19:22:53,315 253.035(s)
hs_10005_p 2007-01-17 19:22:53,318 253.035(s)
hs_10001_p 2007-01-17 19:22:53,315 253.846(s)
hs_10002_p 2007-01-17 19:22:53,315 262.531(s)
hs_10006_p 2007-01-17 19:25:23,367 3146.827(s)

语法题【需理解stringstream输入输出流】
大佬の话

O(nlogn)
使用了c++11支持的匿名函数(非常方便好用)
stringstream 作为字符串输入输出流,可以替代cin cout使用
sscanf可以从给定的char[]中读取数据

#include <iostream>
#include <cstring>
#include <algorithm>
#include <sstream>

using namespace std;

const int N = 10010;

int n;
string logs[N];

int main()
{
    while (getline(cin, logs[n]))
        if (logs[n].size()) n ++ ;
        else break;

    sort(logs, logs + n, [](string& a, string &b) {
        stringstream ssina(a), ssinb(b); //把字符串转化成类似cin ,可以读入各种类型
        string sa[4], sb[4];
        for (int i = 0; i < 4; i ++ )
        {
            ssina >> sa[i];
            ssinb >> sb[i];
        }

        if (sa[3] == sb[3]) return sa[1] + sa[2] < sb[1] + sb[2];

        double ta, tb;
        sscanf(sa[3].c_str(), "%lf(s)", &ta);
        sscanf(sb[3].c_str(), "%lf(s)", &tb);
        return ta < tb;
    });

    for (int i = 0; i < n; i ++ )
        cout << logs[i] << endl;

    return 0;
}

3504. 字符串转换整数

给定一个字符串,字符串由数字和大小写字母构成,请你找到并输出其中的第一个有效整数。

如果无法找到有效整数,或者找到的有效整数超过 int 范围,则输出 −1。

输入格式
一行一个字符串。

输出格式
一个整数表示结果。

数据范围
字符串长度不超过 100
输入样例1:
2016
输入样例1:
2016
输入样例2:
o627CSo1
输出样例2:
627
输入样例3:
123456789123abc
输出样例3:
-1
输入样例4:
abc
输出样例4:
-1

#include <iostream>
#include <cstring>
#include <algorithm>
#include<limits.h>

using namespace std;
typedef long long LL;
LL x;

int main()
{
    string s;
    cin >> s;

    for(int i = 0; i < s.size(); i ++)
    {
        if(isdigit(s[i]))  //找到了有效数字【取出连续】
        {
            while(i < s.size() && isdigit(s[i]))
            {
                x = x * 10 + (s[i] - '0');  //isdigit(s[i]) + 秦九昭

                if(x > INT_MAX)  //超过了int范围  【INT_MAX在limits.h库】
                {
                    cout << -1 << endl;
                    return 0;
                }

                i ++;//别忘了移动指针
            }

            cout << x << endl;
            return 0;
        }
    }

    cout << -1 << endl; //没找到有效数字

    return 0;
}




#include <iostream>
#include <algorithm>
#include <unordered_map>
#include <cstdio>
#include <cstring>
#include <stack>

using namespace std;
typedef long long LL;

const LL N=1e4+10,Max=2147483647;

int main (){
    string Line;
    cin >> Line;
    vector<int> num;
    int first = 0; 
    for (auto &s : Line){
        if (s >= '0' && s <= '9'){ //或者isdigit(s[i])
            num.push_back(s-'0');
            if (!first) first = 1;//第一位标记
        }
        else {
            if (first){//字符串为空
                break;
            }
        }
    }//单独分离保存到数组,然后后计算
    // 单独计算的原因是判断是否越界
    LL res = 0;
    for (auto &c : num){
        res = res * 10 + c;
    }
    if (res > Max || !first) puts("-1");
    else cout << res << endl;
}


//作者:xkyss 链接:https://www.acwing.com/solution/content/72041/



递归:

3441. 重复者

给定一个仅包含一种字符和空格的模板,将之不断重复扩大。

例如,模板如下所示

# #
 # 
# #
那么,第 1 级的图形为

# #
 # 
# #
第 2 级的图形为

# #   # #
 #     # 
# #   # #
   # #   
    #    
   # #   
# #   # #
 #     # 
# #   # #

模板中包含多少元素,那么任意级别的图形中就包含多少元素。

模板或 1 级图形中的元素为单个字符,而高等级的图形中的元素为低一级别的图形。

现在给定模板,请你输出该模板的第 Q 级图形的具体图案。

输入格式
输入包含多组测试数据。

每组数据第一行包含整数 N,表示模板的尺寸大小为 N×N。

接下来 N 行,每行包含 N 个字符,用来描述模板。

最后一行,包含一个整数 Q,表示所求的图形等级。

当输入 N=0 时,表示输入结束。

输出格式
每组数据输出一个所求的具体图案。

数据范围
3≤N≤5,
输出图案保证尺寸不超过 3000×3000,
每个输入最多包含 10 组数据。

输入样例:
3
# #
 # 
# #
1
3
# #
 # 
# #
3
4
 OO 
O  O
O  O
 OO 
2
0
输出样例:
# #
 # 
# #
# #   # #         # #   # #
 #     #           #     # 
# #   # #         # #   # #
   # #               # #   
    #                 #    
   # #               # #   
# #   # #         # #   # #
 #     #           #     # 
# #   # #         # #   # #
         # #   # #         
          #     #          
         # #   # #         
            # #            
             #             
            # #            
         # #   # #         
          #     #          
         # #   # #         
# #   # #         # #   # #
 #     #           #     # 
# #   # #         # #   # #
   # #               # #   
    #                 #    
   # #               # #   
# #   # #         # #   # #
 #     #           #     # 
# #   # #         # #   # #
     OO  OO     
    O  OO  O    
    O  OO  O    
     OO  OO     
 OO          OO 
O  O        O  O
O  O        O  O
 OO          OO 
 OO          OO 
O  O        O  O
O  O        O  O
 OO          OO 
     OO  OO     
    O  OO  O    
    O  OO  O    
     OO  OO     
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

int n;
vector<string> p;

vector<string> g(int k)//第k级模板  【递归】
{
    if (k == 1) return p; //1就直接返回原模板p
    auto s = g(k - 1);//s存上一级模板
    int m = s.size();//上一级矩阵长度  【下一级的每个字符展开成m*m的矩阵 , 共n个字符res长度nm*nm 】

    vector<string> res(n * m);//每行
    for (int i = 0; i < n * m; i ++ )
        res[i] = string(n * m, ' ');//初始为长度n*m的空格

    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n; j ++ )
            if (p[i][j] != ' ')//模板此位置有 '#',说明要展开用上一级模板代替:枚举上一级模板
                for (int x = 0; x < m; x ++ )    
                    for (int y = 0; y < m; y ++ )
                        res[i * m + x][j * m + y] = s[x][y];   //坐标[i * m][j * m] 第i,j块 + 偏移量x,y

    return res;
}

int main()//给出模板p,递归出p的k级模板
{
    while (cin >> n, n)
    {
        p.clear();//先把p清空,再把模板读入p
        getchar();  // 读掉n后的回车  [用getline一定要记得读掉上一行的回车]
        for (int i = 0; i < n; i ++ )//按行读入模板p
        {
            string line;
            getline(cin, line);
            p.push_back(line);
        }

        int k;
        cin >> k;
        auto res = g(k);
        for (auto& s: res) cout << s << endl; //输出串
    }

    return 0;
}


//作者:yxc 链接:https://www.acwing.com/activity/content/code/content/1707067/

背包问题:

2. 01背包问题

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式
输出一个整数,表示最大价值。

数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8

/*考研算法辅导课版*/
#include <iostream>
using namespace std;
const int N = 1010;
int n,m;
int v,w;
int f[N];
int main()
{
    scanf("%d%d",&n,&m);
    
    for(int i = 0;i < n;i++)
    {
        scanf("%d%d",&v,&w);
        for(int j = m;j >= v; j --)
            f[j] = max( f[j] , f[j - v] + w);
    }
       
    cout << f[m] << endl;
    return 0;
}

二维朴素版:



1. 题目介绍
有 N 件物品和一个容量为 V 的背包,每件物品有各自的价值且只能被选择一次,要求在有限的背包容量下,装入的物品总价值最大。

「0-1 背包」是较为简单的动态规划问题,也是其余背包问题的基础。

动态规划是不断决策求最优解的过程,「0-1 背包」即是不断对第 i 个物品的做出决策,「0-1」正好代表不选与选两种决定。

2. 题解代码(C++2.1 版本1 二维
(1)状态f[i][j]定义:前 i 个物品,背包容量 j 下的最优解(最大价值):

当前的状态依赖于之前的状态,可以理解为从初始状态f[0][0] = 0开始决策,有 N 件物品,则需要 N 次决 策,每一次对第 i 件物品的决策,状态f[i][j]不断由之前的状态更新而来。
(2)当前背包容量不够(j < v[i]),没得选,因此前 ii 个物品最优解即为前 i−1 个物品最优解:

对应代码:f[i][j] = f[i - 1][j]。
(3)当前背包容量够,可以选,因此需要决策选与不选第 ii 个物品:

选:f[i][j] = f[i - 1][j - v[i]] + w[i]。
不选:f[i][j] = f[i - 1][j] 。
我们的决策是如何取到最大价值,因此以上两种情况取 max() 。
代码如下:


#include<bits/stdc++.h>

using namespace std;

const int N = 1005;
int v[N];    // 体积
int w[N];    // 价值 
int f[N][N];  // f[i][j], j体积下前i个物品的最大价值 

int main() 
{
    int n, m;   
    cin >> n >> m;
    for(int i = 1; i <= n; i++) 
        cin >> v[i] >> w[i];

    for(int i = 1; i <= n; i++) 
        for(int j = 1; j <= m; j++)
        {
            //  当前背包容量装不进第i个物品,则价值等于前i-1个物品
            if(j < v[i]) 
                f[i][j] = f[i - 1][j];
            // 能装,需进行决策是否选择第i个物品
            else    
                f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
        }           

    cout << f[n][m] << endl;
    
   
    //打出状态路径  ------------------测试观察规律
    for(int i = 1;i <= n;i++)  
    {
        for(int j = 1;j <= m;j++)
            printf("%d ",f[i][j]);
        puts("");
    }  
    
    return 0;
}



一维优化:


2.2 版本2 一维
将状态f[i][j]优化到一维f[j],实际上只需要做一个等价变形。

为什么可以这样变形呢?我们定义的状态f[i][j]可以求得任意合法的i与j最优解,但题目只需要求得最终状态f[n][m],因此我们只需要一维的空间来更新状态。

(1)状态f[j]定义:N件物品,背包容量j下的最优解。

(2)注意枚举背包容量j必须从m开始。

(3)为什么一维情况下枚举背包容量需要逆序?在二维情况下,状态f[i][j]是由上一轮i - 1的状态得来的,f[i][j]与f[i - 1][j]是独立的。而优化到一维后,如果我们还是正序,则有f[较小体积]更新到f[较大体积],则有可能本应该用第i-1轮的状态却用的是第i轮的状态。

(4)例如,一维状态第i轮对体积为 33 的物品进行决策,则f[7]由f[4]更新而来,这里的f[4]正确应该是f[i - 1][4],但从小到大枚举j这里的f[4]在第i轮计算却变成了f[i][4]。当逆序枚举背包容量j时,我们求f[7]同样由f[4]更新,但由于是逆序,这里的f[4]还没有在第i轮计算,所以此时实际计算的f[4]仍然是f[i - 1][4]。

(5)简单来说,一维情况正序更新状态f[j]需要用到前面计算的状态已经被「污染」,逆序则不会有这样的问题。

状态转移方程为:f[j] = max(f[j], f[j - v[i]] + w[i]for(int i = 1; i <= n; i++) 
    for(int j = m; j >= 0; j--)
    {
        if(j < v[i]) 
            f[i][j] = f[i - 1][j];  // 优化前
            f[j] = f[j];            // 优化后,该行自动成立,可省略。
        else    
            f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);  // 优化前
            f[j] = max(f[j], f[j - v[i]] + w[i]);                   // 优化后
    }    
实际上,只有当枚举的背包容量 >= v[i] 时才会更新状态,因此我们可以修改循环终止条件进一步优化。

for(int i = 1; i <= n; i++)
{
    for(int j = m; j >= v[i]; j--)  
        f[j] = max(f[j], f[j - v[i]] + w[i]);
} 
关于状态f[j]的补充说明
二维下的状态定义f[i][j]是前 i 件物品,背包容量 j 下的最大价值。一维下,少了前 i 件物品这个维度,我们的代码中决策到第 i 件物品(循环到第i轮),f[j]就是前i轮已经决策的物品且背包容量 jj 下的最大价值。

因此当执行完循环结构后,由于已经决策了所有物品,f[j]就是所有物品背包容量 jj 下的最大价值。即一维f[j]等价于二维f[n][j]

边输入边输出:




2.3 版本3 优化输入
我们注意到在处理数据时,我们是一个物品一个物品,一个一个体积的枚举。

因此我们可以不必开两个数组记录体积和价值,而是边输入边处理。


#include<bits/stdc++.h>

using namespace std;

const int N = 1005;
int f[N];  

int main() 
{
    int n, m;   
    cin >> n >> m;

    for(int i = 1; i <= n; i++) {
        int v, w;
        cin >> v >> w;      // 边输入边处理
        for(int j = m; j >= v; j--)
            f[j] = max(f[j], f[j - v] + w);
    }

    cout << f[m] << endl;

    return 0;
}

//作者:深蓝,链接:https://www.acwing.com/solution/content/1374/

3445. 点菜问题

北大网络实验室经常有活动需要叫外卖,但是每次叫外卖的报销经费的总额最大为 C 元,有 N 种菜可以点,经过长时间的点菜,网络实验室对于每种菜 i 都有一个量化的评价分数(表示这个菜可口程度),为 Vi,每种菜的价格为 Pi, 问如何选择各种菜,使得在报销额度范围内能使点到的菜的总评价分数最大。

注意:由于需要营养多样化,每种菜只能点一次。

输入格式
输入的第一行有两个整数 C 和 N,C 代表总共能够报销的额度, N 代表能点菜的数目。

接下来的 N 行每行包括两个整数 Pi 和 Vi,分别表示第 i 道菜的价格和评价分数。

输出格式
输出共一行,一个整数,表示在报销额度范围内,所点的菜能够得到的最大评价分数。

数据范围
1≤C≤1000,
1≤N≤100,
1≤ P i , V i P_i,V_i Pi,Vi≤100
输入样例:
90 4
20 25
30 20
40 50
10 18
输出样例:
95

#include<iostream>
#include <algorithm>
using namespace std;
const int N = 110,M = 1010;//f需开1000以上 >= m
int v[N],w[N];
int m,n;
int f[M];
int main()
{
    cin >> m >> n;
    
    for(int i = 1;i <= n;i++) scanf("%d%d",&v[i],&w[i]);
    
    for(int i = 1;i <= n;i++)
        for(int j = m;j >= v[i];j--)
            f[j] = max(f[j] , f[j - v[i]] + w[i]);
    
    cout << f[m] << endl;
    
    return 0;
}

3442. 神奇的口袋

有一个神奇的口袋,总的容积是 40,用这个口袋可以变出一些物品,这些物品的总体积必须是 40。

John 现在有 n 个想要得到的物品,每个物品的体积分别是 a1,a2……an。

John 可以从这些物品中选择一些,如果选出的物体的总体积是 40,那么利用这个神奇的口袋,John 就可以得到这些物品。

现在的问题是,John 有多少种不同的选择物品的方式。

输入格式
输入的第一行是正整数 n,表示不同的物品的数目。

接下来的 n 行,每行有一个 1 到 40 之间的正整数,分别给出 a1,a2……an 的值。

输出格式
输出不同的选择物品的方式的数目。

数据范围
1≤n≤20,
1≤ a i a_i ai≤40
输入样例:
3
20
20
20
输出样例:
3

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

int n;
const int N = 30;
int f[N],v[N];
//只看体积  ,恰好j体积的方案数  : f[j]     [一维优化,边输入边变化]
int main()
{
    cin >> n;
    f[0] = 1;//什么都不选也是1种方案
    while(n --)
    {
        int v;
        cin >> v;
        for(int i = 40;i >= v;i--)
        f[i] += f[i - v];
    }
    
    printf("%d",f[40]);        
    return 0;
}




#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 45;

int f[N][N];    // f[i][j]表示:所有从前i个物品中选,且总体积恰好为j的选法的数目

int main()
{
    int n;
    cin >> n;
    f[0][0] = 1;
    for (int i = 1; i <= n; i ++)
    {
        int v;
        cin >> v;
        for (int j = 0; j <= 40; j ++)
        {
            f[i][j] = f[i - 1][j];
            if (j >= v)
                f[i][j] = f[i][j] + f[i - 1][j - v];
        }
    }
    cout << f[n][40] << endl;

    return 0;
}
//作者:xjtu_zfw 链接:https://www.acwing.com/activity/content/code/content/1722149/

状态定义:f[i][j]表示考虑前i个物品总重量恰好等于j的方案数
集合划分:根据第i个物品选或者不选将集合划分为两部分
状态计算:f[i][j] = f[i - 1][j] + f[i - 1][j - w[i]]

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

const int N = 50;

int f[N][N];
int n, w[N];

int main()
{
    cin >> n;

    for(int i = 1; i <= n; i ++) cin >> w[i];

    f[0][0] = 1;  //初始化

    for(int i = 1; i <= n; i ++)
    {
        for(int j = 0; j <= 40; j ++)
        {
            f[i][j] = f[i - 1][j];

            if(j >= w[i]) f[i][j] += f[i - 1][j - w[i]];
        }
    }

    cout << f[n][40] << endl;

    return 0;
}


//作者:我呼吸了 链接:https://www.acwing.com/solution/content/131950/



3382. 整数拆分

一个整数总可以拆分为 2 的幂的和。

例如:7 可以拆分成

7=1+2+4,7=1+2+2+2,7=1+1+1+4,7=1+1+1+2+2,

7=1+1+1+1+1+2,7=1+1+1+1+1+1+1

共计 6 种不同拆分方式。

再比如:4 可以拆分成:4=4,4=1+1+1+1,4=2+2,4=1+1+2。

用 f(n) 表示 n 的不同拆分的种数,例如 f(7)=6。

要求编写程序,读入 n,输出 f(n)mod109

输入格式
一个整数 n。

输出格式
一个整数,表示 f(n)mod109

数据范围
1≤N≤106
输入样例:
7
输出样例:
6

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1000010, MOD = 1e9;

int n;//数作为体积  : 恰好价值为j的方案数 :f[j] = f[j] +f[j - i];
int f[N];

int main() //每种物品放无限次【完全背包】
{
    scanf("%d", &n);
    f[0] = 1;
    for (int i = 1; i <= n; i *= 2)//物品价值枚举2的次方   , 划分logn次
        for (int j = i; j <= n; j ++ )
            f[j] = (f[j] + f[j - i]) % MOD;
    cout << f[n] << endl;

    return 0;
}


//作者:yxc 链接:https://www.acwing.com/activity/content/code/content/1707267/

16. 高精度和因式分解

add(vector & A ,vector B): 加引用&拿过来用,会更快
考的多,上机题(复试),熟练掌握1h

3389. N 的阶乘

输入一个正整数 N,输出 N 的阶乘。

输入格式
输入包含多组测试数据。

每组数据占一行,包含一个整数 N。

输出格式
每组数据输出占一行,输出 N 的阶乘。

数据范围
1≤N≤1000,
每个输入最多包含 100 组数据

输入样例:
4
5
15
输出样例:
24
120
1307674368000

由于时间复杂度打表


#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 1010;

vector<int> F[N];

vector<int> mul(vector<int>& A, int b)
{
    vector<int> C;
    for (int i = 0, t = 0; i < A.size() || t; i ++ )//A的第i位 * b ,t每位上的乘积 , %10,取余数放入C   【于A.size()时但t不为0,则还需向最高位进位】
    {
        if (i < A.size()) t += A[i] * b;
        C.push_back(t % 10);
        t /= 10;
    }
    return C;
}

int main()//高精度
{
    int n;
    F[0] = {1};
    for (int i = 1; i <= 1000; i ++ ) F[i] = mul(F[i - 1], i);//先存下1-1000

    while (cin >> n)//直接查询
    {
        for (int i = F[n].size() - 1; i >= 0; i -- )//ans = res逆序输出 
            cout << F[n][i];//大数高精度存数组:按位输出
        cout << endl;
    }

    return 0;
}

//作者:yxc 链接:https://www.acwing.com/activity/content/code/content/1709445/

3448. 基本算术

孩子们在学习多位数加法运算。

计算时,相同数位要对齐,从个位加起,哪一位上的数相加满十,就向上一位进一。

许多孩子表示,“进位”操作十分困难,是个不小的挑战。

现在,给定你一些多位数加法运算的问题,请你统计每个问题在计算过程中需要用到多少次“进位”操作。

输入格式
输入包含多组测试数据。

每组数据占一行,包含两个无符号整数。

当输入一行为 0 0 时,表示输入结束。

输出格式
每组数据输出一个结果,用来描述进位的次数。

具体形式参考样例。

数据范围
每个输入最多包含 100 组数据。
给定整数均小于 10 位。

输入样例:
123 456
555 555
123 594
0 0
输出样例:
No carry operation.
3 carry operations.
1 carry operation.

y总版:


#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

int add(vector<int>& A, vector<int>& B)
{
    int res = 0;
    for (int i = 0, t = 0; i < A.size() || i < B.size() || t; i ++ )
    {
        if (i < A.size()) t += A[i];
        if (i < B.size()) t += B[i];
        t /= 10;//大于等于10,则为1
        res += t;
    }
    return res;
}

int main()
{
    string a, b;
    while (cin >> a >> b, a != "0" || b != "0")
    {
        vector<int> A, B;
        for (int i = a.size() - 1; i >= 0; i -- ) A.push_back(a[i] - '0');
        for (int i = b.size() - 1; i >= 0; i -- ) B.push_back(b[i] - '0');
        int res = add(A, B);
        if (!res) puts("No carry operation.");
        else if (res == 1) puts("1 carry operation.");
        else printf("%d carry operations.\n", res);
    }

    return 0;
}

解决此error:a,b类型改为string
warning: comparison of integer expressions of different signedness: ‘int’ and ‘std::vector::size_type’ {aka ‘long unsigned int’} [-Wsign-compare]
for (int i = 0; i < A.size(); i ++ )

my直接add模板:


#include <iostream>
#include <string>
#include <algorithm>
#include <cstring>

using namespace std;

int cnt;
string a,b;


//my直接套模板
vector<int> add(vector<int> &A, vector<int> &B)  // C = A + B, A >= 0, B >= 0
{
    if (A.size() < B.size()) return add(B, A);

    vector<int> C;
    int t = 0;
    for (int i = 0; i < A.size(); i ++ )
    {
        t += A[i];
        if (i < B.size()) t += B[i];
        if(t >= 10)cnt++; //t >= 10 有进位cnt计数
        C.push_back(t % 10);
        t /= 10;
    }

    if (t) C.push_back(t); 
    return C;
}

int main()
{
    while(cin >> a >> b)
    {
        if(a == "0" && b == "0")continue;
        cnt = 0;
        vector<int> A,B,C;
	
    	for(int i = a.size() - 1;i >= 0;i--) A.push_back(a[i] - '0');  //赋值A = {6,5,4,3,2,1}; 从个位开始逆序存放数值 
    	for(int i = b.size() - 1;i >= 0;i--) B.push_back(b[i] - '0');  //(注意:最后一位对应数组下标是size() - 1)
    	
    	C = add(A,B);
    	
        if(!cnt)puts("No carry operation.");
        else if(cnt == 1)puts("1 carry operation.");  //什么英语单词坑!
        else printf("%d carry operations.\n",cnt);
    }
    
    return 0;
}



3453. 整数查询

给定若干个可能很大的正整数,请你计算它们相加的和。

输入格式
最多不超过 100 个正整数,每个数的长度不超过 100。

每个整数占一行。

最后输入一行一个 0,表示输入结束。

注意整数可能有前导 0。

输出格式
输出所有数的和。

输入样例:
123456789012345678901234567890
123456789012345678901234567890
123456789012345678901234567890
0
输出样例:
370370367037037036703703703670

求正整数 N(N>1)的质因数的个数。

相同的质因数需要重复计算。

如 120=2×2×2×3×5,共有 5 个质因数。

输入格式
输入可能包含多组测试数据。

每组数据占一行,包含一个正整数 N。

输出格式
对于每组输入,输出一行结果表示 N 的质因数的个数。

数据范围
1<N< 1 0 9 10^9 109,
每个输入最多包含 100 组测试数据。

输入样例:
120
输出样例:
5

高精度加法


#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

vector<int> add(vector<int>& A, vector<int>& B)
{
    vector<int> C;
    for (int i = 0, t = 0; i < A.size() || i < B.size() || t; i ++ )
    {
        if (i < A.size()) t += A[i];
        if (i < B.size()) t += B[i];
        C.push_back(t % 10);
        t /= 10;
    }
    return C;
}

int main()
{
    vector<int> A{0};
    string b;
    while (cin >> b, b != "0")
    {
        vector<int> B;
        for (int i = b.size() - 1; i >= 0; i -- )
            B.push_back(b[i] - '0');
        A = add(A, B);
    }

    while (A.size() > 1 && !A.back()) A.pop_back();//数组中值逆序存放,若尾部为0去除,即去除前导0
    for (int i = A.size() - 1; i >= 0; i -- )
        cout << A[i];
    cout << endl;

    return 0;
}


因式分解:

3380. 质因数的个数

求正整数 N(N>1)的质因数的个数。

相同的质因数需要重复计算。

如 120=2×2×2×3×5,共有 5 个质因数。

输入格式
输入可能包含多组测试数据。

每组数据占一行,包含一个正整数 N。

输出格式
对于每组输入,输出一行结果表示 N 的质因数的个数。

数据范围
1<N<109,
每个输入最多包含 100 组测试数据。

输入样例:
120
输出样例:
5

性质n中最多只包含一个大于 n \sqrt n n 的质因子

试除法过程中不用预先枚举质数
举例:如4肯定会先被2除尽,其余合数倍数同理,最终先被质数除尽

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

int main()
{
    int n;
    while (cin >> n)
    {
        int res = 0;//质因数个数  [res不写在循环内,不会重复初始化,但一般编译器都会有优化]
        for (int i = 2; i <= n / i; i ++ )
            if (n % i == 0)//是因数
            {
                while (n % i == 0)//循环除尽此因数,记录个数 【存因数及其对应个数加2个数组就好了】
                {
                    n /= i;
                    res ++ ;
                }
            }

        if (n > 1) res ++ ;
        cout << res << endl;
    }

    return 0;
}


3377. 约数的个数

输入 n 个整数,依次输出每个数的约数的个数。

输入格式
第一行包含整数 n。

第二行包含 n 个整数 ai。

输出格式
共 n 行,按顺序每行输出一个给定整数的约数的个数。

数据范围
1≤n≤1000,
1≤ai≤ 1 0 9 10^9 109
输入样例:
5
1 3 4 6 12
输出样例:
1
2
3
4
6

约数个数 ∏ \prod (各质因数个数+1)


//开全局变量快一些
#include <iostream>

using namespace std;

int n ,res;

int main()
{
    int T;
    cin >> T;
    while (T -- )
    {
        cin >> n;
        res = 1;
        for (int i = 2; i <= n / i; i ++ )
            if (n % i == 0)
            {
                int s = 0;
                while (n % i == 0) n /= i, s ++ ;//除尽质因数,计算个数
                res *= s + 1;
            }
        if (n > 1) res *= 2;//若有一个大于\sqrt n ,还要再加1  ,  按式子 res = res * (1 + 1)
        cout << res << endl;
    }
    return 0;
}


#include <iostream>

using namespace std;


int main()
{
    int T;
    cin >> T;
    while (T -- )
    {
        int n;
        cin >> n;
        int res = 1;
        for (int i = 2; i <= n / i; i ++ )
            if (n % i == 0)
            {
                int s = 0;
                while (n % i == 0) n /= i, s ++ ;//除尽质因数,计算个数
                res *= s + 1;
            }
        if (n > 1) res *= 2;//若有一个大于\sqrt n ,还要再加1  ,  按式子 res = res * (1 + 1)
        cout << res << endl;
    }
    return 0;
}



/*不记这个hh
//试除法:约数成对出现  &O(\sqrt n)&
#include<iostream>

using namespace std;

int add(long long n)
{
    int k = 0;
    for(int i = 1;i <= n / i; i ++)
    {
        if(i * i == n) k ++;
        else 
            if(n % i == 0) k += 2;
    }
    return k;
}
int main()
{
    int n;
    cin >> n;
    while(n --)
    {
        long long b;
        cin>> b;
        cout << add(b) << endl;
    }
}
*/
//链接:https://www.acwing.com/solution/content/123424/

3507. 阶乘的末尾

给定一个整数 n,求 n 的阶乘末尾连续 0 的个数。

输入格式
一个整数 n。

输出格式
一个整数,表示 n 的阶乘末尾连续 0 的个数。

数据范围
1≤n≤ 1 0 9 10^9 109
输入样例:
5
输出样例:
1

取决于包含多少个10,也就是包含多少个2和5 取min(num(2),num(5));
n中包含多少个质因子p : p取2和5就行
根据公式:5值更大,做分母更快除尽 , 5的个数肯定小于2的个数,因此只需计算p=5 的个数
==>含有[1~k]次方的p的数量的和

尾部0的个数 ==>n中包含10的个数 ==> n中包含5的个数 ==> 含有质因数 5 1 5^1 51~ 5 k 5^k 5k的数量的和
此定理只能用于质数【但可以把合数拆成质数】,合数会被分解,重复计算



#include <iostream>

using namespace std;

int main()
{
    int n;
    cin >> n;
    int res = 0;
    while (n / 5) res += n / 5, n /= 5;
    cout << res << endl;

    return 0;
}

3484. 整除问题

给定 n,a 求最大的 k,使 n! 可以被 a k a_k ak 整除但不能被 a k + 1 a_{k+1} ak+1 整除。

输入格式
一行,两个整数 n 和 a。

输出格式
一个整数 k。

数据范围
2≤n,a≤1000
输入样例:
6 10
输出样例:
1

a可能为合数 ==> 拆成质因数的乘积 ==> 可以用质因数p个数求法
==> 计算a与n的质因数个数差值即幂次差(n中最多包含多少a) n ( q k ) a ( q k ) \frac{n(q^{k})}{a(q^{k})} a(qk)n(qk) ,取min
[n有 p 4 p^4 p4而a有 p 2 p^2 p2,则可以说n含有两个a]

\frac{}{}


#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

vector<vector<int>> get_ds(int n)
{
    vector<vector<int>> res;
    for (int i = 2; i * i <= n; i ++ )
        if (n % i == 0)
        {
            int s = 0;
            while (n % i == 0) n /= i, s ++ ;
            res.push_back({i, s});
        }

    if (n > 1) res.push_back({n, 1});//最多含有一个大于\sqrt n 的质因数
    return res;//返回
}

int get_p(int n, int p)//质因数p的个数
{
    int res = 0;
    while (n / p) res += n / p, n /= p;
    return res;
}

int main()
{
    int n, m;
    cin >> n >> m;

    auto ds = get_ds(m);//二维可变数组ds 
    int res = 1e8;

    for (int i = 0; i < ds.size(); i ++ )
    {
        int p = ds[i][0], s = ds[i][1];//p:质因数值 , s:质因数的个数
        res = min(res, (get_p(n, p) / s)); //res = min( n中对应不同p的次方数量 / a中对应不同p的次方数量)
    }
    cout << res << endl;

    return 0;
}

17. 枚举和位运算

枚举:

3434. 与7无关的数

一个正整数,如果它能被 7 整除,或者它的十进制表示法中某个位数上的数字为 7,则称其为与 7 相关的数。

现求所有小于等于 n 的与 7 无关的正整数的平方和。

输入格式
一行,包含一个正整数 n。

输出格式
对于每个测试案例输出一行,输出小于等于 n 的与 7 无关的正整数的平方和。

数据范围
0<n<100
输入样例:
21
输出样例:
2336

check 不能被7整除 && 各个位上数值 != 7

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

int n,sum;

bool check(int x)
{
    if( x % 7 ==  0)return false;
    while(x)
    {
        int t = x % 10;
        if( t == 7) return false;
        x /= 10;
    }
    return true;
}

int main()
{
 
    cin >> n;
    
    for(int i = 1;i <= n;i ++)
    {
        if(check(i))sum += i * i;
    }
    
    printf("%d",sum);
    
    return 0;
}

3437. 打印极值点下标

在一个整数数组上,对于下标为 i 的整数,如果它大于所有它相邻的整数, 或者小于所有它相邻的整数,则称为该整数为一个极值点,极值点的下标就是 i。

现在给定整数数组,请你找到所有极值点,并输出这些极值点的下标。

输入格式
第一行包含整数 N 表示数组中包含的整数个数。

第二行包含 N 个整数,表示数组中的元素。

数组中的元素的下标依次为 0∼N−1。

输出格式
共一行,按从小到大的顺序输出所有极值点的下标。

数据范围
1≤N≤100,
1≤ 数组中的元素值 ≤1000
输入样例:
10
10 12 12 11 11 12 23 24 12 12
输出样例:
0 7

#include <iostream>

using namespace std;

const int N = 110;

int n;
int w[N];

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++ ) cin >> w[i];

    for (int i = 0; i < n; i ++ )
        if (!i)//在最左边只要看相邻的i+1就行 [只要不相等就行,极小值或极大值点]
        {
            if (n == 1 || w[i] != w[i + 1]) cout << i << ' ';//n == 1 如特例:输入1个0的情况
        }
        else if (i == n - 1)//在最右边只要看相邻的i-1就行 [只要不相等就行,极小值或极大值点]
        {
            if (w[i] != w[i - 1]) cout << i << ' ';
        }
        else //中间看i-1和i+1   :分为极大值或极小值点
        {   //太长分段写
            if (w[i] > w[i - 1] && w[i] > w[i + 1] ||      
                w[i] < w[i - 1] && w[i] < w[i + 1])
                    cout << i << ' ';
        }

    return 0;
}

3408. 最简真分数

给出 n 个正整数,任取两个数分别作为分子和分母组成最简真分数,编程求共有几个这样的组合。

输入格式
输入包含多组测试数据。

每组数据第一行包含整数 n。

第二行包含 n 个整数,范围 [1,1000] 且互不相同。

当 n=0 时表示输入结束。

输出格式
每组数据输出一行结果,表示最简真分数组合的个数。

数据范围
2≤n≤600,
每个输入最多包含 100 组数据。

输入样例:
7
3 5 7 9 11 13 15
3
2 4 5
0
输出样例:
17
2

最简真分数 ==> 分子 > 分母 && gcd(分子,分母) == 1

algorithm含有封装好的最大公约数函数:_ _gcd [双下斜杆]

#include <iostream>
#include<algorithm>//含有__gcd   [双下斜杆]

using namespace std;

const int N = 610;

int n;
int w[N];

int gcd(int a, int b)  // 欧几里得算法
{
    return b ? gcd(b, a % b) : a;
}

int main()
{
    while (cin >> n, n)
    {
        for (int i = 0; i < n; i ++ ) cin >> w[i];

        int res = 0;
        for (int i = 0; i < n; i ++ )
            for (int j = 0; j < n; j ++ )
                if (w[i] < w[j] && gcd(w[i], w[j]) == 1) //最简真分数:分子 < 分母 && gcd(分子,分母) == 1 (互质==>最大公约数1)
                    res ++ ;
        cout << res << endl;
    }

    return 0;
}


3444. 买房子

某程序员开始工作,年薪 N 万,他希望在中关村公馆买一套 60 平米的房子,现在价格是 200 万,假设房子价格以每年百分之 K 增长,并且该程序员未来年薪不变,且不吃不喝,不用交税,每年所得 N 万全都积攒起来,问第几年能够买下这套房子(第一年房价 200 万,收入 N 万)

输入格式
输入包含多组测试数据。

每组数据共一行,包含两个整数 N 和 K。

输出格式
对于每组数据,如果在第 21 年或者之前就能买下这套房子,则输出一个整数 M,表示最早需要在第 M 年能买下,否则输出 Impossible。

每组数据输出占一行。

数据范围
10≤N≤50,
1≤K≤20,
每个输入最多包含 100 组数据。

输入样例:
50 10
40 10
40 8
输出样例:
8
Impossible
10

语法题~应该刷刷

#include <iostream>

using namespace std;

int main()
{
    double n, k;//用到百分比,存在小数
    while (cin >> n >> k)
    {
        double sum = n, house = 200;//每轮初始值

        bool flag = false;//标记是否能买到
        for (int i = 1; i <= 21; i ++ ) //ans = 1-21年 或 Impossible
        {
            if (sum >= house)
            {
                cout << i << endl;
                flag = true;
                break;
            }

            sum += n;
            house *= 1 + k / 100;
        }

        if (!flag) puts("Impossible");//买不到
    }

    return 0;
}

3550. Special数

设一个正整数既是平方数又是立方数时,称之为 Special 数。

输入整数 n,请你计算 1 到 n 中包含的所有 Special 数的个数。

输入格式
第一行包含整数 T,表示共有 T 组测试数据。

每组数据占一行,包含一个整数 n。

输出格式
每组数据输出一行,一个结果。

数据范围
1≤T≤100,
1≤n≤109
输入样例:
2
1
64
输出样例:
1
2

语法课*2

#include <iostream>
#include <algorithm>
#include <cmath>//sqrt()

using namespace std;

int main()
{
    int T;
    cin >> T;

    while (T -- )
    {
        int n;
        cin >> n;

        int res = 0;
        for (int i = 1; i * i * i <= n; i ++ )  //直接确定有i^3在n内,然后看x = i^3是不是平方数 
        {
            int x = i * i * i;
            int r = sqrt(x);//cmath
            if (r * r == x)//判平方是否等于原数值
                res ++ ;
        }

        cout << res << endl;
    }

    return 0;
}



位运算:

3435. 位操作练习

给出两个不大于 65535 的非负整数,判断其中一个的 16 位二进制表示形式,是否能由另一个的 16 位二进制表示形式经过循环左移若干位而得到。

循环左移和普通左移的区别在于:最左边的那一位经过循环左移一位后就会被移到最右边去。

比如: 1011 0000 0000 0001 经过循环左移一位后,变成 0110 0000 0000 0011,若是循环左移2位,则变成 1100 0000 0000 0110。

输入格式
输入包含多组测试数据。

每组数据包含两个整数,占一行。

输出格式
每组数据输出一个结果,如果两个数之间能够进行移位转化则输出 YES,否则输出 NO。

数据范围
输入整数不超过 65535。
输入最多包含 100 组数据。

输入样例:
2 4
9 18
45057 49158
7 12
输出样例:
YES
YES
YES
NO

to_string + find + 二进制string x >> i & 1

#include <iostream>
#include <string>//to_string
#include<algorithm>//find
using namespace std;

int main()
{
    int a, b;
    while (cin >> a >> b)
    {
        string x, y;
        for (int i = 15; i >= 0; i -- )//公式: x >> 1 & 1 取二进制第i位数字:1或0
        {
            x += to_string(a >> i & 1);//初始赋值x、y
            y += to_string(b >> i & 1);
        }

        y += y;//把y拼接成两倍则可从左到右顺序截取所有移位后得到的子串
        if (y.find(x) != -1) puts("YES");//查找y中是否含有x
        else puts("NO");
    }

    return 0;
}

3530. 二进制数

大家都知道,数据在计算机里中存储是以二进制的形式存储的。

有一天,小明学了 C 语言之后,他想知道一个类型为 unsigned int 类型的数字,存储在计算机中的二进制串是什么样子的。

你能帮帮小明吗?

并且,小明不想要二进制串中前面的没有意义的 0 串,即要去掉前导 0。

输入格式
输入包含多组测试数据。

每组数据占一行,包含一个整数。

输出格式
每组数据输出一行,一个二进制串表示结果。

数据范围
输入整数范围 [0,4294967295]。
每个输入最多包含 100 组数据。

输入样例:
23
535
2624
56275
989835
输出样例:
10111
1000010111
101001000000
1101101111010011
11110001101010001011

重要操作:to_string(n & 1),n >>= 1 和 reverse(res.begin(), res.end())

#include <iostream>//函数未知库时常用头文件全部写上
#include <cstring>
#include <algorithm>

using namespace std;

int main()
{
    unsigned int n;//依据题目
    
    while (cin >> n)
    {
        string res;
        if (!n) res = "0"; // n & 1 取二进制数第n位值 : 0 或 1
        while (n)res += to_string(n & 1), n >>= 1; //char --> string 从个位开始加【逆序】(去除前导0) , 再删去二进制个位
        reverse(res.begin(), res.end());//翻转
        cout << res << endl; //string就用cout输出
    }
    
    return 0;
}

18. 矩阵、计算几何和前缀和

矩阵:

3527. 旋转矩阵

任意输入两个 9 阶以下矩阵,要求判断第二个是否是第一个的旋转矩阵(顺时针),如果是,输出旋转角度(0、90、180、270),如果不是,输出 −1。

输入格式
第一行包含整数 n,表示矩阵阶数。

接下来 n 行,每行包含 n 个空格隔开的整数,表示第一个矩阵。

再接下来 n 行,每行包含 n 个空格隔开的整数,表示第二个矩阵。

输出格式
判断第二个矩阵是否是第一个的旋转矩阵,如果是,输出旋转角度(0、90、180、270),如果不是,输出 −1。

如果旋转角度的结果有多个,则输出最小的那个。

数据范围
1≤n≤9,
矩阵中元素取值范围 [1,9]
输入样例:
3
1 2 3
4 5 6
7 8 9
7 4 1
8 5 2
9 6 3
输出样例:
90

旋转矩阵函数vector<vector> rotate(vector<vector> a)
翻转后规律:res[i][j] = a[k][i];
第i行等于翻转后第i列,第j个元素等于翻转后倒数第j个元素,所以用k = n-1,k–表示倒数填入
罕见写法:vector<vector<int>> a(n, vector<int>(n))

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 10;

int n;

vector<vector<int>> rotate(vector<vector<int>> a) //二维可变长数组类型 返回值和形参 
{
    auto res = a;

    for (int i = 0; i < n; i ++ )
        for (int j = 0, k = n - 1; j < n; j ++, k -- )   //第i行等于翻转后第i列,第j个元素等于翻转后倒数第j个元素,所以用k = n-1,k--表示倒数填入
            res[i][j] = a[k][i];

    return res;
}

int main()
{
    cin >> n;
    
    vector<vector<int>> a(n, vector<int>(n)); //n行的vector<int>(n)[每行n个]  [二维可变长数组指定长度 ]
    auto b = a;
    //读入a, b
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n; j ++ )
            cin >> a[i][j];

    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n; j ++ )
            cin >> b[i][j];

    for (int i = 0; i < 4; i ++ )//四次翻转全部情况 ,每次判断翻转后若有a == b , 则输出翻转的角度
    {
        if (a == b)//vector自带比较函数
        {
            cout << i * 90 << endl;//ans = (0、90、180、270)中任意一个
            return 0;
        }

        a = rotate(a);
    }

    puts("-1");
    return 0;
}


3534. 矩阵幂

给定一个 n×n 的矩阵 P,求该矩阵的 k 次幂,即 Pk。

输入格式
第一行包含两个整数 n 和 k。

接下来有 n 行,每行 n 个整数,其中,第 i 行第 j 个整数表示矩阵中第 i 行第 j 列的矩阵元素 Pij。

输出格式
n 行 n 列个整数,每行数之间用空格隔开。

数据范围
2≤n≤10,
1≤k≤5,
0≤Pij≤10,
数据保证最后结果不会超过 108

输入样例:
2 2
9 8
9 3
输出样例:
153 96
108 81

memcpy(拷贝地址, 被拷贝地址, 拷贝大小:sizeof 被拷贝地址)

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 15;

int n, m;
int w[N][N];


// C = A * B
void mul(int c[][N], int a[][N], int b[][N])//形参c不是数组类型指针,不会返回数组长度,而是返回指针的长度
{
    static int tmp[N][N];//写tmp原因:数组指针,会返回数组的长度
    memset(tmp, 0, sizeof tmp);
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n; j ++ )
            for (int k = 0; k < n; k ++ )
                tmp[i][j] += a[i][k] * b[k][j];//矩阵乘法:(i,j)的值 :∑对应行元素×对应列元素

    memcpy(c, tmp, sizeof tmp);// [c拷贝, tmp被拷贝, sizeof 被拷贝]
}

int main()
{
    cin >> n >> m;
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n; j ++ )
            cin >> w[i][j];

    int res[N][N] = {0};//初始化单位矩阵res  
    for (int i = 0; i < n; i ++ ) res[i][i] = 1;

    while (m -- ) mul(res, res, w);//1-m次矩阵幂

    for (int i = 0; i < n; i ++ )
    {
        for (int j = 0; j < n; j ++ )
            cout << res[i][j] << ' ';
        cout << endl;
    }

    return 0;
}

3535. C翻转

给定一个 5×5 的矩阵,对其进行翻转操作。

操作类型共四种,具体形式如下:

1 2 x y,表示将以第 x 行第 y 列的元素为左上角,边长为 2 的子矩阵顺时针翻转 90 度。
1 3 x y,表示将以第 x 行第 y 列的元素为左上角,边长为 3 的子矩阵顺时针翻转 90 度。
2 2 x y,表示将以第 x 行第 y 列的元素为左上角,边长为 2 的子矩阵逆时针翻转 90 度。
2 3 x y,表示将以第 x 行第 y 列的元素为左上角,边长为 3 的子矩阵逆时针翻转 90 度。
注意:矩阵下标从1开始。

输入格式
前 5 行,每行 5 个空格隔开的整数,表示矩阵。

第 6 行,四个整数,用来描述操作类型,为 1 2 x y,1 3 x y,2 2 x y,2 3 x y 中的一种。

输出格式
输出翻转后的数组。

数据范围
矩阵元素取值范围 [1,100]。
输入保证合法。

输入样例:
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
21 22 23 24 25
1 3 1 1
输出样例:
11 6 1 4 5
12 7 2 9 10
13 8 3 14 15
16 17 18 19 20
21 22 23 24 25

C_rotate区间翻转
逆时针翻转90 <==> 顺时针翻转270 (翻转3次)

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 5;

int n;
int g[N][N];

void rotate(int x, int y, int m) //功能:以第x行第y列的元素为左上角,边长为m的子矩阵顺时针翻转90度
{
    int w[N][N];
    //memcpy(w, g, sizeof g);//按m行m列遍历,初始赋值可以不写,最终w会按规翻转90度的,过程覆盖

    for (int i = 0; i < m; i ++ )//以区域(x, y) ~ (x + m, y + m) 起始偏移量(x,y)
        for (int j = 0, k = m - 1; j < m; j ++, k -- )
            w[i][j] = g[x + k][y + i];//翻转后的行<==>原列从下往上数 , 翻转后的列 <==> 原行从左往右

   for (int i = 0; i < m; i ++ )//覆盖g 
        for (int j = 0; j < m; j ++ )
            g[x + i][y + j] = w[i][j];//偏移量(x,y) 

}

int main()
{
    for (int i = 0; i < 5; i ++ )
        for (int j = 0; j < 5; j ++ )
            cin >> g[i][j];

    int a, b, x, y;
    cin >> a >> b >> x >> y;//a翻转1顺时针,2逆时针(等效顺时针翻转270度,翻转3次)
    x --, y -- ;//依题意:矩阵下标从1开始,则初始下标偏移

    if (a == 1) rotate(x, y, b);//顺时针翻转
    else
    {
        for (int i = 0; i < 3; i ++ )//逆时针翻转 == 顺时针翻转3次
            rotate(x, y, b);
    }

    for (int i = 0; i < 5; i ++ )
    {
        for (int j = 0; j < 5; j ++ )
            cout << g[i][j] << ' ';
        cout << endl;
    }

    return 0;
}

计算几何:

3561. 球的计算

给定一个球的中心点坐标和球面上某一点的坐标,请你计算球的半径和体积。

输入格式
第一行包含整数 T,表示共有 T 组测试数据。

每组数据占一行,包含六个整数 x0,y0,z0,x1,y1,z1,分别表示球的球心坐标 (x0,y0,z0) 和球面上一点坐标 (x1,y1,z1)。

输出格式
每组数据输出一行,一个结果,首先输出球的半径,然后输出球的体积。

两数之间空格隔开,保留两位小数。

数据范围
1≤T≤10,
−5≤x0,y0,z0,x1,y1,z1≤5
输入样例:
1
0 0 0 1 0 0
输出样例:
1.00 4.19

语法题
const double PI = acos(-1); 反三角函数

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>

using namespace std;

const double PI = acos(-1);//反三角函数

int main()
{
    int T;
    cin >> T;
    while (T -- )
    {
        double x0, y0, z0, x1, y1, z1;
        cin >> x0 >> y0 >> z0 >> x1 >> y1 >> z1;

        double dx = x0 - x1;//减少代码量
        double dy = y0 - y1;
        double dz = z0 - z1;

        double r = sqrt(dx * dx + dy * dy + dz * dz);
        double v = 4.0 / 3 * PI * r * r * r;  //4.0 / 3 转double

        printf("%.2lf %.2lf\n", r, v);
    }
    return 0;
}

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>

using namespace std;

const double PI = acos(-1);//反三角函数

int main()
{
    int T;
    cin >> T;
    while (T -- )
    {
        double x0, y0, z0, x1, y1, z1;
        cin >> x0 >> y0 >> z0 >> x1 >> y1 >> z1;

        double dx = x0 - x1;//减少代码量
        double dy = y0 - y1;
        double dz = z0 - z1;

        double r = sqrt(dx * dx + dy * dy + dz * dz);
        double v = 4.0 / 3 * PI * r * r * r;  //4.0 / 3 转double

        printf("%.2lf %.2lf\n", r, v);
    }
    return 0;
}


//作者:yxc 链接:https://www.acwing.com/activity/content/code/content/1738422/

3571. 点的距离

创建一个 CPoint 类,代表平面直角坐标系中的点,创建构造函数和运算符重载函数,运算符重载为类重载(非友元重载),可以实现计算两个点之间的距离。

要求:

输入两个点的坐标,输出两个点之间的距离
重载运算符为 -
输入格式
第一行包含整数 T,表示共有 T 组测试数据。

每组数据一行,每行 4 个整数分别表示两个点的横纵坐标。

输出格式
每组数据输出一行,一个答案,表示两点之间距离,保留两位小数。

数据范围
1≤T≤10,
坐标取值范围 [0,10]
输入样例:
1
0 0 2 0
输出样例:
2.00

面向对象语法题

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>

using namespace std;

class CPoint
{
public:
    double x, y;

    CPoint(){}
    CPoint(double _x, double _y): x(_x), y(_y) {}

    double operator- (const CPoint& t) const
    {
        double dx = x - t.x;
        double dy = y - t.y;
        return sqrt(dx * dx + dy * dy);
    }
};

int main()
{
    int T;
    cin >> T;

    while (T -- )
    {
        CPoint a, b;
        cin >> a.x >> a.y >> b.x >> b.y;
        printf("%.2lf\n", a - b);
    }

    return 0;
}

//作者:yxc 链接:https://www.acwing.com/activity/content/code/content/1738432/	

3572. 直角三角形

创建一个 CTriangle 类,用 3 点来代表一个三角形,输入三个点的坐标,实现判断此三角形是不是直角三角形,并输出此三角形的周长。

输入格式
第一行包含整数 T,表示共有 T 组测试数据。

每组数据占一行,包含 6 个整数分别表示三个点的横纵坐标。

输出格式
每组数据输出两行,第一行根据是否直角三角形输出 Yes 或 No,第二行输出三角形的周长,保留小数点后两位。

数据范围
1≤T≤10,
坐标取值范围 [0,5],
保证输入的三个点能够构成三角形。

输入样例:
1
0 0 3 0 0 4
输出样例:
Yes
12.00

面向对象:成员函数+成员变量嵌套调用
return fabs(d[0] * d[0] + d[1] * d[1] - d[2] * d[2]) < 1e-8;
double有误差,取极小误差范围内认为相等
两点距离:重载减号

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>//sqrt

using namespace std;

struct CPoint 
{
    double x, y;

    double operator- (const CPoint& t) const //重载减号:a - b为返回两点的坐标轴间距离 
    {
        double dx = x - t.x;
        double dy = y - t.y;
        return sqrt(dx * dx + dy * dy);//公式
    }
};

struct CTriangle 
{
    CPoint a, b, c;

    bool check()
    {
        double d[3] = {a - b, b - c, a - c}; //存边长
        sort(d, d + 3); //排序 , 若 a * a + b * b == c * c  ====>直角三角形
        return fabs(d[0] * d[0] + d[1] * d[1] - d[2] * d[2]) < 1e-8; //double有误差,取极小误差范围内认为相等 
    }

    double get_len()//三角形周长
    {
        return (a - b) + (b - c) + (a - c);//返回三边和
    }
};

int main()
{
    int T;
    cin >> T;
    while (T -- )
    {
        CTriangle t;
        cin >> t.a.x >> t.a.y >> t.b.x >> t.b.y >> t.c.x >> t.c.y;//成员函数二级调用
        if (t.check()) puts("Yes");//成员函数调用
        else puts("No");
        printf("%.2lf\n", t.get_len());//成员函数调用
    }

    return 0;
}

前缀和:

3553. 最长平衡串*

给定一个只含 01 的字符串,找出它的最长平衡子串的长度。

如果一个 01 字符串包含的 0 和 1 的个数相同,则称其为平衡串。

输入格式
一行,一个 01 字符串。

输出格式
一个整数,表示结果。

数据范围
输入字符串的长度不超过 106

输入样例:
101011000
输出样例:
8

前缀和思想:分别计算区间0、1个数 a[] , b[]
转换为区间平衡【注意区间为[j,i]】 : a[i] - a[j] == b[i] - b[j] <==> a[i] - b[i] == a[j] = b[j]
令s[i] = a[i] - b[i] 前i个数0与1的差为多少,区间平衡子串
<==> s[i] == s[j] 0与1抵消(说明区间0与1数量相等)
找最长:找到一个最小的j,则平衡区间最长 res = i - j = i - hash[s]
综上:s代表0与1区间差值,找到两个差值相等的区间,说明区间内0与1数量相等抵消,即平衡区间
unordered_map<int, int> hash; ----> hash[s] = j (循环中j值为i指针代表下标)


#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_map>

using namespace std;

const int N = 1000010;

int n;
char str[N];

int main()
{
    scanf("%s", str + 1);//起始地址为str + 1
    unordered_map<int, int> hash; //映射(hash[s]值,最小位置j) 每个平衡子串出现的最小位置 (,起始位置i)       
    n = strlen(str + 1);//strlen语法:起始地址

    int res = 0;

    hash[0] = 0;//可以取0,存入可能性
    for (int i = 1, s = 0; i <= n; i ++ )
    {
        if (str[i] == '0') s ++ ;//s = 区间0与1个数差值
        else s -- ;

        if (hash.count(s)) res = max(res, i - hash[s]);//出现过最小位置hash[s] = j,则依据公式:相等s的下标差值为平衡区间
        else hash[s] = i;//没有出现过与s差值相等的位置,存最小相等位置
    }

    printf("%d\n", res);

    return 0;
}

19. 推公式、最短路、思维题

在这里插入图片描述

此题分两段求最短路

推公式:

3455. 数字台阶

如下图所示,我们在一个平面上建立了一个数字台阶,从点 (0,0) 开始,我们在平面中依次标注了 0,1,2… 等所有非负整数。

例如,在点 (1,1),(2,0),(3,1) 处,我们依次标注了 1,2,3。

在这里插入图片描述

现在,给定一个若干坐标,请你判断,该坐标上是否标有数字,以及标有什么数字。

输入格式
第一行包含整数 N。表示询问坐标数量。

接下来 N 行,每行包含两个整数 x,y,表示询问坐标为 (x,y)。

输出格式
对于每个询问,输出一行结果。

如果询问坐标上存在数字,则输出该数字,否则输出 No Number。

数据范围
1≤N≤20000,
0≤x,y≤5000
输入样例:
3
4 2
6 6
3 4
输出样例:
6
12
No Number

找规律

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

int main()
{
    int T;
    cin >> T;
    while (T -- )
    {
        int x, y;
        cin >> x >> y;
        if (y != x && y != x - 2) puts("No Number"); //不在 y = x 和 y = x - 2 ,没有数字
        else if (y == x) //在y = x 上
        {
            if (x % 2 == 0) cout << x * 2 << endl;   //第一个等差数列规律 【序列的偶数项】  
            else cout << x * 2 - 1 << endl; //第二个等差数列规律 【序列的奇数项】
        }
        else//在y = x - 2 上
        {
            if (x % 2 == 0) cout << x * 2 - 2 << endl; // 【序列的偶数项】 
            else cout << x * 2 - 3 << endl; // 【序列的奇数项】 
        }
    }

    return 0;
}


3558. 整数和

编写程序,读入一个整数 N。

若 N 为非负数,则计算 N 到 2N 之间的整数和;若 N 为一个负数,则计算 2N 到 N 之间的整数和(包括两端整数)。

输入格式
第一行包含整数 T,表示共有 T 组测试数据。

每组数据一行,一个整数 N。

输出格式
每组数据一行,一个结果。

数据范围
1≤T≤100,
−100≤N≤100
输入样例:
2
2
-1
输出样例:
9
-3

等差数列公式
等差数列区间求和: (首项 + 末项) * 项数 / 2

#include <iostream>

using namespace std;

int main()
{
    int T;
    cin >> T;
    while (T -- )
    {
        int n;
        cin >> n;
        int a = n, b = n * 2;
        if (a > b) swap(a, b);//为负数区间
        cout << (a + b) * (b - a + 1) / 2 << endl; //等差数列公式: (首项 + 末项) * 项数 / 2 
    }
    return 0;
}


3570. 弹地小球

一个小球,从高为 H 的地方下落,下落弹地之后弹起高度为下落时的一半,比如第一次弹起高度为 H/2,如此往复,计算从小球 H 高度下落到第 n 次弹地,往返的总路程。

输入格式
第一行包含整数 T,表示共有 T 组测试数据。

每组数据占一行,包含两个整数 H 和 n。

输出格式
每组数据输出一行,一个结果,表示小球往返的总路程,保留两位小数。

数据范围
1≤T≤10,
1≤H≤100,
1≤n≤5
输入样例:
1
5 2
输出样例:
10.00

可快速幂 + 等比数列
规律H ,H + H ,H + H + H / 2, H + H + H / 2 + H / 4 … 【除了第一次下降H,第二次开始为等比数列】
等比数列公式 a1*(1 - q^n) / (1-q) [用上公式要加上快速幂,O(快速幂操作次数logn)]


#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

int main()
{
    int T;
    cin >> T;
    while (T -- )
    {
        double h;
        int n;
        cin >> h >> n;
        double s = h;
        for (int i = 0; i < n - 1; i ++ ) //每次弹回高度为上一次的一半 
        {
            s += h;
            h /= 2;
        }

        printf("%.2lf\n", s);
        
        
        printf("%.2lf\n", s);
    }

    return 0;
}

最短路:

3403. 我想回家

某国正处于激烈的内战之中,该国的各个城市按照支持领导人的不同分属两个阵营。

作为一个商人,M 先生并不关心政治,但他能够感受到目前事态的严峻。

你需要帮助他尽快回家。

出于安全的考虑,你所提供的回家线路中,最多只能包含一条连接两个不同阵营城市的道路。

请你计算,M 先生回家所需花费的最短时间。

输入格式
输入包含多组测试数据。

每组数据第一行包含整数 N,表示该国家的城市数量。

第二行包含整数 M,表示该国家的道路数量。

接下来 M 行,每行包含三个整数 A,B,T,表示城市 A 和城市 B 之间存在一条道路,通过它的时间为 T。

最后一行包含 N 个整数 1 或 2,其中的第 i 个整数是 1,则表示城市 i 位于阵营 1,否则,表示城市 i 位于阵营 2。

所有城市编号 1∼N。

为了简化问题,我们假设 M 先生是从城市 1 出发,目的地是城市 2,并且城市 1 一定位于阵营 1,城市 2 一定位于阵营 2。

注意,所有道路都是双向的,且两个城市之间最多只有一条道路。

输入 N=0 时,表示输入结束。

输出格式
每组数据输出一行一个结果,表示最短时间。如果无法到达目的地,则输出 −1。

数据范围
每个输入最多包含 10 组数据。
2≤N≤600,
0≤M≤10000,
1≤A,B≤N,
1≤T≤500
输入样例:
2
1
1 2 100
1 2
3
3
1 2 100
1 3 40
2 3 50
1 2 1
5
5
3 1 200
5 3 150
2 5 160
4 3 170
4 2 170
1 2 2 2 1
0
输出样例:
100
90
540
在这里插入图片描述

分段最短路加绿边枚举
基础课再刷SPFA


#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 610, M = 20010, INF = 0x3f3f3f3f;

int n, m;
int h[N], e[M], w[M], ne[M], idx;
int q[N], dist1[N], dist2[N];
bool st[N];
int team[N];

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

void spfa(int start, int dist[])
{
    int hh = 0, tt = 1;
    q[0] = start;
    memset(dist, 0x3f, sizeof dist1);//注意sizeof 原数组
    dist[start] = 0;

    while (hh != tt)
    {
        int t = q[hh ++ ];
        if (hh == N) hh = 0;
        st[t] = false;

        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (team[j] != start) continue;
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])
                {
                    q[tt ++ ] = j;
                    if (tt == N) tt = 0;
                    st[j] = true;
                }
            }
        }
    }
}


int main()
{
    while (scanf("%d", &n), n)
    {
        scanf("%d", &m);
        memset(h, -1, sizeof h), idx = 0;

        while (m -- )
        {
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c);
            add(a, b, c), add(b, a, c);
        }

        for (int i = 1; i <= n; i ++ ) scanf("%d", &team[i]);

        spfa(1, dist1);
        spfa(2, dist2);

        int res = INF;
        for (int i = 0; i < idx; i ++ )
        {
            int a = e[i ^ 1], b = e[i];
            if (team[a] == 1 && team[b] == 2)
                res = min(res, dist1[a] + w[i] + dist2[b]);
        }

        if (res == INF) puts("-1");
        else cout << res << endl;
    }

    return 0;
}

3488. 最短路径

N 个城市,标号从 0 到 N−1,M 条道路,第 K 条道路(K 从 0 开始)的长度为 2K,求编号为 0 的城市到其他城市的最短距离。

输入格式
第一行两个正整数 N,M,表示有 N 个城市,M 条道路。

接下来 M 行两个整数,表示相连的两个城市的编号。

输出格式
N−1 行,表示 0 号城市到其他城市的最短路,如果无法到达,输出 −1,数值太大的以 mod100000 的结果输出。

数据范围
2≤N≤100,
1≤M≤500
输入样例:
4 4
1 2
2 3
1 3
0 1
输出样例:
8
9
11

最小生成树-贪心

等比数列公式 : a 1 ( 1 − q n ) 1 − q , ( n 为项数 ) 等比数列公式:\frac{a_1(1-q^n)}{1-q} ,(n为项数) 等比数列公式:1qa1(1qn),(n为项数)
2 k > 2 0 + 2 1 + 2 2 + . . . + 2 k − 1 = 2 k − 1 2^k > 2^0 + 2^1 + 2^2 + ... + 2^{k-1} = 2^k-1 2k>20+21+22+...+2k1=2k1
由上式可知:后面一条边能把前面所有边颠覆
【大于前面所有边之和==> 最早未连通时就立刻选择通路-路径长度最短】
(类似的贪心想法题:145.最大异或对)

a,b连通:一定不加 ; 不连通:一定要加 [越早加越短]
等价最小生成树 ==> Kruskal算法贪心 (需并查集) + Floyd(求多源最短路)

数大于LL若要比较则要写高精度,此题贪心选取则不需要比较
过程不用排序:依题意知长度已经从小到大排好序 【按点的编号递增:点编号k --> 路径长度 2 k 2^k 2k

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 110, MOD = 100000, INF = 0x3f3f3f3f;

int n, m;
int p[N];
int d[N][N];

int find(int x)//并查集
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main()
{
    cin >> n >> m;
    for (int i = 0; i < n; i ++ ) p[i] = i; //依题:下标从0开始

    memset(d, 0x3f, sizeof d);
    for (int i = 0; i < n; i ++ ) d[i][i] = 0;

    for (int i = 0, len = 1; i < m; i ++, len = len * 2 % MOD) //每条路径长度 : 2^k % 100000
    {
        int a, b;
        cin >> a >> b;
        if (find(a) != find(b))
        {
            p[find(a)] = find(b);
            d[a][b] = d[b][a] = len;//长度len = 2^k
        }
    }

    for (int k = 0; k < n; k ++ )//Floyd
        for (int i = 0; i < n; i ++ )
            for (int j = 0; j < n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);

    for (int i = 1; i < n; i ++ )
        if (d[0][i] == INF) puts("-1");//无法到达
        else cout << d[0][i] % MOD << endl;//依题:最后res = 最短路径 % 100000

    return 0;
}

3556. 最短路径

给定一个 n 个点 m 条边构成的无重边和自环的无向连通图。

点的编号为 1∼n。

请问:

从 1 到 n 的最短距离。
去掉 k 条边后,从 1 到 n 的最短距离。
输入格式
第一行包含整数 T,表示共有 T 组测试数据。

每组数据第一行包含三个整数 n,m,k。

接下来 m 行,每行包含三个整数 x,y,z,表示点 x 和点 y 之间存在一条长度为 z 的边。

最后一行包含 k 个空格隔开的整数,表示去掉的边的编号。

所有边按输入顺序从 1 到 m 编号。

输出格式
每组数据输出占两行。

第一行输出从 1 到 n 的最短距离。

第二行输出去掉 k 条边后,从 1 到 n 的最短距离。无法到达,则输出 −1。

数据范围
1≤T≤10,
1≤n≤50,
1≤m≤n(n−1)/2,
1≤x,y≤n,
1≤z≤100,
1≤k≤m
输入样例:
1
4 4 1
1 2 1
2 3 1
3 4 1
1 4 1
4
输出样例:
1
3

多次判断最小生成树

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 55, M = N * N / 2, INF = 0x3f3f3f3f;

int n, m, Q;
int g[N][N], d[N][N];
struct Edge//存边编号, 也可以pair数组存
{
    int a, b;
}e[M];

void floyd()
{
    memcpy(d, g, sizeof d);//每轮g会更新,复制到d中重新计算最短路

    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

int main()
{
    int T;
    cin >> T;
    while (T -- )
    {
        cin >> n >> m >> Q;
        memset(g, 0x3f, sizeof g);//不可达:INF
        for (int i = 0; i < n; i ++ ) g[i][i] = 0;//邻接矩阵, 自环距离0

        for (int i = 1; i <= m; i ++ )//读入
        {
            int a, b, c;
            cin >> a >> b >> c;
            e[i] = {a, b};
            g[a][b] = g[b][a] = c;
        }

        floyd();
        printf("%d\n", d[1][n]);//指定求从点1-->点n的最短距离

        while (Q -- )//把边删掉再次计算最短路径
        {
            int t;//去掉的边的编号
            cin >> t;
            int a = e[t].a, b = e[t].b;
            g[a][b] = g[b][a] = INF;//改回初始值
        }

        floyd();//再次计算d数组最短路径, 输出结果
        if (d[1][n] == INF) puts("-1");
        else cout << d[1][n] << endl;
    }

    return 0;
}

思维题:

3509. 棋盘遍历问题

给定一个 N×M 的方格棋盘,请问一个棋子从棋盘左上角出发,能否在不重复经过棋盘上的方格的情况下,遍历整个棋盘一次再回到起点。

输入格式
输入包含多组测试数据。

每组数据占一行,包含两个整数 N,M。

输出格式
每组数据输出一行一个结果,如果能回到起点则输出 Y,否则输出 N。

数据范围
1≤N,M≤10
输入样例:
1 2
2 2
输出样例:
N
Y

在这里插入代码片

20. 哈希表、双指针、序列型DP

哈希表:

3447. 子串计算

给出一个 01 字符串,求其每一个子串出现的次数。

输入格式
输入包含多行,每行一个字符串。

输出格式
对每个字符串,输出它所有出现次数在 1 次以上的子串和这个子串出现的次数,输出按字典序排序。

数据范围
输入字符串的长度不超过 100。
每组输入最多包含 100 个字符串。

输入样例:
10101
输出样例:
0 2
01 2
1 3
10 2
101 2

O ( n 2 ) O(n^2) O(n2) 类似插入排序遍历【各种不同长度的连续的组合穷举】
n ( n + 1 ) / 2 n(n+1) / 2 n(n+1)/2


#include <iostream>
#include <cstring>
#include <algorithm>
#include <map> //(key,value)  默认按key排序 string类型时:默认按字典序排序

using namespace std;

int main()
{
    string str;
    while (cin >> str)
    {
        map<string, int> hash;
        for (int i = 0; i < str.size(); i ++ )
        {
            string s;
            for (int j = i; j < str.size(); j ++ )
            {
                s += str[j];
                hash[s] ++ ;
            }
        }

        for (auto &[k, v]: hash)// []二元遍历      &不加也可以不要求改变值
            if (v > 1)//输出出现次数大于1的
                cout << k << ' ' << v << endl;
    }

    return 0;
}

3542. 查找

给定一个长度为 n 的数组 a1,…,an 和一个长度为 m 的数组 b1,…,bm。

对于每一个 bi,请你查找其是否在数组 a 中出现过。

输入格式
第一行包含整数 n。

第二行包含 n 个整数 a1,…,an。

第三行包含整数 m。

第四行包含 m 个整数 b1,…,bm。

输出格式
共 m 行,对于第 i 行,如果 bi 在数组 a 中出现过,则输出 YES,否则输出 NO。

数据范围
1≤m,n≤100,
1≤ai,bi≤109
输入样例:
5
1 5 2 4 3
3
2 5 6
输出样例:
YES
YES
NO

unordered_set 仅判是否存在不需要存重复元素
unordered_set<int> S调用S.insert(S类型: value)和S.count(S类型: value)


#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_set>

using namespace std;

int main()
{
    int n, m;
    cin >> n;

    unordered_set<int> S;//只需要判断是否出现过
    while (n -- )
    {
        int x;
        cin >> x;
        S.insert(x);//unordered_set中调用insert插入函数,存入数组a的值
    }

    cin >> m;

    while (m -- )
    {
        int x;
        cin >> x;
        if (S.count(x)) puts("YES");//unordered_set中调用count使用:判是否出现(查找某个元素x)
        else puts("NO");
    }

    return 0;
}

3581. 单词识别

输入一行英文句子,统计句子中出现的各个单词(不区分大小写)及其出现次数。

要求能识别英文句号和逗号,即是说单词由空格、句号和逗号隔开。

输入格式
共一行,包含一个长度不超过 1000 的字符串。

字符串中只包含大小写字母,空格,英文句号和逗号。

输出格式
按字典顺序,输出每个单词及其出现次数。

单词在输出时,应将字母全部转化为小写。

每个单词的输出占一行。

具体格式为:

word:times

数据范围
输入字符串长度不超过 1000。
至少存在一个有效单词,单词一定完全由字母构成。

输入样例:
A blockhouse is a small castle that has four openings through which to shoot.
输出样例:
a:2
blockhouse:1
castle:1
four:1
has:1
is:1
openings:1
shoot:1
small:1
that:1
through:1
to:1
which:1

map:hash + 双指针
判断字母:isalpha(str[i])
转小写字母 : tolower(str[i])
转大写字母 : toupper(str[i])



#include <iostream>
#include <cstring>
#include <algorithm>
#include <map>

using namespace std;

int main()
{
    string str;
    getline(cin, str);//有空格,读入一行,之后再拆分读取单词

    map<string, int> hash;
    for (int i = 0; i < str.size(); i ++ )
    {
        if (isalpha(str[i]))
        {
            int j = i;
            string word;
            while (j < str.size() && isalpha(str[j]))//isalpha判断是否为字母([a,z]或[A,Z])
                word += tolower(str[j ++ ]);//tolower字母全部转小写; 相对应的还有toupper全部转大写
            hash[word] ++ ;
            i = j;
        }
    }

    for (auto& [k, v]: hash)
        cout << k << ':' << v << endl;

    return 0;
}

双指针:

3487. 最小面积子矩阵

一个 N×M 的矩阵,找出这个矩阵中所有元素的和不小于 K 的面积最小的子矩阵(矩阵中元素个数为矩阵面积)。

输入格式
第一行包含三个整数 N,M,K。

接下来 N 行,每行包含 M 个整数,表示矩阵中元素的值。

输出格式
输出最小面积的值。

如果出现任意矩阵的和都小于 K,直接输出 −1。

数据范围
1≤N,M≤100,
1≤K≤109,
矩阵中元素的值的范围 [0,1000]。

输入样例:
4 4 10
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
输出样例:
1
前缀和优化 , 枚举列宽:满足元素和>=k的最小面积矩阵[可能无解]

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 110, INF = 100000;

int n, m, k;
int s[N][N];

int main()
{
    cin >> n >> m >> k;  //【前缀和用到下标[i-1]:从1开始】
    for (int i = 1; i <= n; i ++ )//计算前缀和 【但只需计算每列的不同行高的值的前缀和】:等效一维
        for (int j = 1; j <= m; j ++ )
        {
            cin >> s[i][j];
            s[i][j] += s[i - 1][j];
        }

    int res = INF;
    for (int x = 1; x <= n; x ++ )
        for (int y = x; y <= n; y ++ )
        {
            for (int i = 1, j = 1, sum = 0; i <= m; i ++ )//i,j左右列宽边界   x,y上下行高边界
            {
                sum += s[y][i] - s[x - 1][i];//第x行到第y行的高度的子矩阵的值:前缀和优化O(n^2)
                while (sum - (s[y][j] - s[x - 1][j]) >= k)//从第j列开始不断扩大列宽
                {
                    sum -= s[y][j] - s[x - 1][j];
                    j ++ ;
                }

                if (sum >= k) res = min(res, (y - x + 1) * (i - j + 1));//面积:行高*列宽
            }
        }

    if (res == INF) res = -1;//全部元素相加小于k(不更新):无解
    cout << res << endl;

    return 0;
}

序列型DP:

3393. 最大序列和

给出一个整数序列 S,其中有 N 个数,定义其中一个非空连续子序列 T 中所有数的和为 T 的“序列和”。

对于 S 的所有非空连续子序列 T,求最大的序列和。

输入格式
第一行包含一个整数 N。

第二行包含 N 个整数,表示序列中的元素。

输出格式
输出一个数,表示最大序列和。

数据范围
1≤N≤106,
序列中的元素的取值范围 [−109,109]。

输入样例1:
5
1 5 -3 2 4
输出样例1:
9
输入样例2:
6
1 -2 3 4 -10 6
输出样例2:
7
输入样例3:
4
-3 -1 -2 -5
输出样例3:
-1
在这里插入图片描述

集合:以第j个数为结尾的子序列
属性:最大值
计算: f[j] 与 f[j - 1] + w[j] : 可提取为 f[i] = max(f[i - 1] , 0) + w[i];

//没有单调性不能双指针,用DP分析,但实现不一定DP写法
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long LL;//1e9级别 LL

const int N = 1000010; 

int n;

int main()
{
    scanf("%d", &n);

    LL res = -1e18; 
    LL sum = 0;
    for (int i = 1; i <= n; i ++ )
    {
        int x;
        scanf("%d", &x); 
        sum += x;
        res = max(res, sum);//返回答案,存最大
        sum = max(0ll, sum);//sum初始为0,小于0说明为负数,清零等效舍弃负数段 
    }

    printf("%lld\n", res);
    return 0;
}


3505. 最长ZigZag子序列

一个整数序列的子序列是指从给定序列中随意地(不一定连续)去掉若干个整数(可能一个也不去掉)后所形成的整数序列。

对于一个整数序列 x1,x2,…,xn,如果满足下列两个条件之一:

∀i∈[1,n−1],当 i%2=1 时,xi−xi+1 为正,当 i%2=0 时,xi−xi+1 为负。
∀i∈[1,n−1],当 i%2=1 时,xi−xi+1 为负,当 i%2=0 时,xi−xi+1 为正。
那么,我们就称这个整数序列为 ZigZag 序列。

换句话说,ZigZag 序列就是一个序列内元素在增大和减小之间不断切换的序列。

例如 1,7,4,9,2,5 就是一个 ZigZag 序列。

现在,给定一个长度为 n 的整数序列,请你求出它的最长 ZigZag 子序列的长度。

输入格式
第一行包含整数 n。

第二行包含 n 个整数。

输出格式
输出一个整数,表示最长 ZigZag 子序列的长度。

数据范围
1≤n≤50,
序列内元素取值范围 [1,1000]。

输入样例:
6
1 7 4 9 2 5
输出样例:
6

上升下降交替最长子序列 - DP

f[i]表示以第i个数字结尾并且是前一个数字上升得到的a[i]
g[i]表示以第i个数字结尾并且是前一个数字下降得到的a[i]
集合划分:【只有一个a[i]】【其他:倒数第二个元素是第j个数字并且需要是下降得到a[j]:g[j] + 1】
状态计算:f[i] = max(f[i], g[j] + 1);

在这里插入图片描述



//f[i]表示以a[i]结尾且最后上升的子序列,g[i]表示以a[i]结尾且最后下降的子序列
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 55;

int n;
int w[N];
int f[N], g[N];

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++ ) cin >> w[i];

    int res = 0;
    for (int i = 0; i < n; i ++ )
    {
        f[i] = g[i] = 1;//不存在上升下降交替,至少自身长度算1个
        for (int j = 0; j < i; j ++ )
            if (w[j] > w[i]) f[i] = max(f[i], g[j] + 1);//下一步为上升序列,选f[i]可以由g[j]+1得到
            else if (w[j] < w[i]) g[i] = max(g[i], f[j] + 1);//下一步为下降序列,选g[i]可以由f[j]+1得到
        res = max(res, max(f[i], g[i]));//每轮最后比较f[i]与g[i]取最长
    }

    cout << res << endl;
    return 0;
}


21. 红黑树和并查集

836. 合并集合

一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。

现在要进行 m 个操作,操作共有两种:

M a b,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
Q a b,询问编号为 a 和 b 的两个数是否在同一个集合中;
输入格式
第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 M a b 或 Q a b 中的一种。

输出格式
对于每个询问指令 Q a b,都要输出一个结果,如果 a 和 b 在同一集合内,则输出 Yes,否则输出 No。

每个结果占一行。

数据范围
1≤n,m≤105
输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

violet~evergarden

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值