#Dynamic Programming(DP) 動態規劃刷題小結html
傳送門:https://www.luogu.com.cn/problem/P1541c++
題目中先給出一個長度爲$n$的序列(咱們把它叫作序列$g$),其中$g_i$表示第$i$個格子的得分數組
另外,有四種卡片,每一種能夠走不一樣的步數,當走到第$i$個格子,就能獲得$g_i$的分數,求最大得分優化
本題數據範圍較小$(\leq40)$,咱們根據DP的多一種狀態升一維的原則,(反正數據範圍小那我數組不是亂開嗎qwq),能夠考慮開四維數組$f[40][40][40][40]$spa
因而,如今咱們能夠用$f_$表示第1、2、3、四種牌分別用了$i、j、k、l$張code
注意到,第1、2、3、四種牌分別能夠走$一、二、三、4$步,那麼$f_\(就表示當前在第\)(i+2j+3k+4l)$個格子的最大得分htm
題目中給出:咱們能夠直接得到$g_1$的分數,因此$f_{0000}=g_1$,這是咱們的初始狀態blog
如今考慮$f_$能夠由哪些狀態轉移獲得:get
$ \begin if(i-1\geq 0) f_{(i-1)jkl}\rightarrow f_\ \ if(j-1\geq 0) f_{i(j-1)kl}\rightarrow f_\ \ if(k-1\geq 0) f_{ij(k-1)l}\rightarrow f_\ \ if(l-1\geq 0) f_{ijk(l-1)}\rightarrow f_\ \end $string
其中,「\(\rightarrow\)」表示能夠由前一種狀態轉移到後一種狀態
能夠發現,轉移時就是至關於選了一張卡牌,咱們用$ovo$表示轉移後到了第$ovo$格,因此有$ovo=i+2j+3k+4l$
那麼咱們此次轉移的新增得分也就是$g_$了,如今咱們用$f_\(和轉移以前的分數\)+g_$取$max$就獲得了狀態轉移方程
下面是書面的狀態轉移方程:
$ \begin if(i-1\geq 0) f_=max(f_,f_{(i-1)jkl})\ \ if(j-1\geq 0) f_=max(f_,f_{i(j-1)kl})\ \ if(k-1\geq 0) f_=max(f_,f_{ij(k-1)l})\ \ if(l-1\geq 0) f_=max(f_,f_{ijk(l-1)})\ \end $
有了狀態轉移方程,代碼就$so easy$了!!!
###\(Code\)
#include<algorithm> #include<cstdio> const int maxn=45; int f[maxn][maxn][maxn][maxn],g[355]; int n,m,a,b,c,d; int main(){ scanf("%d%d",&n,&m); for(int i=0;i<n;i++) scanf("%d",g+i); for(int i=0,x;i<m;i++){ scanf("%d",&x); if(x==1) a++; else if(x==2) b++; else if(x==3) c++; else if(x==4) d++; }//統計每一種牌出現的數量,分別存到a,b,c,d裏 f[0][0][0][0]=g[0]; for(int i=0;i<=a;i++)//從一張都沒有到所有選擇完循環四種牌 for(int j=0;j<=b;j++) for(int k=0;k<=c;k++) for(int l=0;l<=d;l++){ int OvO=i*1+j*2+k*3+l*4;//注意不能選負數張牌,因此特判一下 if(i!=0) f[i][j][k][l]=std::max(f[i][j][k][l],f[i-1][j][k][l]+g[OvO]); if(j!=0) f[i][j][k][l]=std::max(f[i][j][k][l],f[i][j-1][k][l]+g[OvO]); if(k!=0) f[i][j][k][l]=std::max(f[i][j][k][l],f[i][j][k-1][l]+g[OvO]); if(l!=0) f[i][j][k][l]=std::max(f[i][j][k][l],f[i][j][k][l-1]+g[OvO]); }//這四個都是剛纔的轉移方程 printf("%d\n",f[a][b][c][d]);//題目保證全部牌恰好用完,因此答案就是f[a][b][c][d] return 0; }
##例題2:滑雪 傳送門:https://www.luogu.com.cn/problem/P1434
本題正解是一個記憶化搜索,可是因爲本人太蒻,不會記搜,只好用DP來作這個題
安利一下大佬的記搜題解叭:https://www.cnblogs.com/sxy2004/p/13353747.html
題目中給出一個大小爲$rc$的矩陣(咱們叫它矩陣$g$),其中$g_\(表示\)(i,j)$這個點的高度
要求也很簡單,咱們在只向上下左右四個方向走的狀況下,求出最長降低的序列的長度
咱們新建一個矩陣:\(f\),用$f_\(表示以\)(i,j)$爲結尾的最長降低序列長度
如今考慮$f_$能夠由哪些狀態轉移獲得,:
$ \begin if(g_<g_{(i-1)j}) f_{(i-1)j}\rightarrow f_\ \ if(g_<g_{(i+1)j}) f_{(i+1)j}\rightarrow f_\ \ if(g_<g_{i(j-1)}) f_{i(j-1)}\rightarrow f_\ \ if(g_<g_{i(j+1)}) f_{i(j+1)}\rightarrow f_\ \end $
能夠發現,轉移時就是從周圍四個比較高的地方走到$f_$,那麼新增的長度就是$1$
咱們用$f_\(和轉移以前的長度\)+1$再取$max$,就獲得了狀態轉移方程:
$ \begin if(g_<g_{(i-1)j}) f_=max(f_,f_{(i-1)j})\ \ if(g_<g_{(i+1)j}) f_=max(f_,f_{(i+1)j})\ \ if(g_<g_{i(j-1)}) f_=max(f_,f_{i(j-1)})\ \ if(g_<g_{i(j+1)}) f_=max(f_,f_{i(j+1)})\ \end $
可是,這個狀態轉移方程的條件是在計算$f_$以前,它周圍比它高的格子已經被計算過了
那麼解決這個問題的最簡單的辦法就是:直接$DP$$nm$遍,可是看到$n\leq 100$的範圍,我就抱着試一試的心態上了
###\(Code\)
#include<algorithm> #include<cstring> #include<cstdio> const int maxn=105; int f[maxn][maxn],g[maxn][maxn],n,m,ans; int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){ scanf("%d",&g[i][j]); f[i][j]=1; } for(int k=1;k<=n*m;k++)//直接DPn*m遍 for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){ if(g[i][j]<g[i-1][j]) f[i][j]=std::max(f[i][j],f[i-1][j]+1); if(g[i][j]<g[i+1][j]) f[i][j]=std::max(f[i][j],f[i+1][j]+1); if(g[i][j]<g[i][j-1]) f[i][j]=std::max(f[i][j],f[i][j-1]+1); if(g[i][j]<g[i][j+1]) f[i][j]=std::max(f[i][j],f[i][j+1]+1); }//上面四句都是推過的狀態轉移方程 for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) ans=std::max(ans,f[i][j]);//自帶大常數的掃一遍求最大值 printf("%d\n",ans); return 0; }
其實這題數據出水了,結果這份$O(n2m2)\(的代碼勇奪90分\)(Test #10 TLE)$
如今考慮怎麼優化:
首先,上面的這個思路的複雜度瓶頸就在於:爲了解決上面標紅的那個問題,咱們進行了$nm$次$DP$,這致使了時間的浪費
我忽然想到一個玄學作法:周圍比它高的格子要先於他計算,那我從最高的格子開始高度依次遞減DP不就得了!
因而要維護一個高度最大值和座標,我又想到了$priority$_\(queue\),這樣時間複雜度猛降到大約$O(nmlogn)$
##\(Code\)
#include<algorithm> #include<cstring> #include<cstdio> #include<queue> const int maxn=105; std::priority_queue< std::pair<int, std::pair<int,int> > >q;//pair套pair,第一個pair的第一維表示高度,維護最大高度,第二維的第一維表示橫座標,第二維表示縱座標 int f[maxn][maxn],g[maxn][maxn],n,m,ans; inline int read(){ int x=0,f=1; char ch=getchar(); while(ch<'0' || ch>'9'){ if(ch=='-') f=-1;ch=getchar();} while(ch>='0' && ch<='9'){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();} return x*f; }//亂寫的快讀 int main(){ n=read(),m=read(); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){ g[i][j]=read(); q.push(std::make_pair(g[i][j],std::make_pair(i,j)));//進入大根堆 f[i][j]=1;//全部點初始化爲1(一個點就算不走,他本身就是1的長度) } while(q.size()!=0){//當大根堆不爲空,循環DP int height=q.top().first; int row=q.top().second.first; int line=q.top().second.second; q.pop(); if(g[row][line]<g[row-1][line]) f[row][line]=std::max(f[row][line],f[row-1][line]+1); if(g[row][line]<g[row+1][line]) f[row][line]=std::max(f[row][line],f[row+1][line]+1); if(g[row][line]<g[row][line-1]) f[row][line]=std::max(f[row][line],f[row][line-1]+1); if(g[row][line]<g[row][line+1]) f[row][line]=std::max(f[row][line],f[row][line+1]+1); ans=std::max(f[row][line],ans);//去掉了大常數qwq }//上面是狀態轉移方程(和第一個同樣只是換了名字 printf("%d\n",ans); return 0; }
事實上證實,這份代碼跑的飛起
關於今天的$DP$刷題整理完成啦!\(OvO\)
PS:感謝您的閱讀$qwq$