DP做题合集
第一题 P7074 [CSP-J2020] 方格取数
做法【dp】
阶段
因为只能往左不能往右,所以我们可以以一列作为一个阶段。
又因为路线不能重复,所以在一列之中,只能一直向上或一直向下,所以我们分类讨论。
PS:妙啊,通过分类讨论来解决路径的后效性问题。
状态定义
表示位于第 行 ,第 列,当 时,表示从上方来到当前格,当 时,表示从下方来到当前格,时得到的最大值。
状态转移方程
当 时:
当 时:
初始化
因为起点在左上角,所以第一列只能自上往下走。
code
#include<iostream> #include<cstdio> #include<cmath> #include<cstring> #define ll long long using namespace std; ll n,m; ll a[1005][1005],v[1005][1005][2],f[1005][1005]; int main(){ scanf("%lld%lld",&n,&m); for(ll i=1;i<=n;i++) for(ll j=1;j<=m;j++) scanf("%lld",&a[i][j]); memset(v,0xcf,sizeof(v)); for(int i=1;i<=n;i++) f[i][1]=f[i-1][1]+a[i][1]; for(int j=2;j<=m;j++){ for(int i=1;i<=n;i++) v[i][j][1]=max(f[i][j-1],v[i-1][j][1])+a[i][j]; for(int i=n;i>=1;i--) v[i][j][0]=max(f[i][j-1],v[i+1][j][0])+a[i][j]; for(int i=1;i<=n;i++) f[i][j]=max(v[i][j][0],v[i][j][1]); } cout<<f[n][m]<<endl; return 0; }
summary
这道题与普通的区间 不同,不止可以往左,往下走,还可以往上走,这样就无法满足 的无后效性。
为了解决这个问题,于是采用了分类讨论的方法。
因为只能往左,所以我们可以把每列分成一个阶段。因为路径不能重叠,所以对于每一列,只能一直向上或者一直向下。
于是我们在正常的区间 上再加一维来记录向上 or 向下走。
第二题 P5662 [CSP-J2019] 纪念品
思路分析
题目中说了,求在最后将所有纪念品卖掉后能拥有的最多金币数量。
那么我们就可以设 表示在第 天把所有纪念品都卖掉后能拥有的最多金币的数量。
由于对于第 天,卖出纪念品和买入纪念品的价格相同,因此,我们可以在第 天先将手上所有的纪念品都卖掉。然后利用与次日相同纪念品的价格差挑选最佳纪念品从中赚取利润(是的,这就叫中间商赚差价)。即可求出第 天初始得到的最大金币(这里指的也是在第 天将所有纪念品卖掉后的,即 )。
那么他的状态转移方程就是:
{}
还有变量无法表示出来,我们就将他加入状态转移方程中。(相信有聪明的小朋友已经看出来接下来就是标准的完全背包了)
表示用第 天把所有纪念品都卖掉后能拥有的最多金币花费元购买纪念品所能得到的最大价值。
花费的是第 天购买纪念品的价格,得到的价值为第 天卖出后得到的收益。
对于一个纪念品 。
第 天能得到的最多金币数为:
是第 天获得的最大金币数, 是第 天能得到的最多金币数。
写代码的过程中可以发现我们己经枚举了天数,且在状态转移方程中 丝毫不动,因此可以将其省略。
code
#include<iostream> #include<cstdio> #include<cmath> #include<cstring> using namespace std; int T,n,m; int dp[10005],val[105][1005]; int main(){ scanf("%d%d%d",&T,&n,&m); for(int i=1;i<=T;i++) for(int j=1;j<=n;j++) scanf("%d",&val[i][j]); int ans=m; for(int t=1;t<T;t++){ memset(dp,0xcf,sizeof(dp)); dp[0]=0; for(int i=1;i<=n;i++){ for(int j=val[t][i];j<=ans;j++){ dp[j]=max(dp[j],dp[j-val[t][i]]+val[t+1][i]); } } int num=ans; for(int i=1;i<=num;i++) ans=max(ans,num-i+dp[i]); } cout<<ans<<endl; return 0; }
summary
通过这题,我们可以总结出,一题DP可以先将状态设为题目要求的东西,再列出状态转移方程。如果状态转移方程有些部分无法表示出来,就将他加入状态之中。
第三题 P1018 [NOIP2000 提高组] 乘积最大
前言
事先声明!博主是不会写高精的屑。因此此题只拿到了开 的 分。
但这并不妨碍我练 。
思路辨析
很容易想到,以前 个数的部分作为一个阶段变量。
有了具体的数量,又很自然的想到将钥匙的个数作为一个变量加进去,也就是 。
诶,好像能行,再看看。
综上, 表示的是前 个数用 个乘号隔开所能得到的最大乘积。
设想一下,当我们知道如上所述的信息时,要怎么通过它得到下一阶段的信息,或它是如何由其它阶段推来的呢?
显然第二种更好推。
已知钥匙的个数,枚举最后一个钥匙所在的位置(这里记为 ),可以将前 个数分成两部分。
前半部分是前 个数用 个乘号断开所得到的乘积最大值,后半部分则是 个字符中第 个字符到第 个字符所组成的数,用两者之积来更新答案。
因此,状态转移方程为
.
注:就是 个字符中第 个字符到第 个字符所组成的数。
code
#include<iostream> #include<cstdio> #include<cmath> #define ll long long using namespace std; ll n,k,a[45],nu[45][45],dp[45][10]; int main(){ scanf("%lld%lld",&n,&k); for(int i=1;i<=n;i++) scanf("%1d",&a[i]);//“%1d”每次只读一个数字 for(int i=1;i<=n;i++) nu[i][i]=a[i]; for(int i=1;i<=n;i++){ for(int j=i+1;j<=n;j++){ nu[i][j]=nu[i][j-1]*10+a[j]; } } //这一段是对nu的预处理 for(int i=1;i<=n;i++) dp[i][0]=nu[1][i];//初始化 for(ll i=1;i<=n;i++){ for(int j=1;j<min(i,k+1);j++){//i个数最多只能放i-1个乘号,所以和k+1取min for(int r=1;r<i;r++){ dp[i][j]=max(dp[r][j-1]*nu[r+1][i],dp[i][j]); } } } cout<<dp[n][k]<<endl; return 0; }
summary
对状态的描述好像有进步!继续加油!
第四题 P1586 四方定理
思路分析
对于一个数 ,它可能由 () 个平方数组成。
我们不妨设 为数 由 个平方数所组成的方案。
那么很容易想到 。即枚举所有小于 的平方数,加上包含其的方案数。
于是我们就能得到以下代码
dp[0][0]=1; for(int i=1;i<=32768;i++){ for(int j=1;j<=4;j++){ for(int k=1;k*k<=i;k++){ dp[i][j]+=dp[i-rec[k]][j-1]; } } }
乍看似乎没什么问题。但输入 后会发现,标准答案是 ,输出却是 。
这是怎么回事呢?仔细研究后会发现。如果这么枚举的话, 和 被当成两种方案被统计了两次。
此时,需要交换枚举顺序,令一种方案被其和数中的最高次方平方数累计。
简单来说,就是令 被 统计,而不是 。
于是乎得到以下代码。
dp[0][0]=1; for(int i=1;i*i<=32768;i++){ for(int j=i*i;j<=32768;j++){ for(int k=1;k<=4;k++){ dp[j][k]+=dp[j-rec[i]][k-1]; } } }
至此,此题就可以 啦。
code
点击查看代码
#include<iostream> #include<cstdio> #include<cmath> using namespace std; int rec[32770],dp[32770][5]; int t,n; int main(){ scanf("%d",&t); for(int i=1;i*i<=32768;i++) rec[i]=i*i; dp[0][0]=1; for(int i=1;i*i<=32768;i++){ for(int j=i*i;j<=32768;j++){ for(int k=1;k<=4;k++){ dp[j][k]+=dp[j-rec[i]][k-1]; } } } for(int i=1;i<=t;i++){ scanf("%d",&n); int num=0; for(int j=1;j<=4;j++) num+=dp[n][j]; cout<<num<<endl; } return 0; }
summary
对于去重似乎打开了新世界的大门。
仔细回想,通过规定顺序来去重的方法已经不是第一次见了。
在深搜中我们经常通过增加变量 来固定枚举顺序以达到剪枝的作用。这里剪掉的,恰好就是重复的方案。
在质数筛中,线性筛法就是通过保证每个合数 只会被它最小质因子 筛一次,从而优化掉了被重复筛的合数。和此题是不是有着异曲同工之妙!
第五题 P2426 删数
题目分析
由于对于题目所得的最优删法,与删除的顺序无关,因此我们可以默认从前往后删片段。
设 表示删除前 个数所得到的最大价值。
对于第 个数,它可以选择独自删除 。状态转移方程为
。
亦可以选择与前面的数一起删掉。求此时与哪几个数一起删得最大值。
表示删除 段的数。
code
#include<iostream> #include<cstdio> #include<cmath> using namespace std; int n,a[105],dp[105]; int main(){ scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&a[i]); dp[1]=a[1]; for(int i=2;i<=n;i++){ for(int j=1;j<i;j++){ dp[i]=max(dp[i],dp[j-1]+abs(a[i]-a[j])*(i-j+1)); } dp[i]=max(dp[i],dp[i-1]+a[i]); } cout<<dp[n]<<endl; return 0; }
summary
今天这题看似很简单,想明白却不容易。
本来一开始定义的是 表示删除 段所获得的最大价值。
但接下来就遇到难点了,我们要如何枚举断点 。
如果只是将断点 看做将 字符分成两端所得到的最大价值,那可出大问题了。
对于一段字符,它的最优断法不一定是断成两段。
但是,若断点表示其中有一段删除的是,而删除 段所获得的最大价值为 。这...状态转移方程不就又出来了。
至此,根本思路与题目分析已无差别。
因此,在思考问题时,一定要注意从问题本身出发,来找状态转移方程。
第六道 P1005 [NOIP2007 提高组] 矩阵取数游戏
前言
今天依旧是不写高精的一天呢!(是的,这位作者又只拿了开 的 分)
思路描述
看到数据 就知道数组可以任性开,心理有个底后,再来看题目。
状态描述
首先肯定要来一个 来表示第 次时取第 行的数。
对于每一次放置,我们要考虑到的是之前每一次都取到什么,也就是现在的头和尾分别是哪两个数。
想明白这一点,就可以描述状态了。
表示第 次时取第 行的数,对于第 行,它的行首被取了 个数,他的行尾被取了 个数。
由于 ,当 确定时, 也一定唯一,因此可以省略。
状态转移方程
描述出状态了,状态转移方程还会远吗?
显然有
。
表示第 次时取位于第 行第 列的数所能获得的得分。
中的两者分别对应了第 次时,在第 行取队首 队尾的情况。
code
#include<iostream> #include<cstdio> #include<cmath> #define ll long long using namespace std; int n,m; ll a[85][85],dp[85][85][85]; int bas[31]; int main(){ scanf("%d%d",&n,&m); bas[0]=1; for(int i=1;i<=30;i++) bas[i]=bas[i-1]*2; for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) scanf("%d",&a[i][j]); for(int i=1;i<=m;i++){ for(int j=1;j<=n;j++){ dp[i][j][i]=dp[i-1][j][i-1]+a[j][i]*bas[i],dp[i][j][0]=dp[i-1][j][0]+a[j][m-i+1]*bas[i];//这两种情况比较特殊,所以单独列。 for(int k=1;k<i;k++){ dp[i][j][k]=max(dp[i-1][j][k-1]+a[j][k]*bas[i],dp[i-1][j][k]+a[j][m-(i-k)+1]*bas[i]); } } } ll ans=0; for(int i=1;i<=n;i++){ ll max_num=0; for(int j=0;j<=m;j++) max_num=max(max_num,dp[m][i][j]); ans+=max_num; } cout<<ans<<endl; return 0; }
ps:经过作者后续习惯性翻翻题解(发现原来区间DP也可以做),以及打输出时的共同启发,发现实际上我们只需要分别枚举对于每一行是的最优解,加起来就可以了。因此状态中表示行的那一维可以省略。然后就有了以下代码。
#include<iostream> #include<cstdio> #include<cmath> #define ll long long using namespace std; int n,m; ll a[85][85],dp[85][85]; int bas[31]; int main(){ scanf("%d%d",&n,&m); bas[0]=1; for(int i=1;i<=30;i++) bas[i]=bas[i-1]*2; for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) scanf("%d",&a[i][j]); ll ans=0,max_num; for(int j=1;j<=n;j++){ for(int i=1;i<=m;i++){ dp[i][i]=dp[i-1][i-1]+a[j][i]*bas[i],dp[i][0]=dp[i-1][0]+a[j][m-i+1]*bas[i]; for(int k=1;k<i;k++){ dp[i][k]=max(dp[i-1][k-1]+a[j][k]*bas[i],dp[i-1][k]+a[j][m-(i-k)+1]*bas[i]); } } max_num=0; for(int i=0;i<=m;i++) max_num=max(max_num,dp[m][i]); ans+=max_num; } cout<<ans<<endl; return 0; }
事实上没太大区别,毕竟它的数据范围可以让我任性开(首尾呼应.jpg(确信))。
summary
对于省略维数有了更深刻的理解。
-
可以用其他维度表示的可以省略。
-
可以通过分开解决时不需要整体来定义。
第七、八道 P1352 没有上司的舞会+P1122 最大子树和(树形DP入门)
前言
今日偶然打开 ,发现树形 例题正好是之前在洛谷上鸽着的一道题。所以......
这例题造的太好了,简直是无痛入门(感动.jpg)
P1352 没有上司的舞会
思路剖析
状态定义
表示的是以 为根节点的子树所获得的最大价值。
由于每个节点代表着一位人物,有来与不来两种状态,所以再加一维状态变量。
表示以 为根节点的子树所能获得的最大价值,且这位人物没来。 则对应来了的状态。
状态转移方程
现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 r_i。 但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。
根据题意描述,容易得出状态转移方程:
指的是 的子节点,且显然 的初始值为 。
code
#include<iostream> #include<cstdio> #include<cmath> using namespace std; int n,a[6005]; int head[6005],nex[6005],edge[6005],tot; int vis[6005],dp[6005][2]; void dfs(int x){ dp[x][1]=a[x]; for(int i=head[x];i;i=nex[i]){ int y=edge[i]; dfs(y); dp[x][1]+=dp[y][0]; dp[x][0]+=max(dp[y][0],dp[y][1]); } return; } int main(){ scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&a[i]); for(int i=1;i<n;i++){ int l,k; scanf("%d%d",&l,&k); nex[++tot]=head[k]; head[k]=tot; edge[tot]=l; vis[l]=1; } for(int i=1;i<=n;i++){ if(!vis[i]){ dfs(i); cout<<max(dp[i][0],dp[i][1])<<endl; return 0; } } }
P1122 最大子树和
思路剖析
谁是根节点
由于这题是无向图(但由于以 条边相连接,所以本质与树并无太大区别),所以要讨论以谁作为根节点。
根节点之所以重要,是因为在递归过程中,我们已经默认根节点所代表的那束花已经被保留了,但根节点代表的花不一定在最优解的集合之中。
仔细模拟后,不难发现,对于以 为根节点的子树, 往下为最优解,而往上由于还未更新,因此相当于剪去 与其根节点的枝桠。
进一步推理,无论通过哪个节点作为根节点,再递归的过程中,其实已经变相枚举了将其剪去的种种情况,所以,只需要在过程中取最优解即可。
状态定义+状态转移方程
这点比较好理解,所以合并在一起阐述。
表示以 为根节点的子树所获得的最大美丽值。
显然有
。
为子节点,当其所带来的价值为负数时,不如直接剪掉。
code
有几处雷点在注释中标记出来了(都是血泪教训啊QAQ)
#include<iostream> #include<cstdio> #include<cmath> using namespace std; int n,ans=-0x3f3f3f3f;//答案可能为负!要初始化为负无穷 int head[16005],nex[35005],edge[35005],tot;//由于是双向边,所以空间要开双倍 int dp[16005],vis[16005]; void dfs(int x){ vis[x]=1;//不要在循环内标记,否则标记不到根节点本身。 for(int i=head[x];i;i=nex[i]){ int y=edge[i]; if(vis[y]) continue; dfs(y); if(dp[y]<=0) continue; dp[x]+=dp[y]; } ans=max(ans,dp[x]); return; } void add(int l,int k) {nex[++tot]=head[k],head[k]=tot,edge[tot]=l;} int main(){ scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&dp[i]); for(int i=1;i<n;i++){ int l,k; scanf("%d%d",&l,&k); add(l,k); add(k,l); } dfs(1); cout<<ans<<endl; return 0; }
第九题 P1387 最大正方形
题目分析
设 表示以 为右下角的最大正方形的边长。
状态转移方程为:
为什么取最大边长却要用 来转移呢?
因为在三者中取最小,等于另外两者一定大于最小的值,不需要考虑另外两者比边长小的问题。且以最小的值 为边长进行状态转移,一定会形成为以 为右下角的最大正方形,再大一点都不符合条件,不会形成正方形。
code
#include<iostream> #include<cstdio> #include<cmath> using namespace std; int n,m,dp[105][105],a[105][105],ans; int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) scanf("%d",&a[i][j]); for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ if(a[i][j]) dp[i][j]=min(dp[i-1][j],min(dp[i-1][j-1],dp[i][j-1]))+1; ans=max(ans,dp[i][j]); } } cout<<ans<<endl; return 0; }
第十题 P3147 [USACO16OPEN]262144 P
题目分析
此题为区间 ,却与一般的 题不同。能够看出是两个相邻区间合并,但是却不知道具体是哪两个区间。
因此,我们需要将区间加入状态描述中。左端点,区间长度,右端点三选二。这里选择左端点和区间长度。
设 表示以 为左端点合成数字 的区间长度。当区间长度为 时,表示不存在这样的区间。
则可得到状态转移方程:
code
#include<iostream> #include<cstdio> #include<cmath> using namespace std; int n,dp[60][270000],ans; int main(){ scanf("%d",&n); int x; for(int i=1;i<=n;i++) scanf("%d",&x),dp[x][i]=1; for(int i=2;i<=58;i++){ for(int j=1;j<=n;j++){ if(!dp[i][j]) if(dp[i-1][j]&&dp[i-1][j+dp[i-1][j]])//判断两个区间是否存在 dp[i][j]=dp[i-1][j]+dp[i-1][j+dp[i-1][j]]; if(dp[i][j]) ans=max(ans,i); } } cout<<ans<<endl; return 0; }
的最大值为 是因为 且范围为 。
所以 的最大值为 。
summary
算是拓宽了在区间 解法中的另一种常规思路吧。和 算法有着异曲同工之妙,也算变相复习了 算法了。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战