nuc算法期末复习资料

第一章 算法概述与算法分析基础

第一部分 算法概述

第二部分 算法分析基础

幻灯片2

第一部分 算法概述

算法的基本概念

为什么学习和研究算法

重要的问题类型

现代算法概览

幻灯片3

1  算法的基本概念

  1. 算法是对特定问题求解步骤的一种描述,是指令的有限序列。
  1. 算法的特性

幻灯片4

算法的五大特性:

⑴ 输入:一个算法有零个或多个输入。

⑵ 输出:一个算法有一个或多个输出。

⑶ 有穷性:一个算法必须总是在执行有穷步之后结束,且每一步都在有穷时间内完成。

⑷ 确定性:算法中的每一条指令必须有确切的含义,对于相同的输入只能得到相同的输出。

⑸ 可行性:算法描述的操作可以通过已经实现的基本操作执行有限次来实现。

幻灯片5

算法概念理解: 问题及问题实例

Problem — 问题

规定了输入与输出之间的关系,可以用通用语言来描述;

Instance of a Problem — 问题实例

某一个问题实例包含了求解该问题所需输入;

输入: 由n个数组成的一个序列<a1,a2,,an>

输出: 对输入系列的 一个排列(重排) <a1’,a2’,,an’>,使得<a1’a2’ an’>

排序问题的一个实例

Input: <31,41,59,26,41,58> —— Output: <26,31,41,41,58,59>

幻灯片6

算法的其他特性:正确性、健壮性、可理解性、抽象分级、高效性

算法的描述方法:自然语言、程序流程图、伪代码、程序设计语言

第二部分 算法分析基础

案例-百鸡问题

算法分析基础

算法的数学基础

2.2 算法分析基础

2.2.1 基本运算与输入规模

2.2.2 时间复杂度与空间复杂度

2.2.3 渐近符号

2.2.4 再看时空复杂度

2.2.5 最优算法

幻灯片10

2.2.1 基本运算与输入规模

  1. 基本运算:比较,加法,乘法,置指针,交换…
  1. 输入规模:输入串编码长度
  1. 通常用下述参量度量:数组元素多少,调度问题的任务个数,图的顶点数与边数等
  1. 算法基本运算次数通常对应输入规模

幻灯片11

2.2.1 基本运算与输入规模

  1. 输入规模:
  1. 排序:数组中元素的个数n
  1. 检索:被检索数组的元素个数
  1. 整数乘法:两个整数的位数m,n
  1. 矩阵相乘:矩阵的行列数I,j,k
  1. 图的遍历:图的顶点数n,边数m

2.2.1 基本运算与输入规模

  1. 基本运算:
  1. 排序:元素之间的比较
  1. 检索:被检索元素x与数组元素的比较
  1. 整数乘法:每位数字相乘(位乘)1次,m位和n位相乘要做mn次位乘
  1. 矩阵相乘:每对元素乘1次,      矩阵与
  1.           矩阵相乘要做ijk次乘法
  1. 图的遍历:置指针

幻灯片13

2.2.2 时间复杂度与空间复杂度

我们需要对算法所需要的两种计算机资源——时间和空间进行估算

  1. 算法时间复杂度:针对指定基本运算,计算算法所做运算次数
  1. 算法空间复杂度:指算法在计算机内执行时所需存储空间的度量
  1. 注意:时间复杂度不是用来计算程序具体耗时的,空间复杂度也不是用来计算程序实际占用的空间的
  1. 算法分析的目的:
  1.  设计算法——设计出复杂度尽可能低的算法
  1.  选择算法——在多种算法中选择其中复杂度最低者

幻灯片14

2.2.2 时间复杂度与空间复杂度

  1. 时间复杂性分析的关键:
  1. 问题规模:输入量的多少;
  1. 基本语句:执行次数与整个算法的执行时间成正比的语句

for (i=1; i<=n; i++)

    for (j=1; j<=n; j++)

         x++;

问题规模:n

基本语句:x++

幻灯片15

2.2.3 渐进符号——o

定义

若对于任意正数c,存在no,对于任意n2no,有0<T(n)<cf(n)成立,记作T(n)=of(n)

例如:

T(n)=n^2+n, 则 f(n)=o(n^3)

幻灯片29

2.2.3 渐进符号——ω

 定义若对于任意正数c,存在n,对于任意n2n,有0 ≤ cf(n) < T(n)成立,则记作f(n) = w(g(n))

例如:

幻灯片31

2.2.3 渐进符号——Θ

Θ符号(运行时间的准确界)

定义1.3  若存在三个正的常数c1、c2和n0,对于任意n≥n0,都有c1f(n)≥T(n)≥c2×f(n),则称T(n)=Θ(f(n))。

Θ符号意味着T(n)与f(n)同阶,用来表示算法的精确阶。

例1.1  T(n)=3n-1

【解答】

当n≥1时,3n-1≤3n=O(n)

当n≥1时,3n-1≥3n-n=2n=Ω(n)

当n≥1时,3n≥3n-1≥2n,则3n-1=Θ(n)

例1.2  T(n)=5n2+8n+1

【解答】当n≥1时,5n2+8n+1≤5n2+8n+n=5n2+9n=O(n2)

当n≥1时,5n2+8n+1≥5n2=Ω(n2)

当n≥1时,14n2≥5n2+8n+1≥5n2,则5n2+8n+1=Θ(n2)

幻灯片33

2.2.3 渐进符号——定理1

2.2.3 渐进符号——一些重要结果

2.2.3 渐进符号——定理2

2.2.3 渐进符号——定理2-例子

2.2.3 渐进符号——定理3

2.3

算法的数学基础

2.3 算法的数学基础

2.3.1 关于阶的重要结论

2.3.2 序列求和

2.3.3 递推方程与算法分析

2.3.4 迭代法

2.3.5 递归树

2.3.6主定理

2.3.1 关于阶的重要结论

2.3.2 序列求和——求和公式

2.3.2 序列求和——求和实例

2.3.2 序列求和——二分检索算法

2.3.2 序列求和——估计和式上界的方法

2.3.2 序列求和——小结

  1. 序列求和基本公式:等差数列、等比数列、调和级数
  1. 估计序列求和:放大法求上界、用积分做和式的渐近的界

2.3.3 递推方程与算法分析

递推方程的求解:

2.3.3 递推方程与算法分析——Fibonacci数列问题

2.3.3 递推方程与算法分析—— Hanoi塔问题

2.3.4 迭代法——    二分归并排序

2.3.4 迭代法——换元

2.3.4 迭代法——迭代求解

2.3.4 迭代法——解得正确性-归纳验证

2.3.4 迭代法——小结

迭代法求解递推方程

  1. 直接迭代,代入初值,然后求和
  2. 对递推方程和初值进行换元,然后求和,求和后进行相反换元,得到原始递推方程的解
  3. 验证方法——数学归纳法

2.3.5 递归树——概念

  1. 递归树是迭代迭代计算的模型,即图形化形式
  2. 递归树的生成过程与迭代过程一致
  3. 递归树上所有项恰好是迭代之后产生和式中的项
  4. 对递归树上的项求和就是迭代后方程的解

2.3.5 递归树——迭代在递归树中的表示

2.3.5 递归树——二层子树的例子

2.3.5 递归树——递归树的生成规则

初始,递归树自有根节点,其值为W(n)

不断继续下述过程:

将函数项叶节点的迭代式W(m)表示成二层子树

用该子树替换叶子节点

继续递归树的生成,直到树中无函数项(只有初值)为止

2.3.5 递归树——二层子树的例子

2.3.5 递归树——递归树

2.3.5 递归树——递归树应用实例

2.3.5 递归树——小结

递归树是迭代的图形表述

递归树的生成规则

如何利用递归树求解递推方程

\\

第2章  蛮力法

2.1  蛮力法的设计思想

2.2  查找问题中的蛮力法

2.3  排序问题中的蛮力法

2.4  组合问题中的蛮力法

2.5  图问题中的蛮力法

【例】百钱百鸡问题。中国古代数学家张丘建在《算经》中提出了著名的“百钱百鸡问题”:鸡翁一,值钱五;鸡母一,值钱三;鸡雏三,值钱一;百钱买百鸡,翁、母、雏各几何?

设计1:

#include <stdio.h>

void main(   )

{ int x,y,z;

    for(x=1;x<=20;x=x+1)

        for(y=1;y<=33;y=y+1)

          for (z=1;z<=100;z++)

            if(x+y+z==100 && 5*x+3*y+z/3.0==100)

            {

              printf("the cock number is %d",x);

                 printf("the hen number is %d", y);

                 printf("the chick number is %d\n",z);

            }

}

 

算法设计2:

 在公鸡(x)、母鸡(y)的数量确定后,小鸡的数量z就固定为:100-x-y,无需再进行枚举了。

  此时约束条件只有一个:  5*x+3*y+z/3=100。

