P2540 [NOIP2015 提高组] 斗地主 加强版

简要题意

给你一副手牌,求最少的次数出完所有手牌。(按照它给出的规定出)
题目

分析

因为求最小次数直接贪心很明显是错的,但又直接写不出 \(dp\) 的式子,所以我们只能够爆搜所有情况,但这样明显会超时,只有剪枝,我们记录了各个数码的个数,但其实除了顺子以外,其他的出牌并不关心数码的大小,只关心个数。所以我们单独处理顺子,然后只记录 \(js[i]\) 表示有 \(i\) 张牌的数码个数,然后在模拟出牌。这个时候我们发现,只要 \(js\) 一样答案就一样与先后顺序等无关,所以记忆化搜索(dp)记录一下就可以了。但一定要注意一些特殊情况,如双王可以当对子出,不能当对子用,有可能将三个的拆成一个和两个更优,同理四个拆成一个和三个或两个两个。

代码

点击查看代码

#include <bits/stdc++.h>
using namespace std;
int t,n,cnt[20],js[10],xz[5]={0,5,3,2};
int dp[30][30][30][30];
int dfs()
{
	if(dp[js[1]][js[2]][js[3]][js[4]]!=-1)return dp[js[1]][js[2]][js[3]][js[4]];
	int sum=n;
	if(js[2])
	{
		js[2]--;js[1]+=2;
		sum=min(sum,dfs());
		js[2]++;js[1]-=2;
	}//chai2
	if(js[3])
	{
		js[3]--;js[1]+=1,js[2]+=1;
		sum=min(sum,dfs());
		js[3]++;js[1]-=1,js[2]-=1;
	}
	if(js[4])
	{
		js[4]--;js[1]+=1,js[3]+=1;
		sum=min(sum,dfs());
		js[4]++;js[1]-=1,js[3]-=1;	
		js[4]--;js[2]+=2;
		sum=min(sum,dfs());
		js[4]++;js[2]-=2;	
	}
	if(js[1])js[1]--,sum=min(sum,1+dfs()),js[1]++;//danpai
	if(js[2])js[2]--,sum=min(sum,1+dfs()),js[2]++;//duizi
	if(js[3])js[3]--,sum=min(sum,1+dfs()),js[3]++;//san
	if(js[4])js[4]--,sum=min(sum,1+dfs()),js[4]++;//炸弹
	if(js[3]&&js[1])
	{
		js[3]--;js[1]--;
		sum=min(sum,1+dfs());
		js[3]++;js[1]++;
	} //3+1 
	if(js[3]&&js[2])
	{
		js[3]--;js[2]--;
		sum=min(sum,1+dfs());
		js[3]++;js[2]++;
	} //3+2
	if(js[2]>=2&&js[4])
	{
		js[4]--;js[2]-=2;
		sum=min(sum,1+dfs());
		js[4]++;js[2]+=2;	
	}
	if(js[1]>=2&&js[4])
	{
		js[4]--;js[1]-=2;
		sum=min(sum,1+dfs());
		js[4]++;js[1]+=2;	
	}//4+2
	return dp[js[1]][js[2]][js[3]][js[4]]=min(sum,js[1]+js[2]+js[3]+js[4]);
} 
int shunzi(int step)
{
	int ans=n;
	for(int k=1;k<=3;k++)
		for(int i=1;i<=12-xz[k]+1;i++)
		{
			int tot=0;
			for(int j=i;j<=12;j++)
			{
				if(cnt[j]>=k)tot++;
				else break;
			} 
			for(int j=i+xz[k]-1;j<=i+tot-1;j++)
			{ 
			//	cout<<i<<" "<<j<<'\n';
				for(int l=i;l<=j;l++)js[cnt[l]]--,cnt[l]-=k,js[cnt[l]]++;
				ans=min(ans,shunzi(step+1));//可以出多个顺子
				for(int l=i;l<=j;l++)js[cnt[l]]--,cnt[l]+=k,js[cnt[l]]++;
			}
		}
	ans=min(ans,step+dfs());//出完顺子直接跑	
	if(cnt[14]==2)
	{
		js[1]-=2;
		ans=min(step+1+dfs(),ans);//出个王炸再跑。
		js[1]+=2;
	}	
	return ans;
}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>t>>n;
	memset(dp,-1,sizeof(dp));
	dp[0][0][0][0]=0;
	while(t--)
	{
		memset(cnt,0,sizeof(cnt));
		memset(js,0,sizeof(js));
		for(int i=1;i<=n;i++)
		{
			int ai,bi;cin>>ai>>bi;
			if(ai>=1)
			{
				if(ai>=3)cnt[ai-2]++;
				else cnt[ai+11]++;//A,2
			}
			else cnt[14]++;//wang
		}
		for(int i=1;i<=14;i++)js[cnt[i]]++;
		if(cnt[14]&&cnt[14]==2)js[1]+=2,js[2]-=1;//处理双王
		cout<<shunzi(0)<<'\n'; 
	}
	return 0;
} 

反思

1.不要省一些极小的内存,增加代码长度,直接将数组当形参传入即可减少一半的长度,不用复原。

2.思考的不全面,一开始没有想到出完一个顺子后再出顺子,以及王炸当对子用(如三带二带王炸),以及开始写前没有充分思考。