Living-Dream 系列笔记 第48期
区间 dp:以区间为子问题的 dp。
特征:
-
从左往右 / 从右往左递推会得到不同结果;
-
常为 合并类 / 拆分类 / 处理两端类;
-
要么枚举中间断点,要么枚举两端点。
T1
令 \(dp_{i,j}\) 表示合并区间 \([i,j]\) 所需代价。易得答案为 \(dp_{1,n}\)。
初始状态即为 \(dp_{i,i}=0\),其余为 \(\infty\)。
考虑枚举中转点 \(k\),将区间 \([i,j]\) 分为区间 \([i,k]\) 与 \([k+1,j]\) 分别进行合并。
因此有转移:
(\(sum_i\) 为 \(m_i\) 的前缀和)
\(O(n^3)\) 转移即可。
code
#include<bits/stdc++.h> #define int long long using namespace std; const int N=3e2+5; int n,a[N],sum[N]; int dp[N][N]; signed main(){ ios::sync_with_stdio(0); cin>>n; memset(dp,0x3f,sizeof(dp)); for(int i=1;i<=n;i++) cin>>a[i],sum[i]=sum[i-1]+a[i],dp[i][i]=0; for(int i=2;i<=n;i++){ for(int j=1;j+i-1<=n;j++){ int s=j,e=j+i-1; for(int k=s;k<=e-1;k++) dp[s][e]=min(dp[s][e],dp[s][k]+dp[k+1][e]+sum[e]-sum[s-1]); } } cout<<dp[1][n]; return 0; }
T2
令 \(dp_{i,j}\) 表示合并区间 \([i,j]\) 所能获得的最大数。易得答案为 \(dp_{1,n}\)。
初始状态即为 \(dp_{i,i}=a_i\),其余为 \(-\infty\)。
显然有转移:
\(O(n^3)\) 转移即可。
code
#include<bits/stdc++.h> #define int long long using namespace std; const int N=3e2+5; int n,a[N],sum[N]; int dp[N][N]; signed main(){ ios::sync_with_stdio(0); cin>>n; memset(dp,0xcf,sizeof(dp)); for(int i=1;i<=n;i++) cin>>a[i],dp[i][i]=a[i]; for(int i=2;i<=n;i++){ for(int j=1;j+i-1<=n;j++){ int s=j,e=j+i-1; for(int k=s;k<=e-1;k++) if(dp[s][k]==dp[k+1][e]) dp[s][e]=max(dp[s][e],dp[s][k]+1); } } int ans=-1e9; for(int i=1;i<=n;i++) for(int j=i;j<=n;j++) ans=max(ans,dp[i][j]); cout<<ans; return 0; }
T3
代码要去掉多测。
作业 T1
串串 + 区间 dp 简单题(?)。
令 \(dp_{i,j}\) 表示将区间 \([i,j]\) 涂成目标颜色所需的最少涂色次数。易得答案为 \(dp_{1,n}\)。
初始状态即为 \(dp_{i,i}=1\),其余为 \(\infty\)。
分类讨论:
-
若 \(s_i=s_j\),则相当于在涂 \(i\) / \(j\) 时顺带将 \(j\) / \(i\) 涂上了,无需额外再涂一次,于是有转移 \(dp_{i,j}=\max(dp_{i+1,j},dp_{i,j-1})\)。
-
否则,有转移 \(dp_{i,j}=\max(dp_{i,j},dp_{i,k}+dp_{k+1,j})\)。
\(O(n^3)\) 转移即可。
code
#include<bits/stdc++.h> #define int long long using namespace std; const int N=1e2+5; string s; int n; int dp[N][N]; signed main(){ ios::sync_with_stdio(0); cin>>s,n=s.size(),s='#'+s; memset(dp,0x3f,sizeof(dp)); for(int i=1;i<=n;i++) dp[i][i]=1; for(int len=2;len<=n;len++){ for(int i=1;i+len-1<=n;i++){ int j=i+len-1; if(s[i]==s[j]) dp[i][j]=min(dp[i+1][j],dp[i][j-1]); else for(int k=i;k<j;k++) dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]); } } cout<<dp[1][n]; return 0; }
作业 T2
令 \(dp_{i,j}\) 表示区间(即一棵子树) \([i,j]\) 的最高加分。易得答案为 \(dp_{1,n}\)。
考虑枚举根 \(k\),则其左子树为 \([i,k]\),右子树为 \([k+1,j]\)。
于是有转移:
注意要特判 \(dp_{i+1,i}\) 与 \(dp_{i,i-1}\)(即只有左子树的节点 / 只有右子树的节点 / 叶子节点)。
至于输出前序遍历,我们每次更新答案时记录根,然后递归输出即可。
code
#include<bits/stdc++.h> #define int long long using namespace std; const int N=31; int n; int dp[N][N]; int root[N][N]; void print(int l,int r){ if(l>r) return; cout<<root[l][r]<<' '; if(l==r) return; print(l,root[l][r]-1); print(root[l][r]+1,r); } signed main(){ ios::sync_with_stdio(0); cin>>n; memset(dp,0xcf,sizeof(dp)); for(int i=1;i<=n;i++) cin>>dp[i][i],dp[i][i-1]=dp[i+1][i]=1,root[i][i]=i; for(int i=2;i<=n;i++){ for(int j=1;j+i-1<=n;j++){ int s=j,e=j+i-1; for(int k=s;k<=e;k++) if(dp[s][e]<dp[s][k-1]*dp[k+1][e]+dp[k][k]) dp[s][e]=dp[s][k-1]*dp[k+1][e]+dp[k][k], root[s][e]=k; } } cout<<dp[1][n]<<'\n'; print(1,n); return 0; }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!