数据结构与算法分析(六)——算法设计技巧

从算法的实现向算法的设计转变,提供解决问题的思路

1.贪心算法

一种局部最优算法设计思路,思想是保证每一步选择在当前达到最优。一个很常见的贪心算法案例是零钱找取问题。

调度问题:书上的调度问题比较简单,其目标是所有作业的平均持续时间(调度+运行)最短,无论是但处理器还是多处理器,最优解的方案总是按作业的长短排序进行调度。《计算机算法设计与分析》上的作业调度的目标是最后完成时间最小,这要稍微复杂一些。

霍夫曼编码:最优编码必定保持满树的性质,基本问题在于找到总之最小的满二叉树。最简单的霍夫曼编码是一个两趟扫描算法:第一趟搜集频率数据,第二趟进行编码。

近似装箱问题:bin packing problem(不同于背包),分为两类:联机装箱(on-line packing)和脱机装箱(off-line packing),区别是脱机装箱具有先验性,而联机装箱不知道下一个到来的元素大小和总元素数目。

-- 可以证明,联机算法不能总给出最优解,也可以证明,存在一些输入使得任意联机装箱算法至少是最优箱子的4/3。有三种简单的算法可以保证最多使用两倍的最优装箱数。

1)丅项适配(next fit):任意一个物品到来时,只检测当前箱子能否装下,线性时间运行。

2)首次适配(first fit):依次扫描之前的箱子,寻找能放下当前物品的第一个箱子,O(N*N)运行(可以优化),可以证明,首次适配使用的箱子不多于1.7M(上取整)。

3)最佳适配(best fit):将物品放到所有箱子中能够容纳它的最满的箱子中,不会超过1.7M。

-- 脱机算法:首次适配递减算法(fist fit decreasing),按照非增进行排序,然后进行fist fit装箱。

-- 应用:操作系统,在动态向堆申请内存的过程中,以隐式空闲链表实现的分配器放置分配的块时所采取的策略。(《深入理解计算机系统》修订版 P635)

2.分治算法

分治算法要求最优子结构 和独立(非重叠)子问题,算法通常由两部分组成divide-conquer。思路是递归解决较小的子问题,然后从子问题的解构建原问题的解。分治算法通常至少含有两个内部递归,其运行时间可通过下面的关系得到:

最近点问题:naive算法花费O(N*N)时间复杂度,可以将点空间划分为两半,递归的解两个子问题,然后进行合并。需要注意的是,合并的复杂度决定了最终的复杂度,所以要对合并的过程进行优化。思路是,首先取左右区域最小值的最小值dmin=min(dleft_min, dright_min),作为中间区域的边界,横轴界为[middle-dmin, mindle+dmin],纵轴自上而下以高度为dmin的窗口进行扫描,按照dmin的定义,窗口内待比较的元素不会超过7个,保证合并过程的线性复杂度,最终复杂度为O(NlogN)。算法执行前需进行O(NlogN)预处理,保留两个分别按照x坐标和y坐标排序的点的表P和Q。
选择问题:利用快排实现的选择问题可以在平均时间O(N)下进行,但其最坏复杂度为O(N*N),即使是使用了三元素中值枢纽元,也不能提供避免最坏情况的保证。改进思路是从中项的样本中找出中项(五分化中项的中项,media-of-media-of-five partitioning):将N个元素分成N/5(上取整)组,找出每组的中项,然后找出中项的中项作为枢纽元返回。可以证明:1)每个递归子问题的大小最多是原问题的70%,这样基本保证了快速选择的问题等分;2)用8次比较可以对5个数进行排序,然后递归的调用选择算法找出中项的中项,所以寻找枢纽元的复杂度为O(N),最终的复杂度也为O(N)。

最后,五分化中项的中项方法系统开销太大,根本不实用。

整数相乘:不仅要进行子问题的划分,还要通过合并变换减少实际的乘法次数

矩阵相乘:同上。

3.动态规划

通常递归算法会重复一些子问题,造成不必要的性能下降。动态规划的思路是弃用递归,将子问题的答案系统的记录在中间表(table)中。其特点是最优子结构和重叠子问题。

