目录
1.枚举(循环暴力列举)
1.1算法概述
枚举方法:
通过逐个尝试所有可能的值或组合来解决问题。
问题空间划分:
将问题空间划分为一系列离散的状态,并通过遍历这些状态来寻找解决方案。
1.2示例

# 定义方程
def equation(x, y):
return x**2 + y**2 == 100
# 枚举 x 和 y 的值,时间复杂度O(n**2)
for x in range(-10, 11):
for y in range(-10, 11):
if equation(x, y):
print(f"解得: x = {x}, y = {y}")
1.3百钱买百鸡问题

for i in range(21):#将循环遍历的范围缩减可有效提高运算速度
for j in range(34):
if i*5+j*3+(100-i-j)/3==100:
print("公鸡:",i,"母鸡:",j,"小鸡:",(100-i-j))
2、排序
2.1冒泡排序

当输入的序列长度为n,冒泡排序进行n-1轮排序;每一轮比较的组数都-1,最后一轮比较列表中第1、2个元素。
2.1.1冒泡排序概述
当输入的序列长度为 n 时,冒泡排序进行 n-1 轮排序。
2.1.2排序过程
给定一个长度为 n 的列表,算法循环 n-1 次可以得到有序序列。
2.1.3循环比较
第一次循环:比较列表中每一对相邻的元素,从 a[0], a[1] 到 a[n-2], a[n-1]。
第二次循环:比较除了最后两个元素之外的所有相邻元素,即 a[0], a[1] 到 a[n-3], a[n-2]。
第三次循环:继续这个过程,直到比较 a[0], a[1] 到 a[n-4], a[n-3]。
第 i 次循环:比较 a[0], a[1] 到 a[n-i-1], a[n-i]。
第 n-1 次循环:比较列表中的前两个元素 a[0], a[1]。
2.1.4时间复杂度
最坏和平均情况下的时间复杂度为 O(n**2)。
2.1.5空间复杂度
空间复杂度为 O(1)O(1),因为冒泡排序是原地排序,不需要额外的存储空间。
2.1.6稳定性
冒泡排序是稳定的排序算法,即相等的元素在排序后保持它们原始的顺序。
2.1.7例题
https://www.lanqiao.cn/problems/3225

# 读取输入的宝藏数量
n = int(input())
# 读取宝藏的珍贵程度,以空格分隔的整数形式输入,并转换为列表
a = list(map(int, input().split()))
# 冒泡排序算法,循环 n-1 次
for i in range(n - 1):
# 第 i 次循环,从 a[0] 到 a[n-i-1]
for j in range(n - i - 1):
# 如果当前元素大于下一个元素,则交换它们
if a[j] > a[j + 1]:
a[j], a[j + 1] = a[j + 1], a[j]
# 打印排序后的宝藏珍贵程度列表
print(' '.join(map(str, a)))
2.2选择排序

区间下标从0开始([0,n-1]),以第一次比对来说,第0次循环从[0,n-1]找最小元素a[x]与 a[0]换位,如果a[0]就是最小的则不动,但这就算一次操作。
当长度为n时也是进行n-1轮。
2.2.1算法概述
给定一个长度为 n 的列表,算法通过 n-1 次循环可以得到一个有序序列。
2.2.2循环过程
第 0 次循环:在列表的 [0, n-1] 范围内找到最小元素 a[x],并与 a[0] 交换位置。如果 a[0] 已经是最小的,则不进行交换,但仍然算作一次操作。
第 1 次循环:在列表的 [1, n-1] 范围内找到最小元素,并与 a[1] 交换。
第 2 次循环:在列表的 [2, n-1] 范围内找到最小元素,并与 a[2] 交换。
第 i 次循环:在列表的 [i, n-1] 范围内找到最小元素,并与 a[i] 交换。
第 n-2 次循环:在列表的 [n-2, n-1] 范围内找到最小元素,并与 a[n-2] 交换。
2.2.3时间复杂度
选择排序的时间复杂度为 O(n2)O(n2),这是因为在最坏和平均情况下,算法都需要进行 n-1 轮循环,每轮循环中都需要遍历剩余的 n-i 个元素(其中 i 是当前循环的索引)。
2.2.4空间复杂度
选择排序的空间复杂度为 O(1)O(1),因为它是原地排序,不需要额外的存储空间。
2.2.5稳定性
选择排序是稳定的排序算法,相等的元素在排序后保持它们原始的顺序。
# 读取输入的序列长度
n = int(input())
# 读取序列并转换为整数列表
a = list(map(int, input().split()))
# 循环 n-1 次进行排序
for i in range(0, n - 1):
# 初始化最小元素为当前索引 i 的元素
min_value = a[i]
min_idx = i
# 从当前索引 i 到列表末尾 n-1 寻找最小元素
for j in range(i, n):
if a[j] < min_value:
min_value = a[j]
min_idx = j
# 将找到的最小元素与当前索引 i 的元素交换
a[i], a[min_idx] = a[min_idx], a[i]
# 打印当前排序状态
print(' '.join(map(str, a)))
2.3插入排序

