[HNOI2009]雙遞增序列(洛谷P4728)+小烈送菜(內部訓練題)——奇妙的dp


博主學習本題的經過嚶嚶嚶:

7.22 : 聽學長講(一知半解)——自己推(推不出來)——網上看題解——以為自己會了(網上題解是錯的)——發現錯誤以后又自己推(沒推出來)——給學長發郵件——得到正確解法——按着學長思路又推一遍——最后理解

(前后的“學長”不是同一個人)

7.23 : 寫出代碼,完善細節。

(建議改成:西         經)

首先,網上對於這道題的題解絕大部分錯誤的!(比如洛谷上的部分題解)

用LIS做是不行

玄學貪心是不行

dp轉移方程不能自圓其說是不行

即使是AC代碼也不一定正確的(2009年的省選,數據太水了嚶嚶嚶)

廢話說完了

~~~~~~~~~~~~~~~嚶嚶來自蒟蒻OIerOrzer的分割線啦嚶嚶嚶~~~~~~~~~~~~~~~~

以下為正文部分嚶嚶嚶:

考慮把一個數列分成兩個集合,有a[i]的為一個集合,沒有a[i]的為一個集合~

我們定義狀態轉移方程dp[i][j]表示對於前i個數,有a[i]的集合的長度為j,沒有a[i]的集合的最后一個數的最小值為dp[i][j](神仙定義)

也就是說,現在有兩個集合,其中一個有a[i],另一個沒有a[i]。嘗試把a[i+1]放到其中一個集合中。

1.嘗試把a[i+1]放到有a[i]的集合當中。那么需要滿足的先決條件就是:a[i+1]>a[i].

此時更新dp[i+1][j+1](因為把a[i]放到長度為j的集合中,於是長度++)此時沒有a[i+1]的集合同時也是沒有a[i]的集合,換句話來說,這個轉移對沒有a[i]的集合是沒有改變的,所以,dp[i+1][j+1]可以直接由dp[i][j]繼承過來。

2.嘗試把a[i+1]放到沒有a[i]的集合當中。那么需要滿足的先決條件就是:a[i+1]>dp[i][j].

此時更新dp[i+1][i-j+1](原來沒有a[i]的集合的長度為(i-j),把a[i+1]放進去,長度++)既然把a[i+1]放到了沒有a[i]的集合中,那么,沒有a[i+1]的集合的最后一個數就是a[i],於是,用a[i]去更新dp[i+1][i-j+1];

(真繞啊嚶嚶嚶)

上代碼嚶嚶嚶:

 

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 const int maxn=2000+10;
 4 int dp[maxn][maxn],a[maxn];
 5 int n,m;
 6 void Solve(){
 7     scanf("%d",&m);
 8     while(m--){
 9         scanf("%d",&n);
10         memset(dp,0x3f,sizeof(dp));
11         memset(a,0x3f,sizeof(a));
12         for(int i=1;i<=n;++i) scanf("%d",&a[i]);
13         dp[1][1]=-1;//因為數據中可能有0,因此不能初始化為0;
14         for(int i=1;i<=n;++i){
15             for(int j=1;j<=i;++j){
16                 if(a[i+1]>a[i]) dp[i+1][j+1]=min(dp[i+1][j+1],dp[i][j]);
17                 if(a[i+1]>dp[i][j]) dp[i+1][i-j+1]=min(dp[i+1][i-j+1],a[i]);
18             }
19         }
20         if(dp[n][n/2]>1e8) printf("No!\n");
21         //沒有更新,說明不能將原序列合法地平分成兩部分,就輸出No;
22         else printf("Yes!\n");
23     }
24 }
25 int main(){
26     Solve();
27     return 0;
28 }

 完結撒花嚶嚶嚶~(然而並沒有)

 

上輩子的題了(大霧)(所以這就是火星水嗎)

但是在解決上一道題后會發現對於這道題會有不一樣的理解~~~

我們先定義dp[i][j]表示小烈1走到i,小烈2走到j時的最大收益。且默認小烈1始終在小烈2前面,且前j個已經被送完。

換句話來說,其實根本沒有小烈1和小烈2(只是我們yy出來的)或者說,並不是划分了“小烈1”和“小烈2”這兩個抽象的概念,划分的應該是“送了a[i]的小烈”和“沒有送a[i]的小烈”!

(woc這不就是上一道題嗎,太像了好叭)

現在我們換一種表達方式來定義dp[i][j]:

dp[i][j]表示前i個客人,沒有送過a[i]的小烈最后一個送的是a[j]時的最大收益。

默認j小於i,且前j個一定已經送過了(如果前j個有沒有送過的,那就不合法了,因為兩個小烈不能回頭)

現在考慮a[i+1]由誰送。

1.由送了a[i]的小烈送。所以,沒有送a[i]的小烈同樣沒有送a[i+1]。也就是說,這個轉移完成后,沒有送a[i]的小烈原來在j,現在還是在j,沒有變化。

所以用(dp[i][j]+a[i]*a[i+1])更新(dp[i+1][j])。

2.由沒有送a[i]的小烈送。所以,送了a[i]的小烈沒有送a[i+1]。也就是說,沒有送a[i+1]的小烈最后一個送的是a[i]!

所以用(dp[i][j]+a[j]*a[i+1])更新(dp[i+1][i]).

(和剛才那個一樣繞嚶嚶嚶

上代碼:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 const int maxn=2500+10;
 4 int dp[maxn][maxn],a[maxn],ans;
 5 void Solve(){
 6     int n;scanf("%d",&n);
 7     for(int i=1;i<=n;++i) scanf("%d",&a[i]);
 8     for(int i=1;i<=n;++i){
 9         for(int j=0;j<i;++j){//j的范圍要搞清楚哦;
10             dp[i+1][j]=max(dp[i+1][j],dp[i][j]+a[i]*a[i+1]);
11             dp[i+1][i]=max(dp[i+1][i],dp[i][j]+a[j]*a[i+1]);
12         }
13     }
14     for(int i=0;i<n;++i) ans=max(ans,dp[n][i]+a[i]*a[n]);
15     /*現實中的小烈是從1走到n,又從n回去,在dp方程里面我們把一個小烈拆成了兩個,
16     其中一個表示現實中正向走的部分,另一個表示現實中反向走的部分(但是令這一個
17     反向走的小烈反過來走,就是正着走)也就是說,當現實中小烈走到n,開始返回的
18     時候,a[n]與dp方程中定義的小烈2經過的最后一個a[i](也就是反着走的第一個
19     a[i])是要產生一個值的,而這個值要加到答案里面才能得到最終結果*/
20     printf("%d",ans);
21 }
22 int main(){
23     Solve();
24     return 0;
25 }
嚶嚶嚶

完結撒花花!!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM