本课程是从少年编程网转载的课程,目标是向中学生详细介绍计算机比赛涉及的编程语言,数据结构和算法。编程学习最好使用计算机,请登陆 www.3dian14.org (免费注册,免费学习)。
最长的交替(Zig-Zag)子序列问题是找到给定序列的最长子序列的长度,以使该子序列中的所有元素交替出现。
【定义】
如果序列{x1,x2,.. xn}是交替序列,则其元素满足以下关系之一:
x1<x2>x3<x4>x5xn
或
x1>x2<x3>x4<x5>…xn
也就是该序列中数字忽大忽小,交替变化。
我们来看几个例子:
【例一】
输入:arr [] = {1, 5, 4}
输出:3
此时,x1=1,x2=5,x3=4 => 1<5>4
整个数组满足x1<x5>x4,所以长度为3。
【例二】
输入:arr [] = {1, 4, 5}
输出:2
长度为2的所有交替子序列为:
序列{1,4}满足 1<4;
序列{1,5}满足 1<5。
【例三】
输入:arr [] = {10, 22, 9, 33, 49, 50, 31, 60}
输出:6
长度为6的所有交替子序列为:
子序列{10, 22, 9, 33, 31, 60}满足 10<22>9<33>31<60
子序列{10, 22, 9, 49, 31, 60}满足 10<22>9<49>31<60
子序列{10, 22, 9, 50, 31, 60}满足 10<22>9<50>31<60
算法分析
我们还是通过动态编程的方法来解决这个问题。一起来看看如何找到最佳的子结构属性。
设A是包含n个整数元素的数组,其中保存了原始序列。
我们定义二维数组Z[n][2],它一共包括n行,每一行包含两个元素:
对应于Z[i]这一行,用于记录原始数组A中,从A[0]到索引为i的元素A[i]为止,找到的最长的交替子序列的长度。此时又分成两种情况:
元素Z[i][0],记录的是最长交替子序列中最后一个元素大于其前一个元素的情况
元素Z[i][1],记录的是最长交替子序列中最后一个元素小于其前一个元素的情况
也就是说:
Z[i][0] = 在索引i处结束的最长交替子序列的长度并且最后一个元素比它前一个元素大
Z[i][1] = 在索引i处结束的最长交替子序列的长度并且最后一个元素比它前一个元素小
它们的形式化递归定义如下:
Z[i][0] = max (Z[i][0], Z[j][1] + 1) (j < i 并且 A[j] < A[i])
Z[i][1] = max (Z[i][1], Z[j][0] + 1) (j < i 并且 A[j] > A[i])
为什么呢?
第一个递归关系基于以下事实:
如果我们在位置i处并且要求此元素必须大于其先前元素,则为了使此序列(直到i)更长,我们将尝试选择位置i之前的元素j(
注意,Z[j][1]中保存的是到索引i为止的最长交替子序列的长度并且最后一个元素比它前一个元素小。我们选择Z[j][1]+1而非Z[j][0]+1是为了满足交替属性,因为按照Z[j][0]的定义,最后一个元素必须大于前一个元素,而A[i]又大于A[j],如果我们选择Z[j][0]+1,就会出现连续三个元素从小到大排列,而它将破坏交替属性。
同理,上面的推导也适用于第二个递归关系。
算法实现
下面是实现上述算法的一个例子。
#include
#include
// 返回两个数字中较大的数
int max(int a, int b) { return (a > b) ? a : b; }
// 计算最长交替序列的长度
int zzis(int arr[], int n)
{
/*
Z[i][0] = 在索引i处结束的最长交替子序列的长度并且最后一个元素比它前一个元素大
Z[i][1] = 在索引i处结束的最长交替子序列的长度并且最后一个元素比它前一个元素小
*/
int Z[n][2];
/* 将数组中的元素初始化为1,想想为什么? */
for (int i = 0; i < n; i++)
Z[i][0] = Z[i][1] = 1;
int res = 1;
/*从底向上计算 */
for (int i = 1; i < n; i++)
{
// Consider all elements as previous of arr[i]
for (int j = 0; j < i; j++)
{
// 如果 arr[i] 比较大, 尝试 Z[j][1]
if (arr[j] < arr[i] && Z[i][0] < Z[j][1] + 1)
Z[i][0] = Z[j][1] + 1;
// 如果 arr[i] 比较小, 尝试 Z[j][0]
if( arr[j] > arr[i] && Z[i][1] < Z[j][0] + 1)
Z[i][1] = Z[j][0] + 1;
}
/* 找出索引i处的最大值 */
if (res < max(Z[i][0], Z[i][1]))
res = max(Z[i][0], Z[i][1]);
}
return res;
}
/* 主程序 */
int main()
{
int arr[] = { 10, 22, 9, 33, 49, 50, 31, 60 };
int n = sizeof(arr)/sizeof(arr[0]);
printf("Length of Longest Zig-Zag subsequence is %d\n",
zzis(arr, n) );
return 0;
}
运行上述程序并校验结果。
算法的改进
上述算法采用了二次循环,佷容易得出它的时间复杂度是O(n*n),而用到的辅助空间是Z[n][2],因此辅助空间复杂度为O(n)。
我们再介绍一种具有时间复杂度O(n)的更好方法:
将序列存储在未排序的整数数组A[N]中。
我们注意到,在交替序列中,元素的大小是交替出现的,因此序列中连续两个元素的差的符号(正号+或负号-)是交替出现的。例如:
子序列{10, 22, 9, 33, 31, 60}满足 10<22>9<33>31<60
对应的前后两个元素的差的符号为:
-+-+-
我们将通过记录两个连续的A元素之差的符号(+或-),来提高处理效率。为此,我们将(A[i]–A[i-1])差值的符号存储在变量中,然后将其与(A[i+1]–A[i])的符号进行比较。如果不同,表示我们可以将A[i+1]加入序列,它满足交替序列的属性。为了检查符号,我们将使用一个简单的是signum函数,该函数将用来确定传递某个数字的符号。
具体的算法如下:
1)数组A[N]记录输入的整数序列; res记录结果,初始值为1
2)设lastSign = 0
3)For i=1到N-1
sign = signum(A[i] - A[i-1])
当 sign!=lastSign并且sign!=0时
res = res + 1; lastSign=sign
4)返回结果res
改进后的算法仅遍历一次序列,因此时间复杂度优化为O(n)。
算法改进算法的实现方法如下:
/*算法改进的例子*/
#include
#include
using namespace std;
int signum(int n); //function prototype.
/* 计算最长交替序列的长度*/
int zzis(int A[], int n)
{
if (n == 0)
{
return 0;
}
int lastSign = 0, res = 1;
for (int i = 1; i < n; ++i)
{
int Sign = signum(A[i] - A[i-1]);
if (Sign != lastSign && Sign != 0) // 符号变化了并且两元素不相等
{
lastSign = Sign; //updating lastSign
res++;
}
}
return res;
}
/* 正数返回1;负数返回-1;0返回0 */
int signum(int n)
{
if (n != 0)
{
return n > 0 ? 1 : -1;
}
else
{
return 0;
}
}
//主程序
int main()
{
int A1[4] = {1, 3 ,6, 2};
int A2[5] = {5, 0, 3, 1, 0};
int n1 = sizeof(A1) / sizeof(*A1);
int n2 = sizeof(A2) / sizeof(*A2);
int maxLength1 = zzis(A1,n1);
int maxLength2 = zzis(A2,n2);
cout << "The maximum length of zig-zag sub-sequence in first sequence is: " << maxLength1;
cout << endl;
cout << "The maximum length of zig-zag sub-sequence in second sequence is: " << maxLength2;
}
输入上述程序并验证它。