循环排序过程中,将序列逻辑上分为左半边(已排序,比对从后往前看)和右半边(未排序, 正常顺序到数组结束),新元素(右半边选到的当前待排元素)、当前元素(左半边比对到的已 排元素),左已排>右未排,左后移一位,直到右在有序的左半边找到位置(一轮结束)。
不稳定:[5,4,3,2,1] --> [1,2,3,4,5]时,时间复杂度差不多O(n**2)
2.3.1插入排序概述
将排序过程比喻为摸牌,每次摸一张牌(元素),然后从后往前判断是否可以插入到已排序的序列中。
2.3.2排序过程
对于第 i 张牌 a[i],[0, i-1] 中的牌都是已经排好顺序的。
2.3.3比较和插入
从后往前逐个判断 a[j] 是否大于 a[i]。
如果 a[j] > a[i]:则 a[j] 往后挪一个位置。
如果 a[j] <= a[i]:此时在 a[j+1] 的位置放置 a[i]。
2.3.4时间复杂度
最坏和平均情况下的时间复杂度为 O(n2)O(n2),因为每轮循环中可能需要比较和移动 n-i 个元素。
2.3.5空间复杂度
空间复杂度为 O(1)O(1),因为插入排序是原地排序,不需要额外的存储空间。
2.3.6稳定性
插入排序是不稳定的排序算法。这意味着如果两个元素相等,它们在排序后的顺序可能与排序前不同。图片中的例子 [5, 4, 3, 2, 1] 排序后变为 [1, 2, 3, 4, 5] 展示了这一点。
# 读取输入的序列长度
n = int(input())
# 读取序列并转换为整数列表
a = list(map(int, input().split()))
# 对于第 i 个数字,在区间 [0, i-1] 中从后往前找对应插入的位置
for i in range(1, n):
value = a[i] # 要插入的值
insert_idx = 0 # 插入元素的下标
for j in range(i - 1, -1, -1): # 从后往前遍历
if a[j] > value: # 如果找到比 value 大的数
a[j + 1] = a[j] # 往后挪一个位置
else:
insert_idx = j + 1 # 记录插入位置
break # 找到插入位置,跳出循环
# 插入第 i 个数字
a[insert_idx] = value
# 打印排序后的序列
print(' '.join(map(str, a)))
3.搜索
搜索算法:
搜索算法是一种穷举算法,它通过遍历问题解空间的部分或所有可能情况来寻找问题的解决方案。
3.1DFS(基础)
3.1.1DFS概述
深度优先搜索(DFS)
深度优先搜索是一种特定的搜索策略,它从根节点开始,沿着树的深度遍历节点,尽可能深地搜索树的分支。
本质上是暴力枚举:
深度优先搜索可以看作是一种暴力搜索方法,因为它不进行剪枝,而是尝试所有可能的路径直到找到解决方案或确定没有解决方案。
深度优先的特点:
在搜索过程中,深度优先搜索尽可能沿着一条路径深入到底,直到无法继续前进(即到达叶子节点或确定该路径不包含解决方案),然后回溯到上一个节点,继续探索其他路径。
DFS原理是递归地遍历图(的节点),从一个点作为出发点,走到规定的终点,每次在一条通路上找没标记过的点继续走,路不通就回退到上一个节点走其他路(也就是回退)。

3.1.2dfs和n重循环
问题描述:
给定一个数字 x,需要将其拆分成3个正整数,且后一个数必须大于等于前一个。
解决方案:
最简单的方法是使用三重循环进行暴力求解,即尝试所有可能的组合。
扩展问题:
将数字拆分成4个正整数。
将数字拆分成 n 个正整数。
算法思想:
对于拆分成 n 个正整数的问题,需要实现 n 重循环。
这种 n 重循环的结构可以看作是一棵特定的树状结构,其中每个节点代表一个可能的拆分方案。
深度优先搜索(DFS):
深度优先搜索算法适合解决这类问题,因为dfs搜索时沿着树状结构的一条路径深入探索所有可能的拆分方案,直到找到满足条件的解或遍历完所有可能。
注意dfs的出口(递归的出口,递归本质是栈的结构性质):
n层循环->n层的树,dfs(0)开始,那么depth=n时就是出口,有条件就在这一层判断是否 合法,没有条件就直接return。

3.2DFS(回溯、剪枝)
3.2.1策略概述
回溯算法:
回溯是深度优先搜索(DFS)的一种形式,用于在搜索过程中寻找问题的解决方案。
当发现当前路径不满足求解条件时,算法会“回溯”返回,尝试其他可能的路径。
回溯的特点:
回溯算法强调如果当前路径不可行(即不能达到问题的解),则需要寻找其他路径。
在搜索过程中,需要对已经走过的路径进行标记,以避免重复搜索。
剪枝策略:
回溯法通常会在DFS的基础上加入一些剪枝策略,以减少搜索空间,提高效率。
剪枝策略包括但不限于:
限制搜索深度、提前终止不可能产生解的路径、使用启发式规则来评估路径的可行性等。
回溯算法常用于解决组合问题、排列问题、切割问题等,如八皇后问题、数独(Sudoku),以及图的哈密顿路径问题等。它通过递归地探索所有可能的候选解,直到找到满足条件的解或遍历完所有可能的候选解。
思考:确定记录路径后是否要清除标记,什么时候清除?

3.2.2例题
N皇后问题
https://www.lanqiao.cn/problems/1508

同行、列及与棋盘边框成45度的斜线(对角线及它的平行线)不能同时有>=2个皇后
def dfs(num, res, row):
if row == num:
print(res)
return
for col in range(num):
if check(col, res, row):
res[row] = col
dfs(num, res, row + 1)
res[row] = 0
def check(col, res, row):
for i in range(row):
if res[i] == col or res[i] + i == row + col or res[i] - i == col - row:
return False
return True
3.3BFS(广度优先遍历)
3.3.1BFS概述
bfs多用在图论题中
BFS遍历:
BFS是一种图遍历算法,通常用于寻找从起点到其他顶点的最短路径。
节点分层:
在BFS中,节点被分为不同的层,起点位于第0层。算法每次处理一层中的所有节点,然后才处理下一层。
求3到5的最短路径:
图中展示了从节点3到节点5的最短路径,以及路径上的节点层级:
第0层:3
第1层:2, 4, 6
第2层:1, 5
第3层:0

BFS算法步骤:
使用队列来实现BFS遍历:将起点加入队列,标记起点,更新起点的距离为0;当队列非空时,执行以下操作:取出队列的第一个元素 u;如果 u 是终点,则结束搜索。
对于所有与 u 相邻的节点 v,如果 v 未被标记,则将 v 加入队列,标记 v,并更新 v 的距离。