矩阵连乘:寻找最优分割点。上式表示存在的组合数目,是一个指数形式,遍历开销太大;下式是对父问题的分解,注意Cleft-1。

算法实现:(数据大小很容易超出类型限制,注意比较边界)

 1 #include "stdafx.h"
 2 #include <iostream>
 3 #include <vector>
 4 #include "matrix.h" //自定义matrix类
 5 using namespace std;
 6
 7 #define INFINITY 999999999
 8
 9 void optMatrix(const vector<int> &c, matrix<long> &m, matrix<long> &lastChange)
10 {
11     //c[0]存储第一个矩阵的行数,剩下的分别存储第i个矩阵的列数
12     int n = c.size() - 1;
13     //将对角元素初始化为0,保护边界
14     for(int i = 1; i <= n; i++)
15         m.data[i][i] = 0;
16     //k = right - left,left和right最大间隔是n-1
17     for(int k = 1; k < n; k++)
18         for(int left = 1; left <= n-k; left++)
19         {
20             int right = left + k;
21             m.data[left][right] = INFINITY;
22             for(int i = left; i < right; i++)
23             {
24                 long currentCost = m.data[left][i] + m.data[i+1][right]
25                     + c[left-1]*c[i]*c[right];
26                 if(currentCost < m.data[left][right])
27                 {
28                     m.data[left][right] = currentCost;
29                     lastChange.data[left][right] = i;
30                 }
31             }
32         }
33 }  

最优二叉查找树:元素带权重(通常是出现的概率),目标是使得查找带来的开销最小,此时平衡二叉树不是最优。与矩阵连乘问题类似,子问题分解公式为:

所有点最短路径:Dijkstra算法对于单源最短路径查找为O(N*N),所有点需要N次迭代。此处给出一种更紧凑的动态规划算法,该算法支持负值路径(Dijkstra不支持)。子问题分解式:

算法实现:                                          

 1 void dynamicDij(matrix<int> &a, matrix<int> &d, matrix<int> &route)
 2 {
 3     //matrix<int> &a不能定义成const,否则调用不了getHeight()
 4     int n = a.getHeight();
 5
 6     for(int i = 0; i < n; i++)
 7         for(int j = 0; j < n; j++)
 8         {
 9             d.data[i][j] = a.data[i][j];
10             route.data[i][j] = -1;
11         }
12
13     for(int k = 0; k < n; k++)
14         for(int i = 1; i < n; i++)
15             for(int j = 0; j < n; j++)
16             {
17                 int cost_tmp = d.data[i][k]+d.data[k][j];
18                 if(cost_tmp < d.data[i][j])
19                 {
20                     d.data[i][j] = cost_tmp;
21                     route.data[i][j] = k;
22                 }
23             }
24 }  

4. 随机化算法

好的随机化算法没有不好的输入,而只有不好的随机数(考虑快排枢纽元的选取)。

随机数生成器:实现真正的随机数生成器是不可能的,依赖于算法,都是些伪随机数。产生随机数最简答的方法是线性同余生成器,Xi+1 = A Xi mod M,序列的最大生成周期为M-1,还需要仔细选择A,贸然修改通常意味着失败。需要注意的是,直接按照前面公式实现有可能发生溢出大数的现象,通常需要进行变换。

跳跃表:借助随机化算法实现以O(NlogN)期望时间支持查找和插入操作数据结构。本质是多指针链表。当查找时,从跳跃表节点的最高阶链开始寻找;当插入元素时,其阶数是随机的(抛硬币直到正面朝上时的总次数)。

素性测试:某些密码方案依赖于大数分解的难度。费马小定理(下1):如果定理宣称一个数不是素数,那这个数肯定不是素数;若宣称是素数的话,有可能不是素数。可借助平方探测的特殊情况(下2)加以判断。

5.回溯算法

虽然分析回溯算法的复杂度可能很高,但实际上的性能却很好,明显优于穷举法。有时还会借助裁剪的方法进一步降低复杂度。与分支限界法不同,回溯算法通常是深度优先递归的搜索所有解,而分支限界通常是广度优先,找出满足条件的一个解。

