1.11 下午-区间 DP & 树形 DP
前言
勿让将来,辜负曾经
从入门到入土……
正文
知识点
区间 DP 和树形 DP 都是动态规划这个大家族中的一个分支
区间 DP 比较明显,数据范围会给你莫大的提示。而树(甚至可以是生成树,缩点后的树,基环树)上的最值、统计方案的问题(期望),都可以往树形 DP 上靠。
一题一解
T1 石子合并(P1775)
区间 DP 的板中之钣,记得初始化即可
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=305;
int n,a[maxn];
int sum[maxn],dp[maxn][maxn];
inline void init(){
for(int i=1;i<=n;i++){
sum[i]=sum[i-1]+a[i];
}
memset(dp,0x3f,sizeof(dp));
for(int i=1;i<=n;i++){
dp[i][i]=0;
}
return;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
init();
for(int len=2;len<=n;len++){
for(int l=1;l<=n-len+1;l++){
int r=l+len-1;
int w=sum[r]-sum[l-1];
for(int k=l;k<r;k++){
dp[l][r]=min(dp[l][r],dp[l][k]+dp[k+1][r]+w);
}
}
}
cout<<dp[1][n]<<endl;
return 0;
}
T2 合并珠子(P1063)
还是很套路的区间 DP,需要注意到其特殊的环形结构,经典转化就是倍长原数组,破环为链
云落直接把石子合并的那一套搬了过来,看山去就比较愚笨哈!还真就记录了头尾标记(晕晕晕)
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=105;
int n,a[maxn<<1];
struct node{
int x,y;
}p[maxn<<1];
int dp[maxn<<1][maxn<<1];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
a[n+i]=a[i];
}
for(int i=1;i<=2*n-1;i++){
p[i]={a[i],a[i+1]};
}
p[2*n]={a[2*n],a[1]};
// for(int i=1;i<=n*2;i++){
// cout<<"Zyx "<<i<<": "<<p[i].x<<" "<<p[i].y<<endl;
// }
for(int len=2;len<=n;len++){
for(int l=1;l+len-1<=2*n;l++){
int r=l+len+-1;
for(int k=l;k<=r-1;k++){
dp[l][r]=max(dp[l][r],dp[l][k]+dp[k+1][r]+p[l].x*p[k].y*p[r].y);
}
}
}
int ans=0;
for(int i=1;i<=n;i++){
ans=max(ans,dp[i][i+n-1]);
}
cout<<ans<<endl;
return 0;
}
T3 关路灯(P1220)
注意到
圆规正传,显然有一个结论:R 不会在一个没有亮着的灯的区间里无聊地游荡,换言之,当 R 搞定每个区间
所以简单设计一下 DP 状态,记
进一步地,根据上面那个显然的结论,我们可以加一维度状态,即
由于 R 从位置
状态设计和初始化都有了,只差一个转移方程了
比较好想的是,
为什么嘞?
因为状态设计,我们这里的
做到这一步其实就差不多了,方程大可以手动推理
贴个代码,辅助理解——
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=55;
int n,c,a[maxn],b[maxn];
int s[maxn],f[maxn][maxn][2];
inline void init(){
for(int i=1;i<=n;i++){
s[i]=s[i-1]+b[i];
}
memset(f,0x3f,sizeof(f));
f[c][c][0]=0;
f[c][c][1]=0;
return;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>c;
for(int i=1;i<=n;i++){
cin>>a[i]>>b[i];
}
init();
for(int len=2;len<=n;len++){
for(int l=1;l+len-1<=n;l++){
int r=l+len-1;
f[l][r][0]=min(
f[l+1][r][0]+(a[l+1]-a[l])*(s[l]+s[n]-s[r]),
f[l+1][r][1]+(a[r]-a[l])*(s[l]+s[n]-s[r])
);
f[l][r][1]=min(
f[l][r-1][1]+(a[r]-a[r-1])*(s[l-1]+s[n]-s[r-1]),
f[l][r-1][0]+(a[r]-a[l])*(s[l-1]+s[n]-s[r-1])
);
}
}
int ans=min(f[1][n][0],f[1][n][1]);
cout<<ans<<endl;
return 0;
}
T4 收集雕像(P6879)
做完关路灯再做这个题就会好很多,思路大致的方向跑不偏捏——
状态设计
经典套路的就是记录
但是如果直接把时间放在 DP 状态中,显然是没有任何前途的。如此巨大的数据范围还没有一个较简单的离散化方法,所以时间维度放在 DP 状态里并不可取
继续观察数据范围,发现其实我们要求的答案是一个和
OF COURSE!
进一步地,这个状态记录的信息自然是那个没有办法离散化的时间维度咯!
所以,总结一下状态定义——记
初始化
初始化也并非易如反掌……
首先,题意给出的描述这个东西是个环,所以考虑倍长数组破环为链。然而,对于原序列的处理不能止步于此。注意到 JOI 君的初始位置不一定恰好在某一个物品上,所以考虑给 JOI 君的初始位置加入一个物品(具体可以看看代码实现)
其次,对于这个新加入的物品,也要相应的给它赋予位置和自爆时间
最后是 DP 状态的初始化,在这种 DP 状态的设计下,我们希望当
状态转移
云落太菜了,没有仔细去想填表法怎么做捏……
考虑
总体来说,转移很好想,但是需要注意一些边界条件(不然就会像云落一样 RE)
答案计算
对于所有合法的时间,找出最大的下标
细节处理
-
加入新物品后破环为链,下标范围是
,数组不要开太小 -
对于破环为链的后半段,他们的位置应当是
,这个也好理解——转一圈嘛 -
新加入的物品的自爆时间赋值为
,表示第一次经过后不会对答案造成任何贡献 -
转移注意边界条件的判断(尤其是刷表法)
-
需要计算的区间长度上界是
,因为新加入的物品是一定会取到的
代码时间
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=205,inf=9e18;
int n,L,X[maxn<<1],T[maxn<<1];
int f[maxn<<1][maxn<<1][maxn][2];
inline void getmin(int &x,int &y){
x=min(x,y);
return;
}
inline void getmax(int &x,int &y){
x=max(x,y);
return;
}
inline void init(){
X[0]=0;
X[n+1]=L;
T[0]=-1;
T[n+1]=-1;
for(int len=1;len<=n+1;len++){
for(int l=0;l+len-1<=2*n+1;l++){
int r=l+len-1;
for(int k=0;k<=len;k++){
f[l][r][k][0]=inf;
f[l][r][k][1]=inf;
}
}
}
f[0][0][0][0]=0;
f[0][0][0][1]=0;
f[n+1][n+1][0][0]=0;
f[n+1][n+1][0][1]=0;
return;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>L;
for(int i=1;i<=n;i++){
cin>>X[i];
X[n+i+1]=X[i]+L;
}
for(int i=1;i<=n;i++){
cin>>T[i];
T[n+i+1]=T[i];
}
init();
for(int len=1;len<=n+1;len++){
for(int l=0;l+len-1<=2*n+1;l++){
int r=l+len-1;
for(int k=0;k<=len;k++){
if(f[l][r][k][0]!=inf){
if(l-1>=0){
int tim=f[l][r][k][0]+X[l]-X[l-1];
getmin(f[l-1][r][k+(tim<=T[l-1])][0],tim);
}
if(r+1<=2*n+1){
int tim=f[l][r][k][0]+X[r+1]-X[l];
getmin(f[l][r+1][k+(tim<=T[r+1])][1],tim);
}
}
if(f[l][r][k][1]!=inf){
if(l-1>=0){
int tim=f[l][r][k][1]+X[r]-X[l-1];
getmin(f[l-1][r][k+(tim<=T[l-1])][0],tim);
}
if(r+1<=2*n+1){
int tim=f[l][r][k][1]+X[r+1]-X[r];
getmin(f[l][r+1][k+(tim<=T[r+1])][1],tim);
}
}
}
}
}
int ans=0,len=n+1;
for(int l=0;l+len-1<=2*n+1;l++){
int r=l+len-1;
for(int k=len;k>=0;k--){
if(f[l][r][k][0]!=inf||f[l][r][k][1]!=inf){
ans=max(ans,k);
break;
}
}
}
cout<<ans<<endl;
return 0;
}
T5 矩阵取数游戏(P1005)
NOIP 的提高组真题捏,还是比较明显的区间 DP 题目
一个性质:行与行间相互独立,互不影响。然后就是行内求最大得分,注意到数据范围很小,考虑区间 DP 撒!
具体地,记
当我们要消去区间
回到转移的方法,对于区间
然后就无了,需要手搓高精或者 __int128
(云落不想敲高精度,只能搓一个手写输入输出的 __int128
力)
点击查看代码
#include<bits/stdc++.h>
#define int __int128
using namespace std;
const int maxn=85;
int n,m,a[maxn][maxn];
int p[maxn],f[maxn][maxn];
inline int read(){
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-'){
f=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+ch-'0';
ch=getchar();
}
return x*f;
}
inline void write(int x){
if(x<0){
putchar('-');
x=-x;
}
if(x>9){
write(x/10);
}
putchar(x%10+'0');
return;
}
inline void init(){
p[0]=1;
for(int i=1;i<maxn;i++){
p[i]=(p[i-1]<<1);
}
return;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
n=read();
m=read();
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
a[i][j]=read();
}
}
init();
int ans=0;
for(int k=1;k<=n;k++){
for(int i=1;i<=m;i++){
for(int j=1;j<=m;j++){
f[i][j]=0;
}
}
for(int len=1;len<=m;len++){
for(int l=1;l+len-1<=m;l++){
int r=l+len-1;
f[l][r]=max(f[l][r],f[l+1][r]+a[k][l]*p[m-len+1]);
f[l][r]=max(f[l][r],f[l][r-1]+a[k][r]*p[m-len+1]);
}
}
// write(f[1][m]);
// puts("");
ans+=f[1][m];
}
write(ans);
puts("");
return 0;
}
T6 聚会(P1352)
树形 DP 的第一道题目,也是一个板中之板。树形 DP 的套路就是由儿子
对于这道题目,我们记录
然后直接转移就好了嘛
以及
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=6e3+5;
int n,r[maxn];
vector<int> G[maxn];
int deg[maxn],rt;
int f[maxn][2];
inline void dfs(int u,int fa){
f[u][0]=0;
f[u][1]=r[u];
for(int v:G[u]){
if(v==fa){
continue;
}
dfs(v,u);
f[u][0]+=max(f[v][0],f[v][1]);
f[u][1]+=f[v][0];
}
return;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>r[i];
}
for(int i=1;i<=n-1;i++){
int u,v;
cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
deg[u]++;
}
for(int i=1;i<=n;i++){
if(deg[i]==0){
rt=i;
break;
}
}
dfs(rt,0);
int ans=max(f[rt][0],f[rt][1]);
cout<<ans<<endl;
return 0;
}
T7 树上最大和(P1122)
难得出两个板题……
提示一个细节就好了,不允许有空树,如果每一朵花的“美丽程度”都是负数的情况要特判一下,答案就是那个最大的负数
(P.S. 题意没有说明根节点是
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=16005,inf=9e18;
int n,a[maxn];
vector<int> G[maxn];
int f[maxn];
inline void dfs(int u,int fa){
f[u]=a[u];
for(int v:G[u]){
if(v==fa){
continue;
}
dfs(v,u);
f[u]+=max(f[v],0ll);
}
return;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n;
int mx=-inf;
for(int i=1;i<=n;i++){
cin>>a[i];
mx=max(mx,a[i]);
}
for(int i=1;i<=n-1;i++){
int u,v;
cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
if(mx<0){
cout<<mx<<endl;
return 0;
}
dfs(1,0);
int ans=-inf;
for(int i=1;i<=n;i++){
ans=max(ans,f[i]);
}
cout<<ans<<endl;
return 0;
}
T8 苹果树(P2015)
树形 DP 现在可是出的越来越花哨了捏。云落好菜,不知道这个东西能不能叫做树上的背包问题
我们记
考虑转移
树上的动态规划问题还是套路式地
额,好叭,一点一点解释——
两个范围——
内部的转移方程——
实现细节
众所周知,这是一个
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=105;
int n,q;
int head[maxn],tot;
struct Edge{
int to,nxt,val;
}e[maxn<<1];
int sz[maxn],f[maxn][maxn];
inline void add(int u,int v,int w){
e[++tot].to=v;
e[tot].val=w;
e[tot].nxt=head[u];
head[u]=tot;
return;
}
inline void dfs(int u,int fa){
sz[u]=1;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to,w=e[i].val;
if(v==fa){
continue;
}
dfs(v,u);
sz[u]+=sz[v];
for(int k1=min(sz[u],q);k1>=1;k1--){
for(int k2=min(sz[v],k1-1);k2>=0;k2--){
f[u][k1]=max(f[u][k1],f[u][k1-k2-1]+f[v][k2]+w);
}
}
}
return;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>q;
for(int i=1;i<=n-1;i++){
int u,v,w;
cin>>u>>v>>w;
add(u,v,w);
add(v,u,w);
}
dfs(1,0);
cout<<f[1][q]<<endl;
return 0;
}
T9 Sequence(P7914)
2021 年的,还挺热乎
记
()*()*()
于是乎,我们考虑增加一维,细化一下“超级括号序列”的种类,避免重复。我们记——
简述一下转移过程,具体就看代码吧……
答案的计算显然是
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=505,mod=1e9+7;
int n,k;
char s[maxn];
int dp[maxn][maxn][5];
signed main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>s[i];
}
for(int i=1;i<=n;i++){
dp[i][i-1][0]=1;
}
for(int len=1;len<=n;len++){
for(int l=1;l+len-1<=n;l++){
int r=l+len-1;
if(len<=k&&dp[l][r-1][0]&&(s[r]=='*'||s[r]=='?')){
dp[l][r][0]=1;
}
if(len>=2){
if((s[l]=='('||s[l]=='?')&&(s[r]==')'||s[r]=='?')){
dp[l][r][1]=(dp[l+1][r-1][0]+dp[l+1][r-1][2]+dp[l+1][r-1][3]+dp[l+1][r-1][4])%mod;
}
for(int k=l;k<=r-1;k++){
dp[l][r][2]=(dp[l][r][2]+dp[l][k][3]*dp[k+1][r][0])%mod;
dp[l][r][3]=(dp[l][r][3]+(dp[l][k][2]+dp[l][k][3])*dp[k+1][r][1])%mod;
dp[l][r][4]=(dp[l][r][4]+dp[l][k][0]*dp[k+1][r][3])%mod;
}
}
dp[l][r][3]=(dp[l][r][3]+dp[l][r][1])%mod;
}
}
cout<<dp[1][n][3]<<endl;
return 0;
}
T10 Coloring(P4170)
看到区间涂色,以及求最小涂色次数,一眼区间 DP。自然地,记
初始化是显然的,对于
枚举断点
因为在上面区间拼接的过程中,我们是默认两个区间是彼此独立的,但是如果
感觉没有蓝题难度
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=55;
char s[maxn];
int n,f[maxn][maxn];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>(s+1);
int n=strlen(s+1);
memset(f,0x3f,sizeof(f));
for(int i=1;i<=n;i++){
f[i][i]=1;
}
for(int len=2;len<=n;len++){
for(int l=1;l+len-1<=n;l++){
int r=l+len-1;
for(int k=l;k<=r-1;k++){
f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]);
}
if(s[l]==s[r]){
f[l][r]--;
}
}
}
cout<<f[1][n]<<endl;
return 0;
}
T11 树上染色(P3177)
做这道题之前建议完成 T8,两者思路是极类似的
众所周知,树形 DP 的状态设计并不是很困难,尤其是这种类似树上背包的问题,状态设计都是具有一定套路性的。记
初始化也是很显然,都赋值为
注意到统计每个点对的贡献是有后效性的,故此,不妨统计边的贡献。对于一条边
代码实现大概长这样——
int val=k2*(k-k2)*w+(sz[v]-k2)*(n-k-sz[v]+k2)*w;
简单解释一下,
所以,转移方程大概也可以写出来了,形如:
额,坨串统计贡献的式子,答案显然是
代码实现上,需要强调的是,
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2e3+5;
int n,k;
int head[maxn],tot;
struct Edge{
int to,nxt,val;
}e[maxn<<1];
int sz[maxn],f[maxn][maxn];
inline void add(int u,int v,int w){
e[++tot].to=v;
e[tot].val=w;
e[tot].nxt=head[u];
head[u]=tot;
return;
}
inline void dfs(int u,int fa){
sz[u]=1;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to,w=e[i].val;
if(v==fa){
continue;
}
dfs(v,u);
sz[u]+=sz[v];
for(int k1=k;k1>=0;k1--){
for(int k2=max(k1-sz[u]+sz[v],0ll);k2<=min(sz[v],k1);k2++){
int val=k2*(k-k2)*w+(sz[v]-k2)*(n-k-sz[v]+k2)*w;
f[u][k1]=max(f[u][k1],f[u][k1-k2]+f[v][k2]+val);
}
}
}
return;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>k;
for(int i=1;i<=n-1;i++){
int u,v,w;
cin>>u>>v>>w;
add(u,v,w);
add(v,u,w);
}
dfs(1,0);
cout<<f[1][k]<<endl;
return 0;
}
后记
也是终于完工了(明明比线段树合并简单,但为什么耗时更长了……)
完结撒花!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!