洛谷专题-更要技巧的动规与记忆化
P1064金明的预算方案(有依赖的背包问题)
题意:
金明有n元钱,每个物件有对应的价值与重要程度,金明希望购物物品的价值乘上重要程度和最大,请求出最大和,现在有些物品为一些其他物品的附属品,如果要买归类为附件的物品,必须先买该附件所属的主件
思路:
带有附件的背包问题,它属于01背包的变式。
即然物品分为主件和附件两类,且每个主件最多包含两个附件,那么我们不妨枚举所有的主件。那么,对于每次枚举,会有五种情况:
- 什么都不买
- 只买主件
- 买主件和第一个附件
- 买主件和第二个附件
- 买主件和两个附件
只要把这五种情况最终的价值算出来,取最大值就可以了
#include <iostream> #define maxn 32005 using namespace std; int n,m; int v,p,q; int main_item_w[maxn]; int main_item_c[maxn]; int annex_item_w[maxn][3]; int annex_item_c[maxn][3]; int f[maxn]; int main(){ cin >> n >> m; for (int i=1;i<=m;i++){ cin >> v >> p >> q; if (!q){ main_item_w[i] = v; main_item_c[i] = v * p; } else{ annex_item_w[q][0]++; annex_item_w[q][annex_item_w[q][0]] = v; annex_item_c[q][annex_item_w[q][0]] = v * p; } } for (int i=1;i<=m;i++) for (int j=n;main_item_w[i]!=0 && j>=main_item_w[i];j--){ f[j] = max(f[j],f[j-main_item_w[i]]+main_item_c[i]); if (j >= main_item_w[i] + annex_item_w[i][1]) f[j] = max(f[j],f[ j - main_item_w[i] - annex_item_w[i][1] ] + main_item_c[i] + annex_item_c[i][1]); if (j >= main_item_w[i] + annex_item_w[i][2]) f[j] = max(f[j],f[ j - main_item_w[i] - annex_item_w[i][2] ] + main_item_c[i] + annex_item_c[i][2]); if (j >= main_item_w[i] + annex_item_w[i][1] + annex_item_w[i][2]) f[j] = max(f[j],f[ j - main_item_w[i] - annex_item_w[i][1] - annex_item_w[i][2] ] + main_item_c[i] + annex_item_c[i][1] + annex_item_c[i][2]); } cout << f[n] << endl; return 0; }
P1541 乌龟棋
题意:
有四种牌,代表分别可向前跳1,2,3,4步,并且有一定的数量限制,得分为路径点的权值和,问从起点跳到终点的得分最大值是多少
思路:
一共有四张卡片,代表所能走的不同的步数,也就是当前数的取得,有可能是在前一个数的基础上,走了一步/两步/三步/四步而来,因而考虑用一个四维的状态数组f[i][j][k][h]分别表示卡片的张数,DP列出所有可能的状态即可。
要注意的细节:
第一格是唯一的起点,所以f[0][0][0][0]=a[1],也就是一张卡片都没有用的时候,取得的初值。
如果用了第一张卡片,则用f[1][0][0][0]表示,有f[1][0][0][0]=f[0][0][0][0]+a[2],也就是从第一格开始,走了一格,实际上要取的数是a[2],也就是a[t+1],其中t为卡片所规定的步数。
每张卡片只能用一次,但每一种卡片不只一张,因此在输入的时候,需要做个统计。
注意动规的初值,循环要从0开始,表示没有用到相应的卡片。
写法一:DP
#include<iostream> #include<algorithm> #include<cstdio> using namespace std; const int maxn=41; int dp[maxn][maxn][maxn][maxn],a[1000],b[5]; int main() { int n,m,x; scanf("%d%d%",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&a[i]); for(int i=1;i<=m;i++){ scanf("%d",&x); b[x]++; } dp[0][0][0][0]=a[1]; for(int i=0;i<=b[1];i++){ for(int j=0;j<=b[2];j++){ for(int k=0;k<=b[3];k++){ for(int l=0;l<=b[4];l++){ int t=a[i+2*j+3*k+4*l+1]; if(i!=0) dp[i][j][k][l]=max(dp[i-1][j][k][l]+t,dp[i][j][k][l]); if(j!=0) dp[i][j][k][l]=max(dp[i][j-1][k][l]+t,dp[i][j][k][l]); if(k!=0) dp[i][j][k][l]=max(dp[i][j][k-1][l]+t,dp[i][j][k][l]); if(l!=0) dp[i][j][k][l]=max(dp[i][j][k][l-1]+t,dp[i][j][k][l]); } } } } cout<<dp[b[1]][b[2]][b[3]][b[4]]<<endl; }
写法二:记忆化搜索
include<bits/stdc++.h> using namespace std; int f[41][41][41][41]; int n; int s[351]; int tot[5]; inline int dfs(int a,int b,int c,int d) { if(f[a][b][c][d]!=0) return f[a][b][c][d]; if(a)f[a][b][c][d]=max(f[a][b][c][d],dfs(a-1,b,c,d)); if(b)f[a][b][c][d]=max(f[a][b][c][d],dfs(a,b-1,c,d)); if(c)f[a][b][c][d]=max(f[a][b][c][d],dfs(a,b,c-1,d)); if(d)f[a][b][c][d]=max(f[a][b][c][d],dfs(a,b,c,d-1)); f[a][b][c][d]+=s[a+2*b+3*c+4*d+1]; return f[a][b][c][d]; } int main() { int n,m; cin>>n>>m; for(register int i=1;i<=n;i++) cin>>s[i]; int q; for(register int i=1;i<=m;i++) { cin>>q; tot[q]++; } f[0][0][0][0]=s[1]; cout<<dfs(tot[1],tot[2],tot[3],tot[4])<<endl; return 0; }
P1026统计单次个数(Hash+DP)
题意:
给你一段字符串,已经一些单次,现在让你把字符串分成k段,使得每段的单次个数和最大
思路:
定义DP[i][j]表示前i个字母分成j段得到的最大单词数,答案是DP[len][k],可以初始化一下F[i][i]和F[i][1]. 方程F(i,j)=max{ F(r,j-1)+w(r+1,i) (r=j...i-1) }. 意思就是把1..r的字母先分成j-1段,剩下的r+1..i的字母分成另一段。
先预处理出w[i][j],表示从i到j的单词数。可以倒着推,w[i][j]=w[i+1][j];(如果存在从A[i]字母开始的单词,则w[i][j]=w[i+1][j]+1.出现同一字母开头的多个单词也还是加1就够了.)
至于怎么判断一段里有多少个单次,我就选择了用哈希来处理
#include<iostream> #include<algorithm> #include<cstring> #include<cstdio> using namespace std; const int ran=233; const int mod=1e9+7; int n,k,s; char ch[300],f[10][300]; int ha[300],sum[300][300],dp[300][300]; int hash(char *c) { int len=strlen(c); int ans=0; for(int i=0;i<len;i++) ans=(ans*ran+c[i])%mod; return ans; } int judge(int x,int y) { char t[300]; for(int i=1;i<=s;i++){ memset(t,0,sizeof(t)); int len=strlen(f[i]); strncpy(t,ch+x,min(len,y-x+1)); if(hash(t)==ha[i])return 1; } return 0; } int main() { scanf("%d%d",&n,&k); for(int i=1;i<=n;i++) scanf("%s",ch+(i-1)*20); scanf("%d",&s); for(int i=1;i<=s;i++) scanf("%s",f[i]); for(int i=1;i<=s;i++) ha[i]=hash(f[i]); int len=strlen(ch); for(int i=len-1;i>=0;i--){ for(int j=i;j>=0;j--){ sum[j][i]=sum[j+1][i]; if(judge(j,i)) sum[j][i]++; } } for(int i=0;i<len;i++){ for(int j=1;j<=k;j++){ if(j==1) dp[i][j]=sum[0][i]; else{ for(int l=0;l<i;l++){ if(dp[l][j-1]) dp[i][j]=max(dp[i][j],dp[l][j-1]+sum[l+1][i]); } } } } cout<<dp[len-1][k]<<endl; return 0; }
P1063能量项链(区间DP)
思路:
重点就是将整体划分为区间,小区间之间合并获得大区间
状态转移方程的推导如下
一、将珠子划分为两个珠子一个区间时,这个区间的能量=左边珠子*右边珠子*右边珠子的下一个珠子
二、区间包含3个珠子,可以是左边单个珠子的区间+右边两珠子的区间,或者左边两珠子的区间右边+单个珠子的区间 。即,先合并两个珠子的区间,释放能量,加上单个珠子区间的能量(单个珠子没有能量。。) ,Energy=max(两个珠子的区间的能量+单个珠子区间的能量,单个珠子的区间的能量+两个珠子的区间的能量 )
三、继续推4个珠子的区间,5个珠子的区间。 于是可以得到方程:Energy=max(不操作的能量,左区间合并后的能量+右区间合并后的能量+两区间合并产生能量)
两区间合并后产生的能量=左区间第一个珠子*右区间第一个珠子*总区间后面的一个珠子
#include<iostream> #include<algorithm> #include<cstdio> #include<cstring> using namespace std; const int maxn=205; struct node{ int x,y; }a[maxn]; int dp[maxn][maxn]; int main() { int n; scanf("%d",& n); for(int i=1;i<=n;i++){ scanf("%d",&a[i].x); a[i-1].y=a[i].x; } a[n].y=a[1].x; for(int i=1;i<n;i++) a[i+n]=a[i]; memset(dp,0,sizeof(dp)); for(int len=1;len<=n;len++){ for(int i=1;i<=2*n-len+1;i++){ for(int k=i;k<i+len;k++){ int j=i+len; int t=a[i].x*a[k].y*a[j].y; dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]+t); } } } int ans=0; for(int i=1;i<=n;i++) ans=max(ans,dp[i][i+n-1]); cout<<ans<<endl; return 0; }
P1156 垃圾陷阱(01背包,记忆化搜索)
题意:
有一只牛在井底,一些时间会从天上丢下垃圾,牛可以选择吃多,多活f秒,或者堆起来增加h高度,问牛最快什么时候能出来,出不来的话最多能活多久
思路:
①记忆化搜索:没什么好说的,搜就完事了
#include<iostream> #include<algorithm> #include<cstring> #include<cstdio> #include<map> #define inf 0x3f3f3f3f const int maxn=105; using namespace std; struct node{ int t,f,h; }a[105]; int D,G,ok,max_time=0,min_time=inf; map<int,map<int,map<int,bool> > > ed; void dfs(int cnt,int hi,int tim) { if(ed[cnt][hi][tim]) return; ed[cnt][hi][tim]=1; if(cnt>G+1) return; if(tim-(a[cnt].t-a[cnt-1].t)<0){ max_time=max(max_time,a[cnt-1].t+tim); return; } else tim-=(a[cnt].t-a[cnt-1].t); if(hi+a[cnt].h>=D){ ok=1; min_time=min(min_time,a[cnt].t); } dfs(cnt+1,hi+a[cnt].h,tim); dfs(cnt+1,hi,tim+a[cnt].f); return; } int cmp(node a,node b){return a.t<b.t;} int main() { scanf("%d%d",&D,&G); for(int i=1;i<=G;i++) scanf("%d%d%d",&a[i].t,&a[i].f,&a[i].h); sort(a+1,a+1+G,cmp); a[G+1].t=inf; dfs(1,0,10); if(ok) cout<<min_time<<endl; else cout<<max_time<<endl; return 0; }
②01背包:这个题非常的像0101背包,但是又有不同之处,因为0101背包中的“不装”就不对状态做修改,这里不装则是将垃圾堆起来
当不选用这个垃圾来当垫子时:f[i+1][j]=max(f[i+1][j],f[i][j]+num[i].f)
当选用这个垃圾来当垫子时:f[i+1][j+h[i]]=max(f[i+1][j+num[i].h],f[i][j])
num[].h表示垃圾可以垫高的高度,num[].f表示吃垃圾可以维持的生命多少
对于爬不出的情况:ans=max(ans,f[i][0]) (1<=i<=n)
初始化:f[0][0]=10
#include<iostream> #include<algorithm> #include<cstring> #include<cstdio> #define inf 0x3f3f3f3f using namespace std; const int maxn=105; struct node{ int t,f,h; }a[105]; int dp[maxn][maxn]; int cmp(node a,node b){return a.t<b.t;} int main() { int h,n; scanf("%d%d",&h,&n); for(int i=1;i<=n;i++) scanf("%d%d%d",&a[i].t,&a[i].f,&a[i].h); sort(a+1,a+1+n,cmp); dp[0][0]=10; for(int i=0;i<n;i++){ for(int j=0;j<=h;j++){ if(dp[i][j]>=a[i+1].t){ int hight=j+a[i+1].h; if(hight>=h){ cout<<a[i+1].t<<endl; return 0; } dp[i+1][j]=max(dp[i+1][j],dp[i][j]+a[i+1].f); dp[i+1][hight]=max(dp[i+1][hight],dp[i][j]); } } } int ans=-inf; for(int i=1;i<=n;i++) ans=max(ans,dp[i][0]); cout<<ans<<endl; return 0; }
P1052过河(路径压缩)
题意:
青蛙每次可以跳[S,T]步,在一些位置上有石头,问青蛙从起点跳到终点,最少要跳到多少个石头上
思路:
仔细分析题目,会发现,石头的最大值只有100个,也就是说,在一个很长的数轴上,只有很少的点,这是一个很稀疏的图, 根据离散的思维,可以把两点之间的距离大幅地缩减。
路径压缩分析
压缩的范围可以从题目中找方法,题目说青蛙每次只能跳1<=s<=t<=10的距离,所以对1-10求最小公倍数,答案是2520,也就是说,如果从a点出发,一定能跳到 a+2520 点和 a+x*2520的点
设当前是从 a 点跳到 b 点, b 点可能有石头(说明 a 点和 b 点之间,一定没有石头,怎么跳都不影响答案);
按照上面的分析:可以分情况讨论:
1 如果 a < b< a+2520,则正常操作;
2.如果 a + 2520 < b,则让 a 先跳到 a+2520 的点上,再重复 1 的操作;
3 依次类推,如果 a + x * 2520 < b,可以看作 a 可以直接飞过中间的x个区间,到离 b 最近的一个位置,在按照1的操作执行;
根据以上的操作,可以把任意两个点之间的距离d,压缩到 2520 以内,因此 f 数组的空间就可以从原来 10^9^ 缩小到 50W以内,然后就可以正常跑DP了。
#include<iostream> #include<algorithm> #include<cstdio> #include<cstring> #define inf 0x3f3f3f3f using namespace std; const int maxn=1e6+10; const int mod=2520; int a[maxn],b[maxn],dp[maxn],flag[maxn]; int main() { int l,s,t,m,n; cin>>l>>s>>t>>m; memset(flag,0,sizeof(flag)); for(int i=1;i<=m;i++) scanf("%d",&a[i]); sort(a+1,a+1+m); for(int i=1;i<=m;i++) b[i]=(a[i]-a[i-1])%mod; for(int i=1;i<=m;i++){ a[i]=a[i-1]+b[i]; flag[a[i]]=1; } n=a[m]; memset(dp,inf,sizeof(dp)); dp[0]=0; for(int i=0;i<=n+t;i++){ for(int j=s;j<=t;j++) if(i-j>=0) dp[i]=min(dp[i],dp[i-j]+flag[i]); } int ans=inf; for(int i=n;i<=n+t;i++) ans=min(dp[i],ans); cout<<ans<<endl; return 0; }