[lnsyoj2256]消除游戏
题意
给定序列 \(a\),每次可以选择其中长度 \(>m\) 且完全相同的一段并删除,并将序列的剩余部分拼合成一段;或在任意位置插入一个数。求最终将序列 \(a\) 清空的最小操作数
sol
很显然,本题的插入操作是为了能够使一段数能够凑够 \(m\) 个,因此我们可以只考虑删除操作,只在操作上下手脚即可。具体地,我们定义 \(cost_i\) 表示将长度为 \(i\) 的一段删除所需要的最小操作数,显然可得:
\[cost_i=\left\{\begin{matrix}
1(x\ge m) \\
m-x+1(x<m)
\end{matrix}\right.\]
本题与[lnsyoj2239/luoguP5336/THUSC2016]成绩单较为相似,都为可以拼合的区间问题。设计 \(f_{l,r,k}\) 表示将区间 \([l,r]\) 清空,且 \(r\) 之后有 \(k\) 个元素与 \(a_r\) 相同的最小代价。 我们可以分为三种情况(实际上是两种,分为三种是为了减少细节处理)
- 将 \(r\) 与后面 \(k\) 个值一起删除;
- 将 \(r\) 与后面 \(k\) 个值放在一起(\(a_{r-1}=a_r\));
- 删除区间 \([x+1,r-1]\),将 \(x\) 与 \(r\) 及后面 \(k\) 个值放在一起(\(a_x=a_r,l\le x < r-1\))
综上可得转移方程:
\[f_{l,r,k}=\min\{f_{l,r-1,0} + cost_{k+1},f_{l,r-1,k+1}(a_{r-1}=a_r),\min_{x=l}^{r-2}\{f_{l,x,k+1} + f_{x+1,r-1,0}(a_x=a_r)\}\}
\]
由于数据基本随机,因此实际的可用状态并不多,因此可以使用记忆化搜索。
这里有一个 trick,我们使 \(f_{l,r,m}\) 表示将区间 \([l,r]\) 清空,且 \(r\) 之后有 \(\ge k\) 个元素与 \(a_r\) 相同的最小代价,这是因为 \(k\) 这一维的主要目的是为了计算代价,且可以证明,每次将一段相同的区间全部拿完代价最小。这样,我们就将每次初始化所需的时空复杂度缩小到了 \(O(n^2m)\)。
代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 405;
int n, m;
int a[N];
int f[N][N][15];
int T;
int cost(int x){
if (x >= m) return 1;
else return m - x + 1;
}
int dfs(int l, int r, int k){
if (k >= m) k = m;
if (f[l][r][k]) return f[l][r][k];
if (l == r) return cost(k + 1);
int res = dfs(l, r - 1, 0) + cost(k + 1);
if (a[r - 1] == a[r]) res = min(res, dfs(l, r - 1, k + 1));
for (int i = l; i < r - 1; i ++ )
if (a[i] == a[r]) res = min(res, dfs(l, i, k + 1) + dfs(i + 1, r - 1, 0));
return f[l][r][k] = res;
}
int main(){
scanf("%d", &T);
for (int u = 1; u <= T; u ++ ) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
memset(f, 0, sizeof f);
printf("%d\n", dfs(1, n, 0));
}
return 0;
}