HHU暑期第四弹——动态规划入门(01背包+完全背包+多重背包+最长上升子序列和公共子序列)

第四弹动态规划入门的主要内容有以下几部分:01背包,完全背包,多重背包,最长上升子序列和公共子序列。


一、01背包

N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。

从这个题目中可以看出,01背包的特点就是:每种物品仅有一件,可以选择放或不放。

                        (1-1)

对于这方方程其实并不难理解,方程之中,现在需要放置的是第i件物品,这件物品的体积是C[i],价值是W[i],因此F[i-1][j]代表的就是不将这件物品放入背包,表示前i-1件物品中选取若干件物品放入剩余空间为j的背包中所能得到的最大价值;而F[i-1][j-C[i]]+W[i]则是代表将第i件放入背包之后的总价值,表示前i-1件物品中选取若干件物品放入剩余空间为j-C[i]的背包中所能取得的最大价值加上第i件物品的价值根据第i件物品放或是不放确定遍历到第i件物品时的状态F[i][j],比较两者的价值,得出最大的价值存入现在的背包之中。理解了这个方程后,将方程代入实际题目的应用之中,可得

无误版01背包模板:

for(i = 1; i<=n; i++)  
{  
    for(j= V; j>=0; j--)//在这里,背包放入物品后,容量不断的减少,直到再也放不进了  
    { 
        if(j>=C[i]) 
        dp[i][j]=max(dp[i-1][j],dp[i-1][j-C[i]]+W[i]);  
        else dp[i][j]=dp[i-1][j];
    }  
}  


//01背包第k优解 
int v[1001],w[1001];
int dp[1001][1001];
int a[1000],b[1000];
memset(dp,0,sizeof(dp));
memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
int n,V,k,d;
scanf("%d%d%d",&n,&V,&k);
for(int i=1;i<=n;i++)
{
	scanf("%d",&w[i]);
}
for(int i=1;i<=n;i++)
{
	scanf("%d",&v[i]);
}
for(int i =1;i<=n;i++)  
{  
    for(int j = V;j>=v[i];j--)  
    {  
        for(d = 1;d<=k;d++)  
        {  
            a[d] = dp[j-v[i]][d] +w[i];  
            b[d] = dp[j][d];  
        }  
        int x =1;
		int y = 1;
		int z = 1;  
        a[d] = b[d] = -1;  //可能a或b数组先比完 
        while(z<=k && (x<=k || y<=k))  //将之前的dp[j]的所有可能值进行合并并排序找第k大
        {  
            if(a[x]>b[y])  
            dp[j][z] = a[x++];  
            else  
            dp[j][z] = b[y++];  
            if(z==1||(dp[j][z]!=dp[j][z-1])) //去重 
            z++;  
        }  
    }  
}  
printf("%d\n",dp[V][k]);//dp[V][k]即为第k优解 



推荐博文-->背包问题——“01背包”详解及实现(包含背包中具体物品的求解)

01背包的题目有:HDOJ2546、1171(多重背包)、2602、2639、2955、3466、1864



二、完全背包

完全背包是在N种物品中选取若干件(同一种物品可多次选取)放在空间为V的背包里,每种物品的体积为C1,C2,…,Cn,与之相对应的价值为W1,W2,…,Wn.求解怎么装物品可使背包里物品总价值最大。

