HNU_算法_实验3(2021级)-算法实验2-单调递增最长子序列问题

该题亦为小班题。21级的期中考试题。

一、实验名称

分析题3-2单调递增最长子序列问题:

3-1 设计一个O(n²)时间的算法,找出由n个数组成的序列的最长单调递增子序列。

3-2 将算法分析题3-1中算法的计算时间减至O(nlogn)

(提示:一个长度为i的候选子序列的最后一个元素至少与一个长度为i-1的候选子序列的最后一个元素一样大。

通过指向输入序列中元素的指针来维持候选子序列)。

二、实验目的

        通过上机实验,要求掌握单调递增最长子序列问题的问题描述、算法设计思想、程序设计。

三、实验原理

        解决单调递增最长子序列问题,并计算出程序运行所需要的时间。

四、实验步骤

1.解法一:最长公共子序列

求解最长单调递增子序列 转换为 求解最长公共子序列;

如例子中的数组A{5,6, 7, 1, 2, 8,9},排序后得到数组{1, 2, 5, 6, 7, 8,9},然后找出数组A和A’的最长公共子序列即可。

显然这里最长公共子序列为{5, 6, 7, 8,9},也就是原数组A最长递增子序列。

2.解法二:动态规划

1.初始化长度为n的数组L,L【i】表示以ai结尾的最长递增子序的长度,即L= {1,1,1,1,1,1}

2.遍历i,计算L【i】;

3.对于每个i,需要遍历小于i的j(0<j < i)

4.如果Ai>A[j]则L[i]= max(L[j]+1,L[i]),即 A[i]元素可以接到L[j]对应的LIS的未尾,因此长度加1,max是为了保证L[i]永远是最大值;否则L[i]=1

5.得到数组L,获取其中的最大值,则为最长递增子序列的长度

使用动态规划来找到最长递增子序列的长度,

并在过程中记录前一个元素的下标,以便构建最长递增子序列

   

3.解法三:基于二分查找

原始数组为arr, 建立一个辅助数组tails;

数组prev:记录以第i个元素结尾的子序列的前一个元素

遍历A中的所有的元素 x = A[i]

如果x > tails的末尾元素,则将x追加到B的末尾,end+=1

如果x < tails的末尾元素,则利用二分查找,寻找B中第一个大于x的元素,并用x进行替换 e.g. x= 1 tails=[5,6,7] ==> tails=[1,6,7]

遍历结束之后,B的长度则为最长递增子序列的长度

五、关键代码

1.解法一:最长公共子序列

int longestCommonSubsequence(vector<int>& nums1, vector<int>& nums2) {
    int m = nums1.size();
    int n = nums2.size();

    // 创建一个二维数组 dp 来存储中间结果,dp[i][j] 表示 nums1[0..i-1] 和 nums2[0..j-1] 的 LCS 长度
    vector<vector<int> > dp(m + 1, vector<int>(n + 1, 0));

    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (nums1[i - 1] == nums2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }

    // dp[m][n] 存储的即为最长公共子序列的长度
    return dp[m][n];
}	

2.解法二:LIS的标准动态规划方法

vector<int> findLongestIncreasingSubsequence(vector<int>& arr) {
    int n = arr.size();
    vector<int> dp(n, 1);  // dp[i] 表示以第i个元素为结尾的最长递增子序列的长度
    vector<int> prev(n, -1);  // prev[i] 用于记录以第i个元素结尾的子序列的前一个元素的下标
    int maxLength = 1;
    int endingIndex = 0;  // 用于跟踪最长递增子序列的结束元素下标

    for (int i = 1; i < n; i++) {
    	cout<<"i: "<<i<<"\tarr[i]: "<<arr[i]<<endl;
        for (int j = 0; j < i; j++) {
        	cout<<"j: "<<j<<endl;
            if (arr[i] > arr[j] && dp[i] < dp[j] + 1) {
                dp[i] = dp[j] + 1;
                prev[i] = j;
                cout<<"UPDATE_DP:dp[i]: "<<dp[i]<<"\tprev[i]: "<<prev[i]<<endl;
                if (dp[i] > maxLength) {
                    maxLength = dp[i];
                    endingIndex = i;
                    cout<<"update_endingIndex: "<<endingIndex<<endl;
                }
            }
        }
        cout<<endl;
    }
    
    vector<int> longestIncreasingSubsequence;
    while (endingIndex != -1) {
    	cout<<"endingIndex: "<<endingIndex<<"\t"<<arr[endingIndex]<<endl;
        longestIncreasingSubsequence.push_back(arr[endingIndex]);
        endingIndex = prev[endingIndex];
        
    }
    reverse(longestIncreasingSubsequence.begin(), longestIncreasingSubsequence.end());

    return longestIncreasingSubsequence;
}

