算法设计分析

一。 简单设计

分治法 :之前 也接触过一些算法的设计和分析。 比较典型的有分治法。在印象中 很过都用到分治法。 在绝大部分的实例中 这也是最常见的设计。简单的说 就是把一个问题分成多个子问题来解决。一般 在程序上来实现 很多事用递归实现。如:二分查找法,快速排序等。

二 。动态规划

动态规划其实也有一些分治法的影子。 不过不同的是 分治法是将一个整体分成 N个 部分。 每个部分处理。 而动态规划 也是需要划分的。但是 却是变化。
标志
1.最优子结构
2.子问题重叠

1.生产线问题

算法导论上面的例子是这样的:
有2条汽车装配的生产线。  
P11------>P12------->P13-------P14
P21------>P22------->P23-------P24
因为装配站的技术不同时代不同 所以完成时间不同。 有时候需要赶时间。 就需要将一辆车在2条线移来移去以便达到最快速度。
而 移动也是花时间的。 
在这一题中 动态规划的思想就是我有2个变量 记着分别到达2条线 第N个 装配站的最小时间 S1N   S2N 这时候  从N到 N+1 装配站有 4种情况。  到P1(N+1) 和 P2(n+1)分别有2种情况  取最快的就可以得到 S1(N+1) 和 S2(n+1).
是不是有递归的感觉。的确 是可以递归解决 不过不推荐。 之前也讨论过递归的性能。 正序循环就Okl 

2.矩阵列乘法-----------

首先我们需要了解一下矩阵。 矩阵是一个很有用的数据结构,在程序上表示 就是一个二维数组。
一个 M*N的矩阵是一个 m行n列的二维数组。  
而两个矩阵相乘 需要满足 前面的 列数等于 后面的 行数。 否则是不可以相乘的。
M*N的 矩阵  和 N*P的矩阵相乘 得到的矩阵是 M*P的
矩阵相乘的结果 a[M*N]  乘以 b[N*P]  得到 C[M*P]  其中  c(i,j) 的值 等于 a(i,1)*b(1,j) +a(i,2)*b(2,j)......+a(i,N)*b(N,j)
矩阵 乘法满足 结合律(多个矩阵相乘 可以随便加括号 结果不变) ,但不满足 交换律(即 不可以交换前后对象)

接下来要探讨的问题 跟矩阵的结合律有关系。
首先 矩阵相乘是满足结合律的。但是相乘的顺序不同。消耗的时间却大不相同。
 a[M*N]  乘以 b[N*P]  得到 C[M*P]    
c中每个值都是循环算出来的。这就需要   M*P次循环
再看每个值是怎么来的  c(i,j) 的值 等于 a(i,1)*b(1,j) +a(i,2)*b(2,j)......+a(i,N)*b(N,j)  这个 做了 N次相加。其实也是一个N次的循环。 这样 我们就可以看到  矩阵相乘  需要做的循环次数   为 M*P*N
10*100  100*5   5*50  这3个矩阵相乘
正常顺序需要次数     10*100*5  *+  10*5*50 =5000+2500=7500
先算后面2个 需要    10*100*50  + 100*5*50=50000+250000=75000
相差10倍。如果 很多矩阵相乘。 需要计算最快的算法顺序。

毫无疑问 穷举是个极坏的方法。 情况太多。括号的不同分配情况的个数 。 我们不需要在考虑穷举 来解决累死的问题。 这也是 我们为什么要了解动态规划的原因。

我们思考过程是这样的
先假设 AiA(i+1)......Aj 对这一系列矩阵划分的最优结果。 最后肯定会在某一个地方分成2个部分。这2个部分相乘 得出最后结果。 我们假设 在 k的位置分开   即 变成 (A(i)A(i+1)......A(k))*(A(k+1)A(k+2)......A(j)) 

