引论:相比与动态规划算法,贪心算法是比较容易理解的,其思想就在于得到当前状态下局部最好选择,当一个问题的最优解包含其子问题的最优解时,即每个贪心选择都是子问题的最优解,那么就能的到该问题的最优解了。本次上机实践的题目虽然不是特别难,但相比前两次,这一次上机实践的效率远低于上两次,因为在实践的时候被第二题难住了,就没有去思考第三题,导致进度大大落后。既然如此,那么本篇博客就以第三题为例来讲解贪心算法吧。
一、实践题目
给定k 个排好序的序列, 用 2 路合并算法将这k 个序列合并成一个序列。 假设所采用的 2 路合并算法合并 2 个长度分别为m和n的序列需要m+n-1 次比较。试设 计一个算法确定合并这个序列的最优合并顺序,使所需的总比较次数最少。 为了进行比较,还需要确定合并这个序列的最差合并顺序,使所需的总比较次数最多。
输入格式:
第一行有 1 个正整数k,表示有 k个待合并序列。 第二行有 k个正整数,表示 k个待合并序列的长度。
输出格式:
输出最多比较次数和最少比较次数。
输入样例:
在这里给出一组输入。例如:
4 5 12 11 2
输出样例:
在这里给出相应的输出。例如:
78 52
二、问题描述
要解决本题,首先要理解题目中提及的 2 路合并算法,其意思是将含m和n个数字的数组合并,则需要m + n - 1 次比较,得到的是一个含m + n个数字的新数组。给出了k个数组,那么就要进行k - 1次2路合并,例如题目的输入样例给出了4个数组,那么就需要进行3次合并。而又因为各数组的数字数目不同,则合并时进行的比较次数也不同,因此需要找到一个合适的合并顺序使得总比较次数最少和最多。所以,本题的解题关键是:找到合并数组的顺序!
三、算法描述
首先列出我解题的代码
1 #include <iostream> 2 #include <queue> 3 using namespace std; 4 5 int main() 6 { 7 int n; //需二路排序的序列数目 8 cin >> n; 9 const int num = n; 10 int array[num]; //各序列的数字数目 11 12 for (int i = 0; i < n; i++) 13 { 14 cin >> array[i]; 15 } 16 17 priority_queue <int, vector<int>, greater<int> > tempQueue; //定义小顶堆 18 priority_queue <int> largeQueue; //定义大顶堆 19 20 for (int i = 0; i < n; i++) //存数组中的数字入堆 21 { 22 tempQueue.push(array[i]); 23 largeQueue.push(array[i]); 24 } 25 26 27 28 int min = 0; //min表示最少比较总次数 29 int max = 0; //max表示最多比较总次数 30 31 while (true) //求最少次数 32 { 33 int sum = 0; //sum表示二路合并后的值 34 int temp = tempQueue.top(); //堆顶元素存入temp中 35 tempQueue.pop(); 36 if (!tempQueue.empty()) 37 { 38 sum = temp + tempQueue.top(); 39 min += (temp + tempQueue.top() - 1); 40 tempQueue.pop(); 41 tempQueue.push(sum); 42 } 43 else 44 { 45 break; 46 } 47 } 48 49 while (true) //求最多次数 50 { 51 int sum = 0; //sum表示二路合并后的值 52 int temp = largeQueue.top(); //堆顶元素存入temp中 53 largeQueue.pop(); 54 if (!largeQueue.empty()) 55 { 56 sum = temp + largeQueue.top(); 57 max += (temp + largeQueue.top() - 1); 58 largeQueue.pop(); 59 largeQueue.push(sum); 60 } 61 else 62 { 63 break; 64 } 65 } 66 67 cout << max << " " << min; 68 return 0; 69 }
题目要求是求出最少和最多的总比较次数,结合我们求哈夫曼树的经验,那么如果想得到最少的总比较次数,那么就需要选出给定的最短的数组,先进行二路合并,得到新数组,再比较新数组和其余数组,再次挑出2个最短的数组,直到只剩下一个数组即为最少总比较次数数组。相反,若想得到比较次数最多,那么我们按照相同的思想,只需要把挑出两个最短的改为挑出两个最长的,直到只剩下一个数组即为最多总比较次数数组。恰好这个解题思路很适合使用<queue>头文件中内置的数据结构——优先队列priority_queue
priority_queue <int> (变量名) //默认为最小的元素为堆顶
当我们让优先队列的头元素出队列时,优先队列是自动排好序的,即保持最小的元素保持在队头,这样我们每次只需要取出队列头元素,并出队列,再取出第二个队列头,进行二路合并,即能达成我们保持合并两个最短队列的思想。
下面用反证法来证明最优子结构策略:(由于求出最少比较次数与最多比较次数类似,所以这里以全球除最少比较次数为例)
设a 与 b 数组是给出的最短数组,其数目分别为m 与 n,合并两个数组的比较次数为 m + n - 1, 得出的新数组的长度为 m + n。 若还有两个数组c 与 d , 他们的数目分别为i 与 j,合并次数为 i + j - 1, 则得出的数组长度为i + j。 而整个问题的最优解还需要在长度为i + j的数组与剩余的数组(设长度为k)二路合并,但由于数组a 与b是原有的最短的数组, 即 m + n - 1 < i + j - 1,且 m + n + k < i + j + k,则导致求出的最优解不是整个问题的最优解,与求出总最少比较次数矛盾。(大问题的最优解包含子问题的最优解)
用反证法来证明贪心选择:
先挑选出原有数组的最短的两个数组(长度为 n 与 m),比较次数为C1 = n + m - 1,合并后的数组长度为(n + m);再挑选出剩下的数组和长度为n + m的数组中最短的两个进行二路合并, 若第二次合并是长度为i的数组与第一次合并得出的数组合并,则比较次数C2 = n + m + i - 2。若有两个数组合并后的长度为 k < n + m,合并它所比较的次数为 q < n + m - 1,第二次合并的比较次数为 q + i,那么总比较次数包含 q + i,但由于不存在该两个数组,得出矛盾。所以本问题的最优解是通过先求出n + m - 1 再得出 n + m + i - 2,再一步步就能得出大问题的最优解。(整体最优解可以通过一系列局部最优的选择)
四、算法时间及空间复杂度分析
对于时间复杂度: 本算法的核心部分为求最少比较次数和最多比较次数的两个while循环,while循环的结束取决于优先队列的长度。根据给进队的循环,则本算法的时间复杂度为O(n)。
对于空间复杂度:由于用到了两个优先队列来分别表示大顶堆和小顶堆(用到了tempQueue和largeQueue),所以空间复杂度为T(2n) = O(n)。
五、心得体会
通过本次实践,我感触最深的就是其实贪心算法的策略我们一直都在使用,贪心算法并不是什么特别难懂的算法,只需要我们找到贪心选择的策略,再证明它是否可行,证明可行后使用贪心算法能够有效又高效率地解决问题。这次结对编程效果没有上一次的好,原因在于我们的思维限制在了一点上,其实可以两个人分别想出不同的贪心策略再检验是否可行,当一种策略不可行就马上换下一种。还有不足就是我还不能很好地证明自己的贪心选择策略,不能有理有据地向大家展示出来,导致自己做题的时候觉得可行,但其实自己也不太清楚其原因。