關於二維DP————站上巨人的肩膀

你的小垃圾發表於2022-03-22

意匠慘淡經營中ing,

語不驚人死不休........

前幾天學了DP,做了個簡單的整理,記錄了關於DP的一些概念之類的,今天記錄一下剛學的一個型別

————關於二維DP

那建立二維陣列主要是幹嘛用的呢???其實就是是記錄兩個狀態,(我也不是很清楚),然後再遞推

直接上題吧

 

T1 、最長公共子序列:

好像有印象,之前做過一道類似的題,叫做最長上升子串,需要注意的是,這倆可不是很一樣,人家子序列可以不連續,只要是相對順序不改變就行,但是子串必須是連續的,針對這種題,我們聯想起原來做題時想的狀態轉移方程,再加以改動就可以了:

f[i][j] 還是表示陣列a的前i個元素和b陣列的前j個元素能組成的最長子串的長度

那很容易以相同的方法聯想到子序列,還是分情況討論:

1、x[i]==y[j]時:f[i][j]=f[i-1][j-1]+1 (就是它們如果相等,長度就是不加上它們時組成的子串的長+1)

2、x[i]!=y[j]時:

1 2 3 4 5  和  2 3 5,這兩個子串在 i=4,j=2 的時候,由於a[i]!=b[j],加上它們倆和不加他們倆其實並不影響最長長度,所以i和j對應的他們以前這些元素(不包括i,j)能組成的最長長度其實和它們現在(包括i,j)能組成的長度是相同的(不必+1),所以當a[i]!=b[j],它們對應的長度可以是f[i-1][j],也可以是f[i][j-1],取個max就好

再考慮邊界,當一個字串有0個元素時,它們的f[i][j]永遠是0,所以f[0][j]=0 ,f[i][0]=0

程式碼如下:

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cmath>
 4 #include<cstring>
 5 using namespace std;
 6 int f[1001][1001];
 7 char x[1001],y[1001];
 8 int maxn;
 9 int main()
10 {
11     cin>>x;
12     cin>>y;
13     int m=strlen(x);
14     int n=strlen(y);
15     for(int i=0;i<m;i++)
16         for(int j=0;j<n;j++)
17         {
18             f[i][0]=0;
19             f[0][j]=0;
20         }
21     for(int i=1;i<=m;i++)
22     {
23         for(int j=1;j<=n;j++)
24         {
25             if(x[i-1]==y[j-1]) f[i][j]=f[i-1][j-1]+1;//人家是從0開始納入的,所以這裡要-1
26             else f[i][j]=max(f[i-1][j],f[i][j-1]);
27             maxn=maxn>f[i][j]? maxn:f[i][j];    
28         }
29     }    
30     printf("%d",maxn);
31     return 0;
32 }

 

 

T2、編輯距離:

題意:有兩個字串,現在有三種操作:1、刪除一個字元,2、插入一個字元,3、將一個字元改成另一個字元,現在要求要麼花最少的字元操作次數,將字串A轉成B(只能操作A串)。

還是先設變數,f[i][j]表示把x[1~i]變為y[i~j]的最少操作次數,現在的目標就是求出狀態轉移方程:

還還還是分兩種情況討論:

x[i]==y[j] 時,那我們就可以不對它們進行操作了,也就是說我們直接讓f[i][j]=f[i-1][j-1]

 x[i]!=y[j] 時,我們有三種操作,需要分別求出他們的狀態轉移方程

1、刪除 x[i],那就是上一次的操作次數再加上1,即 f[i][j]=f[i-1][j]+1

2、給x[i]插入新字元:那就是相當於給y[j]刪去一位,即 f[i][j]=f[i][j-1]+1

