题目链接:洛谷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);
}
}