您的位置:首页 > 其它

NYOJ 17 单调递增最长子序列(O(n2))+HDU 1025 Constructing Roads In JGShining +NYOJ 214 单调递增子序列(二)(O(nlogn))(整理)

2012-02-08 07:21 579 查看
题目链接:http://acm.nyist.net/JudgeOnline/problem.php?pid=17

这题其实是跟导弹拦截一样的,因为还有个加强版,所以把这个跟加强版一起贴上来。经典动态规划题,以后的动态规划很多都是从这个衍生出来的,所以就找了段自己认为比较详细的解释来了,保存下来,备用,语言组织能力太差。。。。。。

一, 最长递增子序列问题的描述

  设L=<a1,a2,…,an>是n个不同的实数的序列,L的递增子序列是这样一个子序列Lin=<aK1,ak2,…,akm>,其中k1<k2<…<km且aK1<ak2<…<akm。求最大的m值。

二,算法1:动态规划法:O(n^2)

  设f(i)表示L中以ai为末元素的最长递增子序列的长度。则有如下的递推方程:

  这个递推方程的意思是,在求以ai为末元素的最长递增子序列时,找到所有序号在L前面且小于ai的元素aj,即j<i且aj<ai。如果这样的元素存在,那么对所有aj,都有一个以aj为末元素的最长递增子序列的长度f(j),把其中最大的f(j)选出来,那么f(i)就等于最大的f(j)加上1,即以ai为末元素的最长递增子序列,等于以使f(j)最大的那个aj为末元素的递增子序列最末再加上ai;如果这样的元素不存在,那么ai自身构成一个长度为1的以ai为末元素的递增子序列。一般在解决问题的时候都是用到动态规划,所以就贴出代码了。主要用这个。。。。。。
代码:

#include<stdio.h>//**O(n^2)
#include<string.h>
int main()
{
char str[10001];
int s,len,i,j,dp[10001],max;
scanf("%d",&s);
while(s--)
{
max=0;
scanf("%s",str);
len=strlen(str);
for(i=0;i<=len-1;i++)
{
dp[i]=1;//**dp[i]的最小值为1**//
}
for(i=len-2;i>=0;i--)
{
for(j=i+1;j<=len-1;j++)
{
if(str[i]<str[j]&&dp[i]<dp[j]+1)
{
dp[i]=dp[j]+1;//**更新dp[i]的值**//
}
}
}
for(i=0;i<=len-1;i++)
{
if(dp[i]>max)
{
max=dp[i];
}
}
printf("%d\n",max);
}
}


三, 算法2:转化为LCS问题求解

  设序列X=<b1,b2,…,bn>是对序列L=<a1,a2,…,an>按递增排好序的序列。那么显然X与L的最长公共子序列即为L的最长递增子序列。这样就把求最长递增子序列的问题转化为求最长公共子序列问题LCS了。

  最长公共子序列问题用动态规划的算法可解。设Li=< a1,a2,…,ai>,Xj=< b1,b2,…,bj>,它们分别为L和X的子序列。令C[i,j]为Li与Xj的最长公共子序列的长度。则有如下的递推方程:

 这可以用时间复杂度为O(n2)的算法求解,由于这个算法上课时讲过,所以具体代码在此略去。求最长递增子序列的算法时间复杂度由排序所用的O(nlogn)的时间加上求LCS的O(n2)的时间,算法的最坏时间复杂度为O(nlogn)+O(n2)=O(n2)。

(额,这种方法没用过,就当作发散下思维吧)LCS算法比较的是任意两个序列的最长公共子序列,在最长递

增子序列中,我们将原序列A首先升序排列得到B,然后将A和B求LCS就可以达到目的。

上面两种方法的复杂度都为O(n^2),第二种方法并没有改进。。。。。。

四, 对算法1的改进(O(nlogn))

题目链接:http://acm.nyist.net/JudgeOnline/problem.php?pid=214

  在第一种算法中,在计算每一个f(i)时,都要找出最大的f(j)(j<i)来,由于f(j)没有顺序,只能顺序查找满足aj<ai最大的f(j),如果能将让f(j)有序,就可以使用二分查找,这样算法的时间复杂度就可能降到O(nlogn)。于是想到用一个数组B来存储“子序列的”最大递增子序列的最末元素,即有B[f(j)]
= aj;

  在计算f(i)时,在数组B中用二分查找法找到满足j<i且B[f(j)]=aj<ai的最大的j,并将B[f[j]+1]置为ai。下面先写出代码,再证明算法的证明性。

#include<stdio.h>//**O(nlogn)**//
#include<string.h>
#define min -32769//**int型最小数为-32768**//
int stack[100001];//**模拟栈,其实不是栈,为了更好形象比较,嘿嘿**//
int main()
{
int n,i,t,top,low,high,mid;
memset(stack,0,sizeof(stack));
while(~scanf("%d",&n))
{
top=0;stack[0]=min;
for(i=0;i<=n-1;i++)
{
scanf("%d",&t);
if(t>stack[top])//**如果输入进来的数比栈顶的数大,直接插入到栈的**//
{
top++;
stack[top]=t;
}
else
{
low=1;high=top;
while(low<=high)//**二分查找,寻找插入位置**//
{
mid=(low+high)/2;
if(t>stack[mid])
{
low=mid+1;
}
else
{
high=mid-1;
}
}
stack[low]=t;//**找到插入位置,并替换点原值**//
}
}
printf("%d\n",top);
}
return 0;
}