3、將x[i]變為y[j]:目前a陣列的前i位和b陣列的前j位都一樣了,那其實就是給上一次的狀態的基礎上再操作一次,即f[i[[j]=f[i-1][j-1]+1

還有就是邊界條件:當有一個陣列為空時,我就全部插入(x陣列為空),或全都輸出(y陣列為空),即 f[i][0]=i  ,f[0][j]=j

程式碼如下:

 

 1 #include<iostream>
 2 #include<cstdio> 
 3 #include<cmath>
 4 using namespace std;
 5 string a,b;
 6 int ans;
 7 int f[2001][2001];
 8 int main()
 9 {
10     cin>>a>>b;
11     int lena=a.size();
12     int lenb=b.size();
13     for(int i=1;i<=lena;i++){
14         f[i][0]=i;
15     } 
16     for(int i=1;i<=lenb;i++){
17         f[0][i]=i;    
18     }
19     for(int i=1;i<=lena;i++){
20         for(int j=1;j<=lenb;j++){
21             if(a[i-1]==b[j-1]) f[i][j]=f[i-1][j-1];
22             else f[i][j]=min(min(f[i-1][j]+1,f[i][j-1]+1),f[i-1][j-1]+1);
23         }
24     }
25     cout<<f[lena][lenb];
26     return 0;
27 }

 

 

 

T3、機器分配問題(難得要命)

題目差不多就是說現在有n個公司,一共有m臺,現在輸入了一個n*m的矩陣,分別代表第i個公司如果分配j臺機器的話的盈利,現在求最大盈利值,並輸出每個公司應分配多少臺機器

首先設一個二維陣列f[i][j]代表前i個公司分配j臺裝置的最大盈利,那我們可以把f[i][j]分為兩個階段:

那就是把狀態 i 分為前 i-1個公司和第 i 公司,設前 i-1 個公司分配了k個機器,那第 i 個公司就剩下了 j-k 臺,我們讓k從0到 j 挨個取一遍,求出最大值就好了,那狀態轉移方程就寫出來了:

for( k=0 ; k<=j ; k++) 

if ( f[i][j] <= f[i-1][k]+a[i][j-k] )

f[i][j] = f[i-1][k] + a[i][j-k]  

那麼我們這個a[i][j]咋來呀,仔細想一想,a[i][j]不就表示的是第 i 個公司分配 j 臺機器的盈利嗎,那就是我們要輸入的那個二維陣列呀!!!

邊界是啥???

當 i =0 時,f[0][j] 相當於 0 個公司分配 j 臺機器,那最大利潤就是 0 (連公司都沒有,肯定不會有利潤),即 f[0][j]=0

當 j =0 時,f[i][0] 相當於 i 個公司分配 0 臺機器,那最大利潤就是 0 (連機器都沒有,肯定不會有利潤),即 f[i][0]=0

你以為就這樣愉快地結束了嗎???

還要輸出每個公司分配的機器數量呢,和原來我們求出最長上升子序列一樣,我們還要記錄用一個陣列記錄前驅,用於最後輸出

實現起來就是這樣的,如果 f[i][j] <= f[i-1][k]+a[i][j-k],那麼在給f[i][j]重新賦值的時候,我們順便記錄一下前驅,即p[i][j]=k,(代表前i-1個公司要用多少臺機器)。在輸出最大利潤之後,從n到1開始遞減,定義一個t=m(就是t臺機器)來表示當前機器數量,建立一個ans陣列來記錄答案,先讓 ans=t-p[i][t],原因是t是當前的機器數量。減去p[i][t](前i-1個用的機器數量),就是第 i 個公司用的機器數量,然後我讓 t=p[i][t]代表前 i-1 個公司的用的機器總數,也就是讓當前總數變成除去第i個公司用的機器後的機器的當前數量(因為我們現在已經不管第i個公司了,所以才要把它從總數量中除去使得目前的機器總數變成前 i-1 個的公司所用的機器總數)。然後重複以上操作,我們就得出了每個公司要用的機器數量,再輸出就好了

程式碼放下面了

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cmath>
 4 #define int long long 
 5 using namespace std;
 6 int ans=100000;
 7 int a[1001][1001],f[1001][1001],p[1001][1001],ans1[1001];
 8 //a用於記錄資料,f[i][j]代表前i個公司用了j臺機器,p是代表第i個公司用了多少臺電腦,ans1是答案
 9 signed main()
10 {
11     int n,m;
12     scanf("%lld%lld",&n,&m);
13     for(int i=1;i<=n;i++){
14         for(int j=1;j<=m;j++){
15             scanf("%lld",&a[i][j]);}
16     }
17     for(int i=0;i<=n;i++){
18         for(int j=0;j<=m;j++){
19             f[0][j]=0;
20             f[i][0]=0;}//邊界條件
21     }
22     for(int i=1;i<=n;i++)
23     {
24         for(int j=0;j<=m;j++)
25         {
26             for(int k=0;k<=j;k++)
27             {
28                 if(f[i][j]<f[i-1][k]+a[i][j-k])//要不要換
29                 {
30                     f[i][j]=f[i-1][k]+a[i][j-k];//換!
31                     p[i][j]=k;//記錄第前i-1個公司用幾臺機器
32                 }
33             }
34         }
35     }
36     cout<<f[n][m]<<endl;//輸出最大利潤
37     int t=m;//相當於一共t臺機器
38     for(int i=n;i>=1;i--)
39     {
40         ans1[i]=t-p[i][t];//記錄第i個公司用了多少臺
41         t=p[i][t];//表示當前去除第i個公司後的機器數量
42     }
43     for(int i=1;i<=n;i++){
44         cout<<i<<" "<<ans1[i]<<endl;//優美的輸出
45     }
46     return 0;//結束
47 }

 

T4、乘積最大(這是嫖的旁邊的大佬的,所以這道題我是以大佬做題時思考的角度來寫的)

直奔正題,分為幾個步驟:

1、建立幾個陣列:①a陣列代表我們想要得到的輸入的陣列,②f[i][j]代表1~i之間有j個乘號時狀態的最大值

2、讀入陣列: 讀題發現輸入的是一個字元陣列,遇到不要慌,我只要把它轉成數字陣列就好了,然後就寫一個read函式來記錄下來想要的a陣列。

3、首先記錄如果有0個乘號,那麼那我們記錄下來如果沒有稱號的話從1到i組成的 i 位數,

也就是

1 for(int i=1;i<=n;i++){
2         f[i][0]=f[i-1][0]*10+a[i];
3     }

4、現在我們來分析一下:

我們首先要明確一個前提,就是階段後面都有一個乘號,而一個階段裡是隻有數字的。

現在我已經處理到第f[i][j]這個狀態了,畫一個圖:

 

                    這裡有 j-1個乘號                 從第 l+1到第 i 個數字之間沒有乘號,他們這些數共同構成了一個數        當前的第i個數字 

      |————*...*...*....*——————————*|—————.......(都是數).........——————————————|

 1                 前j-1個乘號                     變數 l,也就是第j個乘號的位置                                當前狀態:第i個數前面有j個1乘號 

 

目前i前面有j個乘號,我們把這些乘號分成兩段,一段是前 j - 1個乘號,另一段是第 j 個乘號,因為每隔一個乘號就會有兩個數,所以前 j-1 個乘號間至少會有 j 個數,也就是說,狀態f[i][j]前的那個乘號一定會在第j個數(能取到j)和第i個數(不能取到i)之間,那我們設一個變數 l ,為第 j 個乘號的位置,那麼j <= l < i這個發現會對找出狀態轉移方程發揮巨大作用,現在的目標就是找出狀態轉移方程:

當前狀態我們要選取第 j 個乘號的位置,那就是取前 j-1 個乘號的階段的狀態 乘上 在 l~i 的位置的陣列成的數,首先定義一個函式wucheng,這裡代表從上一個乘號到下一個乘號之間的數字組合成的一個多位數(一位數字也有可能),前面的狀態是前 l個數字(為啥是前 l 個數字?看圖)有j-1個乘號(j-1的原因是因為這個狀態我又取了一個乘號,總共有j個,那前面的階段就是j-1個),那就是f[l][j-1],而從上個乘號到這個乘號之間的數就是從第 l-1 位到第 i 位的每個數字組成的一個數字wucheng(l+1,i),他們倆一乘就是取了乘號後的狀態,所以得到方程

f[i][j] = max( f[i][j],f[l][j-1] * wucheng(l+1,i))

還有一點需要注意的是, j 不能無限加下去,一共只有 k 個乘號,所以 j 必須小於等於k,然後就沒事了

OK,上程式碼

 1 #include<iostream>
 2 #include<cmath>
 3 #include<cstdio> 
 4 #define ll long long
 5 using namespace std;
 6 ll n,k;//k代表總共輸入多少乘號,n代表一共輸入多少個數字 
 7 ll a[50];
 8 void read()
 9 {
10     char b=getchar();
11     while(b<'0'||b>'9')
12     {
13         b=getchar();
14     }
15     int cnt=1;
16     a[cnt]=b-'0';
17     while(cnt<=n)
18     {
19         b=getchar();
20         a[++cnt]=b-'0';
21     }//得到陣列a 
22 }
23 int f[50][10];
24 int wucheng(int i,int j)
25 {
26     int cnt=0;
27     for(int l=i;l<=j;l++)
28     {
29         cnt*=10;
30         cnt+=a[l];
31     }
32     return cnt;
33 }//定義這個函式,是為了記錄從l到i之間的數字組成的那一個多(單)位數 
34 int main(){
35     scanf("%d%d",&n,&k);
36     read();
37     for(int i=1;i<=n;i++)
38     {
39         f[i][0]=f[i-1][0]*10+a[i];
40     }
41     for(int i=2;i<=n;i++)//從第一個數到最後一個數挨個列舉 
42     {
43         for(int j=1;j<=i-1&&j<=k;j++)//乘號個數,不能多餘k個 
44         { 
45             for(int l=j;l<i;l++)
46             {
47                 f[i][j]=max(f[i][j],f[l][j-1]*wucheng(l+1,i));
48             }//l後面就沒有乘號了,挨個列舉l,就能得到最優解
49             //我們想要最優的l位置,那就找到max就好了
50         }
51     }
52     cout<<f[n][k];
53     return 0;
54 }

 

T5、複製書稿

題目描述:輸入兩個數分別表示m本書和n個人抄,第二行輸入m個數,表示每本書抄寫所用的時間,現在讓這n個人同時抄這些書,求如何分配才能使得總時間最少,要求:每個人抄的書幾本必須是連續的,而且希望第一個人抄的最少,輸出一共n行,每行代表第幾個人抄的書是從第幾本到第幾本,中間用一個空格分隔

覺得這道題好像跟機器分配還是有些像的,還是設 f[i][j] 為前i個人抄寫前j本書花的時間的最小值,也還是設前 i-1 個人抄了前 l 本書,第 i 個人抄了從第 l+1 到第 j 本書,再設一個二維陣列 d來表示從抄第1本書到第 j 本書所用的時間,也就是字首和,我需要提前求的,這樣我們就能求得當只有一個人的時候抄書的時間(也算是找到起始資料)。

現在來講講思路,第一步肯定是求出這個他們當中花的時間最多的對吧,這就要用到DP,第二步要用貪心,因為我要保證第一個人花的時間最少(用貪心的原因是第一個人花時間最少的前提就是總時間最少,所以就是說第一個人花的時間的或多或少只要不會讓時間變長,就不會影響最短的總時間,也就是說子問題最優解不會影響整個問題的最優解,所以用貪心),再從後往前遞推就得到每個人的分配情況

所以現在有兩個問題需要解決:  1、怎麼用DP?狀態轉移方程怎麼寫?  2、怎麼用貪心? 怎麼遞推?

首先解決第一個問題:首先還是原來的老套路,用一個二維陣列 f[i][j] 來表示前 i 個人抄了前 j 本書,也還是將 i 個人分成兩段,第一段是前 i-1 個人,設他們抄了 l 本書,(因為每個人至少抄一本,那這些人抄的書 l 必須大於等於人數 i-1,而且一共只有j 本書,保證最後一個人還有書抄,l 必須小於 j,所以列舉 l 的時候範圍就是 l=i-1; l<j)那麼第二段,也就是第 i 個人就抄了 j-l 本書。

那為了求出最優解,我們就要求出抄書時間最長的人抄書的最短時間,首先比較前 i-1 個人抄書的最長時間(f[i-1][l]),與第 i 個人抄書的時間進行比較( f[1][j] - f[1][l],我用字首和表示的 ),得到最長的花的時間,再遍歷每一個 l ,得到其中的最小值(因為我們想讓這些人裡抄書時間最長的人花的時間最少)

一點也不繞,我們很容易就能得到狀態轉移方程:

f[i][j] = min( f[i][j] ,max( f[i-1][l] , f[1][j] - f[1][l] ) )

然後就是第二個問題:貪心!

我用了一個DFS

 1 void dfs(int i,int j,int t)//為了遞推用的 
 2 {
 3     if(j==0)return ;
 4     if(i<j||f[1][t]-f[1][i-1]>f[k][m])
 5     {
 6         dfs(i,j-1,i);
 7         cout<<i+1<<" "<<t<<endl;
 8         return ;
 9     }
10     dfs(i-1,j,t);
11 }

首先要明確的是,如果從後往前列舉,發現有一段的數字(抄書時間)之和小於等於最長時間而且再抄一本書就會超過最長時間,那這段數就是當前的人需要抄寫的

首先頭頂上的那三個變數中,j 代表目前有多少個人,i 和 t 代表從m開始列舉的兩個數(我講講你就知道啥意思了,現在不太好說),i 一般是在 t 的前面。

如果只有0個人(j==0),直接返回。

現在我一直dfs並讓  i--,為啥呢?讓 i 一直減減,如果 f[1][t]-f[1][i-1]>f[k][m],(還是用的字首和)我就可以從後往前找到兩個數使得他們之間的數的和小於等於所有人抄書最長要用的時間(最長時間也就是 f[k][m],我們剛才用DP求出來了,其中 k 代表我們要輸入的人數,m 是我們要輸入的書的本數)這樣就能把這些數字當成一個人抄的書,之後讓人數 j 減1,讓當前的人分配到這些書。

或者是當 i < j 時,就說明要讓多餘書的個數的人分這些書,那肯定有人拿不著啊,由於不讓划水,還是讓人數 j 減1,代表這個人應該分配到這本書。

然而,如果不滿足這兩個限制,那你就繼續 dfs(i-1,j,t),直到找出滿足這兩個條件其中之一的數段

至於為啥要從後往前列舉,這樣可以滿足第一個人最少(可以自己想想)

程式碼如下:

 1 #include<iostream>
 2 #define INF 0x3f3f3f
 3 using namespace std;
 4 int read(){
 5     int x=0,f=1;
 6     char a=getchar();
 7     while(a<'0'||a>'9'){
 8         if(a=='-')f=-1;
 9         a=getchar();
10     }
11     while(a>='0'&&a<='9'){
12         x*=10;
13         x+=a-'0';
14         a=getchar();
15     }
16     return x*f;
17 }
18 int m,k;
19 int a[510];
20 int f[510][510];
21 void dfs(int i,int j,int t)//為了遞推用的 
22 {
23     if(j==0)return ;
24     if(i<j||f[1][t]-f[1][i-1]>f[k][m])
25     {
26         dfs(i,j-1,i);
27         cout<<i+1<<" "<<t<<endl;
28         return ;
29     }
30     dfs(i-1,j,t);
31 }
32 int main(){
33     m=read();
34     k=read();
35     for(int i=1;i<=m;i++)
36     {
37         a[i]=read();
38         f[1][i]=f[1][i-1]+a[i];
39     }
40     for(int i=2;i<=k;i++)
41     {
42         for(int j=i;j<=m;j++)
43         {
44             f[i][j]=INF;
45             for(int l=i-1;l<j;l++){
46                 f[i][j]=min(f[i][j],max(f[i-1][l],f[1][j]-f[1][l]));
47             }
48         }
49     }
50     dfs(m,k,m);
51     return 0;
52 }

 

記錄的也不多,就先到這裡吧 

小生才疏學淺,孤陋寡聞,記錄的東西也不算多好,以後會查缺補漏,願日臻完善!

也十分感謝zyb大哥的幫助,這些題的思路包括程式碼實現都是人家資助的

再見!2022/3/20

 

相關文章