Loading

算法导论笔记

  • CUMT算法导论课程

  • 课本:计算机算法设计与分析(王晓东)

  • 此blog代码均为Java实现

第二章 分治与递归

递归

直接或间接调用算法自身。

全排列问题

求出n个元素的全排列,可以使用递归。即若要求出\(Perm(X_i)\),可以先求出\(Perm(X_{i-1})\),然后再依次加上前缀。

\(R=\){\(r_1,r_2,r_3,···,r_n\)}是要进行排列的n个元素,\(R_i=R-\){\(r_i\)}。\((r_i)\)\(Perm(X)\)代表X的全排列加上前缀ri

R的全排列定义为:

当n = 1时,Perm(R) = r。

当n>1时,Perm(R)为 \((r_1)Perm(R_1),(r_2)Perm(R_2),···(r_n)Perm(R_n)\)

代码实现:

/**
 * @author CrisKey
 * @date 2022/11/17 18:38
 * 全排列问题
 */
public class Arrangement {
    public int fullPermutation(List<String> list,int k,int m,int count){ //打印list[k:m]
        //递归到递归基
        if (k==m){
            System.out.println(list);
            count++;
        }
        for (int i=k;i<m;i++){
            Collections.swap(list,i,k);
          count =   fullPermutation(list,k+1,m,count);
            Collections.swap(list,i,k);
        }
        return count;
    }

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list = Arrays.asList("1","3","5","7","9");
        Arrangement arrangement = new Arrangement();
        int count = 0;
        count = arrangement.fullPermutation(list,0,list.size(),count);
        System.out.println("总共有"+count+"种排列方式");
    }
}

整数划分问题

将正整数n表示为一系列正整数之和,

\(n=n_1+n_2+···+n_k\) (其中,\(n_1\ge n_2\ge n3 \ge ···\ge n_k \ge 1,k\ge 1\))

同样,本题可以用递归求解。

分析后可知,本题分为四种情况。

/**
 * @author CrisKey
 * @date 2022/11/17 19:39
 * 整数划分问题
 */
public class Divide {

    public static int  partition(int n,int m){

        if (n==1||m==1){
           return 1;
        }else if (m==n){
                //整形本身
          return   partition(n,m-1)+1;
        }else if (m>n){
         return    partition(n,n);
        }else if (n<1||m<1) {
            return 0;
        }else {
            return partition(n,m-1)+partition(n-m,m);
        }

    }
    public static void main(String[] args) {
        int x = 6;

            int count = partition(x,x);

        System.out.println("正整数x共有:"+String.valueOf(count)+"种划分");
    }
}

Hanoi塔问题

汉诺塔是典型的递归问题。

/**
 * @author CrisKey
 * @date 2022/11/17 20:48
 * 汉诺塔问题
 */
public class HanoiProblem {
    public static void hanoi(int n,String A,String B,String C){
        if (n>0){
            hanoi(n-1,A,C,B);
            System.out.println("将"+String.valueOf(n)+"从"+A+"移到"+C+"借助"+B);
            hanoi(n-1,C,B,A);
        }
    }

    public static void main(String[] args) {
        hanoi(3,"A","B","C");
    }
}

分治与递归

二分搜索

二分搜索给定已排序好的n个元素,在这n个元素中找到特定元素x。

可以利用分治与递归,将问题划分为在数组的左边查找或者在数组的右边查找,可以降低时间复杂度。

使用二分搜索最坏情况下复杂度为\(O(logn)\)

/**
 * @author CrisKey
 * @date 2022/11/17 21:15
 */
public class BinarySearchProblem {
    public static void BinarySearch(int[] arr,int left,int right,int flag){
        if (right<left){
            System.out.println("错误");
        }else if (right==left){
            if (flag==arr[right]){
                System.out.println(right);
            }
        }
        else{
            int mid = (right-left)/2;
            if (arr[mid]==flag){
                System.out.println(mid);
            }else if (arr[mid]<flag){
               BinarySearch(arr,mid+1,right,flag);
            }else {
                BinarySearch(arr,left,mid-1,flag);
            }
        }
    }

    public static void main(String[] args) {
        int []arr = new int[]{0,1,2,3,4,5,6,7,8,9,10};
        BinarySearch(arr,0,10,5);
    }
}

大整数乘法

\(设X与Y都是n位二进制数,现在要计算他们的乘积XY\)

可以将\(X Y\)分段,将其分别分为两段,每段长\(n/2\)位。

棋盘覆盖

利用分治思想,将一个大棋盘分为四个小棋盘,其中没有残缺格的三个子棋盘可以用一个L型骨牌连接。

合并排序

合并排序可以使用分治递归求解。合并排序时间复杂度\(O(nlog n)\)

package second;

import java.util.Arrays;
import java.util.List;

/**
 * @author CrisKey
 * @date 2022/11/18 09:01
 * 各种排序问题
 */
public class SortProblem {

    public static void mergeSort(int[]arr,int left,int right){
        if (right<=left) {
            return;
        }

        int mid = (right+left)/2;
      mergeSort(arr,left,mid);
       mergeSort(arr,mid+1,right);
       merge(arr,left,mid,right);
}
public static void merge(int[]arr,int left,int mid,int right){
        //创建一个临时数组存放合并数据
        int[] temp = new int[right-left+1];
        int i = left;
        int j = mid+1;
        int k = 0;
        while(i<=mid&&j<=right){
            if (arr[i]<=arr[j]){
                temp[k++] = arr[i++];
            }else {
                temp[k++] = arr[j++];
            }
        }
        if (i<=mid){
            while (i<=mid){
                temp[k++] = arr[i++];
            }
        }
        if (j<=right){
            while(j<=right) {
                temp[k++] = arr[j++];
            }

        }
        k=0;
        for (int index =left;index<=right;index++){
            arr[index] = temp[k++];

        }

    }



}

也可以使用非递归实现。

  /**
     *  非递归实现
     */
    public static void sort(int[] arr){
        int len = 1;
        int size = arr.length;
        while(len<size){
            for (int i = 0 ;i+len<size;i+=len*2){
                int left = i;
                int mid =i+len-1;
                int right = i+len*2-1;
                if (right>size-1) right = size-1;
                merge(arr,left,mid,right);
            }
            len=len*2;
        }

    }

快速排序

快速排序使用分治递归求解,求解步骤分为三步:

  1. 将数组以基准元素分为左右两边
  2. 对左边递归
  3. 对右边递归
 public static void quickSort(int[]arr,int left,int right){
        if (left<right){
            int p = partition(arr,left,right);
            quickSort(arr,left,p);
            quickSort(arr,p+1,right);
        }
       
    }
    public static int  partition(int[] arr,int left,int right){
        int index;
        //以最左元素作为基准

        int flag = arr[left];
        int i = left;
        int j = right+1;
       
        while (true){
          //找到左边第一个大于flag的和右边第一个小于flag的,交换。然后依次找第二个第三个····
            while (arr[++i]<flag&&i<right){

            }
            while (arr[--j]>flag){
            }
            if (i>=j) {
                break;
            }
            //将i与j交换
            swap(arr,i,j);
        }
        swap(arr,j,left);
        return j ;
    }

    public static void  swap(int[]arr,int i1,int i2){
        int x = arr[i1];
        arr[i1] = arr[i2];
        arr[i2] = x;
    }

选择基准时,也可以通过随机选择

快速排序平均时间复杂度为\(O(nlog n)\)

线性时间选择

元素选择问题的一般提法是:给定线性集中\(n\)个元素和一个整数\(k\),\(1\le k\le n\),要求找出这n个元素中第k小的元素 。当\(k=1\)时,就是要找最小元素。当\(k=(n+1)/2\)时,称查找中位数。

在最坏情况下可以在\(O(n)\)时间就完成选择任务的算法Select。

在线性时间内找到一个划分基准,使得按这个基准所划分出的两个子数组的长度都至少为原数组长度的\(\varepsilon\)\((0<\varepsilon<1)\),那么在最坏情况下用\(O( n)\)就可以完成选择任务。

找到满足条件的划分基准的步骤:

  1. \(将n个输入元素划分为[n/5]组,使用某种排序算法进行排序,取出每组的中位数共[n/5]个\)
  2. \(递归调用Select找出这[n/5]个元素的中位数。若[n/5]为偶数,则选取中位数中较大的元素作为基准\)

第三章 动态规划

动态规划算法适用于解最优化问题。通常可按照以下4个步骤设计:

  1. 找出最优解性质,并刻画其结构特征。
  2. 递归的定义最优值。
  3. 以自底向上的方式计算出最优值。
  4. 根据计算最优值时得到的信息,构造最优解。

矩阵连乘问题

若要求解出\(A_1A_2···A_n\),则必存在k使得\((A_1A_2···A_k)*(A_{k+1}···A_n)\)为最优。然后分别找出左半部分最优和右半部分最优。

使用一个二维数组来记录矩阵连乘所需次数,一个二维数组记录最佳位置。

/**
 * @author CrisKey
 * @date 2022/11/18 15:56
 * 矩阵连乘问题
 */
