[ABC325G] offence 区间dp 题解
题意
给你一个字符串 \(S\) 和一个自然数 \(K\)。请找出对字符串 \(S\) 执行下列操作 \(0\) 次或 \(0\) 次以上所得到的字符串的最小长度:
- 在字符串中选择一个连续出现的
of; - 选择一个介于 \(0\) 和 \(K\) 之间的整数 \(i\);
- 从字符串中删除选中的
of和其后面的 \(i\) 个字符。
思路
看到这种区间合并的最优解问题我们就想到区间 dp。
解法一
规定 \(dp(l,r)\) 为区间 \([l,r]\) 经过若干次操作后可以得到的最小长度,其中 \(0<l\leq r\leq |S|\)。
对于区间 \([l,r]\),规定其最小长度最劣的转移方程为 \(dp(l,r)=1+dp(l+1,r)\),(这么做是为了和接下来更新答案的方式对应)紧接着继续考虑删除操作。
规定 \([l,r]\) 之间的某个字符下标为 \(x(l<x<r)\),当且仅当:
- \(S[l]\) 为字符
o; - \(S[x]\) 为字符
f; - \(dp(l+1,x-1) = 0\),即 \([l+1,x-1]\) 可以完全被删除。
这样,\([l,x]\) 就可以被完全删除。在这种情况下,转移方程就是:
\[\min \{ 1+dp(l+1,r),\max[dp(x + 1,r) - p,0]\}
\]
如何理解 \(\max[dp(x + 1,r) - p,0]\)?
\([l,x]\) 已经被清空了,剩下 \([l+1,r]\) 的最短长度就是 \(dp(l+1,r)\),而题目规定可以在 of 后再删除 \(K\) 个字符,那么能删则删,删得越多越好,一直删到 \(0\) 为止。
解法二
还有另外一种设计状态转移的方法,第三重循环枚举中间分界的时候,里面套有三次循环:
- 第一次循环:先规定 \(dp(l,r)\) 最劣的答案为当前选择的区间长度 \(r-l+1\),然后枚举中间分解点 \(x\),更新一波答案:\(dp(l,r)=\min[dp(l,r),dp(l,x) + dp(x + 1,r)]\)。
- 第二次循环:如果 \(s[l]=o\),那么再枚举一波中间分界点 \(x\),找到 \(s[x] = f\)。然后 \(dp(l,r) = \min\{dp(l,r),\max[dp(x + 1,r) - p,0]\}\)。
- 第三次循环:如果 \(s[r]=f\),那么再枚举一波中间分界点 \(x\),找到 \(s[x] = o\)。然后 \(dp(l,r) = \min[dp(l,r),dp(l,x-1)]\)。
不难证明这样转移不重不漏。尽管这样写常数大,但是是区间 dp 更一般的思路,所以推荐大家也看一看。
我只给出解法一的代码:
代码
算法的时间复杂度为 \(O(|S|^3)\)。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1003;
string s;
int n,p,dp[maxn][maxn];
int main(){
cin >> s >> p;
n = s.size(); s = " " + s;
for(int i = 1;i <= n;++i) dp[i][i] = 1;
for(int len = 2;len <= n;++len){
for(int i = 1;i <= n + len - 1;++i){
int j = i + len - 1;
dp[i][j] = 1 + dp[i + 1][j];
if(s[i] != 'o') continue;
for(int k = i + 1;k <= j;++k)
if(s[k] == 'f' && dp[i + 1][k - 1] == 0)
dp[i][j] = min(dp[i][j],
max(dp[k + 1][j] - p,0));
}
}
cout << dp[1][n] << endl;
return 0;
}
这题的 \(|S|\) 真的小于 \(300\) 吗?我开到了 \(1000\) 才没有 RE。。。

浙公网安备 33010602011771号