区间DP问题
一般来说,区间DP问题都能被转化为多个子区间的合并,在过程中可以枚举合并点将问题分为左右两个部分,从子区间的最优解转移到区间的最优状态上
线性区间合并
https://www.luogu.com.cn/problem/P1775
int m[310];
int dp[310][310];
int pre[310];
void solve(){
int n;
cin>>n;
for (int i=1;i<=n;i++) cin>>m[i];
for (int i=1;i<=n;i++) pre[i]=pre[i-1]+m[i];
memset(dp,0x3f,sizeof dp);
for (int i=1;i<=n;i++) dp[i][i]=0;
for (int i=2;i<=n;i++){
for (int j=1;j+i-1<=n;j++){
for (int k=j;k<j+i-1;k++){
dp[j][i+j-1]=min(dp[j][i+j-1],dp[j][k]+dp[k+1][i+j-1]+pre[i+j-1]-pre[j-1]);
}
}
}
cout<<dp[1][n];
}
定义 $dp_{i,j}$ 表示将区间 $[i,j]$ 合并的最小花费
因为在转移到父区间时需要用到子区间的状态,所以我们首先需要从子区间的问题开始解决
for (int i=2;i<=n;i++)
即第一层循环从小到大枚举区间的长度,这样在区间长度变长后才能进行转移
初始化时,已经将区间为 $1$ 的状态进行设置了,这就是状态开始转移的基石,从已知考虑未知
for (int j=1;j+i-1<=n;j++)
第二层循环枚举区间的左端点,右端点可以根据区间长度进行计算,对每一层区间长度,我们都需要遍历所有的子区间,才能得到当前区间长度下的所有状态
for (int k=j;k<j+i-1;k++)
第三层循环枚举区间合并的位置,即将当前的区间 $[j,j+i-1]$ 分解为左右区间 $[j,k],[k+1,j+i-1]$
更新父区间的最优状态,这道题中,状态的转移方程为:
$$ dp_{i,j}={\rm{min}_{k=j}^{j+i-2}}(dp_{j,k}+dp_{k+1,j+i-1}+\sum_{x=j}^{j+i-1} m_x) $$
合并两个区间,需要加上这两堆石子的总数量相同的花费
环形区间合并
https://www.luogu.com.cn/problem/P1880
int a[210];
int pre[210];
int dp[210][210];
int dpp[210][210];
void solve(){
int n;
cin>>n;
for (int i=1;i<=n;i++) cin>>a[i];
for (int i=1;i<=n;i++) a[i+n]=a[i];
for (int i=1;i<=2*n;i++) pre[i]=pre[i-1]+a[i];
memset(dp,0x3f,sizeof dp);
for (int i=1;i<=2*n;i++) dp[i][i]=0;
for (int i=2;i<=n;i++){
for (int j=1;j+i-1<=2*n;j++){
for (int k=j;k<j+i-1;k++){
dp[j][j+i-1]=min(dp[j][j+i-1],dp[j][k]+dp[k+1][i+j-1]+pre[i+j-1]-pre[j-1]);
dpp[j][j+i-1]=max(dpp[j][j+i-1],dpp[j][k]+dpp[k+1][i+j-1]+pre[i+j-1]-pre[j-1]);
}
}
}
int mx=0,mn=1e9;
for (int i=1;i<=n;i++){
mn=min(mn,dp[i][i+n-1]);
mx=max(mx,dpp[i][i+n-1]);
}
cout<<mn<<endl<<mx;
}
对于环状的区间,最暴力的办法就是把两个元素之间断开,然后形成一条链,这样的话时间复杂度会多一层 $O(n)$ ,绝大多数情况下会超时
可以类比字符串最小表示法的破环成链的办法:即将原区间延长两倍,第 $i$ 个元素和 第$i+n$ 个元素相同,再用线性区间上的动态规划求解可以降低复杂度
for (int i=2;i<=n;i++)
即使我们的区间被延长了两倍,但是实际上我们仍然是计算区间长度为 $n$ 的最优解(将共计 $n$ 堆石子合并)
for (int j=1;j+i-1<=2*n;j++)
第二层循环则是枚举延长两倍区间上的所有左端点,余下的操作和线性区间的操作相差不大
形象化的,对于一般的区间DP,状态转移都可以被表示为:
$$ dp_{i,j}={\rm{max}_{k=i}^{j-1}}(dp_{i,k}+dp_{k+1,j}+cost_{k}) $$
其中 $cost_k$ 表示以 $k$ 为合并点需要的费用
特殊区间合并,序列对齐问题
通过状态转移覆盖所有可能对齐的方式,与传统的区间DP基于区间分割的方式不同
https://www.luogu.com.cn/problem/P1140
string a,b;
int dp[110][110];
unordered_map<char,int> mp;
int d[6][6]={
0,0,0,0,0,0,
0,5,-1,-2,-1,-3,
0,-1,5,-3,-2,-4,
0,-2,-3,5,-2,-2,
0,-1,-2,-2,5,-1,
0,-3,-4,-2,-1,0,
};
void solve(){
int n,m;
cin>>n;
cin>>a;a=' '+a;
cin>>m;
cin>>b;b=' '+b;
memset(dp,-0x3f,sizeof dp);
dp[0][0]=0;
mp['A']=1,mp['C']=2,mp['G']=3,mp['T']=4,mp['-']=5;
for (int i=1;i<=n;i++) dp[i][0]=dp[i-1][0]+d[mp[a[i]]][mp['-']];
for (int i=1;i<=m;i++) dp[0][i]=dp[0][i-1]+d[mp['-']][mp[b[i]]];
for (int i=1;i<=n;i++){
for (int j=1;j<=m;j++){
dp[i][j]=max(dp[i][j],dp[i-1][j-1]+d[mp[a[i]]][mp[b[j]]]);
dp[i][j]=max(dp[i][j],dp[i][j-1]+d[mp['-']][mp[b[j]]]);
dp[i][j]=max(dp[i][j],dp[i-1][j]+d[mp[a[i]]][mp['-']]);
}
}
cout<<dp[n][m];
}
这道题目中存在两个不同的区间,这个时候就不能直接套路做题了
定义 $dp_{i,j}$ 表示第一个序列前 $i$ 个元素与第二个序列前 $j$ 个元素的最大匹配值
这是基于区间扩充的思想定义下来的,即考虑两个区间从 $0$ 个元素递推到 $n$ 个元素,核心在于两个序列的逐步对齐
初始化时,当 $dp_{i,j}$ 中任意的 $i,j=0$ 时,只能通过空碱基进行匹配,那么需要前缀和计算出 $dp_{0,j},dp_{i,0}$
然后考虑状态的转移 $i,j$ 对应的碱基存在三类匹配方式:
- $i,j$ 直接匹配,$dp_{i,j}=dp_{i-1,j-1}+d_{i,j}$
- 给 $i$ 匹配空碱基,$dp_{i,j}=dp_{i-1,j}+d_{i,-}$
- 给 $j$ 匹配空碱基,$dp_{i,j}=dp_{i,j-1}+d_{-,j}$
对这三种情况取最优解,注意上述代码中需要初始化 $dp_{i,j}$ 为极小值
评论区(暂无评论)