洛谷 动态规划一日游 P2577、P1070、P2051
2018-03-19 20:00
288 查看
记
2018年3月19日
贼颓呢,一天就写了两道DP,还都不会写,这可GG。动态规划真的难且有趣,算法题中动态规划占到了很大的比例,而且动态规划往往是辅助解决一些其他类型问题的基础,加深加强对动态规划问题的认识和训练非常有必要。
P2577 午餐
题意
题意见题目链接题解
这道题目本质上是一道背包问题,是背包问题的变形,这道题不同的地方在于有两个背包。因此,我们设计状态的时候,两个背包的状态都要记录。
贪心
由于每个队列的排队顺序不一样,结果也是不一样的。而可以证明,当把所有的同学按照其吃饭时间降序排序的话,这样的排队顺序一定是最优的顺序。朴素
我们最初想到的状态是这样的:dp[i][j][k]dp[i][j][k]表示当前考虑的是前i个人已经被分配,且第一个队列排队打饭时间和为i,第二个队列排队打饭的时间和为j,最早的集合时间。
那么转移方程可以写成
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]))
转移方法:把i分给第1个窗口或者把i分给第2个窗口。
显然这样时间、空间复杂度会爆炸。
空间复杂度为O(2005)O(2005)
优化
因此我们必须优化,进一步挖掘条件以降维。我们发现j+k=sumb[i]j+k=sumb[i]
这样的话,我们直接可以减少一维,定义
dp[i][j]dp[i][j]表示考虑前i个人,第一个窗口的同学总打饭时间为j时候,前i个同学最早集合的时间。
空间复杂度变成了O(2003)O(2003)
转移方程:
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]))
进一步优化
由于我们发现i状态的转移只与i-1有关系,因此,我们可以用滚动数组继续优化掉一维。时间复杂度O(2002)O(2002)
总的时间复杂度为O(2003)O(2003)
细节
注意边界条件以及转移成立的条件。代码
#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 ;++j){ if(dp[n&1][j] < mi){ mi = dp[n&1][j]; } } cout<<mi<<endl; return 0; }
P1070道路游戏
题意
题目链接看题意题解
这道题本质上就是在下图斜线上的dp。Column表示机器人出售站。
Row表示时间轴。
字母相同的斜线上相邻的两个表格表示由上一个表格的数字,下一步将取到下面表格的数字。
这样就相当于我们把整个表格横着切几段,然后每一段内部选一条连续的斜线,并把斜线里的数字加起来减去该段斜线第一个站点的费用。把所有的段的值加起来得到一个新的数值,我们要做的就是最大化这个数值。
定义状态:
opt[i]opt[i]表示前i行所能收集的最大金币值。
sum[i][j]sum[i][j]表示从第一行的第i站出发,沿着斜线走,一共收集j格的金币得到的总和。
那么状态转移方程就是
ii表示行,jj表示机器人购买点所在的斜线编号。
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的下标是j+kj+k:
因为j表示的斜线编号,在第k+1行买机器人,实际的购买站点是j+k
这样做的话,时间复杂度是O(n∗m∗p)O(n∗m∗p)不满足要求,因此我们必须继续优化。
看着形式,我就只能想到单调队列或者是斜率优化了,这道题单调队列优化显然是可以的。
我们i,j循环变量不能省略,能省略的就只有k了。我们把i和j与k分离开:
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
分离出来的这部分:
(opt[k]−sum[j][k]−cost[j+k])(opt[k]−sum[j][k]−cost[j+k])
就只与kk和jj有关了,因此,我们可以建立nn个单调队列对应jj,队列内部对应kk。
dp核心代码
for(int i = 1;i <= m;++i){ //i表示时刻 for(int j = 1;j <= n;++j){ //j表示机器人的购买点所在的斜线编号 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:出发点 2: 步数 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; //dp for(int i = 1;i <= m;++i){ //i表示时刻 for(int j = 1;j <= n;++j){ //j表示机器人购买点所在的斜线编号 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 中国象棋
题意
点击链接查原题目题解
我好菜啊,这道是原题,暑假集训的时候做过,而且当时还独自做出来了,过了半年,自己再做的时候发现竟然不会做了,看了题解的提示才想出来。咋越练越菜呢???这道题目关键点在状态的定义!
我们需要把无用的信息都去除掉,在状态定义的时候尽量捕捉最本质、最关键的信息。
例如本题,按行考虑,在考虑到第i行的时候,我们关注点在前i行中,形成的0炮列有几个,1个炮的列有几个,2个跑的列有几个,这样。
而我们不需要知道具体的前i行的棋子排列。
状态定义就出来了:
dp[i][j][k]dp[i][j][k]表示前i行,构成有j个1炮列、k个2炮列可行的方案数。
这样的话,转移方程也很容易得到,就是乘法原理。
注意分类的时候这样分:
第i行不加棋子的转移、加1个棋子的转移、加2个棋子的转移。
加1个棋子的转移又可以分为:把这个棋子加到0炮列,把这个棋子加到1炮列。
加2个棋子的转移又可以分为:全都加到0炮列、全都加到1炮列、0炮列1炮列分别加一个。
按照这样分类转移,转移方程很好写。
代码
#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){ //0 dp[i][j][k] = dp[i-1][j][k]; //1 if(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; } //2 if(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 [i][j])%mod; } } cout<<ans<<endl; }
相关文章推荐
- 动态规划 洛谷P1070 道路游戏
- 洛谷---动态规划---动归---dp
- |洛谷|NOIP2010|动态规划|P1541 乌龟棋
- |洛谷|NOIP2006|动态规划|P1063 能量项链
- 洛谷-最大子段和-动态规划
- 洛谷P2577 [ZJOI2005]午餐
- 洛谷Oj-Likecloud-吃、吃、吃-多维动态规划
- |洛谷|动态规划|P1282 多米诺骨牌
- |洛谷|动态规划|P2014 选课
- |洛谷|动态规划|P1115 最大子段和
- 洛谷P2426 删数 [2017年4月计划 动态规划12]
- |洛谷|NOIP2007|动态规划|P1095 守望者的逃离
- 洛谷 P2051 [AHOI2009]中国象棋
- 洛谷-教主的花园-动态规划
- |洛谷|动态规划|P1968 美元汇率
- 洛谷P1973 [NOI2011]Noi嘉年华(动态规划,决策单调性)
- 洛谷 P2051 [AHOI2009]中国象棋
- 洛谷-摆花-动态规划
- 洛谷P1002 过河卒 [2017年4月计划 动态规划15]
- 洛谷月赛 Hello World(升级版) - 动态规划