刷了一些算法题,以这篇文章作为一个阶段性的总结。(菜鸡一枚,各位大佬轻喷)
这篇文章包含了一些典型的算法题目。
除了题解,会尽量包含题解的思考过程。
(学生课程作业,一个赞0.1分,各位日后事业有成的帅哥仙女大佬点点赞吧)
一、动态规划:
1.0 动态规划的基本认识
动态规划的三个基本要素:无后效性、状态转移方程、最优子结构。感觉不理解也不太影响刷中等难度的动态规划题目。(因为我就是)
关于无后效性
可以根据无后效性去判断定义的状态和状态转移方程是否正确。
需要注意的是,一个问题,可以通过不同的状态定义来消除掉后效性。就比如股票时机,从某种角度上来讲,就是通过更多的状态之间的转换关系消除掉单个状态的后效性。
123. 买卖股票的最佳时机 III - 力扣(LeetCode)
所以在考虑是否有无后效性的时候,如果当前的状态定义存在后效性,那么我会考虑是否能通过更多中间子状态的转换关系来消除掉这种后效性。
关于状态转移方程
我觉得状态的定义和状态转移方程是动态规划的核心,其他两个特性是用来判断当前定义的状态转移是否合理(无后效)以及是否能够求出最优解(最优解)
但是实际上状态转移方程和状态定义有时候是比较难想到的。我在刷题时注意到一个比较讨巧(违背祖宗的)的方法,如果很难从题目本身想应该用什么作为状态,不妨反过来尝试,比如字符类型,实际上大部分的状态都是字符长度。直接去拿那些常用的状态定义一个个试,实际上足以应付大部分题目了。
1.1 0-1背包:
0-1背包问题是一个经典的组合优化问题。
我第一次做的时候,看到很多人说这个问题很基础很简单,但自己还感觉挺难的。
如果没有学过类似的题目第一次想的话,我个人觉得是不太好想的。
问题定义:
- 给定一组物品,每件物品有两个属性:重量(weight)和价值(value)。
- 有一个背包,它的容量(capacity)是有限的,即背包能承载的最大重量是确定的。
- 目标是在不超过背包容量的前提下,从这些物品中选择一些放入背包,使得背包中物品的总价值最大。
解法:
0-1背包问题的解法主要有三种(大概就是现代的回字的四种写法罢):动态规划、回溯法、分支限界法。这里,会简单介绍分支限界法和动态规划方法,因为它是解决这类问题的最常用且效率较高的方法。
分支限界法
分支限界最基本的思想就是,搜索所有解空间,通过上界(或者下界)函数进行剪枝,大幅度降低时间复杂度。
搜索所有解空间的过程很简单,定义一个大小为物品数的数组,对于每一个物品放入则为0不放入则为1。时间复杂度为O(2**n)
但是我们实际上可以在分支之前先看看这条分支究竟有没有继续探索的必要,我们通过一个上界函数来实现这一点。通常我们会使用物品的价值与重量的比值(即单位重量的价值)作为排序依据。逐渐取剩下物品中单位重量价值高的物品。如果取不满则用剩余容量*单位重量比值。如果上界小于当前最大价值就可以直接剪枝。
具体更详细的解释B站有一个up主做的很好。
分支限界法求解0/1背包问题动画演示(Implementation of 0/1 Knapsack using Branch and Bound)_哔哩哔哩_bilibili
动态规划解法
动态规划是一种通过把原问题分解为相对简单的子问题的方式来求解复杂问题的方法。在0-1背包问题中,我们可以通过构建一个表格来逐步求解最优解。
步骤:
-
初始化一个表格: 创建一个
(n+1) x (W+1)
的二维数组dp
,其中n
是物品数量,W
是背包的容量。dp[i][w]
表示对于前i
件物品,当前背包容量为w
时的最大价值。 -
填充表格:
- 对于每一件物品
i
和每一种背包容量w
,你需要决定是否将这件物品放入背包中。 - 如果将物品
i
放入背包,那么dp[i][w] = dp[i-1][w-w_i] + v_i
,其中w_i
和v_i
分别是物品i
的重量和价值。 - 如果不放入背包,那么
dp[i][w] = dp[i-1][w]
。 - 你需要在这两种选择中取最大值:
dp[i][w] = max(dp[i-1][w], dp[i-1][w-w_i] + v_i)
。
- 对于每一件物品
-
处理边界条件:
- 当
i=0
或w=0
时,dp[i][w]
应该为0,因为没有物品或者背包没有容量时,所能装载的最大价值自然为0。
- 当
-
找到最优解:
- 经过填充后,
dp[n][W]
就是问题的解,即在不超过背包容量的情况下,能够装载的最大价值。
- 经过填充后,
伪代码
以下是0-1背包问题的动态规划解法的伪代码:
- function knapsack(W, wt[], val[], n):
- dp = array of size (n+1) x (W+1) initialized to 0
-
- for i from 1 to n:
- for w from 1 to W:
- if wt[i-1] <= w:
- dp[i][w] = max(dp[i-1][w], dp[i-1][w-wt[i-1]] + val[i-1])
- else:
- dp[i][w] = dp[i-1][w]
-
- return dp[n][W]
1.2 编辑距离
具体来说,编辑距离是将一个字符串转换为另一个字符串所需的最少编辑操作次数。这些编辑操作通常包括插入、删除和替换字符。
问题描述
假设有两个字符串:
- 字符串 A,长度为 m。
- 字符串 B,长度为 n。
目标是找到将字符串 A 转换为字符串 B 所需的最少编辑操作次数。每一次操作可以是以下之一:
- 插入: 在字符串 A 的任意位置插入一个字符。
- 删除: 删除字符串 A 中的某个字符。
- 替换: 将字符串 A 中的某个字符替换为另一个字符。
关于编辑距离的思路:运气比较好,第一次做的时候没看答案就做出来了。当时的想法是(违背祖宗的)比较取巧的一种思路。
在做之前看标签,知道是二维动态规划的题目。然后就想设啥当状态,好像也只有字符串长度。
然后递推关系,一般的动态规划好像都是从左侧数据和上侧数据推公式,于是就试了各种动态规划方程。然后提交了几次莫名其妙就过了。。。
动态规划解法
步骤:
-
初始化二维数组: 创建一个
(m+1) x (n+1)
的二维数组dp
,其中m
和n
分别是字符串 A 和 B 的长度。dp[i][j]
表示字符串 A 的前i
个字符和字符串 B 的前j
个字符之间的最小编辑距离。 -
填充数组:
- 第一行和第一列分别代表将空字符串转换成对应长度的字符串 A 和 B,因此填充为其索引值(即
dp[i][0] = i
和dp[0][j] = j
)。 - 对于每个
i
(1 到 m)和j
(1 到 n),计算dp[i][j]
,可以有三种操作:- 如果 A[i] 等于 B[j],则
dp[i][j] = dp[i-1][j-1]
。 - 否则,选取以下三个操作中的最小值:
- 删除操作:
dp[i-1][j] + 1
- 插入操作:
dp[i][j-1] + 1
- 替换操作:
dp[i-1][j-1] + 1
- 删除操作:
- 如果 A[i] 等于 B[j],则
- 第一行和第一列分别代表将空字符串转换成对应长度的字符串 A 和 B,因此填充为其索引值(即
-
得到最终结果:
dp[m][n]
就是将字符串 A 转换为字符串 B 的最小编辑距离
伪代码
以下是编辑距离问题的动态规划解法的伪代码:
- function editDistance(A, B):
- m = length of A
- n = length of B
- create a matrix dp of size (m+1) x (n+1)
-
- for i from 0 to m:
- dp[i][0] = i
-
- for j from 0 to n:
- dp[0][j] = j
-
- for i from 1 to m:
- for j from 1 to n:
- if A[i-1] == B[j-1]:
- dp[i][j] = dp[i-1][j-1]
- else:
- dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
-
- return dp[m][n]
二、回溯
以下是一些典型的回溯算法应用:
1. N皇后问题
- 问题描述: 在
N x N
的棋盘上放置N
个皇后,使得它们互不攻击,即没有两个皇后在同一行、同一列或同一对角线上。 - 解法: 逐行放置皇后,对于每一行,尝试所有列并检查是否安全(即是否满足上述条件)。如果某位置安全,则放置皇后,并递归地在下一行中尝试。如果在某行中找不到安全的位置,则回溯到上一行并移动皇后。
2. 图的着色问题
- 问题描述: 给定无向图和颜色数量,要求给图中每个顶点着色,使得相邻的顶点颜色不同。
- 解法: 从第一个顶点开始,尝试所有的颜色,对于每种颜色,检查是否满足相邻顶点颜色不同的条件。如果是,则递归地对下一个顶点进行着色。如果找不到合适的颜色,则回溯。
3. 子集和问题
- 问题描述: 给定一组数字,判断是否存在一个子集,其和等于给定值。
- 解法: 遍历每个数字,对于每个数字,可以选择将其包含在子集中或不包含。递归地对剩余数字进行相同操作,并检查总和是否与目标值相等。如果在某个阶段和超过目标值,或者所有数字都已经考虑过,回溯。
4. 排列组合问题
- 问题描述: 给定一组数字,生成这些数字的所有排列或组合。
- 解法: 对于排列,使用回溯法生成所有可能的序列。对于组合,考虑每个元素两种可能的情况:包含在组合中或不包含。
回溯算法模板
回溯算
法的基本框架可以大致概括为:
- def backtrack(选择列表, 路径):
- if 满足结束条件:
- 结果.add(路径)
- return
-
- for 选择 in 选择列表:
- 做选择
- backtrack(选择列表, 路径)
- 撤销选择
关键点:
- 路径: 当前的解决方案。
- 选择列表: 当前可以做的选择。
- 结束条件: 达到决策树底层,无法再做选择的条件。
回溯算法的核心是递归,在递归之前“做选择”,在递归之后“撤销选择”,形成一个决策树的遍历过程。
性能注意事项:
- 回溯算法可能会涉及到大量的递归调用和返回,因此可能在时间和空间上都比较昂贵。
- 在实际实现时,适当的剪枝操作能显著提高效率,即提前排除那些显然不会得到正确解的路径。
通过调整选择列表和结束条件,回溯算法框架可以应用于各种组合问题的解决方案中。
三、博弈
待更新
四、贪心
待更新
评论记录:
回复评论: