刚刚考完操作系统,虽然操作系统考试已经跪了,但是算法OJ还得继续写(我哭爆),好在这次算法OJ(作业8)不难,是两道简单的动态规划问题,下面是问题A
问题 A: 最长回文子序列
时间限制: 1 Sec 内存限制: 75 MB题目描述
回文:一个序列是回文的,是指这个序列从左往右读,从右往左读是一样的。
子序列:一个序列a1, a2, …, an,它的子序列指的是从中任选几个元素,按照其原先序号从小到大排好。比如a5, a9, a11就是原序列的一个子序列(假设n>=11)。
求:一个序列的最长回文子序列的长度。
输入
输出
最长回文子序列的长度
样例输入A C G T G T C A A A A T C G | 样例输出8 |
提示
算法的时间复杂度不要超过O(n2)
使用short而不是int以节省内存
使用scanf而不是cin来节约时间和内存
少做没有意义的操作,否则可能会时间超限
以上就是问题A,求最长回文子序列。首先什么是回文子序列呢,就是正着看和倒着看都一样的序列。注意这里是子序列,而不是字串,如果是字串,那么还隐含了另一个条件,那就是连续。
这道题直观上看是需要使用递归的,对于给定长度,设为n,的字符序列A[1...n],A[1...n]中最长子序列的长度分为以下两种情况:
1、A[1] == A[n]
这种情况下,A[1...n]的最长回文子序列长度是A[2...n-1]最长回文子序列的长度加2。可以证明A[2...n-1]的最长回文子序列的长度不短于A[1...n-1]和A[2...n],而又有A[1]==A[n],所以A[1], A[n]加上A[2...n]中最长回文子序列的长度是要大于A[1...n-1]和A[2...n]的,这就证明了算法的正确性。
2、A[1] != A[n]
这种情况下,1中那种情况已经是不可能了,因此只有从剩下的两种情况——A[1...n-1]和A[2...n]的最长回文子序列长度中选取那个最大的了。
递归伪代码如下(请注意!伪代码加*注释的行没有考虑边界条件)
MAX_SEQ(A, begin, end){
if A[begin] == A[end]
return 2 + MAX_SEQ(A, begin + 1, end - 1); //*
else
return max{MAX_SEQ(A, begin, end - 1), MAX_SEQ(A, begin + 1, end)}; //*
}
众所周知的是,递归效率极其之低,以至于尽管是一道规模不大OJ题,OJ服务器也不想拿出很多时间和资源去进行递归计算(开玩笑的)。动态规划的出现就是为了解决递归中存在的巨量冗余计算,其思想就是把已经执行过一遍的计算记录下来,下次再执行这个计算时,直接将上一次的计算值取出来使用,避免了二次计算。
动态规划相关知识参见《算法设计与分析》(黄宇 著 机械工业出版社)第14章.
这道题是从字符序列的两端向内求解,需要记录从i开始到j这段字符序列中最大回文序列,i和j都是可变动值,因此是一个二维的动态规划问题。以下给出动态规划数组递归式:
MAX_SEQ[i, j] = 2 + MAX_SEQ[i + 1, j - 1], seq[i] == seq[j] && i + 1 < j
2, i + 1 == j
max{MAX_SEQ[I, j - 1], MAX_SEQ[i + 1, j] }, seq[i] != seq[j]
最终输出MAX_SEQ[0, n-1]即可。
实现中的一些问题:
1、动态二维数组
动态数组使用起来很方便,但是动态数组只能有一维是动态的,所以不可能进行如下申请:
int* MAX_SEQ = new int[seq.size][seq.size];
而神器vector又只是一维的。那不想额外浪费空间(注意到本题内存限制高达75MB,而我只用了8MB。。。所以节省空间的考虑可能是多此一举的),该怎么办呢,是不是有想自己封装动态数组的想法?回忆前几次OJ中,用vector储存图结构时,每个点的结构体内部又封装了一个vector,便自然而然产生了以下办法,在vector中嵌套一个vector,具体实现如下:
vector<vector<int>>MAX_SEQ(seq.size);
这样就有了一个二维动态数组,但是每一行现在都是空的,此时利用vector提供的resize函数(注意!不是reserve函数!)对列进行扩充,如下:
for(int i = 0; i < seq.size; i++)
MAX_SEQ[i].resize(seq.size);
这样,就成功申请了一个size*size的二维数组,满足了按需取内存的要求
2、初始化和计算顺序问题
首先要把MAX_SEQ[i][i]初始化为1,因为任何长度为1的序列都是回文序列。
计算动态规划数组时,显然要贴着矩阵(二维数组)的对角线计算,从程序的空间局部性和for循环的易读性考虑,建议从倒数第二行往前,每一行从对角线往右遍历
for i = seq.size - 2 to 0 do
for j = i + 1 to seq.size - 1 do
/*do what you want to do*/
鸣谢
Aaron leo