3.3.2例题
https://www.lanqiao.cn/problems/3819


分类讨论:
1、从起点(A,B)出发,可以直接走到终点(C,D)(路是通的,体力足够)
- 从(A,B)走到任意圣泉,再走到终点
- 第一种情况从起点一次BFS到终点做判断。第二种从起点一次BFS,终点再一次BFS,都是BFS到圣泉,就可以枚举所有圣泉(O(1))判断是否有合法解。(第二种原理是都边走边打标记,当以标记连成的两条‘半路’加上一处圣泉是通路时为合法解。对于一处圣泉,它的四面有>=两处的标记代表能路能连通)
from collections import deque
# 读取地图的尺寸 n*m,起点坐标 (A, B) 和终点坐标 (C, D)
n, m = map(int, input().split())
a, b, c, d = map(int, input().split())
# 将坐标转换为从0开始的索引
a, b, c, d = a - 1, b - 1, c - 1, d - 1
# 读取地图,其中 '.' 表示可走的路,'#' 表示墙壁,'V' 表示圣泉
s = [input() for _ in range(n)]
# 读取小蓝的初始能量 E
E = int(input())
# 设置无穷大数,用于初始化距离矩阵
inf = float('inf')
# 初始化两个距离矩阵,用于存储从起点和终点到每个点的距离
dist1 = [[inf] * m for _ in range(n)]
dist2 = [[inf] * m for _ in range(n)]
# 定义移动方向:上、下、左、右
dir = [(1, 0), (-1, 0), (0, 1), (0, -1)]
# 定义 BFS 函数,用于计算从 (x, y) 到所有可达点的最短路径
def bfs(x, y, dist):
vis = [[0] * m for _ in range(n)] # 访问标记矩阵,记录点是否被访问过
q = deque() # 使用双端队列实现 BFS
q.append([x, y]) # 将起点加入队列
vis[x][y] = 1 # 标记起点为已访问
dist[x][y] = 0 # 起点到自身的距离为0
while q: # 当队列非空时,继续 BFS 过程
x, y = q.popleft() # 取出队列头部的点
for i in range(4): # 遍历四个移动方向
xx, yy = x + dir[i][0], y + dir[i][1] # 计算新坐标
if 0 <= xx < n and 0 <= yy < m and vis[xx][yy] == 0 and s[xx][yy] != '#': # 检查新坐标是否在地图内、未访问过、不是墙壁
q.append([xx, yy]) # 将新点加入队列
vis[xx][yy] = 1 # 标记新点为已访问
dist[xx][yy] = dist[x][y] + 1 # 更新新点到起点的距离
# 从起点执行 BFS,计算到所有点的距离
bfs(a, b, dist1)
# 从终点执行 BFS,计算到所有点的距离
bfs(c, d, dist2)
# 计算从起点到终点的最短路径长度
res = dist1[c][d]
# 如果路径长度小于等于能量 E,则可以直接输出结果
if res <= E:
print(res)
else:
res = inf # 如果路径长度大于能量,初始化结果为无穷大
# 遍历所有点,检查是否有圣泉,如果有,计算通过圣泉的最短路径
for i in range(n):
for j in range(m):
if s[i][j] == 'V': # 如果点是圣泉
res = min(res, dist1[i][j] + dist2[i][j]) # 更新结果为通过圣泉的最短路径
# 如果结果仍然是无穷大,表示无法逃离,输出 No
if res == inf:
print('No')
else:
# 如果可以通过圣泉逃离,计算并输出所需时间
print((res - E) * 2 + E)
4.贪心
4.1思想
贪心算法思想:
贪心算法是一种在每一步选择中都采取当前状态下最好或最优的选择,从而希望导致结果是全局最好或最优的算法。
核心性质:
贪心算法的核心性质是局部最优解能决定全局最优解。这意味着在每个决策步骤中,选择当前看起来最优的选择,最终能够导致整个问题的最优解。
适用条件:
如果问题具有贪心选择性质,即局部最优解能导致全局最优解,那么可以采用贪心算法来求解。
4.2例子

贪心算法的局限性:
并不是所有问题采用局部最优都能得到全局最优解。例如,如果硬币面值改为1元、2元、4元、5元、6元,支付9元时,贪心算法可能会选择6+2+1,需要3枚硬币,但最优解是4+5,只需要2枚硬币。
4.3例题
https://www.lanqiao.cn/problems/545

import heapq
# 读取输入的数字个数
n = int(input())
# 读取数字并转换为整数列表
a = list(map(int, input().split()))
# 将列表转换为堆
heapq.heapify(a)
# 初始化答案变量
ans = 0
# 当堆中元素个数大于1时,执行循环
while len(a) != 1:
# 弹出堆中的两个最小元素
x = heapq.heappop(a)
y = heapq.heappop(a)
# 将两个元素的和放回堆中
heapq.heappush(a, x + y)
# 累加答案
ans += x + y
# 打印最终答案
print(ans)
5.模拟
5.1概述
模拟题:这类题目通常要求你根据题目的描述来模拟一个过程或系统,而不是解决一个算法问题。
注意点:
读懂题:在编程之前,首先要彻底理解题目的要求和流程。
代码和步骤一一对应:编写的代码应该与解题步骤紧密对应,包括变量名、函数名以及每个函数的功能都应该清晰明确。
提取重复的部分:在编程过程中,如果发现有重复的代码块,应该将其提取出来,写成独立的函数或子模块,以提高代码的可读性和可维护性。
按顺序写,分块调试:按照逻辑顺序编写代码,并在完成每个部分后进行调试,确保每个模块都能正确运行。
5.2例题

