P2688 斗地主/P2540 斗地主加强版

题目描述

牛牛最近迷上了一种叫斗地主的扑克游戏。斗地主是一种使用黑桃、红心、梅花、方片的 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。

输出格式

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

样例 #1

样例输入 #1

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

样例输出 #1

3

样例 #2

样例输入 #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<bits/stdc++.h>
using namespace std;
int num[30],ans;
int calc()
{
    int c[5],tot=0;
    memset(c,0,sizeof c);
    bool wz=false;
    if(num[0]==2)wz=true;
    c[1]+=num[0];
    for(int i=2;i<=14;i++)c[num[i]]++;
    while(!c[3]&&c[1]==1&&c[2]==1&&c[4]>1)
        c[4]-=2,c[1]--,c[2]--,tot+=2;
    //把炸弹拆成3张和单牌,再出一组四带二单和三带一
    while(!c[2]&&c[1]==1&&c[4]==1&&c[3]>1)
        c[3]-=2,c[1]--,c[4]--,tot+=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]--,tot++;//拆三张,4带两队余一单
    if(c[3]+c[4]>c[1]+c[2])//还多继续拆
        while(c[4]>1&&!c[2]&&!c[1]&&c[3]>1)
            c[3]-=2,c[4]-=2,tot+=2;
    while(c[4]&&c[1]>1)c[4]--,c[1]-=2,tot++;//四带两单
    while(c[4]&&c[2]>1)c[4]--,c[2]-=2,tot++;//四带两对
    while(c[4]&&c[2])c[4]--,c[2]--,tot++;//对看成两单再四带
    while(c[3]&&c[1])c[3]--,c[1]--,tot++;//三带一
    while(c[3]&&c[2])c[3]--,c[2]--,tot++;//三带二
    while(c[4]>1&&c[3])c[3]--,c[4]-=2,tot+=2;//把一个炸拆成一对和两单,再出三带二和四带两单
    while(c[3]>1&&c[4])c[4]--,c[3]-=2,tot+=2;//把一个炸拆成两对,再出两组三带一对
    while(c[3]>2)c[3]-=3,tot+=2;//同上,把一组三张拆成单和对,再出三带一和三带二
    while(c[4]>1)c[4]-=2,tot++;//把一个炸拆成两对,再出一组四带两对
    if(wz&&c[1]>=2)//有王炸并且没被带跑
        tot--;
    return tot+c[1]+c[2]+c[3]+c[4];//出剩余的牌,返回答案
}
void dfs(int step)//深搜
{
    if(step>ans)return;
    ans=min(ans,step+calc());//寻找最小步数
    for(int p=3;p>=1;p--)//枚举顺子
    {
        for(int j=3;j<=14;j++)
        {
            int k=j;
            int len=0;
            while(num[k]>=p&&k<=14)
            {
                k++;
                len++;
                if((p==3&&len>=2)||(p==2&&len>=3)||(p==1&&len>=5))
                {
                    for(int i=j;i<k;i++)
                        num[i]-=p;
                    dfs(step+1);
                    for(int i=j;i<k;i++)
                        num[i]+=p;
                }
            }
        }
    }
}
int main()
{
    int t,n;
    cin>>t>>n;
    while(t--)
    {
        ans=n;
        memset(num,0,sizeof num);
        for(int i=0;i<n;i++)
        {
            int x,y;
            cin>>x>>y;
            if(x==1)x=14;//1代表A,所以变成14
            num[x]++;
        }
        dfs(0);
        cout<<ans<<endl;
    }
    return 0;
}

代码还算有点长,认真地思考才能完全理解哦。

后记

写这个斗地主可耗了我一个多小时啊!代码不易,点个赞吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值