变治策略(transform-and-conquer)
在“变”的阶段,出于这样或者那样的原因,把问题的实例变得更容易求解;
在“治”的阶段,对实例进行求解。
3种主要的变换方式:
- 实例化简:变换为同样问题的一个更简单或者更方便的实例。
- 改变表现:变换为同样实例的不同表现。
- **问题化简:**变换为另一个问题的实例,这种问题的算法是已知的。
- 求最小公倍数
- 计算图中的路径数量
- 优化问题化简
- 线性规划
- 简化为图的问题
引例1:检验数组中元素的唯一性
- 蛮力法:对数组中的元素对进行比较,直到找到两个相等的元素。最差效率 Θ ( n 2 ) \Theta(n^2) Θ(n2)
- 预排序:先对数组进行排序,然后只检查它的连续元素;如果该数组有相等的元素,则一定有一对元素是相互紧挨着的,反之亦然。基于预排序的算法最差的效率也属于 O ( n log n ) O(n\log n) O(nlogn)。
PresortElementUniqueness(A[0..n-1])
//x先对数组A[0..n-1]排序来解元素唯一性问题
//输入:n个可排序元素构成的一个数组A[0..n-1] ; 输出:如果A没有相等的元素,返回true;否则返回false
对数组A排序
for i← to n-2 do
if A[i] = A[i+1] return false
return true
引例2:模式计算,在给定的数字列表中最经常出现的一个数值称为模式。
- 蛮力法:对数组中的元素对进行扫描,并计算它的所有不同值出现的频率。最差效率 Θ ( n 2 ) \Theta(n^2) Θ(n2)
- 预排序:先对数组进行排序,相等的元素相邻,求有序数组中邻接次数最多的等值元素。
对多元线性方程组做初等变化转化为具有上三角(或下三角)系数矩阵的等价方程组: A x = b ↔ A ′ x = b ′ Ax=b \leftrightarrow A^{'}x=b^{'} Ax=b↔A′x=b′
初等变换(elementary operation)
- 交换方程组中方程位置;
- 方程乘上 c , c ≠ 0 c,c\ne 0 c,c=0;
- 方程之间倍数 c c c相加减。
LU分解(不需要额外的存储空间)
下三角矩阵 L L L由主对角线上的“1”以及在高斯消去过程中行的乘数所构成。
矩阵的逆与行列式
高斯消去法也可以用来求矩阵的逆, A A − 1 = E AA^{-1}=E AA−1=E。若矩阵的逆不存在,称之为退化矩阵。
当系数矩阵的行列式 ∣ A ∣ ≠ 0 |A|\ne 0 ∣A∣=0时,方程组有唯一解。
克拉默法则
x 1 = ∣ A 1 ∣ ∣ A ∣ , x 2 = ∣ A 2 ∣ ∣ A ∣ , … , x n = ∣ A n ∣ ∣ A ∣ 其中, ∣ A j ∣ 是把 A 的第 j 列用列 b 替换后得到的矩阵的行列式 x_1 = \frac{|A_1|}{|A|},x_2 = \frac{|A_2|}{|A|},\dots,x_n = \frac{|A_n|}{|A|} \qquad 其中,|A_j|是把A的第j列用列b替换后得到的矩阵的行列式 x1=∣A∣∣A1∣,x2=∣A∣∣A2∣,…,xn=∣A∣∣An∣其中,∣Aj∣是把A的第j列用列b替换后得到的矩阵的行列式
平衡二叉树又称AVL树,它具有以下性质:是一棵空树或它的**(平衡因子)左右两棵子树的高度差的绝对值不超过1**,并且左右两棵子树都是一棵平衡二叉树。由于普通二叉排序树容易失去平衡,极端情况下,二叉查找树会退化成线性的链表,导致插入和查找的复杂度下降到O(n),所以这也是设计平衡二叉树的初衷。对于一棵有n个结点的AVL树,其高度保持在 O ( l o g 2 n ) O(log_{2}n) O(log2n)数量级,ASL也保持在 O ( l o g 2 n ) O(log_{2}n) O(log2n)量级。
LL旋转
在左子树添加节点造成不平衡,进行右单旋转(R-rotation,顺时针旋转)。
RR旋转
在右子树添加节点造成不平衡,进行左单旋转(L-rotation,逆时针旋转)。
LR旋转
在根节点左子树的右子树上插入结点导致不平衡,先逆时针再顺时针旋转。
RL旋转
在根节点右子树的左子树上插入结点导致不平衡,先顺时针再逆时针旋转。
漫画:什么是红黑树?
红黑树(red-black tree)是一种自平衡二叉查找树,能够容忍同一节点的一棵子树高度是另一棵子树的两倍。可以在 O ( l o g n ) O(log n) O(logn)时间内做查找,插入和删除,这里的n 是树中元素的数目。它具有以下5条性质:
- 性质1:每个节点要么是黑色,要么是红色。
- 性质2:根节点是黑色。
- 性质3:每个叶子节点(NIL)是黑色。
- 性质4:每个红色结点的两个子结点一定都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。
二叉查找树(二分查找)所需查找的最大次数为二叉查找树的高度,但是存在缺陷——假设初始的二叉查找树只有三个节点,根节点值为9,左孩子值为8,右孩子值为12。接下来我们依次插入如下五个节点:7,6,5,4,3。依照二叉查找树的特性,结果会变成下图这样:
这样的形态虽然野符合二叉查找树的特性,但是查找的性能大打折扣,几乎变成了线性。而解决二叉查找树多次插入新节点导致不平衡的方法就是红黑树。
正因为有上述5条性质的限制,才能保证红黑树的自平衡。红黑树从根到叶子的最长路径不会超过最短路径的两倍。红黑树的调整策略主要有 变色和旋转。应用:
- C++ STL中的关联式容器:集合set、多重集合multiset、映射map、多重映射multimap;
- JAVA集合框架:TreeSet和TreeMap
- 在Linux内核中,用于组织虚拟区间的数据结构也是红黑树。
2-3树
它允许一棵查找树的单个节点中不止包含一个元素(简单化的B树),高度平衡,所有叶子结点位于同一层。
n个元素的序列 { k 1 , k 2 , … , k n } \{k_1,k_2,…,k_n\} {k1,k2,…,kn},当且仅当满足下列关系时,成为堆:
小根堆 { k i ≤ k 2 i k i ≤ k 2 i + 1 或 大根堆 { k i ≥ k 2 i k i ≥ k 2 i + 1 小根堆\begin{cases} k_i \leq k_{2i} \\ k_i \leq k_{2i+1} \end{cases} \qquad 或 \qquad 大根堆\begin{cases} k_i \geq k_{2i} \\ k_i \geq k_{2i+1} \end{cases} 小根堆{ki≤k2iki≤k2i+1或大根堆{ki≥k2iki≥k2i+1
如果将序列看成一个二叉树,树形为完全二叉树(高度为 ⌊ l o g 2 n ⌋ \lfloor log_{2}n \rfloor ⌊log2n⌋),非终端结点的值均小于或大于左右子结点的值。**注意:**利用树的结构特征来描述堆,所以树只是作为堆的描述工具,堆实际是存放在线形空间中的。
堆排序的基本思想:
堆的构造
初始无序序列(未成堆)
从第 ⌈ n / 2 ⌉ \lceil n/2 \rceil ⌈n/2⌉个元素起,至第一个元素止,进行反复筛选
堆排序过程
/** 堆排序:
堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,
使得在当前无序区中选取最大(或最小)关键字的记录变得简单。 */
public class HeapSort {
public int[] heapSort(int[] arrays) {
for (int i = 0; i < arrays.length; i++) {
//建大根堆
createMaxdHeap(arrays, arrays.length - 1 - i);
//将当前无序区的堆顶记录R[0]和该区间的最后一个记录R[last]交换。
swap(arrays, 0, arrays.length - 1 - i);
//打印每一次创建堆的过程
System.out.println(Arrays.toString(arrays));
}
return arrays;
}
public void createMaxdHeap(int[] arrays, int lastIndex) {
for (int i = (lastIndex - 1) / 2; i >= 0; i--) {
// 保存当前正在判断的节点
int k = i;
// 若当前节点的子节点存在
while (2 * k + 1 <= lastIndex) {
// biggerIndex总是记录较大节点的值,先赋值为当前判断节点的左子节点
int biggerIndex = 2 * k + 1;
if (biggerIndex < lastIndex) {
// 若右子节点存在,否则此时biggerIndex应该等于 lastIndex
if (arrays[biggerIndex] < arrays[biggerIndex + 1]) {
// 若右子节点值比左子节点值大,则biggerIndex记录的是右子节点的值
biggerIndex++;
}
}
if (arrays[k] < arrays[biggerIndex]) {
// 若当前节点值比子节点最大值小,则交换2者得值,交换后将biggerIndex值赋值给k
swap(arrays, k, biggerIndex);
k = biggerIndex;
}else { break; }
}
}
}
public void swap(int[] arrays, int i, int j) {
// if (i == j) { return; }
int temp = arrays[i];
arrays[i] = arrays[j];
arrays[j] = temp;
// arrays[i] = arrays[i] + arrays[j];
// arrays[j] = arrays[i] - arrays[j];
// arrays[i] = arrays[i] - arrays[j];
}
public static void main(String[] args) {
HeapSort s = new HeapSort();
int[] arrays = new int[] {5, 3, 6, 2, 1, 9, 4, 8, 7};
System.out.println("未排序的数组:" + Arrays.toString(arrays));
s.heapSort(arrays);
System.out.println("排序后的数组:" + Arrays.toString(arrays));
}
}
算法分析:堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为 O ( n l o g n ) O(nlogn) O(nlogn),它也是不稳定排序。
霍纳法则计算多项式
p ( x ) = ( ⋯ ( a n x + a n − 1 ) x + ⋯ ) x + a 0 p(x)=(⋯(a_nx+a_n−1)x+⋯)x+a_0 p(x)=(⋯(anx+an−1)x+⋯)x+a0
例如:对于多项式: p ( x ) = 2 x 4 − x 3 − 3 x 2 + x − 5 p(x) = 2x^4−x^3−3x^2+x−5 p(x)=2x4−x3−3x2+x−5,利用霍纳法则有
p ( x ) = 2 x 4 − x 3 − 3 x 2 + x − 5 = x ( 2 x 3 − x 2 − 3 x + 1 ) − 5 = x ( x ( 2 x 2 − x − 3 ) + 1 ) − 5 = x ( x ( x ( 2 x − 1 ) − 3 ) + 1 ) − 5 \begin{aligned} p(x) &= 2x^4−x^3−3x^2+x−5 \\ &= x(2x^3−x^2−3x+1)−5 \\ &= x(x(2x^2−x−3)+1)−5 \\ &= x(x(x(2x−1)−3)+1)−5 \end{aligned} p(x)=2x4−x3−3x2+x−5=x(2x3−x2−3x+1)−5=x(x(2x2−x−3)+1)−5=x(x(x(2x−1)−3)+1)−5
系数 | 2 | -1 | 3 | 1 | -5 |
---|---|---|---|---|---|
x = 3 x=3 x=3 | 2 | 3 × 2 + ( − 1 ) = 5 3\times 2 + (-1)=5 3×2+(−1)=5 | 3 × 5 + 3 = 18 3\times 5+3=18 3×5+3=18 | 3 × 18 + 1 = 55 3\times 18+1=55 3×18+1=55 | 3 × 55 + ( − 5 ) = 160 3\times 55+(-5)=160 3×55+(−5)=160 |
Horner(P[0..n],x)
//用霍纳法则求一个多项式在一个给定点的值
//输入:一个n次多项式的系数数组P[0..n](从低到高存储),以及一个数字x; 输出:多项式在x点的值
p←P[n]
for i←n−1 downto 0 do
p←x∗p+P[i]
return p
算法分析: M ( n ) = A ( n ) = ∑ i = 0 n − 1 1 = n M(n)=A(n)=∑_{i=0}^{n−1}1=n M(n)=A(n)=∑i=0n−11=n
二进制幂
霍纳法则计算 a n a^n an时退化成蛮力算法。
问题描述: 设 n = b I ⋯ b i ⋯ b 0 设n=b_I⋯b_i⋯b_0 设n=bI⋯bi⋯b0是在二进制系统中,表示一个正整数的位串。通过多项式 p ( x ) = b I x I + ⋯ + b i x i + ⋯ + b 0 p(x)=b_Ix^I+⋯+b_ix^i+⋯+b_0 p(x)=bIxI+⋯+bixi+⋯+b0(这里x为特定值2)计算n的值。
例如,如果n=13,它的二进制表示是1101: 13 = 1 × 2 3 − 1 × 2 2 + 0 × 2 1 + 1 × 2 0 13=1×2^3−1×2^2+0×2^1+1×2^0 13=1×23−1×22+0×21+1×20; a n = a p ( 2 ) = a b I 2 I + ⋯ + b i 2 i + ⋯ + b 0 a^n=a^{p(2)}=a^{b_I2^I+⋯+b_i2^i+⋯+b_0} an=ap(2)=abI2I+⋯+bi2i+⋯+b0。
用霍纳法则计算二进制多项式p(2) | 对于 a n = a p ( 2 ) a^n=a^{p(2)} an=ap(2)的意义 |
---|---|
p ← 1 当 n ≥ 1 , 第一个数总是 1 f o r i ← I − 1 d o w n t o 0 d o p ← 2 ∗ p + b i p←1 当n≥1,第一个数总是1 \\ for \quad i←I−1 \quad downto \quad 0 \quad do \\\qquad p←2∗p+b_i p←1当n≥1,第一个数总是1fori←I−1downto0dop←2∗p+bi | a p ← a 1 f o r i ← I − 1 d o w n t o 0 d o a p ← a 2 p + b i a^p←a^1 \\ for \quad i←I−1 \quad downto \quad 0 \quad do \\ a^p←a^{2p+b_i} ap←a1fori←I−1downto0doap←a2p+bi |
a 2 p + b i = a 2 p × a b i = ( a p ) 2 × a b i = { ( a p ) 2 i f b i = 0 ( a p ) 2 × a i f b i = 1 a^{2p+b_i}=a^{2p}×a^{b_i}=(a^p)^2×a^{b_i}=\begin{cases} (a^p)^2 \qquad if\;b_i=0 \\ (a^p)^2×a \qquad if\;b_i=1 \end{cases} a2p+bi=a2p×abi=(ap)2×abi={(ap)2ifbi=0(ap)2×aifbi=1
leftRightBinaryExponentiation(a,b(n))
//用从左至右二进制幂算法计算a^n
//输入:一个数字a和二进制位b_I,⋯b_0的列表b(n),这些位来自于一个正整数n的二进制展开式 ; 输出: a^n 的值
product←a
for i←I−1 downto 0 do
product←product∗product
if b_i=1 product←product∗a
return product
算法分析:总的乘法次数M(n): ( b − 1 ) ≤ M ( n ) ≤ 2 ( b − 1 ) b − 1 = ⌊ l o g z n ⌋ (b−1)≤M(n)≤2(b−1)\qquad b−1=⌊log_zn⌋ (b−1)≤M(n)≤2(b−1)b−1=⌊logzn⌋,其中b为指数n位串长度。从左至右二进制幂算法效率是对数级的。
使用同样的多项式但不使用霍纳法则。 a n = a b I 2 I + ⋯ + b i 2 i + ⋯ + b 0 = a b I 2 I × ⋯ × a b i 2 i × ⋯ × a b 0 a^n=a^{b_I2^I+⋯+b_i2^i+⋯+b_0}=a^{b_I 2^I}×⋯×a^{b_i2^i}×⋯×a^{b_0} an=abI2I+⋯+bi2i+⋯+b0=abI2I×⋯×abi2i×⋯×ab0。
a b i 2 i = { ( a 2 i ) i f b i = 0 1 i f b i = 1 a^{b_i2^i} = \begin{cases} (a^{2^i}) \qquad if\;b_i=0 \\ 1 \qquad if\;b_i=1 \end{cases} abi2i={(a2i)ifbi=01ifbi=1
1 | 1 | 0 | 1 | n的二进制位 |
---|---|---|---|---|
a 8 a^8 a8 | a 4 a^4 a4 | a 2 a^2 a2 | a | 项 a 2 i a^{2^i} a2i |
a 5 × a 8 = a 13 a^5 \times a^8 = a ^{13} a5×a8=a13 | a × a 4 = a 5 a\times a^4 = a^5 a×a4=a5 | a | 累乘器 |
RightleftBinaryExponentiation(a,b(n))
//用从右至左二进制幂算法计算a^n
//输入:一个数字a和二进制位b_I,⋯b_0的列表b(n),这些位来自于一个正整数n的二进制展开式; 输出: a^n 的值
term←a //初始化
if b_0=1 product ←a
else product ←1
for i←1 to I do
term←term∗term
if b_i=1 product←product∗term
return product
输入增强:对问题的部分或全部输入做预处理,然后将获得的额外信息进行存储,以加速后面问题的求解。(以空间换时间)
- 计数排序;
- Boyer-Moore字符串匹配算法和霍斯普尔提出的简化版本(Horspool算法)。
预构造:使用额外的空间来实现更快和更方便的数据存取。
- 散列法;
- 以B树作索引 。
和空间换时间权衡思想相关的算法设计技术:动态规划(把给定问题中重复子问题的解记录在表中,然后求得所讨论问题的解。)
算法思想:针对待排序列表中的每一个元素,算出列表中小于该元素的元素个数,并把结果记录在一张表中。这个“个数”指出了该元素在有序列表的位置。
ComparisonCountingSort(A[0..n-1])
//用比较计数法一个数组A[0..n-1]排序
//输入:一个可排序的数组A[0..n-1]; 输出:A中元素构成的非降序排列的数组S[0..n-1]
for i←0 to n-1 do Count[i] ←0 //初始化计数数组
for i ←0 to n-2 do
for j ←i+1 to n-1 do
if A[i]<A[j] Count[j] ←Count[j]+1
else Count[i] ←Count[i]+1
for i ←0 to n-1 do
S[Count[i]]←A[i]
return S
算法分析: 设比较次数为C(n)。
C ( n ) = ∑ i = 0 n − 2 ∑ j = i + 1 n − 1 1 = ∑ i = 0 n − 2 [ ( n − 1 ) − ( i + 1 ) + 1 ] = ∑ i = 0 n − 2 ( n − 1 − i ) = n ( n − 1 ) 2 C(n)=∑_{i=0}^{n−2}∑_{j=i+1}^{n−1}1 = ∑_{i=0}^{n−2} [(n−1)−(i+1)+1] = ∑_{i=0}^{n−2}(n−1−i)=\frac{n(n−1)}{2} C(n)=i=0∑n−2j=i+1∑n−11=i=0∑n−2[(n−1)−(i+1)+1]=i=0∑n−2(n−1−i)=2n(n−1)时间复杂度为 O ( n 2 ) O(n^2) O(n2);优势在于使得键值的移动次数最小化。
A[0] | A [1] | A[2] | A[3] | A[4] | A[5] |
---|---|---|---|---|---|
13 | 11 | 12 | 13 | 12 | 12 |
数组值 | 11 | 12 | 13 |
---|---|---|---|
频率 | 1 | 3 | 2 |
分布值 | 1 | 4 | 6 |
分布值指出了最后的有序数组中它们的元素最后一次出现的正确位置。对于数组下标是从0到 n − 1 n-1 n−1,为得到相应元素位置,分布值要减1。
Distribution(A[0..n-1],l,u)
//用分布计数法,对来自于有限范围(l,u)整数的一个数组A[0..n-1]排序
//输入:一个可排序的数组A[0..n-1],数组中的整数位于l和u之间 ; 输出:A中元素构成的非降序排列的数组S[0..n-1]
for j←0 to u-l do D[j] ←0 //初始化频率数组
for i ←0 to n-1 do D[A[i]-l] ←D[A[i]-l]+1 //计算频率值
for j ←1 to u-l do D[j] ←D[j-1]+D[j] //重用于分布
for i ←n-1 downto 0 do
j ← A[i]-l
s[D[j]-1] ←A[i]
D[j] ← D[j]-1
return S
算法分析:利用频率数组来记录元素出现的次数,空间换时间。对输入的数据有要求。时间复杂度为O(n)。
字符串匹配中的输入增强思想
对模式进行预处理以得到它的一些信息,把这些信息存储在表中,然后在给定文本中实际查找模式时使用这些信息。
应用该思想的两种算法:KMP(模式和文本从左到右比较)和Boyer-Moore算法(从右到左比较)。
算法思想:某个文本匹配模式(长度为m),从模式的最后一个字符开始从右向左,比较相应字符对。如果模式中所有字符都匹配成功,算法停止,查找成功;如果遇到一对不匹配字符,需要把模式右移,根据文本中对齐模式的最后一个字符c的不同情况确定不同的移动策略。
Case1:如果模式中不存在c,模式的安全移动距离就是它的全部长度。
Case2:如果模式中存在c,但它不是模式的最后一个字符,移动时应该把模式中最右边的c和文本中的c对齐。
Case3:如果c正好是模式中的最后一个字符,但是在模式的其他 m − 1 m-1 m−1个字符中不包含c,移动情况类似case1,即移动距离等于模式的全部长度m。
Case4:如果c正好是模式的最后一个字符,而且在模式的前 m − 1 m-1 m−1个字符中也包含c,移动情况类似case2,即应该把模式中前 m − 1 m-1 m−1个字符中的c和文本中的c对齐。
f ( c ) = { 模式的长度 m , 如果 c 不包含在模式的前m-1个字符 模式前 m − 1 个字符中最右边的 c 到模式最后一个字符的距离 , 其他情况 f(c)= \begin{cases} 模式的长度m, &\text{如果}c\text{不包含在模式的前m-1个字符}\\ 模式前m-1个字符中最右边的c到模式最后一个字符的距离, &\text{其他情况} \end{cases} f(c)={模式的长度m,模式前m−1个字符中最右边的c到模式最后一个字符的距离,如果c不包含在模式的前m-1个字符其他情况
移动表
字符c | A | B | C | D | E | F | … | … | R | … | Z | _ |
---|---|---|---|---|---|---|---|---|---|---|---|---|
移动距离 t ( c ) t(c) t(c) | 4 | 2 | 6 | 6 | 1 | 6 | 6 | 6 | 3 | 6 | 6 | 6 |
ShiftTable(P[0...m-1])// 用Horspool算法填充移动表
// 输入:模式P[0...m-1]以及一个可能出现字符的字母表
// 输出:以字母表中字符为索引的数组Table[0...size-1] 标中填充的移动距离是根据上面的表达式计算出来的。
for i ← 0 to size-1 do Table[i] ← m
for j ← 0 to m-2 do Table[p[j]] ← m-1-j
return Table
HorspoolMatching(P[0...m-1],T[0...n-1])// 实现Horspool字符串匹配算法
// 输入:模式P[0...m-1],文本T[0...n-1]
// 输出:第一个匹配子串最左端字符的下标,若无匹配子串,返回-1
ShiftTable[P[0...m-1]) //生成移动表
i ← m-1 //模式最右端的距离
while i≤n-1 do
k ← 0
while k ≤ m-1 and P[m-1-k] = T[i-k] do
k ← k+1
if k = m
return i-m+1
else i ← i+Table[T[i]]
return -1
算法分析:最差效率为 O ( m n ) O(mn) O(mn),对于随机文本来说,它的效率为 O ( n ) O(n) O(n)。
最好情况:(文本串) A B C A B C A B C A B C (模式串) A B C C b e s t = m 最坏情况:(文本串) 000000000000 (模式串) 100 C w o r s t = m ( n − m + 1 ) 最好情况:(文本串)ABCABCABCABC \qquad (模式串)ABC \qquad C_{best}=m \\ 最坏情况:(文本串)000000000000 \qquad (模式串)100 \qquad C_{worst}=m(n-m+1) 最好情况:(文本串)ABCABCABCABC(模式串)ABCCbest=m最坏情况:(文本串)000000000000(模式串)100Cworst=m(n−m+1)
如果模式最右边的字符和文本中的相应字符 c c c所做的初次比较失败,该算法同Horspool算法操作一致。若在遇到一个不匹配字符之前,如果已经有 k ( 0 < k < m ) k(0
k(0<k<m) 个字符成功匹配,则BM算法与Horpool算法处理不同。①坏字符规则:当文本串中的某个字符跟模式串的某个字符不匹配时,称文本串中的这个失配字符为坏字符,此时模式串需要向右移动,导致这种移动的原因和导致Horspool算法移动的原因是一样的。BM算法计算坏符号移动 d 1 d_1 d1的公式为: d 1 = m a x { t 1 ( c ) − k , 1 } d_{1} = max\{t_{1}(c)-k,1\} d1=max{t1(c)−k,1};其中, t 1 ( c ) t1(c) t1(c)是Horspool算法用到的预先算好的表中的单元格,而k是成功匹配的字符个数。
②好后缀规则:由模式中最后 k > 0 k>0 k>0个成功匹配的字符确定,将模式的结尾部分叫做模式的长度为k的后缀,记为suff(k)。和坏字符根据单独的字符c填充移动表不同的是,在好后缀移动表中,是根据模式后缀的长度 { 1 , . . . , m − 1 } \{1,...,m-1\} {1,...,m−1}来分别填充表格的。 d 2 d_2 d2是从右数第二个suff(k)到最右边suff(k)的距离。
k 模式 d 2 d_{2} d2 1 A B C B ‾ A B ‾ ABC\overline{B}A\underline{B} ABCBAB 2 2 A B ‾ C B A B ‾ \overline{AB}CB\underline{AB} ABCBAB 4 3 A B ‾ C B A B ‾ \overline{AB}C\underline{BAB} ABCBAB 4 4 A B ‾ C B A B ‾ \overline{AB}\underline{CBAB} ABCBAB 4 5 A B ‾ C B A B ‾ \overline{AB}\underline{CBAB} ABCBAB 4 找出长度 l < k l
l<k 的最长前缀,它能和长度同样为 l l l的后缀完全匹配。如果存在这样的后缀,通过求出前缀后缀之间的距离来作为移动距离 d 2 d_2 d2,否则,把 d 2 d_2 d2设置为模式的长度 m m m。
d = { d 1 , k = 0 m a x { d 1 , d 2 } , k > 0 d= \begin{cases} d_{1},&k=0\\ max\{d_{1},d_{2}\},&k>0 \end{cases} d={d1,max{d1,d2},k=0k>0
例子:在一个由英文字母和空格构成的文本中查找“BAOBAB”,坏符号移动表如下
字符c | A | B | C | D | … | O | … | Z | _ |
---|---|---|---|---|---|---|---|---|---|
移动距离 t 1 ( c ) t_1(c) t1(c) | 1 | 2 | 6 | 6 | 6 | 3 | 6 | 6 | 6 |
好后缀移动表如下
k | 模式 | d 2 d_{2} d2 |
---|---|---|
1 | B A O B ‾ A B ‾ BAO\overline{B}A\underline{B} BAOBAB | 2 |
2 | B ‾ A O B A B ‾ \overline{B}AOB\underline{AB} BAOBAB | 5 |
3 | B ‾ A O B A B ‾ \overline{B}AO\underline{BAB} BAOBAB | 5 |
4 | B ‾ A O B A B ‾ \overline{B}A\underline{OBAB} BAOBAB | 5 |
5 | B ‾ A O B A B ‾ \overline{B}\underline{AOBAB} BAOBAB | 5 |
算法分析:最好情况是在字符集比较大的前提下,时间复杂度可以达到非常理想的 O ( n / m ) O(n/m) O(n/m)。最差情况是线性情况下,构造坏符号移动表和好后缀移动表时间复杂度为 O ( m + n ) O(m+n) O(m+n)。
基本思想:把键分布在一个称为散列表的一维数组 H [ 0 … m − 1 ] H[0\dots m-1] H[0…m−1]中,记录的存储位置与关键字之间存在对应关系;通过对每个键计算散列函数完成散列地址映射。查找速度极快O(1),查找效率与元素个数n无关。
- 散列表的长度相对于键的个数不应过大,但同时也不应过小从而影响算法的时间效率;
- 散列函数需要把键在散列表的单元格中尽可能均匀地分布(m的值常为质数);
- 散列函数须容易计算。
如果选择的散列表长度m小于键的数量n,会遇到碰撞(也叫冲突,不同的关键码映射到同一个哈希地址)。
同义词:具有相同函数值的两个关键字
冲突不可避免,只能尽量减少冲突
参考博文:哈希表及处理冲突的方法
构造好的哈希函数;
数字分析法
如果事先知道关键字集合,并且每个关键字的位数比哈希表的地址码位数多时,可以从关键字中选出分布较均匀的若干位,构成哈希地址。
例如,有80个记录,关键字为8位十进制整数 d 1 d 2 d 3 … d 7 d 8 d_1d_2d_3…d_7d_8 d1d2d3…d7d8,如哈希表长取100,则哈希表的地址空间为: 00 − 99 00 - 99 00−99。假设经过分析,各关键字中 d 4 d_4 d4和 d 7 d_7 d7的取值分布较均匀,则哈希函数为: h ( k e y ) = h ( d 1 d 2 d 3 … d 7 d 8 ) = d 4 d 7 h(key)=h(d_1d_2d_3…d_7d_8)=d_4d_7 h(key)=h(d1d2d3…d7d8)=d4d7。例如, h ( 81346532 ) = 43 , h ( 81301367 ) = 06 h(81346532)=43,h(81301367)=06 h(81346532)=43,h(81301367)=06。相反,假设经过分析,各关键字中 d 1 d_1 d1和 d 8 d_8 d8的取值分布极不均匀, d 1 d_1 d1都等于5, d 8 d_8 d8 都等于2,此时,如果哈希函数为: h ( k e y ) = h ( d 1 d 2 d 3 … d 7 d 8 ) = d 1 d 8 h(key)=h(d_1d_2d_3…d_7d_8)=d_1d_8 h(key)=h(d1d2d3…d7d8)=d1d8,则所有关键字的地址码都是52,显然不可取。
平方取中法
当无法确定关键字中哪几位分布较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。这是因为:平方后中间几位和关键字中每一位都相关,故不同关键字会以较高的概率产生不同的哈希地址。
分段叠加法
除留余数法
伪随机数法
制定一个好的解决冲突方案。
开放定址法(线性探测、二次探测、 伪随机探测再散列)
再哈希法
链地址法
建立公共溢出区
装填因子( α \alpha α):越大,表中记录数越多,说明表装得越满,发生冲突的可能性就越大,查找时比较次数就越多。 α = 表中填入记录数 哈希表的长度 \alpha = \frac{表中填入记录数}{哈希表的长度} α=哈希表的长度表中填入记录数。所以哈希表的查找效率ASL与装填因 α \alpha α有关!既不是严格的O(1),也不是O(n)。
平衡的多叉树,称为B树。B树中所有节点中孩子节点个数的最大值,通常用m表示 ( m > = 3 ) (m >= 3) (m>=3),称为m阶B树。描述一颗B树时需要指定它的阶数,阶数表示了一个结点最多有多少个孩子结点,一般用字母m表示阶数。
一颗m阶的B树定义如下:
(1)每个结点最多有 m − 1 m-1 m−1个关键字,具有2到m个子结点;
(2)根结点最少可以只有1个关键字;非根结点至少有 ⌈ m / 2 ⌉ − 1 \lceil m/2 \rceil-1 ⌈m/2⌉−1个关键字;
(3)每个结点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它。
(4)所有叶子结点都位于同一层,或者说根结点到每个叶子结点的长度都相同。
B树的插入
B树的删除
B树的性能分析:包含 N 个关键码的 B 树,有 N + 1 N+1 N+1 个外部空指针,假设外部指针在第 k k k 层。则各层的结点数目为, 第 0 层为根,第一层至少两个结点;第二层至少 2 ⋅ ⌈ m / 2 ⌉ 2·⌈m/2⌉ 2⋅⌈m/2⌉个结点;第 k 层至少 2 ⋅ ⌈ m / 2 ⌉ k − 1 2·⌈m/2⌉^{k−1} 2⋅⌈m/2⌉k−1个结点。
N + 1 ≥ 2 ⌈ m / 2 ⌉ k − 1 , k ≤ 1 + l o g ⌈ m / 2 ⌉ ( N + 1 2 ) N = 1 , 999 , 998 , m = 199 时 , k = 4 , 一次检索最多 4 层 N+1 \geq 2⌈m/2⌉^{k-1},k \leq 1+log_{⌈m/2⌉}(\frac{N+1}{2}) \\ N=1,999,998,m=199 时, \qquad k=4,一次检索最多4层 N+1≥2⌈m/2⌉k−1,k≤1+log⌈m/2⌉(2N+1)N=1,999,998,m=199时,k=4,一次检索最多4层
如果问题是由交叠子问题构成的,就可以用动态规划技术解决。对每个较小的子问题一次又一次求解,还不如对每个较小的子问题只求一次解并把结果记录在表中,这样就可以从表中的得出原始问题的解。
最优化法则: 对于一个具有n个输入的最优化问题,其求解过程往往可以划分为若干个阶段,每一阶段的决策仅依赖于前一阶段的状态,由决策所采取的动作使状态发生转移,成为下一阶段决策的依据。从而,一个决策序列在不断变化的状态中产生。这个决策序列产生的过程称为多阶段决策过程。
在每一阶段的决策中有一个赖以决策的策略或目标,这种策略或目标是由问题的性质和特点所确定,通常以函数的形式表示并具有递推关系,称为动态规划函数。
动态规划算法设计步骤
①划分子问题
用参数表达子问题的边界,将问题求解转 变成多步判断的过程;
②确定优化函数
以该函数的极大(或极小)作为判断的依据,确定是否满足优化原则;
③ 列出关于优化函数的递推方程 (或不等式)和边界条件 ;
④考虑是否需要设立标记函数;
⑤自底向上计算,以备忘录方法 (表格)存储中间结果 。
动态规划设计要素
问题描述:设图G=(V, E)是一个带权有向连通图,如果把顶点集合V划分成k个互不相交的子集 V i ( 2 ≤ k ≤ n , 1 ≤ i ≤ k ) V_i(2≤k≤n, 1≤i≤k) Vi(2≤k≤n,1≤i≤k),使得E中的任何一条边(u, v),必有 u ∈ V i , v ∈ V i + m ( 1 ≤ i < k , 1 < i + m ≤ k ) u∈V_i,v∈V_{i+m}(1≤i<k, 1<i+m≤k) u∈Vi,v∈Vi+m(1≤i<k,1<i+m≤k),则称图G为多段图,称 s ∈ V 1 s∈V_1 s∈V1为源点, t ∈ V k t∈V_k t∈Vk为终点。多段图的最短路径问题是求从源点到终点的最小代价路径。
由于多段图将顶点划分为k个互不相交的子集,所以,多段图划分为k段,每一段包含顶点的一个子集。不失一般性,将多段图的顶点按照段的顺序进行编号,同一段内顶点的相互顺序无关紧要。假设图中的顶点个数为n,则源点 s s s的编号为0,终点 t t t的编号为 n − 1 n-1 n−1,并且,对图中的任何一条边 ( u , v ) (u, v) (u,v),顶点u的编号小于顶点v的编号。
设 s , s 1 , s 2 , … , s p , t s, s_1, s_2, …, s_p, t s,s1,s2,…,sp,t是从s到 t t t的一条最短路径,从源点s开始,设从s到下一段的顶点 s 1 s_1 s1已经求出,则问题转化为求从 s 1 s_1 s1到 t t t的最短路径,显然 s 1 , s 2 , … , s p , t s_1, s_2, …, s_p, t s1,s2,…,sp,t一定构成一条从s1到t的最短路径,如若不然,设s1, r1, r2, …, rq, t是一条从 s 1 s_1 s1到 t t t的最短路径,则 s , s 1 , r 1 , r 2 , … , r q , t s, s_1, r_1, r_2, …, r_q, t s,s1,r1,r2,…,rq,t将是一条从s到 t t t的路径且比 s , s 1 , s 2 , … , s p , t s, s_1, s_2, …, s_p, t s,s1,s2,…,sp,t的路径长度要短,从而导致矛盾。所以,多段图的最短路径问题满足最优性原理。
问题描述
输入:n个硬币的面值 { c 1 , c 2 , … , c n } , c i ∈ Z + \{c_1,c_2,…,c_n\},c_i∈Z^+ {c1,c2,…,cn},ci∈Z+
输出:面值的总金额最大值;
约束条件:硬币的原始位置互不相邻。
递推函数和边界条件: F ( n ) = m a x { c n + F ( n − 2 ) , F ( n − 1 ) } , n > 1 初始值为 F ( 0 ) = 0 , F ( 1 ) = c 1 F(n)=max\{c_n+F(n−2),F(n−1)\}, \qquad n>1 \qquad 初始值为F(0)=0, F(1)=c_1 F(n)=max{cn+F(n−2),F(n−1)},n>1初始值为F(0)=0,F(1)=c1
求解步骤,6个硬币的面值分别为{5,1,2,10,6,2}
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
C | 5 | 1 | 2 | 10 | 6 | 2 | |
F | 0 | 5 |
0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
5 | 1 | 2 | 10 | 6 | 2 | |
0 | 5 | 5 |
0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
5 | 1 | 2 | 10 | 6 | 2 | |
0 | 5 | 5 | 7 |
0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
5 | 1 | 2 | 10 | 6 | 2 | |
0 | 5 | 5 | 7 | 15 |
0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
5 | 1 | 2 | 10 | 6 | 2 | |
0 | 5 | 5 | 7 | 15 | 15 |
0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
5 | 1 | 2 | 10 | 6 | 2 | |
0 | 5 | 5 | 7 | 15 | 15 | 17 |
CoinRow(C[1..n])
//应用公式,自底向上求最大金额 在满足所选硬币 不相邻 的条件下,从一排硬币中选择最大金额的硬币
//输入:数组c[1..n]保存n个硬币的面值; 输出:可选硬币的最大金额
F[0] ←0;F[1] ←c[1];
for i ← 2 to n do
F[i] ←max(c[i]+F[i-2],F[i-1])
return F[n]
算法分析:耗费 Θ ( n ) \Theta(n) Θ(n)的时间和 Θ ( n ) \Theta(n) Θ(n)的空间。
问题描述
输入:找零金额n,m种面值为 { d 1 , d 2 , … , d m } , d i ∈ Z + . d 1 < d 2 < … < d m \{d_1,d_2,…,d_m\},d_i∈Z^+. d_1
{d1,d2,…,dm},di∈Z+.d1<d2<…<dm ;输出:硬币的总数最小值k;
约束条件: ∑ i = 1 k d i = n ∑_{i=1}^k d_i=n ∑i=1kdi=n。
递推函数、边界条件:
F ( n ) = m i n j : n ≥ d j F ( n − d j ) + 1 , n > 0 , F ( 0 ) = 0 F(n)=min_{j:n≥d_j}{F(n−d_j)}+1, \qquad n>0, F(0)=0 F(n)=minj:n≥djF(n−dj)+1,n>0,F(0)=0
求解步骤,n=6,币值为1,3,4的硬币
ChangeMaking(D[1..m],n)
//应用动态规划法求解找零问题,找出使硬币加起来等于n时所需最少的硬币数目
//其中d1
//输入:正整数n,以及用于表示币值的递增数组D[1..m],D[1]=1
//输出:总金额等于n的硬币最少的数目
F[0] ←0;
for i 1 to n do
temp ←∞;j ←1
while(j≤m and i≥D[j]
temp ←min(F[i-D[j]],temp)
j=j+1
F[i]=temp+1
return F[n]
算法分析:耗费 Θ ( m n ) \Theta(mn) Θ(mn)的时间和 Θ ( n ) \Theta(n) Θ(n)的空间。
问题描述: n ∗ m n*m n∗m的矩阵,矩阵中的元素上放置了一枚硬币,左上方的机器人需要收集尽可能多的硬币并把它们带到右下方的单元格。求解机器人能收集到的最大硬币数。
约束条件:机器人只能从当前位置向右移动或向下移动一格。
递推函数、边界条件:( c i j = 1 代表有硬币,为 0 无硬币 c_{ij}=1代表有硬币,为0无硬币 cij=1代表有硬币,为0无硬币) F ( i , j ) = m a x { F ( i − 1 , j ) , F ( i , j − 1 ) } + c i j , 其中 F ( 0 , j ) = 0 F ( i , 0 ) = 0 1 ≤ i ≤ n 且 1 ≤ j ≤ m F(i,j)=max\{F(i−1,j), F(i,j−1)\}+c_{ij},其中F(0,j)=0 \; F(i,0)=0\qquad 1≤ i≤n且1≤j≤m F(i,j)=max{F(i−1,j),F(i,j−1)}+cij,其中F(0,j)=0F(i,0)=01≤i≤n且1≤j≤m
求解步骤
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
1 | ● | |||||
2 | ● | ● | ||||
3 | ● | ● | ||||
4 | ● | ● | ||||
5 | ● | ● |
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
1 | 0 | 0 | 0 | 0 | 1 | 1 |
2 | 0 | 1 | 1 | 2 | 2 | 2 |
3 | 0 | 1 | 1 | 3 | 3 | 4 |
4 | 0 | 1 | 2 | 3 | 3 | 5 |
5 | 1 | 1 | 2 | 3 | 4 | 5 |
RobotCoinCollection(C[1..n,1..m])
//应用动态规划法计算机器人在n*m木板上所能收集的最大硬币数
//机器人从c[1,1]出发,每次向右或向下移动
//输入:矩阵C[1..n,1..m]
//输出:机器人在单元格(n,m)中收集到的最大硬币数
F[1,1] ←C[1,1];
for j ← 2 to m do F[1,j] ←F[1,j-1]+c[1,j]
for i ← 2 to n do
F[i,1] ←F[i-1,1]+c[i,1]
for j ← 2 to m do
F[i,j] ←max(F[i-1,j],F[i,j-1])+C[i,j]
return F[n,m]
算法分析:耗费 Θ ( m n ) \Theta(mn) Θ(mn)的时间和 Θ ( m n ) \Theta(mn) Θ(mn)的空间。
问题描述:m元钱,n项投资, f i ( x ) f_i (x) fi(x)为将 x 元投入第 i 个项目的效益。
目标函数: m a x { f 1 ( x 1 ) + f 2 ( x 2 ) + … + f n ( x n ) } max \{f_1(x_1) + f_2(x_2) + … + f_n(x_n) \} max{f1(x1)+f2(x2)+…+fn(xn)}
约束条件: x 1 + x 2 + … + x n = m , x i ∈ N x_1 + x_2 + … + x_n = m,x_i \in N x1+x2+…+xn=m,xi∈N
实例:5万元钱,4个项目,效益函数如下表所示
x | f 1 ( x ) f_1(x) f1(x) | f 2 ( x ) f_2(x) f2(x) | f 3 ( x ) f_3(x) f3(x) | f 4 ( x ) f_4(x) f4(x) |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
1 | 11 | 0 | 2 | 20 |
2 | 12 | 5 | 10 | 21 |
3 | 13 | 10 | 30 | 22 |
4 | 14 | 15 | 32 | 23 |
5 | 15 | 20 | 40 | 24 |
子问题划分和优化函数
设 F k ( x ) F_k(x) Fk(x) 表示 x元钱投给前k 个项目的最大效益。假设知道 p 元钱( p ≤ x p \leq x p≤x)投给前 k − 1 k-1 k−1个项目的最大效益,决定 x 元钱投给前 k 个项目的分配方案。
递推方程和边界条件
F k ( x ) = m a x 0 ≤ x k ≤ x { f k ( x k ) + F k − 1 ( x − x k ) } , k > 1 F 1 ( x ) = f 1 ( x ) F_k(x) = max_{0\leq x_k \leq x} \{ f_k(x_k) + F_{k-1}(x-x_k) \}, k >1 \qquad F_1(x) = f_1(x) Fk(x)=max0≤xk≤x{fk(xk)+Fk−1(x−xk)},k>1F1(x)=f1(x)
x k ( x ) x_k (x) xk(x)为标记函数,即 F k ( x ) F_k (x) Fk(x) 取得最大值时应分给第 k 个项目的钱数。
x | F 1 ( x ) x 1 ( x ) F_1(x) \quad x_1(x) F1(x)x1(x) | F 2 ( x ) x 2 ( x ) F_2(x) \quad x_2(x) F2(x)x2(x) | F 3 ( x ) x 3 ( x ) F_3(x) \quad x_3(x) F3(x)x3(x) | F 4 ( x ) x 4 ( x ) F_4(x) \quad x_4(x) F4(x)x4(x) |
---|---|---|---|---|
1 | 11 1 | 11 0 | 11 0 | 20 1 |
2 | 12 2 | 12 0 | 13 1 | 31 1 |
3 | 13 3 | 16 2 | 30 3 | 33 1 |
4 | 14 4 | 21 3 | 41 3 | 50 1 |
5 | 15 5 | 26 4 | 43 4 | 61 1 |
问题描述:一个旅行者准备随身携带一个背包. 可以放入背包的物品有n 种, 每种物品的重量和价值分别为 w j , v j w_j , v_j wj,vj 。如果背包的最大重量限制是 b, 怎样选择放入背包的物品以使得背包的价值最大?
目标函数: m a x ∑ j = 1 n v j x j 约束条件: ∑ j = 1 n w j x j ≤ b x j ∈ N 目标函数: \quad max \sum_{j=1}^{n} v_j x_j \\ 约束条件: \quad \sum_{j=1}^{n} w_j x_j \leq b \quad x_j \in N 目标函数:maxj=1∑nvjxj约束条件:j=1∑nwjxj≤bxj∈N
子问题划分:设 F ( i , j ) F(i,j) F(i,j)为该实例的最优解的物品总价值。 F ( i − 1 , j ) F(i−1,j) F(i−1,j)表示在不包括第 i i i个物品的子集中,最优子集的价值; v i + F ( i − 1 , j − w i ) v_i+F(i−1,j−w_i) vi+F(i−1,j−wi)表示包括第 i i i个物品的子集中 ( j − w i ≥ 0 ) (j−w_i≥0) (j−wi≥0),最优子集是由该物品和前 i − 1 i-1 i−1个物品中能放进去承重量为 j − w i j−w_i j−wi的背包的最优子集组成。递推函数:
F ( i , j ) = { m a x { F ( i − 1 , j ) , v i + F ( i − 1 , j − w i ) } j − w i ≥ 0 F ( i − 1 , j ) j − w i ≤ 0 F(i,j) = \begin{cases} max\{F(i−1,j),v_i+F(i−1,j−w_i)\} \qquad j−w_i≥0 \\ F(i−1,j) \qquad j−w_i≤0 \end{cases} F(i,j)={max{F(i−1,j),vi+F(i−1,j−wi)}j−wi≥0F(i−1,j)j−wi≤0
边界条件: 当 j ≥ 0 时, F ( 0 , j ) = 0 ; 当 i ≥ 0 时, F ( i , 0 ) = 0 当j≥0时,F(0,j)=0;当i≥0时,F(i,0)=0 当j≥0时,F(0,j)=0;当i≥0时,F(i,0)=0。
实例:(承重量W=5)
物品 | 重量 | 价值 |
---|---|---|
1 | 2 | 12 |
2 | 1 | 10 |
3 | 3 | 20 |
4 | 2 | 15 |
设 v 1 = 12 , v 2 = 10 , v 3 = 20 , v 4 = 15 w 1 = 2 , w 2 = 1 , w 3 = 3 , w 4 = 2 v_1 = 12, v_2 = 10, v_3 = 20, v_4 = 15 \\ w_1 = 2, w_2 = 1, w_3 = 3, w_4 = 2 v1=12,v2=10,v3=20,v4=15w1=2,w2=1,w3=3,w4=2,则F(i,j)计算表如下:
物品i 承重j | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 12 | 12 | 12 | 12 |
2 | 0 | 10 | 12 | 22 | 22 | 22 |
3 | 0 | 10 | 12 | 22 | 30 | 32 |
4 | 0 | 10 | 15 | 25 | 30 | 37 |
效率分析:时间复杂度 Ѳ ( n w ) Ѳ(nw) Ѳ(nw),空间复杂度 Ѳ ( n w ) Ѳ(nw) Ѳ(nw)
记忆化
求解给定问题时有些较小的子问题的解常常不是必需的。
物品i 承重j | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 12 | 12 | 12 | 12 |
2 | 0 | -1 | 12 | 22 | -1 | 22 |
3 | 0 | -1 | -1 | 22 | -1 | 32 |
4 | 0 | -1 | -1 | -1 | -1 | 37 |
背包问题的递归算法
MFKnapsack(i,j)//对背包问题实现记忆功能的方法
//输入:一个非负整数i表示先考虑的物品的数量,一个非负整数j表示背包的承重量
//输出:前i个物品的最优可行子集的价值
//注意:我们把输入数组weights[1..n],values[1..n]和表格F[0..n,0..w]作为全局变量
//除了行0和列0用0初始化以外,其余所有单元格用-1初始化
if F[i,j]< 0
if j< weights[i]
value ← MFKnapsack(i-1,j)
else
value←max(MFKnasack(i-1,j),
values[i]+MFKnasack(i-1,j-weights[i]))
F[i,j] ← value
return F[i,j]
包含n个键的二叉查找树的总数量等于第n个卡特兰数: n > 0 时, c ( n ) = 1 1 + n C 2 n n n>0时,c(n)=\frac{1}{1+n}C_{2n}^{n} n>0时,c(n)=1+n1C2nn
二叉排序树数据元素存取概率的分布
给定序列 S = < x 1 , x 2 , … , x n > S=
S=<x1,x2,…,xn>,空隙为 ( x 0 , x 1 ) , ( x 1 , x 2 ) , … , ( x n − 1 , x n ) , ( x n , x n + 1 ) (x_0,x_1),(x_1,x_2),…,(x_n−1,x_n),(x_n,x_n+1) (x0,x1),(x1,x2),…,(xn−1,xn),(xn,xn+1),其中 x 0 = − ∞ , x n + 1 = + ∞ x_0=−∞,x_n+1=+∞ x0=−∞,xn+1=+∞。设 x 在 x i 的概率为 b i , x 在 ( x i , x i + 1 ) 的概率为 a i x在x_i的概率为b_i,x在(x_i,x_i+1)的概率为a_i x在xi的概率为bi,x在(xi,xi+1)的概率为ai,则S的存取概率分布如下:
P = < a 0 , b 1 , a 1 , b 2 , a 2 , … , b n , a n > P=P=<a0,b1,a1,b2,a2,…,bn,an>
平均比较次数的计算为:
t = ∑ i = 1 n b i ( 1 + d ( x i ) ) + ∑ j = 0 n a j d ( L j ) 结点 x i 在 T 中的深度是 d ( x i ) , 空隙 L j 的深度为 d ( L j ) t = \sum_{i=1}^{n} b_i(1+d(x_i)) + \sum_{j=0}^{n} a_j d(L_j) \qquad 结点 x_i 在T中的深度是d(x_i),空隙L_j的深度为d(L_j) t=i=1∑nbi(1+d(xi))+j=0∑najd(Lj)结点xi在T中的深度是d(xi),空隙Lj的深度为d(Lj)
最优二叉树即为平均比较次数最少的二分检索树
子问题划分
子问题概率之和
优化函数的递推方程
在所有 k k k的情况下, m [ i , j ] k m[i, j]_k m[i,j]k的最小值初值 m [ i , i − 1 ] = 0 m[i,i−1]=0 m[i,i−1]=0对应于空的子问题。例如: S = < A , B , C , D , E > S= S=<A,B,C,D,E>,取A作根, i = 1 , k = 1 i=1,k=1 i=1,k=1,左边子问题为空树对应于: S [ 1 , 0 ] , m [ 1 , 0 ] = 0 S[1,0],m[1,0]=0 S[1,0],m[1,0]=0的情况。
实例说明
键 A B C D 查找概率 0.1 0.2 0.4 0.3
- 初始表格如下
主表 0 1 2 3 4 1 0 0.1 2 0 0.2 3 0 0.4 4 0 0.3 5 0 r o o t 表 0 1 2 3 4 1 1 2 2 3 3 4 4 5 主表 \\ \begin{array}{c|c c c c c|} &0&1&2&3&4\\ \hline 1 &0& 0.1 & & & \\ 2 & & 0 & 0.2 & & \\ 3 & & & 0 &0.4& \\ 4 & & & & 0 & 0.3\\ 5 & & & & & 0 \\ \hline \end{array} \qquad \\ root表 \\ \begin{array}{c|c c c c c |} &0&1&2&3&4\\ \hline 1 & & 1 & & & \\ 2 & & & 2 & & \\ 3 & & & & 3 & \\ 4 & & & & & 4 \\ 5 & & & & & \\ \hline \end{array} 主表123450010.1020.2030.4040.30root表12345011223344
m [ 1 , 2 ] = m i n { k = 1 : m [ 1 , 0 ] + m [ 2 , 2 ] + w [ 1 , 2 ] = 0 + 0.2 + 0.3 = 0.5 k = 2 : m [ 1 , 1 ] + m [ 3 , 2 ] + w [ 1 , 2 ] = 0.1 + 0 + 0.3 = 0.4 = 0.4 其中, w [ i , j ] = ∑ s = i j p s m[1,2]=min \begin{cases} k=1:m[1,0]+m[2,2]+w[1,2]=0+0.2+0.3=0.5 \\ k=2:m[1,1]+m[3,2]+w[1,2]=0.1+0+0.3=0.4 \end{cases} =0.4 \qquad 其中,w[i,j]=\sum_{s=i}^{j} p_s m[1,2]=min{k=1:m[1,0]+m[2,2]+w[1,2]=0+0.2+0.3=0.5k=2:m[1,1]+m[3,2]+w[1,2]=0.1+0+0.3=0.4=0.4其中,w[i,j]=s=i∑jps
- 最终表格如下
主表 0 1 2 3 4 1 0 0.1 0.4 1.1 1.7 2 0 0.2 0.8 1.4 3 0 0.4 1.0 4 0 0.3 5 0 r o o t 表 0 1 2 3 4 1 1 2 3 3 2 2 3 3 3 3 3 4 4 5 主表 \\ \begin{array}{c|c c c c c|} &0&1&2&3&4\\ \hline 1 &0& 0.1 & 0.4 &1.1 &1.7\\ 2 & & 0 & 0.2 &0.8 &1.4\\ 3 & & & 0 &0.4 &1.0\\ 4 & & & & 0 & 0.3\\ 5 & & & & & 0 \\ \hline \end{array} \qquad \\ root表 \\ \begin{array}{c|c c c c c |} &0&1&2&3&4\\ \hline 1 & & 1 & 2 & 3 & 3 \\ 2 & & & 2 & 3 & 3 \\ 3 & & & & 3 & 3 \\ 4 & & & & & 4 \\ 5 & & & & & \\ \hline \end{array} 主表123450010.1020.40.2031.10.80.4041.71.41.00.30root表12345011222333343334
算法分析
i , j i,j i,j的所有组合 O ( n 2 ) O(n^2) O(n2)种,每种对不同的 k k k进行计算, k = O ( n ) k=O(n) k=O(n)。时间复杂度为 O ( n 3 ) O(n^3) O(n3),空间复杂度为 O ( n 2 ) O(n^2) O(n2)
计算有向图的传递闭包
一个n个顶点有向图的传递闭包可以定义为一个n阶布尔矩阵 T = { t i j } T=\{t_{ij}\} T={tij},如果第 i i i个顶点和第 j j j个顶点之间存在一条有效路径(即长度大于0的有效路径),矩阵第 i ( 1 ≤ i ≤ n ) i(1≤i≤n) i(1≤i≤n)行第 j ( 1 ≤ j ≤ n ) j(1≤j≤n) j(1≤j≤n)列的元素为1,否则, t i j t_{ij} tij为0。
- 第一个矩阵 R ( 0 ) R^{(0)} R(0)反映不包含中间点的路径,即为邻接矩阵本身,框起来的行和列计算 R ( 1 ) R^{(1)} R(1)
- 第二个矩阵 R ( 1 ) R^{(1)} R(1)反映编号不大于1的中间顶点(即a点)的路径(有一条从d到b的新路径),框起来的行和列计算 R ( 2 ) R^{(2)} R(2)
- 第三个矩阵 R ( 2 ) R^{(2)} R(2)反映编号不大于2的中间顶点(即a和b)的路径(有两条新路径)
计算全部的最短路径
Floyd算法和Warshall 算法类似,通过一系列的n阶布尔矩阵来计算一个具有n个顶点带权图的距离矩阵。
引例:找零问题,面值为 { d 1 = 25 , d 2 = 10 , d 3 = 5 , d 4 = 1 } \{ d_1=25,d_2=10,d_3=5,d_4=1 \} {d1=25,d2=10,d3=5,d4=1}的硬币,给出48美分的找零?
遵循一种从当前几种可能的选择中确定一个最佳选择序列的逻辑策略。
第一步,给出 d 1 = 25 d_1=25 d1=25;第二步,不能给出 d 1 = 25 d_1=25 d1=25,违反了问题约束,因此给出 d 2 = 10 d_2=10 d2=10;第三步,给出 d 2 = 10 d_2=10 d2=10;第四步,给出3个 d 4 = 1 d_4=1 d4=1。
对于某些金额,贪婪算法无法给出一个最优解。如 { d 1 = 25 , d 2 = 10 , d 3 = 1 } \{ d_1=25,d_2=10,d_3=1 \} {d1=25,d2=10,d3=1},而 n = 30 n=30 n=30。
贪婪技术的核心,每一步的选择必须满足以下条件:
贪心算法的特点:
强连通分量
强连通分量和无向图中的连通性一样,有向图中的强连通性也是一种顶点之间的等价关系,因为它有着以下性质:
自反性:任意顶点v和自己都是强连通的。
对称性:如果v和w是强连通的,那么w和v也是强连通的。
传递性:如果v和w是强连通的且w和x也是强连通的,那么v和x也是强连通的。
作为一种等价关系,强连通性将所有顶点分为了一些等价类,每个等价类都是由相互均为强连通的顶点的最大子集组成的,我们将这些子集称为强连通分量。
一个含有V个顶点的有向图含有1~V个强连通分量,一个强连通图只含有一个强连通分量,而一个有向无环图中则含有V个强连通分量。
最近邻点策略生成加权连通图的最小生成树(MST)——包含图中所有顶点且权重最小的连通无环子图。
算法描述:
1)输入:一个加权连通图,其中顶点集合为V,边集合为E;
2)初始化: V n e w = { x } V_{new} = \{x\} Vnew={x},其中x为集合V中的任一节点(起始点), E n e w = { } E_{new} = \{\} Enew={},为空;
3)重复下列操作,直到 V n e w = V V_{new} = V Vnew=V:
a.在集合E中选取权值最小的边 < u , v > <u,v>,其中u为集合 V n e w V_{new} Vnew中的元素,而v不在 V n e w V_{new} Vnew集合当中,并且 v ∈ V v∈V v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
b.将v加入集合 V n e w V_{new} Vnew中,将边加入集合 E n e w E_{new} Enew中;
4)输出:使用集合 V n e w V_{new} Vnew和 E n e w E_{new} Enew来描述所得到的最小生成树。
简单证明Prim算法:
反证法:假设prim生成的不是最小生成树
1)设prim生成的树为 G 0 G_0 G0;
2)假设存在 G m i n G_{min} Gmin使得 c o s t ( G m i n ) < c o s t ( G 0 ) cost(G_{min})
3)将加入 G 0 G_0 G0中可得一个环,且不是该环的最长边(这是因为 < u , v > ∈ G m i n ∈G_{min} <u,v>∈Gmin)
4)这与prim每次生成最短边矛盾,故假设不成立,命题得证。
算法时间复杂度:邻接矩阵: O ( v 2 ) O(v^2) O(v2) 邻接表: O ( e l o g 2 v ) O(elog_{2}v) O(elog2v),适用于稠密图(顶点多,边少)
最短边策略生成加权连通图的最小生成树。
算法描述:
1)记Graph中有v个顶点,e个边;
2)新建图 G n e w G_{new} Gnew, G n e w G_{new} Gnew中拥有原图中相同的e个顶点,但没有边;
3)将原图Graph中所有e个边按权值从小到大排序;
4)Loop:从权值最小的边开始遍历每条边 直至图Graph中所有的节点都在同一个连通分量中
if 这条边连接的两个节点于图 G n e w G_{new} Gnew中不在同一个连通分量中
添加这条边到图 G n e w G_{new} Gnew中
简单证明Kruskal算法:
对图的顶点数n做归纳,证明Kruskal算法对任意n阶图适用。
归纳基础:
n=1,显然能够找到最小生成树。
归纳过程:
假设Kruskal算法对 n ≤ k n≤k n≤k阶图适用,那么,在 k + 1 k+1 k+1阶图G中,我们把最短边的两个端点a和b做一个合并操作,即把u与v合为一个点 v ′ v' v′,把原来接在u和v的边都接到v’上去,这样就能够得到一个k阶图 G ′ G' G′(u,v的合并是 k + 1 k+1 k+1少一条边), G ′ G' G′最小生成树 T ′ T' T′可以用Kruskal算法得到。
以下证明 T ′ + < u , v > T'+{} T′+<u,v>是G的最小生成树
用反证法,如果 T ′ + < u , v > T'+{} T′+<u,v>不是最小生成树,最小生成树是T,即 W ( T ) < W ( T ′ + { < u , v > } ) W(T)
\}) W(T)<W(T′+{<u,v>})。显然T应该包含 < u , v > <u,v>,否则,可以用 < u , v > <u,v>加入到T中,形成一个环,删除环上原有的任意一条边,形成一棵更小权值的生成树。而 T − { < u , v > } T-\{\} T−{<u,v>},是 G ′ G' G′的生成树。所以 W ( T − { < u , v > } ) < = W ( T ′ ) W(T-\{\})<=W(T') W(T−{<u,v>})<=W(T′),也就是 W ( T ) < = W ( T ′ ) + W ( < u , v > ) = W ( T ′ + { < u , v > } ) W(T)<=W(T')+W()=W(T'+\{\}) W(T)<=W(T′)+W(<u,v>)=W(T′+{<u,v>}),产生了矛盾。于是假设不成立, T ′ + { < u , v > } T'+\{\} T′+{<u,v>}是G的最小生成树,Kruskal算法对 k + 1 k+1 k+1阶图也适用。由数学归纳法,Kruskal算法得证。
时间复杂度: O ( e l o g 2 e ) O(elog_{2}e) O(elog2e) e为图中的边数,适用于稀疏图(边少顶点多)
求解单源最短路径,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。注意:该算法要求图中不存在负权边。
算法思想:
设 G = ( V , E ) G=(V,E) G=(V,E)是一个带权有向图,把图中顶点集合V分成两组:
第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将加入到集合S中,直到全部顶点都加入到S中,算法就结束了);
第二组为其余未确定最短路径的顶点集合(用U表示),按最短路径长度的递增次序依次把第二组的顶点加入S中。
在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。
算法描述:
(1) 初始时,S只包含起点s;U包含除s外的其他顶点,且U中顶点的距离为"起点s到该顶点的距离"[例如,U中顶点v的距离为(s,v)的长度,然后s和v不相邻,则v的距离为∞]。
(2) 从U中选出"距离最短的顶点k",并将顶点k加入到S中;同时,从U中移除顶点k。
(3) 更新U中各个顶点到起点s的距离。之所以更新U中顶点的距离,是由于上一步中确定了k是求出最短路径的顶点,从而可以利用k来更新其它顶点的距离;例如,(s,v)的距离可能大于(s,k)+(k,v)的距离。
(4) 重复步骤(2)和(3),直到遍历完所有顶点。
实例说明:(下图以顶点D为起点)
算法分析:Dijkstra算法的时间效率依赖于用来实现优先队列的数据结构以及用来表示输入图本身的数据结构。若使用邻接矩阵实现,属于 Θ ( V 2 ) \Theta(V^2) Θ(V2);若使用邻接链表实现。属于 Θ ( E l o g V ) \Theta(ElogV) Θ(ElogV)。
最优前缀码问题:给定字符集 C = { x 1 , x 2 , … , x n } C=\{x_1, x_2, … , x_n\} C={x1,x2,…,xn}和每个字符的频率 f ( x i ) , i = 1 , 2 , … , n f(x_i), i=1,2,…,n f(xi),i=1,2,…,n, 求关于C 的一个最优前缀码。
哈夫曼算法:
Huffman(C) //求解最优前缀编码问题
//输入:C={x1, x2, … , xn}, f(xi), i=1, 2, … , n.
//输出:Q //队列
n ← |C|
Q←C //按频率递增构成队列Q
for i←1 to n←1 do
z←Allocate-Node() // 生成结点 z
z.left←Q中最小元 // 取出Q最小元作z的左儿子
z.right←Q中最小元 // 取出Q最小元作z的右儿子
f(z)←f(x)+f(y)
Insert(Q,z) // 将 z 插入Q
return Q
将几何描述“翻译”成更具算法精确性的代数语言。标准形式要满足的要求如下:
- 须是一个最大化问题;
- 所有的约束都必须用线性方程的形式表示(除了非负约束);
- 所有变量都非负。
- 可行解——满足约束条件和非负条件的变量;
- 可行域——全体可行解;
- 最优解——目标函数值最小(最大)的可行解;
- 最优值——最优解的目标函数值。
生产计划问题:用 3 种原料混合配制 2 种清洁剂,这 2 种清洁剂应各配制多少才能使总价值最大?
原料1 原料2 原料3 售价(万元/吨)
清洁剂 A 0.25 0.50 0.25 12
清洁剂 B 0.50 0.50 15
存量 (吨) 120 150 50解: 设清洁剂 A 和 B 分别配制 x 和 y
m a x z = 12 x + 15 y 目标函数 s . t . 0.25 x + 0.50 y ≤ 120 0.50 x + 0.50 y ≤ 150 约束条件 0.25 x ≤ 50 x ≥ 0 , y ≥ 0 非负条件 max z = 12x +15y \qquad 目标函数 \\ \begin{aligned} s.t. 0.25x + 0.50y &\leq 120 \\ 0.50x + 0.50y &\leq 150 \qquad 约束条件 \\ 0.25x &\leq 50 \\ x \geq 0, y \geq 0 \qquad 非负条件 \end{aligned} maxz=12x+15y目标函数s.t.0.25x+0.50y0.50x+0.50y0.25xx≥0,y≥0非负条件≤120≤150约束条件≤50
其中, O ( 0 , 0 ) , A ( 0 , 240 ) , B ( 120 , 180 ) , C ( 200 , 100 ) , D ( 200 , 0 ) O(0,0), A(0,240),B(120,180), C(200,100),D(200,0) O(0,0),A(0,240),B(120,180),C(200,100),D(200,0)。最优解为 x = 120 , y = 180 x=120,y=180 x=120,y=180,即点B处;最优值为 z = 4140 z=4140 z=4140。
解的4种可能:
可行域是一个凸多边形 (可能无界, 也可能是空集),具有有限数量的顶点,称之为极点。如果有最优解, 则一定可以在凸多边形的顶点取到(极点定理)
将同一目标函数所有系数 c i c_i ci都加上负号变为 − c i -c_i −ci即可。若问题的约束是不等式,可以用一个等价的等式替换它,方法是在等式中加入松弛变量。
m a x z = 3 x 1 − 2 x 2 + x 3 s . t . x 1 + 3 x 2 − 3 x 3 ≤ 10 4 x 1 − x 2 − 5 x 3 ≤ − 30 x 1 ≥ 0 , x 2 ≥ 0 , x 3 任意(自由变量) 化为标准形: m i n z ′ = − 3 x 1 + 2 x 2 − x 3 s . t . x 1 + 3 x 2 − 3 x 3 + u = 10 − 4 x 1 + x 2 + 5 x 3 − v = 30 x 1 ≥ 0 , x 2 ≥ 0 , u ≥ 0 , v ≥ 0 u , v 为松弛变量 max z = 3x_1 -2x_2 +x_3 \\ \begin{aligned} s.t. \; x1 + 3x_2 - 3x_3 &\leq 10 \\ 4x_1 - x_2 - 5x_3 &\leq -30 \end{aligned} \\ x_1 \geq 0,x_2 \geq 0 ,x_3任意(自由变量) \\ 化为标准形:min z^{'} = -3x_1 + 2x_2 - x_3 \\ \begin{aligned} s.t. \; x1 + 3x_2 - 3x_3 + u &= 10 \\ -4x_1 + x_2 + 5x_3 - v &= 30 \end{aligned} \\ x_1 \geq 0,x_2 \geq 0 ,u \geq 0,v \geq 0 \quad u,v为松弛变量 maxz=3x1−2x2+x3s.t.x1+3x2−3x34x1−x2−5x3≤10≤−30x1≥0,x2≥0,x3任意(自由变量)化为标准形:minz′=−3x1+2x2−x3s.t.x1+3x2−3x3+u−4x1+x2+5x3−v=10=30x1≥0,x2≥0,u≥0,v≥0u,v为松弛变量
设A的秩为m, A的m个线性无关的列向量称作标准形的基(记为B),其中的列向量的变量称作基变量。基变量构成的向量记作 x B x_B xB, 非基变量构成的向量记作 x N x_N xN。 令 x N = 0 x_N = 0 xN=0, 等式约束变成 B x B = b Bx_B = b BxB=b
解得 x B = B − 1 b x_B=B_{-1}b xB=B−1b。 这个向量 x 满足约束 A x = b Ax=b Ax=b且非基变量全为 0,称作关于基 B 的基本解 。 如果 x 是一个基本解且 x ≥ 0 x\geq 0 x≥0, 则称 x 是一个基本可行解,对应的基 B为可行基。基本可行解的性质
- A x = b Ax = b Ax=b 的解 α \alpha α是基本解 ⇔ x \Leftrightarrow x ⇔x 中非零分量对应的列向量线性无关;
- 如果标准形有可行解, 则必有基本可行解;
- 如果标准形有最优解, 则必存在一个基本可行解是最优解
(1) 确定初始基本可行解;
(2) 检查当前的基本可行解:
(3) 重复(2)。
实例说明:
设容量网络 N = < V , E , c , s , t > N =
N=<V,E,c,s,t>, f f f 是$ N $上的一个可行流。 $N $中流量等于容量的边称作饱和边 , 流量小于容量的边称作非饱和边。 流量等于 0 的边称作零流边, 流量大于 0 的边称作非零流边。 不考虑边的方向, N中从顶点 $i $到 $j 的一条边不重复的路径称作 ∗ ∗ i − j 链 ∗ ∗ 。 i − j 链的方向是从 的一条边不重复的路径称作**i-j 链**。 i-j 链的方向是从 的一条边不重复的路径称作∗∗i−j链∗∗。i−j链的方向是从 i$ 到$ j$。 链中与链的方向一致的边称作前向边,与链的方向相反的边称作后向边。
如果链中所有前向边都是非饱和的, 所有后向边都是非零流的, ,则称这条链为 i-j 增广链。
FF算法基本步骤
从给定的初始可行流 (通常取零流) 开始,寻找关于当前可行流的 s-t 增广链P,修改链上的流量,得到一个新的可行流。 重复进行, 直到不存在 s-t 增广链为止。
从 s 开始, 逐个给顶点作标号,直到 t 得到标号为止。 顶点 j 得到标号表示已找到从 s 到 j的增广链,标号为$ (l_j,\delta_j) 。其中 ∗ ∗ 。其中 ** 。其中∗∗\delta_j$ 等于这条到 j 的链上所有前向边的容量与流量之差以及所有后向边的流量的最小值。**
$ l_j =+i 表示链是从 i 到 j 且 < i , j > 是前向边, 表示链是从 i 到 j 且是前向边, 表示链是从i到j且<i,j>是前向边, l_j =-i$ 表示链是从 i 到 j 的且是后向边。