《算法导论》学习笔记之一:初识算法

关于算法的定义

  学术地说法,算法是任何良定义的计算过程
  通俗地说法,算法是计算的方法

算法在实际中的应用

  我是自动化专业出身的,实际应用较多的算法就是排序算法、数值算法、最短路径搜索算法等,擅长工程中的软件应用和嵌入式软件,对纯软件算法编程理论知识的理解并没有科班出身的童鞋那么深刻,因此对线性规划、动态规划、拓扑排序、数据结构、NP完全问题并没有很深入地理解和掌握。所以此章记录了几个我不太擅长的算法,也算是对自己理解知识的一次梳理吧。

线性规划

  什么地方会用线性规划?最常用的例子:一家石油公司如何根据已探明的油田来设置油井,从而使其利润最大化。一位政治候选人怎么确定在什么地方投放竞选广告才能最大化赢得竞选。一家航空公司在确保每个航班被覆盖而且满足政府有关乘务员调度的法规下,怎么才能最廉价地分配乘务员到每个航班上。一个互联网服务提供商怎么确定在什么地方放置附加的资源才能有效地服务客户。这些都是线性规划的范畴。我们先不讨论线性规划原理是什么、如何代码实现,但这里我们首先要对线性规划有个初步地认识,以便后面我们在专门学习线性规划的时候有更深刻的理解。那么,什么是线性规划?

  非常简单,线性规划的问题产生来自资源的不足。可以这么理解,在有限资源下我们需要一种方法去最大化或最小化我们的目标,这就是线性规划的本质。具体举个例子说明一下。

  假如你是一位政治候选人,试图赢得一场选举。你的选区有三个不同的区域:市区(100,000个选民)、郊区(200,000个选民)、乡村(50,000个选民)。为了确保当选,三个选区都要至少一半的选民投票给你。你的议题有4种:修筑道路、枪支管理、农场补贴、增加汽油税。根据竞选班子的研究,你可以估计通过在每项议题上花费1000块钱做广告,在每个选区可以赢取或输掉多少选票。

政策市区郊区乡村
修路-253
枪支管理82-5
农场补贴0010
汽油税100-2

  上面表格的每个条目通过花费1000块钱广告费支持某个特定议题,在市区、郊区、乡村可以赢得选民的千人数,负数项表示失去的选民数。你的目标是:计算出花费最少的钱数,来以赢得50,000张市区选票、100,000张郊区选票、25,000乡村选票(因为为了确保当选,三个选区都要至少一半的选民投票给你)。

  那么,我们可以引入4个变量:
  1. x1是花费在修筑道路广告上的金额;
  2. x2是花费在枪支管理广告上的金额;
  3. x3是花费在农场补贴广告上的金额;
  4. x4是花费在汽油税广告上的金额;

  我们可以将赢得至少50,000张市区选票的需求写成:

(1) − 2 x 1 + 8 x 2 + 0 x 3 + 10 x 4 ≥ 50 -2x_{1}+8x_{2}+0x_{3}+10x_{4}\ge50\tag 1 2x1+8x2+0x3+10x450(1)
  我们可以将赢得至少100,000张郊区选票和25,000乡村选票的需求写成:
(2) 5 x 1 + 2 x 2 + 0 x 3 + 0 x 4 ≥ 100 5x_{1}+2x_{2}+0x_{3}+0x_{4}\ge100\tag 2 5x1+2x2+0x3+0x4100(2) (3) 3 x 1 − 5 x 2 + 10 x 3 − 2 x 4 ≥ 25 3x_{1}-5x_{2}+10x_{3}-2x_{4}\ge25\tag 3 3x15x2+10x32x425(3)
  任何一组满足不等式(1)~(3)的变量 x1,x2,x3,x4取值,都可以构成一种能够赢得足够数量选票的策略。当然,为了使花费尽可能小,希望最小化广告费用。也就是说,我们要做的,就是最小化下面的表达式:
