【暴力-搜索-贪心-剪枝】NOIP2013-Day1——斗地主

前言

从开始做题到AC前前后后花了近一天...=。=...

题意没说清楚关于大王小王的使用啊,只有自己摸索着打,先WA再T,终于AC了,心力憔悴...

甚至TLE习惯了,最后看到绿色的、明亮的AC时,我都没有反应过来,很懵——嗯?我AC了?=。=

题目

题目描述

牛牛最近迷上了一种叫斗地主的扑克游戏。斗地主是一种使用黑桃、红心、梅花、方片的A到K加上大小王的共54张牌来进行的扑克牌游戏。在斗地主中,牌的大小关 系根据牌的数码表示如下:3<4<5<6<7<8<9<10<J<Q<K<A<2<小王<大王,而花色并不对牌的大小产生影响。每一局游戏中,一副手牌由 nnn 张牌组成。游戏者每次可以根据规定的牌型进行出牌,首先打光自己的手牌一方取得游戏的胜利。

现在,牛牛只想知道,对于自己的若干组手牌,分别最少需要多少次出牌可以将它们打光。请你帮他解决这个问题。

需要注意的是,本题中游戏者每次可以出手的牌型与一般的斗地主相似而略有不同。具体规则如下:

本题数据随机,不支持hack,要hack或强力数据请点击这里

输入格式

第一行包含用空格隔开的2个正整数 T , n,表示手牌的组数以及每组手牌的张数。

接下来 TTT 组数据,每组数据 n 行,每行一个非负整数对 ai,bi ,表示一张牌,其中 ai 表示牌的数码, bi表示牌的花色,中间用空格隔开。特别的,我们用 1 来表示数码 A, 11 表示数码J , 12 表示数码Q , 13 表示数码 K;黑桃、红心、梅花、方片分别用 1−4来表示;小王的表示方法为 01 ,大王的表示方法为 02 。

输出格式

共 T 行,每行一个整数,表示打光第 i 组手牌的最少次数。

输入输出样例

输入

1 8
7 4
8 4
9 1
10 4
11 1
5 1
1 4
1 1

输出

3

输入

1 17
12 3
4 3
2 3
5 4
10 2
3 3
12 2
0 1
1 3
10 1
6 2
12 1
11 3
5 2
12 4
2 2
7 2

输出

6

说明/提示

样例1说明

共有1组手牌,包含8张牌:方片7,方片8,黑桃9,方片10,黑桃J,黑桃5,方片A以及黑桃A。可以通过打单顺子(方片7,方片8,黑桃9,方片10,黑桃J),单张牌(黑桃5)以及对子牌(黑桃A以及方片A)在3次内打光。

对于不同的测试点, 我们约定手牌组数T与张数n的规模如下:

数据保证:所有的手牌都是随机生成的。

题目大意

给你一副牌,告诉了你出牌的规则(哪些牌可以怎样出、和谁出...),求最小的甩完牌的步数

分析(很多题解没有提及一些点,我这里进行了特别探讨)

感知一下,暴搜能过,直接上【暴搜】,似乎除了暴搜也没啥辙了=。=,最多DP优化一下

(一)先搜“顺子”

1.无论怎样来说,搜顺子有利于我们快速而轻松的打出5张甚至以上的牌,其余的东西,就留到最后慢慢打出就好了

2.“大小王”和“2”在顺子中不能出现

(二)注意“大王”和“小王”

1.若要大王小王同时打出,只能单独作“火箭”打出(其实不用管)

2.在“带牌”中,可以作为单张牌被带出,但不能作为对牌被带出

(三)关于“对子牌”、“火箭”、“三张牌”和“炸弹”和“剩下的牌”怎么处理

实验发现,导致我代码TLE的是对“对子牌”和“炸弹”的搜索:

/*对子牌*/ 
if(cnt[i]>=2)
{
	cnt[i]-=2;
	dfs(tot+1);
	cnt[i]+=2;
}
/*炸弹*/
if(cnt[i]>=4)
    dfs(tot+1);