3.解法三:基于二分查找

// 通过二分查找找到第一个大于等于target的元素的下标
int binarySearch(vector<int>& tails, int left, int right, int target) {
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (tails[mid] < target) {
            left = mid + 1;
        } else {
            right = mid;
        }
    }
    return left;
}

vector<int> findLIS(vector<int>& arr) {
    int n = arr.size();
    vector<int> tails;
    vector<int> prev(n, -1); // 用于记录以第i个元素结尾的子序列的前一个元素

    for (int i = 0; i < n; i++) {
        int len = tails.size();
        if (len == 0 || arr[i] > tails[len - 1]) {
            // 当前元素比当前子序列中的所有元素都大,直接添加到末尾
            tails.push_back(arr[i]);
            if (len > 0) {
                prev[i] = tails[len - 1];
            }
        } 
		else {
            // 当前元素大于等于子序列中的一部分元素,使用二分查找找到合适的位置插入
            int idx = binarySearch(tails, 0, len, arr[i]);
            tails[idx] = arr[i];
            if (idx > 0) {
                prev[i] = tails[idx - 1];
            }
        }
//      cout<<"tails: ";      for (int num : tails) {cout << num << "\t";}	cout<<endl;
//  	cout<<"arrr:  ";      for (int num : arr)   {cout << num << "\t";}	cout<<endl;
//		cout<<"prev:  ";	  for (int num : prev)  {cout << num << "\t";}	cout<<endl<<endl;
  }

    // 构建最长递增子序列
    vector<int> longestIncreasingSubsequence;
    int cur = tails.size() > 0 ? tails.back() : -1;
    for (int i = n - 1; i >= 0; i--) {
        if (arr[i] == cur) {
            longestIncreasingSubsequence.push_back(arr[i]);
            cur = prev[i];
        }
    }
    reverse(longestIncreasingSubsequence.begin(), longestIncreasingSubsequence.end());

    return longestIncreasingSubsequence;
}

4.自己创建测试数据

		//生成规模为n的随机数 
		cout<<"请输入数据规模n:"<<endl; 
		int n;
		cin>>n;
		ofstream out("input1.txt");
		out<<n<<'\n';
		srand((unsigned)time(NULL));
		int a=0,b=100000;
		for(int i=0;i<n;i++){
			out<<(rand() % (b-a+1))+ a<<' ';
		} 
		out.close();

5.运行时间的测量与可视化绘制

ofstream out1("output1.txt");
	ofstream out2("output2.txt");
	ofstream out3("output3.txt");
		int cishu=1;
		int copy_cishu=cishu;
		long long result;
		//解法一:最长公共子序列
		while(cishu--){
			QueryPerformanceFrequency(&nFreq);	
			QueryPerformanceCounter(&nBegin);
			result=longestCommonSubsequence(arr);
			QueryPerformanceCounter(&nEnd);
			time+=(double)(nEnd.QuadPart-nBegin.QuadPart)/(double)nFreq.QuadPart; 
		}
		time=time/copy_cishu;
		cishu=copy_cishu;
		cout<<"1结果:"<<result<<"\n1查询时间:"<<time<<endl<<endl;
		out1<<n<<' '<<time<<endl;
		
		//解法二:LIS的标准动态规划方法
		time=0; 
		while(cishu--){
			QueryPerformanceFrequency(&nFreq);	
			QueryPerformanceCounter(&nBegin);
			vector<int> longestSubsequence = findLongestIncreasingSubsequence(arr);
		    result=longestSubsequence.size();
//		    cout << "最长单调递增子序列为: ";
//		    for (int num : longestSubsequence) {
//		        cout << num << " ";
//		    }
			QueryPerformanceCounter(&nEnd);
			time+=(double)(nEnd.QuadPart-nBegin.QuadPart)/(double)nFreq.QuadPart; 
		}
		time=time/copy_cishu;
		cishu=copy_cishu;
		cout<<"2结果:"<<result<<"\n2查询时间:"<<time<<endl<<endl;
		out2<<n<<' '<<time<<endl;
		
			//解法三:二分查找
		time=0;
		while(cishu--){
			QueryPerformanceFrequency(&nFreq);	
			QueryPerformanceCounter(&nBegin);
			vector<int> lis = findLIS(arr);
		    result=lis.size();
//		    cout << "最长单调递增子序列为: ";
//		    for (int num : lis) {
//		        cout << num << " ";
//		    }
			QueryPerformanceCounter(&nEnd);
			time+=(double)(nEnd.QuadPart-nBegin.QuadPart)/(double)nFreq.QuadPart; 
		}
		time=time/copy_cishu;
		cishu=copy_cishu;
		cout<<"3结果:"<<result<<"\n3查询时间:"<<time<<endl<<endl<<endl;
		out3<<n<<' '<<time<<endl;
		in.close();		
	}
	out1.close();
	out2.close();
	out3.close();

六、测试结果

时间复杂度:

解法1:最长公共子序列法 O(n^2)

排序复杂度是nlgn;

因为有一个m*n的嵌套循环,而且m与n相等

解法2:动态规划法 O(n^2)

有一个嵌套循环,复杂度也是n的平方量级;

但是相较于解法1,动规的实际复杂度会小很多。

解法3:基于二分查找 O(NlgN)

算法遍历一遍就行,复杂度是n;

但是会用到二分查找,复杂度是nlgn

n从100-20000,每次递增100的测试结果:

自定义输入n的数值的测试结果: 

 

七、实验心得

        通过这次实验,我了解熟悉了单调递增最长子序列问题的求解过程及原理。对于自己实现的案例,感觉存在误差,画出来的图不符合直觉,也可能是求解运行时间的程序有问题。

        

八、完整代码

#include <iostream>
#include <vector>
#include <algorithm>
#include <fstream>
#include <windows.h>
#include <time.h>
#define len(a) (sizeof(a) / sizeof(a[0])) //数组长度  
using namespace std; 

//**********************解法一:最长公共子序列**********************************************
int longestCommonSubsequence(vector<int> nums1) {    
    int m = nums1.size();
    vector<int> nums2(nums1);
    int n = nums2.size();
	sort(nums2.begin(),nums2.end()); //排序 
    // 创建一个二维数组 dp 来存储中间结果,dp[i][j] 表示 nums1[0..i-1] 和 nums2[0..j-1] 的 LCS 长度
    vector<vector<int> > dp(m + 1, vector<int>(n + 1, 0));

    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (nums1[i - 1] == nums2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }

    // dp[m][n] 存储的即为最长公共子序列的长度
    return dp[m][n];
}


//**********************解法二:LIS的标准动态规划方法*********************************************
vector<int> findLongestIncreasingSubsequence(vector<int> arr) {
    int n = arr.size();
    vector<int> dp(n, 1);  // dp[i] 表示以第i个元素为结尾的最长递增子序列的长度
    vector<int> prev(n, -1);  // prev[i] 用于记录以第i个元素结尾的子序列的前一个元素的下标
    int maxLength = 1;
    int endingIndex = 0;  // 用于跟踪最长递增子序列的结束元素下标

    for (int i = 1; i < n; i++) {
    //	cout<<"i: "<<i<<"\tarr[i]: "<<arr[i]<<endl;
        for (int j = 0; j < i; j++) {
    //    	cout<<"j: "<<j<<endl;
            if (arr[i] > arr[j] && dp[i] < dp[j] + 1) {
                dp[i] = dp[j] + 1;
                prev[i] = j;
    //            cout<<"UPDATE_DP:dp[i]: "<<dp[i]<<"\tprev[i]: "<<prev[i]<<endl;
                if (dp[i] > maxLength) {
                    maxLength = dp[i];
                    endingIndex = i;
    //                cout<<"update_endingIndex: "<<endingIndex<<endl;
                }
            }
        }
    //    cout<<endl;
    }
    
    vector<int> longestIncreasingSubsequence;
    while (endingIndex != -1) {
    //	cout<<"endingIndex: "<<endingIndex<<"\t"<<arr[endingIndex]<<endl;
        longestIncreasingSubsequence.push_back(arr[endingIndex]);
        endingIndex = prev[endingIndex];
        
    }
    reverse(longestIncreasingSubsequence.begin(), longestIncreasingSubsequence.end());

    return longestIncreasingSubsequence;
}