(4) x 1 + x 2 + x 3 + x 4 x_{1}+x_{2}+x_{3}+x_{4}\tag 4 x1+x2+x3+x4(4)
  还有就是,广告费不可能是负的。
(5) x 1 ≥ 0 , x 2 ≥ 0 , x 3 ≥ 0 , x 4 ≥ 0 x_{1}\ge0,x_{2}\ge0,x_{3}\ge0,x_{4}\ge0\tag 5 x10,x20,x30,x40(5)
  到此,问题已经很清晰了。我们总结一下上述问题,无非就是上面不等式的合集。将该问题数学化为下面的形式:
  我们的目标就是使下面式子的和最小化
(6) x 1 + x 2 + x 3 + x 4 x_{1}+x_{2}+x_{3}+x_{4}\tag 6 x1+x2+x3+x4(6)
  而其中4变量满足下面4个约束条件
(7) − 2 x 1 + 8 x 2 + 0 x 3 + 10 x 4 ≥ 50 -2x_{1}+8x_{2}+0x_{3}+10x_{4}\ge50\tag 7 2x1+8x2+0x3+10x450(7) (8) 5 x 1 + 2 x 2 + 0 x 3 + 0 x 4 ≥ 100 5x_{1}+2x_{2}+0x_{3}+0x_{4}\ge100\tag 8 5x1+2x2+0x3+0x4100(8) (9) 3 x 1 − 5 x 2 + 10 x 3 − 2 x 4 ≥ 25 3x_{1}-5x_{2}+10x_{3}-2x_{4}\ge25\tag{9} 3x15x2+10x32x425(9) (10) x 1 ≥ 0 , x 2 ≥ 0 , x 3 ≥ 0 , x 4 ≥ 0 x_{1}\ge0,x_{2}\ge0,x_{3}\ge0,x_{4}\ge0 \tag{10} x10,x20,x30,x40(10)

动态规划

  什么是动态规划?这个概念比较晦涩难懂,很多人都是看了动态规划的原理和流程之后大概懂了,但是过了几天没看就忘了,基本上很难能一直记住。简单点来说,动态规划是指通过不重复地组合子问题的解来求解原问题。这么说是因为要区别于分治法。分治方法将问题划分成互不相交的子问题,递归地求解子问题,再将它们的解组合起来,求出原问题的解。动态规划的方法恰恰相反,动态规划对每个子问题只求解一次,将其解保存在一个表格中,从而无需每次求解一个子子问题时都重新计算,避免了不必要的计算。

  具体举个如何切钢条问题的例子说明一下。

  某公司购买了长钢条,将其切割为短钢条出售。切割工序本身没有成本支出。公司管理层希望知道最佳的切割方案。假定我们知道公司出售一段长度为i英寸的钢条的价格为pi。钢条的长度均为整英寸。下表是价格表。

长度i12345678910
价格pi1589101717202430

  具体问题是:给定一段长度为n英寸的钢条和一个价格表pi,求切割钢条方案,使得销售收益最大化。注意,如果长度为n英寸的钢条的价格pi足够大,最优解可能就是完全不需要切割。
明显,长度为n英寸的钢条共有2n-1种不同的切割方案,因为在距离钢条左端i英寸处(i=1,2,…,n-1),我们总可以选择切割或不切割。我们用普通的加法符号表示切割方案,因此7=2+2+3表示将长度为7英寸的钢条切割为三段——两段长度为2英寸、一段长度为3英寸。如果一个最优解将钢条切割为k段(1≤k≤n),那么最优切割方案
(11) n = i 1 + i 2 + . . . + i k n=i_{1}+i_{2}+...+i_{k}\tag{11} n=i1+i2+...+ik(11)
  将钢条切割为长度分别为i1,i2,…,ik的小段,得到最大收益