我们定义一下 A(i)矩阵 是  P(i-1)*P(i)    而 m(i,j)来表示  AiA(i+1)......Aj 的最优解
这样 m(i,j) 就变成了 2种情况 
1 i==j  m(i,j)=0; 因为只有一个矩阵 压根就不需要相乘。
2 i<j   m(i,j)= m(i,k) + m(k+1,j)+p(i-1)*p(k)*(j);
这里 k 不固定的  所有 需要k从i到 j-1 找最小值。
这样 就是变成了一个递归问题。但是 这样的成本 就是穷举啊。 那么 我们需要关注动态规划的第二个特性了。子问题重叠。
仔细观察 可以 看到 这个表达式中 有太多的重复求解了。 比如  
m(1,10)=m(1,5)+m(6,10)+k
m(1,9)=m(1,5)+m(6,9)+k2
.......
m(1,5)会重复计算。 
其实所有的都会被重复计算很多次。
按照思路 不用递归。 因为递归的恶劣 。 我们需要自底向上的方法。 其实 需要一个 辅助变量 记下 m(1......n,1......n)的值。
这样 我们从 m(1,1)开始循环 就可以 把所有的都记录下来了
伪代码如下
思路是 通过循环 (i 从1到n)
先m(i,i)肯定是 一个 矩阵 为0.
分别算出  m(i,i+1)  第二次循环 算出 m(i,i+2) 一次类推。 一直到 m(i,i+n-1)