动态规划(DP):

        1) 子问题定义:F[i][j]表示前i种物品中选取若干件物品放入剩余空间为j的背包中所能得到的最大价值。

        2) 根据第i种物品放多少件进行决策

                                     (2-1)

        其中F[i-1][j-K*C[i]]+K*W[i]表示前i-1种物品中选取若干件物品放入剩余空间为j-K*C[i]的背包中所能得到的最大价值加上k件第i种物品;

       设物品种数为N,背包容量为V,第i种物品体积为C[i],第i种物品价值为W[i]。

       与01背包相同,完全背包也需要求出NV个状态F[i][j]。但是完全背包求F[i][j]时需要对k分别取0,…,j/C[i]求最大F[i][j]值,耗时为j/C[i]。那么总的时间复杂度为O(NV∑(j/C[i]))。

       这跟01背包问题一样有O(N*V)个状态需要求解,但求解每个状态的时间已经不是常数了,求解状态f[i][v]的时间是O(v/c[i]),总的复杂度是超过O(VN)的。将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明01背包问题的方程的确是很重要,可以推及其它类型的背包问题。但我们还是试图改进这个复杂度。
         一个简单有效的优化:完全背包问题有一个很简单有效的优化,是这样的:若两件物品i、j满足c[i]<=c[j]且w[i]>=w[j],则将物品j去掉,不用考虑。这个优化的正确性显然:任何情况下都可将价值小费用高得j换成物美价廉的i,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。这个优化可以简单的O(N^2)地实现,一般都可以承受。另外,针对背包问题而言,比较不错的一种方法是:首先将费用大于V的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以O(V+N)地完成这个优化。这个不太重要的过程就不给出伪代码了,希望你能独立思考写出伪代码或程序。
        转化为01背包问题求解:既然01背包问题是最基本的背包问题,那么我们可以考虑把完全背包问题转化为01背包问题来解。最简单的想法是,考虑到第i种物品最多选V/c[i]件,于是可以把第i种物品转化为V/c[i]件费用及价值均不变的物品,然后求解这个01背包问题。这样完全没有改进基本思路的时间复杂度,但这毕竟给了我们将完全背包问题转化为01背包问题的思路:将一种物品拆成多件物品。
        更高效的转化方法是:把第i种物品拆成费用为c[i]*2^k、价值为w[i]*2^k的若干件物品,其中k满足c[i]*2^k<=V。这是二进制的思想,因为不管最优策略选几件第i种物品,总可以表示成若干个2^k件物品的和。这样把每种物品拆成O(log(V/c[i]))件物品,是一个很大的改进。但我们有更优的O(VN)的算法。

O(VN)的算法:

for (int i = 1; i <= N; i++)
   for(int v=c[i];v<=V;v++)
      f[v]=max(f[v],f[v-c[i]]+w[i]);


完全背包模板:

for(int i=1;i<=N;i++){
	for(int j=0;j<=V;j++){
		if(j<v[i]) f[i][j]=f[i-1][j];
		else{
		     for(int k=0;k<=j/v[i];k++){
			f[i][j]=max(f[i-1][j-k*v[i]]+k*v[i],f[i][j]);	
			}
		}
	}
}



推荐博文-->  背包问题——“完全背包”详解及实现(包含背包具体物品的求解)

完全背包的题目有:POJ 125213842063


三、多重背包

问题:有个容量为V大小的背包,有很多不同重量weight[i](i=1..n)不同价值value[i](i=1..n)的货物,第i种物品最多有n[i]件可用,计算一下最多能放多少价值的货物。

对于多重背包的基本实现,与完全背包是基本一样的,不同就在于物品的个数上界不再是v/c[i]而是n[i]与v/c[i]中较小的那个。状态转移方程如下

     f(i,v) = max{ f(i-1,v-k*c[i]) + k*w[i] | 0<=k<=n[i] }

代码与完全背包的区别仅在内部循环上由

     for(k = 1; k <= j/weight[i]; ++k)

变为

     for(k = 1; k <=n[i] && k<=j/weight[i]; ++k)

当然,输入上的区别就不说了。

多重背包二进制拆分实现

跟完全背包一样的道理,利用二进制的思想将n[i]件物品i拆分成若干件物品,目的是在0-n[i]中的任何数字都能用这若干件物品代换,另外,超过n[i]件的策略是不允许的。

方法是将物品i分成若干件,其中每一件物品都有一个系数,这件物品的费用和价值都是原来的费用和价值乘以这个系数,使得这些系数分别为1,2,4,…,2^(k-1),n[i]-2^k+1,且k满足n[i]-2^k+1>0的最大整数。例如,n[i]=13,就将该物品拆成系数为1、2、4、6的四件物品。分成的这几件物品的系数和为n[i],表明不可能取多于n[i]件的第i种物品。另外这种方法也能保证对于0..n[i]间的每一个整数,均可以用若干个系数的和表示。

代码如下:测试用例见代码末的注释

#include <iostream>
using namespace std;

/* 多重背包 二进制拆分
 * Time Complexity  大于O(N*V)
 * Space Complexity O(N*V)
 * 设 V <= 200 N <= 10 ,拆分后 物品总数 < 50
 * 每件物品有 log n[i]种状态
 */

int maxV[201];
int weight[50]; /* 记录拆分后物体重量 */
int value[50];  /* 记录拆分后物体价值 */
int V, N;

void main()
{
	int i, j;
	scanf("%d %d",&V, &N);
	int weig, val, num;
	int count = 0;

	for(i = 0; i < N; ++i)
	{
		scanf("%d %d %d",&weig,&val,&num);

		for(j = 1; j <= num; j <= 1) // 二进制拆分
		{
			weight[count] = j * weig;
			value[count++] = j * val;
			num -= j;
		}
		if(num > 0)
		{
			weight[count] = num * weig;
			value[count++] = num * val;
		}
	}
	for(i = 0; i < count; ++i)  // 使用01背包
	{
		for(j = V; j >= weight[i]; --j)
		{
			int tmp = maxV[j-weight[i]] + value[i];
			maxV[j] = maxV[j] > tmp ? maxV[j] : tmp;
		}
	}
	printf("%d",maxV[V]);
}

/*
	【输入样例】
	4 20
	3     9     3
	5     9     1
	9     4     2
	8     1     3
	【输出样例】
	47
*/

/* 
各种背包问题的模板 
 
【若要求恰好装满,初始化时f[1...V] = -INF(求最大)或INF(求最小),f[0] = 0】 
 
【若费用==价值时,如硬币能组成多少钱,用背包做时,f[i(费用)] 必定 == i(最大价值) (设能组成i元)  ,因为能组成i元。费用为i时,最大价值若少于i的x的话与能组成i元,矛盾(存在比x大的i),所以必定等于i元,如HDU2844】 
*/  
#include <set>  
#include <map>  
#include <list>  
#include <cmath>  
#include <ctime>  
#include <deque>  
#include <queue>  
#include <stack>  
#include <cstdio>  
#include <string>  
#include <vector>  
#include <cctype>  
#include <cstring>  
#include <sstream>  
#include <fstream>  
#include <cstdlib>  
#include <cassert>  
#include <iostream>  
#include <algorithm>  
  
using namespace std;  
//Constant Declaration  
/*--------------------------*/   
//#define LL long long   
#define LL __int64  
const int M=110;  
const int INF=1<<30;  
const double EPS = 1e-11;  
const double PI = acos(-1.0);  
/*--------------------------*/  
// some essential funtion  
/*----------------------------------*/  
void Swap(int &a,int &b){ int t=a;a=b;b=t; }  
int Max(int a,int b){ return a>b?a:b; }  
int Min(int a,int b){ return a<b?a:b; }  
int Gcd(int a,int b){ while(b){b ^= a ^=b ^= a %= b;} return a; }  
/*----------------------------------*/  
//for (i = 0; i < n; i++)  
/*----------------------------------*/  
  
int c[M], w[M], n1[M];//c:费用 w:价值 n1:数量  
int f[M];//f[与V有关],c和w[与n]有关  
int v, V, V1;//V:容量 V1:容量2  
  
//01背包  
void ZeroOnePack(int c, int w)  
{  
    for (int v = V; v >= c; v--)  
    {  
        f[v] = Max(f[v], f[v-c] + w);  
    }  
}  
  
//完全背包  
void CompletePack(int c, int w)  
{  
    for (int v = c; v <= V; v++)  
    {  
        f[v] = Max(f[v], f[v-c] + w);  
    }  
}  
  
//多重背包,二进制。  
void MultiplePack(int c, int w, int n1)  
{  
    if (c * n1 >= V)  
    {  
        CompletePack(c, w);   
    }  
    else  
    {  
        int k = 1;  
        while (k < n1)  
        {  
            ZeroOnePack(k*c, k*w);  
            n1 -= k;  
            k <<= 1;  
        }  
        ZeroOnePack(n1*c, n1*w);  
    }  
}  
  