(12) r n = p i 1 + p i 2 + . . . + p i k r_{n}=p_{i_{1}}+p_{i_{2}}+...+p_{i_{k}}\tag{12} rn=pi1+pi2+...+pik(12)
  根据价格表,我们可以观察所有最优收益值ri(i=1,2,…,10)及对应的最优切割方案:

rn15810131718222530
最优解1=1(无切割)2=2(无切割)3=3(无切割)4=2+25=2+36=6(无切割)7=1+6=2+2+38=2+69=3+610=10(无切割)

  推广到一般条件下,对于rn(n≥1),我们用更短的钢条的最优切割收益来描述它:
(13) r n = m a x ( p n , r 1 + r n − 1 , r 2 + r n − 2 , . . . , r n − 1 + r 1 ) r_{n}=max(p_{n},r_{1}+r_{n-1},r_{2}+r_{n-2},...,r_{n-1}+r_{1})\tag{13} rn=maxpnr1+rn1r2+rn2...rn1+r1(13)
  第一个参数pn对应不切割,直接出售长度为n英寸的钢条的方案。其他n-1个参数对应另外n-1种方案:对每一个i=1,2,…,n-1,首先将钢条切割成长度为i和n-i的两段,接着求解这两段的最优切割收益ri和rn-i(每种方案的最优收益为两段的最优收益之和)。由于无法预知哪种方案会获得最优收益,我们必须考察所有可能的i,选取其中收益最大者。如果直接出售原钢条会获得最大收益,我们当然可以选择不做任何切割。
  注意到,为了求解规模为n的原问题,我们先求解形式完全一样、但规模更小的子问题。即当完成首次切割后,我们将两段钢条看成两个独立的钢条切割问题实例。我们通过组合两个相关子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大者,构成原问题的最优解。我们称钢条切割问题满足最优子结构性质:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解
  除了上述求解方法外,钢条切割问题还存在一种相似的但更为简单的递归求解方法:我们将钢条从左边切割下长度为i的一段,只对右边剩下的长度为n-i的一段继续进行切割(递归求解),对左边的一段不再进行切割。即问题分解的方式为:将长度为n的钢条分解为左边开始一段,以及剩余部分继续分解的结果。这样,不做任何切割的方案就可以描述为:第一段长度为n,收益为pn,剩余部分长度为0,对应的收益为r0=0。于是我们可以得到公式(13)的简化版:
(14) r n = max ⁡ 1 ≤ i ≤ n { p i + r n − i } r_{n}=\max \limits_{1≤i≤n}\{p_{i}+r_{n-i}\}\tag{14} rn=1inmax{pi+rni}(14)
  在此公式中,原问题的最优解只包含一个相关子问题(右端剩余部分)的解,而不是两个。
  下面的过程实现了公式(14)的计算,采用的是一种直接的自顶向下的递归方法。
  CUT-ROD(p,n)
  1 if n==0
  2   return 0;
  3 q=- ∞ \infty
  4 for i=1 to n
  5   q=max(q,p[i]+CUT-ROD(p,n-i))
  6 return q
  过程CUT-ROD以价格数组p[1…n]和整数n为输入,返回长度为n的钢条的最大收益。若n=0,不可能有任何收益,所以CUT-ROD的第2行返回0。
  第3行将最大收益q初始化为- ∞ \infty ,以便第4~5行的for循环能正确计算 q = max ⁡ 1 ≤ i ≤ n { p i + C U T − R O D ( p , n − i ) } q=\max \limits_{1≤i≤n}\{p_{i}+CUT-ROD(p,n-i)\} q=1inmax{pi+CUTROD(p,ni)},第6行返回计算结果,利用简单的归纳法,可以证明此结果与公式(14)计算出的最大收益rn是相等的。
  如果你用熟悉的编程语言实现CUT-ROD,并在你计算机上运行它,你会发现,一旦输入规模稍微变大,程序运行时间会变得相当长。为什么CUT-ROD效率这么差?
  原因在于,CUT-ROD反复地用相同的参数值对自身进行递归调用,即它反复求解相同的子问题。如下图,显示了n=4时的调用过程。这棵递归调用树显示了n=4时,CUT-ROD(p,n)的递归调用过程。每个结点的标号为对应子问题的规模n,因此,从父结点s到子结点t的边表示从钢条左端切下长度为s-t的一段,然后继续递归求解剩余的规模为t的子问题。从根结点到叶结点的一条路径对应长度为n的钢条的2n-1种切割方案之一。一般来说,这棵递归调用树共有2n个结点,其中有2n-1个叶结点


