【笔记】区间DP

记录一些基础的区间 DP 题。

0x00 AT_dp_n N - Slimes

最板的区间 DP

f[i][j] 表示合并 ij 区间的最小代价,初始化为 inff[i][i]=0 (一个数不需要代价)。

第一维从小到大枚举区间长度 len,第二维枚举左端点 i,同时可以计算出右端点 j=i+len1,第三维枚举段点 k,转移方程为

f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+s[j]s[i1])

其中 s 为前缀和。

AC code

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(false)
#define TIE cin.tie(0),cout.tie(0)
#define mod 998244353
#define int long long
using namespace std;
int a[405],n,m,f[405][405],s[405];
signed main(){
	IOS;TIE;
	cin>>n;
	memset(f,0x3f,sizeof(f));
	for(int i=1;i<=n;i++) cin>>a[i],f[i][i]=0,s[i]=s[i-1]+a[i];
	for(int len=2;len<=n;len++){
		for(int i=1;i+len-1<=n;i++){
			int j=i+len-1;
			for(int k=i;k<j;k++){
				f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+s[j]-s[i-1]);
			}
		}
	}
	cout<<f[1][n]<<endl;
	return 0;
}

双倍经验:P1775 石子合并(弱化版)


0x01 P1880 [NOI1995] 石子合并

来看常规版,也就是环上问题。

首先显然断环成链,把石子复制到两倍长,然后同时维护区间最大最小代价即可。最小要初始化为 inf

注意区间长度最大仍取到 n,但右端点最大能取到 2×n,最后答案就是 f[1,n]f[n1,n×21] 的最大 or 最小值。

AC Code

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(false)
#define TIE cin.tie(0),cout.tie(0)
#define int long long 
#define mod 998244353
using namespace std;
int n,a[205],s[205],fmx[205][205],fmn[205][205];
int ansmx,ansmn=1e9;
signed main(){
	IOS;TIE;
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i],a[n+i]=a[i];
	for(int i=1;i<=n*2;i++) s[i]=s[i-1]+a[i];
	for(int len=2;len<=n;len++){
		for(int i=1;i+len-1<=n*2;i++){
			int j=i+len-1;
			fmn[i][j]=1e9;
			for(int k=i;k<j;k++){
				fmx[i][j]=max(fmx[i][j],fmx[i][k]+fmx[k+1][j]+s[j]-s[i-1]);
				fmn[i][j]=min(fmn[i][j],fmn[i][k]+fmn[k+1][j]+s[j]-s[i-1]);
			}
		}
	}
	for(int i=1;i<n;i++){
		ansmx=max(ansmx,fmx[i][i+n-1]);
		ansmn=min(ansmn,fmn[i][i+n-1]);
	}
	cout<<ansmn<<endl<<ansmx<<endl;
	return 0;
}

0x02 P3146 [USACO16OPEN]248 G

同样是区间合并,记 f[i][j]ij 区间可以合并出的数(一定要刚好合并成一个数),转移为

if(f[i][k]==f[k+1][j] and f[i][k]) f[i][j]=max(f[i][j],f[i][k]+1)

其中判断条件意为两个相邻区间可以合成的数相同,但是不能为 0

不同的是答案不为 f[1][n],因为整个序列最后不一定能合成一个数,每一次合时都要更新一下答案。

AC Code

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(false)
#define TIE cin.tie(0),cout.tie(0)
#define int long long 
#define mod 998244353
using namespace std;
int n,a[255],f[255][255],ans;
signed main(){
	IOS;TIE;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i],ans=max(ans,a[i]);
		f[i][i]=a[i];
	}
	for(int len=2;len<=n;len++){
		for(int i=1;i+len-1<=n;i++){
			int j=i+len-1;
			for(int k=i;k<j;k++){
				if(f[i][k]==f[k+1][j]&&f[i][k]){
					f[i][j]=max(f[i][j],f[i][k]+1);
					ans=max(ans,f[i][j]);
				}
			}
		}
	}
	cout<<ans<<endl;
	return 0;
}

0x03 P3147 [USACO16OPEN]262144 P

与上一题唯一的区别是数据范围,显然不能支持 n3 算法了。

巧妙的思路:设 f[i][j] 表示左端点为 j,能合出 i 这个数字的区间的右端点位置 +1,其转移方程为

f[i][j]=f[i1][f[i1][j]]

初始化为对于每个输入的 af[a][i]=i+1

借助一个样例理解:2 2 3。假设我们已经知道 f[3][1]=3,因为以 1 为左端点,能合出 3 的区间的右端点 +13,也就是区间 [1,2],这时我们要算 f[4][1]f[4][1]=f[41][f[3][1]]=f[3][3],在读入 3f[3][3]=4,所以就成功合并了。

所以第一维枚举 i,看一下数据范围发现最多只能合到 58,第二维枚举可以转移来的区间的左端点 j 就好。

AC Code

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(false)
#define TIE cin.tie(0),cout.tie(0)
#define int long long 
#define mod 998244353
using namespace std;
int n,a,f[65][270005],ans;
signed main(){
	IOS;TIE;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a;
		f[a][i]=i+1;
	}
	for(int i=2;i<=60;i++){
		for(int j=1;j<=n;j++){
			if(!f[i][j]) f[i][j]=f[i-1][f[i-1][j]];
			if(f[i][j]) ans=i;
		}
	}
	cout<<ans<<endl;
	return 0;
}

0x04 P4170 [CQOI2007]涂色

首先得知连续相同的一段颜色可以看做一个点。然后要分类讨论一下:

如果当前区间的两端点颜色相等,则转移只需要分别减掉左右端点取 min,转移方程为

f[i][j]=min(f[i+1][j],f[i][j1])

否则就枚举断点,转移方程为

f[i][j]=min(f[i][j],f[i][k]+f[k+1][j])

AC Code

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(false)
#define TIE cin.tie(0),cout.tie(0)
#define int long long 
#define mod 998244353
using namespace std;
int n,x,a[55],f[55][55],lst=-1;
string s;
signed main(){
	IOS;TIE;
	cin>>s;
	for(int i=0;i<s.size();i++){
		x=s[i]-'A';
		if(x!=lst) a[++n]=x,lst=x;
	}
	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 i=1;i+len-1<=n;i++){
			int j=i+len-1;
			if(a[i]==a[j]) f[i][j]=min(f[i+1][j],f[i][j-1]);
			else{
				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;
}

0x05 P2858 [USACO06FEB]Treats for the Cows G/S

与之前的区间合并不太一样,这次只需把一个点合入一段区间即可,所以不用枚举断点的一维。

但是每个点都有要乘一个权值。考虑倒着做。单点最后取的倍率为 n,所以初值 f[i][i]=a[i]×n

设当前已取区间的长度为 len,则取数的倍率为 nlen+1 。考虑从左边取还是从右边取,转移方程就是

f[i][j]=max(f[i][j1]+a[j]×(nlen+1),f[i+1][j]+a[i]×(nlen+1))

AC Code

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(false)
#define TIE cin.tie(0),cout.tie(0)
#define int long long 
#define mod 998244353
using namespace std;
int n,a[2005],f[2005][2005];
signed main(){
	IOS;TIE;
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i],f[i][i]=n*a[i];
	for(int len=2;len<=n;len++){
		for(int i=1;i+len-1<=n;i++){
			int j=i+len-1;
			f[i][j]=max(f[i][j-1]+a[j]*(n-len+1),f[i+1][j]+a[i]*(n-len+1));
		}
	}
	cout<<f[1][n]<<endl;
	return 0;
}
posted @   Binary_Lee  阅读(41)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
Title
点击右上角即可分享
微信分享提示