第8章 高效算法设计(感谢作者)
这一章学完了,总感觉做题时找不到感觉,或是没思路,这种题也算是一种难题了吧,以后的分析要加强,所以要在下一章多写一些习题的题解,并且能够多思考,多读
之前的内容,下面就是第8章的内容的整理以及个人思路感受。
高效算法需要我们对特定的问题进行描述,这样的算法才更加符合问题,所以才会更高效,所以这需要我们更加认真思考问题,挖掘问题的本质,找出合适的解法。
解法合不合适,需要给解法的速度或者效率下一个定义,下面引出渐进时间复杂度的概念。
最大连续和:给出一个长度为n的序列,求最大连续和。(题意简述)
下面是枚举的解法:
tot = 0; best = A[1]; for (int i = 1; i <= n; ++i) for (int j = i; j <= n; ++j) { int sum = 0; for (int k = i; k <= j; ++k) { sum += A[k]; tot++; } if (sum > best) best = sum; }
我们可以通过暴力的方式来求出最大连续和,这里引入的tot是计算基本操作的数量(sum += A[k])这里引入tot的好处就是对于任何计算机执行这段程序时当输入的n
数量相同时,tot的值总是一样的,这样我们分析算法效率时不在受计算机硬件配置的影响,能够直接衡量算法本身的优劣程度。
我们将加法(sum+=A[k])的操作次数与n的规模记为T(n),当n很大时平方项和一次项对整个多项式的影响不大,可以用T(n) = O(n^3)来表示(这里说的十分简略,
具体的计算过程与定义建议参考书籍或者网络),这样我们得到了渐进时间复杂度为O(n^3),这样估算会忽略无关紧要的(或者影响不大)的二次项、一次项、常数
这样会与实际有一定的误差,但是尽管这样,我们也抓住了主体部分,所以这样的分析仍然是有用的。
下面就是上界分析,就是将算法的各个部分都用最坏的情况考虑,这样得到的时间复杂度为T(n) = O(n^3)三重循环,最坏情况都是n,这样的上界有可能会让我们估计
大了(做题时会有感受,会让我们不敢去编写),对于上界的分析,我认为越与实际情况相同越好,称之为“紧”的上界,要知道,时间复杂度的分析只能是辅助,
拿不准的,我们编一下就知道了。
下面给出优化的最大连续和:
S[0] = 0; for (int i = 1; i <= n; ++i) S[i] = S[i-1] + A[i]; for (int i = 1; i <= n; ++i) for (int j = i; j <= n; ++j) best = max(best, S[j]-S[i-1]);
上界时间复杂度为O(n^2)
分治法:
通过分支法来解决最大连续和的问题,一般将分支法分为三个步骤:划分问题、递归求解、合并问题
将序列分为左右序列(平均分为两半),然后递归求解就是求出左右序列的最大字段和,合并问题就是将左右序列的最大字段和与原序列进行比较,找出最优解。
int maxsum(int A[], int x, int y) { if (y-x == 1) return A[x]; int m = x + (y-x) / 2; int maxs = max(maxsum(A, x, m), maxsum(A, m, y)); int v, L, R; v = 0; L = A[m-1]; for (int i = m-1; i >= x; --i) L = max(L, v+=A[i]); v = 0; R = A[m]; for (int i = m; i < y; ++i) R = max(R, v+=A[i]); return max(maxs, L+R); }
我们通过分析上面的代码,得出T(n) = 2*T(n/2) + n, T(1) = 1的递归方程,通过计算,我们得出了T(n) =O(nlogn)的结论,这样的算法比上面的更优。
其实还有O(n)的算法,在这里我就不说了,感受一下不同算法的效率就行了。
下面给出了不同算法的运算规模:
n! 2^n n^3 n^2 nlogn n
11 26 464 10000 4.5*10^6 10^8
将前两个运算量的算法称为指数时间算法,最后面的称为多项式时间算法,也称为有效算法,往往后面的算法能够解决算法竞赛的问题,根据不同的规模考虑算法的
构造,当然对于n<=300时 n^3算法可以 n^2的算法更可以。
下面说一下归并排序:
归并排序属于分治,所以可以用分治的方法求解:
划分问题:把序列分成元素个数尽量相等的两半
递归求解:把两半元素分别排序
合并问题:把两个有序表合并成一个
下面是代码:
void merge_sort(int* A, int x, int y, int* T) { if (y-x > 1) { int m = x + (y-x) / 2; int p = x, q = m, i = x; merge_sort(A, x, m, T); merge_sort(A, m, y, T); while (p < m || q < y) { if (q >= y || (p < m && A[p] <= A[q]) T[i++] = A[p++]; else T[i++] = A[q++]; } for (int i = x; i < y; ++i) A[i] = T[i]; } }
通过分治可以解决逆序对等问题,这里就不说了。
快速排序也不说了。
下面说一说二分查找,下面时代码:
int bsearch(int* A, int x, int y, int v) { int m ; while (x < y) { m = x + (y-x) / 2; if (A[m] == v) return m; else if (A[m] > v) y = m; else x = m+1; } return -1; }
通过修改,我们可以得到lower_bound与upper_bound,将这两个求出就可以查找一个数字再序列中的完整区间了,下面是代码:
int lower_bound(int* A, int x, int y, int v) { int m; while (x < y) { m = x + (y-x) / 2; if (A[m] >= v) y = m; else x = m+1; } return x; } int upper_bound(int* A, int x, int y, int v) { int m; while (x < y) { int m = x + (y-x) / 2; if (A[m] <= v) x = m+1; else y = m; } }
当然,我们可以使用#include <algorithm>中的lower_bound与upper_bound了。
下面说一下贪心法:
其中有背包相关问题与区间相关问题:
背包相关问题:
最优装载问题:给出n个物体,第i个物体的重量为wi。选择尽量多的物体,使得总重量不超过C。
思路:从小的开始选。
部分背包问题:有n个物体,第i个物体的重量为wi,价值为vi。再总重量不超过C的情况下让总价值尽量高。每个物体都可以只取走一部分,价值和重量按比例计算。
思路:选比值大的然后选比值小的。
乘船问题:有n个人,第i个人的重量为wi。每艘船的最大载重量为C,且最多只能乘两个人。用最少的船装载所有人。
思路:小对应大一定不亏......
区间相关问题:
选择不相交的区间:数轴上有n个开区间(ai, bi)。选择尽量多个区间,使得这些区间两辆没有公共点。
思路:先对b进行排序,b小的放在前面,b相同时对a进行排序,a大的放在前面,通过思考,当一个区间包含另一个区间时大的区间一定不选,
所以我们得到了b<=b<=b a<=a<=a这种顺序,如果两个区间相交时,我们一定要选择前面的区间,这样一定会更优(想一想,为什么?)
区间选点问题:数轴上有n个闭区间[ai, bi]。去尽量少的点,使得每个区间内都至少有一个点(不同区间内含的点可以是同一个)。
思路:将所有区间按照选择不相交的区间进行排序,然后取最后一个点就行了(最后那个点?大家可以去考虑)
区间覆盖问题:数轴上有n个闭区间[ai, bi],选择尽量少的区间覆盖一条指定线段[s, t]。
思路:排序与上面不同,我们先按照a从小到大进行排序,然后选择最长的区间,选完后,将前面覆盖的地方忽略,假设所有区间以最长区间结尾处为起点,继续向
前选择。
下面是Huffman编码,主要解决的问题是最优编码问题(这里的题意以及背景就不说了)说一说贪心的部分。
最优编码组成的树一定是二叉树,如果不是二叉树,那么将那个只有一个叶子的结点删掉,让那个叶子代替不就更优吗?
Huffman算法:将最小频率的两个字符合并,然后放入集合中,然后继续在集合里找最小的字符。
算法正确性的证明(不正式):
假设我们知道了一个二叉树,那么最小频率的两个字符一定在最深处,如果不在最深处,我们将它们放在最深处一定更优。
将这两个字符合并看成一个后与剩下的字符组成的二叉树一定是最优的,肯定是最优的,我们可以推导出来。
推导:设T’的编码长度为L,其中字符{x, y}的深度为h,则把字符{x, y}拆成两个后,长度变为
L-(f(x)+f(y))*h+(f(x)+f(y))*(h+1) = L + f(x) + f(y),从推导式中可以看出之前树最优解是由比之前树小的树的最优解得到的(说的不清楚,建议看看原书),
这样我们可以想出取两个最小然后保证剩下的最优,对于剩下的最优我们仍然采用取两个最小然后取剩下的最优的方法,这样与huffman算法的过程相同,
所以证明了huffman算法的正确性。
下面就是算法中的一些其他思路与例题了,对于例题,题意就不说明了,说一说思路:
构造法:直接构造解,没什么可说的,这需要长期的练习与智慧(反正我可能没有)
UVa120煎饼:选择排序(想一想怎么排序)
UVa1605联合国大楼:两层就够了,第一层第i行全为i,第二层第j列全为j,然后就能出答案(想法非常的玄学)。
中途相遇法:类似双向广度优先搜索,从两个不同的发向来解决问题。
UVa1152和为0的4个值:可以4重枚举,但是我们可以将前两个枚举出来,后两个枚举出来,然后去求解。
问题分解:将问题分解为子问题,然后去求解。
UVa11134传说中的车:这道题我写过博客,去前面看看解法吧。
等价转换:也是算法,也是思维,比较灵活,慢慢去学。
UVa11054:将村庄1,2的需求转换成村庄2的需求,然后就可以解决(建议自己思考)。
扫描法:类似有顺序的枚举法,扫描时候需要维护重要的量,然后简化计算。
UVa1606两亲性分子:比较的难,需要用到极角排序。
滑动窗口:顾名思义。
UVa11572唯一的雪花:通过滑动窗口来求解,这道题的想法自然而然。
使用数据结构:不改变主算法从而提高运行效率。
有单调栈?单调队列?大家自行摸索。
UVa1471防线:
这道题是一道好题,所以这道题说的详细一点(建议看原书)
我们可以通过枚举i,j(A[j] < A[i])然后数一数,这样需要O(n^3)
其实可以把数一数的过程省略掉,用f(i)表示第i个元素为开头的最长元素,g(i)表示第j个元素为结尾的最长元素,
这样就变成了O(n^2)。
其实我们可以只枚举i然后二分查找寻找j,如何这样查找呢?我们对于A[j]来说如果存在A[j'] <= A[j] && g(j') > g(j), 那么A[j]
一定不是最优的,所以我们去掉就可以了, 这样我们就能够保证A[j] < A[x] && g(j) < g(x)了,所以这时候查找靠近最靠近A[i]的
A[j], 此时的g(j)一定也是最大的。
这是对于i固定的情况,每次算完i之后,我们要把A[i]插入,我们需要用到STL中的set,去看看A[i]能不能插入,可能前面的
更优,上一段的情况,然后插入后判断后面的是否符合条件,然后删除知道符合条件为止。
数形结合:一种分析方式。
例题略......
例题与习题就不说了,难度很大,比上一章不好做,为了锻炼自己,
第八章动态规划的思路我会尽量呈现出来的,希望能够有所提升。