洛谷 P1070 道路遊戲 解題報告
P1070 道路遊戲
題目描述
小新正在玩一個簡單的電腦遊戲。
遊戲中有一條環形馬路,馬路上有\(n\)個機器人工廠,兩個相鄰機器人工廠之間由一小段馬路連接。小新以某個機器人工廠為起點,按順時針順序依次將這\(n\)個機器人工廠編號為\(1-n\),因為馬路是環形的,所以第\(n\)個機器人工廠和第\(1\)個機器人工廠是由一段馬路連接在一起的。小新將連接機器人工廠的這\(n\)段馬路也編號為\(1-n\),並規定第\(i\)段馬路連接第\(i\)個機器人工廠和第\(i+1\)個機器人工廠(\(1≤i≤n-1\)),第\(n\)段馬路連接第\(n\)個機器人工廠和第1個機器人工廠。
遊戲過程中,每個單位時間內,每段馬路上都會出現一些金幣,金幣的數量會隨著時間發生變化,即不同單位時間內同一段馬路上出現的金幣數量可能是不同的。小新需要機器人的幫助才能收集到馬路上的金幣。所需的機器人必須在機器人工廠用一些金幣來購買,機器人一旦被購買,便會沿著環形馬路按順時針方向一直行走,在每個單位時間內行走一次,即從當前所在的機器人工廠到達相鄰的下一個機器人工廠,並將經過的馬路上的所有金幣收集給小新,例如,小新在\(i\)
以下是遊戲的一些補充說明:
遊戲從小新第一次購買機器人開始計時。
購買機器人和設定機器人的行走次數是瞬間完成的,不需要花費時間。
購買機器人和機器人行走是兩個獨立的過程,機器人行走時不能購買機器人,購買完機器人並且設定機器人行走次數之後機器人才能行走。
在同一個機器人工廠購買機器人的花費是相同的,但是在不同機器人工廠購買機器人的花費不一定相同。
購買機器人花費的金幣,在遊戲結束時再從小新收集的金幣中扣除,所以在遊戲過程中小新不用擔心因金幣不足,無法購買機器人而導致遊戲無法進行。也因為如此,遊戲結束後,收集的金幣數量可能為負。
現在已知每段馬路上每個單位時間內出現的金幣數量和在每個機器人工廠購買機器人需要的花費,請你告訴小新,經過 mm 個單位時間後,扣除購買機器人的花費,小新最多能收集到多少金幣。
輸入輸出格式
輸入格式:
第一行3個正整數\(n,m,p\),意義如題目所述。
接下來的\(n\)行,每行有\(m\)個正整數,每兩個整數之間用一個空格隔開,其中第\(i\)行描
述了\(i\)號馬路上每個單位時間內出現的金幣數量(1≤ 金幣數量 ≤100),即第\(i\)行的第\(j\)( \(1≤j≤m\))個數表示第\(j\)個單位時間內\(i\)號馬路上出現的金幣數量。
最後一行,有\(n\)個整數,每兩個整數之間用一個空格隔開,其中第\(i\)個數表示在\(i\)號機器人工廠購買機器人需要花費的金幣數量( 1≤ 金幣數量 ≤100 )。
輸出格式:
共一行,包含1個整數,表示在\(m\)個單位時間內,扣除購買機器人
花費的金幣之後,小新最多能收集到多少金幣。
說明
對於 40%的數據, 2≤n≤40,1≤m≤40 。
對於 90%的數據, 2≤n≤200,1≤m≤200。
對於 100%的數據, 2≤n≤1000,1≤m≤1000,1≤p≤m。
NOIP 2009 普及組 第四題
寫這個題真是累啊,好毒。應該練練處理比較麻煩的DP。
在讀完題目以後,其實初始版的方程並不難想
\(dp[i][j][k]\)表示時間\(i\)在工廠\(j\)當機器人前已經走了\(k\)步的方案。
\(k\)那一維可以通過直接枚舉消去
\(dp[i][j][1/0]\)表示時間\(i\)工廠\(j\)是否能夠繼續再走
轉移為
\(dp[i][j][1]=max(dp[i][j][1],dp[i-k][j-k][0]+cal(i,j,k))\)//沒有列出關於環的情況的,以下會詳細說
\(dp[i][j][0]=max(dp[i][j][0],dp[i][k][1]-cost[j])\)
確實很不完美,我當時也只是想到了這麽多,感覺過個90還是比較輕松的,1000的點拿單調隊列優化一下就行了。
然後我就開始寫\(cal\)函數,處理那個對角線式的前綴和,然後成功把自己攪糊了。
這個題把點權釋放到了邊權上,人話就是存儲的路徑位置其實是某個點伸出去的那一條
比如用這個圖來描述讀入的某時間某費用數組
\(f[i][j]\)表示時間\(i\)位置\(j\)所延伸回去的45°的鏈的值,如下圖,黃點為\((i,j)\)的位置,則它所代表的鏈為藍色的一條鏈上的點權之和。
好吧,弄清楚了這個,寫一下\(cal(i,j,k)\)函數了,如下圖,它要返回這樣一個鏈的值
事實上看起來也不是那麽麻煩
int cal(int i,int j,int k)
{
return f[i-1][j-1]-f[i-k-1][j-k-1];
}
但是,當轉移設計到拐彎時
確實有點麻煩。。。我最開始漏掉了那條虛線。。
這是帶拐彎的轉移:\(dp[i][j][1]=max(dp[i][j][1],dp[i-k][n+j-k][0]+cal(i-j+1,n+1,k-j+1)+cal(i,j,j-1))\)
好吧,到這裏我已經感覺我寫不出單調隊列了。
交了一下果然拿到了90分,其實在如果在考場上做到這裏已經可以了(鬼知道為什麽部分分有這麽多)
部分分代碼:
#include <cstdio>
#include <cstring>
int max(int x,int y){return x>y?x:y;}
int min(int x,int y){return x<y?x:y;}
const int N=1010;
const int inf=0x3f3f3f3f;
int dp[N][N][2],n,m,p,harv[N][N],f[N][N],cost[N],ans=-inf;//n數量,m時間
int cal(int i,int j,int k)
{
return f[i-1][j-1]-f[i-k-1][j-k-1];
}
int main()
{
memset(dp,-0x3f,sizeof(dp));
scanf("%d%d%d",&n,&m,&p);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
scanf("%d",&harv[i][j]);
f[j][i]=f[j-1][i-1]+harv[i][j];
}
for(int i=1;i<=n;i++)
scanf("%d",cost+i);
for(int i=1;i<=n;i++)
dp[1][i][0]=-cost[i];
m++;
for(int i=2;i<=m;i++)//時間
{
for(int j=1;j<=n;j++)//路程
{
for(int k=1;k<=min(i,p);k++)//從第幾個之前轉移
{
if(j>k)
dp[i][j][1]=max(dp[i][j][1],dp[i-k][j-k][0]+cal(i,j,k));
else if(i>j)
dp[i][j][1]=max(dp[i][j][1],dp[i-k][n+j-k][0]+cal(i-j+1,n+1,k-j+1)+cal(i,j,j-1));
}
}
for(int j=1;j<=n;j++)
for(int k=1;k<=n;k++)
{
if(k==j) continue;
dp[i][j][0]=max(dp[i][j][0],dp[i][k][1]-cost[j]);
}
}
for(int i=1;i<=n;i++)
ans=max(ans,dp[m][i][1]);
printf("%d\n",ans);
return 0;
}
看看各位佬爺的題解。
才發現自己的方程太不優秀了,優秀的方程\(O(N^3)\)甚至可以卡過。
\(dp[i]\)表示時間\(i\)的最大答案。
轉移:\(dp[i]=max(dp[i-k]+cal(i,j,k)-cost[j-k])\)//無環
看著減去的\(cost[i]\),我明白了應該給\(dp[i]\)加一個定語
\(dp[i]\)表示時間\(i\)處在某位置上還未在此位置上消費機器人的最大答案,每一步的機器人花費是在被轉移的時候才扣得啊。而我最初的方程,是代表當前時間\(i\)和地點\(j\)已經買了機器人的最大答案。為了區分是否處理花費機器人的狀態,我甚至得用第三維的0/1維護。
讀了讀題目中“必須立刻在 任意 一個機器人工廠中購買一個新的機器人”,我明白了為什麽可以不要地點這一維,其實每一個時間都可以當做是步數已經到了的時間,而此時地點的選擇是具有任意性的,即此時地點也是不重要的。
我把方程改了改,果然\(O(N^3)\)卡過了
單調隊列優化
但是,如果數據再卡一點,單隊優化就是必須的了。
在這裏,因為實在是覺得這種點權下放的方式不優雅,我將工廠的實際編號和時間給減去了1,而讀入時不變
這樣,查詢\(f[i][j]\)所代表的就不是再多延伸出去一條鏈了,很舒服了。
再列出轉移方程://無環
\(dp[i]=max(dp[i-k]+f[i][j]-f[i-k][j-k]-cost[j-k])\)
\(=max(dp[i-k]-f[i-k][j-k]-cost[j-k])+f[i][j]\)
轉移時維護\(q[i][j]=dp[i]-f[i][j]-cost[j]\)即可
因為每一個\(f[i][j]\)都可以唯一的確定一個\(dp[i]\)和\(cost[j]\),所以我們考慮\(f[i][j]\)在轉移時的分布。
對同一個答案的貢獻,這個分布大概是這樣。
為了準確的定位某個答案從哪個單調隊列轉移,考慮給每一個單調隊列編號,將單隊與\(location\)軸相交的那個點作為它的編號。
在還沒拐彎時,所屬單隊即為\(j-i\),拐彎了以後我們發現它減去了\(l\)個\(n\),\(l\)為拐彎次數,很簡單,取膜以後加一個再取膜即可
int get(int i,int j)//獲取單隊編號
{
return ((j-i)%n+n)%n;
}
還有兩點要註意的地方
一是虛線所連的邊仍然需要特判一下
二是為了確保拐彎後不出現問題,要把\(dp[i]=max(dp[i-k]-f[i-k][j-k]-cost[j-k])+f[i][j]\)中的\(f[i][j]\)加上它失去的鏈的長度,維護一個\(add[i]\)數組。
參考代碼:
#include <cstdio>
#include <cstring>
const int N=1010;
int max(int x,int y){return x>y?x:y;}
int n,m,p;
int f[N][N],cost[N],q[N][N],loc[N][N],l[N],r[N],add[N],dp[N];
int get(int i,int j)//獲取單隊編號
{
return ((j-i)%n+n)%n;
}
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",&f[j][i]);
f[j][i]+=f[j-1][i-1];
}
for(int i=0;i<n;i++)
{
scanf("%d",cost+i);
q[i][++r[i]]=-cost[i],l[i]++;
}
memset(dp,-0x3f,sizeof(dp));
dp[0]=0;
for(int i=1;i<=m;i++)
{
for(int j=0;j<n;j++)
{
int id=get(i,j);
while(l[id]<=r[id]&&loc[id][l[id]]+p<i) l[id]++;
if(!j) add[id]+=f[i][n];
if(l[id]<=r[id])
dp[i]=max(dp[i],q[id][l[id]]+add[id]+f[i][j]);
}
for(int j=0;j<n;j++)
{
int id=get(i,j);
int tmp=dp[i]-add[id]-f[i][j]-cost[j];
while(l[id]<=r[id]&&q[id][r[id]]<=tmp)
r[id]--;
loc[id][++r[id]]=i;
q[id][r[id]]=tmp;
}
}
printf("%d\n",dp[m]);
return 0;
}
2018.6.22
洛谷 P1070 道路遊戲 解題報告