洛谷 动态规划一日游 P2577、P1070、P2051
記
2018年3月19日
賊頹呢,一天就寫(xiě)了兩道DP,還都不會(huì)寫(xiě),這可GG。
動(dòng)態(tài)規(guī)劃真的難且有趣,算法題中動(dòng)態(tài)規(guī)劃占到了很大的比例,而且動(dòng)態(tài)規(guī)劃往往是輔助解決一些其他類(lèi)型問(wèn)題的基礎(chǔ),加深加強(qiáng)對(duì)動(dòng)態(tài)規(guī)劃問(wèn)題的認(rèn)識(shí)和訓(xùn)練非常有必要。
P2577 午餐
題意
題意見(jiàn)題目鏈接
題解
這道題目本質(zhì)上是一道背包問(wèn)題,是背包問(wèn)題的變形,這道題不同的地方在于有兩個(gè)背包。
因此,我們?cè)O(shè)計(jì)狀態(tài)的時(shí)候,兩個(gè)背包的狀態(tài)都要記錄。
貪心
由于每個(gè)隊(duì)列的排隊(duì)順序不一樣,結(jié)果也是不一樣的。而可以證明,當(dāng)把所有的同學(xué)按照其吃飯時(shí)間降序排序的話(huà),這樣的排隊(duì)順序一定是最優(yōu)的順序。
樸素
我們最初想到的狀態(tài)是這樣的:
dp[i][j][k]dp[i][j][k]表示當(dāng)前考慮的是前i個(gè)人已經(jīng)被分配,且第一個(gè)隊(duì)列排隊(duì)打飯時(shí)間和為i,第二個(gè)隊(duì)列排隊(duì)打飯的時(shí)間和為j,最早的集合時(shí)間。
那么轉(zhuǎn)移方程可以寫(xiě)成
dp[i][j][k]=min(max(dp[i?1][j?a[i]][j],j+b[i]),max(dp[i?1][j][k?a[i]],k+b[i]))dp[i][j][k]=min(max(dp[i?1][j?a[i]][j],j+b[i]),max(dp[i?1][j][k?a[i]],k+b[i]))
轉(zhuǎn)移方法:把i分給第1個(gè)窗口或者把i分給第2個(gè)窗口。
顯然這樣時(shí)間、空間復(fù)雜度會(huì)爆炸。
空間復(fù)雜度為O(2005)O(2005)
優(yōu)化
因此我們必須優(yōu)化,進(jìn)一步挖掘條件以降維。
我們發(fā)現(xiàn)j+k=sumb[i]j+k=sumb[i]
這樣的話(huà),我們直接可以減少一維,定義
dp[i][j]dp[i][j]表示考慮前i個(gè)人,第一個(gè)窗口的同學(xué)總打飯時(shí)間為j時(shí)候,前i個(gè)同學(xué)最早集合的時(shí)間。
空間復(fù)雜度變成了O(2003)O(2003)
轉(zhuǎn)移方程:
dp[i][j]=min(max(dp[i?1][j?a[i]],j+b[i]),max(dp[i?1][j],sumb[i]?j+b[i]))dp[i][j]=min(max(dp[i?1][j?a[i]],j+b[i]),max(dp[i?1][j],sumb[i]?j+b[i]))
進(jìn)一步優(yōu)化
由于我們發(fā)現(xiàn)i狀態(tài)的轉(zhuǎn)移只與i-1有關(guān)系,因此,我們可以用滾動(dòng)數(shù)組繼續(xù)優(yōu)化掉一維。
時(shí)間復(fù)雜度O(2002)O(2002)
總的時(shí)間復(fù)雜度為O(2003)O(2003)
細(xì)節(jié)
注意邊界條件以及轉(zhuǎn)移成立的條件。
代碼
#include <iostream> #include <cstdio> #include <algorithm> #include <cstring> using namespace std; typedef pair<int,int> pii; const int maxn = 205; const int inf = 0x3f3f3f3f; pii ps[maxn]; int dp[2][maxn*maxn],sum[maxn]; int n; int main(){memset(dp,0x3f,sizeof(dp));scanf("%d",&n);for(int i = 0;i < n;++i){int a,b;scanf("%d%d",&a,&b);ps[i+1] = make_pair(-b,a);}sort(ps+1,ps+1+n);dp[0][0] = 0;for(int i = 1;i <= n;++i){memset(dp[i&1],0x3f,sizeof(dp[i&1]));sum[i] = sum[i-1] + ps[i].second;for(int j = 0;j <= sum[i];++j){if(j >= ps[i].second)dp[i&1][j] = min(dp[i&1][j],max(dp[(i-1)&1][j-ps[i].second],j-ps[i].first));if(sum[i]-j >= ps[i].second)dp[i&1][j] = min(dp[i&1][j],max(dp[(i-1)&1][j],sum[i]-j-ps[i].first));}}int mi= inf;for(int j = 0;j <= sum[n];++j){if(dp[n&1][j] < mi){mi = dp[n&1][j];}}cout<<mi<<endl;return 0; }P1070道路游戲
題意
題目鏈接看題意
題解
這道題本質(zhì)上就是在下圖斜線(xiàn)上的dp。
Column表示機(jī)器人出售站。
Row表示時(shí)間軸。
字母相同的斜線(xiàn)上相鄰的兩個(gè)表格表示由上一個(gè)表格的數(shù)字,下一步將取到下面表格的數(shù)字。
這樣就相當(dāng)于我們把整個(gè)表格橫著切幾段,然后每一段內(nèi)部選一條連續(xù)的斜線(xiàn),并把斜線(xiàn)里的數(shù)字加起來(lái)減去該段斜線(xiàn)第一個(gè)站點(diǎn)的費(fèi)用。把所有的段的值加起來(lái)得到一個(gè)新的數(shù)值,我們要做的就是最大化這個(gè)數(shù)值。
定義狀態(tài):
opt[i]opt[i]表示前i行所能收集的最大金幣值。
sum[i][j]sum[i][j]表示從第一行的第i站出發(fā),沿著斜線(xiàn)走,一共收集j格的金幣得到的總和。
那么狀態(tài)轉(zhuǎn)移方程就是
ii表示行,jj表示機(jī)器人購(gòu)買(mǎi)點(diǎn)所在的斜線(xiàn)編號(hào)。
kk表示上一次剛好停在哪一行。
opt[i]=max(opt[k]+sum[j][i]?sum[j][k]?cost[j+k]),1<=k<=popt[i]=max(opt[k]+sum[j][i]?sum[j][k]?cost[j+k]),1<=k<=p
解釋為什么costcost的下標(biāo)是j+kj+k:
因?yàn)閖表示的斜線(xiàn)編號(hào),在第k+1行買(mǎi)機(jī)器人,實(shí)際的購(gòu)買(mǎi)站點(diǎn)是j+k
這樣做的話(huà),時(shí)間復(fù)雜度是O(n?m?p)O(n?m?p)不滿(mǎn)足要求,因此我們必須繼續(xù)優(yōu)化。
看著形式,我就只能想到單調(diào)隊(duì)列或者是斜率優(yōu)化了,這道題單調(diào)隊(duì)列優(yōu)化顯然是可以的。
我們i,j循環(huán)變量不能省略,能省略的就只有k了。我們把i和j與k分離開(kāi):
opt[i]=max(sum[j][i]+(opt[k]?sum[j][k]?cost[j+k])),1<=k<=popt[i]=max(sum[j][i]+(opt[k]?sum[j][k]?cost[j+k])),1<=k<=p
分離出來(lái)的這部分:
(opt[k]?sum[j][k]?cost[j+k])(opt[k]?sum[j][k]?cost[j+k])
就只與kk和jj有關(guān)了,因此,我們可以建立nn個(gè)單調(diào)隊(duì)列對(duì)應(yīng)jj,隊(duì)列內(nèi)部對(duì)應(yīng)kk。
dp核心代碼
for(int i = 1;i <= m;++i){//i表示時(shí)刻for(int j = 1;j <= n;++j){//j表示機(jī)器人的購(gòu)買(mǎi)點(diǎn)所在的斜線(xiàn)編號(hào)while(PQS[j].size() && i-PQS[j].getfront().first > p) PQS[j].pop();pii p = PQS[j].getfront();opt[i] = max(opt[i],sum[j][i] + p.second);}for(int j = 1;j <= n;++j){int pre = j+i;while(pre > n) pre -= n;PQS[j].push(i,opt[i] - sum[j][i] - cost[pre]);}}總的代碼
#include <iostream> #include <cstdio> #include <cstring> using namespace std; const int maxn = 1005; typedef pair<int,int> pii;struct PQ{pii ps[maxn];int front,tail;void push(int idx,int key){while(tail > front && key >= ps[tail-1].second)tail--;ps[tail++] = make_pair(idx,key);}void pop(){if(front < tail) ++front;}int size(){return tail-front;}pii getfront(){return ps[front];} }PQS[maxn];int n,m,p; int val[maxn][maxn]; int sum[maxn][maxn];//1:出發(fā)點(diǎn) 2: 步數(shù) int opt[maxn]; int cost[maxn]; const int inf = 1e9; int main(){scanf("%d%d%d",&n,&m,&p);for(int i = 1;i <= n;++i){for(int j = 1;j <= m;++j){scanf("%d",&val[i][j]);}}for(int i = 1;i <= n;++i){scanf("%d",&cost[i]);}for(int i = 1;i <= n;++i){int pre = i-1;for(int j = 1;j <= m;++j){if(++pre == n+1) pre = 1;sum[i][j] = sum[i][j-1]+val[pre][j];//printf("i:%d,j:%d,sum:%d\n",i,j,sum[i][j]);}}for(int i = 1;i <= n;++i)PQS[i].push(0,-cost[i]); for(int i = 1;i <= m;++i)opt[i] = -inf;//dpfor(int i = 1;i <= m;++i){//i表示時(shí)刻for(int j = 1;j <= n;++j){//j表示機(jī)器人購(gòu)買(mǎi)點(diǎn)所在的斜線(xiàn)編號(hào)while(PQS[j].size() && i-PQS[j].getfront().first > p) PQS[j].pop();pii p = PQS[j].getfront();opt[i] = max(opt[i],sum[j][i] + p.second);}for(int j = 1;j <= n;++j){int pre = j+i;while(pre > n) pre -= n;PQS[j].push(i,opt[i] - sum[j][i] - cost[pre]);}}cout<<opt[m]<<endl;return 0; }P2051 中國(guó)象棋
題意
點(diǎn)擊鏈接查原題目
題解
我好菜啊,這道是原題,暑假集訓(xùn)的時(shí)候做過(guò),而且當(dāng)時(shí)還獨(dú)自做出來(lái)了,過(guò)了半年,自己再做的時(shí)候發(fā)現(xiàn)竟然不會(huì)做了,看了題解的提示才想出來(lái)。咋越練越菜呢???
這道題目關(guān)鍵點(diǎn)在狀態(tài)的定義!
我們需要把無(wú)用的信息都去除掉,在狀態(tài)定義的時(shí)候盡量捕捉最本質(zhì)、最關(guān)鍵的信息。
例如本題,按行考慮,在考慮到第i行的時(shí)候,我們關(guān)注點(diǎn)在前i行中,形成的0炮列有幾個(gè),1個(gè)炮的列有幾個(gè),2個(gè)跑的列有幾個(gè),這樣。
而我們不需要知道具體的前i行的棋子排列。
狀態(tài)定義就出來(lái)了:
dp[i][j][k]dp[i][j][k]表示前i行,構(gòu)成有j個(gè)1炮列、k個(gè)2炮列可行的方案數(shù)。
這樣的話(huà),轉(zhuǎn)移方程也很容易得到,就是乘法原理。
注意分類(lèi)的時(shí)候這樣分:
第i行不加棋子的轉(zhuǎn)移、加1個(gè)棋子的轉(zhuǎn)移、加2個(gè)棋子的轉(zhuǎn)移。
加1個(gè)棋子的轉(zhuǎn)移又可以分為:把這個(gè)棋子加到0炮列,把這個(gè)棋子加到1炮列。
加2個(gè)棋子的轉(zhuǎn)移又可以分為:全都加到0炮列、全都加到1炮列、0炮列1炮列分別加一個(gè)。
按照這樣分類(lèi)轉(zhuǎn)移,轉(zhuǎn)移方程很好寫(xiě)。
代碼
#include <iostream> #include <cstdio> using namespace std; typedef long long ll; ll n,m; const int maxn = 106; const ll mod = 9999973; ll dp[maxn][maxn][maxn]; int main(){cin>>n>>m;dp[1][0][0] = 1;dp[1][1][0] = m;dp[1][2][0] = m*(m-1)/2;for(int i = 2;i <= n;++i){for(int j = 0;j <= m;++j){for(int k = 0;k+j <= m;++k){//0dp[i][j][k] = dp[i-1][j][k];//1if(j >= 1){ll p = m - k - (j-1);dp[i][j][k] += dp[i-1][j-1][k]*p%mod;dp[i][j][k] %= mod;}if(k-1 >= 0){dp[i][j][k] += dp[i-1][j+1][k-1]*(j+1)%mod;dp[i][j][k] %= mod;}//2if(j >= 2){ll p = (m-k-j+2);p = p*(p-1)/2%mod;dp[i][j][k] += dp[i-1][j-2][k]*p%mod;dp[i][j][k] %= mod;}if(k >= 2){ll p = (j+2)*(j+1)/2%mod;dp[i][j][k] += dp[i-1][j+2][k-2]*p%mod;dp[i][j][k] %= mod;}if(k >= 1){ll p = m - j - (k-1);dp[i][j][k] += dp[i-1][j][k-1]*j*p%mod;dp[i][j][k] %= mod;}//dp[i][j][k] += }}}ll ans = 0;for(int i = 0;i <= m;++i){for(int j = 0;j+i <= m;++j){ans = (ans + dp[n][i][j])%mod;}}cout<<ans<<endl; }總結(jié)
以上是生活随笔為你收集整理的洛谷 动态规划一日游 P2577、P1070、P2051的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 洛谷P1169 树上分组背包
- 下一篇: 洛谷P1801 黑匣子 双堆套路的使用