public class MatrixMultiply {
    public static void multiply(int[] m){
        int size = m.length;
        //存储矩阵乘后结果
        int[][] result =new int[size][size];
        //存储分割点
        int[][] s = new int[size][size];

        for (int i=1;i<size;i++) {
            result[i][i] = 0;

        }
       for (int r=2;r<size;r++){
           for (int l=r-1;l>=1;l--){
               result[l][r] = result[l][l]+result[l+1][r]+m[l-1]*m[l]*m[r];
               s[l][r] = l;
               for (int k=l+1;k<r;k++){
                   int t = result[l][k]+result[k+1][r]+m[l-1]*m[k]*m[r];
                   if (t<result[l][r]) {
                       s[l][r] = k;
                       result[l][r] = t;
                   }
               }
           }
       }
        System.out.println(result[1][size-1]);


    }


    public static void main(String[] args) {
        int[] m = new int[]{30,35,15,5,10,20,25};
        multiply(m);
    }
}

最长公共子序列

  1. 分析结构
  2. 建立递归关系
  3. 计算最优值

完整代码

package third;

/**
 * @author CrisKey
 * @date 2022/11/19 19:11
 * 最长公共子序列
 */
public class LCSLength {
    public static void lcsLength(String A,String B){
        char[] a = A.toCharArray();
        char[] b = B.toCharArray();

        int[][] m = new int[a.length+1][b.length+1];
        for (int i=1;i<=a.length;i++) {
            m[i][0] = 0;
        }
        for (int j=1;j<=b.length;j++){
                m[0][j] = 0;
            }
        m[0][0]=0;

        for (int i=1;i<=a.length;i++) {
            for (int j=1;j<=b.length;j++){
                if (a[i-1]==b[j-1]){
                    m[i][j] = m[i-1][j-1]+1;
                }
                else if (m[i][j-1]>m[i-1][j]){
                    m[i][j] = m[i][j-1];
                }else {
                    m[i][j] = m[i-1][j];
                }
            }
        }
        System.out.println("最长公共子序列是:");
        for (int i=1;i<=a.length;i++) {
            for (int j=1;j<=b.length;j++){
                if (m[i][j]==m[i-1][j]+1){
                    System.out.print(a[i-1]);
                    break;
                }
            }
        }
        System.out.println();
        System.out.println("==========");

    }



    public static void main(String[] args) {
        String A = "AFVGHBJNKKJL";
        String B = "AVNGJFDSKDBSL";
        lcsLength(A,B);
    }
}

最大字段和

最大字段和有多种方式求解,下面介绍的是动态规划求解。

package third;

import java.util.Arrays;

/**
 * @author CrisKey
 * @date 2022/11/19 20:15
 * 最大字段和
 */
public class MaxSum {
    public static void maxSum(int []a){
        int[] b= new int[a.length];
        b[0] = a[0];
        for (int i=1;i<a.length;i++){
            if (b[i-1]<0) b[i] = a[i];
            else b[i] = b[i-1]+a[i];
        }
        System.out.println("最大字段和为:");
        System.out.println(Arrays.stream(b).max().getAsInt());
    }
    public static void main(String[] args) {
        int[] a = new int[]{-2,11,7,-3,-12,7,-3,7};
        maxSum(a);
    }
}

凸多边形最优三角剖分

类似于矩阵连乘,矩阵连乘积的最优计算次序问题是凸多边形最优三角剖分问题的特殊情形。

图像压缩问题

电路布线问题

package third;

import java.util.ArrayList;
import java.util.List;

/**
 * @author CrisKey
 * @date 2022/11/20 11:49
 * 电路布线问题
 */
public class MCS {
    public static void mcs(int[]C){
        int[][] size = new int[C.length][C.length];
        List<Integer> list = new ArrayList<>();
        /**
         * i=1时,初始化
         */
        for (int i=0;i<C[1];i++) size[1][i] = 0;
        for (int i = C[1];i<C.length;i++) size[1][i] = 1;

        for (int i=2;i<C.length;i++){
            for (int j=1;j<C.length;j++){
                if (j<C[i]) {
                    size[i][j] = size[i-1][j];
                } else {
                    //不要i点
                    if (size[i-1][j]>size[i-1][C[i]-1]+1){
                        size[i][j] = size[i-1][j];
                    }
                    //要i点
                    else {
                        size[i][j] = size[i-1][C[i]-1]+1;
                    }
                }
            }
        }
        int flag = C.length-1;
       int  j=C.length-1;
            for (int i = C.length-1;i>=1;i--){
                    if (size[i][j]!=size[i-1][j]){
                        list.add(i);
                        j=C[i]-1;
                    }

            }



        System.out.println(list);
    }