这棵递归调用树显示了n=4时,CUT-ROD(p,n)的递归调用过程。每个结点的标号为对应子问题的规模n,因此,从父结点s到子结点t的边表示从钢条左端切下长度为s-t的一段,然后继续递归求解剩余的规模为t的子问题。从根结点到叶结点的一条路径对应长度为n的钢条的2n-1种切割方案之一。一般来说,这棵递归调用树共有2n个结点,其中有2n-1个叶结点

  CUT-ROD(p,n)对i=1,2,…,n调用CUT-ROD(p,n-i),等价于j=0,1,…,n-1调用CUT-ROD(p,j)。当这个过程递归展开时,它做的工作量会爆炸性地增长。
  为了分析CUT-ROD的运行时间,令T(n)表示第二个参数值为n时CUT-ROD的调用次数。此值等于递归调用树中根为n的子树中的结点总数。注意,此值包含了根结点对应的最初的一次调用,因此T(0)=1,且
(15) T ( n ) = 1 + ∑ j = 0 n − 1 T ( j ) T(n)=1+\sum_{j=0}^{n-1}T(j)\tag{15} T(n)=1+j=0n1T(j)(15)
  第一项“1”表示函数的第一次调用(递归调用树的根结点),T(j)为调用CUT-ROD(p,n-i)所产生的所有调用(包括递归调用)的次数,此处j=n-i。那么CUT-ROD的运行时间为n的指数函数:
(16) T ( n ) = 2 n T(n)=2^{n}\tag{16} T(n)=2n(16)
  回头看,CUT-ROD的指数运行时间并不令人惊讶。对于长度为n的钢条,CUT-ROD显然考察了所有2n-1种可能的切割方案。递归调用树中共有2n-1个叶结点,每个叶结点对于一种可能的钢条切割方案。对每条从根到叶的路径,路径上的标号给出了每次切割前右边剩余部分的长度(子问题的规模)。也就是说,标号给出了对应的切割点(从钢条右端测量)。