现在来证明这个算法为什么是正确的。要使算法正确只须证如下命题:

命题1:每一次循环结束数组B中元素总是按递增顺序排列的。

证明:用数学归纳法,对循环次数i进行归纳。

  当i=0时,即程序还没进入循环时,命题显然成立。

设i<k时命题成立,当i=k时,假设存在j1<j2,B[j1]>B[j2],因为第i次循环之前数组B是递增的,因此第i次循环时B[j1]或B[j2]必有一个更新,假设B[j1]被更新为元素ai+1,由于ai+1=B[j1]>
B[j2],按算法ai+1应更新B[j2]才对,因此产生矛盾;假设B[j2]被更新,设更新前的元素为s,更新后的元素为ai+1,则由算法可知第i次循环前有B[j2]=s<
ai+1< B[j1],这与归纳假设矛盾。命题得证。

命题2:B[c]中存储的元素是当前所有最长递增子序列长度为c的序列中,最小的最末元素,即设当前循环次数为i,有B[c]={aj| f(k)=f(j)=c∧k,j≤i+1→aj≤ak}(f(i)为与第二种算法中的f(i)含义相同)。

证明:程序中每次用元素ai更新B[c]时(c=f(i)),设B[c]原来的值为s,则必有ai<s,不然ai就能接在s的后面形成长度为c+1的最长递增子序列,而更新B[c+1]而不是B[c]了。所有B[c]中存放的总是当前长度为c的最长递增子序列中,最小的最末元素。

命题3设第i次循环后得到的p为p(i+1),那么p(i)为以元素ai为最末元素的最长递增子序列的长度。

证明:只须证p(i)等于第二种算法中的f(i)。显然一定有p(i)<=f(i)。假设p(i)<f(i),那么有两种情况,第一种情况是由二分查找法找到的p(i)不是数组B中能让ai接在后面成为新的最长递增子序列的最大的元素,由命题1和二分查找的方法可知,这是不可能的;第二种情况是能让ai接在后面形成长于p(i)的最长递增子序列的元素不在数组B中,由命题2可知,这是不可能的,因为B[c]中存放的是最末元素最小的长度为c的最长递增子序列的最末元素,若ai能接在长度为L(L>
p(i))的最长递增子序列后面,就应该能接在B[L]后面,那么就应该有p(i)=L,与L>
p(i)矛盾。因此一定有p(i)=f(i),命题得证。

算法的循环次数为n,每次循环二分查找用时logn,所以算法的时间复杂度为O(nlogn)。这个算法在第二种算法的基础上得到了较好的改进。

如果证明看晕了,暂时先用一组数据进行形象比较,就可以理解代码含义了。。。。(其实我也看晕了,嘿嘿)

假设存在一个序列d[1...9]=2 1 5 3 6 4 8 9 7,可以看出它的LIS长度为5.

下面一步一步试着找出它。

我们定义一个序列B,然后令 i = 1 to 9 逐个考察这个序列。

此外,我们用一个变量Len来记录现在最长算到多少了

首先,把d[1]有序地放到B里,令B[1] = 2,就是说当只有1一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1;

然后,把d[2]有序地放到B里,令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1]=2已经没用了,很容易理解吧。这时Len=1;

接着,d[3] = 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候B[1..2] = 1, 5,Len=2

再来,d[4] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B[1..2] = 1, 3,Len = 2

继续,d[5] = 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3] = 6, 这时B[1..3] = 1, 3, 6,还是很容易理解吧? Len = 3 了噢。

第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3] = 4。B[1..3] = 1, 3, 4, Len继续等于3

第7个, d[7] = 8,它很大,比4大,嗯。于是B[4] = 8。Len变成4了

第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。

最后一个, d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,B[1..5] = 1, 3, 4, 7, 9,Len = 5。

于是我们知道了LIS的长度为5。

!!!!!注意。这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到d[5], 9更新到d[6],得出LIS的长度为6。

然后应该发现一件事情了:

在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN)~~~~~于是算法的时间复杂度就降低到了O(NlogN)~!

再加上一题来练手,题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1025

仍然是(O(nlogn))的二分插入。思想都是一样,就不再分析了。。。。

代码:

#include<stdio.h>
int s[500001],dp[500001];
int main()
{
int n,i,a,b,len,up,low,mid,count=1;
while(~scanf("%d",&n))
{
for(i=1;i<=n;i++)
{
scanf("%d %d",&a,&b);
s[a]=b;//**关键思想**//
}
dp[1]=s[1];len=1;
for(i=1;i<=n;i++)
{
low=1;up=len;
while(low<=up)
{
mid=(low+up)/2;
if(dp[mid]>=s[i])
{
up=mid-1;
}
else
{
low=mid+1;
}
}
dp[low]=s[i];
if(low>len)
{
len++;
}
}
printf("Case %d:\n",count++);
if(len>1)
{
printf("My king, at most %d roads can be built.\n\n",len);
}
else
{
printf("My king, at most %d road can be built.\n\n",len);
}
}
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