給定一個長度爲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; }