forked from faranten/bupt-cs-gallery
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
44 changed files
with
982 additions
and
0 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
2022/12/4 | ||
|
||
# 回溯法 | ||
|
||
[email protected] | ||
|
||
## 回溯法框架 | ||
|
||
回溯法本质上是一种搜索算法,但是带有一定的跳跃性,因此比一般的搜索算法更为高效。 | ||
|
||
在使用回溯法的时候,我们往往需要构建解空间树,并在这棵树上进行深度优先搜索,每当遇到一个节点的时候,首先判断该节点是否具有问题的解,如果可能包含问题的解则进入进行搜索,否则跳过该节点(及其子树上的所有节点)并搜索后续节点。 | ||
|
||
回溯法求解问题的时候,只要得到问题的一个解就结束。 | ||
|
||
回溯法的基本思路可以概括如下: | ||
|
||
- 构建问题的解空间:解具有什么样的形式?在该形式下,解一共有多少种情况? | ||
- 将解空间组织起来,常见的思路是按照树结构进行组织 | ||
- 从开始节点出发,以深度优先方式搜索整个解空间 | ||
- 在搜索过程中使用剪枝函数减去不可能的子空间 | ||
- 回溯的两种思路:递归回溯、迭代回溯 | ||
- 子集树与排列树: | ||
- 子集树:往往是二叉树,分别表示当前活结点对应的物品是选还是不选 | ||
- 排列树:往往是多叉树,表示当前步骤中选择哪一个物品放入当前位置 | ||
|
||
|
||
|
||
## 装载问题 | ||
|
||
> 有一批共有$n$个集装箱要装上两艘载重量分别为$c_1$和$c_2$的货船,其中第$i$个集装箱的重量为$w_i$,要求确定是否有方案可以将这$n$个集装箱装上货船,如果有则找出一种装载方案 | ||
这是子集树问题。 | ||
|
||
我们的目标是将所有集装箱装入两艘货船,因此对于其中一批集装箱而言,我们希望其承载的集装箱总重量尽可能接近$c_1$,这样才有最有可能将所有集装箱装上两艘货船。 | ||
|
||
约束函数:如果当前箱子装上去的重量大于$c_1$,则在当前步骤下不考虑装载该箱子的情况。同样,如果当前价值加上剩余总价值小于当前最优价值,则不进行该子树的搜索。 | ||
|
||
|
||
|
||
## 批处理作业调度 | ||
|
||
> 给定$n$个作业,每个作业都需要在两台机器上完成(必须先由机器$1$进行处理、然后由机器$2$进行处理),且第$i$个作业在第$j$台机器上的处理时间为$t_{ij}$,试确定一种调度方案,使总的处理时间最短 | ||
这是排列树问题。 | ||
|
||
对于第一个进入机器$1$的作业,有$n$个选择,我们需要做的就是遍历这$n$个选择,并进行相应的第二个进入到机器$1$的作业的选择,并不断递归,直到最后选择完毕所有的作业,就构成了一个序列。 | ||
|
||
显然,我们需要一个辅助的数据结构`selected[i]`表示第$i$个作业是否已经被选择。 | ||
|
||
约束函数:如果当前选择的作业序列所花费的时间已经超过最优解,那么就不再进行这一选择后相应的递归。 | ||
|
||
|
||
|
||
## 符号三角形 | ||
|
||
> 给定一个数字$n$,表示第一行有$n$个符号,符号位加号或者减号,两个同号下面是加号、两个异号下面是减号,问一共有多少个不同的三角形含有的加号和减号数量相同 | ||
设数组`x[i]`表示第$%j$个字符是加号还是减号($1$表示加号、$0$表示减号),显然的思路就是逐行递归直到当前行只有$1$个元素,并且在递归的过程中维护加号和减号的数量。 | ||
|
||
约束函数:如果加号和减号的数量相差过大、以至于就算三角形中剩下的位置全部填满其中较少的那个也无法使两种符号相等,则剪枝。定量地讲,对于给定的$n$,可以知道总的符号数量为$n(n+1)/2$,因此加号和减号的数量一定为$n(n+1)/4$,以此作为剪枝标准。 | ||
|
||
注意这道题具体的实现思路,很巧妙,当我们确定了第$i$个元素之后,可以构建第一行为前$i$个元素时所对应的小三角形,并记下此时加号和减号的数量;然后确定第$i+1$个元素,此时第一行为前$i+1$个元素时所对应的三角形可以通过向第一行为前$i$个元素时所对应的小三角形右侧增加”一条元素“来构造得到! | ||
|
||
|
||
|
||
## $n$皇后 | ||
|
||
> 在$n\times n$格的棋盘上防止彼此不攻击的$n$个皇后,问有多少种放法 | ||
当我们放置一个皇后之后,可以据此皇后所在位置去对其他皇后的可能的位置进行剪枝。 | ||
|
||
|
||
|
||
## 连续邮资 | ||
|
||
> 假设有$n$种面值的邮票,并且一个信封上最多允许贴$m$张邮票,”连续邮资“指每次增加$1$,试求出在给定$n$和$m$时的最大连续邮资区间(限制最低邮资从$1$开始),并给出相应的面值设计方案 | ||
由于最低邮资一定从$1$开始,那么所有面值的最小面值一定是$1$。我们接下来需要确定下一个面值,为了进行剪枝操作降低复杂度,我们需要确定下一个面值的上界和下界: | ||
|
||
- 下界:上一张面值加一 | ||
- 上界:当前所能达到的最大面值加一,如果再大则会出现断层 | ||
|
||
据此便可以使用回溯法求解问题。 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
2022/11/20 | ||
|
||
# 贪心算法 | ||
|
||
[email protected] | ||
|
||
## 思路简述 | ||
|
||
贪心算法似乎可以理解为动态规划算法的一种效率比较高的近似? | ||
|
||
需要注意的是,贪心算法并不一定保证得到全局最优解,但是我们可以从事实的观测中得出,如果满足以下两个特点,那么对于这道题,贪心算法就是可以得到全局最优解的: | ||
|
||
1. 贪心选择性质:只需要基于当前情况做出最优解,即可保证全局最优解。但是,在动态规划算法中,当前状态的最优解与子问题的解具有非常强的关联性,不能仅根据当前问题的性质得到解。如何证明这一性质?常见的思路是:假设有一个当前问题的最优解,如果能找到一个执行方案,使这个最优解以某种角度上的”贪心选择“作为开始,并据此将问题划分为更小规模的问题,那么就可以证明贪心选择性质 | ||
2. 最优子结构性质:这个问题的整体最优解包括子问题的最优解,也就是说,如果得到了当前问题的最优解,那么如果将当前问题切分成小规模的子问题,那么可以直接得到小规模子问题的最优解 | ||
|
||
|
||
|
||
## 活动安排 | ||
|
||
> 设有$n$个活动的集合$E=\{1,2,\cdots,n\}$,其中每个活动都需要使用同一资源,而在同一时间内只有一个活动可以使用这个资源。每个活动都有开始时间$s_i$和结束时间$f_i$,该活动在区间$[s_i,f_i)$内占有这个资源,试在活动集合中选择出可以使总活动数最多的活动子集合并给出安排方案 | ||
这道题可以使用贪心算法来进行求解,即:每次选择活动的时候总是选择结束时间$f_i$最早的活动,从而为接下来的活动预留最多的时间。虽然贪心算法并不一定能够得到全局最优解,但是对于这个问题而言,却总是可以得到全局最优解的,这可以由书上所示的数学归纳法进行证明。 | ||
|
||
代码见相应的源文件。 | ||
|
||
|
||
|
||
## 背包问题 | ||
|
||
> 给定$n$种物品和容量为$C$的背包,第$i$件物品的价值为$w_i$、所需空间为$c_i$,01背包问题是说从中挑选物品放入背包使总价值最大,但这里的问题是:在选择物品放入背包时,可以放入第$i$件物品的一部分,而不需要全部放入,试求解此问题 | ||
看似比较复杂,但实际上有一个很简单的贪心思路可以求解这个问题:计算每种物品的单位价值$w_i/c_i$,从高到低依次选择单位价值最高的物品进行放入,若还有剩余空间则选择后续的物品。 | ||
|
||
|
||
|
||
## 最优装载 | ||
|
||
> 有一批集装箱要装上一艘载重为$C$的轮船,第$i$个集装箱的重量为$w_i$,要求将尽可能多的集装箱装上轮船 | ||
很显然,为了装入尽可能多的集装箱,需要先将集装箱按照重量从小到大进行排序,然后选择前面的若干个集装箱(直到装满为止),在具体实现上可以使用前缀和进行求和并与总载重量进行比较,详见源代码。 | ||
|
||
|
||
|
||
## 哈夫曼编码 | ||
|
||
> 哈夫曼算法根据字符在文件中的使用次数选择一种使用$0$和$1$的编码方式,使总所需存储空间最小,为了简化问题,这里假定文件中一共只出现前$6$个字母 | ||
一个直观的思路就是给出现频率高的字母更短的编码、而给出现频率低的字母更长的编码,这样便可以使总存储空间向更小趋近。由于要求以$0$和$1$来构造编码,因此可以采用“前缀码”的思路进行编码,并使用二叉树进行构造。在构造的时候采取自底向上的思路,每次选择“频率最低”的两棵子树进行合并,直到最后得到一棵完整的二叉树。 | ||
|
||
代码见相应的源文件。 | ||
|
||
|
||
|
||
## 最优服务次序 | ||
|
||
> 设有$n$名顾客同时等待一项服务,顾客$i$需要的服务时间为$t_i$,问如何安排$n$个顾客的访问次序使总的平均访问时间最小? | ||
显然应当采取贪心算法,假定有一种活动安排方案$\{t_1,t_2,\cdots,t_n\}$使总的平均等待时间最小,那么这$n$个活动一定按照所需的活动时间非递减进行排序,下面来证明这一结论。 | ||
|
||
使用反证法,假设$n$个活动不按照非递减顺序进行排序是最优解,则存在$1\leq i,j\leq n$且$i<j$,使得$t_i>t_j$,那么总的平均等待时间 | ||
$$ | ||
\begin{align} | ||
\bar{t}_{\text{总1}}&=\frac1n[(t_1)+\cdots+(t_1+\cdots+t_i)+\cdots(t_1+\cdots+t_i+\cdots+t_j)+\cdots+(t_1+\cdots+t_n)]\\ | ||
&=\frac1n[n\cdot t_1+(n-1)\cdot t_2+\cdots+(n-i+1)\cdot t_i+\cdots+(n-j+1)\cdot t_j+\cdots+t_n] | ||
\end{align} | ||
$$ | ||
如果我们交换$t_i$和$t_j$,得到$\bar{t}_{\text{总2}}$: | ||
$$ | ||
\begin{align} | ||
\bar{t}_{\text{总2}}&=\frac1n[(t_1)+\cdots+(t_1+\cdots+t_j)+\cdots(t_1+\cdots+t_j+\cdots+t_i)+\cdots+(t_1+\cdots+t_n)]\\ | ||
&=\frac1n[n\cdot t_1+(n-1)\cdot t_2+\cdots+(n-i+1)\cdot t_j+\cdots+(n-j+1)\cdot t_i+\cdots+t_n] | ||
\end{align} | ||
$$ | ||
则有 | ||
$$ | ||
\bar{t}_{\text{总1}}-\bar{t}_{\text{总2}}=(n-i+1)(t_i-t_j)+(n-j+1)(t_j-t_i)=(j-i)(t_i-t_j)>0 | ||
$$ | ||
这违背了“$n$个活动不按照非递减顺序进行排序是最优解”这一假设,因此矛盾,所以$n$个活动按照非递减顺序进行排序是最优解。 | ||
|
||
代码见相应的源文件。 | ||
|
||
|
||
|
||
## $d$森林问题 | ||
|
||
> 设$T$是一颗带权树,树的每条边带有一个正权,$S$是$T$的顶点集,$T/S$是从树$T$中将$S$中顶点删去后得到的森林。如果$T/S$中所有树从根到叶的路长都不超过$d$,则称$T/S$是一个$d$森林,设计一个算法求$T$的最小顶点集$S$,使$T/S$是$d$森林。(输入输出约定见书上相关内容)(注意,可能无解) | ||
这道题看似比较复杂,但实际上就是从每个叶节点到根节点进行移动,只要路长超过$d$则断开,此时将下一个节点设为当前节点的爷爷节点,并且当前节点的父结点和爷爷节点都设为叶子节点,并且将当前节点的父结点设为$0$. | ||
|
||
代码见相应的源文件。 | ||
|
||
|
||
|
||
## 区间覆盖 | ||
|
||
> 设直线上有$n$个点$\{x_1,x_2,\cdots,x_n\}$,现在用固定长度的闭区间覆盖这$n$个点,问至少需要多少个这样的固定长度闭区间?(闭区间的长度$k$通过标准输入给出) | ||
首先将这些点按照从小到大的顺序进行排序,然后从第一个点开始扫描,并进行相应处理,采取贪心算法,每次覆盖尽可能多的点,这样可以使所用闭区间的数量最少。 | ||
|
||
代码见相应的源文件。 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
2022/11/18 | ||
|
||
# 递归与分治问题 | ||
|
||
[email protected] | ||
|
||
## 基本思想 | ||
|
||
直接或间接调用自身的算法(函数)称为是递归的,递归的基本思路是从最大规模的问题开始,并分析 | ||
|
||
1. 问题规模是如何缩小的? | ||
2. 问题的最基本的情况是什么? | ||
|
||
这两个问题,之后再理论上便可以解题。 | ||
|
||
|
||
|
||
## 整数划分 | ||
|
||
> 现有一个正整数$n$,将$n$表示成若干个正整数相加的形式$n=n_1+n_2+\cdots+n_k~(k\geq1)$称为$n$的一个划分,试对给定的任意正整数$n$,求它的划分数 | ||
设$f(n,m)$表示分段中最大数字不超过$m$的划分数,那么 | ||
$$ | ||
f(n,m)= | ||
\begin{cases} | ||
1 & n=1,m=1\\ | ||
f(n,n) & n < m\\ | ||
1+f(n,n-1) & n = m\\ | ||
f(n,m-1)+f(n-m,m) & n>m>1 | ||
\end{cases} | ||
$$ | ||
据此便可以写出代码,不再赘述。 | ||
|
||
|
||
|
||
## 汉诺塔 | ||
|
||
> 设$a$、$b$、$c$是三个塔座,开始时,在塔座$a$上有$n$个从大到小、自底向上堆叠的圆盘,试按照汉诺塔移圆盘的规则,求出将者$n$个圆盘移动到塔座$b$的总移动次数? | ||
非常经典的问题!假设将塔座$a$最上面的$n-1$个圆盘移动到了塔座$c$,那么问题就变成了:将塔座$a$上的$n-1$个圆盘移动到塔座$b$,然后再将原先最下面的那个圆盘从塔座$a$移动到塔座$b$,然后再将塔座$c$上的$n-1$个圆盘移动到塔座$b$。因此,有如下思路: | ||
$$ | ||
\text{总次数}=\text{hanoi}(n-1,a,c,b)+\text{move}(a,b)+\text{hanoi}(n-1,c,b,a) | ||
$$ | ||
有了这个之后,代码就是容易实现的,此处不再赘述。 | ||
|
||
|
||
|
||
## 二分搜索 | ||
|
||
> 给定一个排好序的数字序列(常见的是从小到大),如果要在其中搜索指定元素,则可以采用二分搜索的思路进行处理 | ||
即折半查找,代码见相应的源文件。 | ||
|
||
|
||
|
||
## Strassen矩阵乘法 | ||
|
||
> 给定两个$n\times n$的矩阵,如何高效计算这两个矩阵的乘法呢? | ||
这样计算: | ||
$$ | ||
\left[ | ||
\begin{matrix} | ||
C_{11} & C_{12}\\ | ||
C_{21} & C_{22} | ||
\end{matrix} | ||
\right] | ||
= | ||
\left[ | ||
\begin{matrix} | ||
A_{11} & A_{12}\\ | ||
A_{21} & A_{22} | ||
\end{matrix} | ||
\right] | ||
\cdot | ||
\left[ | ||
\begin{matrix} | ||
B_{11} & B_{12}\\ | ||
B_{21} & B_{22} | ||
\end{matrix} | ||
\right] | ||
$$ | ||
其中, | ||
$$ | ||
\begin{align} | ||
C_{11}&=A_{11}B_{11}+A_{12}B_{21}\\ | ||
C_{12}&=A_{11}B_{12}+A_{12}B_{22}\\ | ||
C_{21}&=A_{21}B_{11}+A_{22}B_{21}\\ | ||
C_{22}&=A_{21}B_{12}+A_{22}B_{22} | ||
\end{align} | ||
$$ | ||
通过分析,可以知道,我们实际上只需要做$7$次乘法(而不是上面所说的$8$次),如下所示: | ||
$$ | ||
\begin{align} | ||
M_1&=A_{11}(B_{12}-B_{22})\\ | ||
M_2&=(A_{11}+A_{12})B_{22}\\ | ||
M_3&=(A_{21}+A_{22})b_{11}\\ | ||
M_4&=A_{22}(B_{21}-B_{11})\\ | ||
M_5&=(A_{11}+A_{22})(B_{11}+B_{22})\\ | ||
M_6&=(A_{12}-A_{22})(B_{21}+B_{22})\\ | ||
M_7&=(A_{11}-A_{21})(B_{11}+B_{12})\\ | ||
C_{11}&=M_5+M_4-M_2+M_6\\ | ||
C_{12}&=M_1+M_2\\ | ||
C_{21}&=M_3+M_4\\ | ||
C_{22}&=M_5+M_1-M_3-M_7 | ||
\end{align} | ||
$$ | ||
分析完毕。 | ||
|
||
|
||
|
||
## 棋盘覆盖 | ||
|
||
> 在一个$2^k\times 2^k$个方格组成的棋盘中,若恰有一个方格与其他方格不同,则称该方格为一个特殊方格,试使用四种形态的$L$型块来填充这个棋盘,试分析这个问题? | ||
分析是简单的,主要就是将大棋盘按照特殊方格出现的位置划分为四个小棋盘,然后递归求解直到大小为$1\times1$的最小棋盘。具体分析此处略去,但不知道这题会怎么考? | ||
|
||
|
||
|
||
## 合并排序 | ||
|
||
> 试使用分治策略完成数字序列的排序? | ||
基本思路是将待排序的数字分为大小相同(或相差$1$)的两段,然后递归进行处理,详见源代码。 | ||
|
||
|
||
|
||
## 快速排序 | ||
|
||
> 复习快速排序的思路并实现快速排序? | ||
快速排序的思路是从递归想法出发,按照以下思路进行排序: | ||
|
||
1. 分解:以`a[p]`为基准将`a[p:r]`分成`a[p:q-1]`、`a[q]`和`a[q+1:r]`三部分,使`a[p:q-1]`中任何一个元素小于等于`a[p]`且`a[q+1:r]`中任何一个元素大于等于`a[p]`。下标`q`应当在划分过程中确定 | ||
2. 递归:对`a[p:q-1]`和`a[q+1:r]`两个部分进行递归 | ||
3. 合并:由于上述两个部分的排序是“就地进行”的,因此不需要合并步 | ||
|
||
详见源代码,这道题最重要的我感觉并不是思路,而是代码细节,尤其是`i_temp`和`j_temp`的变化以及终止递归条件的判断,非常容易错! | ||
|
||
|
||
|
||
## 线性时间选择 | ||
|
||
> 给定$n$个元素和一个整数$k$,要求在这$n$个元素中选择出第$k$小的元素 | ||
详见书本分析和源代码文件,这题最关键的在于理论分析! | ||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
## *参考资料 | ||
|
||
1. 王晓东,《计算机算法设计与分析》,2018 |
Oops, something went wrong.