zoukankan      html  css  js  c++  java
  • Siki_Unity_3-13_编程内功修炼-算法

    Unity 3-13 编程内功修炼 -- 算法

    任务1&2:课程介绍

    主要算法:
      分治法
      堆排序
      二叉树
      动态规划
      贪心算法
      图

    任务3:分治算法 -- Divide and Conquer

    分治算法:对于一个规模为n的问题,若该问题可以容易地解决(n较小),则直接解决
      否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同
      递归地解决这些子问题,然后将各子问题的解合并得到原问题的解。

    使用分治法求解的一些经典问题:
      二分搜索
      大整数乘法
      Strassen矩阵乘法
      棋盘覆盖
      合并排序
      快速排序
      线性时间选择
      最接近点对问题
      循环赛日程表
      汉诺塔

    任务4:最大子数组问题
    &5:暴力求解最大子数组问题
    &6&7:分治法求解最大子数组问题

    最大子数组问题:

    已知17天的股票价格, 问哪一天买入哪一天卖出收益最高

    --> 问题转化:价格波动的哪一个子数组为最大子数组

     任务5:暴力求解:

    // Siki老师的方法
    int[] priceArray = { 100, 113, 110, 85, 105, 102, 86, 63, 81, 
              101, 94, 106, 101, 79, 94, 90, 97 }; int[] priceFluctuationArray = new int[priceArray.Length - 1];//价格波动的数组 for (int i = 1; i < priceArray.Length; i++) { priceFluctuationArray[i - 1] = priceArray[i] - priceArray[i - 1]; } int total = priceFluctuationArray[0];//默认数组的第一个元素 是最大子数组 int startIndex = 0; int endIndex = 0; for (int i = 0; i < priceFluctuationArray.Length; i++) { //取得以i为子数组起点的 所有子数组 for (int j = i; j < priceFluctuationArray.Length; j++) { //由i j 就确定了一个子数组 int totalTemp = 0;//临时 最大子数组的和 for (int index = i; index < j + 1; index++) { totalTemp += priceFluctuationArray[index]; } if (totalTemp > total) { total = totalTemp; startIndex = i; endIndex = j; } } } Console.WriteLine("startindex : " + startIndex); Console.WriteLine("endIndex : " + endIndex); Console.WriteLine("购买日期是第"+startIndex+"天, 出售是第"+(endIndex+1)+""
              + ";盈利:" + total); Console.ReadKey();

    我自己的改良方法:O(n^2)

    // 已知股票的价格数组
    int[] priceArray = { 100, 113, 110, 85, 105, 102, 86, 63, 81, 
                        101, 94, 106, 101, 79, 94, 90, 97 };
    // 求价格波动的数组
    int[] priceFluctuationArray = new int[priceArray.Length-1];
    for (int i = 1; i < priceArray.Length; i++) {
        priceFluctuationArray[i - 1] = priceArray[i] - priceArray[i - 1];
    }
    // 计算每个子数组的值
    // 从i到j的子数组
    int maxInterest = priceFluctuationArray[0];
    int startDate = 0;
    int endDate = 0;
    for (int i = 0; i < priceFluctuationArray.Length; i++)  {
        int interest = 0;
        // 子数组头
        for (int j = i; j < priceFluctuationArray.Length; j++) {
            // 子数组尾
            interest += priceFluctuationArray[j];
            if(interest > maxInterest) {
                startDate = i;
                endDate = j;
                maxInterest = interest;
            }
        }
    }
    Console.WriteLine("From " + startDate + " to " + (endDate+1) 
                    + "; interest: " + maxInterest);
    Console.ReadKey();

    任务6:分治法求解

    策略:
      1. 设low为数组开始的index,high为数组结束的index
        [low, high]
      2. 取mid的index
        [low, mid]  [mid+1, high]
        设i, j分别为最大子数组的开始索引和结束索引
        三种情况:
          a) i, j同时位于低区间
          b) i, j同时位于高区间
          c) i位于低区间,j位于高区间
        分别求出这三种情况的最大子数组;比较这三个最大子数组,求出最大子数组
        a和b情况均为将大数组变为小数组,可以使用递归来解决(分解为最小)
        c情况很容易做到:分别取得i~mid的最大值,j~mid的最大值即可:O(n)
      3. 重复步骤2(递归)

    代码实现:

    1. Main
      求完priceFluctuationArray后
      GetMaxSubArray(0, priceFluctuationArray.Length-1, priceFluctuationArray);
      来得到最大子数组

    2. 定义GetMaxSubArray();
      返回值需要最大子数组的startIndex、endIndex和price的总和
      因此设返回值为一个struct
      Struct SubArray {
        int startIndex; int endIndex; int total;
      }

    3. 定义GetMaxSubArray()为:
      static SubArray GetMaxSubArray(int low, int high, int[] array);

    4. 得到mid的值
      int mid = (low + high) / 2;

    5. 三种情况分别讨论

    a) i, j 在低区间
      GetMaxSubArray(low, mid, array);
      // 需要保存返回值,因为最后要比较三种情况,得出最大子数组
      SubArray subArray1 = GetMaxSubArray(...);

    b) i, j 在高区间
      SubArray subArray2 = GetMaxSubArray(mid+1, high, array);

    c) i 在低区间,j 在高区间
      // 不需调用,可以直接解决 -- 一个for循环

    // i 在低区间 [low, mid] 找到最大子数组 [i, mid]
    int maxLowTotal = array[mid];
    int lowTotal = array[mid];
    int startIndex = mid;
    for (int i = mid-1; i >= low; i--) {
      lowTotal += array[i];
      if (lowTotal > maxLowTotal) {
        maxLowTotal = lowTotal;
      startIndex = i;
    }}

    // j 在高区间 [mid+1, high] 找到最大子数组 [mid+1, j]
    for (int j = mid+1; j<= high; j++) {
      highTotal += array[j];
      if (highTotal > maxHighTotal) {
        maxHighTotal = highTotal;
      endIndex = j;
    }}

    // 得到该情况下的最大子数组
    SubArray subArray3;
    subArray.startIndex = startIndex;
    subArray.endIndex = endIndex;
    subArray.total = maxLowTotal + maxHighTotal;

    6. 比较这三种情况下哪一个最大子数组会成为最终的最大子数组

    if (subArray1.total >= subArray2.total && subArray1.total >= subArray3.total) {
      return subArray1;
    } else if (subArray2.total >= subArray1.total && subArray2.total >= subArray3.total) {
      return subArray2;
    } else {
    return subArray3;
    }

    7. 那么,递归什么时候是个头呢?
      当小数组只有一个元素的时候,即low=high的时候
      此时,这个小数组的最大子数组就是本身
      if(low == high) {
        SubArray subArray = new SubArray();
        subArray.startIndex = low;
        subArray.endIndex = high;
        subArray.total = array[low];
        return subArray;
      }

    namespace 最大子数组问题___分治法 {
        class Program {
    
            //最大子数组的结构体
            struct SubArray {
                public int startIndex;
                public int endIndex;
                public int total;
            }
            static void Main(string[] args) {
                int[] priceArray = { 100, 113, 110, 85, 105, 102, 86, 63, 
                      81, 101, 94, 106, 101, 79, 94, 90, 97 }; int[] priceFluctuationf = new int[priceArray.Length - 1]; for (int i = 1; i < priceArray.Length; i++) { priceFluctuationf[i - 1] = priceArray[i] - priceArray[i-1]; } SubArray subArray = GetMaxSubArray(0, priceFluctuationf.Length-1, priceFluctuationf); Console.WriteLine(subArray.startIndex); Console.WriteLine(subArray.endIndex); Console.WriteLine("我们在第" + subArray.startIndex + "天买入,在第" + (subArray.endIndex + 1) + "天卖出"); Console.ReadKey(); } // 这个方法是用来取得array 这个数组 从low到high之间的最大子数组 static SubArray GetMaxSubArray(int low, int high, int[] array) { if (low == high) { // 分治到最小部分 SubArray subArray; subArray.startIndex = low; subArray.endIndex = high; subArray.total = array[low]; return subArray; } else { // 三种情况 // a) i, j同时位于低区间 // b) i, j同时位于高区间 // c) i位于低区间,j位于高区间 int mid = (low + high) / 2; // a) SubArray subArray1 = GetMaxSubArray(low, mid, array); // b SubArray subArray2 = GetMaxSubArray(mid + 1, high, array); // c // i 在低区间 [low, mid] 找到最大子数组 [i, mid] int maxLowTotal = array[mid]; int lowTotal = array[mid]; int startIndex = mid; for (int i = mid - 1; i >= low; i--) { lowTotal += array[i]; if (lowTotal > maxLowTotal) { maxLowTotal = lowTotal; startIndex = i; } } // j 在高区间 [mid+1, high] 找到最大子数组 [mid+1, j] int maxHighTotal = array[mid + 1]; int highTotal = array[mid + 1]; int endIndex = mid + 1; for (int j = mid + 2; j <= high; j++) { highTotal += array[j]; if (highTotal > maxHighTotal) { maxHighTotal = highTotal; endIndex = j; } } SubArray subArray3; subArray3.startIndex = startIndex; subArray3.endIndex = endIndex; subArray3.total = maxLowTotal + maxHighTotal; // 比较这三种情况下哪一个最大子数组会成为最终的最大子数组 if (subArray1.total >= subArray2.total && subArray1.total >= subArray3.total) { return subArray1; } else if (subArray2.total >= subArray1.total && subArray2.total >= subArray3.total) { return subArray2; } else { return subArray3; } } } } }

    任务8:树(数据结构)

    树 (Tree)是n(n>=0)个结点的有限集。
    n=0时称为空树
    在任意一棵非空树中,1. 有且仅有一个特定的结点称为根结点 (root)
      2. 当n>1时,其余结点可分为m(m>0)个互不相交的有限集,其中每个集合本身又是一棵树
        并称为根的子树 (SubTree)

    一些概念:

    结点的度:拥有子树的个数
    树的层次:根结点为第一层,根结点的子结点为第二层,以此类推
    树的深度:最大的层次
    兄弟关系:有同一个父亲结点的两个子结点之间的关系为兄弟关系(如G和H)
      堂兄弟关系:层次相同(没有共同父亲的即为堂兄弟,如I和J)
    一个结点只能有一个父结点,子树之间是不相交的

    任务9:树的存储结构

    存储结构:顺序存储和链式存储
      如果使用顺序存储(数组),结点之间的关系就很难表示出来
      因此使用链式存储:
        1. 双亲表示法 -- 
          优点:可以很方便地访问父亲结点
          缺点:不容易访问兄弟结点和子结点
        2. 孩子表示法 -- 
          优点:通过父亲能很方便地访问所有子结点
          缺点:不容易访问父亲结点和兄弟结点;
             浪费空间:每个结点的孩子数量不同,有很多指向孩子地址的空间为null

        3.孩子兄弟表示法 -- 
          保存了第一个孩子和右边的兄弟

     优点:方便地访问所有的子结点(通过firstchild访问rightsib即可)
        方便地访问它的兄弟
     缺点:不容易访问父结点

    4. 还有其他表示法

    任务10&11:二叉树的定义和性质(满二叉树、完全二叉树)

    二叉树 (Binary Tree):每个结点最多有两个子结点(子结点有左右的顺序)

    左右子树:每个结点可能有一个左子树和/或一个右子树

    特殊二叉树:
      斜树:(左斜树、右斜树):所有结点只有左 (右)子树的二叉树
      满二叉树:树的任意一个结点都有两个子结点(叶子结点没有子结点)
      完全二叉树:对一棵具有n个结点的二叉树按层序编号,如果编号为i (1<=i<=n)的结点与同
        样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则该树为完全二叉树
        (树的任意一个结点都有两个子结点(最后一层叶子结点可以从右开始连续缺失))
      满二叉树一定是完全二叉树,反之就不对了

    二叉树的性质:
      二叉树的第i层上最多有2(i-1)个结点
      深度为k的二叉树至多有2k-1个结点
          20+21+22+23+24+...+2k-1
         =  20+21+22+23+24+...+2k-1 + 1 - 1
         =  1 + 20+21+22+23+24+...+2k-1 - 1
         =  20 + 20+21+22+23+24+...+2k-1 - 1
         =  2k - 1
      n个结点的完全二叉树,对结点从1开始编号(按满二叉树的方式),对任意结点i有:
        a) 它的父结点是 i/2
        b) 它的左孩子是 2i, 右孩子是2i+1
        c) 如果2i>n 说明无子结点;如果2i+1>n说明无右孩子

    任务12:二叉树的存储结构

    对于一般树来说,使用链式存储比较方便。
    但是对于二叉树来说,兄弟、父亲、孩子可以很方便的通过编号得到,所以使用顺序存储

    顺序存储:将所有二叉树都当作完全二叉树来存储
      依次按结点编号(从1开始)放入数组(编号=index+1)
      不存在的结点=null即可 -- 非完全二叉树会有空间浪费

    链式存储:结点结构:

    无法方便地访问父结点;一个结点需要三倍内存空间;
    头结点head指向根结点;每个结点的两个指针指向存在的子结点

    任务13:二叉树的四种遍历方法

    二叉树的遍历是指:从根结点出发,按照某种次序依次访问二叉树中的所有结点,使每个结点仅被访问一次

    前序遍历:先输出当前结点的数据,再依次遍历输出左结点和右结点
      ABDGHCEIF

    中序遍历:先遍历输出左结点,再输出当前结点的数据,再遍历输出右结点
      GDHBAEICF

    后序遍历:先遍历输出左结点,再遍历输出右结点,最后输出当前结点的数据
      GHDBIEFCA

    层序遍历:从树的第一层开始,从上到下逐层遍历;同一层中,从左到右对结点逐个访问输出
      ABCDEFGHI

    任务14&15&16:代码实现二叉树的顺序存储和遍历

    使用数组实现二叉树

    class BiTree<T> {
        private T[] data;
        private int count = 0;
    
        public BiTree(int capacity) {
            data = new T[capacity];
        }
    
        public bool Add(T item) {
            if (count >= data.Length) {
                // 二叉树满了
                return false;
            }
            data[count] = item;
            count++;
            return true;
        }
        ...
    }

    二叉树的遍历:

    前序遍历:void FirstTraversal(int index) {
        // index为想要遍历的索引
        // 先输出结点数据
        Console.Write(data[index]);
        // 得到要遍历的结点编号
        int number = index + 1;
        // 遍历左子结点
        FirstTraversal(number * 2 - 1);  // index = number - 1
        // 遍历右子结点
        FirstTraversal(number * 2 + 1 - 1);
        }

    但是,每一种递归都需要一个终止条件,当结点没有子结点的时候,即可返回
      if(index >= data.Length)  return;

    而且,当二叉树不是完全二叉树时,需要判断结点是否为空
      if(data[index] == null)  return;

    如何开始遍历呢?遍历编号为1的结点即可;
      外界调用封装好的方法 public void FirstTraversal();
      public void FirstTraversal() {
        FirstTraversal(0);
      }
      并设FirstTraversal(int index) 为 private

    // siki老师的方法
    public void FirstTraversal() {
        FirstTraversal(0);
    }
    private void FirstTraversal(int index) {
        if (index >= count) return;
        //得到要遍历的这个结点的编号 
        int number = index + 1;
        if (data[index].Equals(-1)) return;
        Console.Write(data[index] + " ");
        //得到左子结点的编号
        int leftNumber = number * 2;
        int rightNumber = number * 2 + 1;
        FirstTraversal(leftNumber - 1);
        FirstTraversal(rightNumber - 1);
    }
    // 我的方法
    public void FirstTraversal() {
        FirstTraversal(0);
    }
    
    private void FirstTraversal(int index) {
        // index为想要遍历的索引
        // 先输出结点数据
        if(data[index ] == null) {
            return;
        }
        Console.Write(data[index]);
        // 得到要遍历的结点编号
        int number = index + 1;
        if (number*2<=count) {
            // 遍历左子结点
            FirstTraversal(number * 2 - 1);
        }
        if(number * 2 + 1 <= count) {
            // 遍历右子结点
            FirstTraversal(number * 2 + 1 - 1);
        }
    }

    中序遍历:

     

     

    后序遍历:

     

     

    层序遍历:

     

     

    任务17:二叉排序树/ 二叉查找树

    二叉排序树:对于任意结点,若左子树不为空,则左子树上所有结点的值均小于根结点的值
      若右子树不为空,则右子树上所有结点的值均大于根结点的值

    优点:排序方便(直接通过中序遍历就能得到有序结果),
       查找方便(从根结点开始,判断往左/ 右子树继续查找即可)
       插入方便(与查找相似)

    二叉排序树的删除操作:
      1. 删除叶子结点:直接删除
      2. 删除的结点只有一个子结点(左图):用子结点替换掉被删除的结点
        删除的结点只有一个子树(右图):用子树的根结点替换掉被删除的结点

    37替代35,93替代99      47替代58,直接建立47~62的连接

    3. 删除的结点有两个子结点/子树:
      需要选出所有子结点中间大小的结点
      a) 找到左子树中的最右子结点(for遍历所有的右结点)(左子树中的最大值)
      b) 找到右子树中的最左子结点(for遍历所有的左结点)(右子树中的最小值)

    链式结构:由于二叉树的结点的位置由自身值的大小有关系,因此需要使用链式结构存储

    任务18&19&20&21:二叉排序树的代码实现

    添加操作

    。。。

    排序和查找

    。。。

    查找方法的优化

    。。。

    删除操作

    。。。

    任务22:堆和堆排序

    堆:完全二叉树
      大顶堆:每个结点的值都大于或等于其孩子结点的值
      小顶堆:每个结点的值都小于或等于其孩子结点的值

    堆排序:利用堆来进行排序
      将待排序的序列构造成一个大顶堆,此时整个序列的最大值就是根结点。
      将它和堆的最后一个元素交换,此时末尾元素就是最大值
      将剩余的n-1个序列重新构造成一个堆,这样就会得到n各元素中的次小值
      反复执行就能得到一个有序序列

     思路:
      使用顺序结构存储完全二叉树
      构造大顶堆:
        叶子结点本身是一个大顶堆,无序遍历
        将[30, 60, 20]构造成一个大顶堆 --> 取最大值60,与根结点30交换即可
        将[10, 60, 70]构造成一个大顶堆 --> 取最大值70,与根结点10交换即可
          但是交换之后,编号为4的结点发生了变化,需要遍历以下整棵子树构造大顶堆

    任务23:大顶堆的代码实现

    。。。

    任务24:堆排序的代码实现

    。。。

    。。。 

    任务25:动态规划算法  (Dynamic Programming)

    动态规划算法:基于一个递归公司及一个或多个初始状态。
      当前子问题的解由上一次子问题的解推出。
      动态规划和分治法相似,都是通过组合子问题的解来求解原问题。
        分治法:问题划分成互不相交的子问题,递归求解子问题,再将子问题的解组合
        动态规划:子问题重叠(不同子问题具有公共的子子问题)。
          此时分治法会反复求解那些公共子问题 (没必要)
          而动态规划算法对每个子子问题求解一次,并将结果保存,无需重新计算

    动态规划的方法进行求解:
      动态规划算法安排求解的顺序,对每个子问题只求解一次,并将结果保存下来。
      如果随后再次需要此子问题的解,只需查找保存的结果,不必重新计算。
      因此动态规划的方法是付出额外的内存空间,来节省计算时间。

    动态规划的两种实现方法:
      方法一:带备忘的自顶向下法
        按照自然的递归形式编写过程,但过程中会保存每个子问题的解(通常保存在一个数组中)。
        当需要计算一个子问题的解时,过程首先检查是否已经保存过此解。
        如果是,则直接返回保存的值,从而节省了计算时间;
        如果没有保存过此解,按照正常方式计算这个子问题。
      方法二:自底向上法
        首先恰当的定义子问题的规模,使得任何问题的求解都只依赖于更小的子问题的解。
        因而我们将子问题按照规模排序,按从小到大的顺序求解。
        当求解某个问题的时候,它所依赖的更小的子问题都已经求解完毕,结果已经保存。

    两个实例讲解动态规划算法:
      钢条切割问题(见任务26)
      背包问题(见任务31)

    任务26&27:钢条切割问题分析(动态规划)

    已知出售一段长度为i英寸的钢条的价格为pi (i=1,2,3...)。
    给一段长度为n的钢条,问怎么切割收益rn最大

    比如:
      n=1: 长度为1,价格为1
      n=2: 长度为2,价格为5
         长度为1+1,价格为1+1=2;
      n=3: 长度为3,价格为8
         长度为1+2,价格为1+5=6;
         长度为1+1+1,价格为1+1+1=3;
      n=4: 长度为4,价格为9;
         长度为1+3,价格为1+8=9;
         长度为1+1+2,价格为1+1+5=7;
         长度为1+1+1+1,价格为1+1+1+1=4;
         长度为1+2+1,价格为1+5+1=7;
         长度为2+2,价格为5+5=10
         长度为2+1+1,价格为5+1+1=7;
         长度为3+1,价格为8+1=9;

    假设最优解把n段切割成了k段:n = i1 + i2 + i3 + ... + ik (1<=k<=n)
      最大收益rn = pi1 + pi2 + pi3 + ... + pik

    求解方案:

    第一种方案:(递归求解,递归太多,过于复杂)

    1. 不切割:收益为pn
    2. 切割成两半:分解为两个子问题
    rn = max(pn, (r1 + rn-1), (r2 + rn-2), ... , (rn-1 + r1))

    第二种方案:

    从钢条的左边切下长度为i (i=n即不切割(i!=0))的一段,
    只对右边剩下长度为n-i的一段继续切割
    rn = max((p1 + rn-1), (p2 + rn-2), ... , (pn-1 + r1), (pn + 0))
    与第一种方案的区别:
    每次递归调用的时候只需递归一种情况而不是两种情况

    代码实现:自顶向下递归实现(求rn需要求rn-1,求rn-1需要求rn-2,最后需要求r1

    任务28&29&30:钢条切割问题的三种代码实现

    。。。

    。。。

    。。。

    任务31:0-1背包问题简介
    &33:背包问题的动态规划分析

    有容量m kg的背包,另外有i种 (1~i)物品 (每种只有一个),总量分别为w[1] w[2] ... w[i] (kg)
      价值分别为p[1] p[2] ... p[i] (元)
      问将哪些物品放入背包可以使得背包的总价值最大?最大价值是多少?

    穷举法:把所有情况列出来,比较得到总价值最大的情况(O(2n))

    动态规划:i个物品放入容量为m(kg)的背包的最大价值 (记为c[i, m])
      对于每种物品只有两种选择:装入 或 不装入 -- 01背包问题
      每种物品不能装入多次

    对于c[i. m]有几种情况:
      c[i, 0] = c[0, m] = 0;
      c[i, m] = c[i-1, m] (w[i]>m) 
      当w[i]<=m时,两种情况:
        不放入i,c[i, m] = c[i-1, m]
        放入i,c[i, m] = c[i-1, m-w[i]] + p(i)
        --> c[i, m] = max(放入i, 不放入i)
    将c[i, m]的问题分解为小问题,直到c[0, m]或c[i, 0]返回 -- 递归 -- 自顶向下
    大问题依赖着小问题,从小问题向上解决到大问题也可以完成 -- 自底向上

    任务32:使用穷举法实现背包问题

    。。。

    任务34&35&36:三种方式实现背包问题(代码)
      (不带备忘的自顶向下法/ 带备忘的自顶向下法/ 自底向上法)

    。。。

    。。。

    。。。

    任务37:贪心算法

    对于许多最优化问题 (钢条切割问题--最大收益/ 背包问题--最大价值),使用动态规划算法有些没必要
      可以使用更加简单高效的算法 -- 贪心算法
    贪心算法在每一步做出当时看起来最佳的选择,即从每一次局部最优的选择,得到最后全局最优解
      对于某些问题并不能保证得到最优解,但对很多问题确实可以得到最优解。
      但即使某些时候不能得到最优解(而是次优解),我们也可能会选择贪心算法求解 -- 高效

    任务38:活动选择问题(贪心算法)

    有n个需要在同一天使用同一个教室的活动a1, a2, ..., an,教师同一时刻只能由一个活动使用。
      每个活动ai都有一个开始时间si和结束事件fi;一旦被选择后,活动ai就占据时间区间 [si, fi)
      如果 [si, fi) 和 [sj, fj) 互不重叠,ai和aj两个活动就可以被安排在这一天。
      活动选择问题:安排这些活动,使尽量多的活动能不冲突地在这一天举行 (最大兼容活动子集)
      注:是尽量多的活动,而不是尽可能长的时间
      最大兼容子集可能不是唯一的。


      如:{a1, a4, a8, a11} 和 {a2, a4, a9, a11} 都是最大兼容子集

    任务39&40&41:动态规划思路解决活动选择问题

    。。。

    。。。

    。。。

    任务42&43&44:贪心算法解决活动选择问题

    什么是局部最优选择:
      对于任何非空集合S,若am是S中结束时间最早的活动,则am一定在S的某个最大兼容子集中
      证明 -- 反证法:
        反证:am不在任意一个S的最大兼容子集中
        证明1号活动不在最大兼容子集中
          如果1号活动在最大兼容子集中,则最大兼容子集为1号活动 + [4, 24)中的最大兼容子集
          如果1号活动不在最大兼容子集中,则后面肯定有一个活动是在最大兼容子集中的
            假如是3号,则最大兼容子集为3号活动 + [6, 24)中的最大兼容子集
            而[4, 24)肯定比[6, 24)的时间长,因此a3开始的兼容子集肯定不会比a1开始的大
          那么如果3号开始的兼容子集最大,那么1号开始的也肯定是最大兼容子集 (数量相等)
          因此1号活动肯定在最大兼容子集中
        因此a1一定在S的某个最大兼容子集中。

    无法一步到位直接找到最大兼容子集的话,寻找局部最优选择(找到头活动--结束时间最早)

    1. 将所有活动按照结束时间早晚进行排序
    2. 判断开始时间是否满足条件 + 比较结束时间是否最早 (无需判断,因为已经有序):遍历即可

      在(0-24)这段时间中,1号活动一定在最大兼容子集中(结束时间最早)
      剩下(4~24)这段时间,寻找4点以后开始,结束时间最早的活动 -- 子问题

    。。。

    。。。

    。。。

    任务45:钱币找零问题(贪心算法)

    假设1元、2元、5元、10元、20元、50元和100元纸币分别有c0, c1, ... c6张来支付K元,至少要几张

    贪心算法:每一步尽可能用面值大的纸币

    static void Main(string[] args) {
        int[] count = { 3, 0, 2, 1, 0, 3, 5 };
        int[] amount = { 1, 2, 5, 10, 20, 50, 100 };
        int[] result = Change(324, count, amount);
        foreach (int i in result) {
            Console.Write(i + " ");
        }
        Console.ReadKey();
    }
    
    public static int[] Change(int k, int[] count, int[] amount) {
        int[] result = new int[count.Length + 1]; // 最后一位表示未支付金额
        // 当k的金额比所有纸币的总额都大,无法达到要求
        // 当小面额的纸币不够补足k的零头,无法达到要求,比如本例中k=4时
        for (int i = count.Length - 1; i >= 0; i--)  {
            if(k<=0) { break; }
            // 情况1:k比该面额大
            // 情况2:k比该面额小
            if (count[i] >= k / amount[i]) {
                result[i] = k / amount[i];
                k %= amount[i];
            } else {
                // 情况3:该面额数量太少,没有足够该面额纸币
                result[i] = count[i];
                k -= (count[i] * amount[i]);
            }
        }
        result[result.Length - 1] = k; // 最后一位存储未支付金额
        return result;
    }

    任务46:图

    图的主要应用是在地图上,如导航寻路等;
      如地图上有一个物品,需要自动寻路捡起该物品 -- 求最短路径

    图 Graph:由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为 G<V,E> (Vertex, Edge)
      注:图的顶点不能是空的,但是边可以为空
      顶点之间的关系:多对多

    图的有关定义:
      无向边:顶点之间的边没有方向,用无序偶对(vivj)表示
      无向图 (Undirected graph):图中任意两个顶点之间的边都是无向边
      有向边/ 弧 (Arc):用有序偶对<vi, vj>表示从vi到vi的有向边,vi为弧尾(Tail),vj为弧头(Head)
      简单图:不存在顶点到自身的边,且同一条边不重复出现
      连通图 (Connected graph):

    图的存储:
      邻接矩阵 Adjacency Matrix(两个数组,一维数组存储顶点信息,二维数组存储边或弧的信息)
      邻接表 Adjacency List(数组与链表相结合,一维数组存储顶点和指向第一个邻接点的指针,
        便于查找该顶点的边信息,每个顶点vi的所有邻接点构成一个线性表(单链表))
      十字链表 Orthogonal List:把邻接表和逆邻接表结合起来
        (邻接表关心了出度问题,逆邻接表关心了入度问题)
      邻接多重表......
      边集数组......

    图的遍历:
      深度优先 Depth First Search (DFS):类似于树的前序遍历
        从图中某个顶点v出发,访问此顶点,然后从v的未被访问的邻接点出发,深度优先遍历图,
        直到图中所有和v有路径相通的顶点都被访问到
        对于非连通图:若图中还有顶点未被访问,则另选图中一个未曾访问的顶点开始DFS
      广度优先 Breadth First Search (BFS):类似于树的层序遍历

      最小生成树 Minimum Cost Spanning Tree:构造连通网的最小代价生成树
        Prim算法:
        Kruskal算法:

    最短路径:
      Dijkstra算法:按路径长度递增的次序 O(n2)
        一步步求出它们之间顶点的最短路径,过程中都是基于已经求出的最短路径的基础上求得
        更远顶点的最短路径
        注:要求图中不存在负权边
      Floyd算法:

    拓扑排序:

    关键路径:

     

     

     

     

     

     

     

  • 相关阅读:
    支付宝自研数据库OceanBase全球开放!淘汰甲骨文
    网易邮箱运维工程师岗位应聘笔试题
    Python中的10个常见安全漏洞及修复方法
    关于数据库性能优化的一些误区
    解密程序员招聘内幕
    JSP---第十次作业
    JSP----第九次作业
    JSP---第八次作业
    JSP---第七次作业
    第六次作业---JDBC连接数据库
  • 原文地址:https://www.cnblogs.com/FudgeBear/p/8640417.html
Copyright © 2011-2022 走看看