算法导论第4章习题与思考题(转)
Exercise 4.1-1
返回 A 中最大的一个负数。
Exercise 4.1-2
#include<iostream> #include<vector> #include<string> #include<set> #include<map> #include<unordered_set> #include<unordered_map> #include<algorithm> #include<xfunctional> using namespace std; int find_maxsum(vector<int>& elem){ int res = INT_MIN; int sum = 0; for (int i = 0; i < elem.size(); i++){ sum = elem[i]; for (int j = i + 1; j < elem.size(); j++){ sum += elem[j]; if (sum > res) res = sum; } } return res; } int main(){ int n; cout << "Input the amount of the element:"; cin >> n; vector<int> elem(n,0); for (int i = 0; i < n; i++) cin >> elem[i]; int res = find_maxsum(elem); cout << "The result: " << res << endl; system("pause"); return 0; }
View Code
也可以用前缀和
4.1-3
这道题n0的结果因人而异,我这里的结果是n0=63。利用这种方法虽然可以改善总体的运行速度,但是不能够改变两种方法运行时间相同时输入规模的大小。
#include <limits.h> #define CROSSOVER_POINT 37 // A struct to represent the tuple typedef struct { unsigned left; unsigned right; int sum; } max_subarray; // The brute force approach max_subarray find_maximum_subarray_brute(int A[], unsigned low, unsigned high) { max_subarray result = {0, 0, INT_MIN}; for (int i = low; i < high; i++) { int current_sum = 0; for (int j = i; j < high; j++) { current_sum += A[j]; if (result.sum < current_sum) { result.left = i; result.right = j + 1; result.sum = current_sum; } } } return result; } // The divide-and-conquer solution max_subarray find_max_crossing_subarray(int A[], unsigned low, unsigned mid, unsigned high) { max_subarray result = {-1, -1, 0}; int sum = 0, left_sum = INT_MIN, right_sum = INT_MIN; for (int i = mid - 1; i >= (int) low; i--) { sum += A[i]; if (sum > left_sum) { left_sum = sum; result.left = i; } } sum = 0; for (int j = mid; j < high; j++) { sum += A[j]; if (sum > right_sum) { right_sum = sum; result.right = j + 1; } } result.sum = left_sum + right_sum; return result; } max_subarray find_maximum_subarray(int A[], unsigned low, unsigned high) { if (high == low + 1) { max_subarray result = {low, high, A[low]}; return result; } else { unsigned mid = (low + high) / 2; max_subarray left = find_maximum_subarray(A, low, mid); max_subarray right = find_maximum_subarray(A, mid, high); max_subarray cross = find_max_crossing_subarray(A, low, mid, high); if (left.sum >= right.sum && left.sum >= cross.sum) { return left; } else if (right.sum >= left.sum && right.sum >= cross.sum) { return right; } else { return cross; } } } // The mixed algorithm max_subarray find_maximum_subarray_mixed(int A[], unsigned low, unsigned high) { if (high - low < CROSSOVER_POINT) { return find_maximum_subarray_brute(A, low, high); } else { unsigned mid = (low + high) / 2; max_subarray left = find_maximum_subarray_mixed(A, low, mid); max_subarray right = find_maximum_subarray_mixed(A, mid, high); max_subarray cross = find_max_crossing_subarray(A, low, mid, high); if (left.sum >= right.sum && left.sum >= cross.sum) { return left; } else if (right.sum >= left.sum && right.sum >= cross.sum) { return right; } else { return cross; } } }
View Code
4.1-4
在算法的最后判断最大子数组的总和是否小于零,如果小于零则返回空子数组(empty subarray)。
#include <limits.h> typedef struct { unsigned left; unsigned right; int sum; } max_subarray; max_subarray find_max_crossing_subarray(int A[], unsigned low, unsigned mid, unsigned high) { max_subarray result = {mid + 1, mid, 0}; int sum = 0, left_sum = INT_MIN, right_sum = INT_MIN; for (int i = mid - 1; i >= (int) low; i--) { sum += A[i]; if (sum > left_sum) { left_sum = sum; result.left = i; } } sum = 0; for (int j = mid; j < high; j++) { sum += A[j]; if (sum > right_sum) { right_sum = sum; result.right = j + 1; } } if (left_sum + right_sum < 0) { max_subarray empty = { mid, mid, 0 }; return empty; } else { result.sum = left_sum + right_sum; return result; } } max_subarray find_maximum_subarray(int A[], unsigned low, unsigned high) { if (high == low + 1) { if (A[low] < 0) { max_subarray empty = {low, low, 0}; return empty; } else { max_subarray result = {low, high, A[low]}; return result; } } else { unsigned mid = (low + high) / 2; max_subarray left = find_maximum_subarray(A, low, mid); max_subarray right = find_maximum_subarray(A, mid, high); max_subarray cross = find_max_crossing_subarray(A, low, mid, high); if (left.sum >= right.sum && left.sum >= cross.sum) { return left; } else if (right.sum >= left.sum && right.sum >= cross.sum) { return right; } else { return cross; } } }
View Code
Exercise 4.1-5
(挺有意思的)
思想: A[1..j+1] 的最大子数组,可以由 A[1..j] 的最大子数组和 A[i..j+1](1≤i≤j+1) 子数组确定。(题目给的提示也许有点隐晦。直白点说就是:在求 A[1..j+1] 的最大子数组时,要求两个量:A[1..j] 的最大子数组,和以 j+1 结尾的最大子数组。两者作比较,得出结果。
函数find_maximum_subarray02,可以很好的说明这一切。current结构体中保存着最大的A[i..j+1] ;result结构体中保存着A[1..j] 的最大子数组。
typedef struct { unsigned left; unsigned right; int sum; } max_subarray; max_subarray find_maximum_subarray(int A[], unsigned low, unsigned high) { max_subarray suffixes[high - low]; suffixes[0].left = low; suffixes[0].right = low + 1; suffixes[0].sum = A[low]; for (int i = low + 1; i < high; i++) { if (suffixes[i - 1].sum < 0) { suffixes[i].left = i; suffixes[i].right = i + 1; suffixes[i].sum = A[i]; } else { max_subarray *previous = &suffixes[i - 1]; suffixes[i].left = previous->left; suffixes[i].right = i + 1; suffixes[i].sum = previous->sum + A[i]; } } max_subarray *max = &suffixes[0]; for (int i = low + 1; i < high; i++) { if (max->sum < suffixes[i].sum) { max = &suffixes[i]; } } return *max; } max_subarray find_maximum_subarray02(int A[], unsigned low, unsigned high) { max_subarray result = {0, 0, INT_MIN}; max_subarray current = {low, low + 1, A[p]}; for (int i = low + 1; i < high; i++) { if (current.sum <= 0) { current.sum = A[i]; current.left = i; current.right = i + 1; } else { current.sum += A[i]; current.right = i + 1; } if (current.sum > result.sum) { result.sum = current.sum; result.left = current.left; result.right = result.right; } return result; }
View Code
总结:
1、由分治法引发的
这一章提出了一个在现在各大IT公司在今天依然很喜欢考的一道笔试面试题:
求连续子数组的最大和 题目描述: 输入一个整形数组,数组里有正数也有负数。 数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和。 求所有子数组的和的最大值。要求时间复杂度为O(n)。 例如输入的数组为1, -2, 3, 10, -4, 7, 2, -5,和最大的子数组为3, 10, -4, 7, 2, 因此输出为该子数组的和18。
要求时间复杂度是O(n),我们暂且不管这个,由浅入深地分析一下这道题,时间复杂度从O(n^2)->O(nlgn)->O(n)。
1)、第一,大部分人想到的肯定是暴力法,两个for循环,时间复杂度自然是O(n^2),如下:
1 /************************************************************************/ 2 /* 暴力法 3 /************************************************************************/ 4 void MaxSubArraySum_Force(int arr[], vector<int> &subarr, int len) 5 { 6 if (len == 0) 7 return; 8 int nMax = INT_MIN; 9 int low = 0, high = 0; 10 for (int i = 0; i < len; i ++) { 11 int nSum = 0; 12 for (int j = i; j < len; j ++) { 13 nSum += arr[j]; 14 if (nSum > nMax) { 15 nMax = nSum; 16 low = i; 17 high = j; 18 } 19 } 20 } 21 for (int i = low; i <= high; i ++) { 22 subarr.push_back(arr[i]); 23 } 24 }
2)、第二,看了《算法导论》,你可能会想到分治法,看完之后你肯定会为该分治思想而惊叹,尤其是“横跨中点”的计算思想。简单说下该分治思想,其实很简单,最大和子数组无非有三种情况:左边,右边,中间。
时间复杂度分析:
根据分治的思想,时间复杂度的计算包括三部分:两边+中间。由于分治的依托就是递归,我们可以写出下面的递推式(和合并排序的递推式是一样的):
其中的Θ(n)为处理最大和在数组中间时的情况,经过计算(怎么计算的,请看本节第二部分:解分治法的三种方法),可以得到分治法的时间复杂度为Θ(nlgn)。代码如下:
1 /************************************************************************/ 2 /* 分治法 3 最大和子数组有三种情况: 4 1)A[1...mid] 5 2)A[mid+1...N] 6 3)A[i..mid..j] 7 /************************************************************************/ 8 //find max crossing left and right 9 int Find_Max_Crossing_Subarray(int arr[], int low, int mid, int high) 10 { 11 const int infinite = -9999; 12 int left_sum = infinite; 13 int right_sum = infinite; 14 15 int max_left = -1, max_right = -1; 16 17 int sum = 0; //from mid to left; 18 for (int i = mid; i >= low; i --) { 19 sum += arr[i]; 20 if (sum > left_sum) { 21 left_sum = sum; 22 max_left = i; 23 } 24 } 25 sum = 0; //from mid to right 26 for (int j = mid + 1; j <= high; j ++) { 27 sum += arr[j]; 28 if (sum > right_sum) { 29 right_sum = sum; 30 max_right = j; 31 } 32 } 33 return (left_sum + right_sum); 34 } 35 36 int Find_Maximum_Subarray(int arr[], int low, int high) 37 { 38 if (high == low) //only one element; 39 return arr[low]; 40 else { 41 int mid = (low + high)/2; 42 int leftSum = Find_Maximum_Subarray(arr, low, mid); 43 int rightSum = Find_Maximum_Subarray(arr, mid+1, high); 44 int crossSum = Find_Max_Crossing_Subarray(arr, low, mid, high); 45 46 if (leftSum >= rightSum && leftSum >= crossSum) 47 return leftSum; 48 else if (rightSum >= leftSum && rightSum >= crossSum) 49 return rightSum; 50 else 51 return crossSum; 52 } 53 }
3)、第三,看了《算法导论》习题4.1-5,你又有了另外一种思路:数组A[1…j+1]的最大和子数组,有两种情况:a) A[1…j]的最大和子数组; b) 某个A[i…j+1]的最大和子数组,假设你现在不知道动态规划,这种方法也许会让你眼前一亮,确实是这么回事,恩,看代码吧。时间复杂度不用想,肯定是O(n)。和暴力法比起来,我们的改动仅仅是用一个指针指向某个使和小于零的子数组的左区间(当和小于零时,区间向左减小,当和在增加时,区间向右增大)。因此,我们给这种方法取个名字叫区间法。
1 /************************************************************************/ 2 /* 区间法 3 求A[1...j+1]的最大和子数组,有两种情况: 4 1)A[1...j]的最大和子数组 5 2)某个A[i...j+1]的最大和子数组 6 /************************************************************************/ 7 void MaxSubArraySum_Greedy(int arr[], vector<int> &subarr, int len) 8 { 9 if (len == 0) 10 return; 11 int nMax = INT_MIN; 12 int low = 0, high = 0; 13 int cur = 0; //一个指针更新子数组的左区间 14 int nSum = 0; 15 for (int i = 0; i < len; i ++) { 16 nSum += arr[i]; 17 if (nSum > nMax) { 18 nMax = nSum; 19 low = cur; 20 high = i; 21 } 22 if (nSum < 0) { 23 cur += 1; 24 nSum = 0; 25 } 26 } 27 for (int i = low; i <= high; i ++) 28 subarr.push_back(arr[i]); 29 }
第四,你可能在平常的学习过程中,听说过该问题最经典的解是用动态规划来解,等你学习之后,你发现确实是这样,然后你又一次为之惊叹。动态规划算法最主要的是寻找递推关系式,大概思想是这样的:数组A[1…j+1]的最大和:要么是A[1…j]+A[j+1]的最大和,要么是A[j+1],据此,可以很容易写出其递推式为:
sum[i+1] = Max(sum[i] + A[i+1], A[i+1])
化简之后,其实就是比较sum[i] ?> 0(sum[i] + A[i+1] ?> A[i+1]),由此,就很容易写出代码如下:
1 /************************************************************************/ 2 /* 动态规划(对应着上面的贪心法看,略有不同) 3 求A[1...j+1]的最大和子数组,有两种情况: 4 1)A[1...j]+A[j+1]的最大和子数组 5 2)A[j+1] 6 dp递推式: 7 sum[j+1] = max(sum[j] + A[j+1], A[j+1]) 8 /************************************************************************/ 9 int MaxSubArraySum_dp(int arr[], int len) 10 { 11 if (len <= 0) 12 exit(-1); 13 int nMax = INT_MIN; 14 int sum = 0; 15 16 for (int i = 0; i < len; i ++) { 17 if (sum >= 0) 18 sum += arr[i]; 19 else 20 sum = arr[i]; 21 if (sum > nMax) 22 nMax = sum; 23 } 24 return nMax; 25 }
可以看出,区间法和动态规划有几分相似,我觉得两种方法的出发点和终点都是一致的,只不过过程不同。动态规划严格遵循递推式,而区间法是寻找使区间变化的标识,即和是否小于零,而这个标识正是动态规划采用的。
十个利用矩阵乘法解决的经典题目
好像目前还没有这方面题目的总结。这几天连续看到四个问这类题目的人,今天在这里简单写一下。这里我们不介绍其它有关矩阵的知识,只介绍矩阵乘法和相关性质。
不要以为数学中的矩阵也是黑色屏幕上不断变化的绿色字符。在数学中,一个矩阵说穿了就是一个二维数组。一个n行m列的矩阵可以乘以一个m行p列的矩阵,得到的结果是一个n行p列的矩阵,其中的第i行第j列位置上的数等于前一个矩阵第i行上的m个数与后一个矩阵第j列上的m个数对应相乘后所有m个乘积的和。比如,下面的算式表示一个2行2列的矩阵乘以2行3列的矩阵,其结果是一个2行3列的矩阵。其中,结果的那个4等于2*2+0*1:
下面的算式则是一个1 x 3的矩阵乘以3 x 2的矩阵,得到一个1 x 2的矩阵:
矩阵乘法的两个重要性质:一,矩阵乘法不满足交换律;二,矩阵乘法满足结合律。为什么矩阵乘法不满足交换律呢?废话,交换过来后两个矩阵有可能根本不能相乘。为什么它又满足结合律呢?仔细想想你会发现这也是废话。假设你有三个矩阵A、B、C,那么(AB)C和A(BC)的结果的第i行第j列上的数都等于所有A(ik)*B(kl)*C(lj)的和(枚举所有的k和l)。
经典题目1 给定n个点,m个操作,构造O(m+n)的算法输出m个操作后各点的位置。操作有平移、缩放、翻转和旋转
这里的操作是对所有点同时进行的。其中翻转是以坐标轴为对称轴进行翻转(两种情况),旋转则以原点为中心。如果对每个点分别进行模拟,那么m个操作总共耗时O(mn)。利用矩阵乘法可以在O(m)的时间里把所有操作合并为一个矩阵,然后每个点与该矩阵相乘即可直接得出最终该点的位置,总共耗时O(m+n)。假设初始时某个点的坐标为x和y,下面5个矩阵可以分别对其进行平移、旋转、翻转和旋转操作。预先把所有m个操作所对应的矩阵全部乘起来,再乘以(x,y,1),即可一步得出最终点的位置。
经典题目2 给定矩阵A,请快速计算出A^n(n个A相乘)的结果,输出的每个数都mod p。
由于矩阵乘法具有结合律,因此A^4 = A * A * A * A = (A*A) * (A*A) = A^2 * A^2。我们可以得到这样的结论:当n为偶数时,A^n = A^(n/2) * A^(n/2);当n为奇数时,A^n = A^(n/2) * A^(n/2) * A (其中n/2取整)。这就告诉我们,计算A^n也可以使用二分快速求幂的方法。例如,为了算出A^25的值,我们只需要递归地计算出A^12、A^6、A^3的值即可。根据这里的一些结果,我们可以在计算过程中不断取模,避免高精度运算。
经典题目3 POJ3233 (感谢rmq)
题目大意:给定矩阵A,求A + A^2 + A^3 + … + A^k的结果(两个矩阵相加就是对应位置分别相加)。输出的数据mod m。k<=10^9。
这道题两次二分,相当经典。首先我们知道,A^i可以二分求出。然后我们需要对整个题目的数据规模k进行二分。比如,当k=6时,有:
A + A^2 + A^3 + A^4 + A^5 + A^6 =(A + A^2 + A^3) + A^3*(A + A^2 + A^3)
应用这个式子后,规模k减小了一半。我们二分求出A^3后再递归地计算A + A^2 + A^3,即可得到原问题的答案。
经典题目4 VOJ1049
题目大意:顺次给出m个置换,反复使用这m个置换对初始序列进行操作,问k次置换后的序列。m<=10, k<2^31。
首先将这m个置换“合并”起来(算出这m个置换的乘积),然后接下来我们需要执行这个置换k/m次(取整,若有余数则剩下几步模拟即可)。注意任意一个置换都可以表示成矩阵的形式。例如,将1 2 3 4置换为3 1 2 4,相当于下面的矩阵乘法:
置换k/m次就相当于在前面乘以k/m个这样的矩阵。我们可以二分计算出该矩阵的k/m次方,再乘以初始序列即可。做出来了别忙着高兴,得意之时就是你灭亡之日,别忘了最后可能还有几个置换需要模拟。
经典题目5 《算法艺术与信息学竞赛》207页(2.1代数方法和模型,[例题5]细菌,版次不同可能页码有偏差)
大家自己去看看吧,书上讲得很详细。解题方法和上一题类似,都是用矩阵来表示操作,然后二分求最终状态。
经典题目6 给定n和p,求第n个Fibonacci数mod p的值,n不超过2^31
根据前面的一些思路,现在我们需要构造一个2 x 2的矩阵,使得它乘以(a,b)得到的结果是(b,a+b)。每多乘一次这个矩阵,这两个数就会多迭代一次。那么,我们把这个2 x 2的矩阵自乘n次,再乘以(0,1)就可以得到第n个Fibonacci数了。不用多想,这个2 x 2的矩阵很容易构造出来:
经典题目7 VOJ1067
我们可以用上面的方法二分求出任何一个线性递推式的第n项,其对应矩阵的构造方法为:在右上角的(n-1)*(n-1)的小矩阵中的主对角线上填1,矩阵第n行填对应的系数,其它地方都填0。例如,我们可以用下面的矩阵乘法来二分计算f(n) = 4f(n-1) – 3f(n-2) + 2f(n-4)的第k项:
利用矩阵乘法求解线性递推关系的题目我能编出一卡车来。这里给出的例题是系数全为1的情况。
经典题目8 给定一个有向图,问从A点恰好走k步(允许重复经过边)到达B点的方案数mod p的值
把给定的图转为邻接矩阵,即A(i,j)=1当且仅当存在一条边i->j。令C=A*A,那么C(i,j)=ΣA(i,k)*A(k,j),实际上就等于从点i到点j恰好经过2条边的路径数(枚举k为中转点)。类似地,C*A的第i行第j列就表示从i到j经过3条边的路径数。同理,如果要求经过k步的路径数,我们只需要二分求出A^k即可。
经典题目9 用1 x 2的多米诺骨牌填满M x N的矩形有多少种方案,M<=5,N<2^31,输出答案mod p的结果
我们以M=3为例进行讲解。假设我们把这个矩形横着放在电脑屏幕上,从右往左一列一列地进行填充。其中前n-2列已经填满了,第n-1列参差不齐。现在我们要做的事情是把第n-1列也填满,将状态转