#include <stdio.h>

void main(   )

{  int x, y,z;

    for (x=1;x<=20;x=x+1)

       for (y=1;y<=33;y=y+1)

         {    z=100-x-y;

              if( 5*x+3*y+z/3.0==100)

                {

                  printf(" the cock number is %d",x);

                    printf(" the hen number is %d", y);

                    printf(" the chick number is %d\n",z);

                   }

         }

}

算法分析:以上算法只需枚举尝试20*33=660次。实现时约束条件为:“5*x+3*y+z/3.0=100”,进一步提高了算法效率。

幻灯片6

【例】解数字迷        A  B  C  A  B

                    ×            A   

                   D  D  D  D  D  D

设计1:

算法1如下:

#include <stdio.h>

void main()

{ int A,B,C,D,E,E1,F,G1,G2,i;

  for(A=3;  A<=9;  A++)

    for(B=0;  B<=9;  B++)

     for(C=0;  C<=9;  C++)

       { F=A*10000+B*1000+C*100+A*10+B;

         E=F*A; E1=E;  G1=E1%10;

         for(i=1;  i<=5;  i++)

           { G2=G1; E1=E1/10;  G1= E1%10;

               if(G1!=G2 )    break;  }

           if(i==6)  

            printf( "%d*%d=%d\n", F,A,E);

      }

 }

 算法设计2:将算式变形为除法:DDDDDD/A=ABCAB。此时只需枚举A:3—9 D:1—9,共尝试7*9=63次。每次尝试,测试商的万位、十位与除数是否相同,千位与个位是否相同,都相同时为解。

#include <stdio.h>

main( )

{int A,B,C,D,E,F;

 for(A=3;A<=9;A++)

    for(D=1;D<=9;D++)

     { E= D*100000+D*10000+D*1000+D*100+D*10+D;

        if( E%A==0) 

              {  F=E/A;

              if(F/10000==A && F%100/10= =A && F/1000%10==F%10)

                printf( "%d*%d=%d\n", F,A,E);

              }

      }

 }

【例】贴纸问题

有A、B、C、D、E五人,每人额头上都帖了一张黑或白的纸。五人对坐,每人都可以看到其他人额头上的纸的颜色。五人相互观察后,

A说:“我看见有三人额头上帖的是白纸,一人额头上帖的是黑纸”

B说:“我看见其他四人额头上帖的都是黑纸”

C说:“我看见有一人额头上帖的是白纸,其他三人额头上帖的是黑纸”

D说:“我看见其他四人额头上帖的都是白纸”

E说:什么也没有说

现在已知额头上帖黑纸的人说的都是谎话,额头上贴白纸的人说的都

是实话,请你编写程序,求出这五个人谁的额头上帖的白纸,谁的额

头上帖的黑纸。

1)枚举范围为:

  A:0—1,B:0—1,C:0—1 D:0—1 E:0—1

  0表示贴白纸,1表示贴黑纸

2)约束条件为:

A说:“我看见有三人额头上帖的是白纸,一人额头上帖的是黑纸”

B说:“我看见其他四人额头上帖的都是黑纸”

C说:“我看见有一人额头上帖的是白纸,其他三人额头上帖的是黑纸”

D说:“我看见其他四人额头上帖的都是白纸”

E说:什么也没有说

现在已知额头上帖黑纸的人说的都是谎话,额头上贴白纸的人说的都

是实话。

每次尝试五人的各种贴纸的方式,若某种方式满足约束条件则找到了问

题的解。   

#include <stdio.h>

void main()

{int A, B, C,D, E;

for(A=0;A<2;A++)

for(B=0;B<2;B++)

 for(C=0;C<2;C++)

  for(D=0;D<2;D++)

    for(E=0;E<2;E++)

if(( A==0 && B+C+D+E==1) ||( A==1 && B+C+D+E!=1))

    if((B==0 && A+C+D+E==4) ||(B==1 &&  A+C+D+E!=4))

     if((C==0 && A+B+D+E==3)||(C==1 && A+B+D+E!=3))         

      if((D==0 && A+B+C+E==0) || (D==1 && A+B+C+E!=0))

     { printf("A头上贴的是%s\n", A==0 ? "白纸" : "黑纸");           

     printf("B头上贴的是%s\n", B==0 ? "白纸" : "黑纸");

     printf("C头上贴的是%s\n", C==0 ? "白纸" : "黑纸");

     printf("D头上贴的是%s\n", D==0 ? "白纸" : "黑纸");

     printf("E头上贴的是%s\n", E==0 ? "白纸" : "黑纸");

    }

}

蛮力法的优点

  1. 逻辑清晰,编写程序简洁
  1. 对于一些重要的问题(比如:排序、查找、矩阵乘法和字符串匹配),可以产生一些合理的算法
  1. 解决问题的实例很少时,可以花费较少的代价
  1. 可以解决一些小规模的问题(使用优化的算法没有必要,而且某些优化算法本身较复杂)
  1. 可以作为其他高效算法的衡量标准

2.2  查找问题中的蛮力法

2.2.1  顺序查找

2.2.2  串匹配问题

2.2.1  顺序查找

【问题】在一个整数集合中查找值为 k 的元素。

【想法 1】将集合中的元素逐个与给定值 k 进行比较,若相等,则查找成功,给出该元素在集合中的序号;若整个集合比较完仍未找到与给定值相等的元素,则查找失败,给出失败信息。

int SeqSearch1(int r[ ], int n, int k)

{  

     int i = n;

     while (i > 0 && r[i] != k)

         i--;

     return i;

}

【想法 2为了避免在查找过程中每一次比较后都要判断查找位置是否越界,可以设置观察哨(sentinel),即将待查值放在查找方向的“尽头”处,则比较位置i至多移动到下标0处,也就是“哨兵”的位置。

【算法实现 2设函数SeqSearch2实现改进的顺序查找算法,程序如下:

int SeqSearch2(int r[ ], int n, int k)

{  

     int i = n;

     data[0] = k;

     while (data[i] != k)

        i--;

     return i;

}

【算法分析】算法SeqSearch1的时间主要耗费在条件表达式(i > 0 && r[i] != k),算法SeqSearch2的时间主要耗费在条件表达式(r[i] != k),设 pi 表示查找第 i 个元素的概率,等概率情况下,执行次数为:

2.4.2  任务分配问题

假设有n个任务需要分配给n个人执行,每个任务只分配给一个人,每个人只分配一个任务,且第j个任务分配给第i个人的成本是C[i, j](1≤i , j≤n),任务分配问题要求找出总成本最小的分配方案。

下图是一个任务分配问题的成本矩阵,矩阵元素C[i, j]代表将任务j分配给人员i的成本。 任务分配问题就是在分配成本矩阵中的每一行选取一个元素,这些元素分别属于不同的列,并且元素之和最小。

由于任务分配问题需要考虑的排列数量是n!,所以,除了该问题的一些规模非常小的实例,蛮力法几乎是不实用的。

2.5  图问题中的蛮力法

2.5.1  哈密顿回路问题

2.5.2  TSP问题

2.5.1  哈密顿回路问题

【问题】著名的爱尔兰数学家哈密顿(William Hamilton)提出的周游世界问题。假设正十二面体的 20 个顶点代表 20 个城市,哈密顿回路问题(Hamilton cycle problem)要求从一个城市出发,经过每个城市恰好一次,然后回到出发城市。

【想法】蛮力法求解哈密顿回路的基本思想是,对于给定的无向图 G=(V, E),依次考察图中所有顶点的全排列,满足以下两个条件的全排列(vi1, vi2, …, vin)构成的回路就是哈密顿回路:

(1)(vij, vij+1)∈E(1≤j≤n-1)

(2)(vin, vi1)∈E

【想法】依次考察图中所有顶点的全排列,满足(1)(vij, vij+1)∈E(1 ≤ j ≤ n-1)

(2)(vin, vi1)∈E 的全排列(vi1, vi2, …, vin)构成的回路就是哈密顿回路。

【算法】蛮力法求解哈密顿回路的算法用伪代码描述如下:

算法:哈密顿回路HamiltonCycle

输入:无向图 G=(V, E)

输出:如果存在哈密顿回路,则输出该回路,否则,输出无解信息

       1. 对顶点集合{1, 2, …, n}的每一个排列 vi1vi2…vin 执行下述操作:

           1.1 循环变量 j 从 1~n-1 重复执行下述操作:

                 1.1.1 如果顶点 vij 和 vij+1 之间不存在边,则转步骤 1 考察下一个排列;

                 1.1.2 否则 j++;

           1.2 如果 vin 和 vi1 之间存在边,则输出排列 vi1vi2…vin,算法结束;

       2. 输出无解信息;

【算法分析】算法HamiltonCycle在找到一条哈密顿回路后,即可结束算法。但是,最坏情况下需要考察顶点集合的所有全排列,时间复杂度为O(n!)。

