P1070 道路游戏
P1070 道路游戏
题目描述
小新正在玩一个简单的电脑游戏。
游戏中有一条环形马路,马路上有 n 个机器人工厂,两个相邻机器人工厂之间由一小段马路连接。小新以某个机器人工厂为起点,按顺时针顺序依次将这 n 个机器人工厂编号为1~n,因为马路是环形的,所以第 n 个机器人工厂和第 1 个机器人工厂是由一段马路连接在一起的。小新将连接机器人工厂的这 n 段马路也编号为 1~n,并规定第 i 段马路连接第 i 个机器人工厂和第 i+1 个机器人工厂(1≤i≤n-1),第 n 段马路连接第 n 个机器人工厂和第 1个机器人工厂。
游戏过程中,每个单位时间内,每段马路上都会出现一些金币,金币的数量会随着时间发生变化,即不同单位时间内同一段马路上出现的金币数量可能是不同的。小新需要机器人的帮助才能收集到马路上的金币。所需的机器人必须在机器人工厂用一些金币来购买,机器人一旦被购买,便会沿着环形马路按顺时针方向一直行走,在每个单位时间内行走一次,即从当前所在的机器人工厂到达相邻的下一个机器人工厂,并将经过的马路上的所有金币收集给小新,例如,小新在 i(1≤i≤n)号机器人工厂购买了一个机器人,这个机器人会从 i 号机器人工厂开始,顺时针在马路上行走,第一次行走会经过 i 号马路,到达 i+1 号机器人工厂(如果 i=n,机器人会到达第 1 个机器人工厂),并将 i 号马路上的所有金币收集给小新。 游戏中,环形马路上不能同时存在 2 个或者 2 个以上的机器人,并且每个机器人最多能够在环形马路上行走 p 次。小新购买机器人的同时,需要给这个机器人设定行走次数,行走次数可以为 1~p 之间的任意整数。当马路上的机器人行走完规定的次数之后会自动消失,小新必须立刻在任意一个机器人工厂中购买一个新的机器人,并给新的机器人设定新的行走次数。
以下是游戏的一些补充说明:
-
游戏从小新第一次购买机器人开始计时。
-
购买机器人和设定机器人的行走次数是瞬间完成的,不需要花费时间。
-
购买机器人和机器人行走是两个独立的过程,机器人行走时不能购买机器人,购买完机器人并且设定机器人行走次数之后机器人才能行走。
-
在同一个机器人工厂购买机器人的花费是相同的,但是在不同机器人工厂购买机器人的花费不一定相同。
- 购买机器人花费的金币,在游戏结束时再从小新收集的金币中扣除,所以在游戏过程中小新不用担心因金币不足,无法购买机器人而导致游戏无法进行。也因为如此,游戏结束后,收集的金币数量可能为负。
现在已知每段马路上每个单位时间内出现的金币数量和在每个机器人工厂购买机器人需要的花费,请你告诉小新,经过 m 个单位时间后,扣除购买机器人的花费,小新最多能收集到多少金币。
输入输出格式
输入格式:
第一行 3 个正整数,n,m,p,意义如题目所述。
接下来的 n 行,每行有 m 个正整数,每两个整数之间用一个空格隔开,其中第 i 行描
述了 i 号马路上每个单位时间内出现的金币数量(1≤金币数量≤100),即第 i 行的第 j(1≤j≤m)个数表示第 j 个单位时间内 i 号马路上出现的金币数量。
最后一行,有 n 个整数,每两个整数之间用一个空格隔开,其中第 i 个数表示在 i 号机器人工厂购买机器人需要花费的金币数量(1≤金币数量≤100)。
输出格式:
共一行,包含 1 个整数,表示在 m 个单位时间内,扣除购买机器人
花费的金币之后,小新最多能收集到多少金币。
输入输出样例
2 3 2 1 2 3 2 3 4 1 2
5
说明
【数据范围】
对于 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思路可以通过记忆化搜索来启发
f[i]到第时间i时的最大值。
枚举时间
枚举买机器人的位置
枚举买机器人运动时间
80分
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #define MAXN 1010 5 int c[MAXN],mo[MAXN][MAXN],f[MAXN]; 6 int n,m,p; 7 void init() 8 { 9 scanf("%d%d%d",&n,&m,&p); 10 for (int i=1; i<=n; ++i) 11 for (int j=1; j<=m; ++j) 12 scanf("%d",&mo[i][j]); 13 for (int i=1; i<=n; ++i) 14 scanf("%d",&c[i]); 15 } 16 void dp() 17 { 18 for (int i=1; i<=m; ++i) //枚举时间 19 { 20 for (int j=1; j<=n; ++j) //枚举位置 21 { 22 int r = j==1?n:j-1, sum = 0; //r是j的前一个点 23 sum += mo[r][i]; //sum加上捡到的金币 24 for (int k=1; k<=p; ++k)//枚举买机器人的时间 25 { 26 if (i-k<0) break ; 27 f[i] = std::max(f[i],f[i-k]-c[r]+sum); 28 //枚举,f[i-k]就是第(i-k)时间时的最优解 29 //r就是买机器人的工厂位置 30 //sum是这个机器人捡的金币,即从r走来时捡到的金币 31 r = r==1?n:r-1; //倒回前一个工厂 32 sum += mo[r][i-k]; //从r走过来的路上捡到的金币 33 } 34 } 35 } 36 printf("%d",f[m]); 37 } 38 int main() 39 { 40 init(); 41 dp(); 42 return 0; 43 }
100分
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 5 #define maxn 1010 6 7 using namespace std; 8 int n,m,p,g[maxn][maxn],w[maxn],f[maxn]; 9 10 int main() 11 { 12 scanf("%d%d%d",&n,&m,&p); 13 for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) 14 scanf("%d",&g[i][j]); 15 for(int i=1;i<=n;i++) scanf("%d",&w[i]); 16 memset(f,128,sizeof(f));f[0]=0; 17 for(int i=1;i<=m;i++) 18 for(int j=1;j<=n;j++)//这次的出发点 19 { 20 int r=j-1,vi=0;//vi 表示累计的收益 21 if(r==0)r=n;vi+=g[r][i];//当前道路花费 22 for(int k=1;k<=p;k++)//枚举买机器人的时间 23 { 24 if(i-k<0)continue; 25 f[i]=max(f[i],f[i-k]-w[r]+vi); 26 if(r==1)r=n;else r--;//前一个工厂 27 vi+=g[r][i-k];//O(1)计算 28 } 29 } 30 printf("%d\n",f[m]); 31 return 0; 32 }
f[i]表示在时间i所有位置的最大收益
状态转移方程
r表示j的前一个点
f[i]=max(f[i],f[i-k]-w[r]+vi);
心得
1、通过计算机输出中间变量的方式比较容易能看懂代码和方法
2、可以弄个pre[i]记录前驱节点,也可以这样
3、状态转移方程,应该可以自己想的会的
4、一定要把这些代码弄懂能够敲出来
5、不会做先跟着它敲一遍,然后再自己敲
6、子结构,子问题,我都没意识到么
7、线性DP
8、因为dp的值可能有负的,又是求最大值,所以memset(128)
9、DP过程,前i分钟走到j位置的最后一个机器人走了k步的方程。
10、ijk合起来不懂的时候我们就看ij
空间换时间
1、这里我能优化是因为我那g[i][j]这个数组换了时间
O(mn)
dp[i][j]表示时间i在j点的最大收益,pre[j]表示j点的上一个,
mx[i]表示在时间i所有位置的最大收益(因为买机器人是任意位置可买,转移时直接用即可),
g[i][j]表示状态(i,j)取最优解时走的最少步数(显然越少越好)
如果已经有机器人并且它还可以走的情况:
dp[i][j]=max(dp[i][j]+dp[i-1][pre[j]]+mon[pre[j]][i]);
如果走了
mx[i]=max(mx[i],dp[i][j]);
g[i][j]=g[i-1][pre[j]]+1;
如果机器人不能走了或者我们想换机器人了
dp[i][j]=max(dp[i][j], mx[i-1]+mon[pre[j]][i]-c[pre[j]]);
如果我们换了机器人
mx[i]=max(mx[i],dp[i][j]);
g[i][j]=g[i-1][pre[j]]+1;
O(MN)
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #define maxn 1005 5 #define inf 0x3f3f3f 6 using namespace std; 7 inline void read(int &num){ 8 char c;bool flag=0;num=0; 9 while((c=getchar())>57||c<48); 10 if(c=='-')flag=1; 11 else num+=c-48; 12 while((c=getchar())>=48&&c<=57)num=num*10+c-48; 13 if(flag)num*=-1; 14 return; 15 } 16 17 /* 18 空间换时间 19 O(mn) 20 dp[i][j]表示时间i在j点的最大收益,pre[j]表示j点的上一个, 21 mx[i]表示在时间i所有位置的最大收益(因为买机器人是任意位置可买,转移时直接用即可), 22 g[i][j]表示状态(i,j)取最优解时走的最少步数(显然越少越好) 23 */ 24 int n,m,p,mon[maxn][maxn],c[maxn],dp[maxn][maxn],mx[maxn],g[maxn][maxn],pre[maxn]; 25 int main(){ 26 memset(dp,-0x3f,sizeof(dp)); 27 memset(mx,-0x3f,sizeof(mx)); 28 mx[0]=0; 29 read(n),read(m),read(p); 30 for(int i=1;i<=n;++i) 31 for(int j=1;j<=m;++j) 32 read(mon[i][j]); 33 for(int i=1;i<=n;++i) read(c[i]),pre[i]=(i-1?i-1:n),dp[0][i]=0; 34 for(int i=1;i<=m;++i){ 35 for(int j=1;j<=n;++j){ 36 //机器人如果还可以走。我们就让机器人走 37 //第一次你肯定不能不买机器人, 38 if(g[i-1][pre[j]]<p&&i>1){ 39 //让之前那个机器人从j的前一步开始走,如果这个值优于dp[i][j],我们就更新它 40 if(dp[i][j]<dp[i-1][pre[j]]+mon[pre[j]][i]){ 41 dp[i][j]=dp[i-1][pre[j]]+mon[pre[j]][i]; 42 //更新mx[i] 43 mx[i]=max(mx[i],dp[i][j]); 44 //机器人的步数加上走的这一步 45 g[i][j]=g[i-1][pre[j]]+1; 46 } 47 } 48 //在j的前面一步买机器人,如果我们买机器人的收益大,我们就买机器人 49 if(dp[i][j]<mx[i-1]+mon[pre[j]][i]-c[pre[j]]){ 50 //更新dp[i][j] 51 dp[i][j]=mx[i-1]+mon[pre[j]][i]-c[pre[j]]; 52 //更新mx[i] 53 mx[i]=max(mx[i],dp[i][j]); 54 //更新步数 55 g[i][j]=1; 56 } 57 } 58 } 59 int ans=-inf; 60 for(int i=1;i<=n;++i) ans=max(ans,dp[m][i]); 61 printf("%d",ans); 62 return 0; 63 }
这里也是一个易错点:
第18行:num[j][i]=num[pre[j]][i-1]+1;//写成了 num[j][i]++
1 #include <bits/stdc++.h> 2 const int N=1e3+10; 3 using namespace std; 4 int dp[N][N],f[N],pre[N],g[N][N],num[N][N],a[N],n,m,p; 5 6 int main(){ 7 //freopen("in.txt","r",stdin); 8 cin>>n>>m>>p; 9 memset(dp,128,sizeof(dp)); memset(f,128,sizeof(f)); 10 f[0]=0; 11 for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) cin>>g[i][j]; 12 for(int i=1;i<=n;i++) cin>>a[i],pre[i]=(i-1?i-1:n),dp[i][0]=0; 13 for(int i=1;i<=m;i++) for(int j=1;j<=n;j++){ 14 if(num[pre[j]][i-1]<p&&i>1) 15 if(dp[j][i]<dp[pre[j]][i-1]+g[pre[j]][i]){ 16 dp[j][i]=dp[pre[j]][i-1]+g[pre[j]][i]; 17 f[i]=max(f[i],dp[j][i]); 18 num[j][i]=num[pre[j]][i-1]+1;//写成了 num[j][i]++ 19 } 20 if(dp[j][i]<dp[pre[j]][i-1]+g[pre[j]][i]-a[pre[j]]){ 21 dp[j][i]=dp[pre[j]][i-1]+g[pre[j]][i]-a[pre[j]]; 22 f[i]=max(f[i],dp[j][i]); 23 num[j][i]=1; 24 } 25 } 26 int ans=-1<<30; 27 for(int i=i;i<=m;i++) ans=max(ans,f[i]); 28 cout<<f[m]<<endl; 29 return 0; 30 }
单调队列+DP优化
用 f[i] 表示到时间 i 可以得到的最多金钱
转移很显然,枚举到达的点 j 以及走的步数 k:
f[i]=max(f[i−k]+(g[j−1][i]−g[j−k−1][i-k])−a[j−k])(0<j<n,0<k≤min(i,p))
(其中 g 数组表示斜着的部分和)注意上面没有考虑环的情况
然而这样子做是 O(N3) 的,理论上是过不了的
我们考虑进行简化,(即省掉 k 循环)先把只有关 i 和 j 的独立出来
f[i]=max(f[i−k]−g[j−k−1][i-k]−a[j−k])+g[j−1][i]
然后我们设一个 h[i][j]:
h[i][j]=f[i]−g[j−1][i]−a[j]
则转移方程变成了:
f[i][j]=max(h[i−k][j−k])+g[j−1][i](0<k≤min(i,p))
而每个对于每个 f[i][j],它都是相当于在斜线上转移:
(相同颜色的格子代表斜线)对于每个 f[i][j],相当于要求斜线上最多 p 个元素的最大值
然后注意到这是个连续的滑动区间,并且是二维的,于是用 n 个单调队列维护一下就好了
1 #include <iostream> 2 #include <algorithm> 3 #include <cstdio> 4 #include <utility> 5 #define N 1020 6 #define INFINITE 999999999 7 using namespace std; 8 9 class Queue 10 { 11 private: 12 pair<int, int> f[N]; 13 int l, r; 14 15 public: 16 Queue() : l(0), r(0) 17 { 18 return; 19 } 20 21 bool Empty(void) 22 { 23 return l == r; 24 } 25 26 pair<int, int> Back (void) 27 { 28 return f[r - 1]; 29 } 30 31 pair<int, int> Front(void) 32 { 33 return f[l ]; 34 } 35 36 void Push_Back(pair<int, int> x) 37 { 38 f[r ++] = x; 39 40 return; 41 } 42 43 void Pop_Back (void) 44 { 45 r --; 46 47 return; 48 } 49 50 void Pop_Front(void) 51 { 52 l ++; 53 54 return; 55 } 56 }; 57 58 int n, a[N]; 59 int f[N], g[N][N]; 60 Queue q[N]; 61 62 void PushOrder(Queue &q, int x, int p) 63 { 64 while(!q.Empty() && q.Back().first <= x) 65 q.Pop_Back(); 66 q.Push_Back(make_pair(x, p)); 67 68 return; 69 } 70 71 void PopOrder(Queue &q, int p) 72 { 73 if(q.Front().second <= p) 74 q.Pop_Front(); 75 76 return; 77 } 78 79 int MaxOrder(Queue &q) 80 { 81 return q.Front().first; 82 } 83 84 int Reload(int x, int k)//解决环和往之前走 85 { 86 return ((x - k) % n + n) % n; 87 } 88 89 int main() 90 { 91 int m, p; 92 int i, j; 93 94 scanf("%d %d %d", &n, &m, &p); 95 for(i = 0;i < n;i ++)//i是工厂编号 96 for(j = 1;j <= m;j ++)//j是时间 97 scanf("%d", &g[i][j]); 98 for(i = 0;i < n;i ++) 99 scanf("%d", &a[i]); 100 //有点像前缀和,这样操作之后保证g[i][j]里面存的是超过i号工厂的前j分钟路上的所有金币,用的时候i-1 101 for(j = 2;j <= m;j ++)//j是时间 102 for(i = 0;i < n;i ++)//i是工厂编号 103 g[i][j] += g[(i - 1 + n) % n][j - 1]; 104 105 for(i = 0;i < n;i ++) 106 PushOrder(q[Reload(i, -1)], -a[i], 0); 107 108 for(i = 1;i <= m;i ++) 109 { 110 for(j = 0, f[i] = -INFINITE;j < n;j ++) 111 f[i] = max(f[i], MaxOrder(q[Reload(j, i - 1)]) + g[Reload(j, 1)][i]); 112 113 for(j = 0;j < n;j ++) 114 { 115 PopOrder (q[Reload(j, i - 1)], i - p); 116 PushOrder(q[Reload(j, i - 1)], f[i] - g[Reload(j, 1)][i] - a[j], i); 117 } 118 } 119 cout << f[m] << endl; 120 121 return 0; 122 }