P2470压缩 题解
这道题来自于 2023/11/18 上午 9:00 由 MikeZ 发起的 DP 专项比赛 T1。这居然是一道 $\color{#9D3DCF} \text{省选}$ 题!(MikeZ 不厚道)
题意简述
这道题的题意比较复杂,有坑。
把一个只有小写字母的字符串压缩,压缩方法:
M
标记重复串的开始,R
重复一遍从上一个M
(如果当前位置左边没有M
,则从串的开始算起)到目前的解压结果。所有的解压结果都不包含M
与R
。
因为这里的 R
只与离它左边最近的 M
对上,所以连续的 M
是无意义的(因为 R
之会与最后的一个 M
对上)。
码前分析
这道题很明显,直接模拟是不可取的,因为你无法保证怎样压缩是最优的。
因为压缩是一段一段的操作,可以理解为区间操作,可以联想到区间 DP。
那 DP 想出来了,怎么设状态呢?
首先,区间 DP 一般设:$dp_{i, j}$ 为区间 i 到 j 的答案。考虑到解压的时候是会让子串直接翻倍,所以有这种转移方法:
- 如果子串能被从中间分开,且左右两边都完全相等,就可以压缩成一个。
- 否则枚举一个 $k(i \le k < j)$,把左右两边相加。
但是,有但是哈。因为题目说:R重复从上一个M算起
那么就不能产生嵌套关系,否则就会让一些 R
与 M
失效或者效果改变,在上文也说过。
所以我们怎么办呢?我认为最方便的方法是在原来的状态方程上再添一位,变成 $f_{i, j, k}$,那这个 k 只能是 1 或 0,如果是 0 表示 $i \; j$ 区间内不能有 M
,反之则有 M
。
这里还需要搞明白一个非常重要的事情:因为你放 M
进去,是要增加长度的,那这个长度在什么时候加上呢?这里有两种选择。
-
当前转移完成了这个状态,马上就加上。
但是有需要特殊处理的两个情况:
- 因为 1 的前面默认有个
M
,所以得特判 - 如果当前的子串是可以压缩的(分成两半且两半相等),你要不要压缩呢?如果压缩的话可能会出现嵌套的关系,不可行;如果不压缩的话又有可能丢失了最优解,大亏特亏。
- 因为 1 的前面默认有个
-
当前转移完了不加上,在被使用的时候再加上
这种情况就没啥需要注意的,就是使用的时候不要忘记加上之前没有加上的
M
了。
所以综上所述,我认为用第二种选择更好 (是个人都选第二个) 。
因此我们假设:推导 $f_{i, j}$ 时,i - 1 处已经有了一个 M
。这就是把 M
放在使用时再算上的一个体现。
我们现在来考虑怎么转移状态:
-
首先我们考虑可以直接压缩,值得注意的是,只有 $f_{i, j, 0}$ 才可以直接压缩,也就是 i 到 j 这个区间中不允许存在
M
,不然就会出现嵌套关系。 -
枚举 k 来转移 $f_{i, j, 0}$,这个很容易就得到: $f_{i, j, 0} = \min(f_{i, j, 0}, \; \min(f_{i, k, 0} + j - k))$
为什么这里是
j - k
而不是 $f_{k + 1, j, 0}$ 呢?是因为 i 到 j 本身是不允许有M
出现的,i 到 k 这一段能保证M
就算出现也在 i - 1,而 k + 1 到 j 却不能保证一定没有M
出现。 -
枚举 k 来转移 $f_{i, j, 1}$,$f_{i, j, 1} = \min(f_{i, j, 1}, \min(f_{i, k, 0}, f_{i, k, 1}) + \min(f_{k + 1, j, 0}, f_{k + 1, j, 1}) + 1)$
这个方程本身是不难理解的,唯一要注意的就是这个
+ 1
,这个 1 加的是 k 这个位置上的M
,因为上文说了,我们在使用 $f_{i, j}$ 的时候再加上M
。如果这里不懂,可以自己想想。
最后输出就是 $\min(f_{1, n, 0}, f_{1, n, 1})$咯。
代码如下:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 55;
int n, f[N][N][2];
char c[N];
bool check(int l, int r)
{
if ((r - l + 1) % 2 == 1) return false;
int lens = (r - l + 1) >> 1;
for (int i = 0; i < lens; i ++ )
{
if (c[l + i] != c[l + lens + i]) return false;
}
return true;
}
int main()
{
scanf("%s", c + 1);
n = strlen(c + 1) ;
for (int i = 1; i <= n; i ++ ) f[i][i][0] = 1, f[i][i][1] = 2;
for (int lens = 2; lens <= n; lens ++ )
{
for (int i = 1; i + lens - 1 <= n; i ++ )
{
int j = i + lens - 1; f[i][j][0] = lens, f[i][j][1] = lens + 1;
if (check(i, j)) f[i][j][0] = f[i][(j + i) >> 1][0] + 1;
for (int k = i; k < j; k ++ )
{
f[i][j][0] = min(f[i][j][0], f[i][k][0] + j - k);
f[i][j][1] = min(f[i][j][1], min(f[i][k][1], f[i][k][0]) + min(f[k + 1][j][0], f[k + 1][j][1]) + 1);
}
}
}
printf("%d\n", min(f[1][n][0], f[1][n][1]));
return 0;
}