题意
东东有两个序列A和B。
他想要知道序列A的LIS和序列AB的LCS的长度。
注意,LIS为严格递增的,即a1<a2<…<ak(ai<=1,000,000,000)。
Input
第一行两个数n,m(1<=n<=5,000,1<=m<=5,000)
第二行n个数,表示序列A
第三行m个数,表示序列B
Output
输出一行数据ans1和ans2,分别代表序列A的LIS和序列AB的LCS的长度
输入样例
5 5
1 3 2 5 4
2 4 3 1 5
输出样例
3 2
提示
分析
这道题中包含的两个问题都是利用动态规划解决的经典题目。
- 动态规划
该算法和贪心算法、分而治之之间具有相似性,相似之处在于将一个问题分解为子问题,但是这三个算法并不完全相同。
动态规划就是将待解决的问题分解成多个子问题,通过多个子问题的解之间的联系进行组合,进而得到问题的解。因此,动态规划中分解出来的每个子问题之间并不是独立的,而是相互关联,含有公共子问题。
- LIS
LIS是指最长上升子序列,意思是在一个无序序列中符合严格递增的子序列中最大的长度。此处子序列并不要求是一段在原序列中的连续序列。
💡LIS问题求解
由于序列是无序的,因此我们在求解过程中无法保证当前扫描到的数字之前的子序列中任意数字与其的大小关系。但是显然,如果当前扫描数字就是我们所求子序列中的最后一个数字,那么在此之前的序列中一定包含着比它小的数字。
也就是说以当前这个数字为最大数的最大序列长度一定是在它之前所有比它小且升序排列的子序列长度+1。
因此,我们可以用一个数组来记录序列中以当前扫描位置处数字为最大数的最大序列长度。对整个序列进行一次从左至右的扫描。当前扫描数字所对应的序列长度一定是在它之前所有比它小的数字中记录序列长度最大的一个+1。
复杂度:O(n^2)
- LCS
LCS是指最长公共子序列,意思是在两个无序序列中共同拥有的符合严格递增的子序列的最大长度。同样,此处的子序列也不是要求在原序列中的连续序列。但是这个序列中都必须是两个序列的子序列,且在两个序列中的的分布都是递增的。
💡LCS问题求解
相似的思想,可以将两个序列中的每个数字拆分开来依次计算。可以想作是模拟两个序列从左至右的还原过程。
用一个二维数据ans[i][j]来代表两个序列中a序列拥有到第ai个数字以及b序列拥有到bj个数字时,当前的最长公共子序列长度。
固定其中一个序列的一个数字,对另一个序列中进行遍历。例如,固定a序列中的ai元素,对b序列进行遍历,当扫描到bj时,
- 若ai = bj,说明当两个序列同时都新增了元素ai和bj后,最长公共子序列的长度+1。因此其对应的二维数组中存储的答案为其二维矩阵中左上方的答案+1。也就是两个序列中都最多只拥有ai-1和bj-1元素时的最长公共子序列长度+1。
- 若ai != bj,说明当前元素同时增加之后,对最长公共子序列长度并没有影响,因此此处记录的答案即为最多拥有ai和bj-1或ai-1和bj时最大的公共子序列长度。
复杂度:O(nm)
- 问题
1. LIS的优化问题
这次的题目主要问题在于理解动态规划算法的基本解题思路。因此代码并没有太大量和太大难度。
但是在我试图优化LIS的解题方法时遇到了问题。我的思路是:
将当前已经得到的所有答案有序存储,因为当前得到的所有答案所对应的数字一定是在序列中位于当前待求数字之前的,因此可以直接对这些答案进行遍历。从最大的答案进行遍历,若当前答案对应序列中的数字小于当前待求数字即为合法。将第一个遍历到的合法答案+1即为当前待求数字的答案。
由于如果只用一个stl实现的最大堆来实现,每次在将不合法答案弹出后,都需要将其在之后重新压入,降低性能,因此我选择用一个数组来进行存储,每次压入答案后重新进行排序。
由于答案与其对应的数字没有直接的关联,因此仅仅存储答案是无法在遍历过程中进行判断的,所以我用了一个pair数组来存储答案和其对应数字在序列中的位置标号,排序时仅根据答案进行排序。
我到后面并没有调试出来问题,所以目前还不太清楚为什么会wa。
不过这个优化的思想是为了优化对每个数字依次遍历在其之前的所有数字这一步骤。因为在这一过程中,可能有多个符合要求的数字,而我们只需要其中记录答案最大的一个,所以如果利用数据结构进行优化,就能将复杂度优化到logn。从而将整个算法的复杂度优化的O(nlogn)。
2. LCS答案数组的初始化
通过之前的分析就知道,在求解过程中,需要知道每个数组位置的左上方、右方和上方位置存储的答案。因此如果不首先将所有数组空间初始化为0,在求解过程中一定会出现问题,比如第一行。
总结
- LCS的求解方法最开始有点难懂,但是实际演算一次过程就能很好理解了。这次动态规划所讲的例题的关键都是要重点理解运用数组存储每个子问题答案这一操作,数组中存储的数据究竟意味着什么,以及彼此之间的联系究竟是什么。
代码
//
// main.cpp
// lab2
//
//
#include <iostream>
#include <vector>
#include <algorithm>
#include <string.h>
using namespace std;
vector<int> a(5010),b(5010);
int main()
{
ios::sync_with_stdio(false);
int n = 0,m = 0,x = 0,y = 0,ans1 = 0;
cin>>n>>m;
int lis[5010];
for( int i = 0 ; i < n ; i++ )
{
cin>>x;
a[i] = x;
lis[i] = 1;
}
for( int i = 0 ; i < m ; i++ )
{
cin>>y;
b[i] = y;
}
for( int i = 0 ; i < n ; i++ ) //遍历所有数字,找到以当前数字为最大数的序列长度
{
for( int j = 0 ; j < i ; j++ ) //遍历之前的所有数字
{
if( a[j] < a[i] ) //若符合要求,进行更新,当且仅当新答案更大时更新
lis[i] = max(lis[i],lis[j]+1);
}
if( lis[i] > ans1 ) //记录最大值
ans1 = lis[i];
}
int lcs[n + 1][m + 1];
memset(lcs, 0, sizeof(lcs));
for( int i = 0 ; i < n ; i++ ) //对a中每个数字
{
for( int j = 0 ; j < m ; j++ ) //遍历整个b
{
//若当前两个数字相等,序列长度比矩阵中记录的斜上方+1
//因为左上方代表的序列正好是没有当前相等的两个数字的ab序列的lcs长度
//b中一个数字在a序列中最多只会出现一次
if( b[j] == a[i] )
lcs[i + 1][j + 1] = max(lcs[i][j] + 1,lcs[i + 1][j + 1]);
else //否则就是其左边或上边存储的最大值
lcs[i + 1][j + 1] = max(lcs[i][j + 1],lcs[i + 1][j]);
}
}
cout<<ans1<<" "<<lcs[n][m]<<endl;
return 0;
}