[JSOI2007] 祖码 - 洛谷https://www.luogu.com.cn/problem/P2145很经典的一道区间dp,区间dp的部分都挺好想的:
我们用数组f [ i ][ j ]表示消除区间[ i , j ]需要的竹子的个数.那么我们进行区间dp,从小区间合并到大区间,状态转移方程式(这里就是板子)就是
f [ i ][ j ]=min(f [ i ][ j ],f [ i ][ k ]+f [ k+1 ][ j ])
这里是先求出小区间的贡献值,在将小区间转换为大区间,求出大区间的贡献值.这就是常规部分的写法.但是还存在另外的情况.当区间两段的值相等arr[ i ]==arr[ j ],那么考虑我们是否可以把中间的部分消除后,两边是否能直接消除.如果这两个端点的相同颜色珠子数相加是大于3的,那么就可以不用发射弹珠,直接消灭,反之则是还需要射出一颗珠子.
现在就到了难点,我们如何判断这两端的珠子可以直接进行消去呢.如果判断这两个珠子两边是否同色,这样处理的话就超出了原有的需要处理的区间,是不可以去的.那么我们可以尝试把原来的珠子串进行压缩把他们压缩成一个个区间:
for(int i=1;i<=n;++i)
{
cin>>arr[i];
if(i!=1&&arr[i]==arr[i-1])b[tot]++;
else
{
arr[++tot]=arr[i];
b[tot]=1;
}
}
这样处理后,举个例子,题目给的样例就会产生如下变化:
变化前
1 1 2 2 3 3 2 1 1
变化后
1 2 3 2 1
然后b数组就记录了每个位置上该种类珠子的连续个数.这样,我们按照上面的想法,判断是否需要新打出一颗珠子把剩余的消灭的情况,只需要判断b[ i ]+b[ j ]是否大于2了,如果大于2就表明我们把中间的区间的珠子全部消去后,两端的珠子会直接消除,不需要打出新的珠子.情况如下:
if(arr[l]==arr[r])
if(b[l]+b[r]>2)
f[l][r]=min(f[l][r],f[l+1][r-1]);
else
f[l][r]=min(f[l][r],f[l+1][r-1]+1);
讲一下如果不进行上文的预处理,那么连续很多个相同珠子的情况就不好用该方法处理.所以我们直接预处理之后跑区间dp即可.
AC代码
#include<map>
#include<cmath>
#include<set>
#include<queue>
#include<string>
#include<vector>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<unordered_set>
#include<unordered_map>
#define int long long
using namespace std;
const int N =5e5+10,mod=998244353;
int f[505][505],arr[505],b[505],tot;
void solve()
{
memset(f,0x3f,sizeof f);
int n;
scanf("%lld",&n);
for(int i=1;i<=n;++i)
{
cin>>arr[i];
if(i!=1&&arr[i]==arr[i-1])b[tot]++;
else
{
arr[++tot]=arr[i];
b[tot]=1;
}
}
n=tot;
for(int i=1;i<=n;++i)
if(b[i]>1)
f[i][i]=1;
else
f[i][i]=2;
for(int len=2;len<=n;len++)
{
for(int l=1;l+len-1<=n;l++)
{
int r=l+len-1;
if(arr[l]==arr[r])
if(b[l]+b[r]>2)
f[l][r]=min(f[l][r],f[l+1][r-1]);
else
f[l][r]=min(f[l][r],f[l+1][r-1]+1);
for(int mid=l;mid<r;mid++)
f[l][r]=min(f[l][r],f[l][mid]+f[mid+1][r]);
}
}
if(f[1][n]==3)
printf("2\n");
else
printf("%d\n",f[1][n]);
return ;
}
signed main()
{
solve();
return 0;
}