本课程是从少年编程网转载的课程,目标是向中学生详细介绍计算机比赛涉及的编程语言,数据结构和算法。编程学习最好使用计算机,请登陆 www.3dian14.org (免费注册,免费学习)。
给定一组数字,在其中找到最长等差数列的长度。
我们看两个例子:
例1)
输入:set [] = {1,7,10,15,27,29}
输出 = 3
最长的等差数列为{1,15,29},3个数字之间依次之间的等差值为14
例2)
set [] = {5,10,15,20,25,30}
输出= 6
最长的等差数列为{5,10,15,20,25,30},63个数字之间依次之间的等差值为5
为简单起见,我们假设给定的给定的数字的集合已排序(我们总可以先对集合进行排序,不影响我们讨论)。
算法分析
很容易想到以下的简单解决方案:
1)将集合中的每对数字作为视为等差数列中的前两个元素,然后依次检查排序集中的其余元素,找出能与这对数字组合成等差数列的其它数字,这样得到一个候选的等差数列。
2)接着再检查下一对数字,再次得到对应的候选等差数列。
3)最后再从这些候选等差数列中选择长度最长的。
但是上面的方案中我们需要检查所有数字对,共需要运行O(n^2)嵌套循环。在嵌套循环中,我们需要运行第三个循环,该循环线性查找候选等差数列中的更多元素,因此此过程需要O(n^3)时间。
这时候,动态规划又一次显示了威力。使用动态规划可以在O(n^2)时间内解决此问题。
为了讲解如何采用动态规划解决此问题,我们首先讨论以下简单问题的解决方案。
给定一个已排序的数字集合set,请判断其中是否存在三个元素可以组成等差数列。如果存在,则输出true, 否则输出false。注意:如果我们可以找到的等差数列包含3个以上的元素,则答案也为true。
要找到这三个元素,我们首先将一个元素固定为中间元素,然后搜索其他两个元素(一个位于较小,位于左侧;而另一个较大,位于右侧)。我们从第二个元素开始,并将每个元素固定为中间元素。
为了使某个元素set[j]位于等差数列的中间,必须存在元素set[i]和set[k],使得set [i] + set [k] = 2 * set [j],其中 0<= i
如何有效地找到给定 j 的 i 和 k?我们可以使用以下简单算法在线性时间内找到i和k。
1)初始化i为j-1和k为j+1
2)当i>=0且j<=n-1时执行以下操作:
如果set [i] + set [k]等于2 * set [j],那么返回true,结束。
如果set [i] + set [k]> 2 * set [j],则将i减1。
否则,如果set [i] + set [k] <2 * set [j],则将k加1。
3)如果在第二步中都没有找到合适的i和k, 返回false。
以下是针对较简单问题的上述算法的C++实现。请您输入并运行它。
#include
using namespace std;
//假设 set[0..n-1]已经排序了。
//该函数判别是否存在一个3个数组成等差数列,如果存在返回true,否则返回false
bool arithmeticThree(int set[], int n)
{
//set[j]将作为中间位置的元素
//依次检查set中第2个元素直到倒数最后第2个位置的元素
for (int j=1; j
{
// i和j初始化为j的前后位置
int i = j-1, k = j+1;
//检查是否存在i和k,使得set[i],set[j],set[k]依次组成等差数列
while (i >= 0 && k <= n-1)
{
if (set[i] + set[k] == 2*set[j])
return true;
(set[i] + set[k] < 2*set[j])? k++ : i--;
}
}
return false;
}
//主程序
int main()
{
int set1[] = {1, 7, 10, 15, 27, 29};
int n1 = sizeof(set1)/sizeof(set1[0]);
arithmeticThree(set1, n1)? cout << "Yes\n" : cout << "No\n";
int set2[] = {1, 7, 10, 15, 27, 28};
int n2 = sizeof(set2)/sizeof(set2[0]);
arithmeticThree(set2, n2)? cout << "Yes\n" : cout << "No\n";
return 0;
}
上述的算法只是解决了判别一个排好序的集合中是否存在3个或3个以上的元素可组成等差数列的问题。如何将上述解决方案扩展到原始问题?
上面的函数返回一个布尔值。原始问题的要求输出是最长等差数列的长度,它是一个整数值。如果给定集合具有两个或更多个元素,则答案的值至少为2(想想看为什么?)。
动态规划算法的精髓是将以前计算得到的中间值保留,以备后续程序查询使用,而无需重复计算。
因此可以创建一个二维表L[n][n]。该表中的条目L[i][j](j>i)存储了一个中间值,它是将set[i]和set[j]作为等差数列的前两个元素时得到的候选等差数列的长度。
该表的最后一列始终为2(想想看为什么)。表格的其余部分从右下到左上填充。要填充表格的其余部分,首先固定j(等差数列中的第二个元素),然后针对这个固定的j,依次搜索i和k。如果发现i和k可以使得set[i],set[j]和set[k]形成等差数列(采用上面讲述的算法),则将L[i][j]的值设置为L[j][k]+1。
请注意,L[j][k]的值必须先填充,然后循环才能从右到左遍历。
算法实现
以下是动态编程算法的实现示例。请您试运行该程序并比对结果。
//用C++寻找排好序的数字集中最长的等差数列长度的实现例子
#include
using namespace std;
//该函数返回给定数字集中存在的最长等差数列的长度
int lenghtOfLongestAP(int set[], int n)
{
if (n <= 2) return n;
//创建一个表L,并使得每个元素初始化为2。
//L[i][j](j>i)存储了一个中间值,它是将se[i]和set[j]作为等差数列的前两个元素时得到的候选等差数列的长度
int L[n][n];
int llap = 2; // 初始化结果为2
//将表L中最后一列的元素初始化为值2
for (int i = 0; i < n; i++)
L[i][n-1] = 2;
//逐步试探将元素j作为等差数列中的第二个元素
for (int j=n-2; j>=1; j--)
{
//i和k是元素j的前后位置,依次搜索
int i = j-1, k = j+1;
while (i >= 0 && k <= n-1)
{
if (set[i] + set[k] < 2*set[j])
k++;
//在改变i之前,将 L[i][j] 设置为2
else if (set[i] + set[k] > 2*set[j])
{ L[i][j] = 2, i--; }
else
{
//找到i和k, 注意,i和j位置的元素是等差数列的前两个元素
//那么[i][j]的值是L[j][k]的值+1
//当然应该先计算 L[j][k] ,这就是为什么我们从右下向左上填充的原因
L[i][j] = L[j][k] + 1;
//取较大的值保留,作为结果
llap = max(llap, L[i][j]);
//改变i和k, 从右下向左上填充
i--; k++;
}
}
//如果k>n-1, 那么剩下的j列中的元素都是2
while (i >= 0)
{
L[i][j] = 2;
i--;
}
}
return llap;
}
//主程序
int main()
{
int set1[] = {1, 7, 10, 13, 14, 19};
int n1 = sizeof(set1)/sizeof(set1[0]);
cout << lenghtOfLongestAP(set1, n1) << endl;
int set2[] = {1, 7, 10, 15, 27, 29};
int n2 = sizeof(set2)/sizeof(set2[0]);
cout << lenghtOfLongestAP(set2, n2) << endl;
int set3[] = {2, 4, 6, 8, 10};
int n3 = sizeof(set3)/sizeof(set3[0]);
cout << lenghtOfLongestAP(set3, n3) << endl;
return 0;
}
结果为 4 3 5
算法优化
很显然,上述动态规划算法的时间复杂度为 O(n^2),空间复杂度为: O(n^2)。如果程序编写得当,我们可以把空间复杂度降低为O(n)。
参看下面的例子。
#include
using namespace std;
int Solution(vector A)
{
int ans = 2;
int n = A.size();
if (n <= 2)
return n;
vector llap(n, 2);
sort(A.begin(), A.end());
for (int j = n - 2; j >= 0; j--)
{
int i = j - 1;
int k = j + 1;
while (i >= 0 && k < n)
{
if (A[i] + A[k] == 2 * A[j])
{
llap[j] = max(llap[k] + 1, llap[j]);
ans = max(ans, llap[j]);
i -= 1;
k += 1;
}
else if (A[i] + A[k] < 2 * A[j])
k += 1;
else
i -= 1;
}
}
return ans;
}
int main()
{
vector a({ 9, 4, 7, 2, 10 });
cout << Solution(a) << endl;
return 0;
}
请您运行上面的程序,并验证结果。