【NOIP 2016】斗地主

时间:2022-04-01 17:47:43

题意

NOIP 2016 斗地主

给你一些牌,按照斗地主的出牌方式,问最少多少次出完所有的牌。

分析

这道题的做法是DFS。

为了体现这道题的锻炼效果,我自己写了好多个代码。

Ver1

直接暴力搞,加深迭代搜索。

发现出牌顺序不影响解决。

优化:先搞顺子,再搞N带N,再搞火箭,再搞N张

int DFS(int nd,int lim)
{
    int cur;
    int siz;

    if (nd>lim)
        return 0;

    siz=0;
    rep(i,3,17)
        siz+=cnt[i];
    if (nd==lim&&!siz)
        return 1;

    rep(i,3,17) if (cnt[i]>=4)
    {
        cnt[i]-=4;
        rep(j,3,17) if (cnt[j]>=1)
        {
            cnt[j]--;
            rep(k,3,17) if (cnt[k]>=1)
            {
                cnt[k]--;
                if (DFS(nd+1,lim))
                    return 1;
                cnt[k]++;
            }
            cnt[j]++;
        }
        cnt[i]+=4;
    }

    rep(i,3,14) if (cnt[i]>=3)
    {
        cur=i;
        while (cur+1<=14&&cnt[cur+1]>=3)
            cur++;
        rep(j,2,cur-i+1)
        {
            rep(k,i,i+j-1) cnt[k]-=3;
            if (DFS(nd+1,lim))
                return 1;
            rep(k,i,i+j-1) cnt[k]+=3;
        }
    }

    rep(i,3,14) if (cnt[i]>=2)
    {
        cur=i;
        while (cur+1<=14&&cnt[cur+1]>=2)
            cur++;
        rep(j,3,cur-i+1)
        {
            rep(k,i,i+j-1) cnt[k]-=2;
            if (DFS(nd+1,lim))
                return 1;
            rep(k,i,i+j-1) cnt[k]+=2;
        }
    }

    rep(i,3,14) if (cnt[i]>=1)
    {
        cur=i;
        while (cur+1<=14&&cnt[cur+1]>=1)
            cur++;
        rep(j,5,cur-i+1)
        {
            rep(k,i,i+j-1) cnt[k]--;
            if (DFS(nd+1,lim))
                return 1;
            rep(k,i,i+j-1) cnt[k]++;
        }
    }

    rep(i,3,17) if (cnt[i]>=3)
    {
        cnt[i]-=3;
        rep(j,3,17) if (cnt[j]>=2)
        {
            cnt[j]-=2;
            if (DFS(nd+1,lim))
                return 1;
            cnt[j]+=2;
        }
        cnt[i]+=3;
    }

    rep(i,3,17) if (cnt[i]>=3)
    {
        cnt[i]-=3;
        rep(j,3,17) if (cnt[j]>=1)
        {
            cnt[j]--;
            if (DFS(nd+1,lim))
                return 1;
            cnt[j]++;
        }
        cnt[i]+=3;
    }

    rep(i,3,17) if (cnt[i]>=4)
    {
        cnt[i]-=4;
        if (DFS(nd+1,lim))
            return 1;
        cnt[i]+=4;
    }

    rep(i,3,17) if (cnt[i]>=3)
    {
        cnt[i]-=3;
        if (DFS(nd+1,lim))
            return 1;
        cnt[i]+=3;
    }

    if (cnt[16]>=1&&cnt[17]>=1)
    {
        cnt[16]--,cnt[17]--;
        if (DFS(nd+1,lim))
            return 1;
        cnt[16]++,cnt[17]++;
    }

    rep(i,3,17) if (cnt[i]>=2)
    {
        cnt[i]-=2;
        if (DFS(nd+1,lim))
            return 1;
        cnt[i]+=2;
    }

    rep(i,3,17) if (cnt[i]>=1)
    {
        cnt[i]--;
        if (DFS(nd+1,lim))
            return 1;
        cnt[i]++;
    }

    return 0;
}

Ver2

在Ver1的基础上:

【优化1】

对于顺子的,原本的操作复杂度为15*15*15:

    rep(i,3,14) if (cnt[i]>=1)
    {
        cur=i;
        while (cur+1<=14&&cnt[cur+1]>=1)
            cur++;
        rep(j,5,cur-i+1)
        {
            rep(k,i,i+j-1) cnt[k]--;
            if (DFS(nd+1,lim))
                return 1;
            rep(k,i,i+j-1) cnt[k]++;
        }
    }

发现每次的操作都是重复的,所以改成15*15的操作:

    rep(i,3,14) if (cnt[i]>=1)
    {
        cur=i;
        while (cur+1<=14&&cnt[cur+1]>=1)
            cur++;
        if (cur-i+1>=5)
        {
            rep(j,i,cur) cnt[j]--;
            per(j,cur,i+4)
            {
                if (j!=cur) cnt[j+1]++;
                if (DFS(nd+1,lim))
                    return 1;
            }
            rep(j,i,i+4) cnt[j]++;
        }
    }

Ver3

改成深搜+剪枝,加一个dwl属性。

枚举完所有的第一种才能枚举第二种,枚举完所有的第二种才能枚举第三种,etc。

void DFS(int nd,int dwl)
{
    int cur;
    int siz;

    if (nd>=ans)
        return;

    siz=0;
    rep(i,3,17)
        siz+=cnt[i];
    if (!siz)
    {
        ans=nd;
        return;
    }

     //①先per优化一个*5
     //②改变顺序
    if (dwl<=1)
    {
        if (cnt[16]>=1&&cnt[17]>=1)
        {
            cnt[16]--,cnt[17]--;
            DFS(nd+1,1);
            cnt[16]++,cnt[17]++;
        }
    }
    //火箭 

    if (dwl<=2)
    {
        rep(i,3,14) if (cnt[i]>=1)
        {
            cur=i;
            while (cur+1<=14&&cnt[cur+1]>=1)
                cur++;
            if (cur-i+1>=5)
            {
                rep(j,i,cur) cnt[j]--;
                per(j,cur,i+4)
                {
                    if (j!=cur) cnt[j+1]++;
                    DFS(nd+1,2);
                }
                rep(j,i,i+4) cnt[j]++;
            }
        }
    }
    //单顺子 

    if (dwl<=3)
    {
        rep(i,3,14) if (cnt[i]>=3)
        {
            cur=i;
            while (cur+1<=14&&cnt[cur+1]>=3)
                cur++;
            if (cur-i+1>=2)
            {
                rep(j,i,cur) cnt[j]-=3;
                per(j,cur,i+1)
                {
                    if (j!=cur) cnt[j+1]+=3;
                    DFS(nd+1,3);
                }
                rep(j,i,i+1) cnt[j]+=3;
            }
        }
    }
    //三顺子 

    if (dwl<=4)
    {
        rep(i,3,14) if (cnt[i]>=2)
        {
            cur=i;
            while (cur+1<=14&&cnt[cur+1]>=2)
                cur++;
            if (cur-i+1>=3)
            {
                rep(j,i,cur) cnt[j]-=2;
                per(j,cur,i+2)
                {
                    if (j!=cur) cnt[j+1]+=2;
                    DFS(nd+1,4);
                }
                rep(j,i,i+2) cnt[j]+=2;
            }
        }
    }
    //双顺子 

    if (dwl<=5)
    {
        rep(i,3,17) if (cnt[i]>=4)
        {
            cnt[i]-=4;
            rep(j,3,17) if (cnt[j]>=1)
            {
                cnt[j]--;
                rep(k,j,17) if (cnt[k]>=1)
                {
                    cnt[k]--;
                    DFS(nd+1,5);
                    cnt[k]++;
                }
                cnt[j]++;
            }
            cnt[i]+=4;
        }
    }
    //四带二

    if (dwl<=6)
    {
        rep(i,3,17) if (cnt[i]>=3)
        {
            cnt[i]-=3;
            rep(j,3,17) if (cnt[j]>=2)
            {
                cnt[j]-=2;
                DFS(nd+1,6);
                cnt[j]+=2;
            }
            cnt[i]+=3;
        }
    }
    //三带二 

    if (dwl<=7)
    {
        rep(i,3,17) if (cnt[i]>=3)
        {
            cnt[i]-=3;
            rep(j,3,17) if (cnt[j]>=1)
            {
                cnt[j]--;
                DFS(nd+1,7);
                cnt[j]++;
            }
            cnt[i]+=3;
        }
    }
    //三带一 

    if (dwl<=8)
    {
        rep(i,3,17) if (cnt[i]>=4)
        {
            cnt[i]-=4;
            DFS(nd+1,8);
            cnt[i]+=4;
        }
    }
    //炸弹 

    if (dwl<=9)
    {
        rep(i,3,17) if (cnt[i]>=3)
        {
            cnt[i]-=3;
            DFS(nd+1,9);
            cnt[i]+=3;
        }
    }
    //三张牌 

    if (dwl<=10)
    {
        rep(i,3,17) if (cnt[i]>=2)
        {
            cnt[i]-=2;
            DFS(nd+1,10);
            cnt[i]+=2;
        }
    }
    //对牌 

    if (dwl<=11)
    {
        rep(i,3,17) if (cnt[i]>=1)
        {
            cnt[i]--;
            DFS(nd+1,11);
            cnt[i]++;
        }
    }
    //单牌
}