int main()  
{  
    //freopen("in.txt","r",stdin);  
    //freopen("out.txt","w",stdout);  
    int t, case1 = 0;  
    scanf("%d", &t);  
    int n, m;//n:物品种数  
    int i, j;  
  
    //scanf("%d%d", &n, &m);  
    while (t--)  
    {  
        scanf("%d%d", &V, &n);  
        for (i = 1; i <= n; i++)  
        {  
            scanf("%d%d%d", &c[i], &w[i], &n1[i]);  
  
        }  
  
        memset(f, 0, sizeof(f));  
  
        for (i = 1; i <= n; i++)  
        {  
            MultiplePack(c[i], w[i], n1[i]);   
        }  
  
        printf("%d\n", f[V]);  
  
    }  
  
    return 0;  
}  


多重背包模板:

int Max(int a,int b){ return a>b?a:b; }     
int c[M], w[M], n1[M];//c:费用 w:价值 n1:数量    
int f[M];//f[与V有关],c和w[与n]有关    
int v, V, V1;//V:容量 V1:容量2    
   
    int n, m;//n:物品种数    
    int i, j;    
    scanf("%d%d", &V, &n);    
    for (i = 1; i <= n; i++)    
    {    
        scanf("%d%d%d", &c[i], &w[i], &n1[i]);    
    }    
    memset(f, 0, sizeof(f));    
    for (i = 1; i <= n; i++)    
    {    
	    if (c[i] * n1[i] >= V)    
	    {    
		    for (int v = c[i]; v <= V; v++)    
		    {    
		        f[v] = Max(f[v], f[v-c[i]] + w[i]);    
		    }       
	    }    
	    else    
	    {    
	        int k = 1;    
	        while (k < n1[i])    
	        {     
				for (int v = V; v >=k*c[i]; v--)    
			    {    
			        f[v] = Max(f[v], f[v-k*c[i]] + k*w[i]);    
			    }       
	            n1[i] -= k;    
	            k <<= 1;    
	        }    
			for (int v = V; v >=k*c[i]; v--)    
		    {    
		        f[v] = Max(f[v], f[v-n1[i]*c[i]] + n1[i]*w[i]);    
		    }     
	    }         
    }    
    printf("%d\n", f[V]);  


多重背包的题目有:POJ  1014 1276 1742 2392 3260 (完全+多重)


简单背包基础总结:

回顾了3种简单背包后,有些思想慢慢体会,实践中,对于01背包和完全背包使用一维数组实现是最简便高效的,对于多重背包,最好就是输入时进行二进制拆分,然后使用01背包,这样比基本实现和在运算时再进行拆分要简捷的多。

背包问题全解  强烈推荐-->dp背包总结大全



四、最长上升子序列和公共子序列


最长公共子序列(LCS)问题描述:

给定两个序列,找出在两个序列中同时出现的最长子序列的长度。一个子序列是出现在相对顺序的序列,但不一定是连续的。例如,“ABC”,“ABG”,“BDF”,“AEG”,“acefg“,..等都是”ABCDEFG“ 序列。因此,长度为n的字符串有2 ^ n个不同的可能的序列。

注意最长公共子串(Longest CommonSubstring)和最长公共子序列(LongestCommon Subsequence, LCS)的区别:子串(Substring)是串的一个连续的部分,子序列(Subsequence)则是从不改变序列的顺序,而从序列中去掉任意的元素而获得的新序列;更简略地说,前者(子串)的字符的位置必须连续,后者(子序列LCS)则不必。比如字符串acdfg同akdfc的最长公共子串为df,而他们的最长公共子序列是adf。LCS可以使用动态规划法解决。

这是一个典型的计算机科学问题,基础差异(即输出两个文件之间的差异文件比较程序),并在生物信息学有较多应用。

例子:
输入序列“ABCDGH”和“AEDFHR” 的LCS是“ADH”长度为3。
输入序列“AGGTAB”和“GXTXAYB”的LCS是“GTAB”长度为4。

