您的位置:首页 > 其它

动态规划--最长单调子序列问题

2015-06-24 09:43 447 查看

1.问题描述:

求一个正整数序列的最长单调自增子序列,子序列不要求是连续的。例如

Input:5

5 2 4 3 1

Output:2

2. 算法复杂度是O(N*N)

确定状态转移方程,设f[i]是以a[i]为结尾的最大值的子序列的长度,那么\[\max \{ f[i]\} \]的最大值就是要的结果。

所以转移方程为:

\[f(i) = \max \{ f(x)|x < i,{a_i} > {a_x}\} + 1\]
所以代码可以为:

void main(void)
{
int arr[] = {10,4,20,10,15,13};
int dist[6];
int path[6];
int num = 6;
int i = 0;

for(i = 0; i < 6; i++)
{
dist[i] = 0;
//-1表示前面没有元素了,用于回头求解的时候定界
path[i] = -1;
}

for(i = 0; i < num; i++)
{
int temp = 0;
int index = -1;

for(int j = i - 1; j >= 0; j--)
{
if(arr[i] > arr[j] && dist[j] > temp)
{
temp = dist[j];
index = j;
}//if
}//for

dist[i] = temp + 1;
path[i] = index;

}//for

//找到最大的那个值
int max = 0;
int maxIndex = -1;
for(int m = 0; m < num; m++)
{
if(dist[m] > max)
{
max = dist[m];
maxIndex = m;
}
}//for
printf("最长单曾子序列的长度是%d.\n", max);

while(path[maxIndex] != -1)
{
printf("%d->", arr[maxIndex]);
maxIndex = path[maxIndex];
}//while
printf("%d\n", arr[maxIndex]);

}


  很显然时间复杂度是O(N*N),那么有没有更快的算法呢?按照正常的思路更快的复杂度应该就是O(N*logN),那么就要涉及到二分了。

3. 算法复杂度是O(N*logN)

建立一个辅助数组c
c[i]=j存储的是子序列长度为i的序列最后一个值j(实际上子序列长度为i的子序列有多个,要的是子序列最后一个值最小的)。

这时要遍历要处理的数组arr


for(i=0;i<n;i++)
{
  j=find(c,n+1,arr[i]);//find是一个二分查找
  c[j]=arr[i];
  dist[i]=j;
}


请看一下上面的例子实际执行的情况:C数组变化的情况

-1 5

-1 2

-1 2 4

-1 1 2

-1 1 4

-1 1 3


arr数组遍历是从前往后的,处理arr[i-1]时arr[i]以及后面的值肯定还没有处理,前面的值都处理过了,看c数组,每个arr数组中的值和c数组中值进行比较,找到合适的位置插入(若插入到c数组的末尾,那么就属于最长递增子序列长度加1,实际上c数组的长度就是最后的最长单调递增子序列的长度。),否则这就替换掉了c数组中原来位置存储的值,这种替换是有意义的,主要是为了后来的arr数组中的值计算dist用(dist[i]中保存的是以arr[i]为最后一个元素的最长单调递增子序列。)好处是若arr[i] <arr[j],dist[i]=dist[j],那么在c中肯定要保存arr[i]呀!!(注意c数组的下标代表的是子序列的长度,c数组中的值也是按递增顺序排列的。这才可能用二分呢,亲)。和O(N*N)的主要区别就是巧妙的借用了c数组,本题的关键就是理解c数组的意义。可以手动模拟一下算法执行的步骤,重要模拟c和b数组的变化情况。c数组是以-1位开头的有序的递增数列。

下面给出完整的算法

#include <stdio.h>

#define MAX 100
void fill(int a[], int len);
int find(int a[], int cLastIndex, int x);

int main()
{
int arr[MAX] = {0};
int dist[MAX] = {0};
int c[MAX];
int num;	//原始数字序列的长度

int i = 0;

freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);

printf("读取数字序列长度...\n");
scanf("%d", &num);
printf("读取数字序列...\n");
for(i = 0; i < num; i++)
{
scanf("%d", &arr[i]);
}//for

//初始化c数组,长度是arr数组+1
fill(c, num + 1);

c[0] = -1;	//使得递增数列的最小值为-1

for(i = 0; i < num; i++)
{
int j = find(c, i + 1, arr[i]);
c[j] = arr[i];
dist[i] = j;
}//for

//c数组中保存的就是结果的数字序列,打印一下
i = 1;
while(c[i] != 100)
{
printf("%d ", c[i]);
i++;
}

fclose(stdin);
fclose(stdout);
return 0;

}

void fill(int a[], int len)
{
for(int i = 0; i < len; i++)
{
a[i] = 100;	////这就是一个初始化,无所谓,目的就是是这个数列递增
}
}

//在数组a中,寻找数字x
//如果存在返回其下表,如果不存在返回其该插入的位置,会覆盖掉原来在该位置的数字
int find(int a[], int cLastIndex, int x)
{
int left = 0;
int right = cLastIndex;
int mid = (left + right) / 2;

while(left <= right)
{
if(x > a[mid])
{
left = mid + 1;
}
else if(x < a[mid])
{
right = mid - 1;
}
else
{
return mid;
}

mid = (left + right) / 2;
}//while

return left;
}//find()


  通过上面的代码可以看到,其实dist数组根本没有用到,只用c数组就能求解问题,其实这种算法已经不是动态规划了,虽然它的时间复杂度比动态规划的要好。这个算法的根本思想就是想到一个c数组,这个有序递增的数组用来存储最长的子序列。确实是一种很不错的手法。

测试数据:

6
10 4 20 10 15 13


运行结果:

读取数字序列长度...
读取数字序列...
4 10 13
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: