算分-DESIGN THECHNIQUES
Posted zyna
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算分-DESIGN THECHNIQUES相关的知识,希望对你有一定的参考价值。
Divide-and-Conquer:
教材中是用快排作为例子介绍分治算法的,主要的是几个式子:
最坏情况下的快排:T(n) = n + T(n-1)
最好情况下的快排:T(n) = n + 2*T((n-1) / 2)
随机情况下的快排:T(n) = n + 1/n * sum(T(i) + T(n-1-i)) for i = 0,1,2,...,n-1
值得一提的是一个尾递归的问题,如果是带有尾递归的话,调用栈的空间占用会达到n的规模,但是取消尾递归且每次调用较小的那一段,调用栈的空间只需要logn的规模,见代码:
1 void Quicksort(int l, int r){ 2 if(l < r){ 3 int m = split(l, r); 4 Quicksort(l, m - 1); 5 Quicksort(m + 1, r); 6 //有尾递归 7 } 8 } 9 10 void Quicksort(int l, int r){ 11 int i = l, j = r; 12 while(i < j){ 13 int m = split(i, j); 14 if(m - i < j - m){ 15 Quicksort(i, m - 1); 16 i = m + 1; 17 } 18 else{ 19 Quicksort(m + 1, j); 20 j = m - 1; 21 } 22 }//取消尾递归 23 }
Prune-and-Search:
教材中用了一个寻找第k大/小的数作为例子,首先如果k = 1是很简单的,问题就在于怎么对其他的k进行处理。
一个比较直观的想法是用快排的思路进行处理,见下:
int rSelect(int l, int r, int i){ int q = rSplit(l, r); int m = q - l + 1; if(i < m){ return rSelect(l, q - 1, i); } else if(i == m) return q; else{ return rSelect(q + 1, r, i - m); } }
这个算法实际上就是一个分治的想法,不难发现T(n)这个复杂度函数是单调递增的,所以可以借助之前快排的分析方法得到T(n)是O(n)的,要注意的T是平均复杂度。那么对于最差的情况,分裂的树还是有可能很不均匀,此时T(n)就会达到n^2的规模,实际上是有办法来解决这件事情的。
用中位数的想法+快排的想法来做,把n个元素分为ceil(n/5)个组,每个组最多5个元素,不妨设分成了m组,第i组有中位数xi,x1~xm有中位数y,然后对y进行快排的split操作,由于至少有3/10n个元素小于等于y,至少有3/10n个元素大于等于y,所以最差情况下的T也会满足:T(n) <= n(split带来的)+ T(n/5)(找中位数的中位数) + T(7/10n) (左/右任意一边小于等于7/10n个)
由于1/5 + 7/10 < 1所以可以得到T(n)是O(n)的,此时的T(n)是关于寻找第k大的数的最坏的复杂度!
Dynamic Programming:
动态规划是在之前两种的方法下产生的,目的是为了减少子问题的重复计算。教材里是用单词编辑距离来作为例子的,两个单词之间的编辑距离定义成通过删除某个字母,替换某个字母,插入某个字母的操作使得两个单词一样的总操作的次数。例如FOOD和MONEY之间的距离是4,FOOD-MOOD-MOND-MONE-MONEY,总共4此操作。但是对于一个比较长的单词就比较难以下手,设两个单词分别是A[1,n]和B[1,m]我们引入一个E(i,j)来记录A[1,i]和B[1,j]之间的最少移动次数,且我们的方法一直是从A变化到B(这个是无所谓的)。
这里要提一下最优子结构的问题,教材里有一个比较不错的想法是用反证法来说明,就是E的一系列操作都是最优的,否则去掉最后一步剩下的如果非最优的可以进行替换得到矛盾(这里是一个理解的方法)。然后就是递推关系的建立了。比如E(i,0) = i, E(0,j)= j; 删除时:E(i,j) = E(i-1,j) + 1;插入时:E(i,j) = E(i,j-1) + 1;替换时:E(i,j) = E(i-1,j-1) + P(i,j),其中P(i,j) = I[A[i] == B[j]],是一个逻辑变量。这样一来就可以进行递推了。写个简陋的程序:
1 #include<iostream> 2 #include<cstring> 3 using namespace std; 4 int main(){ 5 int E[20][20]; 6 char s[20], t[20]; 7 cin >> s >> t; 8 int lens = strlen(s), lent = strlen(t); 9 E[0][0] = 0; 10 for(int i = 1; i <= lens; i++){ 11 E[i][0] = i; 12 } 13 for(int j = 1; j <= lent; j++){ 14 E[0][j] = j; 15 } 16 for(int i = 0; i < lens; i++){ 17 for(int j = 0; j < lent;j++){ 18 int m = (s[i] != t[j]); 19 E[i + 1][j + 1] = min(E[i][j + 1] + 1, min(E[i + 1][j] + 1, E[i][j] + m)); 20 } 21 } 22 for(int i = 0; i <= lens; i++){ 23 for(int j = 0; j <= lent; j++){ 24 cout << E[i][j] << ‘,‘; 25 } 26 cout << endl; 27 } 28 return 0; 29 }
这样一来,我们就可以得到整个E的表了,同时我们可以通过比较E[i][j]和之前的E的大小关系来确定当前步是进行了哪一步操作(插入、删除、替换)。
Greedy Algorithms:
贪心算法的核心在于贪心,也就是只顾眼前的最优,不管将来,但是最重要的是去证明可行性(也就是正确性)。
第一个例子是关于任务选择,每个任务有开始时间si和结束时间fi,现在要你尽可能的选择出多的任务使得任务之间的时间不会有交集。想法是先按fi排序,然后先选择1号任务,再顺序找出第一个si>f1的任务,把结束时间定成fi,再依次找出开始时间大于fi的,依次类推,伪代码如下:
1 sort(A + 1, A + n, f_lower); 2 c = 1, S = {1} 3 for i = 1 to n: 4 if f[i] >= s[c]: 5 S = S U {i} 6 c = i 7 return S
这样子就可以找到整个S了,为什么呢?我们只需要考察正确答案的第一个任务a即可,设我们选择的任务是b,由于b结束时间小于等于a,如果b的开始时间大于等于a的结束时间,就可以加入a,矛盾!否则可以删除b而加入a不影响最优解的数量,所以可以证明我们的选择是可行的。复杂度是O(nlogn)。
第二个例子是二叉哈夫曼树,首先构造哈夫曼树的方法是每次都选择最小权重的两个节点,然后删除两个节点,生成一个新节点,权重为两个旧节点的和。至于正确性的证明分两步:
第一步,证明命题:如果T是一棵满二叉树,P(T)是它的带权外部路径长度,S(T)是所有节点的集合,其中u,v有最小的权重,我们可以构造一棵树T‘,满足下面三个条件,1)S(T‘) = (S(T) - {u,v})U{k},其中k是由u,v合并得到的,2)w(k) = w(u) + w(v), 3)P(T‘) <= P(T) - w(u) - w(v),当且仅当u,v为兄弟时取等号。
第二步时用归纳法的方法来证明对于任何的节点集合按照上述方法产生的哈夫曼树的外部权重路径最小。(注意用归纳法证明)
以上是关于算分-DESIGN THECHNIQUES的主要内容,如果未能解决你的问题,请参考以下文章