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

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

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

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

  设L=<a1,a2,…,an>n个不同的实数的序列,L的递增子序列是这样一个子序列Lin=<aK1,ak2,…,akm>,其中k1<k2<…<kmaK1<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],这与归纳假设矛盾。命题得证。

命题2B[c]中存储的元素是当前所有最长递增子序列长度为c的序列中,最小的最末元素,即设当前循环次数为i,有B[c]={aj| f(k)=f(j)=ck,ji+1ajak}(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次循环后得到的pp(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;
}


你可能感兴趣的:(c,优化,算法,存储,UP,n2)