//int findLongestIncreasingSubsequence(vector<int>& arr) {
//    int n = arr.size();
//    vector<int> dp(n, 1);  // dp[i] 表示以第i个元素为结尾的最长递增子序列的长度
//    int maxLength = 1;
//
//    for (int i = 1; i < n; i++) {
//        for (int j = 0; j < i; j++) {
//            if (arr[i] > arr[j]) {
//                dp[i] = max(dp[i], dp[j] + 1);
//                maxLength = max(maxLength, dp[i]);
//            }
//        }
//    }
//
//    return maxLength;
//}



//**********************解法三:二分查找*********************************************
// 通过二分查找找到第一个大于等于target的元素的下标
int binarySearch(vector<int>& tails, int left, int right, int target) {
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (tails[mid] < target) {
            left = mid + 1;
        } else {
            right = mid;
        }
    }
    return left;
}

vector<int> findLIS(vector<int>& arr) {
    int n = arr.size();
    vector<int> tails;
    vector<int> prev(n, -1); // 用于记录以第i个元素结尾的子序列的前一个元素

    for (int i = 0; i < n; i++) {
        int len = tails.size();
        if (len == 0 || arr[i] > tails[len - 1]) {
            // 当前元素比当前子序列中的所有元素都大,直接添加到末尾
            tails.push_back(arr[i]);
            if (len > 0) {
                prev[i] = tails[len - 1];
            }
        } 
		else {
            // 当前元素大于等于子序列中的一部分元素,使用二分查找找到合适的位置插入
            int idx = binarySearch(tails, 0, len, arr[i]);
            tails[idx] = arr[i];
            if (idx > 0) {
                prev[i] = tails[idx - 1];
            }
        }
//      cout<<"tails: ";      for (int num : tails) {cout << num << "\t";}	cout<<endl;
//  	cout<<"arrr:  ";      for (int num : arr)   {cout << num << "\t";}	cout<<endl;
//		cout<<"prev:  ";	  for (int num : prev)  {cout << num << "\t";}	cout<<endl<<endl;
  }

    // 构建最长递增子序列
    vector<int> longestIncreasingSubsequence;
    int cur = tails.size() > 0 ? tails.back() : -1;
    for (int i = n - 1; i >= 0; i--) {
        if (arr[i] == cur) {
            longestIncreasingSubsequence.push_back(arr[i]);
            cur = prev[i];
        }
    }
    reverse(longestIncreasingSubsequence.begin(), longestIncreasingSubsequence.end());

    return longestIncreasingSubsequence;
}



