[学习笔记] 区间DP
P2858 [USACO06FEB]Treats for the Cows G/S#
这道题加深了我对区间DP阶段划分的理解(原来 是可以更新的!)
我们可以先想状态设 表示 到 天的最多能得到的钱,得到:
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+7;
bool ed;
int v[N],ans=-1,n,f[2020][2020];
bool st,flag;
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
int dfs(int cnt,int l,int r){
if(r<l) return 0;
if(f[l][r]) return f[l][r];
f[l][r]=max(dfs(cnt+1,l+1,r)+cnt*v[l],dfs(cnt+1,l,r-1)+cnt*v[r]);
return f[l][r];
}
signed main(){
n=read();
for(int i=1;i<=n;i++) v[i]=read();
dfs(1,1,n);
cout<<f[1][n]<<endl;
return 0;
}
DP:
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+7;
bool ed;
int v[N],ans=-1,n,f[2020][2020];
bool st,flag;
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
int dfs(int cnt,int l,int r){
if(r<l) return 0;
if(f[l][r]) return f[l][r];
f[l][r]=max(dfs(cnt+1,l+1,r)+cnt*v[l],dfs(cnt+1,l,r-1)+cnt*v[r]);
return f[l][r];
}
signed main(){
n=read();
for(int i=1;i<=n;i++)
v[i]=read();
for(int i=1;i<=n;i++) f[i][i]=v[i]*n;
for(int len=2;len<=n;len++)//因为是以len为阶段所以可以转化
for(int l=1;l<=n-len+1;l++){
int r=l+len-1;
f[l][r]=max(f[l+1][r]+v[l]*(n-len+1),f[l][r-1]+v[r]*(n-len+1));
}
cout<<f[1][n]<<endl;
return 0;
}
P1220 关路灯#
首先看数据范围:, ,再看要求什么:第 盏到第 盏路灯的位置和功率
感觉是区间DP,然后就想状态 表示关闭 ~ 这段区间所需要的最小花费
“因为在关的过程中适当地调头有可能会更省一些”从这句话可以知道,每一步有两种决策
- 折返
- 向前
所以我们可以设 第三维表示老张关闭这段区间的灯后,在 , 哪一端,易得关闭一段区间的灯后在端点处最优
然后就想怎么转移:
由上一题可以知道 是可以更新 的,所以得出:
要注意,我们在前往关闭第 盏灯时,这灯是还会有贡献的,数组维护功率前缀和
这是可能会有疑惑:
为什么就由 转移过来不能由 转移来吗?(这句话是我自己的疑惑,记录一下.....
不能,因为每一步只有两种决策
代码:
#include <bits/stdc++.h>
using namespace std;
const int N=60;
bool ed;
int n,c,a[N],sum[N],f[N][N][3],b[N];
bool st,flag;
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
signed main(){
n=read();c=read();
for(int i=1;i<=n;i++) a[i]=read(),b[i]=read();
for(int i=1;i<=n;i++){
sum[i]=sum[i-1]+b[i];
}
memset(f,0x3f,sizeof(f));
f[c][c][1]=f[c][c][0]=0;
for(int len=2;len<=n;len++)
for(int i=1;i<=n-len+1;i++){
int j=i+len-1;
f[i][j][0]=min(f[i+1][j][0]+(a[i+1]-a[i])*(sum[n]-sum[j]+sum[i]),f[i+1][j][1]+(a[j]-a[i])*(sum[n]-sum[j]+sum[i]));
f[i][j][1]=min(f[i][j-1][1]+(a[j]-a[j-1])*(sum[n]-sum[j-1]+sum[i-1]),f[i][j-1][0]+(a[j]-a[i])*(sum[n]-sum[j-1]+sum[i-1]));
}
cout<<min(f[1][n][0],f[1][n][1])<<endl;
return 0;
}
P4302 [SCOI2003]字符串折叠#
题目简述:
给你一个字符串,求它折叠后的最短长度
折叠:3(A) = AAA, 2(B) = BB,3(A)C2(B) = AAACBB
解题思路
字符串S,长度保证不超过100,又是字符串的题
想到区间DP
设状态 表示 ~的最短长度(好像也只能这么设了
初始化就是 ,然后思考如何进行状态转移
一开始想,可以不可以由 ,发现是不行的,然后就换一个思路,可以设置断点 ,就得到一个显而易见的转移方程:
直接把两个部分拼起来,但是我们还有一个条件折叠没有用上
通过这个断点 我们可以逐一判断,计算循环节 通过 函数判断是否可以折叠
得出:
数组存一个数的位数, 是值括号的长度
为什么一个是 一个是 ?因为折叠是可以重复折叠的但是判断折叠成什么要看原来的数
记得每一次都要 因为折叠后可能更劣
代码
#include <bits/stdc++.h>
using namespace std;
const int N=110;
bool ed;
string s;
int n,m[N],dp[N][N];
bool st,flag;
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
bool check(int l,int r,int len){//len表示循环节的长度
for(int i=l;i<=r;i++){
if(s[i]!=s[(i-l)%len+l]) return 0;//把l当作1
}
return 1;
}
signed main(){
cin>>s;
n=s.size();
s=' '+s;
for(int i=1;i<=9;i++) m[i]=1;
for(int i=10;i<=99;i++) m[i]=2;
m[100]=3;
memset(dp,0x3f,sizeof(dp));
for(int i=1;i<=n;i++) dp[i][i]=1;
for(int l=2;l<=n;l++)
for(int i=1,j=i+l-1;j<=n;i++,j++){
for(int k=i;k<j;k++){
int len=k-i+1;
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]);
if(l%len!=0) continue;
if(check(i,j,len)) dp[i][j]=min(dp[i][j],dp[i][k]+2+m[l/len]);
}
}
cout<<dp[1][n]<<endl;
return 0;
}
P3205 [HNOI2010]合唱队#
题目简述
- 第一个人直接插入空的当前队形中。
- 对从第二个人开始的每个人,如果他比前面那个人高,那么将他插入当前队形的最右边。如果他比前面那个人矮,那么将他插入当前队形的最左边
给出一个队形,按照给定的排序方法,问有多少种初始排序方案可以得到理想队形
解题思路
我们要求的是构成一个特定的区间有多少种方案,很显然,这是区间DP
那如何设计状态呢?同样的,先设 表示一段区间的方案,接着我们看进队的方式,也就是决策,
可以发现,一个人进队后要么在左边,要么在右边,所以联想到关路灯这道题,我们可以设
表示进队后在左边还是右边,然后就想如何转移,一个人进队后在左边说明,他一定比他前面的人矮,
而他前面可以是 ,将他们进行比较即可,得出:
另一边同理(好像统计方案的DP大部分是相加的
代码
#include <bits/stdc++.h>
using namespace std;
const int mod=19650827;
int f[2000][2000][4],n,a[2005];
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
int main(){
n=read();
for(int i=1;i<=n;i++) a[i]=read();
for(int i=1;i<=n;i++) f[i][i][0]=1;
for(int len=2;len<=n;len++)
for(int i=1,j=i+len-1;j<=n;i++,j++){
if(a[i]<a[i+1]) f[i][j][0]+=f[i+1][j][0];
if(a[i]<a[j]) f[i][j][0]+=f[i+1][j][1];
if(a[j]>a[j-1]) f[i][j][1]+=f[i][j-1][1];
if(a[j]>a[i]) f[i][j][1]+=f[i][j-1][0];
f[i][j][0]%=mod;
f[i][j][1]%=mod;
}
cout<<(f[1][n][0]+f[1][n][1])%mod<<endl;
return 0;
}
CF607B Zuma#
题意简述
给定一个串,目标是消灭所有的数字最少的次数,消灭一个数字算一次,如果有回文串,消灭一整串算一次
解题思路
一段一段区间消灭区间DP无疑了
和前面的题一样 表示消灭一段区间的最小次数,根据“字符串折叠”这道题的经验,可以直接设出
那如何运用回文串这个条件?判断一下头和尾即可,头尾相同则可以继承,设出:
不过这题还有一个恶心之处——初始化,看到上面的 我们就不能从 开始枚举了
#include <bits/stdc++.h>
using namespace std;
int f[600][600],a[600],n;
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
int main(){
n=read();
for(int i=1;i<=n;i++) a[i]=read();
memset(f,0x3f,sizeof(f));
for(int i=1;i<=n;i++) f[i][i]=1;
for(int i=1;i<n;i++) f[i][i+1]=1+(a[i]!=a[i+1]);
for(int len=3;len<=n;len++)
for(int i=1,j=i+len-1;j<=n;i++,j++){
if(a[i]==a[j]) f[i][j]=f[i+1][j-1];
for(int k=i;k<j;k++)
f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]);
}
cout<<f[1][n]<<endl;
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现