JOI2019 有趣的家庭菜园3

问题描述

家庭菜园专家 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)计算代价我怕是要想一天......
原文地址:https://www.cnblogs.com/LSlzf/p/11720964.html