【RQNOJ 285】USACO2008 Feb 麻烦的聚餐(重庆一中高2018级信息学竞赛测验9) 解题报告

【问题描述】  
  
  为了避免餐厅过分拥挤,FJ要求奶牛们分3批就餐。每天晚饭前,奶牛们都会在餐厅前排队入内,按FJ的设想,所有第3批就餐的奶牛排在队尾,队伍的前端由设定为第1批就餐的奶牛占据,中间的位置就归第2批就餐的奶牛了。由于奶牛们不理解FJ的安排,晚饭前的排队成了一个大麻烦。 


  第i头奶牛有一张标明她用餐批次 D_i 的卡片。虽然所有N头奶牛排成了很整齐的队伍,但谁都看得出来,卡片上的号码是完全杂乱无章的。 


  在若干次混乱的重新排队后,FJ找到了一种简单些的方法:奶牛们不动,他沿着队伍从头到尾走一遍,把那些他认为排错队的奶牛卡片上的编号改掉,最终得到一个他想要的每个组中的奶牛都站在一起的队列,例如111222333或者333222111。哦,你也发现了,FJ不反对一条前后颠倒的队列,那样他可以让所有奶牛向后转,然后按正常顺序进入餐厅。


  你也晓得,FJ是个很懒的人。他想知道,如果他想达到目的,那么他最少得改多少头奶牛卡片上的编号。所有奶牛在FJ改卡片编号的时候,都不会挪位置。 
 
    
 【输入格式】  
  
  第1行: 1个整数:N 
  第2..N+1行: 第i+1行是1个整数,为第i头奶牛的用餐批次D_i 


 
    
 【输出格式】  
   
  第1行: 输出1个整数,为FJ最少要改几头奶牛卡片上的编号,才能让编号变成他设想中的样子 
 
    
 【输入样例】   
   
5
1
3
2
1
1


 
    
 【输出样例】  
   
1
 
    
 【数据范围】  
   
  1<=D_i<= 3
  1<=N<=30,000
 
    
 【来源】  
  
  队列中共有5头奶牛,第1头以及最后2头奶牛被设定为第一批用餐,第2头奶牛的预设是第三批用餐,第3头则为第二批用餐。如果FJ想把当前队列改成一个不下降序列,他至少要改2头奶牛的编号,一种可行的方案是:把队伍中2头编号不是1的奶牛的编号都改成1。不过,如果FJ选择把第1头奶牛的编号改成3,就能把奶牛们的队伍改造成一个合法的不上升序列了。


 做题思路(错解):拿到这道题,在审题时以为队伍中的奶牛一定要分3批,于是推出了不完整的状态转移方程和边界,最后只得了40分。


解题思路(正解):根据题意,思路应该明确,主要算法就是动态规划,但为了方便分析状态转移方程,将之前顺序的队列与前后颠倒的队列分开考虑。

设状态函数f(i,j)表示前i头奶牛中,第i头奶牛卡片编号为j时的最少改动次数,接下来分析状态转移方程,第i头奶牛的编号可以为1、2、3,如果第i头奶牛原来的编号与现在的编号不符,则改动次数就要加1,当第i头奶牛的编号为1时,它前面的奶牛编号只能为1;当第i头奶牛的编号为2时,它前面的奶牛编号可以为1或者2;当第i头奶牛的编号为3时,因为编号为2的部分可以空缺,所以它前面的奶牛编号可以为1或者2或者3。所以,状态转移方程为if(a[i]!=j)  f(i,3)=min{f(i-1,1),f(i-1,2),f(i-1,3)}+1   f(i,2)=min{f(i-1,1),f(i-1,2)}+1   f(i,1)=f(i-1,1)+1,if(a[i]==j)  f(i,3)=min{f(i-1,1),f(i-1,2),f(i-1,3)}    f(i,2)=min{f(i-1,1),f(i-1,2)}    f(i,1)=f(i-1,1)。因为队伍中的奶牛不一定要分3批,所以边界条件为if(a[1]!=1)  f(1,1)=1  else  f(1,1)=0 ; if(a[1]!=2)  f(1,2)=1   else  f(1,2)=0 ; if(a[1]!=3)  f(1,3)=1    else  f(1,3)=0。答案即在f(N,1),f(N,2),f(N,3)中取最小值。该算法的时间复杂度为O(3*n)。

