区间dp:无非是枚举长度,然后处理合并信息;
树形dp:处理出子树信息,用子树信息维护当前节点信息
区间dp的难点在于处理合并信息;O()
树形dp难点在于 子树信息到当前节点信息的状态转移;O()
区间dp题目清单:
石子合并 - 洛谷 P1880 - Virtual Judge
石子合并是最简单的区间dp,合并信息的处理简单:为选取某点,作为区间分割点,维护两边信息
从这一点出发,我们可以迁移到树上,对于dfs序的中序遍历来说,枚举顺序为左子树,节点,右子树,那么区间合并也可以作用到上面,来维护合并信息,题目见找不到页面 - AcWing加分二叉树
下面看一些合并信息复杂一些的题目:
Rectangle Painting 1 - CodeForces 1198D - Virtual Judge
这道题是维护矩阵信息,我们枚举分割线,将左右分割块的信息相加即可得到区间块信息,这里记忆化搜索加速
#include <bits/stdc++.h>
using u32 = unsigned;
using i64 = long long;
using u64 = unsigned long long;
using namespace std;
const int N=51,mod=1e9+7;
int n;
char g[N][N];
int f[N][N][N][N];
int dfs(int x1, int y1, int x2, int y2){
if(f[x1][y1][x2][y2]!=-1){
return f[x1][y1][x2][y2];
}
int v=max(y2-y1+1,x2-x1+1);
//立着
for(int i=x1; i<x2; i++){
v=min(v,dfs(x1,y1,i,y2)+dfs(i+1,y1,x2,y2));
}
//横着
for(int j=y1; j<y2; j++){
v=min(v,dfs(x1,y1,x2,j)+dfs(x1,j+1,x2,y2));
}
f[x1][y1][x2][y2]=v;
return v;
}
void solve() {
cin>>n;
memset(f,-1,sizeof f);
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
cin>>g[i][j];
f[i][j][i][j]=(g[i][j]=='#');
}
}
cout<<dfs(1,1,n,n)<<endl;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
solve();
return 0;
}
https://vjudge.net/problem/CodeForces-607B/origin
信息为:最小删除次数,我们维护[l,r]的最小次数,枚举分割点l<=i<r,但是区间[l,r]的最优解不一定为[l,i]+[i+1,r],因为可能l和r的字符一致,导致最优解可能为[l+1,r-1](这个区间枚举长度时已经处理过),但是枚举i时没有处理这个信息,于是我们需要特判一下,维护信息。
#include <bits/stdc++.h>
using u32 = unsigned;
using i64 = long long;
using u64 = unsigned long long;
using namespace std;
void solve() {
int n;
cin>>n;
vector<int>a(n+1);
for(int i=1; i<=n; i++)cin>>a[i];
vector<vector<int>>f(n+1,vector<int>(n+1,(int)1e9+10));
for(int len=1; len<=n; len++){
for(int i=1; i+len-1<=n; i++){
int j=i+len-1;
if(i==j){
f[i][j]=1;
}else{
if(a[i]==a[j]){
if(i+1==j){
f[i][j]=1;
}
else f[i][j]=min(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;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
solve();
return 0;
}
https://vjudge.net/problem/CodeForces-1178F1/origin
可以知道对于一个区间来说,设区间最小值的位置为p,对于[l,r]这一次处理的位置一定为p,我们可以设f[l,r]代表这个区间全部为同一种颜色,那么对于p,我们可以给l<=i<=p,p<=j<=r;[i,j]这个区间涂抹上p的颜色,这时候注意到[l,i-1]和[j+1,r]的颜色是不变的,根据前面的假设可以知道,这两个区间的颜色都是一样的,于是对于[i,j]区间来说,答案为
f[l,i-1]*f[i,j]*f[j+1,r](l<=i<=p,p<=j<=r);
注意到乘法表示方案数:于是当i-1<=l时,f[l,i-1]=1;
根据乘法原理我们可以拆开f[i,j]=f[i,p]*f[p+1,j];
于是合并信息处理结束,注意使用记忆化搜索
#include <bits/stdc++.h>
using u32 = unsigned;
using i64 = long long;
using u64 = unsigned long long;
using namespace std;
const int N=510,mod=998244353;
int n,m;
int a[N];
int f[N][N];
typedef long long LL;
LL dfs(LL l, LL r){
if(f[l][r]){
return f[l][r];
}
if(l>=r)return f[l][r]=1;
int p=l;
for(int i=l+1; i<=r;i++ ){
if(a[i]<a[p])p=i;
}
LL ans1=0,ans2=0;
for(int i=l; i<=p; i++)ans1=(ans1+(dfs(l,i-1)*dfs(i,p-1)%mod))%mod;
for(int i=p; i<=r; i++)ans2=(ans2+(dfs(p+1,i)*dfs(i+1,r)%mod))%mod;
return f[l][r]=ans1*ans2%mod;
}
void solve() {
cin>>n>>m;
memset(f,0,sizeof f);
for(int i=1; i<=n; i++){
cin>>a[i];
}
dfs(1,m);
cout<<f[1][m]<<endl;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
solve();
return 0;
}
利用合并特点优化的一道题:
需要知道合并[l,r]区间结束只能得到一个值,不会存在最大和最小值之分,假设合并完区间后,存在最大值和最小值,那么minnum<=maxnum-1;设区间长度为n,可以知道合并完区间,增量为n-1;
假设存在某种合并方式使得n-1的增量使得合并完区间的值为minnum,那么当最大值为maxnum时,增量最少为2*(n-1),结论和增量守恒矛盾。于是合并完某个区间只能得到一个值。
代码:
#include <bits/stdc++.h>
using u32 = unsigned;
using i64 = long long;
using u64 = unsigned long long;
using namespace std;
const int N=3*1e5+10,mod=998244353;
int n,m;
int a[N];
int f[61][N];
void solve() {
cin>>n;
int res=0;
for(int i=1; i<=n; i++){
cin>>a[i];
f[a[i]][i]=i+1;
res=max(res,a[i]);
}
for(int len=2; len<=58; len++){
for(int j=1; j<=n; j++){
f[len][j]=max(f[len-1][f[len-1][j]],f[len][j]);
if(f[len][j])res=len;
}
}
cout<<res<<endl;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
solve();
return 0;
}
树形dp题目清单:
潜入行动 - 洛谷 P4516 - Virtual Judge
状态转移方程较多,别写错即可
树形dp常用于求树的长度,观察所有节点或者边的最小代价,最大代价;方案数。
这里题目链接被加密发不出;