Ver4

忘记自己干了啥了。。

Ver5

始终是30ptTLE掉。

所以直接看正解了。

http://blog.csdn.net/xieguofu2014/article/details/50193545

既然有正解了,这里就不再写了。

关键在于把操作进行一下分类:顺子、N带N,然后归纳通性。

附网上的STD(含自己乱七八糟的注释):

#include<cstdio>
#include<cstring>
#include<cmath>
#include<cstdlib>
#include<ctime>

typedef long long ll;
typedef double lf;

const int INF=1<<30;

inline int read(){
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
    return x*f;
}

int T,n,ans;
struct state{
    int num[16];
    inline int& operator[](int x){return num[x];}
}now;
int temp[10];

void dfs(int);
void group(int c,int s,int depth)
//个数为c,至少要有s个,当前已经进行了depth次
{
    for(int st=1,fi;st<=12-s+1;st++){
        for(fi=st;now[fi]>=c&&fi<=12;fi++){
            now[fi]-=c;
            if(fi-st+1>=s)
                dfs(depth+1);
        }
        for(int i=st;i<fi;i++)
            now[i]+=c;
    }
    //暴力剪
}

void dfs(int depth){
    if(depth>ans)return;
    //剪!! 

    int sum=0;
    for(int i=1;i<=15;i++)
        temp[now[i]]++;
    //temp[i]: now[j]=i的个数 

    while(temp[4]>0){
        temp[4]--,sum++;
        //4张直接出
        if(temp[2]>=2)
            temp[2]-=2;
        else if(temp[1]>=2)
            temp[1]-=2;
    }
    //四带二 

    while(temp[3]>0){
        temp[3]--,sum++;
        if(temp[2]>=1)
            temp[2]--;
        else if(temp[1]>=1)
            temp[1]--;
    }
    //三带 

    sum+=temp[1]+temp[2];
    //一和二直接 

    if(temp[1]>=2&&now[14]>=1&&now[15]>=1)
        sum--;
    //处理JOKER的情况:sum-- 

    temp[1]=temp[2]=0;
    if(depth+sum<ans)
        ans=depth+sum;
    //若depth+sum<ans那么就更新

    //上面一大段,解决的是:昨晚所有的顺子之后,直接贪心求解的情况 

    group(1,5,depth);
    //枚举单张,长度为5的单顺子
    group(2,3,depth);
    //枚举双张,长度为3的双顺子
    group(3,2,depth);
    //枚举3张,长度为2的三顺子
}

