目录
3.POJ3258、POJ3104、POJ1759、POJ1064
3. POJ1759 - Climbing the Hill
一、二分搜索
1.整数上的二分
算法步骤
-
初始化:设定两个指针,
left
指向数组的起始位置,right
指向数组的结束位置。 -
循环查找:
- 计算中间位置
mid = left + (right - left) // 2
。 - 如果
nums[mid] = = target
,则找到目标元素,返回其下标。 - 如果
nums[mid] < target
,说明目标元素在mid
右侧,则更新left = mid + 1
。 - 如果
nums[mid] > target
,说明目标元素在mid
左侧,则更新right = mid - 1
。
- 计算中间位置
-
终止条件:当
left > right
时,查找结束。如果找不到目标元素,通常返回 -1 或其他标志值。
代码实现(Python)
def binary_search(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1 # 表示未找到目标元素
复杂度分析
- 时间复杂度:O(log n),每次查找范围缩小一半。
- 空间复杂度:O(1),只使用了常数级别的额外空间。
示例
nums = [1, 2, 4, 5, 7, 9, 12]
target = 5
result = binary_search(nums, target)
print(result) # 输出: 3
2.实数上的二分
在实数域上进行二分查找,通常用于求解函数的根、极值、或其他需要精确求解的数值问题。与离散整数上的二分查找不同,实数上的二分查找需要在一定的精度范围内找到一个解,而不是精确的索引。
应用场景
- 求解方程的根,如 f(x)=0
- 找到函数的极值点
- 找到某个条件下的最优解
算法步骤
-
初始化:确定实数范围的两个端点
left
和right
,它们之间包含目标值或解。 -
设定精度:由于实数是连续的,设定一个精度值
epsilon
,当区间范围小于epsilon
时,认为找到了解。 -
循环查找:
- 计算中间点
mid = (left + right) / 2
。 - 根据问题的性质,判断中间点处的值与目标值或目标条件的关系,选择下一步缩小的区间:
- 如果
f(mid)
满足条件(例如足够接近 0),则认为找到了解,退出循环。 - 如果
f(mid)
不满足条件,则根据f(mid)
与f(left)
或f(right)
的关系,确定下一个查找的区间。 - 更新区间的左端点或右端点。
- 如果
- 计算中间点
-
终止条件:当
right - left < epsilon
时,查找结束。返回mid
作为结果。
代码实现(Python示例)
以下是求函数 f(x)=x²−2的根,也就是求√2的近似值。
def binary_search_real(f, left, right, epsilon=1e-7):
while right - left > epsilon:
mid = (left + right) / 2
if f(mid) == 0: # 精确找到根
return mid
elif f(mid) < 0: # 根在右半边
left = mid
else: # 根在左半边
right = mid
return (left + right) / 2 # 返回近似解
# 求解 x^2 - 2 = 0 的根,即 √2
result = binary_search_real(lambda x: x**2 - 2, 1, 2)
print(result) # 输出接近 √2 的值
复杂度分析
- 时间复杂度:O(log(1/epsilon)),每次查找将区间缩小一半,直至满足精度要求。
- 空间复杂度:O(1),只使用了常数级别的额外空间。
示例说明
-
在这个例子中,我们通过二分查找来逼近。假设我们知道 2\sqrt{2}2 位于区间 [1, 2],二分查找逐步缩小区间,直到找到一个足够接近的值。
-
设置的
epsilon
决定了最终结果的精度。若需要更高的精度,可以设置更小的epsilon
。
3.POJ3258、POJ3104、POJ1759、POJ1064
1. POJ3258 - River Hopscotch
题目简介
给定一条河流,河上有一些石头以及起点和终点。你可以移走最多 m
个石头,求移除后的石头之间的最小距离的最大值。
解题思路
这是一个典型的二分查找+贪心算法的问题。问题可以转化为:如何找到一个最大的最小距离,使得在移除最多 m
个石头的情况下,石头之间的最小距离仍然不小于该值。
具体步骤如下:
- 二分查找:最小距离的上下界可以通过石头间的最小间隔和最大间隔确定。
- 贪心判断:在每次二分的过程中,通过贪心算法计算在当前距离下是否可以满足条件。
代码示例(Python)
def can_place_stones(min_dist, stones, m):
last_position = 0 # 起点
remove_count = 0 # 已经移除的石头数量
for i in range(1, len(stones)):
if stones[i] - stones[last_position] < min_dist:
remove_count += 1 # 移除这个石头
if remove_count > m:
return False
else:
last_position = i # 更新最近的石头位置
return True
def max_min_distance(stones, m):
left, right = 0, stones[-1] - stones[0]
while left <= right:
mid = (left + right) // 2
if can_place_stones(mid, stones, m):
left = mid + 1 # 尝试更大的最小距离
else:
right = mid - 1 # 减小最小距离
return right
# 输入:石头位置和可移除石头数
stones = [0, 2, 11, 14, 17, 21, 25] # 示例输入
m = 2 # 可以移除的石头数
print(max_min_distance(stones, m)) # 输出最大最小距离
2. POJ3104 - Drying
题目简介
有 n
件衣服,每件衣服初始时都有一定的湿度。现在可以使用一种自然晾干方式,每单位时间衣服的湿度减少 1;也可以使用一种特殊晾干机,每单位时间可以使衣服的湿度减半。求使所有衣服干燥的最少时间。
解题思路
这也是一个典型的二分查找+贪心算法问题。我们可以通过二分搜索答案,即找到一个时间值 T
,判断在该时间内能否将所有衣服晾干。
- 二分查找:我们假设一个最少时间
T
,然后判断在该时间内是否能把所有衣服晾干。 - 贪心判断:对于每件衣服,如果自然晾干不能在
T
时间内完成,那么需要使用特殊晾干机,并计算需要使用多少次特殊晾干机才能使衣服干燥。
代码示例(Python)
def can_dry_all_clothes(T, clothes, k):
extra_time = 0
for wetness in clothes:
if wetness > T:
extra_time += (wetness - T + k - 2) // (k - 1)
if extra_time > T:
return False
return True
def min_drying_time(clothes, k):
left, right = 0, max(clothes)
while left < right:
mid = (left + right) // 2
if can_dry_all_clothes(mid, clothes, k):
right = mid # 尝试更少的时间
else:
left = mid + 1 # 增加时间
return left
# 输入:衣服湿度和晾干机效果
clothes = [10, 20, 30] # 示例输入
k = 2 # 每次减半
print(min_drying_time(clothes, k)) # 输出最少晾干时间
3. POJ1759 - Climbing the Hill
题目简介
给定一个长度为 n
的数组,数组的第一个元素 a1
已知。你需要找出数组的第二个元素 a2
,使得数组中的所有元素均为正数,并且最大值最小。
解题思路
这是一个二分查找和模拟的问题。通过二分查找确定 a2
的最小值,然后依次推导后续元素,判断是否所有元素为正且满足要求。
代码示例(Python)
def is_valid_sequence(a1, a2, n):
heights = [0] * n
heights[0], heights[1] = a1, a2
for i in range(2, n):
heights[i] = 2 * heights[i - 1] - heights[i - 2]
if heights[i] <= 0:
return False
return True
def find_minimum_maximum(n, a1):
left, right = 0, 2 * a1
while right - left > 1e-7:
mid = (left + right) / 2
if is_valid_sequence(a1, mid, n):
right = mid
else:
left = mid
return right
# 输入:数组长度和第一个元素
n = 5 # 示例输入
a1 = 10 # 第一个元素
print(find_minimum_maximum(n, a1)) # 输出最大最小值
4. POJ1064 - Cable Master
题目简介
给定 n
条电缆,需要将这些电缆切割成长度相同的若干段,使得至少能得到 k
段。求可以得到的电缆最大长度。
解题思路
这是典型的二分查找问题。可以通过二分搜索电缆的长度,找到最大值。
- 二分查找:在最大长度和最小长度之间搜索可以切割的最大长度。
- 贪心判断:对于每次二分确定的长度,判断是否能切割出
k
段。
代码示例(Python)
def can_cut_cables(length, cables, k):
count = 0
for cable in cables:
count += cable // length
return count >= k
def max_cable_length(cables, k):
left, right = 0, max(cables)
while right - left > 1e-7:
mid = (left + right) / 2
if can_cut_cables(mid, cables, k):
left = mid # 尝试更大的长度
else:
right = mid # 减小长度
return left
# 输入:电缆长度数组和目标段数
cables = [4, 7, 9] # 示例输入
k = 5 # 需要得到的段数
print(max_cable_length(cables, k)) # 输出最大长度
二、深度优先搜索
深度优先搜索(Depth-First Search, DFS)是一种用于遍历或搜索图或树数据结构的算法。DFS 尽可能深地搜索树或图的分支,直到无法继续为止,然后回溯并继续探索其他分支。这种算法可以用来解决路径查找、连通性判断、拓扑排序等问题。
1.回潮法、子集树、m叉树、排列树
1. 回潮法(Backtracking)
概念
回潮法(又称为回溯法,Backtracking)是一种通过探索所有可能的解来找到问题的解法,适用于解决组合问题。其核心思想是在尝试解决问题时,逐步构建解,并在发现当前部分解无法扩展为有效解时,撤销该部分解(即“回溯”),然后尝试其他可能的路径。
应用场景
- 组合问题:如求解排列、组合、子集。
- 排列问题:如八皇后问题、数独问题。
- 图搜索问题:如 Hamilton 回路、迷宫问题。
典型算法结构
回溯法可以用递归来实现。每次递归调用时,尝试扩展当前解,如果发现扩展后的解非法,则回溯。
def backtrack(solution):
if solution is a complete solution:
process(solution)
return
for option in possible_options:
if option is valid:
solution.add(option)
backtrack(solution)
solution.remove(option) # 回溯
2. 子集树(Subset Tree)
概念
子集树是一种用于求解子集问题的搜索树,通常用于组合问题中。例如,给定一个集合,要求出所有可能的子集。每个节点表示是否包含集合中的某个元素。
特点
- 二叉树结构:子集树通常是一棵二叉树,每个节点有两个子节点,分别表示包含或不包含某个元素。
- 树的深度:如果集合的大小为
n
,那么树的深度为n
,树的叶子节点数为2^n
。
应用场景
- 求子集:找到集合的所有子集。
- 背包问题:通过子集树来求解背包问题的可行解。
例子
假设集合为 {a, b, c}
,子集树的部分结构如下:
{}
/ \
{a} {}
/ \ / \
{a,b} {a} {b} {}
/ \ / \ / \ / \
{a,b,c}... {a}...
3. m 叉树(m-ary Tree)
概念
m
叉树是一种树形结构,其中每个节点最多有 m
个子节点。m
叉树是一种广义的树结构,二叉树(m=2
)是它的特例。m 叉树广泛应用于文件系统、数据库索引等场景。
特点
- 节点:每个节点最多有
m
个子节点。 - 递归结构:每个子树也是一棵
m
叉树。
常见类型
- 完全 m 叉树:每个节点都有
m
个子节点,除最后一层的叶子节点外。 - 平衡 m 叉树:所有叶子节点的深度相同或相差不大。
应用场景
- B 树和 B+ 树:用于数据库和文件系统的索引。
- 游戏 AI:搜索树用于决策(如 Minimax 算法)。
4. 排列树(Permutation Tree)
概念
排列树是一种用于生成集合中所有排列的搜索树。在排列树中,每个节点表示某个元素被放置在排列中的某个位置,搜索树的深度等于排列的长度。
特点
- 深度优先遍历:通常通过深度优先遍历(DFS)来构建排列树,递归生成排列。
- 剪枝:可以通过剪枝来减少不必要的排列生成,优化搜索效率。
应用场景
- 全排列生成:列出集合中元素的所有排列。
- 旅行商问题:通过排列树来生成所有路径,寻找最优路径。
例子
假设集合为 {a, b, c}
,其排列树结构如下:
{}
/ | \
a b c
/ \ / \ / \
ab ac ba bc ca cb
| | | | | |
abc acb bac bca cab cba
5.总结
- 回潮法:用于解决组合问题的通用方法,通过递归搜索并在失败时回溯。
- 子集树:用于生成集合子集的二叉树结构。
- m 叉树:通用的树结构,每个节点最多有
m
个子节点,广泛应用于数据库和文件系统。 - 排列树:用于生成集合排列的搜索树结构,通常与深度优先搜索结合。
2.DFS 的特点
- 递归/栈实现:DFS 可以通过递归或显式使用栈来实现。
- 回溯:在探索完一条路径后,DFS 会回溯到上一个节点,继续探索未访问的路径。
3.DFS 算法的应用
- 路径查找:寻找从起点到终点的路径。
- 连通性检测:判断图是否连通,或找到连通分量。
- 拓扑排序:在有向无环图(DAG)中,确定节点的线性顺序。
- 迷宫求解:探索所有可能的路径,找到出口。
4.DFS 的实现
DFS 可以应用于图或树,我们以图为例,假设图用邻接表表示。
1. 递归实现
def dfs(graph, node, visited):
if node in visited: # 如果节点已经访问过,返回
return
visited.add(node) # 将节点标记为已访问
print(node) # 打印当前访问的节点(或进行其他操作)
for neighbor in graph[node]: # 访问当前节点的所有邻居
dfs(graph, neighbor, visited) # 对每个邻居递归调用 DFS
2. 使用栈的迭代实现
def dfs_iterative(graph, start):
visited = set() # 存储已访问节点
stack = [start] # 使用栈存储待访问节点
while stack:
node = stack.pop() # 弹出栈顶元素
if node not in visited:
print(node) # 打印当前访问的节点(或进行其他操作)
visited.add(node) # 将节点标记为已访问
for neighbor in graph[node]: # 将未访问的邻居加入栈
if neighbor not in visited:
stack.append(neighbor)
5.示例
给定一个图,如下所示:
A -- B -- D
| |
C E
使用邻接表表示图:
graph = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A'],
'D': ['B'],
'E': ['B']
}
执行 DFS:
visited = set()
dfs(graph, 'A', visited)
输出可能是:
A
B
D
E
C
或其他等效的顺序,具体取决于邻居的访问顺序。
6.复杂度分析
- 时间复杂度:O(V + E),其中 V 是图中的节点数,E 是边数。DFS 会访问每个节点和每条边一次。
- 空间复杂度:O(V),递归调用栈或显式栈需要保存当前路径中的节点,最坏情况下需要 O(V) 的空间。
7.总结
深度优先搜索是一种强大且灵活的图遍历算法,适用于多种问题场景。它的递归和迭代版本都有广泛的应用,尤其在图的遍历、路径搜索和连通性判断等方面表现出色。
三、广度优先搜索
广度优先搜索(Breadth-First Search, BFS)是一种用于遍历或搜索图或树数据结构的算法。与深度优先搜索不同,BFS 是从根节点开始,首先遍历其所有邻居节点,然后再遍历每个邻居的邻居节点,层层向外扩展,直到找到目标节点或遍历完所有节点。
BFS 的特点
- 队列实现:BFS 通常使用队列来实现,队列用于存储待访问的节点。
- 层次遍历:BFS 会逐层遍历图中的节点,先访问距离起点最近的节点,然后逐步扩展到更远的节点。
- 最短路径:在无权图中,BFS 能够找到从起点到终点的最短路径,因为它按层次遍历,每层的距离都比前一层大一。
BFS 的应用
- 最短路径:在无权图中,BFS 可以找到从起点到终点的最短路径。
- 连通性检测:判断图是否连通,或找到连通分量。
- 遍历图的每一层:用于分层遍历或查找。
- 解决迷宫问题:在网格图中,寻找从起点到终点的最短路径。
BFS 的实现
假设图用邻接表表示,BFS 的基本实现如下:
from collections import deque
def bfs(graph, start):
visited = set() # 用于存储已访问的节点
queue = deque([start]) # 初始化队列,起点入队
while queue:
node = queue.popleft() # 从队列中取出第一个节点
if node not in visited:
print(node) # 处理当前节点(例如打印节点)
visited.add(node) # 标记节点为已访问
for neighbor in graph[node]: # 遍历当前节点的邻居
if neighbor not in visited:
queue.append(neighbor) # 邻居节点入队
给定一个图,如下所示:
A -- B -- D | | C E
使用邻接表表示图:
graph = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A'],
'D': ['B'],
'E': ['B']
}
执行 BFS:
bfs(graph, 'A')
输出可能是:
A
B
C
D
E
BFS 会按照层次遍历图的节点,先访问 A
的邻居 B
和 C
,然后访问 B
的邻居 D
和 E
。
BFS 的复杂度分析
- 时间复杂度:O(V + E),其中 V 是图中的节点数,E 是边数。BFS 会访问每个节点和每条边一次。
- 空间复杂度:O(V),队列最多会包含每层的所有节点,因此需要 O(V) 的空间来存储这些节点。
应用场景
- 图的遍历:BFS 可以遍历图的所有节点,并在图中寻找某个特定节点或检查图的连通性。
- 最短路径问题:在无权图中,BFS 是寻找最短路径的有效算法。
- 拓扑排序:在某些情况下,BFS 可以用于计算拓扑排序(例如基于入度的 Kahn 算法)。
- 迷宫求解:在网格图中,BFS 可以用于找到从起点到终点的最短路径,适用于迷宫问题。
总结
广度优先搜索是一种基于层次遍历的算法,特别适合解决最短路径问题和连通性检测问题。它通过队列的方式进行节点管理,确保按层次顺序访问节点,因此在无权图中,BFS 可以找到最短路径,是一种非常重要且高效的图搜索算法。
四、启发式搜索
启发式搜索(Heuristic Search)是一种利用启发式信息(即对问题解空间的某种估计)来指导搜索过程的算法,常用于在复杂搜索空间中快速找到近似最优解。它通过优先搜索那些看起来更接近目标状态的节点,从而减少搜索空间,加速搜索过程。
核心思想
- 启发函数:启发式搜索依赖于一个启发函数(Heuristic Function),该函数用于估计从当前状态到目标状态的成本或距离。启发函数通常用 h(n)h(n)h(n) 表示,其中 nnn 是当前节点。
- 优先搜索:启发式搜索会优先探索启发函数值较小的节点,因为这些节点被认为离目标更近。
常见算法
-
贪心搜索(Greedy Search):
- 只考虑启发函数 h(n),每次选择启发函数值最小的节点进行扩展。
- 优点:相对简单,计算效率高。
- 缺点:由于只考虑当前节点到目标节点的估计距离,容易陷入局部最优。
-
A 算法*:
- 结合实际代价和启发式估计,综合考虑从起点到当前节点的实际代价 g(n) 和从当前节点到目标的启发式估计 h(n),总的代价函数为 f(n)=g(n)+h(n)。
- 优点:如果启发函数是一致的或可接受的,A* 能保证找到最优解。
- 缺点:计算开销较大,尤其在复杂的搜索空间中。
-
IDA(Iterative Deepening A)算法**:
- 结合深度优先搜索和 A* 算法的优点,采用迭代加深的方式,在每一层搜索中使用启发式估计 f(n)=g(n)+h(n) 来限制搜索深度。
- 优点:在大规模搜索问题上节省内存,适合用于内存受限的场景。
- 缺点:需要多次重复搜索,可能会增加计算时间。
算法示例(A* 算法)
A* 算法是启发式搜索的经典代表,它通过结合实际代价和启发式估计来寻找最优路径。
A* 算法步骤
- 初始化:将起点加入开放列表(Open List),并将其代价 f(n)初始化为 f(n)=g(n)+h(n),其中 g(n)=0,h(n)是启发式估计。
- 选择节点:从开放列表中选择代价最小的节点作为当前节点进行扩展。
- 扩展节点:生成当前节点的邻居节点,并计算每个邻居节点的代价 f(n)=g(n)+h(n)。
- 更新列表:如果邻居节点未被访问过,或通过当前节点到达该邻居的路径比之前找到的更优,将其加入开放列表或更新其代价。
- 重复步骤 2-4,直到找到目标节点或开放列表为空。
- 输出结果:如果找到目标节点,则输出路径;如果开放列表为空且未找到目标,则表示无解。
代码实现(Python 示例)
以下是 A* 算法的简单实现示例:
import heapq
def a_star_search(graph, start, goal, h):
open_list = []
heapq.heappush(open_list, (0, start)) # 优先队列,存储 (f, node)
came_from = {} # 记录节点的前驱节点,用于回溯路径
g_score = {start: 0} # 从起点到当前节点的代价
while open_list:
_, current = heapq.heappop(open_list) # 选择 f 最小的节点
if current == goal:
return reconstruct_path(came_from, current)
for neighbor, cost in graph[current].items():
tentative_g_score = g_score[current] + cost
if neighbor not in g_score or tentative_g_score < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g_score
f_score = tentative_g_score + h(neighbor)
heapq.heappush(open_list, (f_score, neighbor))
return None # 未找到路径
def reconstruct_path(came_from, current):
path = [current]
while current in came_from:
current = came_from[current]
path.append(current)
return path[::-1] # 返回反转后的路径
# 示例图(使用邻接表表示)
graph = {
'A': {'B': 1, 'C': 4},
'B': {'A': 1, 'C': 2, 'D': 5},
'C': {'A': 4, 'B': 2, 'D': 1},
'D': {'B': 5, 'C': 1}
}
# 启发函数(例如:使用曼哈顿距离)
def heuristic(node):
heuristics = {
'A': 7,
'B': 6,
'C': 2,
'D': 0
}
return heuristics.get(node, 0)
# 执行 A* 搜索
start, goal = 'A', 'D'
path = a_star_search(graph, start, goal, heuristic)
print("Path found:", path)
A* 算法的特点
- 优点:当启发函数是可接受或一致时,A* 算法能保证找到最优解。
- 缺点:由于需要维护开放列表,且需要计算所有节点的估计代价,A* 算法的空间和时间复杂度较高,尤其在搜索空间非常大的情况下。
应用场景
- 路径规划:例如机器人导航、游戏中的路径寻找等。
- 问题求解:例如八数码问题、滑块拼图等。
- 人工智能:例如在博弈树中寻找最佳策略。
- 优化问题:例如求解组合优化问题,如旅行商问题。
总结
启发式搜索通过使用启发函数来指导搜索过程,减少了搜索空间,提高了搜索效率。A* 算法作为启发式搜索的代表,能够在保证最优性的前提下高效地找到解。虽然启发式搜索的效率依赖于启发函数的质量,但它在路径规划、优化问题等方面的应用非常广泛。