2.5.2  TSP问题

 【问题】TSP问题(traveling salesman problem)是指旅行家要旅行 n 个城市然后回到出发城市,要求各个城市经历且仅经历一次,并要求所走的路程最短。

【想法】蛮力法求解 TSP问题的基本思想是,找出所有可能的旅行路线,即依次考察图中所有顶点的全排列,从中选取路径长度最短的哈密顿回路。

【算法分析】蛮力法求解 TSP问题必须依次考察顶点集合的所有全排列,从中找出路径长度最短的简单回路,因此,时间下界是 Ω(n!)。除了该问题的一些规模非常小的实例,蛮力法几乎是不实用的。随着城市数量的增长,TSP问题的可能解也在迅速地增长。例如:

10 城市的 TSP问题有大约 180 000 个可能解;

20 城市的 TSP问题有大约 60 000 000 000 000 000 个可能解;

50 城市的 TSP问题有大约 1062 个可能解。

第6章  动态规划法

6.1  概  述 

6.2  图问题中的动态规划法

6.3  组合问题中的动态规划法

6.4  查找问题中的动态规划法

海盗分钻石问题

五个海盗抢了一百颗钻石,每颗都价值连城。五个海盗都很贪婪,他们都希望自己能分得最多的钻石,但同时又都很明智。于是他们按照抽签的方法排出一个次序。首先由抽到一号签的海盗说出一套分钻石的方案,如果5个人中有50%以上的人同意,那么便依照这个方案执行,否则的话,这个提出方案的人将被扔到海里喂鱼,接下来再由抽到二号签的海盗继续说出一套方案,然后依次类推到第五个。记住,五个海盗都很聪明哦!

  1. 故答案是:(97、0、1、0、2)(97、0、1、2、0)。

6.1  概  述

6.1.1  最优化问题

6.1.2  最优性原理

6.1.3  动态规划法的设计思想

6.1.1  最优化问题

动态规划(dynamic programming)是 20 世纪 50 年代美国数学家贝尔曼(Richard Bellman)为研究最优控制问题而提出,在计算机科学领域,动态规划法成为一种通用的算法设计技术用来求解多阶段决策最优化问题。

最优化问题(optimization problem):有 n 个输入,问题的解由这 n 个输入的一个子集组成,这个子集必须满足某些事先给定的约束条件(constraint condition),满足约束条件的解称为问题的可行解(feasible solution)。为了衡量可行解的优劣,通常以函数的形式给出一定的评价标准,这些标准函数称为目标函数(objective function,也称评价函数),目标函数的极值(极大或极小)称为最优值(optimal value),使目标函数取得极值的可行解称为最优解(optimal solution)。

多阶段决策过程:一个决策序列在不断变化的状态中产生的过程。具有n个输入的最优化问题,其求解过程划分为若干个阶段,每一阶段的决策仅依赖于前一阶段的状态,由决策所采取的动作使状态发生转移,成为下一阶段决策的依据。

动态规划法将待求解问题分解成若干个相互重叠的子问题,每个子问题对应决策过程的一个阶段,动态规划法的求解过程由以下三个阶段组成:

(1)划分子问题:将原问题的求解过程划分为若干个阶段,每个阶段对应一个子问题,并且子问题之间具有重叠关系;

(2)动态规划函数:根据子问题之间的重叠关系找到子问题满足的递推关系式,这是动态规划法的关键;

(3)填写表格:根据动态规划函数设计表格,以自底向上的方式计算各个子问题的最优值并填表,实现动态规划过程。

动态规划过程只能求得问题的最优值,如果要得到使目标函数取得极值的最优解,通常在动态规划过程中记录每个阶段的决策,再根据最优决策序列通过回溯构造最优解。

付款问题:

9.1.3  一个简单的例子——网格上的最短路径

【问题】给定一个包含正整数的 m×n 网格,每次只能向下或者向右移动一步,定义路径长度是路径上经过的整数之和。请找出一条从左上角到右下角的路径,使得路径长度最小。

【算法实现】设二维数组a[m][n]存储 m×n 网格,dist[m][n]存储网格的最短路径长度,path[m][n]存储每个网格的决策,注意数组下标从 0 开始,程序如下:

int MinPath(int a[100][100], int m, int n)

{

    int dist[m][n], path[m][n], i, j;

    dist[0][0] = a[0][0]; path[0][0] = 0;

    for (j = 1; j < n; j++)                     //填写第0行

    {

        dist[0][j] = dist[0][j-1] + a[0][j]; path[0][j] = 1;

    }

    for (i = 1; i < m; i++)                     //填写第0列

    {

        dist[i][0] = dist[i-1][0] + a[i][0]; path[i][0] = 0;

    }

    for (i = 1; i < m; i++)                                //填写每一行

        for (j = 1; j < n; j++)

         if (dist[i-1][j] < dist[i][j-1]) { dist[i][j] = dist[i-1][j] + a[i][j]; path[i][j] = 0; }

             else { dist[i][j] = dist[i][j-1] + a[i][j]; path[i][j] = 1;  }

    for (i = m - 1, j = n - 1; i > 0 || j > 0; )        //回溯求最优解

    {

        cout<<a[i][j]<<"<--";

        if (path[i][j] == 0) i--;

        else j--;

    }

    cout<<a[0][0];

    return dist[m-1][n-1];                      //返回最优值

}

① 状态:表示每个阶段开始时,问题或系统所处的客观状况。状态既是该阶段的某个起点,又是前一个阶段的某个终点。通常一个阶段有若干个状态。

 状态无后效性:如果某个阶段状态给定后,则该阶段以后过程的发展不受该阶段以前各阶段状态的影响,也就是说状态具有马尔科夫性。

适于动态规划法求解的问题具有状态的无后效性

② 策略:各个阶段决策确定后,就组成了一个决策序列,该序列称之为一个策略。由某个阶段开始到终止阶段的过程称为子过程,其对应的某个策略称为子策略。

6.1.2  最优性原理

    对于一个具有n个输入的最优化问题,其求解过程往往可以划分为若干个阶段,每一阶段的决策仅依赖于前一阶段的状态,由决策所采取的动作使状态发生转移,成为下一阶段决策的依据。从而,一个决策序列在不断变化的状态中产生。这个决策序列产生的过程称为多阶段决策过程。

    在每一阶段的决策中有一个赖以决策的策略或目标,这种策略或目标是由问题的性质和特点所确定,通常以函数的形式表示并具有递推关系,称为动态规划函数。

   多阶段决策过程满足最优性原理(Optimal Principle):无论决策过程的初始状态和初始决策是什么,其余的决策都必须相对于初始决策所产生的当前状态,构成一个最优决策序列。

    如果一个问题满足最优性原理通常称此问题具有最优子结构性质

幻灯片17

原问题E的解依赖于子问题C和D的解,子问题D的解依赖于子问题C和B的解,子问题C的解依赖于子问题A和B的解,子问题B的解依赖于子问题A的解,因此,动态规划的求解过程从初始子问题A开始,逐步求解并记录各子问题的解,直至得到原问题E的解。

6.1.3  动态规划法的设计思想

    动态规划法将待求解问题分解成若干个相互重叠的子问题,每个子问题对应决策过程的一个阶段,一般来说,子问题的重叠关系表现在对给定问题求解的递推关系(也就是动态规划函数)中,将子问题的解求解一次并填入表中,当需要再次求解此子问题时,可以通过查表获得该子问题的解而不用再次求解,从而避免了大量重复计算。

例:计算斐波那契数

    用动态规划法求解的问题具有特征:

  1.  能够分解为相互重叠的若干子问题;
  1.  满足最优性原理(也称最优子结构性质):该问题的最优解中也包含着其子问题的最优解。

(用反证法)分析问题是否满足最优性原理:

  1. 先假设由问题的最优解导出的子问题的解不是最优的;
  1. 然后再证明在这个假设下可构造出比原问题最优解更好的解,从而导致矛盾。

幻灯片24

  1. 动态规划法设计算法一般分成三个阶段:
  1. (1)分段:将原问题分解为若干个相互重叠的子问题;
  1. (2)分析:分析问题是否满足最优性原理,找出动态规划函数的递推式;
  1. (3)求解:利用递推式自底向上计算,实现动态规划过程。 
  1.  动态规划法利用问题的最优性原理,以自底向上的方式从子问题的最优解逐步构造出整个问题的最优解。

一个简单的例子——数塔问题

问题描述:从数塔的顶层出发,在每一个结点可以选择向左走或向右走,一直走到最底层,要求找出一条路径,使得路径上的数值和最大。

数塔问题——想法

求解初始子问题:底层的每个数字可以看作1层数塔,则最大数值和就是其自身;

再求解下一阶段的子问题:第4层的决策是在底层决策的基础上进行求解,可以看作4个2层数塔,对每个数塔进行求解;