后来分析可能是因为一系列牌出完后,也许会剩下一些牌,其每种剩余牌数量为1~4张(大王0或1张、小王0或1张)

那么我们可以针对这四种情况,对每种牌分别出“单牌”(一张王牌)、“对子”(两张王牌即“火箭”)、“三张牌”、“炸弹”

每种牌就最多1次出完

所以我们不用考虑搜索这几种情况,只需在搜索的最后累加剩余了牌的卡牌种数即可

(四)小剪枝

当当前搜到的步数大于等于已搜到的答案时,停止搜索

(虽然我试了,加不加没影响(数据太水),但为了养成良好习惯,还是加上吧=。=)

(五)关于“数据”

NOIP原题似乎数据很水,暴搜+贪心就能过了,不过有一个数据加强版,使得策略有一定调整

顺子依然要爆搜,但原有的贪心换用DP对其进行优化

对应状态就是牌数为多少的牌有多少种,再套上一个双王的数目(可以没有),打完这些牌所需的最小步数

考虑拆牌的情况,四张拆一张与三张,或是两个两张,或是两张加一对,三张拆成一张加一对,或是三张单牌

套在记搜内直接讨论即可

对牌和单牌直接出就好


【搜索顺序参考图】(主要是“顺子”和“带牌”的顺序,其他的小顺序(比如三带一、三带二)亲测好像影响不大)

 

TLE90分代码

这个版本是我没想好“对子”、“火箭”、“炸弹”和“大小王”

亲测发现:

大小王搞清楚了+搜了“对子牌”-5分+搜了“炸弹”-5分=90分TLE

大小王没搞清楚+去掉搜索“对子”、“火箭”和“炸弹”=30分WA

