【搜索】NOIP2015斗地主

题目链接:洛谷2668

题目描述

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

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

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

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

输入输出格式

输入格式:

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

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

输出格式:

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

输入输出样例

输入样例#1: 

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

输出样例#1: 

3

输入样例#2: 

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

输出样例#2: 

6

说明

样例1说明

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

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

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

题解:

首先,思考+观察数据范围可知,本题应该用搜索算法,其次,发现每个状态的子状态数过多且搜索最深层数不深,因此宜用DFS。如果暴力搜索,时间一定扛不住,考虑如何有技巧地DFS。发现顺子对牌数有较大影响,即如果出一个顺子,那么牌数将减少许多,因此,我们暴力地搜索顺子,然后再贪心地考虑带牌或单牌,就可以对DFS的效率有很大优化。

还有对DFS的剪枝。

第一、如果我们当前搜索的层数比已经得到的答案还要大,那么直接返回。

第二、利用贪心带牌+暴力搜顺子的搜索方式,提高DFS效率并降低DFS层数。

在此怼一下出题人...第一次做这道题的时候,读题读了半个多小时,还没读懂...因此在这里解释一下题意。

出牌方式包括单牌、对子、三张、炸弹、火箭(王炸)、三带一、三带二、单顺、双顺、三顺和四带二。其中,火箭算作对子牌,也就是说,允许三带两张王。四带二中允许四张带一个对子,此时这个对子视为两张单牌。单顺需要5张及以上、双顺需要3张及以上、三顺需要2张及以上。其他的应该没有问题了。

还有,网上的题解也有些问题,90%以上的题解(包括这篇)过不了特殊构造的数据(因为贪心时考虑情况不全),比如对于数据:三张3,三张5,三张7。正确答案应该是2,第一次三张3带一张5,第二次三张7带两张5。而题解中在贪心带牌时没有考虑拆三张,因此会输出错误答案3。不过数据中保证手牌随机生成,出现这类特殊数据的概率极小,因此可以通过此题。

代码如下(跟其他题解一样不能通过特殊数据...不过通过本题还是够了),洛谷上有一道斗地主增强版,数据要求比较严格,更加严格的代码将在后续的博客中给出。

#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=25;
int T,n,ans;
int cards[maxn];//cards[i]表示i牌有cards[i]张.
int GetAns(){
	int cnt=0,c[maxn]={0};//cnt用来统计带牌的步数,c[i]表示有c[i]个张数为i的牌.
	for (int i=0;i<=13;i++) c[cards[i]]++;//统计张数为1,2,3,4的牌的个数.
	while (c[4] && c[2]>=2) c[4]-=1,c[2]-=2,cnt++;//四带二(两个对子).
	while (c[4] && c[1]>=2) c[4]-=1,c[1]-=2,cnt++;//四带二(两张单牌).
	while (c[4] && c[2]) c[4]-=1,c[2]-=1,cnt++;//四带二(一个对子看做两张单牌).
	while (c[3] && c[2]) c[3]-=1,c[2]-=1,cnt++;//三带一(一个对子).
	while (c[3] && c[1]) c[3]-=1,c[1]-=1,cnt++;//三带一(一张单牌).
	return cnt+c[4]+c[3]+c[2]+c[1];//输出贪心所需要的步骤(带牌+炸弹+三张+对子+单牌).
}
void DFS(int depth){
	if (depth>ans) return ;//剪枝一————当前步数大于答案.
	int t=GetAns();//贪心带牌(剪枝二).
	if (depth+t<ans) ans=depth+t;//当前步数+贪心带牌的步数比答案更优.
        //接下来考虑出顺子以减少步数.
	for (int i=2;i<=13;i++){//三顺.
		int pos=i;
		while (cards[pos]>=3) pos++;//pos表示能连续的三张的最后一张.
		if (pos-i>=2){//如果连续三张的长度大于2(能成顺).
			for (int j=i+1;j<=pos-1;j++){
				for (int k=i;k<=j;k++) cards[k]-=3;//出三顺,更新牌数.
				DFS(depth+1);//进一步搜索.
				for (int k=i;k<=j;k++) cards[k]+=3;//搜索结束,复原牌数.
			}
		}
	}
	for (int i=2;i<=13;i++){//双顺.
		int pos=i;
		while (cards[pos]>=2) pos++;//pos表示能连续的两张的最后一张.
		if (pos-i>=3){//如果连续两张的长度大于3(能成顺).
			for (int j=i+2;j<=pos-1;j++){
				for (int k=i;k<=j;k++) cards[k]-=2;//出双顺,更新牌数.
				DFS(depth+1);//进一步搜索.
				for (int k=i;k<=j;k++) cards[k]+=2;//搜索结束,复原牌数.
			}
		}
	}
	for (int i=2;i<=13;i++){//单顺.
		int pos=i;
		while (cards[pos]>=1) pos++;//pos表示能连续的单张的最后一张.
		if (pos-i>=5){//如果连续两张的长度大于5(能成顺).
			for (int j=i+4;j<=pos-1;j++){
				for (int k=i;k<=j;k++) cards[k]-=1;//出单顺,更新牌数.
				DFS(depth+1);//进一步搜索.
				for (int k=i;k<=j;k++) cards[k]+=1;//搜索结束,复原牌数.
			}
		}
	}
}
int main(){
//	freopen("landlords.in","r",stdin);
//	freopen("landlords.out","w",stdout);
	scanf("%d%d",&T,&n);
	while (T--){
        //初始化.
		memset(cards,0,sizeof(cards));
		ans=0x7fffffff;
		for (int i=1,x,color;i<=n;i++){
			scanf("%d%d",&x,&color);
			if (x==1) x=13;//特殊处理A.
			else if (x) x--;
			cards[x]++;//统计牌.
		}
		DFS(0);
		printf("%d\n",ans);
	}
	return 0;
}

 

猜你喜欢

转载自blog.csdn.net/zhanxufeng/article/details/83025397
今日推荐