【搜索】NOIP2015斗地主(增强版)

题目链接:洛谷2540

题目描述

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

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

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

具体规则如下:

在此题中认为两个王不能组成对子牌

输入输出格式

输入格式:

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

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

输出格式:

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

输入输出样例

输入样例#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次内打光。

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

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

题解:

本题的数据比原版的斗地主强了不少,几乎一半的数据都有特殊构造。如果没有做过原版的同学,建议先做下原版。附上原版题目链接题解(本题的大部分解题思路仍然在这里)

基本思路和原版差不多,搜索+剪枝。但是,需要注意的有两点:

第一、本题中,双王(火箭)不算做对子牌,与原版题意有出入。

第二、在贪心带牌的时候,情况考虑不够全。也就是说,我们要优化计算贪心带牌的函数,要多考虑许多情况,例如拆三张、拆炸弹等,以使得出牌次数最小。具体情况详见代码。

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n,T,ans;
int cards[20];//记牌.
int Qans(){//贪心打散牌.
	int c[5]={0},num=0;
	bool Rocket=false;
	if (cards[1]==2) Rocket=true;//是否有王炸.
	c[1]+=cards[1];//王算单牌(本题中双王不算对子).
	for (register int i=2;i<=14;i++) c[cards[i]]++;//统计个数.
	while (!c[3] && c[1]==1 && c[2]==1 && c[4]>1) c[4]-=2,c[1]--,c[2]--,num+=2;//特判.
	//把一个炸拆成3张和单牌,再出一组四带二单和三带一.
	while (!c[2] && c[1]==1 && c[4]==1 && c[3]>1) c[3]-=2,c[1]--,c[4]--,num+=2;//特判.
	//把一组三张拆成一对和一单,再出一组四带二单和三带二.
	if (c[3]+c[4]>c[1]+c[2])//三四张的比单牌和对牌多,拆牌.
		while (c[4] && c[2] && c[3]) c[2]--,c[3]--,c[1]++,c[4]--,num++;//拆三张,4带两对余一单.
	if (c[3]+c[4]>c[1]+c[2])//还多继续拆.
		while (c[4] && c[1] && c[3]) c[1]--,c[3]--,c[2]++,c[4]--,num++;//拆三张,4带两单余一对.
	while (c[4] && c[1]>1) c[4]--,c[1]-=2,num++;//四带两单.
	while (c[4] && c[2]>1) c[4]--,c[2]-=2,num++;//四带两对.
	while (c[4] && c[2]) c[4]--,c[2]--,num++;//对看成两单再四带.
	if (c[3]%3==0 && c[1]+c[2]<=1)//三张的太多了拆三张.
		while (c[3]>2) c[3]-=3,num+=2;//把一组三张拆成单和对,再出三带一和三带二.
	while (c[3] && c[1]) c[3]--,c[1]--,num++;//三带一.
	while (c[3] && c[2]) c[3]--,c[2]--,num++;//三带二.
	//还剩三张和炸,组合出.
	while (c[4]>1 && c[3]) c[3]--,c[4]-=2,num+=2;//把一个炸拆成一对和两单,再出三带二和四带两单.
	while (c[3]>1 && c[4]) c[4]--,c[3]-=2,num+=2;//把一个炸拆成两对,再出两组三带一对.
	while (c[3]>2) c[3]-=3,num+=2;//同上,把一组三张拆成单和对,再出三带一和三带二.
	while (c[4]>1) c[4]-=2,num++;//把一个炸拆成两对,再出一组四带两对.
	//有王炸并且没被带跑.
	if (Rocket && c[1]>=2) return num+c[2]+c[3]+c[4]+c[1]-1;//双王一块出.
	else return num+c[1]+c[2]+c[3]+c[4];//出剩余的牌,返回答案.
}
void DFS(int step){//开始出牌.
	if (step>=ans) return ;//最优性剪枝.
	int tmp=Qans();//打散牌.
	ans=min(tmp+step,ans);//更新最优解.
	//三顺子.
	for (register int i=3;i<=13;i++){//枚举连续牌起始点.
		register int l=0;
		while (cards[i+l]>=3) l++;//找出最大长度.
		for (register int j=l;j>=2;j--){//枚举出牌长度.
			register int end=i+j-1;
			for (register int k=i;k<=end;k++) cards[k]-=3;//出飞机.
			DFS(step+1);//继续出牌.
			for (register int k=i;k<=end;k++) cards[k]+=3;//搜索回溯.
		}
	}
	//单顺子.
	for (register int i=3;i<=12;i++){//枚举连续牌起始点.
		register int l=0;
		while (cards[i+l]>=2) l++;//找出最大长度.
		for (register int j=l;j>=3;j--){//枚举出牌长度.
			register int end=i+j-1;
			for(register int k=i;k<=end;k++) cards[k]-=2;//出连对.
			DFS(step+1);//继续出牌.
			for(register int k=i;k<=end;k++) cards[k]+=2;//搜索回溯.
		}
	}
	//双顺子.
	for(register int i=3;i<=10;i++) {//枚举连续牌起始点.
		register int l=0;
		while (cards[i+l]>=1) l++;//找出最大长度.
		for (register int j=l;j>=5;j--){
			register int end=i+j-1;
			for(register int k=i;k<=end;k++) cards[k]-=1;//出顺子.
			DFS(step+1);//继续出牌.
			for(register int k=i;k<=end;k++) cards[k]+=1;//搜索回溯.
		}
	}
}
int main(){
//	freopen("landlords.in","r",stdin);
//	freopen("landlords.out","w",stdout);
	scanf("%d%d",&T,&n);
	while (T--){
		ans=0x7fffffff;
		memset(cards,0,sizeof(cards));
		for (register int i=1,a,b;i<=n;i++){
			scanf("%d%d",&a,&b);
			if (a==1) cards[14]++;//14代表A
			else if (a==0) cards[1]++;//1代表王
			else cards[a]++;
		}
		DFS(0);
		printf("%d\n",ans);
	}
}

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值