【Python算法】基础语法、算法技巧模板、二分、DFS与BFS
建议全部跟着手敲一遍!!!
import sys
from math import gcd
from bisect import bisect_left
from collections import defaultdict, deque
import heapq
from itertools import permutations, combinations
from functools import lru_cache
from typing import List
# ===================== 输入输出优化 =====================
# 重新定义输入函数,使用sys.stdin.readline().strip()实现快速输入
# 这是因为sys.stdin.readline()比普通的input()函数读取速度更快
input = lambda: sys.stdin.readline().strip()
# 示例:读取n和n个数字
def read_data():
# 读取一行输入,并按空格分割成字符串列表
data = input().split()
# 将列表的第一个元素转换为整数,作为数字的数量n
n = int(data[0])
# 将列表中从第二个元素开始的n个元素转换为整数,并存储在列表nums中
nums = list(map(int, data[1:n+1]))
return n, nums
# ===================== 格式化输出 =====================
# 定义圆周率π的值
pi = 3.1415926
# 使用f-string格式化输出,保留两位小数
print(f"{pi:.2f}") # 保留两位小数 → 3.14
# 使用f-string格式化输出,将整数5补零到3位
print(f"{5:03d}") # 补零到3位 → 005
# ===================== 数据结构技巧 =====================
# 列表初始化
# 创建一个长度为100的一维数组,所有元素初始化为0
arr = [0] * 100
# 正确创建二维数组(3行5列)
# 使用列表推导式创建一个3行5列的二维数组,所有元素初始化为0
matrix = [[0] * 5 for _ in range(3)]
# 字典默认值处理
# 创建一个默认值为0的字典
# 当访问字典中不存在的键时,会自动创建该键并将其值初始化为0
d = defaultdict(int)
# 对字典中键为"key"的值加1,无需判断key是否存在
d["key"] += 1
# 字典排序(按值降序)
# 对字典d的键值对进行排序,按照值的降序排列
# 使用lambda函数指定排序的键为值的相反数,实现降序排序
sorted_dict = dict(sorted(d.items(), key=lambda x: -x[1]))
# 双端队列(BFS常用)
# 创建一个双端队列对象
q = deque()
# 在队尾插入元素1
q.append(1)
# 从队首移除并返回元素
q.popleft()
# ===================== 排序与查找算法 =====================
# 快速排序(分治思想)
def quick_sort(arr):
# 如果数组长度小于等于1,直接返回该数组
if len(arr) <= 1:
return arr
# 选择中间元素作为基准
pivot = arr[len(arr)//2]
# 小于基准的元素组成的列表
left = [x for x in arr if x < pivot]
# 等于基准的元素组成的列表
middle = [x for x in arr if x == pivot]
# 大于基准的元素组成的列表
right = [x for x in arr if x > pivot]
# 递归地对左右子数组进行排序,并合并结果
return quick_sort(left) + middle + quick_sort(right)
# 二分查找左边界(寻找第一个≥target的位置)
def bisect_left_custom(arr, target):
# 左指针初始化为0
l, r = 0, len(arr)
# 当左指针小于右指针时,继续循环
while l < r:
# 计算中间位置
mid = (l + r) // 2
# 如果中间元素小于目标值,将左指针移动到mid+1
if arr[mid] < target:
l = mid + 1
# 否则,将右指针移动到mid
else:
r = mid
# 返回插入位置
return l
# ===================== 数学与数论 =====================
# 埃拉托斯特尼筛法(素数筛选)
def sieve(n):
# 创建一个长度为n+1的布尔数组,初始值都为True,表示所有数都是素数
is_prime = [True] * (n+1)
# 0和1不是素数,将其标记为False
is_prime[0] = is_prime[1] = False
# 从2开始遍历到根号n
for i in range(2, int(n**0.5)+1):
# 如果i是素数
if is_prime[i]:
# 标记i的倍数为非素数
for j in range(i*i, n+1, i):
is_prime[j] = False
# 返回所有标记为True的数,即素数
return [i for i, prime in enumerate(is_prime) if prime]
# 最大公约数与最小公倍数
# 计算12和18的最大公约数
gcd_val = gcd(12, 18) # 6
# 根据公式计算最小公倍数
lcm_val = 12 * 18 // gcd_val # 36
# 组合数计算(模运算优化)
# 定义模数
MOD = 10**9 + 7
def comb(n, k):
# 初始化结果为1
res = 1
# 从1到k进行循环
for i in range(1, k+1):
# 根据递推公式计算组合数
res = res * (n - k + i) // i # 递推公式避免浮点
# 返回结果对MOD取模的值
return res % MOD
# ===================== 图论算法 =====================
# 邻接表建图(无向图)
# 创建一个默认值为列表的字典,用于存储图的邻接表
graph = defaultdict(list)
# 定义图的边
edges = [[1,2], [2,3], [1,3]]
# 遍历每条边
for u, v in edges:
# 将v添加到u的邻接表中
graph[u].append(v)
# 将u添加到v的邻接表中,因为是无向图
graph[v].append(u)
# Dijkstra最短路径(堆优化)
def dijkstra(graph, start):
# 创建一个最小堆,初始元素为(0, start),表示从起点到起点的距离为0
heap = [(0, start)]
# 初始化距离字典,起点到自身的距离为0
dist = {start: 0}
# 当堆不为空时,继续循环
while heap:
# 从堆中取出距离最小的节点及其距离
d, u = heapq.heappop(heap)
# 如果取出的距离大于当前记录的最短距离,跳过该节点
if d > dist.get(u, float('inf')):
continue
# 遍历该节点的所有邻居节点
for v, w in graph[u]:
# 如果通过当前节点到达邻居节点的距离更短
if dist.get(v, float('inf')) > d + w:
# 更新邻居节点的最短距离
dist[v] = d + w
# 将邻居节点及其最短距离加入堆中
heapq.heappush(heap, (dist[v], v))
# 返回距离字典
return dist
# ===================== 动态规划 =====================
# 01背包问题(空间优化版)
def knapsack(weights, values, capacity):
# 创建一个长度为capacity+1的数组,用于存储最大价值
dp = [0] * (capacity + 1)
# 遍历每个物品
for w, v in zip(weights, values):
# 逆序更新dp数组,避免重复选取物品
for j in range(capacity, w-1, -1):
# 选择放入或不放入当前物品的最大价值
dp[j] = max(dp[j], dp[j - w] + v)
# 返回背包容量为capacity时的最大价值
return dp[capacity]
# 最长上升子序列(贪心+二分)
def lengthOfLIS(nums):
# 创建一个空列表,用于存储上升子序列的末尾元素
tails = []
# 遍历数组中的每个元素
for num in nums:
# 找到元素num在tails列表中应该插入的位置
idx = bisect_left(tails, num)
# 如果插入位置等于tails列表的长度,说明num比tails中的所有元素都大
if idx == len(tails):
# 将num添加到tails列表的末尾
tails.append(num)
else:
# 否则,将tails列表中该位置的元素替换为num
tails[idx] = num
# 返回上升子序列的长度
return len(tails)
# ===================== 回溯算法 =====================
# 全排列生成
def permute(nums):
def backtrack(path):
# 如果当前路径的长度等于数组的长度,说明已经生成了一个全排列
if len(path) == len(nums):
# 将当前路径的副本添加到结果列表中
res.append(path.copy())
return
# 遍历数组中的每个元素
for num in nums:
# 如果元素不在当前路径中
if num not in path:
# 将元素添加到当前路径中
path.append(num)
# 递归调用backtrack函数,继续生成排列
backtrack(path)
# 回溯,移除最后一个元素
path.pop()
# 初始化结果列表
res = []
# 调用backtrack函数,从空路径开始生成排列
backtrack([])
# 返回结果列表
return res
# ===================== 前缀和与差分 =====================
# 前缀和数组
# 初始化前缀和数组,第一个元素为0
prefix = [0]
# 遍历数组[1, 2, 3, 4]
for num in [1, 2, 3, 4]:
# 计算前缀和,将前一个前缀和加上当前元素的值
prefix.append(prefix[-1] + num) # prefix = [0,1,3,6,10]
# 差分数组(区间更新)
# 创建一个长度为5的差分数组,所有元素初始化为0
diff = [0] * 5
# 差分数组的第一个元素设为1
diff[0] = 1
# 示例差分,计算差分数组的其他元素
for i in range(1, 5):
diff[i] = (i+1) - i # 示例差分
# ===================== 其他实用技巧 =====================
# 排列组合生成
# 生成[1,2,3]中取2个元素的所有排列,并打印结果
print(list(permutations([1,2,3], 2))) # 排列
# 生成[1,2,3]中取2个元素的所有组合,并打印结果
print(list(combinations([1,2,3], 2))) # 组合
# 堆操作(默认小顶堆)
# 创建一个空堆
heap = []
# 向堆中插入元素2
heapq.heappush(heap, 2)
# 向堆中插入元素1
heapq.heappush(heap, 1)
# 从堆中取出最小的元素并打印
print(heapq.heappop(heap)) # 1
# 大顶堆技巧(存入负数)
# 向堆中插入元素-3
heapq.heappush(heap, -3)
# 从堆中取出最大的元素(因为存入的是负数,所以取反)
max_val = -heapq.heappop(heap)
# 位运算技巧
# 定义一个二进制数
n = 0b1010
# 清除最低位的1,并打印结果
print(n & (n-1)) # 清除最低位的1 → 0b1000
# 计算2的3次方,并打印结果
print(1 << 3) # 计算2^3 → 8
# 判断5的奇偶性,奇数返回0,偶数返回1
print(~5 & 1) # 判断奇偶(5是奇→0,偶→1)
# 记忆化搜索(斐波那契数列)
# 使用lru_cache装饰器实现记忆化搜索
@lru_cache(maxsize=None)
def fib(n):
# 如果n小于2,返回n
if n < 2:
return n
# 递归计算斐波那契数列的值
return fib(n-1) + fib(n-2)
# ===================== 高频考题模板 =====================
# 和为K的子数组个数(前缀和+哈希)
def subarraySum(nums: List[int], k: int) -> int:
# 创建一个默认值为0的字典,用于存储前缀和及其出现的次数
prefix_sum = defaultdict(int)
# 前缀和为0的情况出现1次
prefix_sum[0] = 1
# 初始化当前前缀和为0
current_sum = count = 0
# 遍历数组中的每个元素
for num in nums:
# 更新当前前缀和
current_sum += num
# 计算需要的前缀和
count += prefix_sum.get(current_sum - k, 0)
# 更新当前前缀和的出现次数
prefix_sum[current_sum] += 1
# 返回和为k的子数组的个数
return count
# 滑动窗口模板
def sliding_window(nums, k):
# 左指针初始化为0
left = 0
# 右指针从0开始遍历数组
for right in range(len(nums)):
# 当窗口大小超过k时,收缩窗口
while right - left + 1 > k:
left += 1
# 处理当前窗口
print(nums[left:right+1])
BFS
下面的解答都是非力扣式,而是自行写输入输出
在一个二维网格中找到由 1 组成的最大连通区域的面积。
求岛屿的最大面积问题:
from collections import deque
grid = []
m,n = map(int,input().split())
for _ in range(m):
row = list(map(int, input().split()))
grid.append(row) # 输入m行n列
m = len(grid)
n = len(grid[0])
direc = [(0,1),(0,-1),(-1,0),(1,0)]
def bfs(i,j):
ans=1
q = deque([(i,j)]) # 入队
grid[i][j]=0 # 标记为已经访问过了
while q:
x,y =q.popleft()
for dx,dy in direc:
nx = x+dx
ny = y+dy
if 0<=nx<m and 0<=ny<n and grid[nx][ny]:
q.append((nx,ny))
grid[nx][ny]=0 # 添加进队列后都要标记为已经访问过了
ans+=1
return ans
res=0
for i,row in enumerate(grid):
for j,x in enumerate(row):
if x==1:
res = max(res,bfs(i,j))
print(res)
力扣1926题
from collections import deque
grid = []
# 输入行数和列数
m, n = map(int, input().split())
# 逐行输入网格数据
for _ in range(m):
row = list(map(str, input().split()))
grid.append(row)
# 输入入口坐标
i, j = map(int, input().split())
direc = [(0, 1), (0, -1), (-1, 0), (1, 0)]
ans = -1 # 初始化结果为 -1,表示未找到出口
q = deque([(i, j, 0)])
grid[i][j] = '+' # 入队后标记为已经访问过了
while q:
x, y, steps = q.popleft()
# 检查是否到达边界且不是入口
if (x == 0 or x == m - 1 or y == 0 or y == n - 1) and (x, y) != (i, j):
ans = steps
break
# 遍历四个方向
for dx, dy in direc:
nx, ny = x + dx, y + dy
# 检查新坐标是否合法且未访问过
if 0 <= nx < m and 0 <= ny < n and grid[nx][ny] == '.':
q.append((nx, ny, steps + 1))
grid[nx][ny] = '+'
print(ans)
力扣994题
from collections import deque
import sys
# 输入行数和列数
m, n = map(int, input().split())
grid = []
fresh = 0
q = deque()
# 读取网格并初始化
for i in range(m):
row = list(map(int, input().split()))
grid.append(row)
for j in range(n):
if grid[i][j] == 1:
fresh += 1
elif grid[i][j] == 2:
q.append((i, j, 0))
# 特殊情况处理:如果没有新鲜橘子,直接返回0
if fresh == 0:
print(0)
sys.exit()
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
minutes = 0
# BFS处理腐烂过程
while q:
x, y, time = q.popleft()
minutes = time
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < m and 0 <= ny < n and grid[nx][ny] == 1:
grid[nx][ny] = 2
fresh -= 1
q.append((nx, ny, time + 1))
# 输出结果
print(minutes if fresh == 0 else -1)
力扣934题:最短的桥
DFS+BFS:
解决最短桥问题的完整方案
算法步骤:
1. 使用DFS找到并标记第一座岛屿(标记为2)
2. 使用多源BFS从第一座岛屿扩展,寻找第二座岛屿
3. 当BFS遇到第二座岛屿时,返回当前扩展的步数
from collections import deque
# 读取网格大小
n = int(input())
# 读取网格数据,转换为二维整数列表
grid = [list(map(int, input().split())) for _ in range(n)]
# 定义四个移动方向:右、左、下、上
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
# 初始化BFS队列
q = deque()
# ========== 第一阶段:标记第一座岛屿 ==========
found = False # 标记是否已找到第一座岛屿
# 遍历整个网格寻找第一座岛屿的起点
for i in range(n):
if found:
break
for j in range(n):
# 当找到第一个陆地单元格(值为1)
if grid[i][j] == 1:
# 使用栈实现迭代式DFS(避免递归深度限制)
stack = [(i, j)]
# 将起始点标记为2(表示属于第一座岛屿)
grid[i][j] = 2
# 开始DFS遍历
while stack:
x, y = stack.pop()
# 将当前岛屿点加入BFS队列,初始步数为0
q.append((x, y, 0))
# 检查四个方向
for dx, dy in directions:
nx, ny = x + dx, y + dy
# 确保新坐标在网格内且是未访问的陆地(值为1)
if 0 <= nx < n and 0 <= ny < n and grid[nx][ny] == 1:
# 标记为属于第一座岛屿
grid[nx][ny] = 2
# 加入栈中继续DFS
stack.append((nx, ny))
# 标记已找到第一座岛屿,跳出循环
found = True
break
# ========== 第二阶段:多源BFS寻找第二座岛屿 ==========
while q:
x, y, steps = q.popleft() # 取出队列中的点和当前步数
# 检查四个方向
for dx, dy in directions:
nx, ny = x + dx, y + dy
# 确保新坐标在网格内
if 0 <= nx < n and 0 <= ny < n:
# 如果遇到值为1的单元格,说明找到第二座岛屿
if grid[nx][ny] == 1:
# 输出当前步数(即需要翻转的最少水域数)
print(steps)
# 直接退出程序
exit()
# 如果是水域(值为0)
if grid[nx][ny] == 0:
# 标记为已访问(-1表示已探索的水域)
grid[nx][ny] = -1
# 加入队列,步数+1
q.append((nx, ny, steps + 1))
print(-1)
DFS
力扣086题:分割回文串
s = input().strip() # 读取输入并去除首尾空格
res = []
path = []
def dfs(start):
"""使用深度优先搜索查找所有回文分割方案
Args:
start: 当前搜索的起始位置
"""
if start == len(s):
res.append(path.copy())
return
for end in range(start + 1, len(s) + 1):
substring = s[start:end]
if substring == substring[::-1]: # 判断是否为回文
path.append(substring)
dfs(end)
path.pop() # 回溯
dfs(0) # 从字符串起始位置开始搜索
print(res)
二分
力扣793题:阶乘后函数K个零
由于阶乘末尾零的数量随着数字的增加而单调递增,我们可以利用二分查找来高效地定位这些数字。
尾随的零 ≤k 的那个最大的数
使用下面的二分模板:
while left <= right:
mid = (left + right) // 2
if count_trailing_zeros(mid) <= k:
left = mid + 1 # 保持"left左侧都满足"
else:
right = mid - 1 # 保持"right右侧都不满足"
# 结束时 right == 最后一个满足条件的值,即最大满足条件的数
upper 定位到 k 个零的区间的右端点。
lower 定位到 k-1 个零的区间的右端点。
两者的差值 upper - lower 直接给出了恰好有 k 个零的数字数量。
完整代码:
def count_trailing_zeros(x):
"""计算x!末尾有多少个零"""
zeros = 0
while x > 0:
x = x // 5
zeros += x
return zeros
def find_max_num_with_k_zeros(k):
"""找到最大的n使得n!的尾随零<=k"""
left, right = 0, 5 * (k + 1) # 足够大的上界
while left <= right:
mid = (left + right) // 2 # 注意这里不需要+1
zeros = count_trailing_zeros(mid)
if zeros <= k:
left = mid + 1 # 尝试更大的数
else:
right = mid - 1 # 尝试更小的数
return right # 注意返回的是right而不是left
# 输入处理
k = int(input())
# 计算恰好有k个零的数字数量
if k == 0:
print(0)
else:
upper = find_max_num_with_k_zeros(k)
lower = find_max_num_with_k_zeros(k - 1)
print(upper - lower)