再求解下一阶段的子问题:第3层的决策是在第4层决策的基础上进行求解,可以看作3个2层的数塔,对每个数塔进行求解;

以此类推,直到最后一个阶段:第1层的决策结果就是数塔问题的整体最优解。

数塔问题——算法

1. 初始化数组maxAdd的最后一行为数塔的底层数据:

    for (j = 0; j < n; j++)

          maxAdd[n-1][j] = d[n-1][j];

2. 从第n-1层开始直到第 1 层对下三角元素maxAdd[i][j]执行下述操作:

    2.1 maxAdd[i][j] = d[i][j] + max{maxAdd[i+1][j], maxAdd[i+1][j+1]};

    2.2 如果选择下标j的元素,则path[i][j] = j,

          否则path[i][j] = j+1;

3. 输出最大数值和maxAdd[0][0];

4. 根据path数组确定每一层决策的列下标,输出路径信息;

【算法实现】设数组d[n][n]存储数塔问题,数组maxAdd[n][n]存储每一步决策得到的最大数值和,数组path[n][n]存储每一步的决策,程序如下:

int DataTorwer(int d[100][100], int n)

{

    int i, j, maxAdd[n][n] = {0}, path[n][n] = {0};      

    for (j = 0; j < n; j++)                           //初始子问题

        maxAdd[n-1][j] = d[n-1][j];

    for (i = n-2; i >= 0; i--)                       //进行第i层的决策

        for (j = 0; j <= i; j++)                      //只填写下三角

            if (maxAdd[i + 1][j]>maxAdd[i + 1][j + 1])

            {

                  maxAdd[i][j] = d[i][j] + maxAdd[i + 1][j];

                  path[i][j] = j;                        //本次决策选择下标j的元素

             }

             else

             {

                  maxAdd[i][j] = d[i][j] + maxAdd[i + 1][j + 1];

                  path[i][j] = j + 1;                     //本次决策选择下标j+1的元素

              }

    printf("路径为:%d", d[0][0]);            //输出顶层数字

    j = path[0][0];                                       //计算maxAdd[0][0]的选择

    for (i = 1; i < n; i++)

    {

        printf("-->%d", d[i][j]);   

        j = path[i][j];                                    //计算maxAdd[i][j]的选择

    }

    return maxAdd[0][0];                          //返回最大数值和

}

6.2  图问题中的动态规划法

6.2.1  多段图的最短路径问题

6.2.2  TSP问题

6.2.1 多段图的最短路径问题

问题描述:设图G=(V, E)是一个带权有向连通图,如果把顶点集合V划分成k个互不相交的子集Vi(2≤k≤n, 1≤i≤k),使得E中的任何一条边(u, v),必有u∈Vi,v∈Vi+m(1≤i<k, 1<i+m≤k),则称图G为多段图,称s∈V1为源点,t∈Vk为终点。多段图的最短路径问题是求从源点到终点的最小代价路径。

幻灯片32

证明多段图问题满足最优性原理

   设s, s1, s2, …, sp, t是从s到t的一条最短路径,从源点s开始,设从s到下一段的顶点s1已经求出,则问题转化为求从s1到t的最短路径,显然s1, s2, …, sp, t一定构成一条从s1到t的最短路径,如若不然,设s1, r1, r2, …, rq, t是一条从s1到t的最短路径,则s, s1, r1, r2, …, rq, t将是一条从s到t的路径且比s, s1, s2, …, sp, t的路径长度要短,从而导致矛盾。所以,多段图的最短路径问题满足最优性原理。

幻灯片33

设cuv表示多段图的有向边<u, v>上的权值,将从源点s到终点t的最短路径长度记为d(s, t),考虑原问题的部分解d(s, v),显然有下式成立:

d(s, v) =csv               (<s, v>∈E)

d(s, v) = min{d(s, u) + cuv}  (<u, v>∈E)

多段图最短路径实例求解过程

首先求解初始子问题,可直接获得:

d(0, 1)=c01=4(0→1)

d(0, 2)=c02=2(0→2)

d(0, 3)=c03=3(0→3)

再求解下一个阶段的子问题,有:

d(0, 4)=min{d(0, 1)+c14, d(0, 2)+c24}=min{4+9, 2+6}=8(2→4)

d(0, 5)=min{d(0, 1)+c15, d(0, 2)+c25, d(0, 3)+c35}=min{4+8, 2+7, 3+4} 

           =7(3→5)

d(0, 6)=min{d(0, 2)+c26, d(0, 3)+c36}=min{2+8, 3+7}=10(2→6)

再求解下一个阶段的子问题,有:

d(0, 7)=min{d(0, 4)+c47, d(0, 5)+c57, d(0, 6)+c67}=min{8+5, 7+8, 10+6}

           =13(4→7)

d(0, 8)=min{d(0, 4)+c48, d(0, 5)+c58, d(0, 6)+c68}=min{8+6, 7+6, 10+5}

           =13(5→8)

直到最后一个阶段,有:

d(0, 9)=min{d(0, 7)+c79, d(0, 8)+c89}=min{13+7, 13+3}=16(8→9)

再将状态进行回溯,得到最短路径0→3→5→8→9,最短路径长度16。

多段图最短路径问题的填表过程

【算法实现】多段图采用代价矩阵arc[n][n]存储,数组cost[n]存储最短路径长度,cost[j]表示从源点 s 到顶点 j 的最短路径长度,数组path[n]记录状态转移,path[j]表示从源点 s 到顶点 j 的路径上顶点 j 的前一个顶点,程序如下:

int ShortestPath(int arc[100][100], int n)

{

    int i, j, cost[n], path[n];

    cost[0] = 0; path[0] = -1;         //顶点0为源点

    for (j = 1; j < n; j++)               //执行填表工作

    {

        cost[j] = 1000;                                //假定权值最大不超过1000

        for (i = 0; i < j; i++)                        //考察所有入边

         if (cost[i] + arc[i][j] < cost[j])  {

                 cost[j] = cost[i] + arc[i][j];   path[j] = i;

         }

    }

    cout<<--n;                                      //输出终点

    for (i = n; path[i] >= 0; )                 //依次输出path[i]

    {

        cout<<"<-"<<path[i];

        i = path[i];                      //求得路径上顶点i的前一个顶点

    }

    return cost[n-1];                   //返回最短路径长度

}

【算法分析】第一个循环依次计算从源点到各个顶点的最短路径长度,执行次数为O(n2)。第二个循环时间性能是O(k)。所以,算法的时间复杂度为O(n2)。

算法6.2:多段图的最短路径问题:

输入:多段图的代价矩阵

输出:最短路径长度及路径

1. 循环变量j从1~n-1重复下述操作,执行填表工作:

    1.1 考察顶点j的所有入边,对于边(i, j)∈E:

        1.1.1  cost[j]=min{cost[i]+cij};

        1.1.2  path[j]=使cost[i]+cij最小的i;

    1.2  j++;

2. 输出最短路径长度cost[n-1];

3. 循环变量i=path[n-1],循环直到path[i]=0:

    3.1 输出path[i];

    3.2  i=path[i];

另一种方法:

对多段图的边(u, v),用cuv表示边上的权值,将从源点s到终点t的最短路径记为d(s, t),则从源点0到终点9的最短路径d(0, 9)由下式确定:

d(0, 9)=min{c01+d(1, 9), c02+d(2, 9), c03+d(3, 9)}

         这是最后一个阶段的决策,它依赖于d(1, 9)、d(2, 9)和d(3, 9)的计算结果,而

d(1, 9)=min{c14+d(4, 9), c15+d(5, 9)}

d(2, 9)=min{c24+d(4, 9), c25+d(5, 9), c26+d(6, 9)}

d(3, 9)=min{c35+d(5, 9), c36+d(6, 9)}

        这一阶段的决策又依赖于d(4, 9)、d(5, 9)和d(6, 9)的计算结果:

d(4, 9)=min{c47+d(7, 9), c48+d(8, 9)}

d(5, 9)=min{c57+d(7, 9), c58+d(8, 9)}

d(6, 9)=min{c67+d(7, 9), c68+d(8, 9)}

这一阶段的决策依赖于d(7, 9)和d(8, 9)的计算,而d(7, 9)和d(8, 9)可以直接获得(括号中给出了决策产生的状态转移):

d(7, 9)=c79=7(7→9)

d(8, 9)=c89=3(8→9)

         再向前推导,有:

d(6, 9)=min{c67+d(7, 9), c68+d(8, 9)}=min{6+7, 5+3}=8(6→8)

d(5, 9)=min{c57+d(7, 9), c58+d(8, 9)}=min{8+7, 6+3}=9(5→8)

d(4, 9)=min{c47+d(7, 9), c48+d(8, 9)}=min{5+7, 6+3}=9(4→8)

幻灯片40