公路收费问题:(重构问题远比创建问题复杂)一般以O(N*NlogN)运行(假设没有回溯,那么以优先队列实现d,每次插取数据logN,共插取了N*N次),最坏需花费指数时间。伪代码:

 1 bool tuinpike(vector<int> &x, DisSet d, int n)
 2 {
 3     x[1] = 0; //设置原点
 4     d.deleteMax(x[n]);
 5     d.deleteMax(x[n-1]);
 6
 7     //由于问题在初始时具有对称性,因此判断一次即可
 8     if(x[n] - x[n-1] in d)
 9     {
10         d.remove(x[n] - x[n-1]);
11         return place(x, d, n, 2, n - 2);
12     }
13     else
14         return false;
15 }
16
17 bool place(vector<int> &x, DisSet d, int n, int left, int right)
18 {
19     bool found = false;
20
21     if (d.isEmpty()) return ture;
22
23     int max = d.findMax();
24
25     if(|x[i] - max| in d, for all 1<=i<left, right<i<=n)
26     {
27         x[right] = max;
28         for(1<=i<left, right<i<=n)
29             d.remove(|x[i] - max|);
30         found = place(x, d, n, left, right - 1);
31
32         //backtrack不可行,恢复问题,换个方向继续试
33         if(found == false)
34         {
35             for(1<=i<left, right<i<=n)
36                 d.insert(|x[i] - max|);
37         }
38     }
39
40     if(found == false && |x[i] - (x[n] - max)| in d, for all 1<=i<left, right<i<=n )
41     {
42         x[left] = x[n] - max; //注意不是max
43         for(1<=i<left, right<i<=n)
44             d.remove(|x[i] - (x[n] - max)|);
45         found = place(x, d, n, left + 1, right);
46
47         if(found == false)
48         {
49             for(1<=i<left, right<i<=n)
50                 d.insert(|x[i] - (x[n] - max)|);
51         }
52     }
53
54     return found;
55 }  

转自:http://blog.csdn.net/woshishuizzz/article/details/8440309

时间: 2024-08-28 17:04:43

数据结构与算法分析(六)——算法设计技巧的相关文章

《数据结构与算法分析:C语言描述》复习——第十章“算法设计技巧”——Alpha-Beta剪枝

2014.07.08 22:43 简介: “搜索”与“剪枝”几乎是如影随形的.此处的“搜索”指的是带有回溯算法的深度优先搜索. 在之前的“Minimax策略”中我们给出了一个三连棋的程序,运行后你就知道计算一步棋要花多少时间. 为了计算最优的一步棋,我们可能需要递归9万多次.如果毫无疑问这种阶乘式的穷举过程必须通过剪枝来加速. 本篇介绍一种用于Minimax策略的剪枝思路——α-β剪枝. 剪枝的英语是pruning,所以不要想当然说成trimming. 图示: 在上一篇讲解Minimax策略的博

五种常用的算法设计技巧之二:分治算法

一,介绍 分治算法主要包含两个步骤:分.治.分,就是递归地将原问题分解成小问题:治则是:在解决了各个小问题之后(各个击破之后)合并小问题的解,从而得到整个问题的解 二,分治递归表达式 分治算法一般都可以写出一个递归表达式:比如经典的归并排序的递归表达式:T(N)=2T(N/2)+O(N) T(N)代表整个原问题,采用了分治解决方案后,它可以表示成: ①分解成了两个规模只有原来一半(N/2)的子问题:T(N/2) ②当解决完这两个子问题T(N/2)之后,再合并这两个子问题需要的代价是 O(N) 递

《数据结构与算法分析》课程设计——贪吃蛇问题

中国矿业大学信控学院 /*文献参考*/ https://blog.csdn.net/Fdog_/article/details/102625969 https://blog.csdn.net/DY_1024/article/details/78841757   一.问题描述 以数据结构思想设计实现贪吃蛇小游戏. 二.需求分析 首先需要考虑如何设计一个win运行窗口来实时显示结果 然后考虑到蛇的身子是一节一节的,此时最容易联想到的数据结构就是顺序表,链表,如果把蛇比做顺序表或者链表,在之后吃到食物