使用动态规划方法求解最优钢条切割问题

  我们现在展示如何将CUT-ROD转换为一个更高效的动态规划算法。
  动态规划方法的思想如下所述。我们已经看到,朴素递归算法之所以效率很低,是因为它反复求解相同的子问题。因此,动态规划方法仔细安排求解顺序,对每个子问题只求解一次,并将结果保存下来。如果随后再次需要此子问题的解,只需查找保存的结果,而不必重新计算。因此,动态规划方法是付出额外的内存空间来节省计算时间,是典型的时空权衡的例子。而时间上的节省可能是非常巨大的:可能将一个指数时间的解转化为一个多项式时间的解。如果子问题的数量是输入规模的多项式函数,而我们可以在多项式时间内求解出每个子问题,那么动态规划方法的总运行时间就是多项式阶的。
  动态规划有两种等价的实现方法,下面以钢条切割问题为例展示这两种方法。
  第一种方法称为带备忘的自顶向下法。此方法仍按照自然的递归形式编写过程,但过程会保存每个子问题的解(通常保存在一个数组或散列表中)。当需要一个子问题的解时,过程首先检查是否已经保存过此解。如果是,则直接返回保存的值,从而节省了计算时间;否则,按通常方式计算这个子问题。我们称这个递归过程是带备忘的,因为它记住了之前已经计算出的结果。
  第二章方法称为自底向上法。这种方法一般需要恰当定义子问题“规模”的概念,使得任何子问题的求解都只依赖于“更小的”子问题的求解。因而我们可以将子问题按规模排序,按由小至大的顺序进行求解。当求解某个子问题时,它所依赖的那些更小的子问题都已求解完毕,结果已经保存。每个子问题只需求解一次,当我们求解它(也是第一次遇到它)时,它的所有前提子问题都已求解完成。
  两种方法得到的算法具有相同的渐近运行时间,仅有的差异是在某些特殊情况下,自顶向下方法并未真正递归地考察所有可能的子问题。由于没有频繁的递归函数调用的开销,自底向上方法的时间复杂性函数通常具有更小的系数。
  下面给出的是自顶向下CUT-ROD过程的伪代码,加入了备忘机制:
  MEMOIZED-CUT-ROD(p,n)
  1 let r[0…n] be a new array
  2 for i=0 to n
  3   r[i]=- ∞ \infty
  4 return MEMOIZED-CUT-ROD-AUX(p,n,r)

  MEMOIZED-CUT-ROD-AUX(p,n,r)
  1 if r[n]≥0
  2   return r[n]
  3 if n==0
  4   q=0
  5 else q=- ∞ \infty
  6   for i=1 to n
  7     q=max(q,p[i]+MEMOIZED-CUT-ROD-AUX(p,n-i,r))
  8 r[n]=q
  9 return q
  这里,主过程MEMOIZED-CUT-ROD讲辅助数组r[0…n]的元素均初始化为- ∞ \infty ,这是一种常见的表示“未知值”的方法(已知收益总是非负值)。然后它会调用辅助过程MEMOIZED-CUT-ROD-AUX。
  过程MEMOIZED-CUT-ROD-AUX是最初的CUT-ROD引入备忘机制的版本。它首先检查所需值是否已知(第1行),如果是则第2行直接返回保存的值;否则,第3~7行用通常方法计算需所需值q,第8行将q存入r[n],第9行将其返回。
  
  自底向上版本更为简单:
  BOTTOM-UP-CUT-ROD(p,n)
  1 let r[0…n] be a new array
  2 r[0]=0
  3 for j=1 to n
  4   q=- ∞ \infty
  5   for i=1 to j
  6     q=max(q,p[i]+r[j-i])
  7   r[j]=q
  8 return r[n]
  自底向上版本BOTTOM-UP-CUT-ROD采用子问题的自然顺序:若i<j,则规模为i的子问题比规模为j的子问题“更小”。因此,过程依次求解规模为j=0,1,…,n的子问题。
  过程BOTTOM-UP-CUT-ROD的第1行创建一个新数组r[0…n]来保存子问题的解,第2行将r[0]初始化为0,因为长度为0的钢条没有收益。第3~6行对j=1,2,…,n按升序求解每个规模为j的子问题。求解规模为j的子问题的方法与CUT-ROD所采用的方法相同,只是现在直接访问数组元素r[j-i]来获得规模为j-i的子问题的解(第6行),而不必进行递归调用。第7行将规模为j的子问题的解存入数组r[j]。最后,第8行返回r[n],即最优解 r n r_{n} rn
  
  可能有的童鞋没耐心看到最后,没关系,这里只是初步的介绍算法,对部分算法有个大概的了解,具体分析和写代码要等到后面慢慢讲解,好了,第一篇就写到这,码了好久也累了,88~

[1]: 《算法导论(原书第3版)》作者: Thomas H.Cormen / Charles E.Leiserson / Ronald L.Rivest / Clifford Stein 出版社: 机械工业出版社
[2]: https://blog.csdn.net/perfumekristy/article/details/8816340
[3]: https://blog.csdn.net/xmc281141947/article/details/56835567
[4]: http://www.mohu.org/info/symbols/symbols.htm
[5]:https://www.zybuluo.com/codeep/note/163962#3如何输入括号和分隔符

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晚餐男孩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值