d(3, 9)=min{c35+d(5, 9), c36+d(6, 9)}=min{4+9, 7+8}=13(3→5)

d(2, 9)=min{c24+d(4, 9), c25+d(5, 9), c26+d(6, 9)}=min{6+9, 7+9, 8+8}=15(2→4)

d(1, 9)=min{c14+d(4, 9), c15+d(5, 9)}=min{9+9, 8+9}=17(1→5)

d(0, 9)=min{c01+d(1, 9), c02+d(2, 9), c03+d(3, 9)}=min{4+17, 2+15, 3+13}=16(0→3)

    最后,得到最短路径为0→3→5→8→9,长度为16。

幻灯片41

       下面考虑多段图的最短路径问题的填表形式。

    用一个数组cost[n]作为存储子问题解的表格,cost[i]表示从顶点i到终点n-1的最短路径,数组path[n]存储状态,path[i]表示从顶点i到终点n-1的路径上顶点i的下一个顶点。则:

cost[i]=min{cij+cost[j]} (i≤j≤n且顶点j是顶点i的邻接点)   (式6.7)

path[i]=使cij+cost[j]最小的j                                (式6.8)

幻灯片42

算法6.2——多段图的最短路径

    1.初始化:数组cost[n]初始化为最大值,数组path[n]初始化为-1;

    2.for (i=n-2; i>=0; i--)

        2.1 对顶点i的每一个邻接点j,根据式6.7计算cost[i];

        2.2 根据式6.8计算path[i];

    3.输出最短路径长度cost[0];

    4. 输出最短路径经过的顶点:

        4.1  i=0

        4.2 循环直到path[i]=n-1

            4.2.1 输出path[i];

            4.2.2  i=path[i];

    算法6.2主要由三部分组成:第一部分是初始化部分,其时间性能为O(n);第二部分是依次计算各个顶点到终点的最短路径,由两层嵌套的循环组成,外层循环执行n-1次,内层循环对所有出边进行计算,并且在所有循环中,每条出边只计算一次。假定图的边数为m,则这部分的时间性能是O(m);第三部分是输出最短路径经过的顶点,其时间性能是O(n)。所以,算法6.2的时间复杂性为O(n+m)。

幻灯片43

6.2.2  TSP问题

【问题】TSP问题(traveling salesman problem)是指旅行家要旅行 n 个城市,要求各个城市经历且仅经历一次然后回到出发城市,并要求所走的路程最短。

    各个城市间的距离可以用代价矩阵来表示。

证明TSP问题满足最优性原理

    设s, s1, s2, …, sp, s是从s出发的一条路径长度最短的简单回路,假设从s到下一个城市s1已经求出,则问题转化为求从s1到s的最短路径,显然s1, s2, …, sp, s一定构成一条从s1到s的最短路径。

    如若不然,设s1, r1, r2, …, rq, s是一条从s1到s的最短路径且经过n-1个不同城市,则s, s1, r1, r2, …, rq, s将是一条从s出发的路径长度最短的简单回路且比s, s1, s2, …, sp, s要短,从而导致矛盾。所以,TSP问题满足最优性原理。

【想法】假设从顶点 i 出发,令 V'=V-i,设d(i, V')表示从顶点 i 出发经过 V' 中各个顶点一次且仅一次,最后回到出发点 i 的最短路径长度。设 cuv 表示边<u, v>上的权值,考虑初始子问题,回到出发点 i 之前只经过一个顶点,设d(k, { })表示从顶点 k 回到顶点 i,显然有:

