题意描述
题意:
给定一个数列,每次可以消除一段相等的数,得分为这一段长度的平方,求最大得分。
看上去就很区间 DP,其实可以写的很简洁的。
算法分析
首先将数据根据颜色不同分为 $n$ 段,$a(i)$ 记录其颜色,$b(i)$ 记录其长度。
一般普通区间 DP 的状态为 $f(l,r)$ 表示区间 $[l,r]$ 消除的最大得分,可惜这并不能满足我们的要求。
原因可以看题目中给出的图,我们发现可能可以先消除中间的某些小段,然后一起消除大段可以得到更优解。
如果朴素枚举哪些段先消去,单次转移的复杂度就达到了 $O(2^n)$,不太行。
为了使转移复杂度更优,我们考虑让状态表示更加复杂。
令 $f(l,r,len)$ 表示区间 $[l,r]$,其中 $r$ 后面还有 $len$ 个与其颜色相同的木块,消除的最大得分。
有了新的一维状态,我们可以轻松地写出状态转移方程:
$$f(l,r,len) = \max_{l\leq k<r,a(k)=a(r)}\{f(l,k,len+b(r))+f(k+1,r-1,0)\}$$
当然,我们也可以直接消去最右边的块,将剩余部分作为子问题:
$$f(l,r,len)=\max\{f(l,r-1,0) + (b(r)+len)^2\}$$
为了保证代码的简洁性,可以考虑记忆化搜索的处理方式,显然有:
-
初始状态:$f(i,i,len)=(b(i)+len)^2$。
-
目标状态:$f(1,n,0)$。
代码实现
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 210;
int T, n;
int a[N], b[N];
int f[N][N][N];
int read(){
int x=0,f=1;char c=getchar();
while(c<'0' || c>'9') f=(c=='-')?-1:1,c=getchar();
while(c>='0' && c<='9') x=x*10+c-48,c=getchar();
return x*f;
}
int dfs(int l, int r, int len){
if(l == r)
return f[l][r][len] = (b[l] + len) * (b[l] + len);
if(f[l][r][len] != -1)
return f[l][r][len];
int mx = dfs(l, r - 1, 0) + (b[r] + len) * (b[r] + len);
for(int k = l; k < r; k ++)
if(a[k] == a[r])
mx = max(mx, dfs(l, k, len + b[r]) + dfs(k + 1, r - 1, 0));
return f[l][r][len] = mx;
}
int main(){
T = read();
int C = 0;
while(T --){
memset(f, -1, sizeof(f));
int m = read(); n = 0;
int last = 0;
for(int i=1; i<=m; i++){
int x = read();
if(x == last) b[n] ++;
else a[++n] = x, b[n] = 1, last = x;
}
printf("Case %d: %d\n", ++C, dfs(1, n, 0));
}
return 0;
}
简单分析一下时间复杂度:
所有状态空间为 $O(n^3)$,单次转移复杂度 $O(n)$,总时间复杂度 $O(n^4)$。
但是每次转移时,需要保证 $a(k)=a(r)$,所以绝对跑不满这个上界,实测可以通过。
完结撒花。
你这个复杂度高了,我优化了下你的代码直接速度加倍你的是1800ms左右我直接搞成了800ms,优化的方法就是从右到左找k这样你找到了第一个k满足比原始值也就是mx = dfs(l, r - 1, 0) + (b[r] + len) * (b[r] + len);这个值大的时候就可以退出循环了,因为你f[l][k][len]在以前就把现在这个最优值考虑进去了,你没必要再去找接下来的k了,因为很明显的是中间和两边合并的个数越多值越大,如果你不这样做,中间的还不是只能自己和自己合并不能和其他东西合并,所以就只能这样了。