贪心算法分析
来源:互联网 发布:ios软件闪退 编辑:程序博客网 时间:2024/06/10 01:42
动态规划是对分治算法的一种改善,当它发现分解出来的子问题有重叠时,使用自底向上的策略来避免重复计算,从而提高了算法的效率。其特点是在每次做出选择前,将所有选择的效果都进行计算,在此计算的基础上选出最优的选项,所以动态规划的每次选择都是最优的。但是,当可选项的数目巨大时,算法将不堪重负,所以在这种条件下,有必要采用新的算法。
贪心算法的基本策略:一步一步地构建问题的最优解,其中每一步均只考虑眼前的最优选择,即希望通过局部最优达到全局最优。与动态规划不同的是,贪心策略将待要解决的问题分解为一个子问题(而不是动态规划里面的多个子问题),这个选择加上对剩下子问题的最优解将合成对原问题的最优解。贪心算法对解空间进行搜索时,并不是搜遍所有的空间,而是在局部范围内进行择优选取,所以贪心算法最终找到的是一个可行解,而不是所有的解,而且贪心算法并不是总能获得最优解。但是如果一个问具有贪心选择性质,则可以保证贪心算法能够得到最优解。在使用贪心算法时,要抓住两个重要特征:
(1)贪心选择性质:贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
(2)最优子结构性质:当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征。
例 1 删数字问题
在n 个数字字符串中,删除其中 k(k<n)个数字后,剩下的数字按原序组成一个新的正整数。如何删除,才能使新的正整数最大。如在整数791863中删除3个数后,所得的最大整数为多大?
分析:要使新的正整数越大,那么就应该使新的高位数越大。由此可以利用贪心策略:每次选择使剩下的数最大的数字作为删除对象,显然删除k个数字的全局最优解包含了删除一个数字的最优解,这满足了最优子结构性质。设当前串长为m,那么删除哪个数字能使得新的整数最大呢?我们可以从左到右扫描,对每两个相邻的数字进行比较,若为增序,就把左边的数字删除。若无增序,即所有数字全为降序或相等,则只需删除最右边的数字即可。
技巧:每删除一个数字a[i-1],则表明a[i-2]之前的序列不含增区间,故下次只需要从a[i-2]开始查找增区间,而不用从头查找。此外,如果每次删除数字后都进行后续数字向前移动的操作,当数字字符串的长度很大时,必然大增加数据移动的次数,所以为了减少移动操作,每次可以将删除的位置为-1,表示此数字已经删除,查找增区间时路过这些位即可。
算法实现如下:
#include<stdio.h>int main(){int i, k, m, n, t, x, a[15000];char b[15000];printf("请输入整数字符串:\n");scanf("%s", b);for(n=0, i=0; b[i]!='\0'; i++){n++;a[i] = b[i] - 48;}printf("\n请输入你要从串中删除多少个数字:\n");scanf("%d", &k);printf("\n从 %d 位整数中删除的 %d 个数字依次为:\n", n, k);t=0; m=0; x=0; i=t+1;while(x<k && i<=n){if(t>=0 && a[t]<a[i]){printf("%d ", a[t]);a[t] = -1;while(t>=0 && a[t]==-1){t--;}}else{t=i++;}}printf("\n\n所得最大的新正整数为:\n");for(i=0, x=0; x<n-k; i++)//打印左边(n-k)个非 -1 的数字{if(a[i] != -1){printf("%d", a[i]);x++;}}printf("\n\n");return 0;}
例 2 哈夫曼编码的实现
哈夫曼编码是广泛地用于数据文件压缩的十分有效的编码方法。其压缩率通常在20%~90%之间。哈夫曼编码算法用字符在文件中出现的频率表来建立一个用0,1串表示各字符的最优表示方式。
给出现频率高的字符较短的编码,出现频率较低的字符以较长的编码,可以大大缩短总码长。
1、前缀码
对每一个字符规定一个0,1串作为其代码,并要求任一字符的代码都不是其它字符代码的前缀。这种编码称为前缀码。
编码的前缀性质可以使译码方法非常简单。
表示最优前缀码的二叉树总是一棵完全二叉树,即树中任一结点都有2个儿子结点。
平均码长定义为:
使平均码长达到最小的前缀码编码方案称为给定编码字符集C的最优前缀码。
2、构造哈夫曼编码
哈夫曼提出构造最优前缀码的贪心算法,由此产生的编码方案称为哈夫曼编码。
哈夫曼算法以自底向上的方式构造表示最优前缀码的二叉树T。
算法以|C|个叶结点开始,执行|C|-1次的“合并”运算后产生最终所要求的树T。
在huffmanTree算法的实现中,给定编码字符集中每一字符c的频率是f(c)。以f为键值的优先队列Q用在贪心选择时有效地确定算法当前要合并的2棵具有最小频率的树。一旦2棵具有最小频率的树合并后,产生一棵新的树,其频率为合并的2棵树的频率之和,并将新树插入优先队列Q。经过n-1次的合并后,优先队列中只剩下一棵树,即所要求的树T。
下面给出哈夫曼算法的完整的一个实现版本,该程序为名为input.txt的文件中出现的字符计算出它们的哈夫曼编码,并最终记录到output.txt文件。其中 input.txt文件中的内容只能是键盘上的所有可显示字符和换行符,由于键盘上的可显示字符不超过128个,所以我定义了一个长度为128的整型数组count[],用于记录文件中各字符出现的数次,该步骤由ReadFile(count);函数实现。接下来调用CreatHtree()函数构造哈夫曼树,在该函数刚开始时,所有结点都还未形成,所以将所有结点(即所有的数组元素)的双亲和孩子的编号都设置为0(在第一for循环中实现);在第二for循环中,根据count[]来生成各叶子结点时要注意,由于可能input.txt文件中不一定会含有键盘上的所有可显示字符,所以有count[i]==0的情况,要跳过这些计数为0的元素;第三for循环由前面生成的初始结点构建哈夫曼树,每次从数组下标从0到(k-1)的结点中选出两个权值最小的两个结点后,将它们的仅值相加并由此值生成新结点,新结点插入在数组下标为k的位置上……
程序源码如下:
#include<iostream> #include<fstream.h> #define MAXLEAFNUM 128 typedef struct node//二叉树的结点结构 { int weight; char ch; int parent; int lchild, rchild; }Hnode; typedef char * * HuffmanCode; HuffmanCode Hc;//用于存储每个字符的哈夫曼编码 Hnode Ht[2 * MAXLEAFNUM];//存储所构造的二叉树 void Select(Hnode Ht[], int n, int &s1, int &s2) {//在Ht[1..k-1]中选择 parent 为 0 且 weight 最小的两个结点,其序号分别为 s1, s2 int x1 = 32767, x2 = 32767, k; //32767为权值上限 s1 = s2 = 1; for(k = 1; k <= n; k++) { if((0 == Ht[k].parent) && (Ht[k].weight < x1)) { x2 = x1; s2 = s1; x1 = Ht[k].weight; s1 = k; } else if((0 == Ht[k].parent) && (Ht[k].weight < x2)) { x2 = Ht[k].weight; s2 = k; } } } void CreatHtree(int n, int count[], int &m)//构造一棵含有 n 个树叶的最优二叉树,并将最优二叉树存放在数组Ht中 { int i, k, s1, s2; for(i = 1; i <= (2 * n - 1); i++)//初始化所有结点 { Ht[i].parent = 0; Ht[i].lchild = 0; Ht[i].rchild = 0; } for(i = 1, k = 0; k < n; k++) { if(count[k] != 0) { Ht[i].weight = count[k];//记入第 i 个叶子的权值 Ht[i].ch = (char)k;//记入第 i 个叶子所代表的字符 ++i; } } m = i - 1;////////////////////// Ht中的实际结点数为 i - 1 ////////////////////////////// for(k = m + 1; k <= (2 * m - 1); k++)//建立最优二叉树 { //在Ht[1..k-1]中选择 parent 为 0 且 weight 最小的两个结点,其序号分别为 s1, s2 Select(Ht, k - 1, s1, s2); Ht[s1].parent = k; Ht[s2].parent = k; Ht[k].lchild = s1; Ht[k].rchild = s2; Ht[k].weight = Ht[s1].weight + Ht[s2].weight; } } void LeafCode(int n)//采用非递归方法遍历最优二叉树, 求 n 个叶子的编码 { int i, p = 2 * n - 1, cdlen = 0; char code[30]; Hc = (HuffmanCode)malloc((n + 1) * sizeof(char *));//用于存储每个字符的哈夫曼编码 for(i = 1; i <= p; i++) { Ht[i].weight = 0;//遍历二叉树时用作结点的状态标志 } while(p)//遍历最优二叉树, 求 n 个叶子的编码 { if(0 == Ht[p].weight)//向左走 { Ht[p].weight = 1; if(Ht[p].lchild != 0)//该结点有左孩子 { p = Ht[p].lchild; code[cdlen++] = '0'; } else if(0 == Ht[p].rchild)//已走到叶子结点, 记下该叶子结点的字符的编码 { Hc[p] = (char *)malloc((cdlen + 1) * sizeof(char)); code[cdlen] = '\0'; strcpy(Hc[p], code); } } else if(1 == Ht[p].weight)//向右走 { Ht[p].weight = 2; if(Ht[p].rchild != 0)//该结点有右孩子 { p = Ht[p].rchild; code[cdlen++] = '1'; } } else//2 == Ht[p].weight, 回退 { Ht[p].weight = 0; p = Ht[p].parent; cdlen--; } }//end of while cout<<endl; for(i = 1; i <= n; i++)// 输出每个字符的编码 { if(10 == (int)Ht[i].ch) { printf("换行符 的哈夫曼编码为 : %s", Hc[i]); cout<<endl; } else if(32 == (int)Ht[i].ch) { printf("\n空格符 的哈夫曼编码为 : %s", Hc[i]); cout<<endl; } else { printf("\n字符 %c 的哈夫曼编码为: %s", Ht[i].ch, Hc[i]); cout<<endl; } } } void ReadFile(int count[])//读取指定的磁盘文件 { char ch; //定义文件输入文件流infile,以输入方式打开磁盘文件input.txt ifstream infile("input.txt",ios::in|ios::nocreate); if(!infile) { cerr<<"打开文件input.txt失败!"<<endl; exit(1); } while(infile.get(ch))//读取字符成功时执行以下语句 { count[(int)ch]++; } infile.close();//关闭磁盘文件 int i; for(i = 0; i < 128; i++)//输出文件中每个字符出现的次数 { if(count[i] != 0) { if(10 == i)//单独处理出现换行的情况 { cout<<"\n该文件中 换行符 的总个数为: "<<count[i]<<" (个)!"<<endl; } else if(32 == i)//单独处理出现空格符的情况 { cout<<"\n该文件中 空格符 的总个数为: "<<count[i]<<" (个)!"<<endl; } else//输出其它字符 { cout<<"\n该文件中字符 "<<(char)i<<" 的总个数为: "<<count[i]<<" (个)!"<<endl; } } } } void WriteFile(int n, int count[])//将所有字符的哈夫曼编码写入 output.txt 文件中 { int i; ofstream outfile("output.txt", ios::out);//定义文件流对象,打开磁盘文件output.txt if(!outfile)//如果打开文件失败,则 outfile 返回 0 { cerr<<"打开文件时失败!"<<endl; exit(0); } for(i = 1; i <= n; i++)// 将每个字符的编码写入到 output.txt 文件中 { if(10 == (int)Ht[i].ch) { outfile<<"换行符 的哈夫曼编码为 : "<<Hc[i]<<endl; cout<<endl; } else if(32 == (int)Ht[i].ch) { outfile<<"\n空格符 的哈夫曼编码为 : "<<Hc[i]<<endl; cout<<endl; } else { outfile<<"\n字符 "<<Ht[i].ch<<" 的哈夫曼编码为: "<<Hc[i]<<endl; cout<<endl; } } for(i = 0; i < 128; i++)//输出文件中每个字符出现的次数 { if(count[i] != 0) { if(10 == i)//单独处理出现换行的情况 { outfile<<"\n该文件中 换行符 的总个数为: "<<count[i]<<" (个)!"<<endl; } else if(32 == i)//单独处理出现空格符的情况 { outfile<<"\n该文件中 空格符 的总个数为: "<<count[i]<<" (个)!"<<endl; } else//输出其它字符 { outfile<<"\n该文件中字符 "<<(char)i<<" 的总个数为: "<<count[i]<<" (个)!"<<endl; } } } outfile.close();//关闭磁盘文件 } int main() { int m = 128; //定义用于存储文件中各个字符出现次数的数组,可显示字符的ASCII值的范围:32---126 int count[128] = {0}; //从磁盘文件input.txt读入字符,并将各字符出现的次数记录到count数组中 ReadFile(count); //构造一棵含有 n 个树叶的最优二叉树,并将最优二叉树存放在数组Ht中 CreatHtree(128, count, m); //采用非递归方法遍历最优二叉树, 求 n 个叶子的编码 LeafCode(m); //将所有字符的哈夫曼编码写入 output.txt 文件中 WriteFile(m, count); return 0; }
显然上述算法的CreatHtree()函数的时间复杂度为O(n^2),因为它在其第二for循环中调用了2m次Select()函数,而Select()函数在查找最小元素时采用简单的从头到尾的比较方法,即每次调用都要遍历整个数组。其实查找最小元素可以由最小堆来实现,即利用最小堆实现Select()函数。因为最小堆查找最小元素需要的运算时间为O(logn),所以这样可以使CreatHtree()函数的复杂度下降到O(nlogn)。(哈哈,理论上是这样,但是在这种复杂的面向过程的程序中,恐怕改写起来也不是轻而易举之事…)
例 3数列操作
给定一个由 n 个整数组成的数列,对数列进行一次操作:去除其中两项a、b,然后添加一项(a * b +1),则操作一次数列就减少一项,经(n-1)次操作后该数列只剩下一个数。求在(n-1)次操作后最后得数的最大值。
分析:设数列有3项x、y、z,x <= y <= z,则有(x * y + 1)* z + 1 >= (x * z + 1)* y + 1 >=(y * z + 1)* x + 1,所以每次只需选取数列中最小的两项进行操作,即可使最终得数最大。(这题很简单,这程序我就不写了…)
- 贪心算法分析
- 贪心算法分析
- 贪心算法举例分析
- 研究生课程 算法分析-贪心法
- 词法分析中的 贪心算法
- 研究生课程 算法分析-贪心法
- 贪心算法(算法分析与设计)
- HDU 1051题解分析---------贪心算法
- 算法设计与分析之贪心法
- 贪心算法思想分析及应用
- 算法与设计分析作业3(贪心)
- 算法设计与分析 普通背包 贪心
- 一步步学算法(算法分析)---6(贪心算法)
- 【算法设计与分析】8、哈弗曼编码,贪心算法实现
- 算法分析与设计复习-贪心算法描述
- 算法分析与设计-15-背包问题的贪心算法
- 01-算法的乐趣-贪心算法(Greedy Algorithm)分析
- 算法设计与分析 活动安排 贪心算法
- Java synchronized的理解!(线程安全,线程同步)
- Linux PATH---ROOT 下arm-linux-gcc command not found
- arm汇编基础一
- java 自动注册odbc
- 2.4.2 地址和地址译码器
- 贪心算法分析
- [Linux-5] gdb常用命令
- 这回是帐号同步异常,血泪经验和教训!
- android-4.0源码编译及内核编译(android-gldfish-2.6.29)
- cocos2dx里真正的随机函数
- Python input和raw_input的区别
- JSP学习1——web发展史(2013.12.15)
- c语言字符串分解
- BP神经网络设计的matlab简单实现