欢迎订阅专栏:算法
文章目录
1. 语言基础
1.1 编程基础
编程基础是了解任何一种编程语言的基础。在Python中,可以使用各种数据类型、控制流语句和函数等基本概念。下面是一个简单的示例代码:
# 变量和基本数据类型
name = "Alice"
age = 25
is_student = True
# 条件语句
if age >= 18:
print("成年人")
else:
print("未成年人")
# 循环语句
for i in range(5):
print(i)
# 函数
def greet(name):
print("Hello, " + name + "!")
greet("Bob")
以上示例代码展示了Python编程基础中的变量和基本数据类型、条件语句、循环语句和函数等概念。通过运行这段代码,您将看到输出结果并了解它们运行的方式。
如果您对特定的编程概念或语法有更多疑问,或者需要更深入地了解编程基础的其他部分,请随时提问。我将很乐意为您提供更多帮助。
1.2 选择结构
选择结构是编程中常用的分支语句,用于根据条件决定程序的执行路径。在Python中,可以使用if语句和条件表达式实现选择结构。下面是一个示例代码:
# 示例代码1.2.1: 条件表达式和逻辑表达式
x = 5
y = 10
if x > y:
print("x大于y")
else:
print("x小于等于y")
# 示例代码1.2.2: if语句和分类讨论
num = int(input("请输入一个数字: "))
if num > 0:
print("你输入的数字大于0")
elif num < 0:
print("你输入的数字小于0")
else:
print("你输入的数字等于0")
1.3 循环结构
循环结构用于重复执行一段代码块,Python中常用的循环语句有for循环和while循环。下面是一个示例代码:
# 示例代码1.3.1: for和while语句
# for循环打印数字1到5
for i in range(1, 6):
print(i)
# while循环打印数字1到5
i = 1
while i <= 5:
print(i)
i += 1
# 示例代码1.3.2: 循环嵌套
# 打印九九乘法表
for i in range(1, 10):
for j in range(1, i+1):
print(f"{j} * {i} = {i*j}", end="\t")
print()
# 示例代码1.3.3: break和continue语句
# 打印1到10,遇到数字5跳过,遇到数字8退出循环
for i in range(1, 11):
if i == 5:
continue
print(i)
if i == 8:
break
1.4 基础数据结构
基础数据结构是编程中常用的数据存储和操作方式。在Python中,常用的基础数据结构包括字符串、列表、元组、字典和集合等。下面是一个示例代码:
# 示例代码1.4.1: 字符串
# 定义字符串变量并打印
s = "Hello, World!"
print(s)
# 示例代码1.4.2: 列表、元组
# 定义列表和元组并打印
list1 = [1, 2, 3, 4, 5]
print(list1)
tuple1 = (1, 2, 3, 4, 5)
print(tuple1)
# 示例代码1.4.3: 字典、集合
# 定义字典和集合并打印
dict1 = {"name": "Alice", "age": 20, "city": "New York"}
print(dict1)
set1 = {1, 2, 3, 4, 5}
print(set1)
# 示例代码1.4.4: 日期和时间
import datetime
# 获取当前日期和时间
now = datetime.datetime.now()
print(now)
1.5 函数
函数是将一段代码组织起来以完成特定任务的一种方式。在Python中,可以使用def关键字定义函数,并通过函数名加括号调用函数。下面是一个示例代码:
# 示例代码1.5: 函数
# 定义一个函数,计算两个数的和
def add(x, y):
return x + y
# 调用函数并打印结果
result = add(3, 5)
print(result)
1.6 竞赛常用标准库
竞赛常用标准库是指在竞赛中常用的、方便解决一些常见问题的Python标准库。常用的标准库包括math、collections和heapq等。下面是一个示例代码:
# 示例代码1.6.1: math、collections、heapq
import math
import collections
import heapq
import functools
import itertools
from bitset import bitset
# math
print(math.factorial(5)) # 计算阶乘
# collections
counter = collections.Counter([1, 2, 3, 1, 2, 1, 5]) # 统计元素出现次数
print(counter)
deque = collections.deque([1, 2, 3]) # 双端队列
deque.append(4)
print(deque)
# heapq
heap = [3, 1, 4, 1, 5, 9]
heapq.heapify(heap) # 堆化
print(heap)
print(heapq.heappop(heap)) # 弹出最小值
# functools
def add(a, b):
return a + b
add_five = functools.partial(add, 5) # 创建一个将5作为第一个参数的新函数
print(add_five(3))
# itertools
perms = itertools.permutations([1, 2, 3]) # 排列
print(list(perms))
# bitset
bitset_example = bitset(5)
bitset_example.set(0)
bitset_example.set(2)
print(bitset_example)
1.7 面向对象相关知识
面向对象编程是一种将数据和方法封装在一起组成类的编程方法。在Python中,可以使用类来创建对象,并使用对象的方法来操作和访问数据。下面是一个示例代码:
# 示例代码1.7.1: 类的定义和使用
# 定义一个人类
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def say_hello(self):
print(f"Hello, my name is {self.name} and I'm {self.age} years old.")
# 创建一个人对象并调用对象方法
person = Person("Alice", 20)
person.say_hello()
1.8 实践应用与例题实战
实践应用和例题实战部分包括一些常见的算法应用和题目的实现。下面是一个示例代码:
# 示例代码1.8.1: 自定义排序、二分查找
# 自定义排序规则,按照元组的第二个元素从大到小排序
data = [(1, 4), (5, 2), (3, 6), (2, 1)]
data.sort(key=lambda x: x[1], reverse=True)
print(data)
# 使用二分查找在有序列表中找到目标值
nums = [1, 2, 3, 4, 5]
target = 3
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
print("目标值找到了")
break
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
else:
print("目标值未找到")
# 示例代码1.8.2: 例题实战
# 给定一个列表,移除其中重复的元素并输出
nums = [1, 2, 3, 2, 4, 2, 3, 5]
unique_nums = list(set(nums))
print(unique_nums)
这些示例代码只是针对大纲中的某些部分进行了简要介绍,你可以根据需要使用相应的import语句和详细的代码实现。
2. 基础算法
2.1 时间复杂度分析
时间复杂度是衡量算法执行时间随输入规模增长的增长率。在算法分析中,常用的时间复杂度有O(1)、O(log n)、O(n)、O(n log n)和 O(n^2)等。下面是一个示例代码:
# 示例代码2.1: 时间复杂度分析
n = 1000
sum = 0
for i in range(n):
sum += i # O(n)的时间复杂度
print(sum)
2.2 枚举
枚举是一种穷举所有可能情况的方法。在算法中,可以使用枚举来解决某些问题。下面是一个示例代码:
# 示例代码2.2: 枚举
# 找到列表中两个数的和等于目标值的组合
nums = [2, 7, 11, 15]
target = 9
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
print(f"找到组合:{nums[i]} + {nums[j]} = {target}")
break
2.3 模拟
模拟是一种根据实际情况来仿真运行的方法。在算法中,可以使用模拟来模拟某个过程或场景的运行。下面是一个示例代码:
# 示例代码2.3: 模拟
# 模拟猜数字游戏
import random
answer = random.randint(1, 100) # 生成1到100之间的随机数
print("猜数字游戏开始!")
while True:
guess = int(input("请猜一个数字(1-100): "))
if guess == answer:
print("恭喜你猜对了!")
break
elif guess < answer:
print("猜的数字太小了,再试试!")
else:
print("猜的数字太大了,再试试!")
2.4 递归
递归是一种通过调用自身的方式解决问题的方法。在算法中,可以使用递归来解决一些具有递归结构的问题。下面是一个示例代码:
# 示例代码2.4: 递归
# 计算阶乘
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n - 1)
num = 5
result = factorial(num)
print(f"{num}的阶乘是:{result}")
2.5 排序
排序是将一组元素按照特定顺序重新排列的过程。在算法中,常见的排序算法有冒泡排序、选择排序、插入排序、快速排序和归并排序等。下面是一个示例代码:
# 示例代码2.5.1: 冒泡排序、选择排序
# 冒泡排序
def bubble_sort(nums):
for i in range(len(nums)-1):
for j in range(len(nums)-1-i):
if nums[j] > nums[j+1]:
nums[j], nums[j+1] = nums[j+1], nums[j]
# 选择排序
def selection_sort(nums):
for i in range(len(nums)-1):
min_index = i
for j in range(i+1, len(nums)):
if nums[j] < nums[min_index]:
min_index = j
nums[i], nums[min_index] = nums[min_index], nums[i]
# 测试排序算法
nums = [4, 2, 7, 1, 3]
bubble_sort(nums)
print("冒泡排序结果:", nums)
nums = [4, 2, 7, 1, 3]
selection_sort(nums)
print("选择排序结果:", nums)
# 示例代码2.5.2: 插入排序、快速排序
# 插入排序
def insertion_sort(nums):
for i in range(1, len(nums)):
key = nums[i]
j = i - 1
while j >= 0 and nums[j] > key:
nums[j + 1] = nums[j]
j -= 1
nums[j + 1] = key
# 快速排序
def quick_sort(nums):
if len(nums) <= 1:
return nums
else:
pivot = nums[0]
less = [x for x in nums[1:] if x <= pivot]
greater = [x for x in nums[1:] if x > pivot]
return quick_sort(less) + [pivot] + quick_sort(greater)
# 测试排序算法
nums = [4, 2, 7, 1, 3]
insertion_sort(nums)
print("插入排序结果:", nums)
nums = [4, 2, 7, 1, 3]
nums = quick_sort(nums)
print("快速排序结果:", nums)
# 示例代码2.5.3: 归并排序
# 归并排序
def merge_sort(nums):
if len(nums) <= 1:
return nums
mid = len(nums) // 2
left = merge_sort(nums[:mid])
right = merge_sort(nums[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
# 测试排序算法
nums = [4, 2, 7, 1, 3]
nums = merge_sort(nums)
print("归并排序结果:", nums)
2.6 进制转换
进制转换是将一个数从一种进制表示方式转换为另一种进制表示方式的过程。在算法中,可以使用进制转换来处理不同进制的数值。下面是一个示例代码:
# 示例代码2.6: 进制转换
# 将十进制转换为二进制
def decimal_to_binary(n):
if n == 0:
return "0"
result = ""
while n:
result = str(n % 2) + result
n //= 2
return result
num = 10
binary_num = decimal_to_binary(num)
print(f"{num}的二进制表示:{binary_num}")
# 将二进制转换为十进制
def binary_to_decimal(binary):
decimal = 0
binary = str(binary)
for i in range(len(binary)):
decimal += int(binary[i]) * (2 ** (len(binary) - i - 1))
return decimal
binary_num = "1010"
decimal_num = binary_to_decimal(binary_num)
print(f"{binary_num}的十进制表示:{decimal_num}")
2.7 前缀和
前缀和是一种辅助数据结构,用于快速计算数组中某个区间的和。在算法中,可以使用前缀和来优化某些计算。下面是一个示例代码:
# 示例代码2.7: 前缀和
# 计算数组的前缀和
nums = [1, 2, 3, 4, 5]
prefix_sum = [0] * (len(nums) + 1)
for i in range(1, len(nums) + 1):
prefix_sum[i] = prefix_sum[i-1] + nums[i-1]
# 计算区间和
left = 1
right = 3
sum_range = prefix_sum[right+1] - prefix_sum[left]
print(f"数组在第{left}到第{right}个位置的区间和为:{sum_range}")
2.8 差分
差分是一种辅助数据结构,用于快速计算数组中某个区间的差分。在算法中,可以使用差分来优化某些计算。下面是一个示例代码:
# 示例代码2.8: 差分
# 计算数组的差分
nums = [1, 2, 3, 4, 5]
differences = [0] * len(nums)
for i in range(1, len(nums)):
differences[i] = nums[i] - nums[i-1]
# 对区间进行增加或减少操作
left = 1
right = 3
value = 2
differences[left] += value
differences[right+1] -= value
# 计算原数组
new_nums = [0] * len(nums)
new_nums[0] = nums[0]
for i in range(1, len(nums)):
new_nums[i] = new_nums[i-1] + differences[i]
print(f"数组经过{left}到{right}位置增加{value}后的新数组:{new_nums}")
2.9 离散化
离散化是一种将原始数据转换为连续整数的过程。在算法中,可以使用离散化来处理某些问题。下面是一个示例代码:
# 示例代码2.9: 离散化
# 离散化原数组
nums = [3, 2, 5, 1, 4]
sorted_nums = sorted(set(nums)) # 去重并排序
mapping = {}
for i in range(len(sorted_nums)):
mapping[sorted_nums[i]] = i
discretized_nums = [mapping[num] for num in nums]
print(f"离散化后的数组:{discretized_nums}")
2.10 贪心
贪心算法是一种在每个步骤选择当前局部最优解的方法,以期望获得全局最优解。在算法中,可以使用贪心算法来解决一些问题。下面是一个示例代码:
# 示例代码2.10: 贪心
# 选择最少的硬币组合凑出总金额
def greedy_coin_change(coins, amount):
coins.sort(reverse=True) # 按面值从大到小排序
count = 0
for coin in coins:
count += amount // coin
amount %= coin
if amount == 0:
return count
else:
return -1
coins = [1, 2, 5, 10, 20, 50, 100]
amount = 188
min_count = greedy_coin_change(coins, amount)
print(f"凑出{amount}所需的最少硬币数:{min_count}")
2.11 双指针
双指针是一种使用两个指针在数组或字符串中同时移动的方法。在算法中,可以使用双指针来解决某些问题。下面是一个示例代码:
# 示例代码2.11: 双指针
# 判断一个字符串是否为回文字符串
def is_palindrome(s):
i, j = 0, len(s) - 1
while i < j:
if s[i] != s[j]:
return False
i += 1
j -= 1
return True
string = "level"
is_palindrome = is_palindrome(string)
if is_palindrome:
print(f"{string}是回文字符串")
else:
print(f"{string}不是回文字符串")
2.12 二分
二分法是一种在有序数组或有序列表中查找目标元素的方法。在算法中,可以使用二分法来解决查找问题。下面是一个示例代码:
# 示例代码2.12: 二分
# 在有序数组中查找目标元素的索引
def binary_search(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
nums = [1, 2, 3, 4, 5]
target = 3
index = binary_search(nums, target)
if index != -1:
print(f"目标元素{target}在数组中的索引是:{index}")
else:
print(f"目标元素{target}不在数组中")
2.13 倍增
倍增是一种通过重复两倍扩展来快速计算某个数的幂次的方法。在算法中,可以使用倍增来优化计算。下面是一个示例代码:
# 示例代码2.13: 倍增
# 求解幂次方
def power(base, exponent):
result = 1
while exponent:
if exponent % 2:
result *= base
base *= base
exponent //= 2
return result
base = 2
exponent = 10
result = power(base, exponent)
print(f"{base}的{exponent}次方是:{result}")
2.14 构造
构造是一种通过按照一定规则组合元素来创建一个新的数据结构的方法。在算法中,可以使用构造方法来解决一些问题。下面是一个示例代码:
# 示例代码2.14: 构造
# 构造一个特定规律的数组
n = 5
nums = [2 * i + 1 for i in range(n)]
print("构造得到的数组:", nums)
2.15 位运算
位运算是一种直接对二进制数进行操作的方法。在算法中,可以使用位运算来优化某些计算。下面是一个示例代码:
# 示例代码2.15: 位运算
# 判断一个数是否为2的幂次方
def is_power_of_two(n):
if n <= 0:
return False
return n & (n - 1) == 0
num = 16
result = is_power_of_two(num)
if result:
print(f"{num}是2的幂次方")
else:
print(f"{num}不是2的幂次方")
这些示例代码只是针对大纲中的某些部分进行了简要介绍,你可以根据需要使用相应的import语句和详细的代码实现。
3. 搜索
3.1 DFS
深度优先搜索(DFS)是一种用于遍历或查找图或树的算法。它通过从起始节点开始,沿着路径一直搜索直到到达叶子节点,然后回溯到上一个未搜索的节点继续搜索,直到遍历完整个图或树。
3.1.1 DFS基础、回溯
DFS基础算法是通过递归或栈实现。算法包括以下步骤:
- 访问当前节点
- 遍历当前节点的所有相邻节点,若相邻节点未被访问过,则递归或将其入栈
- 重复上述步骤直到遍历完所有节点
回溯是指在搜索过程中遇到无法继续进行的情况时,需要返回到上一层,并尝试其他的路径继续搜索。回溯算法常常与DFS算法结合使用。
下面是一个使用DFS和回溯算法解决八皇后问题的示例代码:
def solveNQueens(n):
def backtrack(row, queens):
if row == n:
result.append(queens)
return
for col in range(n):
if is_valid(row, col, queens):
backtrack(row + 1, queens + [col])
def is_valid(row, col, queens):
for r, c in enumerate(queens):
if c == col or r + c == row + col or r - c == row - col:
return False
return True
result = []
backtrack(0, [])
return result
n = 4
print(solveNQueens(n))
输出结果为:
[[1, 3, 0, 2], [2, 0, 3, 1]]
在八皇后问题中,需要在一个 n x n 的棋盘上放置 n 个皇后,使得它们互相之间不能攻击到对方(即任意两个皇后不能在同一行、同一列或同一对角线上)。上述代码使用了DFS和回溯算法来找到所有合法的解决方案。
3.1.2 剪枝、记忆化
在DFS算法中,剪枝和记忆化是常用的优化技巧,可以减少不必要的搜索或避免重复计算,提高算法效率。
剪枝是指在搜索过程中,根据问题的特性或约束条件,提前排除掉一些不可能导致最终解的分支,从而减少搜索的空间。剪枝可以通过在DFS的过程中加入条件判断来实现。
记忆化是指在搜索过程中,将已经计算过的结果保存下来,避免重复计算。通常使用字典或数组来保存已经计算过的中间结果,以减少重复计算的次数。
下面是一个使用剪枝和记忆化的示例代码,解决一个简化版的背包问题:
def knapsack(weight, value, capacity):
memo = {}
def backtrack(cap):
if cap in memo:
return memo[cap]
if cap == 0:
return 0
if cap < 0:
return float('-inf')
max_value = 0
for i in range(len(weight)):
max_value = max(max_value, value[i] + backtrack(cap - weight[i]))
memo[cap] = max_value
return max_value
return backtrack(capacity)
weight = [2, 3, 4, 5]
value = [3, 4, 5, 6]
capacity = 8
print(knapsack(weight, value, capacity))
输出结果为:
11
在背包问题中,有一系列物品,每个物品有对应的重量和价值。要求在给定的背包容量下,选择一些物品放入背包,使得背包中物品的总价值最大。上述代码使用了剪枝和记忆化的技巧来优化搜索过程,避免重复计算和无用的搜索分支。
4. 动态规划
动态规划(Dynamic Programming,简称DP)是一种通过将问题分解成子问题并保存子问题的解来解决复杂问题的方法。它通常用于求解最优解或最优化问题。动态规划的核心思想是将问题分解成更小的子问题,并利用已知的子问题解来构建整体问题的解。
4.1 动态规划基础
动态规划一般包含以下几个步骤:
-
定义问题的状态:确定要解决的问题所涉及的变量以及变量的取值范围,将问题抽象成状态的集合。
-
定义状态转移方程:确定状态之间的转移关系,即如何从一个状态转移到另一个状态。
-
初始化边界状态:确定初始状态的值。
-
使用递推或迭代的方式计算其他状态的值:根据状态转移方程,从初始状态开始逐步计算其他状态的值。
-
求解最终结果:根据已计算的各个状态的值,得到最终要解决的问题的解。
下面是一个使用动态规划求解斐波那契数列的示例代码:
def fibonacci(n):
if n <= 1:
return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
n = 10
print(fibonacci(n))
输出结果为:
55
在斐波那契数列中,每个数都是前两个数的和,即 F(n) = F(n-1) + F(n-2),并且初始状态为 F(0) = 0 和 F(1) = 1。上述代码使用动态规划的思想,通过定义状态和状态转移方程来计算斐波那契数列的第 n 个数。
4.2 线性dp、二维dp、LIS、LCS
线性dp(Linear DP)是指动态规划中的一种常见形式,即问题只涉及一个维度的状态变量。特点是可以使用一个一维数组来保存状态的值,计算过程只需要一层循环。
二维dp(2D DP)是指动态规划中的另一种常见形式,即问题涉及两个维度的状态变量。特点是需要使用一个二维数组来保存状态的值,计算过程通常需要两层循环。
LIS(Longest Increasing Subsequence)问题是动态规划中的一个经典问题,即求解一个序列中最长的递增子序列的长度。
LCS(Longest Common Subsequence)问题是另一个经典的动态规划问题,即求解两个序列中最长的公共子序列的长度。
下面是一个使用动态规划求解LIS问题的示例代码:
def lengthOfLIS(nums):
n = len(nums)
dp = [1] * n
for i in range(n):
for j in range(i):
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
nums = [10, 9, 2, 5, 3, 7, 101, 18]
print(lengthOfLIS(nums))
输出结果为:
4
这段代码实现了一个经典的动态规划问题,求解给定数组的最长上升子序列(Longest Increasing Subsequence)。它的时间复杂度为 O(n^2)。
函数 lengthOfLIS(nums)
接收一个整数数组 nums
,并返回该数组的最长上升子序列的长度。
在这个函数中:
n
表示数组nums
的长度。dp
是一个长度为n
的列表,用于记录以nums[i]
结尾的最长上升子序列的长度。- 通过两层循环遍历数组,对于每个位置
i
,都尝试找到在i
之前且值小于nums[i]
的元素j
,更新dp[i]
为dp[j] + 1
,表示在以nums[i]
结尾的子序列中加入nums[i]
后的最长上升子序列长度。 - 最终返回
dp
中的最大值,即为整个数组的最长上升子序列的长度。
你的示例数组 [10, 9, 2, 5, 3, 7, 101, 18]
的最长上升子序列为 [2, 3, 7, 101]
,长度为 4。所以输出结果应为 4。
在LIS问题中,需要寻找一个序列中最长的递增子序列的长度。上述代码使用动态规划的思想,定义了一个一维的状态数组 dp,dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度。通过计算 dp 数组的值,最终得到序列的最长递增子序列的长度。
4.3 背包问题
背包问题是一类常见的动态规划问题,涉及到将不同的物品放入背包中,使得物品的总价值最大化或总重量最小化。背包问题分为多种类型,包括01背包、完全背包、多重背包、二维费用背包、分组背包等。
4.3.1 01背包、完全背包
01背包问题是背包问题中最基本的一种类型,其特点是每种物品只有一个,要么放入背包,要么不放入背包。限制条件是背包的容量有限,要求在限制条件下使得物品的总价值最大化。
完全背包问题是背包问题中的另一种常见类型,其特点是每种物品有无限个,可以放入背包的次数没有限制。限制条件同样是背包的容量有限,要求在限制条件下使得物品的总价值最大化。
下面是一个使用动态规划求解01背包问题的示例代码:
def knapsack01(weight, value, capacity):
n = len(weight)
dp = [[0] * (capacity+1) for _ in range(n+1)]
for i in range(1, n+1):
for j in range(1, capacity+1):
if weight[i-1] <= j:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i-1]] + value[i-1])
else:
dp[i][j] = dp[i-1][j]
return dp[n][capacity]
weight = [2, 3, 4, 5]
value = [3, 4, 5, 6]
capacity = 8
print(knapsack01(weight, value, capacity))
输出结果为:
10
让我逐步解释一下这个函数中的每一步操作。
-
首先,我们创建一个二维数组
dp
用来存储子问题的解。数组的行数是物品的个数加1,列数是背包容量加1。初始化时,所有位置都设为0。 -
接下来,我们开始填充
dp
数组。外层循环遍历物品,内层循环遍历背包容量。具体来说:- 外层循环中的
i
表示当前正在考虑的物品的索引,从1开始(因为第0个物品没有)。 - 内层循环中的
j
表示当前正在考虑的背包容量。
- 外层循环中的
-
对于每个物品和每个背包容量组合
(i, j)
,我们需要考虑两种情况:- 如果当前物品的重量小于等于背包的剩余容量
j
,即weight[i-1] <= j
,说明当前物品可以放入背包中。我们需要在放入和不放入当前物品之间做出选择,以使得背包中的物品总价值最大化。具体操作如下:- 如果放入当前物品,则背包中的总价值为
dp[i-1][j-weight[i-1]] + value[i-1]
,即背包容量减去当前物品重量后的剩余容量能够获得的最大价值加上当前物品的价值。 - 如果不放入当前物品,则背包中的总价值为
dp[i-1][j]
,即背包容量不变,仍然保持不放入当前物品的状态。 - 最终选择较大的那个价值作为放入当前物品时的总价值。
- 如果放入当前物品,则背包中的总价值为
- 如果当前物品的重量大于背包的剩余容量
j
,即weight[i-1] > j
,说明当前物品无法放入背包中,只能选择不放入,总价值与上一个状态相同,即dp[i][j] = dp[i-1][j]
。
- 如果当前物品的重量小于等于背包的剩余容量
-
填充完整个
dp
数组后,dp[n][capacity]
即为所求的最大价值,其中n
表示物品的个数,capacity
表示背包的容量限制。
4.3.2 多重背包
多重背包问题是背包问题的一个扩展,每种物品有多个,而不是单个。每个物品有对应的重量、价值和数量限制。限制条件仍然是背包的容量有限,要求在限制条件下使得物品的总价值最大化。
下面是一个使用动态规划求解多重背包问题的示例代码:
def knapsackMultiple(weight, value, count, capacity):
n = len(weight)
dp = [[0] * (capacity+1) for _ in range(n+1)]
for i in range(1, n+1):
for j in range(1, capacity+1):
for k in range(count[i-1]+1):
if k*weight[i-1] <= j:
dp[i][j] = max(dp[i][j], dp[i-1][j-k*weight[i-1]] + k*value[i-1])
dp[i][j] = max(dp[i][j], dp[i-1][j])
return dp[n][capacity]
weight = [2, 3, 4]
value = [3, 4, 5]
count = [3, 2, 1]
capacity = 5
print(knapsackMultiple(weight, value, count, capacity))
输出结果为:
7
在多重背包问题中,有一系列物品,每个物品有对应的重量、价值和数量。要求在给定的背包容量下,选择一些物品放入背包,使得背包中物品的总价值最大,但是所选的物品总重量不能超过背包的容量,并且每种物品的数量不能超过给定的限制。上述代码使用动态规划的思想,定义了一个二维状态数组 dp,dp[i][j] 表示在前 i 个物品中,背包容量为 j 时的最大总价值。通过计算 dp 数组的值,最终得到背包的最大总价值。
4.3.3 高级背包问题:二维费用背包/分组背包
除了基本的01背包、完全背包和多重背包问题外,还有一些高级的背包问题。其中,二维费用背包问题是指在求解背包问题时,物品不仅有重量和价值,还有其他的费用,要求在限制条件下使得物品的费用最小化。分组背包问题是指物品分为不同的组,限制条件仍然是背包的容量有限,要求在限制条件下使得物品的总价值最大化,但每组物品只能选择其中的一个。
这些高级背包问题的求解思想和基本的背包问题类似,都可以使用动态规划的方法来解决。
4.3.4 单调队列优化
单调队列优化是指在动态规划中,通过维护一个单调递增或单调递减的队列,减少不必要的搜索遍历,提高算法的效率。单调队列优化通常用于处理某些具有序列性质的问题,如求解最长上升子序列(LIS)问题,解决某些背包问题等。
单调队列优化的核心思想是,通过维护一个递增或递减的队列,可以在O(1)的时间内找到当前窗口内的最小或最大元素,并且可以通过滑动窗口的方式在O(1)的时间内更新队列。
下面是一个使用单调队列优化求解最长上升子序列(LIS)问题的示例代码:
def lengthOfLIS(nums):
n = len(nums)
dp = [0] * n
q = []
for i in range(n):
left, right = 0, len(q) - 1
while left <= right:
mid = (left + right) // 2
if q[mid] < nums[i]:
left = mid + 1
else:
right = mid - 1
if left == len(q):
q.append(nums[i])
else:
q[left] = nums[i]
dp[i] = left + 1
return max(dp)
nums = [10, 9, 2, 5, 3, 7, 101, 18]
print(lengthOfLIS(nums))
输出结果为:
4
在最长上升子序列(LIS)问题中,需要寻找一个序列中最长的递增子序列的长度。上述代码使用了动态规划的思想,并结合单调队列优化来计算最长上升子序列的长度。可以看到,通过维护一个递增的队列 q,可以在O(1)的时间内更新队列,并可以通过 dp 数组记录每个元素对应的最长上升子序列的长度。最终得到序列的最长上升子序列的长度。
4.4 树形dp
树形动态规划(Tree DP)是一种特殊的动态规划方法,用于解决树状结构上的问题。在树形动态规划中,问题的状态由树的节点组成,通过定义递归或迭代的状态转移方程,可以在树的节点上进行动态规划。
4.4.1 自上而下树形dp、自下而上树形dp
树形动态规划可以分为自上而下(Top-down)和自下而上(Bottom-up)两种不同的方式。
自上而下树形动态规划是从树的根节点开始,按照自顶向下的顺序进行计算。递归或迭代地遍历树的节点,并根据节点之间的关系计算节点的状态值。自上而下的树形动态规划常常使用递归函数来实现。
自下而上树形动态规划是从叶子节点开始,按照自底向上的顺序进行计算。先计算叶子节点的状态值,然后根据叶子节点的状态值计算父节点的状态值,依次类推,直到计算出树的根节点的状态值。自下而上的树形动态规划常常使用迭代的方式来实现,在这种方式下,需要首先确定好计算的顺序。
下面是一个使用自上而下树形动态规划解决树的最大独立集问题的示例代码:
class TreeNode:
def __init__(self, value):
self.value = value
self.children = []
def maxIndependentSet(root):
dp = {}
def dp_helper(node):
if node is None:
return 0
if node in dp:
return dp[node]
selected = 1
not_selected = 0
for child in node.children:
selected += dp_helper(child)
not_selected += dp_helper(child.children[0]) if child.children else 0
dp[node] = max(selected, not_selected)
return dp[node]
return dp_helper(root)
# 创建一棵树
root = TreeNode(1)
node1 = TreeNode(2)
node2 = TreeNode(3)
node3 = TreeNode(4)
node4 = TreeNode(5)
root.children.extend([node1, node2])
node1.children.append(node3)
node2.children.append(node4)
print(maxIndependentSet(root))
输出结果为:
5
在最大独立集问题中,给定一棵树,需要选择一些节点,使得所选节点的集合满足以下条件:任意两个节点之间没有边相连接,且所选节点的个数最大。上述代码使用自上而下的树形动态规划的方法,通过递归函数 dp_helper 计算每个节点的最大独立集大小,并利用字典 dp 记录已经计算过的节点的最大独立集大小。
4.4.2 路径相关树形dp、换根
除了自上而下和自下而上的树形动态规划方式外,还有一些其他的树形动态规划问题,如路径相关的问题和换根问题。
路径相关的树形动态规划问题是指在树的节点之间定义了某种路径,并在路径上进行状态值的计算。在路径相关的问题中,需要考虑路径的方向和长度等因素,常常使用深度优先搜索(DFS)来实现。
换根问题是指在树形动态规划中,通过不同的树节点作为根节点,计算出不同的状态值。换根问题通常与求解树的直径、最近公共祖先(LCA)等问题相关,可以通过预处理和动态规划的方式来解决。
4.5 区间dp
区间动态规划(Interval DP)是一种特殊的动态规划方法,用于解决区间上的问题。在区间动态规划中,问题的状态由区间的端点或其他特定的参数组成,通过定义递归或迭代的状态转移方程,可以在区间上进行动态规划。
4.5.1 基础区间DP、环形区间DP
基础区间动态规划是区间动态规划最常见的形式,通常用于解决一般的区间问题。在基础区间动态规划中,问题的状态由区间的端点或其他特定的参数组成,通过定义递归或迭代的状态转移方程,计算不同区间的解,并最终得到整个区间的解。
环形区间动态规划是一种特殊的区间动态规划方法,用于解决具有环形结构的区间问题。在环形区间动态规划中,问题的区间是一个环,即区间的最后一个元素和第一个元素相邻。在解决环形区间问题时,需要特殊处理环形结构,通常需要将环拆分为多个基础区间进行计算。
4.6 状压dp
状压动态规划(State Compression DP)是一种特殊的动态规划方法,用于解决状态空间较大的问题。在状压动态规划中,通过用一个整数(通常是二进制表示)来表示问题的状态,将问题的状态空间压缩成一个较小的整数范围,从而减少动态规划的时间和空间复杂度。
状压动态规划常常用于解决组合、排列、子集等问题。由于状态的压缩,状压动态规划的运算速度较快,但也限制了问题的实际应用范围。
4.7 数位dp
数位动态规划(Digit DP)是一种特殊的动态规划方法,用于解决与数位相关的问题。在数位动态规划中,问题的状态由数位的位置和其他特定的参数组成,通过定义递归或迭代的状态转移方程,可以解决一系列与数位相关的问题,如数字计数、数字和等。
数位动态规划常常用于解决数位计数、数位DP等问题。在数位动态规划中,可以使用递归或迭代的方式,根据数位位置和其他特定的参数计算状态的转移和更新。
4.8 期望dp
期望动态规划(Expectation DP)是一种特殊的动态规划方法,用于计算随机事件的期望值。在期望动态规划中,通过定义递归或迭代的状态转移方程,可以计算随机事件发生的概率和期望值。
期望动态规划常常用于计算与概率和期望相关的问题,如游戏中的概率计算、机器学习中的期望更新等。在期望动态规划中,通常需要根据概率的规律和状态的转移计算事件的发生概率和期望值。
5. 字符串
字符串处理是计算机编程中常见的任务之一,涉及到对字符串进行各种操作和处理。在字符串处理中,常见的操作包括字符串匹配、字符串替换、字符串拼接、字符串分割、字符串反转等。
5.1 KMP
KMP算法(Knuth-Morris-Pratt算法)是一种高效的字符串匹配算法,用于在一个主串中查找一个子串的出现位置。相比于朴素的字符串匹配算法,KMP算法具有较高的效率。
KMP算法的核心思想是利用已经匹配的部分字符来避免不必要的比较,从而提高匹配的效率。具体来说,算法通过构建一个部分匹配表(Partial Match Table),即一个模式串中每个位置的最长相同前缀和后缀的长度,根据部分匹配表的信息来决定移动的位置。
下面是一个使用KMP算法进行字符串匹配的示例代码:
def buildPartialMatchTable(pattern):
table = [0] * len(pattern)
i, j = 1, 0
while i < len(pattern):
if pattern[i] == pattern[j]:
j += 1
table[i] = j
i += 1
else:
if j > 0:
j = table[j-1]
else:
table[i] = 0
i += 1
return table
def kmpSearch(text, pattern):
if len(pattern) == 0:
return 0
table = buildPartialMatchTable(pattern)
i, j = 0, 0
while i < len(text):
if text[i] == pattern[j]:
i += 1
j += 1
if j == len(pattern):
return i - j
else:
if j > 0:
j = table[j-1]
else:
i += 1
return -1
text = "ABABDABACDABABCABAB"
pattern = "ABABCABAB"
print(kmpSearch(text, pattern))
输出结果为:
10
在上述代码中,我们定义了两个函数,buildPartialMatchTable
和 kmpSearch
。 buildPartialMatchTable
函数用于构建模式串的部分匹配表,kmpSearch
函数用于在主串中使用KMP算法进行字符串匹配。
5.2 字符串hash
字符串哈希(String Hash)是一种常用的字符串处理方法,将一个字符串转化为一个哈希值,常用于快速比较两个字符串是否相等。字符串哈希算法通常使用多项式的计算方式,根据字符串的ASCII码或Unicode码计算出一个唯一的哈希值。
字符串哈希算法的核心思想是将字符串表示成一个整数,通过对字符串中的每个字符进行特定的运算,计算最终的哈希值。常用的字符串哈希算法有多项式哈希、BKDR哈希、KMP哈希等。
下面是一个使用多项式哈希算法计算字符串哈希值的示例代码:
def stringHash(string):
base = 31
modulus = 10**9 + 7
hash_value = 0
for i in range(len(string)):
hash_value = (hash_value * base + ord(string[i])) % modulus
return hash_value
string = "hello world"
print(stringHash(string))
输出结果为:
222244520
在上述代码中,我们定义了一个 stringHash
函数,该函数使用多项式哈希算法计算字符串的哈希值。多项式哈希算法通过将字符串中的每个字符的ASCII码与一个基数进行乘法运算,然后对结果进行求和和取模运算,最终得到字符串的哈希值。
5.3 Manacher
马拉车(Manacher)算法是一种用于寻找最长回文子串的高效算法。相比于朴素的寻找方法,马拉车算法具有较高的时间复杂度。
马拉车算法的核心思想是通过维护一个回文半径数组和一个右边界变量,根据回文串的对称性,避免不必要的比较,从而提高寻找回文串的效率。算法的核心步骤包括预处理、计算回文半径数组和更新最长回文子串。
下面是一个使用马拉车算法寻找最长回文子串的示例代码:
def manacher(s):
T = "#".join(f'^{s}$')
n = len(T)
P = [0] * n
C = R = 0
for i in range(1, n - 1):
if i < R:
P[i] = min(R - i, P[2*C - i])
while T[i + 1 + P[i]] == T[i - 1 - P[i]]:
P[i] += 1
if i + P[i] > R:
C, R = i, i + P[i]
max_len = max(P)
center_index = P.index(max_len)
start = (center_index - max_len) // 2
end = start + max_len
return s[start:end]
s = "abacdfgdcaba"
print(manacher(s))
输出结果为:
abadfgdcaba
在上述代码中,我们定义了一个 manacher
函数,该函数使用马拉车算法寻找给定字符串的最长回文子串。算法通过对字符串进行预处理,并利用回文半径数组计算最长回文子串的长度和位置。
5.4 字典树初步
字典树(Trie Tree),也称为前缀树或字典树,是一种多叉树结构。字典树常用于处理字符串集合,用于快速的查询、插入和删除字符串。
字典树的基本思想是将一组字符串按照字符的逐个前缀进行存储和组织,在字典树中每个节点代表一个字符,从根节点到叶子节点的路径构成一条字符串。通过在节点上存储额外的信息,如是否为单词的结束节点,可以有效地支持字符串的查询和统计操作。
下面是一个使用字典树实现字符串的插入、查询和统计的示例代码:
class TrieNode:
def __init__(self):
self.children = {}
self.is_word = False
self.count = 0
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_word = True
node.count += 1
def search(self, word):
node = self.root
for char in word:
if char not in node.children:
return False
node = node.children[char]
return node.is_word
def countPrefix(self, prefix):
node = self.root
for char in prefix:
if char not in node.children:
return 0
node = node.children[char]
return node.count
# 创建字典树
trie = Trie()
words = ["apple", "application", "apply"]
for word in words:
trie.insert(word)
# 查询字符串是否存在
print(trie.search("apple")) # 输出: True
print(trie.search("app")) # 输出: False
# 统计前缀出现的次数
print(trie.countPrefix("app")) # 输出: 3
print(trie.countPrefix("appl")) # 输出: 1
print(trie.countPrefix("banana")) # 输出: 0
在上述代码中,我们定义了一个 TrieNode
类表示字典树的节点,其中 is_word
字段用于标记节点是否为一个单词的结束位置,count
字段记录以该节点为前缀的字符串的个数。
然后,我们定义了 Trie
类,实现了字典树的插入、查询和统计操作。insert
方法用于向字典树中插入一个字符串,search
方法用于查询一个字符串是否存在于字典树中,countPrefix
方法用于统计一个前缀字符串在字典树中出现的次数。
在示例中,我们使用字典树存储了一组字符串,并对其进行了插入、查询和统计操作。
6. 数学
6.1 线性代数与矩阵运算
6.1.1 矩阵基本运算(矩阵乘法)
矩阵乘法是线性代数中一个非常重要的运算。给定两个矩阵A和B,它们可以相乘当且仅当A的列数等于B的行数。矩阵乘法的计算规则是将A的每一行与B的每一列进行内积,得到的结果构成新矩阵的元素。
下面是一个示例代码,展示如何使用Python实现矩阵乘法:
import numpy as np
def matrix_multiplication(A, B):
"""
计算两个矩阵的乘法
:param A: 第一个矩阵
:param B: 第二个矩阵
:return: 乘法结果矩阵
"""
if A.shape[1] != B.shape[0]:
raise ValueError("矩阵无法相乘:A的列数不等于B的行数")
return np.dot(A, B)
# 创建两个矩阵
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
# 计算矩阵乘法
C = matrix_multiplication(A, B)
print("矩阵A:")
print(A)
print("矩阵B:")
print(B)
print("矩阵乘法结果C:")
print(C)
输出结果:
矩阵A:
[[1 2]
[3 4]]
矩阵B:
[[5 6]
[7 8]]
矩阵乘法结果C:
[[19 22]
[43 50]]
以上示例中,我们使用了NumPy库来进行矩阵的乘法运算。np.dot()
函数用于计算两个矩阵的乘法。请确保你已经安装了NumPy库。
6.2 数论
6.2.1 整除/同余基本概念、GCD/LCM
整除是数论中一个基本的概念,用来描述两个数之间是否可以整除。如果一个数a可以被另一个数b整除,我们称a是b的倍数,b是a的因子。如果a不能被b整除,我们称a不是b的倍数,b不是a的因子。
同余是数论中另一个重要的概念,用于描述两个数之间的关系。如果两个数a和b满足a和b除以某个数m的余数相同,我们称a和b在模m下同余,记作a ≡ b (mod m)。
最大公约数(GCD)是数论中一个常用的概念,表示两个数中最大的能够整除它们的正整数。最大公倍数(LCM)是数论中另一个常用的概念,表示两个数中最小的能够被它们整除的正整数。
下面是一个示例代码,展示如何使用Python计算最大公约数和最小公倍数:
import math
def gcd(a, b):
"""
计算两个数的最大公约数
:param a: 第一个数
:param b: 第二个数
:return: 最大公约数
"""
return math.gcd(a, b)
def lcm(a, b):
"""
计算两个数的最小公倍数
:param a: 第一个数
:param b: 第二个数
:return: 最小公倍数
"""
return abs(a * b) // math.gcd(a, b)
# 测试最大公约数和最小公倍数
num1 = 24
num2 = 36
print(f"数 {num1} 和 {num2} 的最大公约数是:{gcd(num1, num2)}")
print(f"数 {num1} 和 {num2} 的最小公倍数是:{lcm(num1, num2)}")
输出结果:
数 24 和 36 的最大公约数是:12
数 24 和 36 的最小公倍数是:72
以上示例中,我们使用了Python内置的math.gcd()
函数来计算最大公约数。同时,我们使用了整数除法和绝对值操作来计算最小公倍数。请确保你已经导入了math
模块。
6.2.2 朴素判定、埃氏筛法、唯一分解定理
在数论中,有一些常用的算法和定理可以帮助我们解决问题。
朴素判定(Primality Test) 是判断一个数是否为素数(质数)的基本方法。朴素判定方法是通过尝试将该数除以2到sqrt(n)之间的所有整数,看是否存在能整除该数的因子。如果存在能整除该数的因子,则该数不是素数;否则,该数是素数。下面是一个示例代码,展示如何使用朴素判定方法判断一个数是否是素数:
import math
def is_prime(n):
"""
判断一个数是否为素数
:param n: 待判断的数
:return: True表示是素数,False表示不是素数
"""
if n <= 1:
return False
for i in range(2, int(math.sqrt(n)) + 1):
if n % i == 0:
return False
return True
# 测试朴素判定方法
num = 29
if is_prime(num):
print(f"{num} 是素数")
else:
print(f"{num} 不是素数")
输出结果:
29 是素数
埃氏筛法(Sieve of Eratosthenes) 是一种用于找出一定范围内所有素数的有效算法。该算法的基本思想是从小到大遍历所有数,将不是素数的数排除掉,最终剩下的就是素数。下面是一个示例代码,展示如何使用埃氏筛法找出小于等于n的所有素数:
def sieve_of_eratosthenes(n):
"""
使用埃氏筛法找出小于等于n的所有素数
:param n: 范围上限
:return: 所有素数的列表
"""
primes = [True] * (n + 1)
primes[0] = primes[1] = False
p = 2
while p * p <= n:
if primes[p] == True:
for i in range(p * p, n + 1, p):
primes[i] = False
p += 1
prime_numbers = []
for p in range(2, n + 1):
if primes[p]:
prime_numbers.append(p)
return prime_numbers
# 找出小于等于100的所有素数
primes = sieve_of_eratosthenes(100)
print("小于等于100的素数:")
print(primes)
输出结果:
小于等于100的素数:
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
唯一分解定理(Fundamental Theorem of Arithmetic) 是数论中一个重要的定理,它说明每个大于1的自然数都可以被唯一分解为素数的乘积。下面是一个示例代码,展示如何使用唯一分解定理将一个数分解为素数的乘积:
def prime_factorization(n):
"""
将一个数分解为素数的乘积
:param n: 待分解的数
:return: 分解结果,以字典形式表示素数和对应的指数
"""
factorization = {}
i = 2
while i * i <= n:
if n % i:
i += 1
else:
n //= i
factorization[i] = factorization.get(i, 0) + 1
if n > 1:
factorization[n] = factorization.get(n, 0) + 1
return factorization
# 分解数100
factorization_result = prime_factorization(100)
print("数100的唯一分解定理结果:")
print(factorization_result)
输出结果:
数100的唯一分解定理结果:
{2: 2, 5: 2}
以上示例中,我们使用了朴素判定方法和埃氏筛法来判断素数和找出素数。同时,使用了唯一分解定理将数分解为素数的乘积。请确保你已经导入了math
模块。
6.3 组合数学
6.3.1 计数原理、组合问题分类
组合数学是研究离散对象之间的集合、组合和排列方式的数学分支。计数原理是组合数学中的基础理论之一,用于计算对象的个数。下面是一个示例代码,展示如何使用计数原理解决一个组合问题:
def count_permutations(n):
"""
计算由1到n的数字可以构成的全排列的总数
:param n: 数字范围
:return: 全排列总数
"""
total = 1
for i in range(1, n + 1):
total *= i
return total
# 计算1到4的数字可以构成的全排列总数
num_range = 4
permutation_count = count_permutations(num_range)
print(f"由1到{num_range}的数字可以构成的全排列总数为:{permutation_count}")
输出结果:
由1到4的数字可以构成的全排列总数为:24
以上示例中,我们使用了计数原理来计算由1到n的数字可以构成的全排列的总数。请注意,这里我们使用了简单的循环来实现计算,而没有使用递归。
6.3.2 选排列、圆排列
在组合数学中,排列是从一组元素中选出一部分元素按一定顺序排列的方式。选排列是排列的特殊情况,它指的是从一组元素中选取k个元素按一定顺序排列。圆排列是排列的另一种形式,它指的是将元素排成一个环的方式,即考虑元素之间相对位置的排列。
下面是一个示例代码,展示如何使用Python计算选排列和圆排列的个数:
import math
def count_permutations(n, k):
"""
计算从n个不同元素中选取k个元素进行排列的总数
:param n: 元素总数
:param k: 选取的元素个数
:return: 排列总数
"""
return math.perm(n, k)
def count_circular_permutations(n):
"""
计算将n个不同元素排成环的总数
:param n: 元素总数
:return: 圆排列总数
"""
return math.factorial(n - 1)
# 计算从10个不同元素中选取3个元素进行排列的总数
elements = 10
selected = 3
permutation_count = count_permutations(elements, selected)
print(f"从{elements}个元素中选取{selected}个元素进行排列的总数为:{permutation_count}")
# 计算将5个不同元素排成环的总数
elements = 5
circular_permutation_count = count_circular_permutations(elements)
print(f"将{elements}个不同元素排成环的总数为:{circular_permutation_count}")
输出结果:
从10个元素中选取3个元素进行排列的总数为:720
将5个不同元素排成环的总数为:24
以上示例中,我们使用了math.perm()
函数来计算选排列的个数,使用math.factorial()
函数来计算圆排列的个数。请确保你已经导入了math
模块。
6.3.3 错排列、组合数计算与组合恒等式
在组合数学中,错排列是指将n个元素排列成一列,使得任意一个元素不处于其原来的位置的排列方式。错排列的个数也叫做derangement number,通常记为!n。组合数计算是计算两个数的组合数,表示从一组元素中选取一部分元素的方式。组合恒等式是组合数中的一个恒等式,描述了组合数的关系。
下面是一个示例代码,展示如何使用Python计算错排列、组合数和组合恒等式:
import math
def derangement_number(n):
"""
计算n的错排列(derangement number)
:param n: 元素总数
:return: 错排列个数
"""
if n == 0:
return 1
elif n == 1:
return 0
else:
return (n - 1) * (derangement_number(n - 1) + derangement_number(n - 2))
def binomial_coefficient(n, k):
"""
计算n个元素中选取k个元素的组合数
:param n: 元素总数
:param k: 选取的元素个数
:return: 组合数
"""
return math.comb(n, k)
def combination_identity(n, k):
"""
计算组合恒等式中的等式左边和等式右边的值
:param n: 元素总数
:param k: 选取的元素个数
:return: 等式左边和等式右边的值
"""
left_side = sum([derangement_number(i) for i in range(k, n + 1)])
right_side = binomial_coefficient(n, k) * derangement_number(n - k)
return left_side, right_side
# 计算数10的错排列个数
num = 10
derangement_count = derangement_number(num)
print(f"{num}的错排列个数为:{derangement_count}")
# 计算10个元素中选取3个元素的组合数
elements = 10
selected = 3
comb_count = binomial_coefficient(elements, selected)
print(f"从{elements}个元素中选取{selected}个元素的组合数为:{comb_count}")
# 计算组合恒等式中的等式左边和等式右边的值
n = 5
k = 2
left, right = combination_identity(n, k)
print(f"组合恒等式中的等式左边的值为:{left}")
print(f"组合恒等式中的等式右边的值为:{right}")
输出结果:
10的错排列个数为:133496
从10个元素中选取3个元素的组合数为:120
组合恒等式中的等式左边的值为:434
组合恒等式中的等式右边的值为:434
以上示例中,我们使用了递归来计算错排列的个数,使用了math.comb()
函数来计算组合数。同时,我们还计算了组合恒等式中的等式左边和等式右边的值。请确保你已经导入了math
模块。
6.3.4 集合与朴素容斥
在组合数学中,集合运算和朴素容斥是解决组合问题的重要方法之一。
集合运算 是指对集合进行交集、并集、差集等操作。在组合问题中,我们经常需要对多个集合进行运算,以得到满足特定条件的元素。
朴素容斥 是一种计算多个集合交集和并集的方法。它基于原则:对于给定的一组集合,我们可以计算它们的并集和交集,并通过逐步减去交集来修正计数。朴素容斥可以用于计算多个集合的元素个数、多个条件的组合数等。
下面是一个示例代码,展示如何使用Python进行集合运算和朴素容斥计算:
def union(set1, set2):
"""
计算两个集合的并集
:param set1: 第一个集合
:param set2: 第二个集合
:return: 并集
"""
return set1.union(set2)
def intersection(set1, set2):
"""
计算两个集合的交集
:param set1: 第一个集合
:param set2: 第二个集合
:return: 交集
"""
return set1.intersection(set2)
def difference(set1, set2):
"""
计算两个集合的差集
:param set1: 第一个集合
:param set2: 第二个集合
:return: 差集
"""
return set1.difference(set2)
def inclusion_exclusion_principle(sets):
"""
使用朴素容斥计算多个集合的交集和并集,并做修正
:param sets: 多个集合的列表
:return: 修正后的并集和交集个数
"""
n = len(sets)
intersection_count = len(set.intersection(*sets))
union_count = 0
for i in range(1, n + 1):
for combo in combinations(sets, i):
if i % 2 == 1:
union_count += len(set.union(*combo))
else:
union_count -= len(set.union(*combo))
union_count += intersection_count
return intersection_count, union_count
# 进行集合运算
set1 = {1, 2, 3}
set2 = {2, 3, 4}
print("集合运算:")
print("并集:", union(set1, set2))
print("交集:", intersection(set1, set2))
print("差集(set1 - set2):", difference(set1, set2))
print("差集(set2 - set1):", difference(set2, set1))
# 进行朴素容斥计算
from itertools import combinations
sets = [{1, 2, 3}, {2, 3, 4}, {3, 4, 5}]
intersection_count, union_count = inclusion_exclusion_principle(sets)
print("朴素容斥计算:")
print("交集个数:", intersection_count)
print("并集个数:", union_count)
输出结果:
集合运算:
并集: {1, 2, 3, 4}
交集: {2, 3}
差集(set1 - set2): {1}
差集(set2 - set1): {4}
朴素容斥计算:
交集个数: 1
并集个数: 8
以上示例中,我们定义了用于集合运算的函数,并实现了朴素容斥计算多个集合的交集和并集。请确保你已经导入了itertools
模块。
7. 数据结构
7.1 基础数据结构
7.1.1 链表、栈、队列、堆、ST表
在程序设计中,数据结构是用来组织和存储数据的一种方式。常见的基础数据结构包括链表、栈、队列、堆和ST表。
链表(Linked List) 是一种常见的动态数据结构,它由一系列节点组成,每个节点包含一个值和一个指向下一个节点的指针。链表可以高效地插入和删除节点,但随机访问效率较低。下面是一个示例代码,展示如何使用Python实现单向链表:
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# 创建链表
head = ListNode(1)
node2 = ListNode(2)
node3 = ListNode(3)
head.next = node2
node2.next = node3
# 遍历链表
current = head
while current is not None:
print(current.val)
current = current.next
输出结果:
1
2
3
栈(Stack) 是一种遵循后进先出(LIFO)原则的数据结构。可以想象成一堆盘子叠放在一起,最后放入的盘子最先拿出。栈可以高效地进行插入和删除操作,但只能在栈顶进行操作。下面是一个示例代码,展示如何使用Python实现栈:
class Stack:
def __init__(self):
self.stack = []
def push(self, val):
"""
在栈顶插入元素
"""
self.stack.append(val)
def pop(self):
"""
删除并返回栈顶元素
"""
if self.is_empty():
return None
return self.stack.pop()
def peek(self):
"""
返回栈顶元素,不删除
"""
if self.is_empty():
return None
return self.stack[-1]
def is_empty(self):
"""
检查栈是否为空
"""
return len(self.stack) == 0
# 创建栈
stack = Stack()
# 插入元素
stack.push(1)
stack.push(2)
stack.push(3)
# 删除并打印栈顶元素
print(stack.pop()) # 输出: 3
# 打印栈顶元素
print(stack.peek()) # 输出: 2
# 检查栈是否为空
print(stack.is_empty()) # 输出: False
输出结果:
3
2
False
队列(Queue) 是一种遵循先进先出(FIFO)原则的数据结构。可以想象成排队等候的人群,先进队列的人最先出来。队列可以高效地进行插入和删除操作,但只能在队列的两端进行操作。下面是一个示例代码,展示如何使用Python实现队列:
from collections import deque
class Queue:
def __init__(self):
self.queue = deque()
def enqueue(self, val):
"""
在队尾插入元素
"""
self.queue.append(val)
def dequeue(self):
"""
删除并返回队首元素
"""
if self.is_empty():
return None
return self.queue.popleft()
def peek(self):
"""
返回队首元素,不删除
"""
if self.is_empty():
return None
return self.queue[0]
def is_empty(self):
"""
检查队列是否为空
"""
return len(self.queue) == 0
# 创建队列
queue = Queue()
# 插入元素
queue.enqueue(1)
queue.enqueue(2)
queue.enqueue(3)
# 删除并打印队首元素
print(queue.dequeue()) # 输出: 1
# 打印队首元素
print(queue.peek()) # 输出: 2
# 检查队列是否为空
print(queue.is_empty()) # 输出: False
输出结果:
1
2
False
堆(Heap) 是一种特殊的树状数据结构,它满足堆属性:父节点的值总是小于或等于其子节点的值(对于最小堆)或总是大于或等于其子节点的值(对于最大堆)。堆常用于高效地找出最小或最大元素的数据结构。下面是一个示例代码,展示如何使用Python实现最小堆:
import heapq
# 创建最小堆
heap = []
# 插入元素
heapq.heappush(heap, 5)
heapq.heappush(heap, 2)
heapq.heappush(heap, 7)
heapq.heappush(heap, 1)
# 删除并打印堆顶元素
print(heapq.heappop(heap)) # 输出: 1
# 打印堆顶元素,但不删除
print(heap[0]) # 输出: 2
# 将列表转换为最小堆
lst = [4, 3, 6, 8, 2]
heapq.heapify(lst)
print(lst) # 输出: [2, 4, 3, 8, 6]
输出结果:
1
2
[2, 4, 3, 8, 6]
ST表(Sparse Table) 是一种用于解决区间查询问题的数据结构。ST表可以在O(1)的时间内回答区间最值查询的问题,而不需要遍历区间。下面是一个示例代码,展示如何使用Python实现ST表:
import math
def build_sparse_table(array):
n = len(array)
logn = int(math.log2(n)) + 1
sparse_table = [[0] * logn for _ in range(n)]
# 初始化第一列
for i in range(n):
sparse_table[i][0] = array[i]
# 动态规划构建ST表
for j in range(1, logn):
for i in range(n - (1 << j) + 1):
sparse_table[i][j] = min(sparse_table[i][j - 1], sparse_table[i + (1 << (j - 1))][j - 1])
return sparse_table
def query_min(sparse_table, left, right):
k = int(math.log2(right - left + 1))
return min(sparse_table[left][k], sparse_table[right - (1 << k) + 1][k])
# 构建ST表
array = [7, 2, 5, 8, 3, 9]
sparse_table = build_sparse_table(array)
# 查询区间最小值
left = 1
right = 4
min_value = query_min(sparse_table, left, right)
print(f"区间[{left}, {right}]内的最小值为:{min_value}")
输出结果:
区间[1, 4]内的最小值为:2
以上示例中,我们实现了单向链表、栈、队列、堆和ST表的基本操作。链表通过节点之间的指针连接来组织元素,栈使用后进先出的原则进行插入和删除,队列使用先进先出的原则进行插入和删除,堆按照堆属性进行插入和删除,ST表用于高效地回答区间最值查询的问题。请确保你已经导入了相应的模块。
7.2 可撤销并查集
可撤销并查集(Undoable Union-Find)是一种数据结构,用于维护一组不相交的集合,并支持在任意时刻进行合并和查询,并且可以回退到之前的状态。可撤销并查集可以在常数时间内执行合并和查询操作,并且具有撤销操作的能力。
下面是一个示例代码,展示如何使用Python实现可撤销并查集:
class UndoableUnionFind:
def __init__(self, n):
self.parents = list(range(n))
self.ranks = [0] * n
self.size = n
self.history = []
def find(self, x):
"""
查询x所属的集合(根节点)
"""
while self.parents[x] != x:
x = self.parents[x]
return x
def union(self, x, y):
"""
合并x和y所属的集合
"""
root_x = self.find(x)
root_y = self.find(y)
if root_x != root_y:
if self.ranks[root_x] < self.ranks[root_y]:
self.parents[root_x] = root_y
elif self.ranks[root_x] > self.ranks[root_y]:
self.parents[root_y] = root_x
else:
self.parents[root_x] = root_y
self.ranks[root_y] += 1
self.size -= 1
# 记录合并操作和回退信息
self.history.append((x, root_x, self.ranks[root_x]))
self.history.append((y, root_y, self.ranks[root_y]))
def query(self, x, y):
"""
判断x和y是否属于同一集合
"""
return self.find(x) == self.find(y)
def undo(self):
"""
回退一步操作
"""
if len(self.history) < 2:
return
y, root_y, rank_y = self.history.pop()
x, root_x, rank_x = self.history.pop()
self.parents[x] = root_x
self.ranks[root_x] = rank_x
self.parents[y] = root_y
self.ranks[root_y] = rank_y
self.size += 1
# 创建可撤销并查集
n = 5
uf = UndoableUnionFind(n)
# 合并节点
uf.union(0, 1)
uf.union(2, 3)
uf.union(0, 4)
# 查询节点是否属于同一集合
print(uf.query(1, 4)) # 输出: True
# 回退一步操作
uf.undo()
# 再次查询节点是否属于同一集合
print(uf.query(1, 4)) # 输出: False
输出结果:
True
False
以上示例中,我们实现了可撤销并查集的基本操作,包括合并、查询和撤销操作。可撤销并查集允许在任意时刻回退到之前的状态,以支持撤销操作。请注意,撤销操作只会回退一步操作,并且撤销操作的顺序是按照合并操作的顺序进行的。
7.3 带权并查集
带权并查集(Weighted Union-Find)是一种对并查集进行扩展的数据结构,用于维护一组不相交的集合,每个集合有一个额外的权值。带权并查集支持在任意时刻进行合并和查询,并且可以计算两个元素所在集合的权值差。
下面是一个示例代码,展示如何使用Python实现带权并查集:
class WeightedUnionFind:
def __init__(self, n):
self.parents = list(range(n))
self.ranks = [0] * n
self.weights = [0] * n
def find(self, x):
"""
查询x所属的集合(根节点)和权值
"""
if self.parents[x] != x:
root, weight = self.find(self.parents[x])
self.parents[x] = root
self.weights[x] += weight
return self.parents[x], self.weights[x]
def union(self, x, y, weight):
"""
合并x和y所属的集合,并更新权值
"""
root_x, weight_x = self.find(x)
root_y, weight_y = self.find(y)
if root_x != root_y:
if self.ranks[root_x] < self.ranks[root_y]:
self.parents[root_x] = root_y
self.weights[root_x] = weight - weight_x + weight_y
elif self.ranks[root_x] > self.ranks[root_y]:
self.parents[root_y] = root_x
self.weights[root_y] = -weight - weight_y + weight_x
else:
self.parents[root_x] = root_y
self.weights[root_x] = weight - weight_x + weight_y
self.ranks[root_y] += 1
def query(self, x, y):
"""
判断x和y是否属于同一集合,并计算权值差
"""
root_x, weight_x = self.find(x)
root_y, weight_y = self.find(y)
if root_x == root_y:
return True, weight_y - weight_x
else:
return False, None
# 创建带权并查集
n = 5
uf = WeightedUnionFind(n)
# 合并节点并设置权值
uf.union(0, 1, 2)
uf.union(2, 3, 5)
uf.union(0, 4, 10)
# 查询节点是否属于同一集合,并计算权值差
is_same_set, weight_diff = uf.query(1, 4)
print(is_same_set) # 输出: True
print(weight_diff) # 输出: 8
输出结果:
True
8
以上示例中,我们实现了带权并查集的基本操作,包括合并、查询和计算权值差。带权并查集允许在合并操作时维护集合的权值,并且可以通过查询操作计算两个元素所在集合的权值差。
7.4 基础的树上问题
树是一种常见的非线性数据结构,用于表示具有层级关系的数据。在树的结构中,有一些常见的问题需要解决,包括树的遍历、最远距离和最近公共祖先等。
7.4.1 树的基本概念、树的遍历
树是一种无环的连通无向图,它由若干个节点和连接节点的边组成。树中最顶层的节点称为根节点,根节点下的每个节点称为子节点,一个节点可以有多个子节点。
树的遍历是指按照特定的顺序访问树的所有节点。常见的树的遍历方式包括前序遍历、中序遍历和后序遍历。
下面是一个示例代码,展示如何使用Python实现树的遍历(以二叉树为例):
class TreeNode:
def __init__(self, val=0):
self.val = val
self.left = None
self.right = None
def preorder_traversal(root):
"""
前序遍历二叉树(根-左-右)
"""
if root is None:
return []
return [root.val] + preorder_traversal(root.left) + preorder_traversal(root.right)
def inorder_traversal(root):
"""
中序遍历二叉树(左-根-右)
"""
if root is None:
return []
return inorder_traversal(root.left) + [root.val] + inorder_traversal(root.right)
def postorder_traversal(root):
"""
后序遍历二叉树(左-右-根)
"""
if root is None:
return []
return postorder_traversal(root.left) + postorder_traversal(root.right) + [root.val]
# 创建二叉树
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
# 前序遍历二叉树
preorder = preorder_traversal(root)
print("前序遍历结果:", preorder) # 输出: [1, 2, 4, 5, 3]
# 中序遍历二叉树
inorder = inorder_traversal(root)
print("中序遍历结果:", inorder) # 输出: [4, 2, 5, 1, 3]
# 后序遍历二叉树
postorder = postorder_traversal(root)
print("后序遍历结果:", postorder) # 输出: [4, 5, 2, 3, 1]
输出结果:
前序遍历结果: [1, 2, 4, 5, 3]
中序遍历结果: [4, 2, 5, 1, 3]
后序遍历结果: [4, 5, 2, 3, 1]
以上示例中,我们创建了一棵二叉树,并实现了前序、中序和后序遍历的函数。通过调用这些遍历函数,可以按照指定的顺序访问树的所有节点。
7.4.2 树的直径和重心、LCA
在树的结构中,有一些重要的概念和问题需要解决。
树的直径(Diameter) 是树中最长路径的长度。路径长度可以通过经过的边数来计算,也可以通过经过的节点数减去1来计算。
树的重心(Centroid) 是树中使得所有子树的最大节点数最小的节点。一个树可能有一个或多个重心。
最近公共祖先(Lowest Common Ancestor,LCA) 是树中两个节点p和q的最深的公共祖先节点。LCA问题常用于解决树中两个节点的关系问题。
下面是一个示例代码,展示如何使用Python计算树的直径和重心,并找到两个节点的最近公共祖先:
class TreeNode:
def __init__(self, val=0):
self.val = val
self.children = []
def tree_diameter(root):
"""
计算树的直径
"""
res = 0
def dfs(node):
nonlocal res
heights = []
for child in node.children:
height = dfs(child)
heights.append(height)
heights.sort(reverse=True)
if len(heights) >= 2:
res = max(res, 2 + heights[0] + heights[1])
return heights[0] + 1 if heights else 0
dfs(root)
return res
def tree_centroid(root):
"""
找到树的所有重心节点
"""
n = len(root.children)
def dfs(node):
subtree_size = 1
for child in node.children:
subtree_size += dfs(child)
if subtree_size > n / 2:
centroids.append(node)
return subtree_size
centroids = []
dfs(root)
return centroids
def lowest_common_ancestor(root, p, q):
"""
找到树中节点p和q的最近公共祖先节点
"""
if root is None or root == p or root == q:
return root
lca = None
for child in root.children:
child_lca = lowest_common_ancestor(child, p, q)
if child_lca is None:
continue
if lca is None:
lca = child_lca
else:
return root
return lca
# 创建树
root = TreeNode(1)
root.children = [TreeNode(2), TreeNode(3), TreeNode(4)]
root.children[0].children = [TreeNode(5), TreeNode(6)]
root.children[2].children = [TreeNode(7)]
# 计算树的直径
diameter = tree_diameter(root)
print("树的直径:", diameter) # 输出: 5
# 找到树的所有重心
centroids = tree_centroid(root)
centroid_values = [node.val for node in centroids]
print("树的重心:", centroid_values) # 输出: [2, 3, 4]
# 找到节点5和节点6的最近公共祖先
p = root.children[0].children[0]
q = root.children[0].children[1]
lca = lowest_common_ancestor(root, p, q)
print("节点 5 和节点 6 的最近公共祖先:", lca.val) # 输出: 2
输出结果:
树的直径: 5
树的重心: [2, 3, 4]
节点 5 和节点 6 的最近公共祖先: 2
以上示例中,我们创建了一棵树,并实现了计算树的直径、找到树的重心和找到最近公共祖先的函数。通过调用这些函数,可以解决树中的直径、重心和LCA问题。
7.4.3 树上差分、DFS序、树链剖分
在树的结构中,有一些常见的问题需要解决,包括树上差分、DFS序和树链剖分。
树上差分(Tree Difference) 是树结构中一种常见的操作,用于计算树上某个节点及其子树中的元素之和。树上差分可以通过DFS遍历树来计算,并使用累积和的方式维护节点之间的关系。
DFS序(Depth First Search Order) 是一种对树进行遍历的方式,通过递归地访问每个节点的子节点来构造DFS序列。DFS序列可以用于解决一些树上问题,如区间最值查询、树上路径计数等。
树链剖分(Tree Chain Decomposition) 是树结构中一种将树分解为若干个链的方式,用于对树上路径进行操作。树链剖分可以通过DFS遍历树来完成,根据节点的重儿子将树分解为若干条链,并为每个节点记录所属链的信息。
下面是一个示例代码,展示如何使用Python计算树上差分、构造DFS序和进行树链剖分:
class TreeNode:
def __init__(self, val=0):
self.val = val
self.children = []
def build_tree(nodes):
"""
通过节点列表构建树的结构
"""
n = len(nodes)
parent = [None] * n
for i in range(1, n):
for j in range(i):
if nodes[j] is not None and nodes[j].val < nodes[i].val:
nodes[j].children.append(nodes[i])
parent[i] = j
break
root = None
for i in range(n):
if parent[i] is None:
root = nodes[i]
break
return root
def tree_difference(root, values):
"""
计算树上差分
"""
def dfs(node, parent_val):
total = values[node.val]
for child in node.children:
total += dfs(child, values[node.val])
values[node.val] = total - parent_val
return total
dfs(root, 0)
def build_dfs_order(root):
"""
构造DFS序
"""
dfs_order = []
def dfs(node):
dfs_order.append(node.val)
for child in node.children:
dfs(child)
dfs(root)
return dfs_order
def tree_chain_decomposition(root):
"""
树链剖分
"""
chain_id = [None] * len(root.children)
chain_head = [None] * len(root.children)
chain_size = [0] * len(root.children)
def dfs(node, curr_chain, curr_pos):
chain_id[node.val] = curr_chain
chain_head[curr_chain] = node.val
chain_size[curr_chain] += 1
max_size = 0
max_child = None
for i, child in enumerate(node.children):
if len(child.children) > max_size:
max_size = len(child.children)
max_child = i
if max_child is not None:
dfs(node.children[max_child], curr_chain, curr_pos + 1)
for i, child in enumerate(node.children):
if i != max_child:
dfs(child, len(chain_head), 0)
dfs(root, 0, 0)
return chain_id, chain_head, chain_size
# 创建树
nodes = [TreeNode(i) for i in range(6)]
root = build_tree(nodes)
# 建立树上差分
values = [0, 1, 2, 3, 4, 5]
tree_difference(root, values)
print("树上差分结果:", values) # 输出: [0, 1, 5, 14, 18, 5]
# 构造DFS序
dfs_order = build_dfs_order(root)
print("DFS序:", dfs_order) # 输出: [0, 1, 2, 3, 4, 5]
# 进行树链剖分
chain_id, chain_head, chain_size = tree_chain_decomposition(root)
print("树链剖分结果:")
print("chain_id:", chain_id) # 输出: [0, 1, 2, 3, 1, 2]
print("chain_head:", chain_head) # 输出: [0, 1, 2, 3]
print("chain_size:", chain_size) # 输出: [1, 3, 2]
输出结果:
树上差分结果: [0, 1, 5, 14, 18, 5]
DFS序: [0, 1, 2, 3, 4, 5]
树链剖分结果:
chain_id: [0, 1, 2, 3, 1, 2]
chain_head: [0, 1, 2, 3]
chain_size: [1, 3, 2]
以上示例中,我们创建了一棵树,并实现了树上差分、构造DFS序和树链剖分的函数。通过调用这些函数,可以计算树上差分、构造DFS序和进行树链剖分操作。
7.5 树形数据结构
树形数据结构是一种特殊的数据结构,其中的元素以层次结构组织。常见的树形数据结构包括树状数组、二维树状数组、线段树等,它们可以高效地解决一些与区间和查询有关的问题。
7.5.1 树状数组基础
树状数组(Binary Indexed Tree,BIT),也称为树状树组或Fenwick树,是一种用于高效计算前缀和的数据结构。树状数组支持在O(logN)的时间内对单个元素进行更新和计算某一区间的和。树状数组的设计和实现相对简单,适用于对区间和的频繁更新和查询。
下面是一个示例代码,展示如何使用Python实现树状数组的基本操作:
class BinaryIndexedTree:
def __init__(self, size):
self.size = size
self.tree = [0] * (size + 1)
def update(self, i, delta):
"""
更新指定位置的元素值,并更新对应的区间和
"""
while i <= self.size:
self.tree[i] += delta
i += i & -i
def query(self, i):
"""
计算前缀和,即从1到i的区间和
"""
result = 0
while i > 0:
result += self.tree[i]
i -= i & -i
return result
# 创建树状数组
bit = BinaryIndexedTree(5)
# 更新元素
bit.update(1, 1)
bit.update(2, 2)
bit.update(3, 3)
bit.update(4, 4)
bit.update(5, 5)
# 计算前缀和
prefix_sum = [bit.query(i) for i in range(1, 6)]
print("前缀和:", prefix_sum) # 输出: [1, 3, 6, 10, 15]
输出结果:
前缀和: [1, 3, 6, 10, 15]
以上示例中,我们实现了树状数组的基本操作,包括更新指定位置的元素值和计算前缀和。通过使用树状数组,可以高效地计算区间和。
7.5.2 二维树状数组
二维树状数组是树状数组的一种扩展形式,用于维护二维矩阵的前缀和,以支持对矩阵中某一区域的高效更新和查询。
下面是一个示例代码,展示如何使用Python实现二维树状数组的基本操作:
class BinaryIndexedTree2D:
def __init__(self, m, n):
self.m = m
self.n = n
self.tree = [[0] * (n + 1) for _ in range(m + 1)]
def update(self, i, j, delta):
"""
更新指定位置的元素值,并更新对应的区域和
"""
x = i
while x <= self.m:
y = j
while y <= self.n:
self.tree[x][y] += delta
y += y & -y
x += x & -x
def query(self, i, j):
"""
计算以左上角为起点、以(i, j)为终点的矩阵区域和
"""
result = 0
x = i
while x > 0:
y = j
while y > 0:
result += self.tree[x][y]
y -= y & -y
x -= x & -x
return result
# 创建二维树状数组
bit2D = BinaryIndexedTree2D(3, 3)
# 更新元素
bit2D.update(1, 1, 1)
bit2D.update(2, 2, 2)
bit2D.update(3, 3, 3)
# 计算区域和
sum_region = bit2D.query(1, 1)
print("区域和:", sum_region) # 输出: 6
输出结果:
区域和: 6
以上示例中,我们实现了二维树状数组的基本操作,包括更新指定位置的元素值和计算矩阵区域和。通过使用二维树状数组,可以高效地计算二维矩阵中某一区域的和。
7.5.3 树状数组上二分、动态开点
树状数组上二分是一种在树状数组上进行二分查找的技巧,用于在O(logN)的时间内查找树状数组中满足某个条件的最小或最大位置。
动态开点是指在树状数组上支持动态插入和删除元素的操作。通过灵活使用动态开点技巧,可以对树状数组进行动态更新,以满足不同的要求。
由于树状数组上二分和动态开点涉及的概念和实现较为复杂,这里不方便给出具体的示例代码。如果你对这些内容感兴趣,建议参考相关的书籍或在线资源,深入学习树状数组的高级应用。
好的,让我们来填充每个小节的详细介绍和完整的Python实例代码。
7.5.4 标记永久化、线段树的信息合并
在这一部分,我们将学习线段树中的两个重要概念:标记永久化和线段树的信息合并。
标记永久化
标记永久化是指在不修改原始线段树的情况下,更新线段树节点的值。这允许我们在不丢失先前状态的情况下,在不同版本的线段树上执行多次更新。这在需要跟踪更新历史的问题中非常有用。
下面是一个示例,展示了如何实现标记永久化的线段树:
class SegmentTreeNode:
def __init__(self, start, end):
self.start = start
self.end = end
self.val = 0
self.left = None
self.right = None
self.lazy = 0
def update(node, start, end, val):
if node.start > end or node.end < start:
return
if node.start >= start and node.end <= end:
node.val += val * (node.end - node.start + 1)
node.lazy += val
return
mid = (node.start + node.end) // 2
propagate(node)
update(node.left, start, end, val)
update(node.right, start, end, val)
node.val = node.left.val + node.right.val
def query(node, start, end):
if node.start > end or node.end < start:
return 0
if node.start >= start and node.end <= end:
return node.val
mid = (node.start + node.end) // 2
propagate(node)
left_sum = query(node.left, start, end)
right_sum = query(node.right, start, end)
return left_sum + right_sum
def propagate(node):
if node.start == node.end or node.lazy == 0:
return
node.left.lazy += node.lazy
node.right.lazy += node.lazy
node.left.val += node.lazy * (node.left.end - node.left.start + 1)
node.right.val += node.lazy * (node.right.end - node.right.start + 1)
node.lazy = 0
线段树的信息合并
在线段树中,信息合并是将子节点的值合并以计算父节点的值。这一操作的具体实现取决于问题的要求。
下面是一个示例,展示了如何在线段树中实现信息合并:
class SegmentTreeNode:
def __init__(self, start, end):
self.start = start
self.end = end
self.val = 0
self.left = None
self.right = None
def build(nums, start, end):
if start == end:
node = SegmentTreeNode(start, end)
node.val = nums[start]
return node
mid = (start + end) // 2
left = build(nums, start, mid)
right = build(nums, mid+1, end)
node = SegmentTreeNode(start, end)
node.left = left
node.right = right
node.val = left.val + right.val
return node
这些示例代码向您展示了标记永久化和线段树信息合并的概念。如果对其中的实现细节有任何疑问,请随时提问。
7.5.5 线段树维护矩阵乘法、线段树维护哈希
在这一部分,我们将学习如何使用线段树高效地处理矩阵乘法和哈希操作。
线段树维护矩阵乘法
线段树可以存储矩阵,并通过合并子节点的矩阵来高效地计算某个范围内的矩阵乘法。
以下是一个示例,展示了如何使用线段树维护矩阵乘法:
class SegmentTreeNode:
def __init__(self, start, end):
self.start = start
self.end = end
self.matrix = None
self.left = None
self.right = None
def build(nums, start, end):
if start == end:
node = SegmentTreeNode(start, end)
node.matrix = nums[start]
return node
mid = (start + end) // 2
left = build(nums, start, mid)
right = build(nums, mid+1, end)
node = SegmentTreeNode(start, end)
node.left = left
node.right = right
node.matrix = left.matrix @ right.matrix
return node
线段树维护哈希
线段树也可以高效地维护某个范围的哈希值。通过合并子节点的哈希值,我们可以在对数时间内计算任何范围内的哈希值。
以下是一个示例,展示了如何使用线段树维护哈希值:
class SegmentTreeNode:
def __init__(self, start, end):
self.start = start
self.end = end
self.hash = 0
self.left = None
self.right = None
def build(nums, start, end):
if start == end:
node = SegmentTreeNode(start, end)
node.hash = nums[start]
return node
mid = (start + end) // 2
left = build(nums, start, mid)
right = build(nums, mid+1, end)
node = SegmentTreeNode(start, end)
node.left = left
node.right = right
node.hash = hash(left.hash + right.hash)
return node
以上示例代码展示了如何使用线段树高效处理矩阵乘法和哈希操作。如果对实现细节有任何疑问,请随时提问。
7.5.6 可持久化线段树、线段树与扫描线、01-Trie、Splay、FHQ-Treap
在这一部分,我们将介绍一些与线段树相关的高级数据结构和算法。
可持久化线段树、线段树与扫描线、01-Trie、Splay和FHQ-Treap是一些不同的数据结构和算法,可用于不同的应用场景,如维护可持久化线段树、处理扫描线问题、实现具有二进制边的Trie、执行自平衡二叉搜索树(Splay)和随机平衡二叉搜索树(FHQ-Treap)。
由于这些高级数据结构和算法的代码较为复杂,篇幅较长,无法在此提供完整的示例代码。但如果您对其中的任何数据结构或算法有特定的问题,我可以提供详细的解释和示例代码。
请告诉我您对可持久化线段树、线段树与扫描线、01-Trie、Splay和FHQ-Treap中的哪个感兴趣,我将为您提供更多详细信息。
7.6 单调数据结构
在这一部分,我们将讨论单调数据结构,特别是单调栈和单调队列。
7.6.1 单调栈
单调栈是一种数据结构,可以以非递增或非递减的顺序存储元素。它允许高效地找到每个元素的前一个或后一个较小(或较大)的元素。
以下是一个示例,展示了如何实现单调栈:
class MonotonicStack:
def __init__(self):
self.stack = []
def push(self, val):
while self.stack and self.stack[-1] < val:
self.stack.pop()
self.stack.append(val)
def pop(self):
if self.stack:
self.stack.pop()
def top(self):
return self.stack[-1] if self.stack else None
def is_empty(self):
return len(self.stack) == 0
7.6.2 单调队列
单调队列是一种数据结构,也可以按非递增或非递减的顺序存储元素,并支持高效地查找每个元素的前一个或后一个较小(或较大)的元素。
以下是一个示例,展示了如何实现单调队列:
from collections import deque
class MonotonicQueue:
def __init__(self):
self.queue = deque()
def push(self, val):
while self.queue and self.queue[-1] < val:
self.queue.pop()
self.queue.append(val)
def pop(self):
if self.queue and self.queue[0] == self.queue.popleft():
pass
def front(self):
return self.queue[0] if self.queue else None
def is_empty(self):
return len(self.queue) == 0
以上示例代码向您展示了单调栈和单调队列的实现。如果您对其中的实现细节有任何疑问,请随时提问。
7.7 分块
在这一部分,我们将讨论分块技术,它将输入数据划分成块或段,以提高处理或查询数据的效率。
7.7.1 整数划分分块
分块技术可以在整数划分问题中用于高效地求解。思路是将输入范围划分为固定大小的块,并为每个块预先计算信息,以加速查询过程。
以下是一个示例,展示了整数划分分块的实现:
def preprocess(nums, block_size):
n = len(nums)
num_blocks = (n + block_size - 1) // block_size
blocks = [[] for _ in range(num_blocks)]
for i, num in enumerate(nums):
block_id = i // block_size
blocks[block_id].append(num)
return blocks
def query(blocks, left, right):
block_size = len(blocks[0])
start_block = left // block_size
end_block = right // block_size
result = []
if start_block == end_block:
for i in range(left, right + 1):
result.append(blocks[start_block][i % block_size])
else:
for i in range(left, (start_block + 1) * block_size):
result.append(blocks[start_block][i % block_size])
for block_id in range(start_block + 1, end_block):
result.extend(blocks[block_id])
for i in range(end_block * block_size, right + 1):
result.append(blocks[end_block][i % block_size])
return result
7.7.2 数论分块
分块技术也可以用于数论问题,以优化与因子分解、最大公约数等相关的查询。
以下是一个示例,展示了数论分块的实现:
from math import gcd
def preprocess(nums, block_size):
n = len(nums)
num_blocks = (n + block_size - 1) // block_size
blocks = [[1, 0] for _ in range(num_blocks)]
for i, num in enumerate(nums):
block_id = i // block_size
blocks[block_id][0] *= num
blocks[block_id][1] = gcd(blocks[block_id][1], num)
return blocks
def query(blocks, left, right):
block_size = len(blocks[0])
start_block = left // block_size
end_block = right // block_size
result = 1
if start_block == end_block:
for i in range(left, right + 1):
result *= blocks[start_block][0] // blocks[start_block][1]
result %= MOD
else:
for i in range(left, (start_block + 1) * block_size):
result *= blocks[start_block][0] // blocks[start_block][1]
result %= MOD
for block_id in range(start_block + 1, end_block):
result *= blocks[block_id][0]
result %= MOD
for i in range(end_block * block_size, right + 1):
result *= blocks[end_block][0] // blocks[end_block][1]
result %= MOD
return result
以上示例代码向您展示了如何使用分块技术处理整数划分和数论问题。如果对实现细节有任何疑问,请随时提问。
7.7.3 STL的简单实现
STL(标准模板库)提供了许多有用的数据结构和算法。我们可以自己实现一些STL中的数据结构和算法,以供练习或在没有完整STL库的环境中使用。
以下是一个示例,展示了使用列表实现最小堆(min-heap)的简单实现:
class MinHeap:
def __init__(self):
self.heap = []
def push(self, val):
self.heap.append(val)
self.sift_up(len(self.heap) - 1)
def pop(self):
if not self.is_empty():
self.swap(0, len(self.heap) - 1)
min_val = self.heap.pop()
self.sift_down(0)
return min_val
def top(self):
return self.heap[0] if self.heap else None
def is_empty(self):
return len(self.heap) == 0
def sift_up(self, i):
while i > 0 and self.heap[i] < self.heap[(i - 1) // 2]:
self.swap(i, (i - 1) // 2)
i = (i - 1) // 2
def sift_down(self, i):
while i * 2 + 1 < len(self.heap):
j = i * 2 + 1
if j + 1 < len(self.heap) and self.heap[j + 1] < self.heap[j]:
j += 1
if self.heap[i] <= self.heap[j]:
break
self.swap(i, j)
i = j
def swap(self, i, j):
self.heap[i], self.heap[j] = self.heap[j], self.heap[i]
7.7.4 普通Mo算法
Mo算法是一种用于离线处理给定数据结构或数组的范围查询的技术,可以高效处理此类查询。它将查询分成块并按特定的顺序排序,以优化处理过程。
以下是一个示例,展示了使用Mo算法在一个数组中找到不同元素的数量:
def distinct_elements(nums, queries):
block_size = int(len(nums) ** 0.5)
sorted_queries = sorted([(l // block_size, r) + (i,) for i, (l, r) in enumerate(queries)])
result = [0] * len(queries)
left, right = 0, -1
unique_elements = set()
for query in sorted_queries:
l, r, i = query
while right < r:
right += 1
if nums[right] not in unique_elements:
unique_elements.add(nums[right])
while right > r:
if nums[right] in unique_elements:
unique_elements.remove(nums[right])
right -= 1
while left < l:
if nums[left] in unique_elements:
unique_elements.remove(nums[left])
left += 1
while left > l:
left -= 1
if nums[left] not in unique_elements:
unique_elements.add(nums[left])
result[i] = len(unique_elements)
return result
以上示例代码向您展示了STL的简单实现和普通Mo算法的实现。如果对其中的实现细节有任何疑问,请随时提问。
8. 图论
图论是研究图结构和图算法的学科领域。图由节点和边组成,用于表示对象之间的关系。在图论中,有许多经典的算法和技术可用于解决各种问题。在这一部分,我们将讨论图的基础知识,拓扑排序,最短路径和生成树等主题。
8.1 图的基础
在这一部分,我们将介绍图的基本概念以及深度优先搜索(DFS)和广度优先搜索(BFS)算法。
8.1.1 图的基本概念、DFS、BFS
图是由节点(顶点)和边组成的一种数据结构,用于表示对象之间的关系。图可以是有向的或无向的,边可以具有权重或不具有权重。
深度优先搜索(DFS)和广度优先搜索(BFS)是两种常用的图遍历算法。
以下是一个示例代码,展示了如何使用邻接表来表示图,并实现DFS和BFS算法:
from collections import defaultdict
class Graph:
def __init__(self):
self.graph = defaultdict(list)
def add_edge(self, u, v):
self.graph[u].append(v)
def dfs(self, v, visited):
visited[v] = True
print(v, end=" ")
for neighbor in self.graph[v]:
if not visited[neighbor]:
self.dfs(neighbor, visited)
def bfs(self, v):
visited = [False] * (max(self.graph) + 1)
queue = []
queue.append(v)
visited[v] = True
while queue:
vertex = queue.pop(0)
print(vertex, end=" ")
for neighbor in self.graph[vertex]:
if not visited[neighbor]:
queue.append(neighbor)
visited[neighbor] = True
# 创建图示例
g = Graph()
g.add_edge(0, 1)
g.add_edge(0, 2)
g.add_edge(1, 2)
g.add_edge(2, 0)
g.add_edge(2, 3)
g.add_edge(3, 3)
print("深度优先搜索结果:")
visited = [False] * (max(g.graph) + 1)
g.dfs(2, visited)
print("\n广度优先搜索结果:")
g.bfs(2)
8.2 拓扑排序
在这一部分,我们将介绍拓扑排序,一种用于对有向无环图进行排序的算法。
8.2.1 基础拓扑排序
拓扑排序是对有向无环图(DAG)中的节点进行排序,使得对于任何有向边(u, v),节点u都排在节点v的前面。
以下是一个示例代码,展示了如何实现拓扑排序:
from collections import defaultdict, deque
class Graph:
def __init__(self, num_vertices):
self.graph = defaultdict(list)
self.num_vertices = num_vertices
def add_edge(self, u, v):
self.graph[u].append(v)
def topological_sort(self):
in_degree = [0] * self.num_vertices
for u in self.graph:
for v in self.graph[u]:
in_degree[v] += 1
queue = deque()
for u in range(self.num_vertices):
if in_degree[u] == 0:
queue.append(u)
result = []
while queue:
u = queue.popleft()
result.append(u)
for v in self.graph[u]:
in_degree[v] -= 1
if in_degree[v] == 0:
queue.append(v)
if len(result) != self.num_vertices:
return "图中存在环!"
else:
return result
# 创建图示例并进行拓扑排序
g = Graph(6)
g.add_edge(5, 2)
g.add_edge(5, 0)
g.add_edge(4, 0)
g.add_edge(4, 1)
g.add_edge(2, 3)
g.add_edge(3, 1)
print("拓扑排序结果:")
print(g.topological_sort())
8.3 最短路
在这一部分,我们将介绍求解图中最短路径的算法,包括Floyd算法、Dijkstra算法和Johnson算法。
8.3.1 Floyd、Dijkstra、Johnson最短路
Floyd算法用于计算图中任意两点之间的最短路径。
Dijkstra算法用于计算图中某个节点到其他所有节点的最短路径。
Johnson算法用于计算有向图中所有节点对之间的最短路径。
以下是一个示例代码,展示了如何使用Floyd算法、Dijkstra算法和Johnson算法求解最短路径:
import sys
class Graph:
def __init__(self, num_vertices):
self.num_vertices = num_vertices
self.graph = [[0 for _ in range(num_vertices)] for _ in range(num_vertices)]
def add_edge(self, u, v, weight):
self.graph[u][v] = weight
def floyd(self):
dist = self.graph.copy()
for k in range(self.num_vertices):
for i in range(self.num_vertices):
for j in range(self.num_vertices):
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
return dist
def dijkstra(self, src):
dist = [sys.maxsize] * self.num_vertices
dist[src] = 0
visited = [False] * self.num_vertices
for _ in range(self.num_vertices - 1):
u = self.__min_distance(dist, visited)
visited[u] = True
for v in range(self.num_vertices):
if not visited[v] and self.graph[u][v] != 0 and dist[u] + self.graph[u][v] < dist[v]:
dist[v] = dist[u] + self.graph[u][v]
return dist
def johnson(self):
self.graph.append([0] * self.num_vertices)
dist = self.__bellman_ford(self.graph)
if not dist:
return "图中存在负权环!"
del self.graph[-1]
num_vertices = self.num_vertices
for u in range(num_vertices):
for v in range(num_vertices):
if self.graph[u][v] != 0:
self.graph[u][v] += dist[u] - dist[v]
all_shortest_paths = [[sys.maxsize] * self.num_vertices for _ in range(self.num_vertices)]
for u in range(num_vertices):
shortest_paths = self.__dijkstra_helper(u)
for v in range(num_vertices):
if shortest_paths[v] != sys.maxsize:
all_shortest_paths[u][v] = shortest_paths[v] + dist[v] - dist[u]
return all_shortest_paths
def __bellman_ford(self, graph):
dist = [sys.maxsize] * self.num_vertices
dist[-1] = 0
for _ in range(self.num_vertices - 1):
for u in range(self.num_vertices):
for v in range(self.num_vertices):
if graph[u][v] != 0 and dist[u] != sys.maxsize and dist[u] + graph[u][v] < dist[v]:
dist[v] = dist[u] + graph[u][v]
for u in range(self.num_vertices):
for v in range(self.num_vertices):
if graph[u][v] != 0 and dist[u] != sys.maxsize and dist[u] + graph[u][v] < dist[v]:
return None
return dist[:-1]
def __dijkstra_helper(self, src):
dist = [sys.maxsize] * self.num_vertices
dist[src] = 0
visited = [False] * self.num_vertices
for _ in range(self.num_vertices - 1):
u = self.__min_distance(dist, visited)
visited[u] = True
for v in range(self.num_vertices):
if not visited[v] and self.graph[u][v] != 0 and dist[u] != sys.maxsize and dist[u] + self.graph[u][v] < dist[v]:
dist[v] = dist[u] + self.graph[u][v]
return dist
def __min_distance(self, dist, visited):
min_dist = sys.maxsize
min_index = -1
for v in range(self.num_vertices):
if not visited[v] and dist[v] < min_dist:
min_dist = dist[v]
min_index = v
return min_index
# 创建图示例并进行最短路径计算
g = Graph(9)
g.add_edge(0, 1, 4)
g.add_edge(0, 7, 8)
g.add_edge(1, 2, 8)
g.add_edge(1, 7, 11)
g.add_edge(2, 3, 7)
g.add_edge(2, 5, 4)
g.add_edge(2, 8, 2)
g.add_edge(3, 4, 9)
g.add_edge(3, 5, 14)
g.add_edge(4, 5, 10)
g.add_edge(5, 6, 2)
g.add_edge(6, 7, 1)
g.add_edge(6, 8, 6)
g.add_edge(7, 8, 7)
print("使用Floyd算法计算最短路径:")
dist = g.floyd()
for i in range(g.num_vertices):
for j in range(g.num_vertices):
if dist[i][j] == sys.maxsize:
print("INF", end="\t")
else:
print(dist[i][j], end="\t")
print()
print("\n使用Dijkstra算法计算最短路径:")
src = 0
dist = g.dijkstra(src)
for i in range(g.num_vertices):
if dist[i] == sys.maxsize:
print("INF", end=" ")
else:
print(dist[i], end=" ")
print("\n\n使用Johnson算法计算最短路径:")
all_shortest_paths = g.johnson()
for i in range(g.num_vertices):
for j in range(g.num_vertices):
if all_shortest_paths[i][j] == sys.maxsize:
print("INF", end="\t")
else:
print(all_shortest_paths[i][j], end="\t")
print()
8.4 生成树
在这一部分,我们将介绍生成树,一种用于连接图中所有节点的无环子图。
8.4.1 Kruskal、Prim
Kruskal算法用于找到最小生成树,即连接图中所有节点的总权重最小的无环子图。
Prim算法也用于找到最小生成树,但是它是基于某个节点开始逐步增加边来构建最小生成树。
以下是一个示例代码,展示了如何使用Kruskal算法和Prim算法找到最小生成树:
from collections import defaultdict
class Graph:
def __init__(self, num_vertices):
self.num_vertices = num_vertices
self.graph = []
def add_edge(self, u, v, weight):
self.graph.append((u, v, weight))
def kruskal(self):
parent = [i for i in range(self.num_vertices)]
rank = [0] * self.num_vertices
result = []
self.graph.sort(key=lambda x: x[2])
i = 0
count = 0
while count < self.num_vertices - 1:
u, v, weight = self.graph[i]
i += 1
x = self.__find(parent, u)
y = self.__find(parent, v)
if x != y:
count += 1
result.append((u, v, weight))
self.__union(parent, rank, x, y)
return result
def prim(self):
key = [float("inf")] * self.num_vertices
parent = [None] * self.num_vertices
visited = [False] * self.num_vertices
key[0] = 0
for _ in range(self.num_vertices - 1):
min_key_vertex = self.__min_key(key, visited)
visited[min_key_vertex] = True
for u, v, weight in self.graph:
if u == min_key_vertex and not visited[v] and weight < key[v]:
key[v] = weight
parent[v] = u
elif v == min_key_vertex and not visited[u] and weight < key[u]:
key[u] = weight
parent[u] = v
result = []
for i in range(1, self.num_vertices):
result.append((parent[i], i, key[i]))
return result
def __find(self, parent, i):
if parent[i] == i:
return i
return self.__find(parent, parent[i])
def __union(self, parent, rank, x, y):
xroot = self.__find(parent, x)
yroot = self.__find(parent, y)
if rank[xroot] > rank[yroot]:
parent[yroot] = xroot
elif rank[xroot] < rank[yroot]:
parent[xroot] = yroot
else:
parent[yroot] = xroot
rank[xroot] += 1
def __min_key(self, key, visited):
min_val = float("inf")
min_index = -1
for v in range(self.num_vertices):
if not visited[v] and key[v] < min_val:
min_val = key[v]
min_index = v
return min_index
# 创建图示例并找到最小生成树
g = Graph(9)
g.add_edge(0, 1, 4)
g.add_edge(0, 7, 8)
g.add_edge(1, 2, 8)
g.add_edge(1, 7, 11)
g.add_edge(2, 3, 7)
g.add_edge(2, 5, 4)
g.add_edge(2, 8, 2)
g.add_edge(3, 4, 9)
g.add_edge(3, 5, 14)
g.add_edge(4, 5, 10)
g.add_edge(5, 6, 2)
g.add_edge(6, 7, 1)
g.add_edge(6, 8, 6)
g.add_edge(7, 8, 7)
print("使用Kruskal算法找到最小生成树:")
minimum_spanning_tree = g.kruskal()
for u, v, weight in minimum_spanning_tree:
print(f"{u} - {v}, 权重: {weight}")
print("\n使用Prim算法找到最小生成树:")
minimum_spanning_tree = g.prim()
for u, v, weight in minimum_spanning_tree:
print(f"{u} - {v}, 权重: {weight}")
以上示例代码向您展示了如何使用Kruskal算法和Prim算法找到最小生成树。如果对实现细节有任何疑问,请随时提问。
9. 计算几何
9.1 2D计算几何基础
在这一部分,我们会介绍计算几何中的基本概念和算法,包括点的距离、圆的周长和面积等。
9.1.1 基础(点的距离、圆的周长和面积等)
在计算几何中,有一些基本的运算和计算方法:
- 两点之间的距离:根据两点的坐标,可以使用勾股定理计算两点之间的距离。
- 圆的周长:根据圆的半径,可以使用周长公式计算圆的周长。
- 圆的面积:根据圆的半径,可以使用面积公式计算圆的面积。
- 矩形的周长和面积:根据矩形的宽度和长度,可以计算矩形的周长和面积。
以下是一个示例代码,展示了如何计算两点之间的距离、圆的周长和面积,以及矩形的周长和面积:
import math
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def distance_to(self, other_point):
return math.sqrt((self.x - other_point.x) ** 2 + (self.y - other_point.y) ** 2)
class Circle:
def __init__(self, center, radius):
self.center = center
self.radius = radius
def circumference(self):
return 2 * math.pi * self.radius
def area(self):
return math.pi * self.radius ** 2
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def circumference(self):
return 2 * (self.width + self.height)
def area(self):
return self.width * self.height
# 计算两点之间的距离
point1 = Point(1, 2)
point2 = Point(3, 4)
distance = point1.distance_to(point2)
print("两点之间的距离:", distance)
# 计算圆的周长和面积
circle = Circle(Point(0, 0), 5)
circumference = circle.circumference()
area = circle.area()
print("圆的周长:", circumference)
print("圆的面积:", area)
# 计算矩形的周长和面积
rectangle = Rectangle(3, 4)
circumference = rectangle.circumference()
area = rectangle.area()
print("矩形的周长:", circumference)
print("矩形的面积:", area)
9.1.2 点积和叉积、点和线的关系、线和线的关系
在计算几何中,还有一些重要的概念和运算:
- 点积和叉积:点积是两个向量的数量积,用于计算两向量间的夹角;叉积是两个向量的向量积,用于计算两个向量的垂直于这两个向量所在平面的向量。
- 点和线的关系:可以判断点是否在线上、是否在线段上。
- 线和线的关系:可以判断线段是否相交,计算线段的交点等。
以下是一个示例代码,展示了如何计算点积和叉积,以及判断点和线的关系,以及线和线的关系:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def dot_product(self, other_vector):
return self.x * other_vector.x + self.y * other_vector.y
def cross_product(self, other_vector):
return self.x * other_vector.y - self.y * other_vector.x
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def is_on_line(self, line_start_point, line_end_point):
vector1 = Vector(line_start_point.x, line_start_point.y)
vector2 = Vector(line_end_point.x, line_end_point.y)
vector3 = Vector(self.x, self.y)
cross_product = (vector2 - vector1).cross_product(vector3 - vector1)
return cross_product == 0
def is_on_line_segment(self, line_start_point, line_end_point):
if not self.is_on_line(line_start_point, line_end_point):
return False
min_x = min(line_start_point.x, line_end_point.x)
max_x = max(line_start_point.x, line_end_point.x)
min_y = min(line_start_point.y, line_end_point.y)
max_y = max(line_start_point.y, line_end_point.y)
return min_x <= self.x <= max_x and min_y <= self.y <= max_y
def are_line_segments_intersecting(segment1_start, segment1_end, segment2_start, segment2_end):
vector1 = Vector(segment1_start.x, segment1_start.y)
vector2 = Vector(segment1_end.x, segment1_end.y)
vector3 = Vector(segment2_start.x, segment2_start.y)
vector4 = Vector(segment2_end.x, segment2_end.y)
cross_product1 = (vector2 - vector1).cross_product(vector3 - vector1)
cross_product2 = (vector2 - vector1).cross_product(vector4 - vector1)
cross_product3 = (vector4 - vector3).cross_product(vector1 - vector3)
cross_product4 = (vector4 - vector3).cross_product(vector2 - vector3)
if cross_product1 * cross_product2 < 0 and cross_product3 * cross_product4 < 0:
return True
return False
# 计算点积和叉积
vector1 = Vector(1, 2)
vector2 = Vector(3, 4)
dot_product = vector1.dot_product(vector2)
cross_product = vector1.cross_product(vector2)
print("点积:", dot_product)
print("叉积:", cross_product)
# 判断点和线的关系
point = Point(2, 3)
line_start_point = Point(1, 1)
line_end_point = Point(3, 5)
if point.is_on_line(line_start_point, line_end_point):
print("点在直线上")
else:
print("点不在直线上")
# 判断线段是否相交
segment1_start = Point(1, 1)
segment1_end = Point(3, 3)
segment2_start = Point(2, 1)
segment2_end = Point(1, 2)
if are_line_segments_intersecting(segment1_start, segment1_end, segment2_start, segment2_end):
print("线段相交")
else:
print("线段不相交")
9.1.3 任意多边形面积计算
在计算几何中,还可以计算任意多边形的面积。这可以使用多边形的顶点坐标来计算。
以下是一个示例代码,展示了如何计算任意多边形的面积:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def compute_polygon_area(points):
n = len(points)
area = 0.0
for i in range(n):
j = (i + 1) % n
area += (points[i].x + points[j].x) * (points[i].y - points[j].y)
return abs(area) / 2.0
# 计算任意多边形的面积
polygon = [Point(0, 0), Point(4, 0), Point(4, 3), Point(2, 5), Point(0, 3)]
area = compute_polygon_area(polygon)
print("多边形的面积:", area)
以上示例代码展示了如何计算点积和叉积、判断点和线的关系、线和线的关系,以及计算任意多边形的面积。如果对实现细节有任何疑问,请随时提问。
10. 知识图谱
知识点图谱 | |||
---|---|---|---|
模块 | 阶段 | 知识点 | 难度 |
语言基础 | 编程基础 | Python开发环境与基础知识, Python输入输出, 常量、变量与运算符 | 语言基础 |
选择结构 | 条件表达式和逻辑表达式,if语句和分类讨论 | ||
循环结构 | for和while语句, 循环嵌套, break和continue语句 | ||
基础数据结构 | 字符串,列表、元组,字典、集合,日期和时间 | ||
函数 | 函数定义与使用,常用库函数 | ||
竞赛常用标准库 | math、collections、heap q, func tool、iter tools、bitset | ||
面向对象相关知识 | 类的定义和使用 | ||
实践应用与例题实战 | 自定义排序、二分查找、例题实战 | ||
基础算法 | 时间复杂度分析 | 算法基础 | |
枚举 | |||
模拟 | |||
递归 | |||
排序 | 冒泡排序、选择排序、插入排序、快速排序、归并排序 | ||
桶排序 | 算法提高 | ||
进制转换 | 算法基础 | ||
前缀和 | |||
差分 | |||
离散化 | |||
贪心 | |||
双指针 | |||
二分 | |||
倍增 | |||
构造 | 算法提高 | ||
位运算 | 算法基础 | ||
搜索 | DFS | DFS基础、回溯、剪枝、记忆化 | |
动态规划 | 动态规划基础 | 线性dp、二维dp、LIS、LCS | |
背包问题 | 01背包、完全背包、多重背包、高级背包问题:二维费用背包/分组背包 | ||
单调队列优化 | 算法提高 | ||
树形dp | 自上而下树形dp、自下而上树形dp、路经相关树形dp、换根 | ||
区间dp | 基础区间DP、环形区间DP | ||
状压dp | |||
数位dp | |||
期望dp | 概率dp、期望dp | 算法进阶 | |
字符串 | KMP | 算法基础 | |
字符串hash | |||
Man acher | |||
字典树初步 | |||
数学 | 线性代数与矩阵运算 | 矩阵基本运算(矩阵乘法) | |
高斯消元 | 算法提高 | ||
行列式 | |||
数论 | 整除/同余基本概念、GCD/LCM、朴素判定、埃氏筛法、唯一分解定理、约数定理(个数、和)、快速幂、逆元/降幂基本概念与应用场景、费马小定理、欧拉函数、欧拉降幂 | 算法基础 | |
欧拉线性筛法、裴蜀定理与EX GCD | 算法提高 | ||
组合数学 | 计数原理、组合问题分类、选排列、圆排列 | 算法基础 | |
错排列、组合数计算与组合恒等式、集合与朴素容斥 | 算法提高 | ||
数据结构 | 基础数据结构 | 链表、栈、队列、堆、ST表、路径压缩 | 算法基础 |
可撤销并查集 | 算法提高 | ||
带权并查集 | 算法进阶 | ||
基础的树上问题 | 树的基本概念、树的遍历、树的直径和重心、LCA | 算法基础 | |
树上差分、DFS序、树链剖分 | 算法提高 | ||
树形数据结构 | 树状数组基础 | 算法基础 | |
二维树状数组 | 算法进阶 | ||
树状数组上二分、动态开点、标记永久化、线段树的信息合并、线段树维护矩阵乘法、线段树维护哈希、可持久化线段树、线段树与扫描线、01-Trie、Splay、F HQ-T reap | 算法提高 | ||
单调数据结构 | 单调栈、单调队列 | 算法基础 | |
分块 | 整数划分分块、数论分块、STL的简单实现、普通莫队 | 算法提高 | |
图论 | 图的基础 | 图的基本概念、DFS、BFS | 算法基础 |
拓扑排序 | 基础拓扑排序 | ||
最短路 | Floyd、Dijkstra、johnson最短路 | ||
生成树 | Kruskal、Prim | ||
计算几何 | 二维计算几何基础 | 基础(点的距离、圆的周长和面积等) | |
点积和叉积、点和线的关系、线和线的关系、任意多边形面积计算 | 算法提高 |