void work(){
    memset(now.num,0,sizeof now.num);
    ans=1<<30;
    for(int i=1;i<=n;i++){
        int k=read(),c=read();
        if(k==0)
            now[13+c]++;
        else if(k<=2)
            now[11+k]++;
        else
            now[k-2]++;
    }
    dfs(0);
    printf("%d\n",ans);
}

int main(){
    freopen("a.in","r",stdin);
    freopen("a.ans","w",stdout);
    T=read(),n=read();
    for(int turn=1;turn<=T;turn++)
        work();
    return 0;
}

附自己的AC代码:

#include <cstdio>
#include <cstring>
#include <cctype>
#include <climits>
#include <algorithm>
using namespace std;

#define rep(i,a,b) for (int i=(a);i<=(b);i++)
#define per(i,a,b) for (int i=(a);i>=(b);i--)

const int MAX=INT_MAX;

int cas;

int n;
int cnt[16];

int ans;
int cal[5];

inline int rd(void)
{
    int x=0,f=1; char c=getchar();
    for (;!isdigit(c);c=getchar()) if (c=='-') f=-1;
    for (;isdigit(c);c=getchar()) x=x*10+c-'0';
    return x*f;
}

int Calc(void)
{
    int sum=0;

    memset(cal,0,sizeof cal);
    rep(i,1,15)
        cal[cnt[i]]++;

    while (cal[4]>0)
    {
        sum++; cal[4]--;
        if (cal[2]>=2)
            cal[2]-=2;
        else if (cal[1]>=2)
            cal[1]-=2;
    }

    while (cal[3]>0)
    {
        sum++; cal[3]--;
        if (cal[2]>=1)
            cal[2]--;
        else if (cal[1]>=1)
            cal[1]--;
    }

    sum+=(cal[2]+cal[1]);

    if (cal[1]>=2&&cnt[14]>=1&&cnt[15]>=1)
        sum--;

    return sum;
}

void DFS(int);
void Solve(int c,int len,int dep)
{
    int cur;
    rep(i,1,12-len+1) if (cnt[i]>=c)
    {
        cur=i;
        while (cur+1<=12&&cnt[cur+1]>=c)
            cur++;
        if (cur-i+1>=len)
        {
            rep(j,i,cur) cnt[j]-=c;
            per(j,cur,i+len-1)
            {
                if (j!=cur) cnt[j+1]+=c;
                DFS(dep+1);
            }
            rep(j,i,i+len-1) cnt[j]+=c;
        }
    }
}

void DFS(int dep)
{
    if (dep>ans)
        return;

    int sum=Calc();
    ans=min(ans,sum+dep);

    Solve(1,5,dep);
    Solve(2,3,dep);
    Solve(3,2,dep);
}

int main(void)
{
//  freopen("a.in","r",stdin);
//  freopen("a.out","w",stdout);

    cas=rd(),n=rd();
    rep(tur,1,cas)
    {
        int x,y; memset(cnt,0,sizeof cnt);
        rep(i,1,n)
        {
            x=rd(),y=rd();
            if (0<x&&x<=2) cnt[x+11]++;
            else if (x>2) cnt[x-2]++;
            else cnt[y+13]++;
        }

        ans=MAX;
        DFS(0);
        printf("%d\n",ans);
    }

    return 0;
}

小结

(1)对于操作很多很繁杂的问题时,分类的作用必不可少。

分类的目的在于:限定具有相同结构的一些对象,研究它们的通性,更好地优化问题。

本题就是按照顺子、N带N这两个来分类,先处理完所有的顺子,N带N的就可以贪心了。

其实这种东西在化学课上才刚讲完,为什么想不出来,真的应该好好反思一下。

(2)当操作顺序不影响的时候,我们的想法是定序:规定枚举顺序。即:必然从前面枚举到后面,减少一个数量级。

其实最后的代码并没有使用定序的优化,但是优化后效果更佳。