#include<cstdio>
#include<cmath>
#include<vector>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN=50,INF=0x3f3f3f3f;
int cnt[MAXN+5]; 
int t,n,ans;
int read()
{
	int x=0,f=1;
	char ch;
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
	return f*x;
}
void dfs(int tot)//已打出的牌数,出手次数 
{
	if(tot>=ans)//小减枝 
		return ;
	//顺子优先于其他牌型,出完后其他牌型随意 
	//顺子-------------------------------
	/*三顺子*/ 
	for(int i=3;i<=14;i++)
		if(cnt[i]>=3)
		{
			int j=i;
			while(cnt[j+1]>=3&&j+1<=14)
			{
				j++;
				if(j-i+1>=2)
				{
					for(int k=i;k<=j;k++)
						cnt[k]-=3;
					dfs(tot+1);
					for(int k=i;k<=j;k++)
						cnt[k]+=3;
				}			
			}
		}
	/*双顺子*/ 
	for(int i=3;i<=14;i++)
		if(cnt[i]>=2)
		{
			int j=i;
			while(cnt[j+1]>=2&&j+1<=14)
			{
				j++;
				if(j-i+1>=3)
				{
					for(int k=i;k<=j;k++)
						cnt[k]-=2;
					dfs(tot+1);
					for(int k=i;k<=j;k++)
						cnt[k]+=2;
				}			
			}
		}	
	/*单顺子*/ 
	for(int i=3;i<=14;i++)
		if(cnt[i]>=1)
		{
			int j=i;
			while(cnt[j+1]>=1&&j+1<=14)
			{
				j++;
				if(j-i+1>=5)
				{
					for(int k=i;k<=j;k++)
						cnt[k]--;
					dfs(tot+1);
					for(int k=i;k<=j;k++)
						cnt[k]++;
				}			
			}
		} 	
	//带牌-------------------------------
	for(int i=3;i<=15;i++)
	{
		/*三牌*/ 
		if(cnt[i]>=3)
		{
			cnt[i]-=3;
			for(int j=3;j<=17;j++)//可以带王 
				if(cnt[j]>=1)
				{
					/*三代一*/
					cnt[j]--;
					dfs(tot+1);
					cnt[j]++;	
				}	
			for(int j=3;j<=15;j++)
				if(cnt[j]>=2)
				{
					/*三代二*/
					cnt[j]-=2;
					dfs(tot+1);
					cnt[j]+=2;					
				}		
			cnt[i]+=3;
		} 		
		/*四牌*/
		if(cnt[i]>=4)
		{
			cnt[i]-=4;
			/*炸弹*/ 
			dfs(tot+1);
			for(int j=3;j<=17;j++)
			{
				/*四带一对*/ 
				if(cnt[j]>=2)
				{
					cnt[j]-=2;
					dfs(tot+1);
					cnt[j]+=2;
				}		
			} 
			for(int j=3;j<=15;j++)
			{
				/*四带两对*/ 
				if(cnt[j]>=2)
				{
					cnt[j]-=2;
					for(int k=3;k<=15;k++)
						if(cnt[k]>=2)
						{
							cnt[k]-=2;
							dfs(tot+1);
							cnt[k]+=2;	
						}
					cnt[j]+=2;
				}
			}
			for(int j=3;j<=17;j++)
			{
				/*四带两单*/
				if(cnt[j]>=1)
				{
					cnt[j]--;
					for(int k=3;k<=17;k++)//可以带王 
					{
						if(cnt[k]>=1)
						{
							cnt[k]-=1;
							dfs(tot+1);
							cnt[k]+=1;		
						}						
					}	
					cnt[j]++;				
				}
			} 
			cnt[i]+=4;						
		}	
		//其他------------------------------- 
		/*对子牌*/ 
		if(cnt[i]>=2)
		{
			cnt[i]-=2;
			dfs(tot+1);
			cnt[i]+=2;
		}	
	}		
	/*火箭*/
	if(cnt[16]&&cnt[17])
	{
		cnt[16]=cnt[17]=0;
		dfs(tot+1);
		cnt[16]=cnt[17]=1;
	} 
	/*出完剩余牌*/
	int tmp=tot;
	for(int i=3;i<=17;i++)
		if(cnt[i])
			 tmp++;
	ans=min(ans,tmp);
}	
int main()
{
	t=read(),n=read();
	while(t--)
	{
		ans=INF;
		for(int i=3;i<=17;i++)
			cnt[i]=0;
		int tmp1,tmp2;
		for(int i=1;i<=n;i++)
		{
			tmp1=read(),tmp2=read();//花色tmp2没有用 
			if(tmp1==1)//A
				cnt[14]++;
			else if(tmp1==2)//2
				cnt[15]++;
			else if(tmp1==0&&tmp2==1)//小王 
				cnt[16]++;//个人的奇怪表示方法qwq 
			else if(tmp1==0&&tmp2==2)//大王 
				cnt[17]++;
			else
			 	cnt[tmp1]++;
		}
		dfs(0);
		printf("%d\n",ans); 
	}
	return 0;
}

AC代码

由于过程实在是太心酸了,无数个“挂掉的”版本,这次就只放AC代码了qwq...

关于编号:3,4,5,6,7,8,9,10,11(J),12(Q),13(K),14(A),15(2),16(大小王)

上面TLE版本是16(大王),17(小王)

