算法学习记录

一、递归与分治法


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 Pi1Pi A i 与 A i + 1 A_i与A_i+1 AiAi+1是可乘的, i = 1 , 2 ⋯   , n − 1 i=1,2\cdots,n-1 i=1,2,n1。如何确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵的连乘积所需的数乘次数最少?

问题快速理解:

现在有三个矩阵, 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=23,A2=34,A3=42,乘数的第一位是行,第二位是列。
第一个问题:为什么说可乘?
线性代数知识。简单来说,我们这里只需要注意, 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 A1A2A3是先 A 1 ∗ A 2 A_1*A_2 A1A2,再将结果乘以 A 3 A_3 A3,而 A 1 ∗ ( A 2 ∗ A 3 ) A_1*(A_2*A_3) A1(A2A3)则是先 A 2 ∗ A 3 A_2*A_3 A2A3,再将结果乘以 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(23)A2(34)得到 A 12 = 2 ∗ 4 : A_{12}=2*4: A12=24:
{ 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\} {12+23+3522+33+1513+24+3423+34+1414+25+3324+35+1315+22+3225+32+12}
2行4列,每一个位置都有3个乘法,使用到了乘法的次数: 2 ∗ 3 ∗ 4 2*3*4 234(就算不会矩阵等定义,也可以开始找规律了),显然 A 12 ∗ A 3 A_{12}*A_3 A12A3的乘法次数也很明显: 2 ∗ 4 ∗ 2 2*4*2 242,所以 A 1 ∗ A 2 ∗ A 3 A_1*A_2*A_3 A1A2A3的总乘法次数: 2 ∗ 3 ∗ 4 + 0 + 2 ∗ 4 ∗ 2 = 40 2*3*4+0+2*4*2=40 234+0+242=40(这里解释一下为什么要加0,因为左部分的乘法次数 + 右部分的乘法次数 + 合并时的乘法次数才是最终乘数,结合下面这条可以更好理解)。所以 A 1 ∗ ( A 2 ∗ A 3 ) A_1*(A_2*A_3) A1(A2A3)的总乘法次数: 0 + 3 ∗ 4 ∗ 2 + 2 ∗ 3 ∗ 2 = 36 0+3*4*2+2*3*2=36 0+342+232=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(A2A3),分割点的左矩阵就是 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 A1A2A3A4的最小数乘次数。

只有一个数的,分别求出 A 1 , A 2 , A 3 , A 4 A_1,A_2,A_3,A_4 A1A2A3A4,由于相乘是二元运算符,所以为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 A1A2A2A3A3A4,这部分怎么求呢?比如说 A 1 ∗ A 2 A_1*A_2 A1A2,先将其分为两部分,把问题变为求出 A 1 和 A 2 A_1和A_2 A1A2两部分的分别最优,由于每一部分只有一个,每一部分的乘数都是0,最终乘数就是 0 + 0 + A 1 ∗ A 2 0+0+A_1*A_2 0+0+A1A2

有三个数的,分别求出 A 1 ∗ A 2 ∗ A 3 , A 2 ∗ A 3 ∗ A 4 A_1*A_2*A_3,A_2*A_3*A_4 A1A2A3A2A3A4,这部分怎么求呢?比如说 A 1 ∗ A 2 ∗ A 3 A_1*A_2*A_3 A1A2A3,可以先分为两部分,也两种分法, A 1 ∗ A 2 和 A 3 A_1*A_2和A_3 A1A2A3 A 1 和 A 2 ∗ A 3 A_1和A_2*A_3 A1A2A3,显然 A 1 ∗ A 2 A_1*A_2 A1A2 A 2 ∗ A 3 A_2*A_3 A2A3都可以在2、中获取,剩下的 A 1 和 A 3 A_1和A_3 A1A3不可再分,直接与前一部分相乘即可,相乘次数为左右部分的计算次数加上两部分相乘的次数,如果哪个部分只有一个矩阵的,说明该部分为0,然后我们再从 A 1 ∗ A 2 和 A 3 A_1*A_2和A_3 A1A2A3 A 1 和 A 2 ∗ A 3 A_1和A_2*A_3 A1A2A3之中取一个最小的作为 A 1 ∗ A 2 ∗ A 3 A_1*A_2*A_3 A1A2A3的值即可,后续类似

