1、动态规划
动态规划是运筹学的一个分支,通常用来解决多阶段决策过程最优化问题。动态规划的基本想法就是将原问题转换为一系列相互联系的子问题,然后通过逐层地推来求得最后的解。
组成部分:
(以背包问题为例)
步骤一:确定状态
状态是指某一阶段问题的一个描述,它应该包含解决问题所需要的信息。在动态规划中,我们需要确定一个或多个状态变量,这些变量能够描述问题的每一个子问题的特征。
例如,在解决背包问题时,状态可以定义为dp[i][j]
,表示在前i
件物品中选择,使得总重量不超过j
的情况下,背包能够达到的最大价值。
步骤二:转移方程
转移方程(或称为状态转移方程)描述了状态之间如何转移,即如何从一个或多个已知状态推导出下一个状态。
继续以背包问题为例,转移方程可以是:
如果不选择第i
件物品,则dp[i][j] = dp[i-1][j]
如果选择第i
件物品,则dp[i][j] = dp[i-1][j-weights[i]] + values[i]
其中,weights
数组存储物品的重量,values
数组存储物品的价值。
转移方程通常是通过“最优子结构”来确定的,即问题的最优解包含了其子问题的最优解。
步骤三:初始条件和边界情况
在应用转移方程之前,需要定义初始状态和边界条件,这些是递推的起点。
对于背包问题,初始条件可能是:
dp[0][...] = 0
,即没有物品时,背包价值为0。
dp[...][0] = 0
,即背包容量为0时,背包价值也为0。
边界情况可能包括物品的重量大于当前背包容量时的处理等。
步骤四:计算顺序
计算顺序是指按照什么样的顺序来计算状态,确保在计算dp[i][j]
时,它所依赖的状态已经被计算过。
在背包问题中,计算顺序通常是先物品后容量,即:
for i in range(1, n+1):
for j in range(1, W+1):
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weights[i]] + values[i] if j >= weights[i] else 0)
例题
假设你是一名盗贼,你有一个背包,最多能承载W
重量的物品。现在你面前有n
件物品,每件物品有自己的重量weights[i]
和价值values[i]
。你希望在不超出背包承载重量的前提下,尽可能多地装入物品,使得背包内物品的总价值最大。
输入
n
:物品数量
W
:背包的最大承重
weights
:长度为n
的数组,表示每个物品的重量
values
:长度为n
的数组,表示每个物品的价值
输出
max_value
:背包能装下的物品的最大价值
动态规划建模过程
- 确定状态:
dp[i][j]
表示考虑前i
件物品,在容量为j
的背包中能装入的最大价值。 - 转移方程:
- 如果不选择第
i
件物品,则dp[i][j] = dp[i-1][j]
- 如果选择第
i
件物品,则dp[i][j] = dp[i-1][j-weights[i]] + values[i]
- 如果不选择第
- 初始条件和边界情况:
dp[0][...] = 0
,没有物品时价值为0dp[...][0] = 0
,背包容量为0时价值为0
- 计算顺序:先物品后背包容量
def knapsack(n, W, weights, values):
# 初始化dp数组,大小为(n+1) x (W+1),初始值为0
dp = [[0 for _ in range(W+1)] for _ in range(n+1)]
# 遍历物品
for i in range(1, n+1):
# 遍历背包容量
for j in range(1, W+1):
# 如果当前物品重量小于等于背包容量,可以选择该物品
if weights[i-1] <= j:
# 选择当前物品和不选择当前物品的最大值
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weights[i-1]] + values[i-1])
else:
# 当前物品重量大于背包容量,不选择该物品
dp[i][j] = dp[i-1][j]
# 返回背包能装下的物品的最大价值
return dp[n][W]
# 示例输入
n = 4 # 物品数量
W = 7 # 背包最大承重
weights = [1, 3, 4, 5] # 每个物品的重量
values = [1, 4, 5, 7] # 每个物品的价值
# 计算最大价值
max_value = knapsack(n, W, weights, values)
print("最大价值为:", max_value)
这段代码中,knapsack
函数实现了0-1背包问题的动态规划解法。我们首先创建了一个二维数组dp
,用于存储每个子问题的解。然后,我们通过两层循环来填充这个数组,外层循环遍历物品,内层循环遍历背包容量。在填充数组的过程中,我们使用转移方程来确定每个状态的最大价值。最后,dp[n][W]
就是背包能装下的物品的最大价值。
2、图论
图论( Graph Theory )是数学的一个分支。它以图为研究对象。图论中的图是由若干给定的点及连接两点的线所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系,用点代表事物,用连接两点的线表示相应两个事物间具有这种关系。 数学建模中的图,是根据实际问题简化而来的模型,对于平面上的n个点,把其中一些点用直线和曲线连接起来,不考虑所画点的位置和所画直线或曲线的长度,这样形成的关系称为“图”。
图论中,每个节点称为顶点,连线称为边,边上标出的数字称 为边的权重(权重和线段画出来的长度无关)
图中的边如果是单向箭头,表示有方向,构成的图称为有向图,如示例中的城市路线,是有方向的 ;如果图里的边没有箭头,只有线,表示双向,构成的图称为无向图,比如通信光缆之类 ;权重不仅可以表示长度之类的,也可以表示时间、费用等概念。
无向图代码实现
class Graph:
def __init__(self):
# 初始化一个空图,使用字典来存储图的边信息
self.graph = {}
def add_edge(self, u, v, w=1):
# 添加边(u, v)到图中,权重为w
# 如果节点u已经在图中,则添加v作为u的邻居
if u in self.graph:
self.graph[u].append((v, w))
else:
self.graph[u] = [(v, w)]
# 由于是无向图,节点v也需要添加节点u作为邻居
if v in self.graph:
self.graph[v].append((u, w))
else:
self.graph[v] = [(u, w)]
def get_neighbors(self, node):
# 获取节点node的所有邻居节点及其权重
return self.graph.get(node, [])
# 示例:表示城市之间的道路连接
graph = Graph()
graph.add_edge('A', 'B', 5) # 城市A和城市B之间有道路,距离为5
graph.add_edge('B', 'C', 3) # 城市B和城市C之间有道路,距离为3
graph.add_edge('A', 'C', 1) # 城市A和城市C之间有道路,距离为1
graph.add_edge('C', 'D', 4) # 城市C和城市D之间有道路,距离为4
# 打印节点A的邻居
print(graph.get_neighbors('A'))
有向图代码实现
class Digraph:
def __init__(self):
# 初始化一个空有向图,使用字典来存储图的边信息
self.graph = {}
def add_edge(self, u, v, w=1):
# 添加有向边u -> v到图中,权重为w
if u in self.graph:
self.graph[u].append((v, w))
else:
self.graph[u] = [(v, w)]
def get_neighbors(self, node):
# 获取节点node的所有出边邻居节点及其权重
return self.graph.get(node, [])
# 示例:表示航班路线
digraph = Digraph()
digraph.add_edge('A', 'B', 100) # 从城市A到城市B有航班,距离为100
digraph.add_edge('A', 'C', 200) # 从城市A到城市C有航班,距离为200
digraph.add_edge('B', 'C', 50) # 从城市B到城市C有航班,距离为50
digraph.add_edge('C', 'D', 75) # 从城市C到城市D有航班,距离为75
# 打印从节点A出发的航班
print(digraph.get_neighbors('A'))
通过图论等方法可以后续解决求解最短路径、路径规划等问题。