这样 每次 计算 m(i,j) (j>i)的 时候  m(i,i+1).....m(i,j-1)  和    m(i+1,j)   m(i+2,j)......m(j-1,j) 都已经计算出来了。
(也可以这样考虑这个问题  就是步长。 先算出 所有步长为1的, 然后算出步长为2的。 。。为3的。一直到 为n-1的。
而 计算步长 为 k的时候。 所需要的 只是 步长小于k的值
当然 到这里 我们只是 得出了 最少的次数。 并没有得出 怎么分配。 看上面的伪代码 s[i,j]大的作用就出来了。 这个记录的是 i到j的最优解的分界点。 需要 分解1......n  先去 s[i1,n]的值 假设 为k  再去 取s[1,k] 和  s[k+1,j] 就ok了。


3.动态规划思想

前面的例子 是动态规划的典型例子。 要解决动态规划 就需要 注意动态规划的2个重要要素
1.最优子结构
最优子结构 往往是 至底往上 来解决问题的基础。 往往在不用动态规划思想的时候。我们 就是用递归来解决。当然 效率太差。 
具有最优子结构的特性。
处理最优子结构的时候。我们先尽管假设当前是最优解。 而这个解又依赖一个或者多个 最优子结构。
动态规划最优子结构的问题中 有2个重要点。 也是影响 动态规划性能的2个要素。
1.子问题的个数
2.处理每个子问题的选择个数。
例如装配线问题。 子问题 总共就有 n个  而每个子问题 的选择 有2种   性能 就 O(n)级别的
  矩阵问题  子问题 总共有 n^2 个  每个矩阵的选择 有 1到 n-1种   性能 就 O(n^3)级别的
要用到最优子结构 必须保证 子问题独立。对于不能使用最优子结构的一定不能使用 例子:算法导论205P的二个问题:
对于有向图 求
1.无权最短路径
2.无权最长简单路径


2.重复子问题
适合动态规划的另一个就是可能 需要解决同样的子问题。 即子问题重复。 其实 这也是 动态规划和分治法的区别。 分治法 每一个递归的子问题都是新的。不会重复。 而动态规划却是 可以重复的。
简单的说 我们把已经解出来的 子结构 存在表格中。下次还需要 解这个子结构的时候 就直接去获取就ok了。这里我用一个别的例子。而不是书上的两个例子来说明。Fibonacci数列:
f(n)=f(n-1)+f(n-2) 对于这个函数 大家 再熟悉不过。 一看就是递归 求解。 可是 我用递归本机测试 貌似 n>60就得不出结果了 我估算弄一下时间。 n=100 貌似要好几千年。 不记得数据了。反正 不会少 只会多。 如果 别人跟我说 我算错了 炫耀 好几 千千年 我都不会感到意外。 因为这是个指数递增的。 到后面 每大一个数 会大很多很多。
之前就是 利用自底向上解决的 先算f(1),然后f(2)。。。到f(N) 但是现在想一想 这就是动态规划的知识啊。 因为 我们 算每个f(n)的时候。 中间的  f(j) 都要被调用很多次。其实 动态规划 最后解决 很多也是用自底向上。 就比如 第一个例子 装配线问题。其实 跟飞、fibonacci数列是类似的问题。

另外还要注意一个 备忘录的解决方案。其实就是一个变种。 不是用自底向上。仍然 使用自顶向下的递归模式。不过在递归的算子结构的时候。先去查表。 有值直接取 没值再递归调用。

三。贪心算法

贪心算法其实脱胎于动态规划。我理解的贪心算法跟动态规划很有关联。他们都利用到了最优子结构。不同的是
动态规划 在把问题转化成最优子问题。而最优子问题取决于N种选择。(生产线是2种选择,矩阵是 1到N种选择 即K的位置)。而贪心算法 就是在这里 做了选择。 动态规划的选择结果是依赖子结构的结果。 所以我们自底向上 先要得出子结构 再求当前的最优解。  而贪心算法 的贪心选择 并不依赖子结构的结果。所以 自顶向下 就ok。
贪心选择 必须保证每次选择,最终产生最优解。


背包问题

有一个背包问题 。可以通过它来了解一下动态规划和贪心算法。有一个窃贼 偷一家商店的东西。他发现了 N件物品。第i件物品 价值 vi   重wi  他的包只能带W 总量的物品。 怎么带。
0-1 背包 问题 是 这些东西都是一个整体。不可以分开。 而 有些背包问题 是每个物品都可以拆分成无数份。
这就好像 金块和金粉一样。
第一个问题 是不可以用贪心算法来解决的。
而可以拆分的背包问题是可以用贪心算法解决的。 很明显。每次选择的 都是 单价最贵的。 而 0-1背包问题没有办法做贪心选择。 我个人理解 贪心选择必须保证 每次选择都是最优解。 而 0-1 背包没法保证。 因为选择之后 没办法保证背包被塞满。0-1背包问题 可以用 动态规划来解决。 转化为 某一个物品 放 或者不放的子问题的比较。
用 f[i][w]来表示 前i个物品 在总总量 为 w的前提条件下 最优选择的价值。
f[i][w]=max(f[i-1][w],f[i-1][w-wi]+vi) 
看到类似的表达式 很显然 重复子结构很多。

拟证

关于贪心算法 有一个 交拟证的组织结构。虽然没有覆盖贪心算法的所有情况,但是 对绝大部分的贪心算法有帮助。
这一段 看不懂。。先放下。

四。平摊分析

平摊分析 是在一个算法中。操作的时间是通过总时间求平均得出的。 即使其中某个步骤耗时很大。平均代价还是小的。需要了解 他与平均情况分析是有本质的区别的。平均情况分析 比如 快速排序。 我们说 快速排序的 的平均 复杂度 为 O(nlogn)。但是在坏的情况下 可能到 O(n^2)  但是平摊分析 并不涉及到 概率。 在最坏的情况下 每个操作具有平均性能。
用通俗易懂的话来讲。 平均情况分析(快速排序这样的) 你可以根据算法 自己编造一组数据 使它的性能 变成最差 而平摊分析 一直都具有平均性能。即使你根据算法 编造数据 也不会影响他的平均性能。

1.聚积分析

其实 说白了也很简单。 就是 算法最坏的情况下  的 操作时间 是T(n) 那么 平均性能 就是 T(N)/n 了。我们所需要处理的 就是找到算法的最坏 操作时间 。这里有2个例子:

1.栈操作

栈有2个基本操作  pop  push 我们假设每个操作的 代价是1. 现在我们自定义一个 操作 multipop(s,k)就是pop出 s栈的前K个对象。 如果栈里不够 就弹出剩下所有的。
这样 我们可分析一下 multipop(s,k)的代价 毫无疑问 是  min(k,S中的个数);如果 最差的情况下 对于 N长的栈。 代价可以达到O(N)。
现在 我们来考虑 一系列 N个 包含 pop push mulitpop 的操作。这样 整个操作 最差将会是 N*N的代价。这样 复杂度 为O(N^2) 这个结论是不够渐进的。 下面我们利用聚积分析 来分析。
对于整个过程的总代价 其实是不超过 2(N-1)的 因为 只有 push进来的 才可以被pop出去。最坏的情况也是 前(N-1)次push 最后一次 miltipop  出全部。所以平均代价 仍然是 O(1) 整体代价 为 O(n)。

2.计数器

我们自定义了一个计数器 是有 K位 二进制数组来实现 A[K] 。我们考虑一下每次增加的代价。 计数器算法很简单。从低位遍历数组。 如果 是 1就变成0  一直遍历到 值为0.将值变成1 跳出循环。
值考虑单个步骤 很明显 每个加1的操作 最坏的情况 是 O(K)。那么 计数器 加到 了 N  最坏代价就是 O(KN)。显然不够渐进。 因为我们不可能 总是最坏的。 我们仍然需要聚积分析:
首先 每次的加一。 第一位 肯定需要变化。 这个没问题。
 第二位呢? 只有当 第一位 变换2次 第二位才会变换一次。  
第三位呢? 只有当第二位变换2次,第三位才会变换一次。
换句话说 当操作N次的总代价 为  n+n/2 +n/4+n/8......   毫无疑问 这个 最后结果 <=2N.
所以 平均代价 仍然是 o(1).

2.记账方法

我们给每个操作都赋予了一个不同的费用。当进行这个操作的时候。 实际费用可能比这个费用 大 也可能比这个少。 当 我们赋予的费用 大于 实际费用的时候。 我们就有剩余。 我们称之为 存款。 而这些存款 用来支付 实际费用 比 我们赋予的费用大的情况。 
需要注意的是:任何时候。 存款都不能小于0。 这样能够保证任何时候 我们赋予的费用 都能够 表现整个操作的上限。
比如 如果 存款能够小于0。 当小于  0的时候 停止的时候。 那么当前赋予的费用就是 小于 总 实际费用的。 那么 我们赋予的费用 就是不正确的了。
记账方法的核心 就是找到正确的 赋予费用。
仍然以之前的2个例子举例。

1.栈操作

3个操作  push pop  multipop  我们给与的费用是  
push   2
pop     0
                multipop    0
首先 每个 push 赋予2的 费用。 其中1 用来支付 实际费用。 另外的1作为存款。 用于以后 push时候的支付费用。那么 无论什么时候 存款都 不可能小于0. 那么 考虑到这样的时候。 我们最差的情况下 是 2N。 所以总代价 仍然是 O(N)。

2.计数器

这个也是一样的道理。我们讲任何一位的 数 从0变成1的代价 赋予2 。其中一个用来支付他的 实际代价。 另外一个 存起来 用来支付 下次 从1变成0的代价。 这样 每一位的存款都不可能小于0,。 中存款当然也 不可能小于0 
其实 记账法。很好理解。 特定的问题 告诉你 用记账法来考虑。也很容易考虑出来。关键就在于,茫茫问题中。某个问题适用于这种情况 能不能想起来。

  3.势能方法

这个是将每一个操作 都假象成对势能的变化。我个人不是很理解。 而且感觉 就是用的记账法的那一套理论。 然后用了一大堆公式弄的很神秘似的。 我感觉 前2种比较通俗易懂。了解就ok了 这个不看了。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值