int main() {
	ofstream out1("output1.txt");
	ofstream out2("output2.txt");
	ofstream out3("output3.txt");
//	int n=20000;
	while(1){
		//生成规模为n的随机数 
		cout<<"请输入数据规模n:"<<endl; 
		int n;
		cin>>n;
		ofstream out("input1.txt");
		out<<n<<'\n';
		srand((unsigned)time(NULL));
		int a=0,b=100000;
		for(int i=0;i<n;i++){
			out<<(rand() % (b-a+1))+ a<<' ';
		} 
		out.close();
	
		int i,maxi,mini;
		LARGE_INTEGER nFreq,nBegin,nEnd;
		double time=0; 
		
		ifstream in("input1.txt");
		in>>n;
		vector<int> arr(n);
		for(int i=0;i<n;i++){
			in>>arr[i];
		}
		int cishu=1;
		int copy_cishu=cishu;
		long long result;
		//**********************解法一:最长公共子序列**********************************************
		while(cishu--){
			QueryPerformanceFrequency(&nFreq);	
			QueryPerformanceCounter(&nBegin);
			result=longestCommonSubsequence(arr);
			QueryPerformanceCounter(&nEnd);
			time+=(double)(nEnd.QuadPart-nBegin.QuadPart)/(double)nFreq.QuadPart; 
		}
		time=time/copy_cishu;
		cishu=copy_cishu;
		cout<<"1结果:"<<result<<"\n1查询时间:"<<time<<endl<<endl;
		out1<<n<<' '<<time<<endl;
		
		//**********************解法二:LIS的标准动态规划方法*********************************************	
		time=0; 
		while(cishu--){
			QueryPerformanceFrequency(&nFreq);	
			QueryPerformanceCounter(&nBegin);
			vector<int> longestSubsequence = findLongestIncreasingSubsequence(arr);
		    result=longestSubsequence.size();
//		    cout << "最长单调递增子序列为: ";
//		    for (int num : longestSubsequence) {
//		        cout << num << " ";
//		    }
			QueryPerformanceCounter(&nEnd);
			time+=(double)(nEnd.QuadPart-nBegin.QuadPart)/(double)nFreq.QuadPart; 
		}
		time=time/copy_cishu;
		cishu=copy_cishu;
		cout<<"2结果:"<<result<<"\n2查询时间:"<<time<<endl<<endl;
		out2<<n<<' '<<time<<endl;
		
			//**********************解法三:二分查找*********************************************
		time=0;
		while(cishu--){
			QueryPerformanceFrequency(&nFreq);	
			QueryPerformanceCounter(&nBegin);
			vector<int> lis = findLIS(arr);
		    result=lis.size();
//		    cout << "最长单调递增子序列为: ";
//		    for (int num : lis) {
//		        cout << num << " ";
//		    }
			QueryPerformanceCounter(&nEnd);
			time+=(double)(nEnd.QuadPart-nBegin.QuadPart)/(double)nFreq.QuadPart; 
		}
		time=time/copy_cishu;
		cishu=copy_cishu;
		cout<<"3结果:"<<result<<"\n3查询时间:"<<time<<endl<<endl<<endl;
		out3<<n<<' '<<time<<endl;
		in.close();		
	}
	out1.close();
	out2.close();
	out3.close();
}


九、绘图代码

import matplotlib.pyplot as plt

def read_file(filename):
    data = []
    with open(filename, 'r') as file:
        for line in file:
            x, y = map(float, line.split())
            data.append((x, y))
    return data

# 读取三个文件的数据
file1_data = read_file('F:\\3-CourseMaterials\\3-1\\3-算法设计与分析\实验\lab3\\1-code\\4-单调递增最长子序列问题\\output1.txt')
file2_data = read_file('F:\\3-CourseMaterials\\3-1\\3-算法设计与分析\实验\lab3\\1-code\\4-单调递增最长子序列问题\\output2.txt')
file3_data = read_file('F:\\3-CourseMaterials\\3-1\\3-算法设计与分析\实验\lab3\\1-code\\4-单调递增最长子序列问题\\output3.txt')

# 分别提取 x 和 y 的值
file1_x, file1_y = zip(*file1_data)
file2_x, file2_y = zip(*file2_data)
file3_x, file3_y = zip(*file3_data)

print(file1_x)
print(file1_y)
print(file2_y)
print(file3_y)
# 绘制图形,指定不同颜色
plt.plot(file1_x, file1_y, label='Solution 1', color='red')
plt.plot(file2_x, file2_y, label='Solution 2', color='green')
plt.plot(file3_x, file3_y, label='Solution 3', color='blue')


# 添加图例、标签等
plt.legend()
plt.title('Data from Three Files')
plt.xlabel('X-axis')
plt.ylabel('Y-axis')

# 显示图形
plt.show()
  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值