n = int(input())
ans = n
while n >= 3:
n -= 3
ans += 1
n += 1
print(ans)
从用户那里获取一个整数输入n
初始化ans为n的值
当n大于或等于3时,执行循环:
从n中减去3
将ans增加1
将n增加1
循环结束后,打印ans的值
n = int(input())
ans = n
while n >= 3:
ans += n // 3
n = n // 3 + n % 3
print(ans)
从用户那里获取一个整数输入n
初始化ans为n的值
当n大于或等于3时,执行循环:
将ans增加n除以3的整数部分(n // 3)
更新n为n除以3的整数部分加上n除以3的余数(n % 3)
循环结束后,打印ans的值
6.二分(查找)
二分的关键除了运用模板,还要通过题意写对check函数,来作为判断下一个区间的条件
6.1概述
二分法:这是一种在有序数组中查找特定元素的搜索算法。它通过比较数组中间元素和目标值来工作。如果中间元素正好是目标值,则搜索结束。如果目标值小于中间元素,则在数组的前半部分继续搜索;如果目标值大于中间元素,则在数组的后半部分继续搜索。这个过程重复进行,每次都将搜索范围缩小一半,直到找到目标值或搜索范围为空。
时间复杂度:二分查找算法的时间复杂度是O(log n),其中n是数组中元素的数量。这是因为每次迭代都将搜索范围减半,所以需要的迭代次数是对数级别的。
前提条件:二分查找的前提是数组必须是有序的,且函数或数组的值具有单调性。单调性意味着数组中的元素要么全部是非递减的(升序),要么全部是非递增的(降序)。
核心:二分查找的核心思想是利用数组的单调性来调整搜索区间。通过比较中间元素和目标值,可以确定目标值位于中间元素的哪一侧,从而缩小搜索范围。
6.2例题
https://www.lanqiao.cn/problems/99


# 读取小朋友的数量K和巧克力的总块数N
N, K = map(int, input().split())
# 初始化一个列表a,用于存储每块巧克力的长和宽
a = []
# 循环N次,每次读取一块巧克力的长和宽,并存储在列表a中
for i in range(N):
x, y = map(int, input().split())
a.append((x, y))
# 二分查找 定义一个函数check(x),用于判断边长为x时,能否切出至少K块正方形巧克力
def check(x):
# 初始化计数器cnt,用于统计切出的正方形巧克力块数
cnt = 0
# 遍历每块巧克力
for h, w in a:
# 对于每块巧克力,计算可以切出多少块边长为x的正方形巧克力
cnt += (h // x) * (w // x)
# 如果切出的巧克力块数大于等于K,返回True
return cnt >= K
# 初始化二分查找的左右边界,left为1,right为100000,ans为1,用于存储最大边长
left, right = 1, 100000
ans = 1
# 当左边界小于等于右边界时,进行二分查找
while left <= right:
# 计算中间值mid
mid = (left + right) // 2
# 如果check(mid)为True,说明边长为mid可以满足条件,尝试更大的边长
if check(mid):
ans = mid # 更新最大边长
left = mid + 1 # 更新左边界
else:
# 如果check(mid)为False,说明边长为mid不能满足条件,尝试更小的边长
right = mid - 1
# 输出最大边长
print(ans)
7.DP(普通一维)
7.1解析
动态规划:是一种算法思想,用于解决具有重叠子问题和最优子结构特征的问题。
重叠子问题:在动态规划中,子问题是原问题的小版本。这意味着在解决原问题的过程中,相同的子问题会被重复计算多次。
最优子结构:在动态规划中,大问题的最优解包含小问题的最优解。这意味着通过解决小问题并存储它们的解,可以推导出大问题的最优解。
7.2例题



# 定义楼梯的最大级数N和模数MOD
N = 100010
MOD = 1000000007
# 读取楼梯的总级数n和坏了的台阶数m
n, m = map(int, input().split())
# 初始化vis数组,用于标记台阶是否损坏,0表示未损坏,1表示损坏
vis = [0] * N
# 初始化f数组,用于存储到达每级台阶的方案数
f = [0] * N
# 读取坏掉的台阶编号,并标记在vis数组中
X = list(map(int, input().split()))
for x in X:
vis[x] = 1 # 将坏掉的台阶编号对应的vis数组位置设为1
# 设置初始条件,第0级台阶有1种到达方式,第1级台阶的到达方式取决于它是否损坏
f[0] = 1
f[1] = 1 - vis[1]
# 动态规划计算到达每一级台阶的方案数
for i in range(2, n + 1):
# 如果当前台阶损坏,则跳过不计算
if vis[i]:
continue
# 如果当前台阶未损坏,计算到达该台阶的方案数,即前两级台阶方案数之和对MOD取模
f[i] = (f[i - 1] + f[i - 2]) % MOD
# 输出到达第n级台阶的方案数,对MOD取模的结果
print(f[n])
8.高精度
高精度加法(是进行阶乘计算的前提,模拟的就是我们小学学习的竖式加法运算过程)
阶乘计算(阶乘计算模拟的是竖式乘法)(乘法分配律)
矩阵的幂运算(模拟矩阵的计算过程、还会分享一下普通矩阵如何转置)
矩阵面积交(探索矩阵顶点之间的关系)(还有扫描线算法)
8.1高精度加法
问题描述

分析
使用一个临时变量,记录之前相加需要进位的数,也就是说在相加的时候,始终有三个变量在相加,相加之后将存储结果的临时变量的个位取出来,放进结果列表中,然后整除以10,用于下一位置的计算。将数组进行了颠倒是为了便于计算,当较短的数组超出的时候就不必再使用短数组进行运算了。
解法
# 初始化结果数组ans,用于存储相加后的结果
ans = []
# 读取输入的两个大整数a和b,并将它们转换为数组形式,同时颠倒数组的顺序以便于从个位开始计算
m = [int(x) for x in list(input()[::-1])]
n = [int(x) for x in list(input()[::-1])]
# 计算两个数组的长度,确定较短和较长的数组
minlen = min(len(n), len(m))
maxlen = max(len(n), len(m))
# 如果数组n比数组m长,交换它们,以确保m是较长的数组
if len(n) > len(m):
m, n = n, m
# 初始化进位变量temp为0,两个个位数相加大于10就要进位 temp
temp = 0
# 从最低位开始逐位相加
for i in range(maxlen):
# 如果当前位在较短数组的长度内,将两个数组的对应位相加
if i < minlen:
temp = n[i] + m[i] + temp
# 如果当前位超出了较短数组的长度,只将较长数组的对应位与进位相加
else:
temp += m[i]
# 将当前位的和的个位数添加到结果数组中
ans.append(temp % 10)
# 更新进位值
temp //= 10
# 将结果数组反转,因为我们是从最低位开始计算的
print("".join(map(str, ans[::-1])))
#10-11:长数组放前面,当短数组超出的时候,有意思是短数组的有效数位加完了,到了前导0,那么长数组长过短数组的第一位有两个结果相加(长数组当前位,临时数组的最后一位,后面都是长数组当前位+0)
#14-17行也有体现

8.2高精度乘法(阶乘)

# 读取输入的正整数n
n = int(input())
# 初始化结果数组ans,长度足够大以存储n!的结果
ans = [0] * 3000
ans[0] = 1 # 初始化乘积为1
# 从2开始到n,逐个乘以ans数组
for i in range(2, n + 1):
j = 0
temp = 0
# 逐位乘以当前的i,并处理进位
while j < len(ans) or temp != 0:
temp = ans[j] * i + temp
ans[j] = temp % 10 # 将个位数存储回ans数组
temp //= 10 # 更新进位,因为j+1了即temp向下整除取到十位上的数值
j += 1
# 反转ans数组并转换为字符串,然后转换为整数输出
print(int("".join(map(str, ans[::-1]))))

#5-6(乘法的竖式长的放后面模拟位的*10)本质上都是加完短的,长的加短的前导0后面第一个进位的,临时数组最后一位
#23-30第一个数整个数×第二个数的当前位,加上前面的累加结果
通过使用数组来存储每个数的每一位,然后从最低位开始逐位相乘,并处理进位,最后得到的结果存储在ans数组中。在输出时,由于我们是从最低位开始计算的,所以需要将ans数组反转以得到正确的结果。
9、数据结构(常规)
9.1栈
先进后出(LIFO):栈是一种特殊类型的列表,遵循先进后出的原则,即最后添加的元素将是第一个被移除的。(也就是后进先出)
栈的基本操作:
添加元素:使用 a.append(x) 将元素添加到栈顶
获取栈顶元素:使用 a[-1] 获取栈顶元素,但不移除它
去除栈顶元素:使用 a.pop() 移除并返回栈顶元素
栈的应用:
括号匹配问题:检查代码中的括号是否正确匹配。
表达式求值:用于计算数学表达式,如中缀表达式转换为后缀表达式(逆波兰表达式)。
递归机制:递归调用时,每次函数调用都会在内存中创建一个新的栈帧,用于存储函数的局部变量和返回地址。递归的本质是利用栈的性质,最外层的调用(栈底)最后解决,最内层的调用(栈顶)优先解决。
图示:图片中的图示展示了栈的可视化表示,其中 e1 到 e5 表示栈中的元素,top 表示栈顶,size 表示栈的大小。push 操作用于添加元素到栈顶,而 pop 操作用于从栈顶移除元素。
# 初始化一个空栈
stack = []
# 栈的基本操作
# 入栈操作
stack.append('e1') # 添加元素e1到栈顶
stack.append('e2') # 添加元素e2到栈顶
stack.append('e3') # 添加元素e3到栈顶
stack.append('e4') # 添加元素e4到栈顶
# 查看栈顶元素(不移除)
top_element = stack[-1]
print("栈顶元素:", top_element)
# 出栈操作
popped_element = stack.pop() # 移除并返回栈顶元素e4
print("出栈元素:", popped_element)
# 再次查看栈顶元素
top_element_after_pop = stack[-1]
print("出栈后的栈顶元素:", top_element_after_pop)
# 打印整个栈
print("当前栈的元素:", stack)

递归机制本质上是利用栈的性质,最外层的问题(栈底)最后解决,最内层的问题优先解决(栈顶)

9.1.1例题


# 读取输入的括号串长度n和括号串s
n = int(input())
s = input()
# 初始化一个空栈a,用于存储左括号
a = []
# 初始化一个标志变量ok,用于判断括号串是否合法
ok = True
# 遍历括号串的每个字符
for i in range(n):
# 如果当前字符是左括号'('
if s[i] == '(':
# 左括号入栈
a.append('(')
# 如果当前字符是右括号')'
else:
# 右括号出栈
# 如果此时栈空,说明右括号多了,非法
if len(a) == 0:
ok = False
break # 终止循环
else:
# 出栈操作,移除栈顶的左括号
a.pop()
# 最后判断栈是否为空,如果不为空则左括号多了,非法
if len(a) != 0:
ok = False
# 根据ok的值输出结果
if ok:
print("Yes")
else:
print("No")

https://www.lanqiao.cn/problems/3194
s = input()
# 1. 3和4要最近的去换位 2. 5和7删除 3. 6就变成9
s = s.replace('5','') # replace()用于对象是字符串的操作
s = s.replace('7','')
s = s.replace('6','9')
# 转换成list,才可以获得索引
s = list(s)
stack = []# py的list就可以实现栈
for i,c in enumerate(s):
# 3 就入栈,注意是下标入栈
if c == '3':
stack.append(i)
# 严谨一点先判断非空,空栈pop会报错
elif len(stack):
if c == '4':
idx = stack.pop()
# 此时的n[i]是4
# 原序列进行换位
s[idx],s[i] = s[i],s[idx]
print(''.join(s))
# eg:3334433
# stack [0 1 2]
# -- 2 [0 1]
# 下一轮: if c == '3':stack.append(i)就会解决3和4换位后重新入栈的问题
# i=4 pop [0 1 4] -- 换位后的3
# 3343433
9.2队列
队列是一种先进先出的数据结构。
元素被添加到队尾,从队首取出。
可以用 Python 的 list 模拟队列,但 deque(双端队列)在执行删除和插入操作时效率更高。
调用
from collections import deque

# 创建一个空的双端队列
dq = deque()
# 添加元素到队尾
dq.append('1')
dq.append('2')
dq.append('3')
# 添加元素到队首
dq.appendleft('0')
# 展示队列
print("Deque after append and appendleft:", dq)
# 从队首移除元素
popped_element = dq.popleft()
print("Popped element from the front:", popped_element)
# 从队尾移除元素
popped_element = dq.pop()
print("Popped element from the back:", popped_element)
# 展示队列
print("Deque after pop and popleft:", dq)


# 清空队列
dq.clear()
print("Deque after clear:", dq)
# 计算队列中元素等于 'x' 的个数
dq.append('1')
dq.append('2')
dq.append('2')
dq.append('3')
count = dq.count('2')
print("Count of '2' in deque:", count)
# 逆序排列队列
dq.reverse()
print("Deque after reverse:", dq)
# 向右循环移动队列
dq.rotate(1)
print("Deque after rotate:", dq)
# 设置队列的最大长度
dq = deque(maxlen=3)
dq.append('1')
dq.append('2')
dq.append('3')
dq.append('4') # 这将替换掉 '1'
print("Deque with maxlen:", dq)

9.2.1例题

from collections import deque
# 读取操作次数N
N = int(input())
# 初始化一个空的双端队列
a = deque()
# 循环处理N次操作
for _ in range(N):
# 读取操作命令和相应的数据
s = list(map(int, input().split()))
# 根据操作命令的第一个数字执行不同的操作
if s[0] == 1:
# 入队操作,将第二个数字添加到队列的末尾
a.append(s[1])
elif s[0] == 2:
# 出队操作,如果队列不为空,则移除并输出队首元素
if len(a) != 0:
print(a.popleft()) # 输出并移除队首元素
else:
print('no') # 如果队列为空,输出"no"并退出
break # 退出循环
elif s[0] == 3:
# 计算队列中元素个数并输出
print(len(a))
9.3链表
链表的特点:
快速插入和删除:链表允许在任意位置快速插入和删除元素,因为只需要改变节点的指针。
存储效率:链表的每个节点需要额外的空间来存储指针,这可能会影响存储效率。
访问速度:访问链表中的元素需要从头节点开始遍历,直到找到目标节点,这可能比数组慢。
Python 列表的特点:
动态数组:Python 的列表是动态数组,可以自动调整大小。
快速访问:通过索引可以快速访问列表中的任何元素。
内置方法:Python 提供了一系列内置方法来操作列表,如 append(), pop(), insert(), remove() 等。

python中list集成了很多实用的序列操作方法,是功能强大的一种基础数据结构

# 定义一个列表
a = [1, 2, 3, 4]
# 访问元素
print(a[0]) # 输出: 1
# 添加元素
a.append(5) # 在列表末尾添加元素5
a.extend([6, 7]) # 在列表末尾添加多个元素
a.insert(1, 8) # 在索引1的位置插入元素8
print(a) #
# 删除元素
del a[2] # 删除索引2的元素
print(a)
a.pop() # 删除并返回列表末尾的元素
print(a)
a.remove(8) # 删除列表中第一个值为8的元素
print(a)
a.clear() # 清空列表
print(a)

a = [1, 2, 3, 4]
# 查找元素
count_of_3 = a.count(3) # 计算元素3在列表中出现的次数
index_of_2 = a.index(2) # 查找元素2在列表中的索引
print("Count of 3:", count_of_3)
print("Index of 2:", index_of_2)

9.3.1例题
https://www.lanqiao.cn/problems/1111


n, k, m = map(int, input().split())
# 初始化一个列表,代表围坐在圆桌周围的人
a = list(range(1, n + 1))
# 设置初始位置
i = k - 1
# 当还有人在列表中时,继续循环
while len(a) > 0:
# 计算出列的人的索引,使用 (i + m - 1) % len(a) 来循环索引
i = (i + m - 1) % len(a)
# 打印出列的人
print(a.pop(i))
a = list(range(1, n + 1)):
创建一个列表,包含从 1 到 nn 的整数,代表围坐在圆桌周围的人。
i = k - 1:
设置初始位置,由于列表索引从 0 开始,所以 kk 需要减 1。
while len(a) > 0:
当列表中还有人时,继续循环。
i = (i + m - 1) % len(a):
计算出列的人的索引。这里 m - 1 是因为报数从 0 开始计数(编号从1开始,报数也就是索引从0开始),% len(a) 确保索引在列表范围内循环。
但是当m=0时程序会出现问题:
n=3,起始位置 k = 1,报数上限 m = 0,这段代码的运行逻辑存在问题。问题在于,当 m = 0 时,按照题目描述,第一个开始报数的人(即位置 k 的人)应该立即出列。然而,代码中的逻辑是尝试计算下一个出列者的位置,这在 m = 0 的情况下是不正确的。
在 m = 0的情况下,代码应该立即让起始位置的人出列,而不需要进行任何计算。此外,当 m = 0 时,(i + m - 1) % len(a) 这部分代码可能会导致错误,因为 m - 1 会导致索引变为负数,这在列表索引中是不允许的。
改进
n, k, m = map(int, input().split())
# 初始化一个列表,代表围坐在圆桌周围的人
a = list(range(1, n + 1))
# 设置初始位置
i = k - 1
# 如果m为0,直接移除起始位置的人
if m == 0:
print(a.pop(i))
n -= 1 # 减少人数
if n == 0:
print("所有人已出列")
else:
# 重新设置初始位置为下一个人
k = 1
i = 0
else:
# 当还有人在列表中时,继续循环
while len(a) > 0:
# 计算出列的人的索引,使用 (i + m - 1) % len(a) 来循环索引
i = (i + m - 1) % len(a)
# 打印出列的人
print(a.pop(i))
首先检查 mm 是否为 0,如果是,则立即让起始位置的人出列,并更新列表和人数。如果 mm 不为 0,则按照原来的逻辑进行循环,直到所有人都出列。
10、数学
10.1线性代数--矩阵运算
10.1.1矩阵加减
两个矩阵上的相同位置分别做加法或者减法

10.1.2常数*矩阵
矩阵上的每一个元素乘上这个常数

10.1.3矩阵转置
原矩阵行变列、列变行

10.1.4矩阵乘法
辅助理解:一行*一列(第一行*第一列就对应答案的矩阵的点的位置:(1,1),第一行*第二 列就对应答案的矩阵的点的位置:(1,2)...)

【2*3】*【3*2】=【2*2】

10.1.5矩阵乘法代码实现
# 定义矩阵乘法函数
def mul(A, B):
N, M = len(A), len(A[0]) # 获取矩阵A的行数和列数
_M, K = len(B), len(B[0]) # 获取矩阵B的行数和列数
if M != _M: # 检查矩阵A的列数是否等于矩阵B的行数
return False # 如果不相等,返回False,表示无法进行乘法
# 初始化结果矩阵C,所有元素为0
C = [[0] * K for _ in range(N)]
for i in range(N): # 遍历矩阵A的行
for j in range(K): # 遍历矩阵B的列
# 计算矩阵C的第i行第j列的元素
for k in range(M): # 遍历矩阵A的列和矩阵B的行
C[i][j] += A[i][k] * B[k][j]
return C # 返回结果矩阵C
# 定义读取矩阵数据的函数
def read(A, n):
for i in range(n):
A.append(list(map(int, input().split())))
# 定义输出矩阵数据的函数
def output(A):
for x in A:
print(' '.join(map(str, x)))
# 主程序
A = [] # 初始化矩阵A
B = [] # 初始化矩阵B
N, M, K = map(int, input().split()) # 读取矩阵A的行数、矩阵A的列数和矩阵B的列数
read(A, N) # 读取矩阵A的数据
read(B, M) # 读取矩阵B的数据
C = mul(A, B) # 执行矩阵乘法
output(C) # 输出结果矩阵C
'''
2 3 2
1 2 3
4 5 6
1 2
3 4
5 6
'''
10.2基础数论
10.2.1整除
整除概念:
小数除法:Python 默认的除法是小数除法,即使两个整数相除,结果也是浮点数。例如,5 / 2 的结果是 2.5。
整除:要进行整除,需要使用两个除法符号 //。例如,5 // 2 的结果是 2,因为整除会舍弃小数部分,只保留整数部分。
整除操作:
向下取整:a // b 表示 a 除以 b 的结果向下取整。
向上取整:可以通过 (a + b - 1) // b 或 (a - 1) // b + 1 来实现 a 除以 b 的结果向上取整。
整除定义:
整除:如果 a 可以被 b 整除,记作 a | b,意味着 b % a == 0,即 b 是 a 的倍数,a 是 b 的因数。
整除性质:
传递性:如果 a / b 且 b / c,则 a / c。
线性组合:如果 c / a 且 c / b,则 c / (ma + nb),其中 m 和 n 是整数。这意味着 c 可以整除 a 和 b 的任何线性组合。
示例:
给定 c = 4,a = 2,b = 2,由于 c 可以整除 a 和 b,因此 c 也可以整除 2a + 2b 的结果,即 c / (2a + 2b)。

| 是整除的意思:a|b 就是a可以整除b,只要没有余数就是整除(即答案是有理数,包括 分数)。但是在原生python代码中|是或运算的意思,注使用时注意区分。

c=4,a=2,b=2有c|a、c|b -- c|(2a+2b),(ma+nb)整除是指除后结果没有余数,不管 结果是不是整数。
a = 5
b = 2
# 小数除法
print(a / b) # 输出: 2.5
# 整除
print(a // b) # 输出: 2
# 向上取整
print((a + b - 1) // b) # 输出: 3
print((a - 1) // b + 1) # 输出: 3
# 整除性质演示
c = 4
m = 2
n = 2
print(a,b,c)
print(False if c // (m * a + n * b) else True) # 输出: True,因为 c 可以整除 (2*2 + 2*2)

10.2.2同余
输出的答案对一个较大的(素)数求余(取模%mod)
mod = 10000007
...
print(ans%mod)

10.2.3gcd最大公因数
最大公因数(GCD)概念:
欧几里得算法:一种高效的计算两个整数最大公因数的方法。它基于这样一个事实:两个整数的最大公因数与它们的差的最大公因数相同。
算法描述:
假设 a > b,a=b×k+c,其中 k 是 a 除以 b 的商,c 是 a 除以 b 的余数,且 c < b
等价关系:gcd(a, b) = gcd(b, c),即 gcd(a, b) = gcd(b, a % b)
定义:设 p = gcd(a, b)和 q = gcd(b, c)。根据定义,p 能整除 a 和 b,q 能整除 b 和 c
推导:
由于c=a−b×k,可以推导出 p 也能整除 c,因此 p 是 b 和 c 的公因子,这意味着 q≥p
(如果 p 是 a 和 b 的最大公因数,那么 p 必须能整除 b 和 a 的任何线性组合,包括 c)
由于 a=b×k+c,可以推导出 q 也能整除 a,因此 q 是 a 和 b 的公因子,这意味着 p≥q
结论:最终,p=q,即gcd(a,b)=gcd(b,a%b)
性质:
如果 a 能被 b 整除,且 b 能被 c 整除,则 a 也能被 c 整除
如果 c 能被 a 和 b 整除,则 c 是 a 和 b 的公因子
# 会用就行
def gcd(a, b):
while b != 0:
a, b = b, a % b
return a
# 示例
a = 48
b = 18
print(gcd(a, b)) # 输出: 6
10.2.4lcm最小公倍数
def gcd(a, b):
# 如果b为0,那么a就是最大公因数
if b == 0:
return a
# 递归调用gcd函数,使用辗转相除法
return gcd(b, a % b)
def lcm(a, b):
# 计算最小公倍数
return a * b // gcd(a, b)
可以保证a%b<b(易得),辗转相除。gcd(n,0) = n ; gcd(n,n) = n。 a*b // gcd(a,b) = a*b 或a(默认a>b)。
10.3质数
质数概念:
质数:也称为素数,是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。
质数的因子只有1和它本身,2到该数减1之间都不是它的因子。
def is_prime(x):
if x <= 1:
return False # 小于等于1的数不是质数
for i in range(2, x): # 从2到x-1检查是否有因子
if x % i == 0: # 如果x能被i整除,说明x不是质数
return False
return True # 如果没有因子,x是质数
优化质数判定的时间复杂度

def is_prime_optimized(x):
if x <= 1:
return False
for i in range(2, int(x**0.5) + 1): # 只需检查到sqrt(x)
if x % i == 0:
return False
return True
理解:
以n = 16为例,16的因数有1,2,4,8,16 。sqrt(16) = 4,可以看出当我们把判断一个 数是否为质数缩小到 [2, sqrt(n)] 是可以实现时间复杂度的优化的,1/16、2/8存在对称 性。
提高: 快速筛选2-n中的素数
第一次筛选出2及2的倍数的非质数

第一次筛选出3及3的倍数的非质数;

每次找到一个质数,将其倍数全部删除。
# 当前遍历到的数是质数,那么在不超过n的范围内,该质数的倍数(2倍开始)就是合数了
def sieve_of_eratosthenes(n):
# 初始化一个布尔数组,表示每个数是否为质数
is_prime = [True] * (n+1)
p = 2
while p * p <= n:
# 如果is_prime[p]没有被改变,那么它就是质数
if is_prime[p]:
# 更新所有p的倍数为非质数
for i in range(p * p, n+1, p):
is_prime[i] = False
p += 1
# 收集所有质数
primes = [p for p in range(2, n+1) if is_prime[p]]
return primes
# 示例:找出100以内的所有质数
primes = sieve_of_eratosthenes(100)
print(' '.join(map(str,primes)))

10.4唯一分解定理

一个数除以x如果能整除,则继续除下一个x,直到除得结果为1 。
例题:
def f(n):
factor = []
for i in range(2,n+1): while n%i == 0:
factor.append(i)
if n == 1:
break
a,b = map(int,input().split())
for i in range(a,b+1):
factor = f(i)
print("{}=".format(i), end='')
print(*factor,sep = '*')
#3 从小到大枚举所有的质数x
#4 如果当前数n能整除x,继续除下一个质数x,并且每次除得的数放入列表factor中
#5 当n=1,表明除到最后一个可分解的质数(除尽),跳出循环,得到当前数n的唯一分解式
#11 输出 k=,end=''后面输入紧随其后
#12 *解包,将列表factor中的元素拆成每一个进行输出,sep=*中间用*隔开
10.5快速幂
引入:

二进制拆分(一般都这么用)原理:二进制通过位的组合可以表示任意十进制数。

快速幂二进制拆分模板:
def fast_power(a, b, mod):
# 初始化结果为1
result = 1
# 将a转换为模mod下的等价值
base = a % mod
# 处理b的每一位,如果为1,则乘以当前的base^(2^i)
while b > 0:
if b & 1: # 如果b的最低位为1
result = (result * base) % mod
base = (base * base) % mod # 更新base为base的平方
b >>= 1 # b右移一位
return result
# 示例:计算 a^b % c
a = 2
b = 100
c = 10000000007
print(fast_power(a, b, c)) # 输出: 464536
#4-6 比如a**100 = a**64 * a**32 * a**4,
这个例子里b=100,100%2=0此时走a = a*a%c,..25%2=1 此时走ans = ans*a%c(ans×上剩下的一个a);
#a**100 = a**64 * a**32 * a**4是逻辑上的表现,实际代码执行时是根据指数b在非0时每除以一次2就对a倍增一次(%2=0时),或再*一个a(%2=1时);
过程:
初始化:result 初始为1,base 为 a % mod。
遍历指数的二进制位:通过 while b > 0 循环,每次循环检查 b 的最低位(即 b & 1)。
更新 result:如果 b 的最低位为1,这意味着在当前的幂次方计算中需要包含 base 的幂次方。因此,将 result 更新为 (result * base) % mod。这一步是必要的,因为只有当指数的二进制位为1时,对应的幂次方才会对最终结果有贡献。
更新 base:无论 b 的最低位是否为1,base 都需要被平方,以准备计算下一个更高位的幂次方。这是通过 base = (base * base) % mod 实现的。这一步确保了 base 始终是当前考虑的幂次方的基数。
右移 b:b >>= 1 将 b 右移一位,以便在下一次循环中检查下一个更高的二进制位。
10.6矩阵快速幂
引入: 
例题:


10.7费马小定理和逆元
扩展欧几里得算法(铺垫):

简化版:

a%b = a - (a//b) * b,即y1 = x2 - (a//b) * y2


逆元:ax + ny = 1,只有在gcd(a,n) = 1时逆元才存在。
费马小定理:

939

被折叠的 条评论
为什么被折叠?