因为队列可以前后颠倒,所以想要计算前后颠倒的序列满足条件时的最少改动次数,只需将输入的数据反向存储即可,动态规划与顺序的队列相同。

值得一提的是,该题的动态规划还有另外的做法,见麻烦的聚餐 解法2 from cqyz_yoyoku


#include
#include
#include
#include
#include
using namespace std;
const int maxn=30005;
int N;
int a[maxn],b[maxn];
/*
f(i,j)表示前i头奶牛中,第i头奶牛卡片编号为j时的最少改动次数
if(a[i]!=j)  f(i,3)=min{f(i-1,1),f(i-1,2),f(i-1,3)}+1
             f(i,2)=min{f(i-1,1),f(i-1,2)}+1
             f(i,1)=f(i-1,1)+1
if(a[i]==j)  f(i,3)=min{f(i-1,1),f(i-1,2),f(i-1,3)}
             f(i,2)=min{f(i-1,1),f(i-1,2)}
             f(i,1)=f(i-1,1)
边界:if(a[1]!=1)  f(1,1)=1 
      else  f(1,1)=0
      if(a[1]!=2)  f(1,2)=1 
      else  f(1,2)=0
      if(a[1]!=3)  f(1,3)=1 
      else  f(1,3)=0
*/
int d1[maxn][5],d2[maxn][5];
void solve()  //动态规划
{
	memset(d1,100,sizeof(d1));
	memset(d2,100,sizeof(d2));
	if(a[1]!=1)  d1[1][1]=1;
	else  d1[1][1]=0;
	if(a[1]!=2)  d1[1][2]=1;
	else  d1[1][2]=0;
	if(a[1]!=3)  d1[1][3]=1;
	else  d1[1][3]=0;
	if(b[1]!=1)  d2[1][1]=1;
	else  d2[1][1]=0;
	if(b[1]!=2)  d2[1][2]=1;
	else  d2[1][2]=0;
	if(b[1]!=3)  d2[1][3]=1;
	else  d2[1][3]=0;
	for(int i=2;i<=N;i++)
	{
		for(int j=1;j<=3;j++)
		{
			if(a[i]!=j)
			{
				if(j==1)  d1[i][1]=d1[i-1][1]+1;
				if(j==2)  d1[i][2]=min(d1[i-1][1],d1[i-1][2])+1;
				if(j==3)  d1[i][3]=min(d1[i-1][1],min(d1[i-1][2],d1[i-1][3]))+1;
			}
			else
			{
				if(j==1)  d1[i][1]=d1[i-1][1];
				if(j==2)  d1[i][2]=min(d1[i-1][1],d1[i-1][2]);
				if(j==3)  d1[i][3]=min(d1[i-1][1],min(d1[i-1][2],d1[i-1][3]));
			}
		}
	}
	for(int i=2;i<=N;i++)
	{
		for(int j=1;j<=3;j++)
		{
			if(b[i]!=j)
			{
				if(j==1)  d2[i][1]=d2[i-1][1]+1;
				if(j==2)  d2[i][2]=min(d2[i-1][1],d2[i-1][2])+1;
				if(j==3)  d2[i][3]=min(d2[i-1][1],min(d2[i-1][2],d2[i-1][3]))+1;
			}
			else
			{
				if(j==1)  d2[i][1]=d2[i-1][1];
				if(j==2)  d2[i][2]=min(d2[i-1][1],d2[i-1][2]);
				if(j==3)  d2[i][3]=min(d2[i-1][1],min(d2[i-1][2],d2[i-1][3]));
			}
		}
	}
	int ans=1000000010;
	for(int i=1;i<=3;i++)
	ans=min(ans,min(d1[N][i],d2[N][i]));
	printf("%d\n",ans);
}
int main()
{
	freopen("dinner.in","r",stdin);
	//freopen("dinner.out","w",stdout);
	scanf("%d",&N);
	for(int i=1;i<=N;i++)
	{
		scanf("%d",&a[i]);
		b[N-i+1]=a[i];  //反向存储输入数据,即将原队列前后颠倒
	}
	solve();
	return 0;
}


考后反思:还是要注意审题,一定要仔细理解题目的意思,以防因对题目的曲解而造成的失误。

你可能感兴趣的:(竞赛测验,动态规划)