这个问题的直观的解决方案是同时生成给定序列的所有子序列,找到最长匹配的子序列。此解决方案的复杂性是指数的。让我们来看看如何这个问题 (拥有动态规划(DP)问题的两个重要特性):

(1)最优子结构:
设输入序列是X [0 .. m-1]和Y [0 .. n-1],长度分别为m和n。和设序列 L(X [0 .. m-1],Y[0 .. n-1])是这两个序列的LCS的长度。

以下为L(X [0 .. M-1],Y [0 .. N-1])的递归定义:

如果两个序列的最后一个元素匹配(即X [M-1] == Y [N-1])
L(X [0 .. M-1],Y [0 .. N-1])= 1 + L(X [0 .. M-2],Y [0 .. N-1])

如果两个序列的最后字符不匹配(即X [M-1]!= Y [N-1])
L(X [0 .. M-1],Y [0 .. N-1])= MAX(L(X [0 .. M-2],Y [0 .. N-1]),L(X [0 .. M-1],Y [0 .. N-2])

例子:

1)考虑输入字符串“AGGTAB”和“GXTXAYB”。最后一个字符匹配的字符串。这样的LCS的长度可以写成:
L(“AGGTAB”, “GXTXAYB”) = 1 + L(“AGGTA”, “GXTXAY”)

2)考虑输入字符串“ABCDGH”和“AEDFHR。最后字符不为字符串相匹配。这样的LCS的长度可以写成:
L(“ABCDGH”, “AEDFHR”) = MAX ( L(“ABCDG”, “AEDFHR”), L(“ABCDGH”, “AEDFH”) )

因此,LCS问题有最优子结构性质!

(2)重叠子问题:
以下是直接的递归实现,  遵循上面提到的递归结构。(有兴趣的读者可以按照前面讲的记忆化存储来实现)

/* 简单的递归实现LCS问题 */
#include<stdio.h>
#include<stdlib.h>
int max(int a, int b);
/* Returns length of LCS for X[0..m-1], Y[0..n-1] */
int lcs( char *X, char *Y, int m, int n )
{
   if (m == 0 || n == 0)
     return 0;
   if (X[m-1] == Y[n-1])
     return 1 + lcs(X, Y, m-1, n-1);
   else
     return max(lcs(X, Y, m, n-1), lcs(X, Y, m-1, n));
}
/* Utility function to get max of 2 integers */
int max(int a, int b)
{
    return (a > b)? a : b;
}
/* 测试上面的函数 */
int main()
{
  char X[] = "AGGTAB";
  char Y[] = "GXTXAYB";
  int m = strlen(X);
  int n = strlen(Y);
  printf("Length of LCS is %d\n", lcs( X, Y, m, n ) );
  getchar();
  return 0;
}

上面直接的递归方法的时间复杂度为O(2 ^ n).(在最坏的情况下。X和Y不匹配的所有字符即LCS的长度为0)。

按照到上述的实现,下面是对输入字符串“AXYT”和“AYZX”的部分递归树:

                       lcs("AXYT", "AYZX")
                       /                 \
         lcs("AXY", "AYZX")            lcs("AXYT", "AYZ")
         /            \                  /               \
lcs("AX", "AYZX") lcs("AXY", "AYZ")   lcs("AXY", "AYZ") lcs("AXYT", "AY")

在上述部分递归树,LCS(“AXY”,“AYZ”)被调用两次。如果我们绘制完整的递归树,那么我们可以看到,我们可以看到很多重复的调用。所以这个问题有重叠的子结构性质,可使用memoization的或打表来避免重新计算。下面是用动态规划(打表)解决LCS问题:

<pre name="code" class="cpp">/ *动态规划实现的LCS问题* /
#include<stdio.h>
#include<stdlib.h>
int max(int a, int b);
/* Returns length of LCS for X[0..m-1], Y[0..n-1] */
int lcs( char *X, char *Y, int m, int n )
{
   int L[m+1][n+1];
   int i, j;
   /* Following steps build L[m+1][n+1] in bottom up fashion. Note
      that L[i][j] contains length of LCS of X[0..i-1] and Y[0..j-1] */
   for (i=0; i<=m; i++)
   {
     for (j=0; j<=n; j++)
     {
       if (i == 0 || j == 0)
         L[i][j] = 0;
       else if (X[i-1] == Y[j-1])
         L[i][j] = L[i-1][j-1] + 1;
       else
         L[i][j] = max(L[i-1][j], L[i][j-1]);
     }
   }
   /* L[m][n] contains length of LCS for X[0..n-1] and Y[0..m-1] */
   return L[m][n];
}
/* Utility function to get max of 2 integers */
int max(int a, int b)
{
    return (a > b)? a : b;
}
/*测试上面的函数 */
int main()
{
  char X[] = "AGGTAB";
  char Y[] = "GXTXAYB";
  int m = strlen(X);
  int n = strlen(Y);
  printf("Length of LCS is %d\n", lcs( X, Y, m, n ) );
  getchar();
  return 0;
}

 

最长递增子序列(LIS)的问题:

可以使用动态规划要解决的问题,例如,最长递增子序列(LIS)的问题是要找到一个给定序列的最长子序列的长度,使得子序列中的所有元素被排序的顺序增加。

例如,{10,22,9,33,21,50,41,60,80}  LIS的长度是6和 LIS为{10,22,33,50,60,80}。

最优子结构:

对于长度为N的数组A[N] = {a0, a1, a2, …, an-1},假设假设我们想求以aj结尾的最大递增子序列长度,设为L[j],那么L[j] = max(L[i]) + 1, where i < j && a[i] < a[j], 也就是i的范围是0到j – 1。这样,想求aj结尾的最大递增子序列的长度,我们就需要遍历j之前的所有位置i(0到j-1),找出a[i] < a[j],计算这些i中,能产生最大L[i]的i,之后就可以求出L[j]。之后我对每一个A[N]中的元素都计算以他们各自结尾的最大递增子序列的长度,这些长度的最大值,就是我们要求的问题——数组A的最大递增子序列。

重叠子问题:

以下是简单的递归实现LIS问题(先不说性能和好坏,后面讨论)。这个实现我们遵循上面提到的递归结构。使用 max_ending_here 返回 每一个LIS结尾的元素,结果LIS是使用指针变量返回。

/* LIS 简单的递归实现 */
#include<stdio.h>
#include<stdlib.h>
/* 要利用递归调用,此函数必须返回两件事情:
   1) Length of LIS ending with element arr[n-1]. We use max_ending_here for this purpose
   2) Overall maximum as the LIS may end with an element before arr[n-1]  max_ref is used this purpose.
The value of LIS of full array of size n is stored in *max_ref which is our final result
*/
int _lis( int arr[], int n, int *max_ref)
{
    /* Base case */
    if(n == 1)
        return 1;
    int res, max_ending_here = 1; // 以arr[n-1]结尾的 LIS的长度
    /* Recursively get all LIS ending with arr[0], arr[1] ... ar[n-2]. If
       arr[i-1] is smaller than arr[n-1], and max ending with arr[n-1] needs
       to be updated, then update it */
    for(int i = 1; i < n; i++)
    {
        res = _lis(arr, i, max_ref);
        if (arr[i-1] < arr[n-1] && res + 1 > max_ending_here)
            max_ending_here = res + 1;
    }
    // Compare max_ending_here with the overall max. And update the
    // overall max if needed
    if (*max_ref < max_ending_here)
       *max_ref = max_ending_here;
    // Return length of LIS ending with arr[n-1]
    return max_ending_here;
}
// The wrapper function for _lis()
int lis(int arr[], int n)
{
    // The max variable holds the result
    int max = 1;
    // The function _lis() stores its result in max
    _lis( arr, n, &max );
    // returns max
    return max;
}
/* 测试上面的函数 */
int main()
{
    int arr[] = { 10, 22, 9, 33, 21, 50, 41, 60 };
    int n = sizeof(arr)/sizeof(arr[0]);
    printf("Length of LIS is %d\n",  lis( arr, n ));
    getchar();
    return 0;
}