#include<cstdio>
#include<cmath>
#include<vector>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN=50,INF=0x3f3f3f3f;
int cnt[MAXN+5]; 
int t,n,ans;
void dfs(int tot)//出手次数 
{
	if(tot>=ans)//小减枝 
		return ;
	//顺子优先于其他牌型,出完后其他牌型随意 
	//顺子-------------------------------
	/*三顺子*/ 
	for(int i=3;i<=14;i++)
		if(cnt[i]>=3)
		{
			int j=i;
			while(cnt[j+1]>=3&&j+1<=14)
			{
				j++;
				if(j-i+1>=2)
				{
					for(int k=i;k<=j;k++)
						cnt[k]-=3;
					dfs(tot+1);
					for(int k=i;k<=j;k++)
						cnt[k]+=3;
				}			
			}
		}
	/*双顺子*/ 
	for(int i=3;i<=14;i++)
		if(cnt[i]>=2)
		{
			int j=i;
			while(cnt[j+1]>=2&&j+1<=14)
			{
				j++;
				if(j-i+1>=3)
				{
					for(int k=i;k<=j;k++)
						cnt[k]-=2;
					dfs(tot+1);
					for(int k=i;k<=j;k++)
						cnt[k]+=2;
				}			
			}
		}	
	/*单顺子*/ 
	for(int i=3;i<=14;i++)
		if(cnt[i]>=1)
		{
			int j=i;
			while(cnt[j+1]>=1&&j+1<=14)
			{
				j++;
				if(j-i+1>=5)
				{
					for(int k=i;k<=j;k++)
						cnt[k]--;
					dfs(tot+1);
					for(int k=i;k<=j;k++)
						cnt[k]++;
				}			
			}
		} 	
	//带牌-------------------------------
	for(int i=3;i<=15;i++)
	{
		/*四牌*/
		if(cnt[i]>=4)
		{
			cnt[i]-=4;
			for(int j=3;j<=15;j++)
			{
				/*四带一对*/ 
				if(cnt[j]>=2)
				{
					cnt[j]-=2;
					dfs(tot+1);
					cnt[j]+=2;
				}		
			} 
			for(int j=3;j<=15;j++)
			{
				/*四带两对*/ 
				if(cnt[j]>=2)
				{
					cnt[j]-=2;
					for(int k=3;k<=15;k++)
						if(cnt[k]>=2)
						{
							cnt[k]-=2;
							dfs(tot+1);
							cnt[k]+=2;	
						}
					cnt[j]+=2;
				}
			}
			for(int j=3;j<=16;j++)
			{
				/*四带两单*/
				if(cnt[j]>=1)
				{
					cnt[j]--;
					for(int k=3;k<=16;k++)//可以带王 
					{
						if(cnt[k]>=1)
						{
							cnt[k]-=1;
							dfs(tot+1);
							cnt[k]+=1;		
						}						
					}	
					cnt[j]++;				
				}
			} 
			cnt[i]+=4;						
		}
		/*三牌*/ 
		if(cnt[i]>=3)
		{
			cnt[i]-=3;
			for(int j=3;j<=16;j++)//可以带王 
				if(cnt[j]>=1)
				{
					/*三代一*/
					cnt[j]--;
					dfs(tot+1);
					cnt[j]++;	
				}	
			for(int j=3;j<=15;j++)
				if(cnt[j]>=2)
				{
					/*三代二*/
					cnt[j]-=2;
					dfs(tot+1);
					cnt[j]+=2;					
				}		
			cnt[i]+=3;
		} 		
	}
	/*出完剩余牌*/
	int tmp=tot;
	for(int i=3;i<=16;i++)
		if(cnt[i])
			 tmp++;
	ans=min(ans,tmp);
}	
int main()
{
	scanf("%d%d",&t,&n);
	while(t--)
	{
		ans=INF;
		for(int i=3;i<=17;i++)
			cnt[i]=0;
		int tmp1,tmp2;
		for(int i=1;i<=n;i++)
		{
			scanf("%d%d",&tmp1,&tmp2);//花色tmp2没有用 
			if(tmp1==1)//A
				cnt[14]++;
			else if(tmp1==2)//2
				cnt[15]++;
			else if(tmp1==0&&(tmp2==1||tmp2==2))//小王\大王 
				cnt[16]++;//个人的奇怪表示方法qwq 
			else
			 	cnt[tmp1]++;
		}
		dfs(0);
		printf("%d\n",ans); 
	}
	return 0;
}

总结

终于把题+博客弄完了,累shi...=。=...

几天后就是 NOIP CSP了,加油啊!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值