给定一个长度为n的序列(n <= 1000) ,记该序列LIS(最长上升子序列)的长度为m,求该序列中有多少位置不相同的长度为m的严格上升子序列。 spa
咱们在解决一些线性区间上的最优化问题的时候,每每也可以利用到动态规划的思想,这种问题能够叫作线性dp。在这篇文章中,咱们将讨论有关线性dp的一些问题。ios
在有关线性dp问题中,有着几个比较经典而基础的模型,例如最长上升子序列(LIS)、最长公共子序列(LCS)、最大子序列和等,那么首先咱们从这几个经典的问题出发开始对线性dp的探索。编程
首先咱们来看最长上升子序列问题。数组
这个问题基于这样一个背景,对于含有n个元素的集合S = {a一、a二、a3……an},对于S的一个子序列S‘ = {ai,aj,ak},若知足ai<aj<ak,则称S'是S的一个上升子序列,那么如今的问题是,在S众多的上升子序列中,含有元素最多的那个子序列的元素个数是多少呢?或者说这样上升的子序列最大长度是多少呢?数据结构
按照惯有的dp思惟,咱们将整个问题子问题化(这在用dp思惟解决问题时很是重要,基于此各子问题之间的联系咱们方能找到状态转移方程),咱们设置数组dp[i]表示以ai做为上升子序列终点时最大的上升子序列长度。那么对于dp[i]和dp[i-1],它们之间存在着以下的关系。学习
if(ai > ai-1) dp[i] = dp[i-1] + 1测试
else dp[i] = 1优化
这就是最基本的最长上升子序列的问题,咱们经过一个具体的问题来继续体会。(Problem source : hdu 1087)this
#include<stdio.h> #include<string.h> #include<algorithm> using namespace std; const int maxn = 1005; const int inf = 999999999; int a[maxn] , dp[maxn]; int main() { int n , m , ans; while(scanf("%d",&n) && n) { memset(dp , 0 , sizeof(dp)); for(int i = 1;i <= n;i++) scanf("%d",&a[i]); for(int i = 1;i <= n;i++) { ans = -inf; for(int j = 0;j < i ;j++) { if(a[i]>a[j]) ans = max(ans , dp[j]); } dp[i] = ans + a[i]; } ans = -inf; for(int i = 1;i <= n;i++) ans = max(ans , dp[i]); printf("%d\n",ans); } }
咱们再来看一道有关LIS增强版的问题。(Problem source : stdu 1800)编码
给定一个长度为n的序列(n <= 1000) ,记该序列LIS(最长上升子序列)的长度为m,求该序列中有多少位置不相同的长度为m的严格上升子序列。 spa
首先咱们看到,此题在关于LIS的定义上加了一个严格上升的,那么咱们在动态规划求解的时候稍微改动一下判断条件便可,这里主要须要解决的问题就是如何记录长度为m的位置不一样的严格上升子序列个数。
其实基于对最长严格上升子序列长度的求解过程,咱们只需在这个过程当中设置一个记录种类数的num[i]来记录当前以第i个元素为终点的最长严格上升子序列的种类数便可,而num[]又知足怎样的递推关系呢?
咱们联系记录最长上升子序列的长度的dp[]数组,在求解dp[i]的时候,咱们存在着这样的状态转移方程:
dp[i] = max{dp[j] | j ∈[1,i-1]) + 1 } 那么咱们能够在计算dp[i]的同时,记录下max(dp[j] | j∈[1,i-1])所对应的j1 、j2 、j3……那么此时咱们容易看到num[i]存在着以下的递推关系。
num[i] = ∑num[jk](k = 一、二、3……) 须要注意的是,根据其严格子序列的定义,在计算dp[i]的时候,须要有a[i] > a[j]的限制条件,一样,在计算num[i]的时候,也须要有a[i] > a[j]的限制条件。
参考代码以下。
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; int main() { int tt; int a[1005]; int dp[1005]; int num[1005]; scanf("%d",&tt); while(tt--) { memset(dp , 0 , sizeof(dp)); memset(num , 0 , sizeof(num)); int n; scanf("%d",&n); for(int i = 1;i <= n;i++) scanf("%d",&a[i]); dp[1] =1; num[1] = 1; for(int i = 2;i <= n;i++) { int Max1 = 0; for(int j = i - 1;j >= 1;j--) { if(a[i] > a[j]) Max1 = max(Max1 , dp[j]); } dp[i] = Max1 + 1; for(int j = i - 1;j >= 1;j--) { if(dp[j] == Max1 && a[i] > a[j]) num[i] += num[j]; } } int sum = 0; int Max = 0; for(int i = 1;i <= n;i++) Max = max(Max , dp[i]); for(int i = 1;i <= n;i++) if(dp[i] == Max) sum += num[i]; printf("%d\n",sum); } }
下面咱们来探讨另一个问题——最长公共子序列问题(LCS)。
LCS问题基于这样一个背景,对于集合S = {a[1]、a[2]、a[3]……a[n]},若是存在集合S' = {a[i]、a[j]、a[k]……},对于下标i、j、k……知足严格递增,那么称S'是S的一个子序列。(不难看出线性dp中的问题是基于集合元素的有序性的)那么如今给出两个序列A、B,它们最长的公共子序列的长度是多少呢?
基于对LIS问题的探讨,这里咱们能够作相似的分析。
首先咱们应作的是将整个问题给子问题化,采用与LIS类似的策略,咱们设置二维数组dp[i][j]用于表示以A序列第i个元素为终点、以B序列第j个元素为终点的两个序列最长公共子序列的长度。
其次咱们开始尝试创建状态转移方程,依旧从过程当中开始分析,考察dp[i][j]和它前面相邻的几项dp[i-1][j-1]、dp[i][j-1]、dp[i-1][j]有着怎样的递推关系。
咱们看到,这种递推关系显然会因a[i]与b[j]的关系而呈现出不一样的关系,所以这里咱们进行分段分析。
若是a[i] = b[j],显然这里咱们基于dp[i-1][j-1]的最优状况,加1便可。即dp[i][j] = dp[i-1][j-1] + 1。
若是a[i] != b[j],那么咱们能够看作在dp[i-1][j]记录的最优状况的基础上,给当前以A序列第i-1个元素为终点的序列A'添加A序列的第i个元素,而根据假设,这个元素a[i]并非当前子问题下最长子序列中的一员,所以此时dp[i][j] = dp[i-1][j]。咱们作同理的分析,也可获得dp[i][j] = dp[i][j-1],显然咱们要给出当前子问题的最优解方可以引导出全局的最优解,所以咱们不可贵到以下的状态转移方程。
dp[i][j] = max(dp[i-1][j] , dp[i][j-1])。
咱们将两种状况综合起来。
for i 1 to len(a)
for j 1 to len(b)
if(a[i] == b[j]) dp[i][j] = dp[i-1][j-1] + 1
else dp[i][j] = max(dp[i-1][j] , dp[i][j-1])
咱们经过一个简单的题目来进一步体会用这种dp思想解决LCS的过程。(Problem source : hdu 1159)
题目大意:给出两个字符串,求解两个字符串的最长公共子序列。
基于上文对LCS的分析,这里咱们只需简单的编程实现便可。
参考代码以下。
#include<stdio.h> #include<string.h> #include<algorithm> using namespace std; int const maxn = 1005; int dp[maxn][maxn]; int main() { char a[maxn] , b[maxn]; int i , j , len1 , len2; while(~scanf("%s %s",a , b)) { len1 = strlen(a); len2 = strlen(b); memset(dp , 0 , sizeof(dp)); for(i = 1;i <= len1;i++) { for(j = 1;j <= len2;j++) { if(a[i-1] == b[j-1]) dp[i][j] = dp[i-1][j-1] + 1; else dp[i][j] = max(dp[i][j-1] , dp[i-1][j]); } } printf("%d\n",dp[len1][len2]); } return 0; }
学习了基本的LIS、LCS,咱们会想,可否将二者结合起来(LCIS)呢?(Problem source : hdu 1423)
题目大意:给定两个序列,让你求解两个最长公共上升子序列的长度。
数理分析:基于对简单的LCS和LIS的了解,这里将两者的结合其实并不困难。不论在LCS仍是LIS中,咱们都用到了一维数组dp[i]来表示以第i为为结尾的区间的最优解,而这里出现了两个区间,咱们很天然的想到须要一个二维数组dp[i][j]来记录子问题的最优解。即用dp[i][j]表示序列一以第i个元素结尾和以序列二前第个元素结尾的LCIS的长度。
完成了子问题化,咱们开始对求解过程进行模拟分析以求获得状态转移方程。咱们定义序列一用数组a[]记录,序列二用数组b[]记录。
因为记录解的dp数组是二维的,咱们显然是须要肯定觉得而后遍历第二维,也就是两层循环枚举出全部的状况。假设咱们当前肯定序列一的长度就是i,咱们用参数j来遍历序列的每种长度。咱们能够找到以下的状态转移方程:
if (a[i] = b[j]) dp[i][j] = max{dp[i][k] | k ∈[1,j-1]}
基于这个状态转移方程咱们即可以编码实现了。
值得注意的一点是,在编程过程当中维护方程中max{dp[i][k] | k ∈[1,j-1]}的时候,须要注意必须知足a[i] > b[j]的,不然会使得该公共子序列不是上升的。
参考代码以下。
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn = 505; int a[maxn] , b[maxn]; int dp[maxn]; int main() { int t , m , n; scanf("%d",&t); while(t--) { scanf("%d",&n); for(int i = 1;i <= n;i++) scanf("%d",&a[i]); scanf("%d",&m); for(int i = 1;i <= m;i++) scanf("%d",&b[i]); memset(dp , 0 , sizeof(dp)); int pos; for(int i = 1;i <= n;i++) { pos = 1; for(int j = 1;j <= m;j++) { if(a[i]>b[j] && dp[j] + 1 > dp[pos]) pos = j; if(a[i] == b[j]) dp[j] = dp[pos] + 1; } } int Max = 0; for(int i = 1;i <= m;i++) Max = max(Max , dp[i]); printf("%d\n",Max); if(t) printf("\n"); } }
下面咱们讨论最大子序列和问题。
该问题依然是基于子序列的定义(上文已经给出),讨论一个整数序列S的子序列S',其中S'的全部元素之和是S的全部子序列中最大的。
而对于S'是连续子序列(即下标连续,如{a[1],a[2],a[3]}),仍是能够不连续的,咱们又要作出不一样的分析。
下面咱们首先讨论最大连续子序列和的问题。(Problem source : hdu 1231)
关于题设,咱们须要注意的一点是咱们在整个问题中只关注正值的大小,而对于结果是负值,咱们均可以视为等同,最大值为0,这一点在下面的问题分析中埋着伏笔。
有该问题是基于子序列元素的连续性,所以咱们在这里难以像上文中给出的两个例子同样对整个问题进行相似的子问题化。所以咱们在这里设置一个变量sum,用来动态地记录当前以S序列的第i个元素a[i]的最优解。
下面咱们开始模拟整个动态规划的过程。咱们起初S第一个元素依次日后开始构造连续子序列,并计算出当前sum的值,并维护一个最大值max_sum。
对于sum的值,有以下两种状况。
sum>0,则代表以前构造的序列能够做为有价值的前缀(由于题设并不关注负值的大小,所以这里便以0做为分界点),那么此时即可以在以往构造的和为sum的连续子序列即可以继续构造当前元素a[i]。
而当sum<0的时候,显然以往构造的和为sum的连续子序列就没有存在的价值了,当前抛弃这个和为负的前缀显然是最优的选择,所以咱们便开始从新构造连续子序列,起点即是这个第i个元素。
而整个过程是怎样实现对最优解的记录呢?显然,在向连续子序列添加第i个元素a[i]的时候,显然须要更新sum,那么在更新的同时完成对max_sum的维护,便完成了对最优解的记录。
而在这个具体问题中对最大和的连续子序列头尾元素的记录,也不难在更新sum和维护max_sum的值的时候完成。
能够看到,相比LCS,LIS,最大连续子序列和的的dp思想显得更加抽象和晦涩,没有显式状态转移方程,可是只要抓住dp思想的两个关键——子问题化和局部最优化,该问题也仍是能够分析的。
参考代码以下。
#include<stdio.h> using namespace std; const int N = 50005; int n_num; int num[N]; int main() { while(scanf("%d",&n_num) , n_num) { for(int i = 0;i < n_num;i++) scanf("%d",&num[i]); int sum , ans , st , ed , ans_st , ans_ed; ans_st = ans_ed = st = ed = sum = ans = num[0]; for(int i = 1;i < n_num;i++) { if(sum > 0) { sum += num[i]; ed = num[i]; } else st = ed = sum = num[i]; if(ans < sum) { ans_st = st , ans_ed = ed , ans = sum; } } if(ans < 0) printf("0 %d %d\n",num[0] , num[n_num - 1]); else printf("%d %d %d\n" , ans , ans_st , ans_ed); } return 0; }
上文给出了一个关于最大连续子序列和的比较抽象化的分析(连状态转移方程)都没给出。这源于笔者从一个比较抽象的角度来理解整个动态规划的过程,其实咱们这里依然能够模拟咱们在LCS、LIS对整个过程的分析。咱们这是数组dp[i]记录以序列S第i个元素为终点的最大和,那么咱们直接考察dp[i]和dp[i-1]的关系,容易看到dp[i-1]呈现出以下两种状态。
若是dp[i-1]是负值,则当前情况下最优的决策显然是抛去先前构造的以a[i-1]为终点的子序列,从a[i]从新构造子序列。
而若是dp[i-1]是正值,则在当前状况下,构造以a[i-1]为终点的子序列中,最优的决策显然是将a[i]放在a[i-1]后面造成新的子序列。须要注意的是,这里的最优状况是全部以a[i-1]为终点的子序列,而非全局的最优状况。
归纳来说,咱们能够获得这样的状态转移方程:
if(dp[i-1] < 0) dp[i] = a[i]
else dp[i] = dp[i-1] + a[i]
更加简练的一种写法以下。
dp[i] = max(dp[i-1] + a[i] , a[i])。
基于dp[1~n](n是序列S的长度),咱们获得了全部子问题的解,随后找到最优解便可。
能够看到,比较对最大连续子序列和的两种分析方式,其核心的动态规划思想是本质相同的,稍有区别的是前者在动态规划的过程当中已经在动态维护着最优解,然后者则是先将全局问题给子问题化而后获得各个子问题的答案,最后遍历一遍子问题的解空间而后维护出最大值。相比较而言,前者效率更高可是过程较为抽象,后者效率偏低可是很好理解。
咱们结合一个问题来体会一下这种对最大连续子序列和的方法。(Problem source : hdu 1003)
基于上文的分析,咱们容易找到最大的和,同时该题须要输出该子序列的首尾元素的下标,根据dp[]数组的内涵,咱们在维护最大和的时候能够记录下尾元素的下标,而后经过该元素的位置往前(S序列中)依次相加判断什么时候获得最大和即可以获得首元素下标。根据题设的第二组数据不难看出,在最大和相同的时候,咱们想让子序列尽可能长,那么在编程实现小小的处理一下细节便可。
参考代码以下。
#include<cstdio> #include<string.h> using namespace std; const int maxn = 100000 + 5; int main() { int t; scanf("%d",&t); int tt = 1; while(t--) { int a[maxn] , dp[maxn]; int n; scanf("%d",&n); for(int i = 0;i < n;i++) scanf("%d",&a[i]); dp[0] = a[0]; for(int i = 1;i < n;i++) { if(dp[i-1] < 0) dp[i] = a[i]; else dp[i] = dp[i-1] + a[i]; } int Max = dp[0]; int e_index = 0; for(int i = 0;i < n;i++) { if(dp[i] > Max) Max = dp[i] , e_index = i; } int temp = 0; int s_index = 0; for(int i = e_index;i >= 0;i--) { temp += a[i]; if(temp == Max) s_index = i ; } printf("Case %d:\n%d %d %d\n",tt++,Max , s_index + 1, e_index + 1); if(t) printf("\n"); } }
讨论了线性dp几个经典的模型,下面咱们便要开始对线性dp进一步的学习。
让咱们再看一道线性dp问题。(Problem source : hdu 4055)
题目大意:给出一个长度为n-1的字符串用于表示[1,n]组成的序列的增减性。若是字符串第i位是I,表示序列中第i位大于第i-1位;若是字符串第i位是D,相反;若是是?,则没有限制。那么请你求解有多少个符合这个字符串描述的序列。
数理分析:容易看到,该题目是基于[1,n]的线性序列的,所以这里咱们能够想到用区间dp中的一些思惟和方法来解决问题。咱们看到对于每种状态有两个维度的描述,一个是当前序列的长度,而另外一个则是当前序列末尾的数字(由于字符串给出的是相邻两位的增减关系,咱们应该可以想到须要记录当前序列末尾的数字以进行比较大小,另外LIS等经典线性dp也是采用相似的方法)。
那么咱们就能够很好的进行子问题化了,设置dp[i][j]表示长度为i,序列末尾是数字j,并符合增减描述的序列种类数。
下面即是寻求状态转移方程。咱们从中间状态分析。定义s[]表示记录序列增减性的字符串。
①s[i-1] = ? => dp[i][j] = ∑dp[i-1][k] (k∈[1,i-1])
②s[i-1] = I => dp[i][j] = ∑dp[i-1][k] (k∈[1,j-1])
③s[i-1] = D => dp[i][j] = ∑dp[i-1][k] (k∈[1,i-1]) - ∑dp[i-1][k] (k∈[1,j-1])
对于∑的形式在计算的时候显得有点繁琐,每次访问都须要扫一遍,计算时间上显得有点捉急,为了访问的简便,咱们设置sum[i][j]表示长度为i,序列最后一个数字小于等于j的符合要求的序列总数,即sum[i][j] = ∑dp[i][k] (k ∈[1,j]),由此咱们能够简化一下状态转移方程,并在求解过程当中维护sum[i][j]的值。
①s[i-1] = ? => dp[i][j] = sum[i-1][i-1]
②s[i-1] = I => dp[i][j] = sum[i-1][j-1]
③s[i-1] = D => dp[i][j] = sum[i-1][i-1] - sum[i-1][j-1]
而对于最终解,对于长度为n的字符串,序列应有n+1个元素,而显然最后一个元素必定小于等于n+1,即sum[n+1][n+1]为最终解。
另外这道问题有一个值得注意的点,即是若是咱们如今填充第i位,咱们基于一个[1,i-1]的子问题,而数字i其实能够混入到这个子问题的符合要求的序列当中,此时咱们若将i所在的位置换成i-1,这即是一个子问题,而这个位置如今是i,实际上并不妨碍这个序列的增减性(i和i-1都是这个序列中最大的数字),所以咱们在填充第i个数的时候,考虑那种特殊状况,本质上开始考虑[1,i-1]的子问题。
基于以上的数理分析,咱们不难进行编码实现。
参考代码以下。
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int maxn = 1005; const int Mod = 1000000007; int dp[maxn][maxn] , sum[maxn][maxn]; char str[maxn]; int main() { while(scanf("%s",str + 2) != EOF) { memset(dp , 0 , sizeof(dp)); memset(sum , 0 , sizeof(sum)); int len = (int)strlen(str + 2); dp[1][1] = 1 , sum[1][1] = 1; for(int i = 2;i <= len + 1;i++) { for(int j = 1;j <= i;j++) { if(str[i] == 'I') dp[i][j] = (sum[i-1][j-1])%Mod; if(str[i] == 'D') { int temp = ((sum[i-1][i-1]-sum[i-1][j-1])%Mod + Mod)%Mod; dp[i][j] = (dp[i][j] + temp)%Mod; } if(str[i] == '?') dp[i][j] = (sum[i-1][i-1]) % Mod; sum[i][j] = (dp[i][j] + sum[i][j-1])%Mod; } } printf("%d\n",sum[len+1][len+1]); } return 0; }