一、递归与分治法
1、二分搜索
简介:
使用折半查找在有序数组中查找某一个数字
口语讲述算法逻辑:
由于数组是有序的,每次取数组中间位置的数与想要查找的数字作比较,就可以得知想要查找的数字在中间位置的左边还是右边,由此排除一半的可能性
书上代码分析以及详细注释:
/**
* @description 该方法通过二分查找法在某个有序数组中找某个数字
* @return 查找的数字在有序数组中的下标
* @param a 有序数组
* @param x 想要查找的那个
* @param n 数组长度,这个显得多余,因为可以使用数组去获取长度
*/
public static int binarySearch(int []a, int x, int n) {
// 搜索范围的最左边,而不是数组的最左边,因为left会随着搜索范围改变而改变
int left = 0;
// 搜索范围的最右边
int right = n - 1;
// 什么情况下可以进入循环?数组中还有位置搜查过
// 如果最左边 <= 最右边,则进入循环
// 需要知道的是,在这里,left与right的位置上的数字,以及两者之间的位置是没有被 搜查过的,所以相等也是可以进入循环的
// 而最左边 > 最右边,显然是矛盾了
while(left <= right) {
// 获取相对中间位置的数组下标
// 虽然不一定是正中间,但是不会影响通过这个位置去划分左右两部分
int middle = (left + right) / 2;
// 需要找到了该数字,自然就是返回
if(x == a[middle])
return middle;
// 如果x大,假设a数组是顺序排序的,说明x在midden的右边,应该修改left的值
if(x > a[middle])
left = middle + 1;
// 上面有了等于与大于情况,所以这里必然是x < a[middle]
// 如果x小,假设a数组是顺序排序的,说明x在midden的左边,应该修改right的值
else
right = middle - 1;
}
// 找了所有位置都找不到,则返回-1
// 如果找到了,就会在while循环中返回了,不会执行到这里
return -1;
}
2、快速排序
简介:
将无序的数组按照一定顺序排序的相对快捷的一种排序算法
口语化快速理解:
- 现在有一个需要排序的数组:[5, 3, 1, 9, 8, 2, 4, 7]
- 回忆上面的二分查找做一个小逆推
- 二分查找:中间位置的数将大于和小于(当然也有等于的情况)它的数字分为两部分
- 快速排序:在数组中找一个数字(基准),将数组分为大于和小于该数组的两部分
- 举例:在上面需要排序的数组中抽出一个数字,可以随机,也可以指定
- 假设取数字5(如果选取了别的数字,由于算法的限制,需要将别的数字与5交换位置)
- 按照我们的思路,排序一次后变成了:[2, 3, 1, 4, 5, 8, 9, 7]
- 过程如下:
- 数组:[5, 3, 1, 9, 8, 2, 4, 7]
- 先从左到右找到一个大于5的数字,也就是9
- 然后从右到左找到一个小于5的数字,也就是4
- 再然后就是交换9和4的位置,变成了[5, 3, 1, 4, 8, 2, 9, 7]
- 再从左到右找到一个大于5的数字,也就是8
- 又从右到左找到一个小于5的数字,也就是2
- 再然后就是交换8和2的位置,变成了[5, 3, 1, 4, 2, 8, 9, 7]
- 最后找到从右到左的第一个小于5的数,也就是2
- 交换2与5的位置,变成了[2, 3, 1, 4, 5, 8, 9, 7],一次排序完成了
- 数字5的左边都是小于5的,右边都是大于5的,从最终排序的结果来看,5已经排序好了属于它自己的位置,因为最终排序好的数组中,数字5的左边肯定都是小于5的,右边肯定都是大于5的(假设是顺序排序)
- 二分查找不断二分,就可以找到数组,快速排序不断划分,就可以排序好数组
书上代码分析以及详细注释:
/**
* @description 该方法将数组中的下标为p到r的数组元素进行左右划分
* @param p 需要划分的数组元素的最左下标
* @param r 需要划分的数组元素的最右下标
*/
private static void quickSort(int p, int r) {
// 假设p == r,说明只有一个数组元素,就不用划分了
// 假设p > r,说明不存在数组元素了,也不用划分了
// 所以这里只需要判断p < r,其余情况不需要任何操作
if(p < r) {
// q是通过partition()这个方法获取到的划分后的基准的位置
// 比如说上面那个例子,数字5所在的位置下标是4,q就是4
// partition()是真正划分实际操作的方法
// partition是分割的意思
int q = partition(p, r);
// 继续上面的例子
// 左边自然就是p到q - 1
quickSort(p, q - 1);
// 右边自然就是q + 1到r
quickSort(q + 1, r);
}
}
/**
* 下面的数组的含义都为 需要划分的数组,而不是整个数组
* @description 根据传入的p与r,然后根据基准进行左右划分
* @return 划分后的基准所在的下标
* @param p 需要划分的数组元素的最左下标
* @param r 需要划分的数组元素的最右下标
*/
private static int partition(int p, int r) {
// Comparable是一个函数式接口(有且仅有一个抽象方法的接口),实现该接口的类就是对应类的比较器,只有一个compareTo()抽象方法
// 比如说,继承了Comparable<Integer>的类就是Integer类型使用Comparable去作比较时的比较器
// 了解的可以跳过,不了解的可以自己debug去看一下JDK源码
// 简单讲讲在该处的大致使用:
// Comparable x = 7;
// x.compareTo(0),7 > 0,compareTo()返回了1
// x.compareTo(7),7 = 7,compareTo()返回了0
// x.compareTo(9),7 < 9,compareTo()返回了-1
// x是用于比较的数字,x = a[p],说明本方法使用的基准是这部分的数组元素的首位数字
Comparable x = a[p];
// i变成了需要划分的数组元素的最左下标,为什么?请看后续
// j变成了需要划分的数组元素的最右下标 + 1,为什么?请看后续
int i = p, j = r + 1;
// 不断执行,直到完成划分
while(true) {
// a[++i]先执行i = i + 1,再执行a[i]
// 所以a[++i]代表的是 大于 需要划分的数组元素的最左小标的 元素
// 由于小于基准的元素是放置在左边的,所以compareTo()的结果< 0,说明是小于,而其本身就是在左边,所以不用操作
// 什么情况下不进入这个while循环?找到一个大于基准的数以及i >= r
// 找到大于基准的数很好理解,但是i为什么要大于等于r呢?i应该是可以等于r的啊?
// 我们可以简单执行一下,假设现在i是5,r是6
// a[++i]后,i变成了6,并且compareTo()返回了-1,如果是i <= r,则继续执行,就会发现,下一次执行的时候,a[++i]后,i变成了7,就会发现越界了
// 假设a[++i]变成了a[i++],那么就可以i <= r,不过此时的i初始化的时候就要变成了i = p + 1,而不是i = p,i的判断,是为了防止越界,当a[i]为之后一个元素时,结束循环
// 下面这个while,完成了一个任务:
// 从基准后一个位置开始,找到一个大于基准的元素的下标,如果后续的所有元素都小于基准,则返回最后一个元素的下标
// 这个下标就是i
while(a[++i].compareTo(x) < 0 && i < r);
// 执行到这里,找到了一个可以用于交换的下标i,i下标对应的数组大于x
// a[--j],先执行j = j - 1,再执行a[j]
// 由于前面写j = r + 1,所以a[--j]就是从下标r开始往左边的所有元素了
// 如果前面写j = r,就可以写成a[j++]了
// 下面的这个while,完成了一个任务:
// 从最后一个元素开始,找到一个小于基准的元素,为什么没有对j的限制条件?请继续看下去
while(a[--j].compareTo(x) > 0);
// 执行到这里,找到了一个可以用于交换的下标j,j下标对应的数组小于x
// 虽然while循环里面没有j的限制条件,是因为放在了这里,同时因为不可能所有都小于x,因为数组里面肯定至少就有一个x,而这个x就是数组的首个元素,所以限制条件天然存在
// 由于i的含义是已经遍历过的数字,也就是说经过判断了,而j也是如此
// 所以当i >= j了,说明所有位置都遍历过了,就不需要重复判断了
if(i >= j)
break;
// 大于的与小于的位置交换,维持有序性
// swap应该是一个交换位置的方法,a是数组,i与j是交互的位置
MyMath.swap(a, i, j);
}
// 执行到这里类似于上面举例排序后的:[5, 3, 1, 4, 2, 8, 9, 7]
// 就会发现5的位置还没放好,而此时j的指向刚好是从右到左的第一个小于5的数字的下标,也就是数字2对应的下标,就是4
// 为什么不用i?因为此时i是代表从左到右的第一个大于x的数,显然不适合与首位的5交换
// 下面就是交换的语句
a[p] = a[j];
a[j] = x;
// 返回j,此时的j就是x排序后的下标
return j;
}
进阶写法:
- 可以发现,上面我们使用的是需要划分的数组元素中的首位元素作为基准,我们最坏情况下,首位元素都是这部分元素中最大的一个,就会导致n的平方的时间复杂度,为了避免这种情况,我们可以随机选取元素,将最坏情况的可能性大大降低
- 那么我们应该怎么改代码?
- 在不变动partition()方法的前提下,我们只需要将随机得到的基准与首位交换即可,简单快捷
- 书上代码:
private static void quickSort(int p, int r) {
if(p < r) {
int q = partition(p, r);
randomizedQuickSort(p, q - 1);
randomizedQuickSort(q + 1, r);
}
}
private static int randomizedQuickSort(int p, int r) {
// 获取从p到r之间的一个随机数,random()这个方法并没有给出定义,但是使用上,只能是这个意思
int i = random(p, r);
// 把随机找到的这个数与首位的数字交换
MyMath.swap(a, i, p);
// 执行之前的操作
return partition(p, r);
}
二、动态规划
动态规划的基本性质(此处不作解释,请看后续使用):最优子结构性质和子问题重叠性质
动态规划的求解步骤(下面步骤为个人理解,更正式的说法请看书上描述):
1)证明是否拥有最优子结构
2)如果有,还需要刻画出结构特征(写出公式,说明每一个格子上的值,一定可以通过公式求得)
3)定义求解每一个格子的值的递归方法
4)从最小的子问题开始求解
5)获取最终的最优解
1、矩阵连乘
问题描述(课本原题描述):
给定n个矩阵{ A 1 , A 2 , ⋯ , A n A_1,A_2,\cdots,A_n A1,A2,⋯,An}, A i A_i Ai的维数为 P i − 1 ∗ P i P_i-1*P_i Pi−1∗Pi, A i 与 A i + 1 A_i与A_i+1 Ai与Ai+1是可乘的, i = 1 , 2 ⋯ , n − 1 i=1,2\cdots,n-1 i=1,2⋯,n−1。如何确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵的连乘积所需的数乘次数最少?
问题快速理解:
现在有三个矩阵,
A
1
=
2
∗
3
,
A
2
=
3
∗
4
,
A
3
=
4
∗
2
A_1=2*3,A_2=3*4,A_3=4*2
A1=2∗3,A2=3∗4,A3=4∗2,乘数的第一位是行,第二位是列。
第一个问题:为什么说可乘?
线性代数知识。简单来说,我们这里只需要注意,
A
1
A_1
A1的列数与
A
2
A_2
A2的行数相等,
A
2
A_2
A2的列数与
A
3
A_3
A3的行数相等,说明可乘。
第二个问题:为什么说确定计算次序?
举个例子,
A
1
∗
A
2
∗
A
3
A_1*A_2*A_3
A1∗A2∗A3是先
A
1
∗
A
2
A_1*A_2
A1∗A2,再将结果乘以
A
3
A_3
A3,而
A
1
∗
(
A
2
∗
A
3
)
A_1*(A_2*A_3)
A1∗(A2∗A3)则是先
A
2
∗
A
3
A_2*A_3
A2∗A3,再将结果乘以
A
1
A_1
A1,这就是计算次序的不同,所以需要确定是哪一个。
第三个问题:什么叫计算次序的连乘积所需的数乘次数最少?
还是用第二个问题中的例子来举例,假设的
A
1
,
A
2
,
A
3
如下
A_1,A_2,A_3如下
A1,A2,A3如下
A
1
:
A_1:
A1:
{
1
2
3
2
3
1
}
\left\{ \begin{matrix} 1 & 2 & 3 \\ 2 & 3 & 1 \\ \end{matrix} \right\}
{122331}
A
2
:
A_2:
A2:
{
2
3
4
5
3
4
5
2
5
4
3
2
}
\left\{ \begin{matrix} 2 & 3 & 4 & 5 \\ 3 & 4 & 5 & 2 \\ 5 & 4 & 3 & 2 \\ \end{matrix} \right\}
⎩
⎨
⎧235344453522⎭
⎬
⎫
A
3
:
A_3:
A3:
{
2
3
3
4
5
4
3
2
}
\left\{ \begin{matrix} 2 & 3 \\ 3 & 4 \\ 5 & 4 \\ 3 & 2 \\ \end{matrix} \right\}
⎩
⎨
⎧23533442⎭
⎬
⎫
A
1
(
2
∗
3
)
∗
A
2
(
3
∗
4
)
A_1(2*3)*A_2(3*4)
A1(2∗3)∗A2(3∗4)得到
A
12
=
2
∗
4
:
A_{12}=2*4:
A12=2∗4:
{
1
∗
2
+
2
∗
3
+
3
∗
5
1
∗
3
+
2
∗
4
+
3
∗
4
1
∗
4
+
2
∗
5
+
3
∗
3
1
∗
5
+
2
∗
2
+
3
∗
2
2
∗
2
+
3
∗
3
+
1
∗
5
2
∗
3
+
3
∗
4
+
1
∗
4
2
∗
4
+
3
∗
5
+
1
∗
3
2
∗
5
+
3
∗
2
+
1
∗
2
}
\left\{ \begin{matrix} 1*2+2*3+3*5 & 1*3+2*4+3*4 & 1*4+2*5+3*3 & 1*5+2*2+3*2 \\ 2*2+3*3+1*5 & 2*3+3*4+1*4 & 2*4+3*5+1*3 & 2*5+3*2+1*2 \\ \end{matrix} \right\}
{1∗2+2∗3+3∗52∗2+3∗3+1∗51∗3+2∗4+3∗42∗3+3∗4+1∗41∗4+2∗5+3∗32∗4+3∗5+1∗31∗5+2∗2+3∗22∗5+3∗2+1∗2}
2行4列,每一个位置都有3个乘法,使用到了乘法的次数:
2
∗
3
∗
4
2*3*4
2∗3∗4(就算不会矩阵等定义,也可以开始找规律了),显然
A
12
∗
A
3
A_{12}*A_3
A12∗A3的乘法次数也很明显:
2
∗
4
∗
2
2*4*2
2∗4∗2,所以
A
1
∗
A
2
∗
A
3
A_1*A_2*A_3
A1∗A2∗A3的总乘法次数:
2
∗
3
∗
4
+
0
+
2
∗
4
∗
2
=
40
2*3*4+0+2*4*2=40
2∗3∗4+0+2∗4∗2=40(这里解释一下为什么要加0,因为左部分的乘法次数 + 右部分的乘法次数 + 合并时的乘法次数才是最终乘数,结合下面这条可以更好理解)。所以
A
1
∗
(
A
2
∗
A
3
)
A_1*(A_2*A_3)
A1∗(A2∗A3)的总乘法次数:
0
+
3
∗
4
∗
2
+
2
∗
3
∗
2
=
36
0+3*4*2+2*3*2=36
0+3∗4∗2+2∗3∗2=36,显然对比第一种,第二种次数少,这个就是第三个问题的意义。(这里也补充一下,怎么求合并时的乘法次数,我们看上面,只要排序不变,
A
1
A_1
A1的行一直都在最后合并的时候用到和
A
3
A_3
A3的列一直都在最后合并的时候用到,因为
A
1
A_1
A1并没有去左乘,
A
3
A_3
A3并没有去右乘,那样我们还需要判断一个中间的分割点的值,其实就是分割点处左矩阵的列数,也是右矩阵的行数,两者相等,比如说,上面的
A
1
∗
(
A
2
∗
A
3
)
A_1*(A_2*A_3)
A1∗(A2∗A3),分割点的左矩阵就是
A
1
A_1
A1,右矩阵就是
A
2
A_2
A2)
分析(按照上述的做题步骤):
1.证明是否拥有最优子结构
看课本P30描述。(看不懂书上的举例,可以结合下面的例子理解)
2. 如果有,还需要刻画出结构特征(写出公式,说明每一个格子上的值,一定可以通过公式求得)
举个例子,比如说现在求
A
1
∗
A
2
∗
A
3
∗
A
4
A_1*A_2*A_3*A_4
A1∗A2∗A3∗A4的最小数乘次数。
只有一个数的,分别求出 A 1 , A 2 , A 3 , A 4 A_1,A_2,A_3,A_4 A1,A2,A3,A4,由于相乘是二元运算符,所以为0
有两个数的,分别求出 A 1 ∗ A 2 , A 2 ∗ A 3 , A 3 ∗ A 4 A_1*A_2,A_2*A_3,A_3*A_4 A1∗A2,A2∗A3,A3∗A4,这部分怎么求呢?比如说 A 1 ∗ A 2 A_1*A_2 A1∗A2,先将其分为两部分,把问题变为求出 A 1 和 A 2 A_1和A_2 A1和A2两部分的分别最优,由于每一部分只有一个,每一部分的乘数都是0,最终乘数就是 0 + 0 + A 1 ∗ A 2 0+0+A_1*A_2 0+0+A1∗A2
有三个数的,分别求出 A 1 ∗ A 2 ∗ A 3 , A 2 ∗ A 3 ∗ A 4 A_1*A_2*A_3,A_2*A_3*A_4 A1∗A2∗A3,A2∗A3∗A4,这部分怎么求呢?比如说 A 1 ∗ A 2 ∗ A 3 A_1*A_2*A_3 A1∗A2∗A3,可以先分为两部分,也两种分法, A 1 ∗ A 2 和 A 3 A_1*A_2和A_3 A1∗A2和A3, A 1 和 A 2 ∗ A 3 A_1和A_2*A_3 A1和A2∗A3,显然 A 1 ∗ A 2 A_1*A_2 A1∗A2和 A 2 ∗ A 3 A_2*A_3 A2∗A3都可以在2、中获取,剩下的 A 1 和 A 3 A_1和A_3 A1和A3不可再分,直接与前一部分相乘即可,相乘次数为左右部分的计算次数加上两部分相乘的次数,如果哪个部分只有一个矩阵的,说明该部分为0,然后我们再从 A 1 ∗ A 2 和 A 3 A_1*A_2和A_3 A1∗A2和A3, A 1 和 A 2 ∗ A 3 A_1和A_2*A_3 A1和A2∗A3之中取一个最小的作为 A 1 ∗ A 2 ∗ A 3 A_1*A_2*A_3 A1∗A2∗A3的值即可,后续类似
有四个数的,求出 A 1 ∗ A 2 ∗ A 3 ∗ A 4 A_1*A_2*A_3*A_4 A1∗A2∗A3∗A4,这部分怎么求呢?按照上面的惯例,分为两部分,有以下分法: A 1 和 A 2 ∗ A 3 ∗ A 4 A_1和A_2*A_3*A_4 A1和A2∗A3∗A4, A 1 ∗ A 2 和 A 3 ∗ A 4 A_1*A_2和A_3*A_4 A1∗A2和A3∗A4, A 1 ∗ A 2 ∗ A 3 和 A 4 A_1*A_2*A_3和A_4 A1∗A2∗A3和A4,显然可以从上面直接获取了,相乘次数为左右部分的计算次数加上两部分相乘的次数,如果哪个部分只有一个矩阵的,说明该部分为0,我们需要做的只是,在三个分法的结果中,取一个最小的值作为 A 1 ∗ A 2 ∗ A 3 ∗ A 4 A_1*A_2*A_3*A_4 A1∗A2∗A3∗A4的结果就好,这样就求出了结果
可以发现,实际上,自从2、中求出了结果,后续就可以不断叠加,基本就不需要做什么复杂运算了,这个就是动态规划的好处。由于上面每一层求的数在减一,所以比较适合二维如下的表格去描述(0是因为相乘是二元运算符):
1 | 2 | 3 | 4 | |
---|---|---|---|---|
1 | 0 | A 1 ∗ A 2 A_1*A_2 A1∗A2 | A 1 ∗ A 2 ∗ A 3 A_1*A_2*A_3 A1∗A2∗A3 | A 1 ∗ A 2 ∗ A 3 ∗ A 4 A_1*A_2*A_3*A_4 A1∗A2∗A3∗A4 |
2 | 0 | A 2 ∗ A 3 A_2*A_3 A2∗A3 | A 2 ∗ A 3 ∗ A 4 A_2*A_3*A_4 A2∗A3∗A4 | |
3 | 0 | A 3 ∗ A 4 A_3*A_4 A3∗A4 | ||
4 | 0 |
显然我们已经可以找出规律了,假设i是行坐标,j是列坐标,k是分为两部分的时候的
A
K
A_K
AK,在(1,2)中就是K=1,在(1,3)中就是K=1,2,得出书上的公式如下:
m
[
i
,
j
]
=
{
0
,
i
=
j
min
i
≤
k
<
j
{
m
[
i
]
[
k
]
+
m
[
k
+
1
]
[
j
]
+
P
i
−
1
P
k
P
j
}
,
i
<
j
m[i,j]= \begin{cases} 0, & i=j \\ \min\limits_{i\leq k <j}\{m[i][k]+m[k+1][j]+P_{i-1}P_kP_j\}, & i<j \end{cases}
m[i,j]=⎩
⎨
⎧0,i≤k<jmin{m[i][k]+m[k+1][j]+Pi−1PkPj},i=ji<j
3. 定义求解每一个格子的值的方法
斜着遍历这些格子就好。
4. 从最小的子问题开始求解
从(1,1),(2,2)等开始求解
5. 获取最终的最优解
最少的乘法次数根据
m
[
i
]
[
j
]
m[i][j]
m[i][j]求得。
最优解的乘法顺序可以根据
s
[
i
]
[
j
]
s[i][j]
s[i][j]求得,一步一步划分下去即可
上代码:
/**
* @description 输入矩阵,得到最小的乘数的计算次序
* @param p 矩阵的维度,如果p:1,2,3,那么矩阵为1*2和2*3,这个在用来做矩阵相乘的时候用到
* @param m 记录子问题的解,比如矩阵为1*2和2*3的计算乘法次数
* @param s 记录得到最优解的时候的分割点矩阵,比如说矩阵为1*2和2*3相乘的分割点矩阵是1*2
*/
public static void MatrixChain(int []p, int [][]m, int [][]s) {
// 此处就是前面步骤中的4),从初始化最小的子问题
// 以及关于这里的n,这里没有给出定义,只能通过联系上下文知道,n表示矩阵个数
// n = p.length - 1;
for (int i = 1;i <= n;i++)
m[i][i] = 0;
// 在讲代码之前,我们需要先理解一下,我们是通过斜着遍历一个斜行的
// 所以我们需要知道如何遍历这么一个斜行,否则看下面的代码就会很懵了
// 关于行i与列j之间的关系,我们可以先举例看看它们之间的关系
// 除了0斜行外的几个斜行坐标
// 第一个斜行:(1,2)(2,3)(3,4),容易得出j = i + 1
// 第二个斜行:(1,3)(2,4),容易得出j = i + 2
// 第三个斜行:(1,4),容易得出j = i + 3
// 我们假设一个r,可以得到j = i + r,r的取值为1,2,3,1 <= r <= n - 1
// 当然我们也还需要i的约束,这样就可以得到j的约束
// 第一个斜行是1,2,3,第二个斜行是1,2,第三个斜行是1
// 这三个斜行对应的r的取值为1,2,3
// 找规律就可以得出,1 <= i <= n - r(根据上面的例子,此处n是4)
// 下面我们开始看代码
// 还记得我们的r的取值范围是1 <= r <= n - 1,这里变成了2 <= r <= n
// 左右范围都增大了一,说明r的范围比我们想要的大了一
// 这种情况下,我们推测一下i的约束该怎么变化?
// 原来的1 <= i <= n - r,发现现在r变大了,多减了一个1,为了保持不变,需要多加一个1
// 变成1 <= i <= n - r + 1
// 那么j的取值公式呢?
// 原先是j = i + r,现在r变大了,j的值比之前多加了一个1,所以需要减去1,j = i + r - 1
// 此时总结以上所有约束
// r:2 <= r <= n
// i:1 <= i <= n - r + 1
// j:j = i + r - 1
// 这里再提一个点,有三个约束,如何确定循环外层的是哪个约束呢?
// 显然由于i与j的约束中都有r,所以必须把r放在最外层
// 由于j有需要i,所以在求j之前,必须把i放置在外层
// 到这里,下面部分代码的循环部分已经讲完了,我们继续看
for (int r = 2;r <= n;r++) {
// i是行数
// 根据上面的约束定义就好
for (int i = 1;i <= n - r + 1;i++) {
// j是列数
// 根据上面的约束定义就好
int j = i + r - 1;
// m[i][j]的公式上面也有给出
// 口语化里面就是,在k位置进行分割
// m[i][j] = 左部分的乘法次数 + 右部分的乘法次数 + 合并时的乘法次数
// 为什么这里合并矩阵的时候是p[i - 1]p[i]p[j]而不是p[i]p[i + 1]p[j]?
// 这是因为我们计算i矩阵的在p数组中对应的矩阵的行列的值为(i-1,i)
// 为什么也不是p[i - 1]p[k]p[j]
// 显然是因为此时k = i啦,也就是说分割点是第一个矩阵后方
// 这个是所有斜行都有的情况,所以就放在了这里
m[i][j] = m[i][i] + m[i + 1][j] + p[i - 1]*p[i]*p[j];
// s记录的是分割点前面的一个矩阵,此处自然就是i,实际上后续就是k
s[i][j] = i;
// 由于k = i已经有了,就在上面,所以这里应该从k = i + 1开始
// k的取值范围,显然要小于j,因为如果等于j,右部分就没有矩阵了
// 通过k++,获取到所有分割点
for (int k = i + 1;k < j;k++) {
// t是某个分割点求得的最终乘法次数
// 毕竟上面的例子我们也能看到m[i][j]因为分割点而有多个可能的值
// 求出所有,然后再选一个最小的作为m[i][j]
int t = m[i][k] + m[k + 1][j] + p[i - 1]*p[k]*p[j];
// 如果小于此时的m[i][j],说明找到了更好的解,自然需要去替换
if (t < m[i][j]) {
// 替换
m[i][j] = t;
// 替换
s[i][j] = k;
}
}
}
}
}
2、电路布线
问题描述:
在一块电路板的上、下端分别有n个接线柱,根据电路设计,要求用导线
(
i
,
π
(
i
)
)
(i,\pi(i))
(i,π(i))将上端接线柱与下端接线柱相连,导线
(
i
,
π
(
i
)
)
(i,\pi(i))
(i,π(i))称为该电路板上的第
i
i
i条连线,其中
π
(
i
)
\pi(i)
π(i)是
{
1
,
2
,
⋯
,
n
}
\{1,2,\cdots,n\}
{1,2,⋯,n}的一个排列,举例如图
有:
i
=
{
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
,
10
}
i = \{1,2,3,4,5,6,7,8,9,10\}
i={1,2,3,4,5,6,7,8,9,10}
π
(
i
)
=
{
8
,
7
,
4
,
2
,
5
,
1
,
9
,
3
,
10
,
6
}
\pi(i) = \{8,7,4,2,5,1,9,3,10,6\}
π(i)={8,7,4,2,5,1,9,3,10,6}
要求:求出最大不相交子集。(对于任何的
1
≤
i
<
j
≤
n
1\leq i < j\leq n
1≤i<j≤n不相交的充分必要条件是
π
(
i
)
<
π
(
j
)
\pi(i)<\pi(j)
π(i)<π(j))
分析:
1.是否具有最优子结构?
我们划分一下子问题,有上接线柱和下接线柱,很容易就可以划分出一个
s
i
z
e
(
i
,
j
)
size(i,j)
size(i,j)的子问题,
i
i
i是上接线柱的个数,
j
j
j是下接线柱的个数。
由于题目是求最大的不相交子集,那么 s i z e ( i , j ) size(i,j) size(i,j)的含义可以尝试定义为在只有 i i i个上接线柱和 j j j个下接线柱的情况下的最大不相交子集,这样,我们就把一个大问题分为了数个小问题,但是我们还需要证明这些大问题是否可以由小问题求解出来,需要找到大问题与小问题之间的关系式。在找关系式之前,我们可以先求一遍 s i z e ( i , j ) size(i,j) size(i,j),对于具体情况的分析,有利于我们找出关系式。
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
由于1上接线柱连接的是8下接线柱,所以在(1,1)到(1,7)都是0,从(1,8)开始就是1,这种情况,我们可以概括为以下公式
m
[
1
,
j
]
=
{
0
,
j
<
π
(
1
)
1
,
j
≥
π
(
1
)
m[1,j]= \begin{cases} 0, & j < \pi(1) \\ 1, & j \geq \pi(1) \end{cases}
m[1,j]={0,1,j<π(1)j≥π(1)
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|
2 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |
由于2上接线柱连接的是7下接线柱,所以(2,7)是1,而(2,8)之后,因为(1,8)与(2,7)相交了,所以只有1,我们如何使用公式概括这一行的情况?
显然我们需要利用上一行的子问题的解来处理这一行的问题。
我们可以分为三种情况,(2,6)是一种情况,没有可以接线的情况,(2,7)是一种情况,有可以接线但是又没有相交的情况,(2,8)之后就是第三种情况,有接线相交了
第一种情况
(
2
,
j
)
(2, j)
(2,j)与
(
1
,
j
)
(1, j)
(1,j)这一行子问题的关系,当没有到达可以添加(2, 7)这条线的情况之前,显然结果就与
(
1
,
j
)
(1,j)
(1,j)是类似的,我们可以得到
s
i
z
e
[
2
,
j
]
=
s
i
z
e
[
1
,
j
]
,
j
<
π
(
2
)
size[2,j] = size[1,j] , j < \pi(2)
size[2,j]=size[1,j],j<π(2)
第二种情况与子问题的联系,现在可以考虑(2, 7)这条线了,能考虑的是什么?当然是加不加这条线啦!怎么判断加不加这条线好呢?当然是看加入这条线的值大还是不加入这条线的值大。不加入这条线的情况该怎么由子问题表达?不就是第一种情况吗?显然可以得出了。加入这条线的情况又怎么由子问题表达?看一下我们对于
s
i
z
e
(
i
,
j
)
size(i,j)
size(i,j)的定义:基于i个上接线柱与j个下接线柱的最大不相交子集,那样的话,
s
i
z
e
(
1
,
π
(
2
)
−
1
)
size(1,\pi(2) - 1)
size(1,π(2)−1)(也就是(1,6))就可以说明什么?
s
i
z
e
(
1
,
π
(
2
)
−
1
)
size(1,\pi(2) - 1)
size(1,π(2)−1)一定不会与我们新加入的这条导线相交,因为它连接线柱都没有,怎么和(2, 7)这条线相交,另外
s
i
z
e
(
1
,
π
(
2
)
−
1
)
size(1,\pi(2) - 1)
size(1,π(2)−1)也是说明已经是该情况下的最大值了,如果非要加入(2, 7)这条导线,那么这种情况下的最大值自然就是
s
i
z
e
(
1
,
π
(
2
)
−
1
)
+
1
size(1,\pi(2) - 1)+1
size(1,π(2)−1)+1,所以我们得出这种情况的以下公式:
s
i
z
e
[
2
,
j
]
=
m
a
x
(
s
i
z
e
[
1
,
j
]
,
s
i
z
e
[
1
,
π
(
2
)
−
1
]
+
1
)
,
j
=
π
(
2
)
size[2,j] = max(size[1,j], size[1, \pi(2)-1]+1) , j = \pi(2)
size[2,j]=max(size[1,j],size[1,π(2)−1]+1),j=π(2)
第三种情况,有冲突了,还是考虑到底要不要加(2, 7)这条导线呢?可以发现,考虑方式其实与第二种情况是类似的,所以我们可以直接得出
s
i
z
e
[
2
,
j
]
=
m
a
x
(
s
i
z
e
[
1
,
j
]
,
s
i
z
e
[
1
,
π
(
2
)
−
1
]
+
1
)
,
j
>
π
(
2
)
size[2,j] = max(size[1,j], size[1, \pi(2)-1]+1) , j > \pi(2)
size[2,j]=max(size[1,j],size[1,π(2)−1]+1),j>π(2)
总结以上,可得
s
i
z
e
[
i
,
j
]
=
{
s
i
z
e
[
i
−
1
,
j
]
,
j
<
π
(
i
)
i
≥
2
m
a
x
(
s
i
z
e
[
i
−
1
,
j
]
,
s
i
z
e
[
i
−
1
,
π
(
i
)
−
1
]
+
1
)
,
j
≥
π
(
i
)
i
≥
2
size[i,j]= \begin{cases} size[i-1,j] , & j < \pi(i) & i \geq 2\\ max(size[i-1,j], size[i-1, \pi(i)-1]+1) , & j \geq \pi(i) & i \geq 2 \end{cases}
size[i,j]={size[i−1,j],max(size[i−1,j],size[i−1,π(i)−1]+1),j<π(i)j≥π(i)i≥2i≥2
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|
3 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
可以发现,我们分析的内容还是都是类似于上面(2, j)的,所以我们可以得出适用于所有子问题的关系式:
m
[
1
,
j
]
=
{
0
,
j
<
π
(
1
)
1
,
j
≥
π
(
1
)
m[1,j]= \begin{cases} 0, & j < \pi(1) \\ 1, & j \geq \pi(1) \end{cases}
m[1,j]={0,1,j<π(1)j≥π(1)
s
i
z
e
[
i
,
j
]
=
{
s
i
z
e
[
i
−
1
,
j
]
,
j
<
π
(
i
)
i
≥
2
m
a
x
(
s
i
z
e
[
i
−
1
,
j
]
,
s
i
z
e
[
i
−
1
,
π
(
i
)
−
1
]
+
1
)
,
j
≥
π
(
i
)
i
≥
2
size[i,j]= \begin{cases} size[i-1,j] , & j < \pi(i) & i \geq 2\\ max(size[i-1,j], size[i-1, \pi(i)-1]+1) , & j \geq \pi(i) & i \geq 2 \end{cases}
size[i,j]={size[i−1,j],max(size[i−1,j],size[i−1,π(i)−1]+1),j<π(i)j≥π(i)i≥2i≥2
2.刻画结构特征
显然上一点已经给出了
3. 定义递归方法
遍历每一行即可
4. 从最小的子问题开始求解
就是只有一根上接线柱的情况
5. 获取最终的最优解
因为题目要求的是最大不相交子集,而不是最大不相交子集的接线数。那么我们如何通过
s
i
z
e
[
i
,
j
]
size[i,j]
size[i,j]获取到最大不相交子集?(建议看着课本的图3.39理解,
P
59
P_{59}
P59中)
由于我们已经求出
s
i
z
e
(
i
,
j
)
size(i,j)
size(i,j),所以我们可以从
s
i
z
e
(
i
最大
,
j
最大
)
size(i_{最大}, j_{最大})
size(i最大,j最大)开始回溯找起。
因为除了第一行以外,其余格子的取值符合以下公式:
s
i
z
e
[
i
,
j
]
=
{
s
i
z
e
[
i
−
1
,
j
]
,
j
<
π
(
i
)
i
≥
2
m
a
x
(
s
i
z
e
[
i
−
1
,
j
]
,
s
i
z
e
[
i
−
1
,
π
(
i
)
−
1
]
+
1
)
,
j
≥
π
(
i
)
i
≥
2
size[i,j]= \begin{cases} size[i-1,j] , & j < \pi(i) & i \geq 2\\ max(size[i-1,j], size[i-1, \pi(i)-1]+1) , & j \geq \pi(i) & i \geq 2 \end{cases}
size[i,j]={size[i−1,j],max(size[i−1,j],size[i−1,π(i)−1]+1),j<π(i)j≥π(i)i≥2i≥2
最大解一定是在
j
≥
π
(
i
)
j\geq\pi(i)
j≥π(i)取得,为什么呢?
因为
j
≥
π
(
i
)
j\geq\pi(i)
j≥π(i)时,已经是考虑了
i
i
i这条线的最大解,而
j
<
π
(
i
)
j<\pi(i)
j<π(i)是还没有考虑的,显然前者包含了后者。所以我们只需要考虑此时得到的最优解来自于
s
i
z
e
[
i
−
1
,
j
]
size[i-1,j]
size[i−1,j]还是
s
i
z
e
[
i
−
1
,
π
(
i
)
−
1
]
+
1
size[i-1,\pi(i)-1]+1
size[i−1,π(i)−1]+1。
当
s
i
z
e
(
i
,
j
)
>
s
i
z
e
(
i
−
1
,
j
)
size(i,j) > size(i-1,j)
size(i,j)>size(i−1,j),说明最优解来自于
s
i
z
e
[
i
−
1
,
π
(
i
)
−
1
]
+
1
size[i-1,\pi(i)-1]+1
size[i−1,π(i)−1]+1。
当
s
i
z
e
(
i
,
j
)
=
s
i
z
e
(
i
−
1
,
j
)
size(i,j) = size(i-1,j)
size(i,j)=size(i−1,j),说明最优解可能是来自于
s
i
z
e
[
i
−
1
,
j
]
size[i-1,j]
size[i−1,j]或者
s
i
z
e
[
i
−
1
,
π
(
i
)
−
1
]
+
1
size[i-1,\pi(i)-1]+1
size[i−1,π(i)−1]+1。
但是我们需要管 s i z e ( i , j ) = s i z e ( i − 1 , j ) size(i,j) = size(i-1,j) size(i,j)=size(i−1,j)来自哪种可能吗?不需要啊,我们只需要知道,互不相交就行,或许最优解有多种可能,但是我们并不在乎,我们只在乎找到一个解就行,遇到 s i z e ( i , j ) = s i z e ( i − 1 , j ) size(i,j) = size(i-1,j) size(i,j)=size(i−1,j),我们不管,继续往别的线找,直到找到 s i z e ( i , j ) > s i z e ( i − 1 , j ) size(i,j) > size(i-1,j) size(i,j)>size(i−1,j)就好,这个肯定是可以找到的,因为肯定有递增的过程,如果找到最后, s i z e ( 1 , j ) size(1,j) size(1,j)还是1,显然1接线柱对应的连线也在里面,但是我们还需要解决一个问题:怎么证明互不相交?
我们找到了一个 s i z e ( i , j ) > s i z e ( i − 1 , j ) size(i,j) > size(i-1,j) size(i,j)>size(i−1,j)的连线,那么接下来只需要在 s i z e ( i − 1 , π ( i ) − 1 ) size(i-1,\pi(i)-1) size(i−1,π(i)−1)这部分继续找不就行了?这样肯定就没有相交了,在剩下部分找,也不会影响我们找 s i z e ( i , j ) > s i z e ( i − 1 , j ) size(i,j) > size(i-1,j) size(i,j)>size(i−1,j)因为根据最优解中找到 s i z e ( i , j ) > s i z e ( i − 1 , j ) size(i,j) > size(i-1,j) size(i,j)>size(i−1,j)的位置肯定不会与现在找到的这个连线相交。
代码:
构造size数组:
/**
* 按照关系式写就好,没有多少需要讲解的地方
* @description 求解子问题集合
* @param size 动态规划求解的子问题集合
*/
public static void mnset(int[] c, int[][] size) {
// 求解的是第一行的前面部分
// 0号列只是为了使得1号列可以直接通过关系式求出
for (int j = 0;j < c[1];j++) {
size[1][j] = 0;;
}
// 求解的是第一行后面部分
// n是接线柱个数,上接线柱10,下接线柱10,n = 10
for (int j = c[1];j <= n;j++) {
size[1][j] = 1;
}
// 两条线以及以上的情况
for (int i = 2;i < n;i++) {
// 第一部分
for (int j = 0;j < c[i];j++) {
size[i][j] = size[i - 1][j];
}
// 第二部分
for (int j = c[i];j <= n;j++) {
size[i][j] = Math.max(size[i - 1][j], size[i - 1][c[i] - 1] + 1);
}
}
// 由于上面的i < n,所以这里还需要再求一下size[n][n]
// 也是因为关系式中,求n行的数据只需要n-1行的数据求出来即可
// 所以就不用再求一遍完整的n行数据
size[n][n] = Math.max(size[n - 1][n], size[n - 1][c[n] - 1] + 1);
}
最大不相交子集:
/**
* @description 找出最优解对应的上接线柱
* @param c 上下接线柱连接情况,比如说c[1] = 8,意思是1号上接线柱对于8号下接线柱
* @param size 上面求出的子问题的解
* @param net 最优解对应的上接线柱
* @return 最优解的连线数
*/
public static int traceback(int[] c, int[][] size,int[] net) {
// n是接线柱个数,比如说上下接线柱各是10个,n = 10
// 这里j其实是用于指下接线柱,size数组里面的列坐标
int j = n;
// m是布线条数
int m = 0;
// i = n,就如我们上面说的,从size[n][n]开始回溯
// 大于1是因为第一行没有上一行进行对比了,所以不能用循环里面的代码进行判断
// 假设i = 0,就会出现size[0][j],但是我们size数组中并没有0行存在
for (int i = n;i > 1;i--) {
// 如果不相等,说明找到了最优解的一条布线
if(size[i][j] != size[i - 1][j]) {
// 将布线的上接线柱放入net数组
net[m++] = i;
// 正如我们上面讨论的,这是为了避免有相交的情况
// 这样下一次循环只会在size(i - 1,c[i] - 1)找,这样就不好有相交
j = c[i] - 1;
}
}
// 第一行的判断方式
// 下面代码等同于
// if(size[j] == 1) {
// net[m++] = 1;
// }
// 其实就是判断在不相交的前提下,是否能加入第一行的接线柱
// j是无相交情况下的最大下接线柱
if(j >= c[1]) {
// 放入net数组中
// 由于后续是直接返回了m,而m初始值是0,所以++还是有必要的,m就是当前数组的长度
net[m++] = 1;
}
// 返回的就是最优布线条数
return m;
}