《数据结构与算法分析》课程设计——迷宫问题

中国矿业大学信控学院   一. 问题描述   问题中迷宫可用方阵[m,n]表示,0表示能通过,1表示不能通过.若要从从左上角[1,1]进入迷宫,设计算法,寻求一条从右下角 [m,n] 出去的路径.我们用递增的数来代表寻找出口方向与步数,用-2来代表寻找过程中找错的路径. 二. 需求分析   需要先创建一个迷宫,在开始后就开始搜寻,当一个点周围有0点(改点并不是以搜寻过的点),那么到这里继续往下搜,如果搜到尽头那么就要倒回去,在搜寻倒回去的周围还有为搜寻过得0点,因此需要一个存储算法,要满足后入先

Redis系列(六)-SortedSets设计技巧

阅读目录: 介绍 Score占位 更多位信息 总结 介绍 Redis Sorted Sets是类似Redis Sets数据结构,不允许重复项的String集合.不同的是Sorted Sets中的每个成员都分配了一个分数值(score),它用于在Sorted Sets中进行成员排序,从最小值到最大值.Sorted Sets中所有的成员都是唯一的,其分数(score)是可以重复的,即是说一个分数可能会对应多个值. 用Sorted Sets可以非常快的进行添加.删除.或更新成员,其复杂度是O(m*lo

016-kruskal算法-贪心-《算法设计技巧与分析》M.H.A学习笔记

最小生成树: 在一给定的连通无向图G = (V, E)中,(u, v) 代表连接顶点u与顶点v的边,而 w(u, v)代表此边的权重,若存在T为G的子集且为无循环图,使得w(T) 最小,则此T为G的最小生成树. 基本思路: kruskal算法总共选择n- 1条边,所使用的贪婪准则是:从剩下的边中选择一条不会产生环路的具有最小耗费的边加入已选择的边的集合中.注意到所选取的边若产生环路则不可能形成一棵生成树.kruskal算法分e 步,其中e 是网络中边的数目.按耗费递增的顺序来考虑这e 条边,每次

017-Prim算法-贪心-《算法设计技巧与分析》M.H.A学习笔记

基本思路: 定义结点集合U, V (U表示已经选择加入MST的结点集合,V表示未选) 1. 任选一个结点加入U 2. 选择一条边权最小的边,他的两个结点分别属于U, V,并把属于V的那个结点加入U 3. 重复执行2直到V空 伪代码: C++代码: int g[mnx][mnx]; int n, m; int d[mnx]; // 朴素 prim, 复杂度O(|V|^2) |V|:点数, |E|:边数 int prim() { memset(d, 0x3f, sizeof d); //初始化 in

014-背包问题-动态规划-《算法设计技巧与分析》M.H.A学习笔记

01背包: 01背包是在M件物品取出若干件放在空间为W的背包里,每件物品的体积为W1,W2--Wn,与之相对应的价值为P1,P2--Pn.求能获得的最大总价值. 基本思路: V[i,j]表示从前i件物品中取出的,能装入体积为j的背包的物品的最大总价值. 初始化条件: V[i,0]和V[0,j]都为0,我们从其意义上就可以理解. 状态转移方程: V[i,j]=max{ V[i-1,j],V[i-1,j-Wi]+Pi } ,前后分别为第i件物品不取和取得情况. 总的就是下面的递推式: 算法分析: 表

013--Floyd算法-动态规划-《算法设计技巧与分析》M.H.A学习笔记

多源最短路:有向图,求从每个顶点到其他所有顶点的最短距离. 基本思路: 假设有向图的所有点编号为1到n,l[i,j]表示从i到j的边的长度,如果不存在边,则置为正无穷. 定义d(k,i,j)表示从点i到点j,并且不经过编号大于k的点的最短距离. 初始化条件: K=0时,d(0,i,j)=l[i,j]. 状态转移方程: d(k,i,j)=min{ d(k-1,i,j),d(k-1,i,k)+d(k-1,k,j) }   1<=k<=n 于是我们有如下的递归式: 算法分析: 显然,Floyd算法的