有四个数的,求出 A 1 ∗ A 2 ∗ A 3 ∗ A 4 A_1*A_2*A_3*A_4 A1A2A3A4,这部分怎么求呢?按照上面的惯例,分为两部分,有以下分法: A 1 和 A 2 ∗ A 3 ∗ A 4 A_1和A_2*A_3*A_4 A1A2A3A4 A 1 ∗ A 2 和 A 3 ∗ A 4 A_1*A_2和A_3*A_4 A1A2A3A4 A 1 ∗ A 2 ∗ A 3 和 A 4 A_1*A_2*A_3和A_4 A1A2A3A4,显然可以从上面直接获取了,相乘次数为左右部分的计算次数加上两部分相乘的次数,如果哪个部分只有一个矩阵的,说明该部分为0,我们需要做的只是,在三个分法的结果中,取一个最小的值作为 A 1 ∗ A 2 ∗ A 3 ∗ A 4 A_1*A_2*A_3*A_4 A1A2A3A4的结果就好,这样就求出了结果

可以发现,实际上,自从2、中求出了结果,后续就可以不断叠加,基本就不需要做什么复杂运算了,这个就是动态规划的好处。由于上面每一层求的数在减一,所以比较适合二维如下的表格去描述(0是因为相乘是二元运算符):

1234
10 A 1 ∗ A 2 A_1*A_2 A1A2 A 1 ∗ A 2 ∗ A 3 A_1*A_2*A_3 A1A2A3 A 1 ∗ A 2 ∗ A 3 ∗ A 4 A_1*A_2*A_3*A_4 A1A2A3A4
20 A 2 ∗ A 3 A_2*A_3 A2A3 A 2 ∗ A 3 ∗ A 4 A_2*A_3*A_4 A2A3A4
30 A 3 ∗ A 4 A_3*A_4 A3A4
40

显然我们已经可以找出规律了,假设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,ik<jmin{m[i][k]+m[k+1][j]+Pi1PkPj},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 1i<jn不相交的充分必要条件是 π ( 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),对于具体情况的分析,有利于我们找出关系式。

12345678910
10000000111

由于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)

12345678910
20000001111

由于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[i1,j],max(size[i1,j],size[i1,π(i)1]+1),j<π(i)jπ(i)i2i2

12345678910
30000111111

可以发现,我们分析的内容还是都是类似于上面(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[i1,j],max(size[i1,j],size[i1,π(i)1]+1),j<π(i)jπ(i)i2i2

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[i1,j],max(size[i1,j],size[i1,π(i)1]+1),j<π(i)jπ(i)i2i2
最大解一定是在 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[i1,j]还是 s i z e [ i − 1 , π ( i ) − 1 ] + 1 size[i-1,\pi(i)-1]+1 size[i1,π(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(i1,j),说明最优解来自于 s i z e [ i − 1 , π ( i ) − 1 ] + 1 size[i-1,\pi(i)-1]+1 size[i1,π(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(i1,j),说明最优解可能是来自于 s i z e [ i − 1 , j ] size[i-1,j] size[i1,j]或者 s i z e [ i − 1 , π ( i ) − 1 ] + 1 size[i-1,\pi(i)-1]+1 size[i1,π(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(i1,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(i1,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(i1,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(i1,j)的连线,那么接下来只需要在 s i z e ( i − 1 , π ( i ) − 1 ) size(i-1,\pi(i)-1) size(i1,π(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(i1,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(i1,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;
}
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值