1.实验目的:
比较同一问题,采用不同策略设计不同算法,分析和比较算法的性能。
2.实验内容:
自学最长子串和的三种算法,总结分析编程实现简单算法、分治法和动态规划算法的理论复杂度,编程实现这些算法。
3.实验要求:
并对一组数据测试实际运行时间,算法实际运行时间检测实际运行时间与理论分析是否一致。
□ \square □ 基础性实验 □ \square □ 综合性实验 ⊠ \boxtimes ⊠ 设计性实验
一、问题分析(模型、算法设计和正确性证明等)
1、简单算法:
模型:用 a [ n ] a[n] a[n]存储序列。用 s u m sum sum和 t e m p temp temp存所有可能的值即可。
算法设计:对 ∀ i ∈ [ 1 , n ] , j ∈ ( i , n ] \forall i \in [1,n], j \in (i,n] ∀i∈[1,n],j∈(i,n],计算所有的 s u m = ∑ k = i k = j ( a [ k ] ) sum = \sum_{k=i}^{k=j}(a[k]) sum=∑k=ik=j(a[k]),再求出最小值即可。
正确性证明:由于遍历了所有情况求出最值,因此必然正确。
2、分治法:
模型:采用递归的方式对问题进行求解,分别用三个变量存储三种情况下的结果即可。
算法设计:规模为 n n n的问题求解 M a x S u b S u m ( 1 , n ) MaxSubSum(1,n) MaxSubSum(1,n)时,将该串分为左右两个子串,则最长子串和一定满足下列三种情况中的一个:
- 最长子串全部位于左子串,即 M a x S u b S u m ( 1 , n ) = M a x S u b S u m ( 1 , n 2 ) MaxSubSum(1,n) = MaxSubSum(1, \frac{n}{2}) MaxSubSum(1,n)=MaxSubSum(1,2n)。
- 最长子串全部位于右子串,即 M a x S u b S u m ( 1 , n ) = M a x S u b S u m ( n 2 + 1 , n ) MaxSubSum(1,n)=MaxSubSum(\frac{n}{2}+1, n) MaxSubSum(1,n)=MaxSubSum(2n+1,n)。
- 最长子串在左右子串中均有分布。在这种情况下,显然 a [ n 2 ] a[\frac{n}{2}] a[2n]和 a [ n 2 + 1 ] a[\frac{n}{2}+1] a[2n+1]均属于最长子串,也就是说问题变成了确定 i , j i,j i,j其中 i ∈ [ 1 , n 2 ] , j ∈ [ n 2 + 1 , n ] i \in[1,\frac{n}{2}], j \in [\frac{n}{2}+1,n] i∈[1,2n],j∈[2n+1,n],使得 i , j i,j i,j分别是最长子串的开始和结束。具体确定方法采用遍历法。
其中,递归出口为序列长为 1 1 1,此时最长子串和为 m a x ( a [ i ] , 0 ) max(a[i],0) max(a[i],0)。
正确性证明:由于每一个主要问题有仅有三种情况,且每个子问题的最小规模必然为0或者当前值,因此正确。
3、动态规划法:
模型:使用 d p [ i ] dp[i] dp[i]表示从初始位置开始一直到 a [ i ] a[i] a[i]结束(包含 a [ i ] a[i] a[i])的最长字段和。
算法设计:按照分治法的情况,若 [ 1 , i ] [1,i] [1,i]分为 [ 1 , i − 1 ] [1,i-1] [1,i−1]和 [ i , i ] [i,i] [i,i]。那么情况一下 d p [ i ] = d p [ i − 1 ] dp[i]=dp[i-1] dp[i]=dp[i−1],情况二下 d p [ i ] = a [ i ] dp[i] = a[i] dp[i]=a[i],情况三下 d p [ i ] = d p [ i − 1 ] + a [ i ] dp[i] = dp[i-1]+a[i] dp[i]=dp[i−1]+a[i]。因此 d p [ i ] = m a x { d p [ i − 1 ] , a [ i ] , d p [ i − 1 ] + a [ i ] } = m a x { d p [ i − 1 ] , 0 } + a [ i ] dp[i] = max\{dp[i-1], a[i], dp[i-1]+a[i] \}=max\{dp[i-1],0 \}+a[i] dp[i]=max{dp[i−1],a[i],dp[i−1]+a[i]}=max{dp[i−1],0}+a[i]。dp表更新完成后对每一个 d p [ i ] dp[i] dp[i]取最大,便是 s u m sum sum。
正确性证明:按照分治法的分析,我们不难得出 d p [ i ] dp[i] dp[i]确实代表从初始位置到到 a [ i ] a[i] a[i]结束(包含 a [ i ] a[i] a[i])的最长字段和。由于最长子序列必然是以 [ 1 , n ] [1,n] [1,n]中的某一个值作为结尾的子段,我们可以确定最终的 s u m sum sum值必然属于我们给出的 n n n个 d p [ i ] dp[i] dp[i]的值,因此对这 n n n个值取最大即可得到最终结果。
二、复杂度分析
1、简单算法:
时间复杂度:
对所有的 i , j i,j i,j共有 n ( n + 1 ) 2 \frac{n(n+1)}{2} 2n(n+1)种可能,对于每种可能计算从 0 0 0到 n n n长度的序列和,因此时间复杂度为 O ( n 3 ) O(n^3) O(n3)。
空间复杂度:
由于只使用了两个变量和一个序列,空间复杂度为 O ( n ) O(n) O(n)。
2、分治法:
时间复杂度:
对原本规模为 n n n的问题,将其分为三个子问题,前两个子问题也就是规模减小一半的同等问题,第三种情况下的求解由遍历的方式给出,由于 i , j i,j i,j只有可能出现在 n n n个位置,因此复杂度为 O ( n ) O(n) O(n)。因此得到递推关系式: T ( n ) = 2 T ( n 2 ) + O ( n ) T(n) = 2T(\frac{n}{2})+O(n) T(n)=2T(2n)+O(n),故时间复杂度为 O ( n log ( n ) ) O(n \log(n)) O(nlog(n))。
空间复杂度:
与简单算法一致,为 O ( n ) O(n) O(n)。
3、动态规划法:
时间复杂度:
实验只需要对dp表进行一次线性更新,再从dp表中线性取最值,因此时间复杂度为 O ( n ) O(n) O(n)。
空间复杂度:
实验采用了长为 n n n的两个数组分别存储数组序列和动态规划表,因此空间复杂度为 O ( n ) O(n) O(n)。
三、程序实现和测试过程和结果(主要描述出现的问题和解决方法)
-
按照文件读取:
由于给出的测试数据体量大、数据多。因此采用文件流的格式读取数据并赋值。考虑到每次仅读取一个文件需要频繁打开可执行程序,因此直接对于每一种方法读取10个文件。实现代码如下:
char filename[][50] = {"test_data/0.in","test_data/1.in","test_data/2.in","test_data/3.in","test_data/4.in"\ ,"test_data/5.in","test_data/6.in","test_data/7.in","test_data/8.in","test_data/9.in"}; fstream fin; for(int in=0; in<10; in++) { fin.open(filename[in]); fin>>n; for(int i=0; i<n; i++) fin>>a[i+1]; ······ fin.close(); }
-
输出运行时间:
直接采用cpp自带函数库实现:
clock_t start = clock(); ······ clock_t stop = clock(); cout<<"time is "<<(stop - start)<<"ms";
-
按照
5.in
给出的运行时间比较:5.in
的规模:10000
。简单算法耗时:
337014ms
。分治算法耗时:104ms
。动态规划耗时:1ms
。由 O ( 10000 ) = 1 O(10000)=1 O(10000)=1, O ( 10000 ∗ l o g ( 10000 ) ) = O ( 14 ∗ 10000 ) = 104 O(10000*log(10000))=O(14*10000)=104 O(10000∗log(10000))=O(14∗10000)=104, O ( 1000 0 3 ) = 337014 O(10000^3)=337014 O(100003)=337014。
很明显可以看出理论分析与实际运行较为一致
1、简单算法:
实现很简单,直接对所有 i , j i,j i,j进行遍历即可:
for(int i=1; i<=n; i++)
for(int j=i; j<=n; j++)
{
temp = 0;
for(int k=i; k<=j; k++) temp+=a[k];
sum = max(sum, temp);
}
2、分治算法
int MaxSubSum(int low, int high)
{
if(low == high) return max(a[low], 0); //递归出口
int mid = (low + high)/2, temp, s1, s2, s3, s3_l=0, s3_r=0;
s1 = MaxSubSum(low, mid); //情况一
s2 = MaxSubSum(mid+1, high); //情况二
for(int i=low; i<=mid; i++) //情况三
{
temp = 0;
for(int j=i; j<=mid; j++) temp+=a[j];
s3_l = max(temp, s3_l);
}
for(int i=mid+1; i<=high; i++)
{
temp = 0;
for(int j=mid+1; j<=i; j++) temp+=a[j];
s3_r = max(temp, s3_r);
}
s3 = s3_l + s3_r;
return max(max(s1, s2),s3); //比较哪一个情况下给出的答案更大
}
3、动态规划
dp[1] = a[1]; //边界赋值
for(int i=2; i<=n; i++) dp[i] = max(dp[i-1], 0)+a[i]; //动态规划
for(int i=1; i<=n; i++) sum = max(sum, dp[i]); //取最大值
实现结果:
测试数据和源码下载链接:https://download.youkuaiyun.com/download/weixin_44609261/12408063