根据上面的实现方式,以下是递归树大小4的调用。LIS(N)为我们返回arr[]数组的LIS长度。

                      lis(4)          
                 /       |      \
         lis(3)      lis(2)    lis(1) 
        /     \        /        
  lis(2)  lis(1)   lis(1)
  /   
lis(1)

我们可以看到,有些重复的子问题被多次计算。所以我们可以使用memoization (记忆化存储)的或打表 来避免同一子问题的重新计算。以下是打表方式实现的LIS。

/* LIS 的动态规划方式实现*/
#include<stdio.h>
#include<stdlib.h>
/* lis() returns the length of the longest increasing subsequence in
    arr[] of size n */
int lis( int arr[], int n )
{
   int *lis, i, j, max = 0;
   lis = (int*) malloc ( sizeof( int ) * n );
   /* Initialize LIS values for all indexes */
   for ( i = 0; i < n; i++ )
      lis[i] = 1;
   /* Compute optimized LIS values in bottom up manner */
   for ( i = 1; i < n; i++ )
      for ( j = 0; j < i; j++ )
         if ( arr[i] > arr[j] && lis[i] < lis[j] + 1)
            lis[i] = lis[j] + 1;
   /* Pick maximum of all LIS values */
   for ( i = 0; i < n; i++ )
      if ( max < lis[i] )
         max = lis[i];
   /* Free memory to avoid memory leak */
   free( lis );
   return max;
}
/* 测试程序 */
int main()
{
  int arr[] = { 10, 22, 9, 33, 21, 50, 41, 60 };
  int n = sizeof(arr)/sizeof(arr[0]);
  printf("Length of LIS is %d\n", lis( arr, n ) );
  getchar();
  return 0;
}

注意,上面动态的DP解决方案的时间复杂度为O(n ^ 2),其实较好的解决方案是 O(nlogn)


最长公共上升子序列:

定义状态

F[i][j]表示以a串的前i个整数与b串的前j个整数且以b[j]为结尾构成的LCIS的长度。

状态转移方程:

①F[i][j] = F[i-1][j] (a[i] != b[j])

②F[i][j] = max(F[i-1][k]+1) (1 <= k <= j-1 && b[j] > b[k])

现在我们来说为什么会是这样的状态转移方程呢?

对于①,因为F[i][j]是以b[j]为结尾的LCIS,如果F[i][j]>0那么就说明a[1]..a[i]中必然有一个整数a[k]等于b[j],因为a[k]!=a[i],那么a[i]对F[i][j]没有贡献,于是我们不考虑它照样能得出F[i][j]的最优值。所以在a[i]!=b[j]的情况下必然有F[i][j]=F[i-1][j]。

对于②,前提是a[i] == b[j],我们需要去找一个最长的且能让b[j]接在其末尾的LCIS。之前最长的LCIS在哪呢?首先我们要去找的F数组的第一维必然是i-1。因为i已经拿去和b[j]配对去了,不能用了。并且也不能是i-2,因为i-1必然比i-2更优。第二维呢?那就需要枚举b[1]...b[j-1]了,因为你不知道这里面哪个最长且哪个小于b[j]。这里还有一个问题,可不可能不配对呢?也就是在a[i]==b[j]的情况下,需不需要考虑F[i][j]=F[i-1][j]的决策呢?答案是不需要。因为如果b[j]不和a[i]配对,那就是和之前的a[1]...a[j-1]配对(假设F[i-1][j]>0,等于0不考虑),这样必然没有和a[i]配对优越。(为什么必然呢?因为b[j]和a[i]配对之后的转移是max(F[i-1][k])+1,而和之前的i`配对则是max(F[i`-1][k])+1。

朴素的LCIS算法实现

Hdu 1423 Greatest Common Increasing Subsequence为例。

预处理:

[cpp]  view plain  copy
  1. #include <iostream>  
  2. #include <cstdlib>  
  3. #include <cstdio>  
  4. #include <cstring>  
  5. #include <string>  
  6. #include <algorithm>  
  7. using namespace std;  
  8.   
  9. const int MAXN = 1001;  
  10.   
  11. int a[MAXN], b[MAXN];  
  12. int f[MAXN][MAXN];  
  13. int n, m;  
  14.   
  15. void init()  
  16. {  
  17.     memset(f, 0, sizeof(f));  
  18. }  
核心代码:

[cpp]  view plain  copy
  1. void dp()  
  2. {  
  3.     init();  
  4.     int i, j, k;  
  5.     for(i = 1; i <= n; i++)  
  6.     {  
  7.         for(j = 1; j <= m; j++)  
  8.         {  
  9.             f[i][j] = f[i-1][j]; // if(a[i] != b[j])  
  10.             if(a[i] == b[j])  
  11.             {  
  12.                 int MAX = 0;  
  13.                 for(k = 1; k <= j-1; k++) if(b[j] > b[k]) //枚举最大的f[i-1][k]   
  14.                 {  
  15.                     MAX = max(MAX, f[i-1][k]);  
  16.                 }  
  17.                 f[i][j] = MAX+1;  
  18.             }  
  19.         }  
  20.     }  
  21.     int ans = 0;  
  22.     for(int i = 1; i <= m; i++) ans = max(ans, f[n][i]);  
  23.     printf("%d\n", ans);  
  24. }  
以上的代码的时间复杂度是O(n^3),那我们怎么去优化呢?通过思考发现,第三层循环找最大值是否可以优化呢?我们能否直接把枚举最大的f[i-1][k]值直接算出来呢?假设存在这么一个序列a[i] == b[j],我们继续看状态转移方程②,会发现b[j] > b[k],即当a[i] == b[j]时,可以 推出a[i] > b[k] ,那么有了这个表达式我们可以做什么呢?可以发现,我们可以维护一个MAX值来储存最大的f[i-1][k]值。即只要有a[i] > a[j]的地方,那么我们就可以更新最大值,所以,当a[i] == b[j]的时候,f[i][j] = MAX+1,即可。

核心代码:

[cpp]  view plain  copy
  1. void dp()  
  2. {  
  3.     for(int i = 1; i <= n; i++)  
  4.     {  
  5.         int MAX = 0; //维护最大值   
  6.         for(int j = 1; j <= m; j++)  
  7.         {  
  8.             f[i][j] = f[i-1][j]; //a[i] != b[j]  
  9.             if(a[i] > b[j]) MAX = max(MAX, f[i-1][j]);  
  10.             if(a[i] == b[j]) f[i][j] = MAX+1;  
  11.         }  
  12.     }  
  13.     int ans = 0;  
  14.     for(int i = 1; i <= m; i++) ans = max(ans, f[n][i]);  
  15.     printf("%d\n", ans);  
  16. }  
可以发现,其实上面的代码有些地方与0/1背包很相似,即每次用到的只是上一层循环用到的值,即f[i-1][j],那么我们可以像优化0/1背包问题利用滚动数组来优化空间。

核心代码:

[cpp]  view plain  copy
  1. void dp()  
  2. {  
  3.     init();  
  4.     for(int i = 1; i <= n; i++)  
  5.     {  
  6.         int MAX = 0;  
  7.         for(int j = 1; j <= n; j++)  
  8.         {  
  9.             if(a[i] > b[j]) MAX = max(MAX, f[j]);  
  10.             if(a[i] == b[j]) f[j] = MAX+1;  
  11.         }  
  12.     }  
  13.     int ans = 0;  
  14.     for(int j = 1; j <= m; j++) ans = max(ans, f[j]);  
  15.     printf("%d\n", ans);  
  16. }  
如果是求最长公共下降子序列呢?很明显嘛,把状态定义改动一下,即f[i][j]表示以a串的前i个整数与b串的前j个整数且以b[j]为结尾构成的LCDS的长度,具体实现的时候只要把a[i] > b[j]改为a[i] < b[j]就可以啦。

扩展阅读:http://wenku.baidu.com/view/3e78f223aaea998fcc220ea0.html


子序列问题转移方程详解-->传送门

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值