第四弹动态规划入门的主要内容有以下几部分: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]);
}
}
}
}
推荐博文--> 背包问题——“完全背包”详解及实现(包含背包具体物品的求解)
三、多重背包
问题:有个容量为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]);
简单背包基础总结:
回顾了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为例。
预处理:
- #include <iostream>
- #include <cstdlib>
- #include <cstdio>
- #include <cstring>
- #include <string>
- #include <algorithm>
- using namespace std;
- const int MAXN = 1001;
- int a[MAXN], b[MAXN];
- int f[MAXN][MAXN];
- int n, m;
- void init()
- {
- memset(f, 0, sizeof(f));
- }
- void dp()
- {
- init();
- int i, j, k;
- for(i = 1; i <= n; i++)
- {
- for(j = 1; j <= m; j++)
- {
- f[i][j] = f[i-1][j]; // if(a[i] != b[j])
- if(a[i] == b[j])
- {
- int MAX = 0;
- for(k = 1; k <= j-1; k++) if(b[j] > b[k]) //枚举最大的f[i-1][k]
- {
- MAX = max(MAX, f[i-1][k]);
- }
- f[i][j] = MAX+1;
- }
- }
- }
- int ans = 0;
- for(int i = 1; i <= m; i++) ans = max(ans, f[n][i]);
- printf("%d\n", ans);
- }
核心代码:
- void dp()
- {
- for(int i = 1; i <= n; i++)
- {
- int MAX = 0; //维护最大值
- for(int j = 1; j <= m; j++)
- {
- f[i][j] = f[i-1][j]; //a[i] != b[j]
- if(a[i] > b[j]) MAX = max(MAX, f[i-1][j]);
- if(a[i] == b[j]) f[i][j] = MAX+1;
- }
- }
- int ans = 0;
- for(int i = 1; i <= m; i++) ans = max(ans, f[n][i]);
- printf("%d\n", ans);
- }
核心代码:
- void dp()
- {
- init();
- for(int i = 1; i <= n; i++)
- {
- int MAX = 0;
- for(int j = 1; j <= n; j++)
- {
- if(a[i] > b[j]) MAX = max(MAX, f[j]);
- if(a[i] == b[j]) f[j] = MAX+1;
- }
- }
- int ans = 0;
- for(int j = 1; j <= m; j++) ans = max(ans, f[j]);
- printf("%d\n", ans);
- }
扩展阅读:http://wenku.baidu.com/view/3e78f223aaea998fcc220ea0.html
子序列问题转移方程详解-->传送门