    public static void main(String[] args) {
        int []C = new int[]{0,8,7,4,2,5,1,9,3,10,6};
        mcs(C);
    }
}

流水线调度

  1. 最优子结构性

  2. 递归计算最优值

0-1背包问题

0-1背包具有最优子结构性质。

\(max\left\{m(i+1,j),m(i+1,j-w_i)+v_i\right\}\)逗号左边表示不选择第i件,右边表示选择i,比较谁最大

j从最小容量往后遍历。

计算为自底向上

import java.util.ArrayList;
import java.util.List;

/**
 * @author CrisKey
 * @date 2022/11/20 21:18
 */
public class ZeronePackage {
    public static void zeronePackage(int[] W,int[] V,int c){
        int[][] g = new int[W.length+1][c+1];
        // 初始化
        for (int i=0;i<=c;i++){
            if (i<W[4]){
                g[5][i] = 0;
            }else {
                g[5][i] = V[4];
            }
        }

        for (int i = 4;i>=1;i--){
            for (int j =0;j<=c;j++)
            if (j<W[i-1]) g[i][j] = g[i+1][j];
            else {
                g[i][j] = g[i+1][j]>g[i+1][j-W[i-1]]+V[i-1]?g[i+1][j]:g[i+1][j-W[i-1]]+V[i-1];
            }
        }
        List<Integer> list = new ArrayList<>();
//        int j = 10;
        int flag = 10;
        int max = 0;
        for(int i =1;i<=4;i++){
    for (int j =flag;j>=0;j--)
                if (g[i][flag]-g[i+1][j]==V[i-1]&&W[i-1]==(flag-j)){
                    list.add(i);
                    max+=V[i-1];
                    flag = j;
                    break;
            }
        }
        if (flag >= W[W.length-1]){
            list.add(W.length-1);
            max+=V[W.length-1];
        }


        System.out.println(list);
        System.out.println(max);
    }
    public static void main(String[] args) {
        //背包容量
        int c = 10;
        int[] W = new int[]{2,2,6,5,4};
        int[] V = new int[]{6,3,5,4,6};
        zeronePackage(W,V,c);
    }
}

回溯法

n皇后问题

package third;

import java.util.Arrays;
import java.util.Collections;

/**
 * @author CrisKey
 * @date 2022/11/22 10:29
 * n皇后问题
 */
public class NQueen {
    public static void nQueen(int n,int[] x,int index){ //index为行号
        //到达叶节点
        if (index==n+1){

                   System.out.println(Arrays.toString(x));
             
          return;
        }
        //i为列号
       for (int i =1;i<=n;i++){
           if (isOk(x,index,i)){
               x[index] = i;
               nQueen(n,x,index+1);
               x[index] = 0;
           }
       }


    }
    /**
     *     判断是否在同一列和同一斜线
     */
    public static boolean isOk(int[] x,int index,int i){
        //j为行号
        for (int j=1;j<index;j++){
            if (x[j]==0)continue;
            if (x[j]==i) return false;
            if (x[j]-j==i-index||x[j]+j==i+index)

                    return false;

        }
        return true;
    }
    public static void main(String[] args) {
      int n = 4;
      int[] x = new int[n+1];
      nQueen(n,x,1);
    }
}

回顾

相邻石子堆合并

C++实现

与矩阵连乘类似

#include<iostream>
using namespace std;
/**
 * @brief
 * 相邻石子堆合并
 * @return int
 */
int b[6][6]; //存放合并后石子堆数量
int c[6][6] ; //存放石子堆合并花费
int main(){
    int a[5] = {1,2,3,4,5};
    for (int i = 1; i <=5; ++i) {
        if (i!=5) c[i][i+1] = a[i-1]+a[i]; //初始化
        b[i][i] = a[i-1];
        }
    for (int i = 1; i <5; i++)
        for (int j = i+1; j <=5 ; j++) {
        b[i][j] = b[i][j-1]+a[j-1];
        }
    for (int i = 3; i <=5 ; ++i) {
        for (int j = i-1; j>=1 ; --j) {
            c[j][i] = c[j][i-1]+b[j][i];
            int flag = c[j][i];
            for (int k = i-1; k >=j ; --k) {
                int sum = c[j][k]+c[k+1][i]+b[j][i];
                if (sum<flag)
                    c[j][i] = sum;
                flag = sum;
            }
        }

}
    cout<<"nice"<<endl;
    cout<<c[1][5]<<endl;
}
posted @ 2022-11-17 21:50  我只有一天的回忆  阅读(73)  评论(0编辑  收藏  举报