该题亦为小班题。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()