问题描述
家庭菜园专家 JOI 先生在他的家庭菜园中种植了一种叫 Joy 草的植物。在他的菜园里,有 N 个花盆自东向西摆放,编号分别为 \(1, \ldots, N\)。每个花盆中有一株 Joy 草。
春天到了,JOI 先生注意到 Joy 草如他期望地长出了各种颜色的叶子,但他也发现 Joy 草的生长速度没有他期望的那么快。他查阅了书籍,找到了草的以下特点:
Joy 草有三种品种,分别会长出红色、绿色和黄色的叶子。
如果两株同一颜色的 Joy 草紧密相邻,它们的生长速度就会减慢。
因此,JOI 先生决定重新摆放花盆,使得没有两株相邻的 Joy 草颜色相同。
花盆非常沉重,因此 JOI 先生每次只能交换相邻的两个花盆。形式化的说,JOI 先生每次操作可以选择一个$ i (1 \le i < N)$,然后交换花盆 i 和花盆 i+1。
请编写一个程序,计算最少的交换次数。
解析
考虑将当前在哪个位置作为阶段,那么我们需要知道前一个阶段的状态。显然,我们需要知道交换后1到i-1每种颜色的草的数量,转移时也需要知道第i-1为放的是什么草。设\(f[i][j][k][0/1/2]\)表示当前完成前i个花盆,交换后有j个红花,k个黄花,i-j-k个绿花,第i为放第0/1/2种花时的最小交换次数。不妨设将第x朵颜色为op的花移动到y位置的交换次数为\(cost(op,x,y)\),那么,我们有如下状态转移方程:
\[f[i][j][k][0]=min(f[i-1][j][k][1]+cost(0,i-j-k+1,i),f[i-1][j][k][2]+cost(0,i-j-k+1,i))
\]
\]
其他的同理。现在讨论如何\(O(1)\)求\(cost\)值。记第x朵颜色为op的花在原序列中出现的位置为\(pos[op][x]\),那么将这朵花移动到i位置需要\(pos[op][x]-i\)步。但是,有可能在之前移动了其他颜色的花到区间\([1,i]\)中,所以移动的长度也要大于之前的值。具体见代码。
代码
#include <iostream>
#include <cstdio>
#include <cstring>
#define N 402
using namespace std;
const int inf=1<<30;
int n,i,j,k,x,f[2][N][N][3],pos[3][N],sum[3][N],a[N],cnt[3];
char c[N];
int cal(int op,int g,int r,int y)
{
int m=g+r+y+1;
if(g>cnt[0]||r>cnt[1]||y>cnt[2]) return inf;
if(op==0){
if(g+1>cnt[0]) return inf;
int p=pos[0][g+1];
return p-m+max(0,r-sum[1][p])+max(0,y-sum[2][p]);
//sum[op][p]表示在[1,p]中有多少颜色为op的花。
//r-sum[1][p]即在此之前移动了多少红花到[1,i]中来,那么绿花移动的长度也要加上r-sum[1][p]。
//当然,如果没有多移动,就不需要加了。
//y-sum[2][p]也是同样的道理。
}
else if(op==1){
if(r+1>cnt[1]) return inf;
int p=pos[1][r+1];
return p-m+max(0,g-sum[0][p])+max(0,y-sum[2][p]);
}
else{
if(y+1>cnt[2]) return inf;
int p=pos[2][y+1];
return p-m+max(0,g-sum[0][p])+max(0,r-sum[1][p]);
}
}
int main()
{
cin>>n>>c;
for(i=1;i<=n;i++){
if(c[i-1]=='R') a[i]=1;
else if(c[i-1]=='Y') a[i]=2;
}
for(i=1;i<=n;i++) pos[a[i]][++cnt[a[i]]]=i;
for(i=1;i<=n;i++){
for(j=0;j<3;j++) sum[j][i]=sum[j][i-1];
sum[a[i]][i]++;
}
memset(f,0x3f,sizeof(f));
f[0][0][0][0]=pos[0][1]-1;
f[0][1][0][1]=pos[1][1]-1;
f[0][0][1][2]=pos[2][1]-1;
for(i=2;i<=n;i++){
x^=1;
memset(f[x],0x3f,sizeof(f[x]));
for(j=0;j<=(i+1)/2;j++){
for(k=0;k<=i-j&&k<=(i+1)/2;k++){
if(i-j-k){
f[x][j][k][0]=min(f[x][j][k][0],f[x^1][j][k][1]+cal(0,i-j-k-1,j,k));
f[x][j][k][0]=min(f[x][j][k][0],f[x^1][j][k][2]+cal(0,i-j-k-1,j,k));
}
if(j){
f[x][j][k][1]=min(f[x][j][k][1],f[x^1][j-1][k][0]+cal(1,i-j-k,j-1,k));
f[x][j][k][1]=min(f[x][j][k][1],f[x^1][j-1][k][2]+cal(1,i-j-k,j-1,k));
}
if(k){
f[x][j][k][2]=min(f[x][j][k][2],f[x^1][j][k-1][0]+cal(2,i-j-k,j,k-1));
f[x][j][k][2]=min(f[x][j][k][2],f[x^1][j][k-1][1]+cal(2,i-j-k,j,k-1));
}
}
}
}
int ans=inf;
for(i=0;i<=n;i++){
for(j=0;j<=n;j++){
for(k=0;k<3;k++) ans=min(ans,f[x][i][j][k]);
}
}
if(ans>=1000000000) cout<<"-1"<<endl;
else if(ans<0) cout<<"0"<<endl;
else cout<<ans<<endl;
return 0;
}
反思
- 打暴搜之前要特判是否在开始就是合法的情况,这样就可以输出0了。
- 其实如果明确了阶段的定义,并想到如何表示一个阶段,状态转移应该也不难想到了。不过O(1)计算代价我怕是要想一天......