考虑重叠子问题,设d(k, V'-{k})表示从顶点 k 出发经过 V'-{k}中各个顶点一次且仅一次,最后回到出发点i的最短路径长度,则:

TSP问题 ——实例

TSP问题 ——实例(填表过程)

假设n个顶点用0~n-1的数字编号,顶点之间的代价存放在数组arc[n][n]中, 从0出发,首先生成1~n-1个元素的子集存放在数组V[2n-1]中,例如n=4时,

V[1]={1}, V[2]={2}, V[3]={3}, V[4]={1,2},…, V[7]={1,2,3},

设数组d[n][2n-1]存放迭代结果,其中d[i][j]表示从顶点i经过子集V[j]中的顶点一次且仅一次,最后回到出发点0的最短路径长度。

TSP问题 ——算法

另一种分析:

动态规划法求解TSP问题的填表过程

    设顶点之间的代价存放在数组c[n][n]中,动态规划法求解TSP问题的算法如下:

    显然,算法6.1的时间复杂性为O(2n)。和蛮力法相比,动态规划法求解TSP问题,把原来的时间复杂性是O(n!)的排列问题,转化为组合问题,从而降低了算法的时间复杂性,但它仍需要指数时间。

        

7.1.1  贪心法的设计思想

        贪心法在解决问题的策略上目光短浅,只根据当前已有的信息就做出选择,而且一旦做出了选择,不管将来有什么结果,这个选择都不会改变。换言之,贪心法并不是从整体最优考虑,它所做出的选择只是在某种意义上的局部最优。

        这种局部最优选择并不总能获得整体最优解(Optimal Solution),但通常能获得近似最优解(Near-Optimal Solution)。

幻灯片7

例:用贪心法求解————付款问题。

【问题】假设有面值为 5元、2元、1元、5 角、2 角、1 角的货币,需要找给顾客4 元 6 角现金,付款问题(payment problem)要求找到一个付款方案,使得付出的货币张数最少。

【想法】付款问题的贪心选择策略是,在不超过应付款金额的条件下,选择面值最大的货币。

4 元 6 角 --> 2 元 --> 2 元 --> 5 角 --> 1 角:最优解

一定是最优解吗?假设面值为 3 元、1 元、8 角、5 角、1 角,情况如何

4 元 6 角 --> 3 元 --> 1 元 --> 5 角 --> 1 角:近似最优解

4 元 6 角 --> 3 元 --> 8 角 --> 8 角:最优解

【算法实现】设数组money[6]存储货币面值,为避免进行实数运算,将货币的面值扩大 10 倍,然后按面值从大到小依次试探。程序如下:

int PayMoney(double sum)

{

    int money[6] = {50, 20, 10, 5, 2, 1}, i, count = 0, n = sum * 10;

    while (n > 0)

    {

        for (i = 0; i < 6; i++)                     /*选取不超过sum的最大面值依次试探*/

        {

            if (n >= money[i])  {

             count++;

             n = n - money[i];       

             break;

            }

        }

    }

    return count;

}

幻灯片8

        在付款问题每一步的贪心选择中,在不超过应付款金额的条件下,只选择面值最大的货币,而不去考虑在后面看来这种选择是否合理,而且它还不会改变决定:一旦选出了一张货币,就永远选定。付款问题的贪心选择策略是尽可能使付出的货币最快地满足支付要求,其目的是使付出的货币张数最慢地增加,这正体现了贪心法的设计思想。

幻灯片9

贪心法求解的问题的特征:

(1)最优子结构性质

        当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质,也称此问题满足最优性原理。

(2)贪心选择性质

        所谓贪心选择性质是指问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来得到。

  1.  动态规划法通常以自底向上的方式求解各个子问题,而贪心法则通常以自顶向下的方式做出一系列的贪心选择。

幻灯片10

7.1.2  贪心法的求解过程

     用贪心法求解问题应该考虑如下几个方面:

(1)候选集合C:为了构造问题的解决方案,有一个候选集合C作为问题的可能解,即问题的最终解均取自于候选集合C。例如,在付款问题中,各种面值的货币构成候选集合。

(2)解集合S:随着贪心选择的进行,解集合S不断扩展,直到构成一个满足问题的完整解。例如,在付款问题中,已付出的货币构成解集合。

  (3)解决函数solution:检查解集合S是否构成问题的完整解。例如,在付款问题中,解决函数是已付出的货币金额恰好等于应付款。

   (4)选择函数select:即贪心策略,这是贪心法的关键,它指出哪个候选对象最有希望构成问题的解,选择函数通常和目标函数有关。例如,在付款问题中,贪心策略就是在候选集合中选择面值最大的货币。

  (5)可行函数feasible:检查解集合中加入一个候选对象是否可行,即解集合扩展后是否满足约束条件。例如,在付款问题中,可行函数是每一步选择的货币和已付出的货币相加不超过应付款。       

幻灯片12

贪心法的一般过程

Greedy(C)  //C是问题的输入集合即候选集合

{

    S={ };  //初始解集合为空集

    while (not solution(S))  //集合S没有构成问题的一个解

    {

       x=select(C);    //在候选集合C中做贪心选择

       if feasible(S, x)  //判断集合S中加入x后的解是否可行

          S=S+{x};

          C=C-{x};

    }

    return S;

}

幻灯片13

7.2  图问题中的贪心法

7.2.1  TSP问题

7.2.2  图着色问题

7.2.3  最小生成树问题

幻灯片14

7.2.1  TSP问题

【问题】TSP问题(traveling salesman problem)是指旅行家要旅行 n 个城市,要求各个城市经历且仅经历一次然后回到出发城市,并要求所走的路程最短。

【想法】TSP问题的贪心策略可以采用最近邻点策略:从任意城市出发,每次在没有到过的城市中选择最近的一个,直至经过了所有城市,最后回到出发城市。

求解TSP问题至少有两种贪心策略是合理的:

(1)最近邻点策略:从任意城市出发,每次在没有到过的城市中选择最近的一个,直到经过了所有的城市,最后回到出发城市。   

幻灯片15

幻灯片16

    设图G有n个顶点,边上的代价存储在二维数组w[n][n]中,集合V存储图的顶点,集合P存储经过的边,最近邻点策略求解TSP问题的算法如下:

算法7.1——最近邻点策略求解TSP问题

   1. P={ };    TSPLength=0;

   2. V=V-{w}; u=w;   //从顶点w出发

   3. 循环直到集合P中包含n-1条边

          3.1  查找与顶点u邻接的最小代价边(u, v)并且v属于集合V;

          3.2  P=P+{(u, v)};   V=V-{v}; TSPLength=TSPLength+Cuv ;

          3.3  u=v;   //从顶点v出发继续求解

   4.    输出TSPLength+Cuw.

伪代码

幻灯片17

        算法7.1的时间性能为O(n2),因为共进行n-1次贪心选择,每一次选择都需要查找满足贪心条件的最短边。

用最近邻点贪心策略求解TSP问题所得的结果不一定是最优解,图7.1(a)中从城市1出发的最优解是1→2→5→4→3→1,总代价只有13。当图中顶点个数较多并且各边的代价值分布比较均匀时,最近邻点策略可以给出较好的近似解,不过,这个近似解以何种程度近似于最优解,却难以保证。例如,在图7.1中,如果增大边(2, 1)的代价,则总代价只好随之增加,没有选择的余地。

【算法实现】数组arc[n][n]存储图中各边的代价,变量TSPLength存储回路长度,edgeCount存储集合 P 中边的个数,flag[n]表示某顶点是否在路径中,程序如下:

int TSP(int arc[100][100], int n, int w)  

{

    int edgeCount = 0, TSPLength = 0, min, u, v, j, flag[n] = {0}; 

    u = w; flag[w] = 1;

    while (edgeCount < n-1)                //循环直到边数等于n-1

    {

        min = 100;

        for (j = 0; j < n; j++)                  //求arc[u]中的最小值

            if ((flag[j] == 0) && (arc[u][j] != 0) && (arc[u][j] < min))

            {

                 v = j; min = arc[u][j]; 

            }

        TSPLength += min;

        flag[v] = 1; edgeCount++;             //将顶点加入路径

        cout<<u<<"-->"<<v<<endl;           //输出经过的路径

        u = v;                                               //下一次从顶点v出发

    }

    cout<<v<<"-->"<<w<<endl;            //输出最后的回边

    return TSPLength + arc[u][w];          //返回回路长度

}

幻灯片18

(2)最短链接策略:每次在整个图的范围内选择最短边加入到解集合中,但是,要保证加入解集合中的边最终形成一个哈密顿回路。因此,当从剩余边集E'中选择一条边(u, v)加入解集合S中,应满足以下条件:

① 边(u, v)是边集E'中代价最小的边;

② 边(u, v)加入解集合S后,S中不产生回路;

③ 边(u, v) 加入解集合S后,S中不产生分枝;

幻灯片19

幻灯片20

    设图G有n个顶点,边上的代价存储在二维数组w[n][n]中,集合E'是候选集合即存储所有未选取的边,集合P存储经过的边,最短链接策略求解TSP问题的算法如下:

算法7.2——最短链接策略求解TSP问题

   1P={ };    

    2E'=E;     //候选集合,初始时为图中所有边

    3.循环直到集合P中包含n-1条边

          3.1 E'中选取最短边(u, v);

          3.2  E'=E'-{(u, v)};

          3.3 如果 (顶点uvP中不连通 and 不产生分枝)

                P=P+{(u, v)};

伪代码

幻灯片21

    在算法7.2中,如果操作“在E'中选取最短边(u, v)”用顺序查找,则算法7.2的时间性能是O(n2),如果采用堆排序的方法将集合E'中的边建立堆,则选取最短边的操作可以是O(log2n),对于两个顶点是否连通以及是否会产生分枝,可以用并查集的操作将其时间性能提高到O(n),此时算法7.2的时间性能为O(nlog2n)。

幻灯片22

7.2.2  图着色问题

【问题】图着色问题(graph coloring problem)求无向连通图G=(V, E)的最小色数 k,使得用 k 种颜色对 G 中的顶点着色,可使任意两个相邻顶点着不同颜色。

【想法】假定 k 个颜色的集合为{1, 2, …, k}。一种显然的贪心策略是选择一种颜色,用该颜色为尽可能多的顶点着色。

幻灯片23

例如,图7.3(a)所示的图可以只用两种颜色着色,将顶点1、3和4着成一种颜色,将顶点2和顶点5着成另外一种颜色。为简单起见,下面假定k个颜色的集合为{颜色1, 颜色2, …, 颜色k}。

幻灯片24

贪心策略:选择一种颜色,以任意顶点作为开始顶点,依次考察图中的未被着色的每个顶点,如果一个顶点可以用颜色1着色,换言之,该顶点的邻接点都还未被着色,则用颜色1为该顶点着色,当没有顶点能以这种颜色着色时,选择颜色2和一个未被着色的顶点作为开始顶点,用第二种颜色为尽可能多的顶点着色,如果还有未着色的顶点,则选取颜色3并为尽可能多的顶点着色,依此类推。 

幻灯片25

    设数组color[n]表示顶点的着色情况,贪心法求解图着色问题的算法如下:

算法7.3——图着色问题

    1.color[1]=1;  //顶点1着颜色1

    2.for (i=2; i<=n; i++)  //其他所有顶点置未着色状态

              color[i]=0;

    3.k=0;   

    4.循环直到所有顶点均着色

          4.1  k++;  //取下一个颜色

          4.2  for (i=2; i<=n; i++)   //用颜色k为尽量多的顶点着色

                  4.2.1  若顶点i已着色,则转步骤4.2,考虑下一个顶点;

                  4.2.2  若图中与顶点i邻接的顶点着色与顶点i着颜色k不冲突,

                            则color[i]=k;

    5.输出k;

伪代码

【算法分析】算法ColorGraph需要试探 k 种颜色,每种颜色需要对所有顶点进行冲突测试,设无向图有 n 个顶点,则算法的时间复杂度是 O(k×n)。

【算法实现】设数组 arc[n][n] 存储图中各边的代价,数组 color[n] 表示各顶点的着色情况,变量 flag 表示图中是否有尚未涂色的顶点,程序如下:

int ColorGraph(int arc[100][100] , int n, int color[ ])

{

    int i, j, k = 0, flag = 1;                

    while (flag == 1)

    {

        k++; flag = 0;                      //取下一种颜色

        for (i = 0; i < n; i++)

        {

            if (color[i] != 0) continue;           //顶点i已着色

            color[i] = k;                                //顶点i着颜色k

            for (j = 0; j < n; j++)              

                if (arc[i][j] == 1 && color[i] == color[j]) break;

            if (j < n) {  color[i] = 0; flag = 1; }            //发生冲突,取消涂色

        }

    }

    return k;

}

幻灯片26

   考虑一个具有2n个顶点的无向图,顶点的编号从1到2n,当i是奇数时,顶点i与除了顶点i+1之外的其他所有编号为偶数的顶点邻接,当i是偶数时,顶点i与除了顶点i-1之外的其他所有编号为奇数的顶点邻接,这样的图称为双向图(Bipartite)。

幻灯片27

7.2.3  最小生成树问题

    设G=(V,E)是一个无向连通网,生成树上各边的权值之和称为该生成树的代价,在G的所有生成树中,代价最小的生成树称为最小生成树(Minimal Spanning Trees)。

幻灯片28

最小生成树问题至少有两种合理的贪心策略:

(1)最近顶点策略:任选一个顶点,并以此建立起生成树,每一步的贪心选择是简单地把不在生成树中的最近顶点添加到生成树中。

      Prim算法就应用了这个贪心策略,它使生成树以一种自然的方式生长,即从任意顶点开始,每一步为这棵树添加一个分枝,直到生成树中包含全部顶点。   

算法:Prim

输入:无向连通网G=(V,E)

输出:最小生成树T=(U,TE)

 1. 初始化:U = {v}; TE={ };

 2. 重复下述操作直到U = V:

           2.1 在E中寻找最短边(i,j),且满足i∈U,j∈V-U;

           2.2 U = U + {j};

           2.3 TE = TE + {(i,j)};

【算法】设图 G 采用代价矩阵存储,起始顶点是 w,Prim算法如下:

算法:Prim算法

输入:无向连通网 G=(V,E),起始点 w

输出:最小生成树

        1. 初始化数组adjvex[n]和lowcost[n];

        2. U = {w}; 输出顶点 w;   

        3. 重复执行下列操作 n-1 次

            3.1 在lowcost中选取最短边,取adjvex中对应的顶点序号 k;

            3.2 输出顶点 k 和对应的权值;

            3.3 U = U + {k};

            3.4 调整数组adjvex[n]和lowcost[n];

【算法分析】步骤 1 的时间开销是O(n),步骤 3 循环 n-1 次,内嵌两个循环,时间开销是O(n),因此,Prim算法的时间复杂度为O(n2)。

【算法实现】设数组arc[n][n]存储图中各边的代价,函数Prim实现从顶点 w 开始构造最小生成树并返回生成树的代价,程序如下:

int Prim(int arc[100][100], int n, int w) 

{  

    int i, j, k, min, minDist = 0, lowcost[n], adjvex[n];    

    for (i = 0; i < n; i++)                               //初始化

    {

        lowcost[i] = arc[w][i]; adjvex[i] = w;

    }

    lowcost[w] = 0;                                //将顶点w加入集合U

    for (i = 0; i < n - 1; i++)      

    {

        min = 100;                                  //假设权值均小于100

        for (j = 0; j < n; j++)                           //寻找最短边的邻接点k

        {

            if ((lowcost[j] != 0) && (lowcost[j] < min)) { min = lowcost[j]; k = j; }

        }

        cout<<adjvex[k]<<"--"<<k<<endl;                //输出最小生成树的边

        minDist = minDist + lowcost[k];

        lowcost[k] = 0;                                //将顶点k加入集合U中

        for (j = 0; j < n; j++)                            //调整数组

        {

            if (arc[k][j] < lowcost[j])  { lowcost[j] = arc[k][j]; adjvex[j] = k;  }

        }

    }

    return minDist;

}

幻灯片29(题上有)

幻灯片30

   设图G中顶点的编号为0~n-1,Prim算法如下:

幻灯片31

(2)最短边策略:设G=(V,E)是一个无向连通网,令T=(U,TE)是G的最小生成树。最短边策略从TE={}开始,每一次贪心选择都是在边集E中选取最短边(u, v),如果边(u, v)加入集合TE中不产生回路,则将边(u, v)加入边集TE中,并将它在集合E中删去。

        Kruskal算法就应用了这个贪心策略,它使生成树以一种随意的方式生长,先让森林中的树木随意生长,每生长一次就将两棵树合并,到最后合并成一棵树。

幻灯片32(题上有)

幻灯片33

幻灯片34

7.3  组合问题中的贪心法

7.3.1  背包问题

7.3.2  活动安排问题

7.3.3  多机调度问题

幻灯片35

7.3.1  背包问题

        给定n种物品和一个容量为C的背包,物品i的重量是wi,其价值为vi,背包问题是如何选择装入背包的物品,使得装入背包中物品的总价值最大?

幻灯片36

设xi表示物品i装入背包的情况,根据问题的要求,有如下约束条件和目标函数:

        于是,背包问题归结为寻找一个满足约束条件式7.1,并使目标函数式7.2达到最大的解向量X=(x1, x2, …, xn)。

幻灯片37

至少有三种看似合理的贪心策略:

   (1)选择价值最大的物品,因为这可以尽可能快地增加背包的总价值。但是,虽然每一步选择获得了背包价值的极大增长,但背包容量却可能消耗得太快,使得装入背包的物品个数减少,从而不能保证目标函数达到最大。

   (2)选择重量最轻的物品,因为这可以装入尽可能多的物品,从而增加背包的总价值。但是,虽然每一步选择使背包的容量消耗得慢了,但背包的价值却没能保证迅速增长,从而不能保证目标函数达到最大。

  (3)选择单位重量价值最大的物品,在背包价值增长和背包容量消耗两者之间寻找平衡。

幻灯片38

        应用第三种贪心策略,每次从物品集合中选择单位重量价值最大的物品,如果其重量小于背包容量,就可以把它装入,并将背包容量减去该物品的重量,然后我们就面临了一个最优子问题——它同样是背包问题,只不过背包容量减少了,物品集合减少了。因此背包问题具有最优子结构性质。

幻灯片39

幻灯片40

【算法实现】设 n 个物品的重量存放在数组w[n]中,价值存放在数组v[n]中,问题的解存放在数组x[n],简单起见,假设物品已按单位重量降序排列,程序如下:

double KnapSack(int w[ ], int v[ ], int n, int C)

{

    double x[n] = {0}, maxValue = 0;         //物品可部分装入

    int i;

    for (i = 0; w[i] < C; i++)

    {

        x[i] = 1;                                   //将物品i装入背包

        maxValue += v[i];

        C = C - w[i];                                //背包剩余容量

    }

    x[i] = (double)C/w[i];                           //物品i装入一部分

    maxValue += x[i] * v[i];

    return maxValue;                               //返回背包获得的价值

}

幻灯片41

7.3.2  活动安排问题

【问题】设有 n 个活动的集合 E={1, 2, …, n},其中每个活动都要求使用同一资源,而在同一时间只有一个活动能使用这个资源。每个活动 i(1 ≤ i ≤ n)都有一个要求使用该资源的起始时间 si 和一个结束时间 fi,且 si < fi 。如果选择了活动 i,则它在半开时间区间[si, fi )内占用资源。若区间[si, fi )与区间[sj, fj )不相交,则称活动 i 与活动 j 是相容的。活动安排问题(activity arrangement problem)要求在所给的活动集合中选出个数最多的相容活动。       

幻灯片42

       贪心法求解活动安排问题的关键是如何选择贪心策略,使得按照一定的顺序选择相容活动,并能安排尽量多的活动。至少有两种看似合理的贪心策略:

  (1)最早开始时间:这样可以增大资源的利用率。

  (2)最早结束时间:这样可以使下一个活动尽早开始。

       由于活动占用资源的时间没有限制,因此,后一种贪心选择更为合理。

    为了在每一次贪心选择时快速查找具有最早结束时间的相容活动,先把n个活动按结束时间非减序排列。这样,贪心选择时取当前活动集合中结束时间最早的活动就归结为取当前活动集合中排在最前面的活动。

   

幻灯片43

例如,设有11个活动等待安排,这些活动按结束时间的非减序排列如下:

幻灯片44

幻灯片45

    【算法】设有 n 个活动等待安排,si 表示活动 i 的起始时间,fi 表示活动 i 的结束时间(1 ≤ i ≤ n),集合 B 存放选定的活动,算法如下:

算法:活动安排问题ActiveManage

输入:n 个活动的开始时间{s1, s2, …, sn}和结束时间{f1, f2, …, fn }

输出:选定的活动集合B

       1. 对{f1, f2, …, fn}按非减序排序,同时相应地调整{s1, s2, …, sn};

       2. 最优解中包含活动 1:B={1};j = 1;

       3. 循环变量 i 从 2~n 依次考察每一个活动:

             3.1 如果(si >= fj ) ,则B = B + {j};j=i;

             3.2  i++;

【算法分析】步骤 1 将活动按结束时间从小到大排序,时间代价是O(nlog2n),步骤 3 依次考察每一个活动,时间代价是O(n),因此,时间复杂度为O(nlog2n)。

【算法实现】简单起见,假设数组 s[n] 和 f[n] 已按结束时间非降序排列,数组 B[n] 存储安排的活动,若活动 i 可以安排,则 B[i] = 1,程序如下。

int ActiveManage(int s[ ], int f[ ], int B[ ], int n)

    int i, j, count;

    B[0] = 1; j = 0; count = 1;               // j 表示目前安排的最后一个活动

    for (i = 1; i < n; i++)   

    {

        if (s[i] >= f[j])                             //活动i与活动j相容

        { 

            B[i] = 1; count++;   j = i;                 //安排活动i

        }

        else B[i] = 0;

     }

     return count;                          //返回已安排的活动个数

幻灯片47

7.3.3  多机调度问题

        设有n个独立的作业{1, 2, …, n},由m台相同的机器{M1, M2, …, Mm}进行加工处理,作业i所需的处理时间为ti(1≤i≤n),每个作业均可在任何一台机器上加工处理,但不可间断、拆分。多机调度问题要求给出一种作业调度方案,使所给的n个作业在尽可能短的时间内由m台机器加工处理完成。

幻灯片48

贪心法求解多机调度问题的贪心策略是最长处理时间作业优先,即把处理时间最长的作业分配给最先空闲的机器,这样可以保证处理时间长的作业优先处理,从而在整体上获得尽可能短的处理时间。按照最长处理时间作业优先的贪心策略,当m≥n时,只要将机器i的[0, ti)时间区间分配给作业i即可;当m<n时,首先将n个作业依其所需的处理时间从大到小排序,然后依此顺序将作业分配给空闲的处理机。

幻灯片49

幻灯片50

算法7.9——多机调度问题

1.将数组t[n]由大到小排序,对应的作业序号存储在数组p[n]中;

2.将数组d[m]初始化为0;

3.for (i=1; i<=m; i++)

     3.1 S[i]={p[i]};   //将m个作业分配给m个机器

     3.2 d[i]=t[i];  

4.  for (i=m+1; i<=n; i++)

     4.1  j=数组d[m]中最小值对应的下标;  //j为最先空闲的机器序号

     4.2  S[j]=S[j]+{p[i]};   //将作业i分配给最先空闲的机器j

     4.3  d[j]=d[j]+t[i];      //机器j将在d[j]后空闲

【代码实现】

#include<stdio.h>

#define MAXLENGTH 10

int d[MAXLENGTH];

int S[MAXLENGTH][MAXLENGTH];

struct work{

         int hour;

         int number;

};

work t[MAXLENGTH];

void bubble_sort(work a[], int n){

         int i, j;

         work temp;

         for (j = 0; j < n - 1; j++){

                   for ( i = 0; i < n-1-j; i++)

                   {

                            if (a[i].hour<a[i+1].hour)

                            {

                                     temp = a[i];

                                     a[i] = a[i + 1];

                                     a[i + 1] = temp;

                            }

                   }

         }

}

void MultiMachine(work t[], int n, int d[], int m);

int main(){

         int n;

         int m;

         int i;

         printf("请输入待处理的作业个数:");

         scanf("%d", &n);

         printf("请输入作业需要处理的时间:");

         for ( i = 0; i <n; i++)

         {

                   scanf("%d", &t[i].hour);

                   t[i].number = i + 1;

         }

         bubble_sort(t, n);

         printf("请输入机器的个数:");

         scanf("%d", &m);

         for ( i = 0; i < m; i++)

         {

                   d[i] = 0;

         }

         MultiMachine(t, n, d, m);

         getchar();

         getchar();

         getchar();

}

void MultiMachine(work t[], int n, int d[], int m){

         int rear[MAXLENGTH];

         int i, j, k;

         for ( i = 0; i < m; i++)

         {

                   S[i][0] = t[i].number;

                   rear[i] = 0;

                   d[i] = t[i].hour;

         }

         for ( i = m; i < n; i++)

         {

                   for (j = 0, k = 1; k < m; k++){

                            if (d[k]<d[j])

                            {

                                     j = k;

                            }

                   }

                   rear[j]++;

                   S[j][rear[j]] = t[i].number;

                   d[j] += t[i].hour;

         }

         for ( i = 0; i < m; i++)

         {

                   printf("机器%d处理:", i + 1);

                   for ( j = 0; S[i][j]>0; j++)

                   {

                            printf("作业%d\t", S[i][j]);

                   }

                   printf("处理时间:%d\n",d[i]);

                   printf("\n");

         }

}

8.3.2  活动安排问题

【算法实现】简单起见,假设数组 s[n] 和 f[n] 已按结束时间非降序排列,数组 B[n] 存储安排的活动,若活动 i 可以安排,则 B[i] = 1,程序如下。

int ActiveManage(int s[ ], int f[ ], int B[ ], int n)

    int i, j, count;

    B[0] = 1; j = 0; count = 1;               // j 表示目前安排的最后一个活动

    for (i = 1; i < n; i++)   

    {

        if (s[i] >= f[j])                             //活动i与活动j相容

        { 

            B[i] = 1; count++;   j = i;                 //安排活动i

        }

        else B[i] = 0;

     }

     return count;                          //返回已安排的活动个数

8.3.3  埃及分数

【问题】古埃及人只用分子为 1 的分数,在表示一个真分数时,将其分解为若干个埃及分数之和,例如:7/8 表示为 1/2 + 1/3 + 1/24。埃及分数问题(Egypt fraction)要求把一个真分数表示为最少的埃及分数之和的形式。

【想法】一个真分数的埃及分数表示不是唯一的,显然,贪心策略是选择真分数包含的最大埃及分数,以 7/8 为例,7/8 > 1/2,则1/2是第一次贪心选择的结果;7/8 - 1/2 = 3/8 > 1/3,则 1/3 是第二次贪心选择的结果;3/8 - 1/3 = 1/24,则 1/24是第三次贪心选择的结果,即 7/8 = 1/2 + 1/3 + 1/24。

设真分数为A/B,B除以A的整数部分为C,余数为D,则有下式成立:

             B = A × C + D

即:

             B/A = C + D/A < C + 1

则:

             A/B > 1/(C + 1)

即1/(C + 1) 即为真分数A/B包含的最大埃及分数。

设E = C + 1,由于

             A/B – 1/E = ((A×E) – B)/(B×E)

则真分数减去最大埃及分数后,得到真分数

            ((A×E) – B)/B×E

该真分数可能存在公因子,需要化简。

【算法】设函数EgyptFraction实现埃及分数问题,算法如下:

算法:埃及分数EgyptFraction

输入:真分数的分子 A 和分母 B

输出:最少的埃及分数之和

       1. E = B/A + 1;

       2. 输出 1/E;

       3. A = A * E – B; B = B * E;

       4. 求 A 和 B 的最大公约数 R,如果 R 不为 1,则将 A 和 B 同时除以 R;

       5. 如果 A 等于 1,则输出 1/B,算法结束;否则转步骤 1 重复执行;

【算法分析】假设真分数是 m/n,考虑最坏情况,m/n 表示为 m 个 1/n 之和,则对 m/n 的分解要执行 m 次,因此,时间复杂度为O(m)。

【算法实现】函数EgyptFraction在执行过程中需要调用函数CommFactor求 A 和 B的最大公约数并对 A/B 进行化简,程序如下:

void EgyptFraction(int A, int B) 

{                     

    int E, R;

    cout<<A<<"/"<<B<<" = ";

    do

    {

        E = B/A + 1;                                             //求真分数A/B包含的最大埃及分数

        cout<<"1/"<<E<<" + ";           

        A = A * E - B;    B = B * E;                     //计算A/B – 1/E

        R = CommFactor(A, B);                          //求A和B的最大公约数

        if (R > 1)  {  A = A/R; B = B/R;   }          //将A/B化简

    } while (A > 1);                  

    cout<<"1/"<<B<<endl;                                //输出最后一个埃及分数1/B

    return;                        

  }

8.4.2  田忌赛马

【问题】田忌和齐王赛马,他们各有 n 匹马,每次双方各派出一匹马进行赛跑,获胜的一方记 1 分,失败的一方记 -1 分,平局不计分,假设每匹马只能出场一次,每匹马有个速度值,比赛中速度快的马一定会获胜。田忌知道所有马的速度值,且田忌可以安排每轮赛跑双方出场的马,问田忌如何安排马的出场次序,使得最后获胜的比分最大?

【想法】贪心法求解田忌赛马的贪心策略是保证每一场赛跑都是最优方案,分别考虑如下情况:

(1)田忌最快的马比齐王最快的马快,则拿两匹最快的马进行赛跑,因为田忌最快的马一定能赢一场,此时选齐王最快的马是最优的。

(2)田忌最快的马比齐王最快的马慢,则拿田忌最慢的马和齐王最快的马进行赛跑,因为齐王最快的马一定能赢一场,此时选田忌最慢的马是最优的。

(3)田忌最快的马与齐王最快的马速度相等,考虑以下两种情况:

          ①田忌最慢的马比齐王最慢的马要快,则拿两匹最慢的马进行赛跑,因为齐王最慢的马一定会输一场,此时田忌选最慢的马一定是最优的。

         ②否则,用田忌最慢的马与齐王最快的马赛跑,因为田忌最慢的马一定不能赢一场,而齐王最快的马一定不会输一场,此时选田忌最慢的马一定是最优的。

【算法实现】设数组t[n]存储田忌 n 匹马的速度,q[n]存储齐王 n匹马的速度,简单起见,数组t[n]和q[n]均按速度升序排列,[left1, right1]和[left2, right2]分别表示双方尚未赛跑的马,程序如下:

int TianjiHorse(int t[ ], int q[ ], int n)

{

    int count = 0, left1, right1, left2, right2;

    left1 = left2 = 0;  right1 = right2 = n - 1;

    while (left1 <= right1)

    {

        if (t[right1] > q[right2]) {

            count++; right1--; right2--;

        }

        else if (t[right1] < q[right2]) {

            count--; left1++; right2--;

        }

        else  {

           if (t[left1] > q[left2]) {

               count++; left1++; left2++;

           }

           else {

               if (t[left1] < q[right2]) count--;

               left1++; right2--;

           }

        }

    }

    return count;

}

  • 12
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值