zoukankan      html  css  js  c++  java
  • NOIP原题 斗地主(20190804)

    题目描述

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

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

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

     

    输入格式

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

    接下来 TT 组数据,每组数据 nn 行,每行一个非负整数对 a_i,b_iai,bi ,表示一张牌,其中 a_iai 表示牌的数码, b_ibi表示牌的花色,中间用空格隔开。特别的,我们用 11 来表示数码 AA, 1111 表示数码JJ, 1212 表示数码QQ, 1313 表示数码 KK;黑桃、红心、梅花、方片分别用 1-414 来表示;小王的表示方法为 0101 ,大王的表示方法为 0202 。

    输出格式

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

    输入输出样例

    输入 #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说明

    共有11组手牌,包含8张牌:方片77,方片88,黑桃99,方片1010,黑桃JJ,黑桃55,方片AA以及黑桃AA。可以通过打单顺子(方片77,方片88,黑桃99,方片1010,黑桃JJ),单张牌(黑桃55)以及对子牌(黑桃AA以及方片AA)在33次内打光。

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

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

    一句话概括题目意思就是斗地主,有多种出牌方式,求最快出完要几步。

    思路:

          

           这题只记得当时去郑州时老师没怎么细说,就说打了搜索搞了八九十分,好像还是当年的第三题,不由得感叹,要是现在的NOIP还是这样该多好。。。。。

          昨天在机房就听到zwjdd和shymm感叹出法的不常规,洛谷题解里也都是“我斗地主会玩,这题不会打”的感叹。。。。,感觉要不是打过一遍也要GG。。。

         这题感觉就是搜索一个一个找,感觉和原来那个麻将好像。。。。

    • 无论怎么搞,顺子都有利于我们打出5或以上的牌,一下子能少掉好多,所以顺子的优先级最高,其余的东西以后一个一个搞就好了。
    • 然后可以适当的剪下枝,毕竟全是for 也怪吓人的。。。
    • 如果当前的出牌数已经超过了当前最优的ans,剪了(最优性剪枝)
    • 其他还有一些神奇的剪枝,如先出牌数多的,后出牌数少的,感觉这样能过,就没加。。。。
    • 顺子要暴力找,不要搞什么幺蛾子。。。。就像3,4,5,6,7,6,7,8,9,10,如果用什么方法去处理有可能就变成了了最长的,这样就剩下了两张牌,但如果出两个顺子,就比上一种方案优秀,所以要暴力,也许是有什么方法可以预处理出来,但我还是太菜了
    • 贪心打散牌,先出四带二,再出三带一,以此类推,这个仅可用与NOIP原题

         下面是搜索顺序:

    • 要注意一个地方两个王不一样,不能当一对,只有火箭可以。,然后就是暴搜,感觉就要注意一下细节。
      1 #include<iostream> 
      2 using namespace std;
      3 int T,n,ans,sum[25];
      4 void dfs(int x)//x为出牌次数
      5 {
      6     if (x>=ans) 
      7     return;
      8     //顺子
      9     int k=0;//单顺子
     10     for (int i=3;i<=14;i++)//注意2和大小王不能考虑
     11     {
     12         if(sum[i]==0) k=0;//顺子断了
     13         else
     14         {
     15             k++;//顺子长度增加
     16             if(k>=5)//单顺子达到五张
     17             {
     18                 for(int j=i;j>=i-k+1;j--) 
     19                 sum[j]--;//出牌
     20                 dfs(x+1);//继续搜
     21                 for(int j=i;j>=i-k+1;j--) 
     22                 sum[j]++;//回溯
     23             }
     24         }
     25     }
     26     k=0;//双顺子
     27     for(int i=3;i<=14;i++)
     28     {
     29         if(sum[i]<=1) k=0;
     30         else 
     31         {
     32             k++;
     33             if(k>=3)//双顺子达到三组
     34             {
     35                 for(int j=i;j>=i-k+1;j--) 
     36                 sum[j]-=2;//出牌
     37                 dfs(x+1);
     38                 for(int j=i;j>=i-k+1;j--) 
     39                 sum[j]+=2;//回溯
     40             }
     41         }
     42     }
     43     k=0;//三顺子    //以下同理
     44     for(int i=3;i<=14;i++)
     45     {
     46         if(sum[i]<=2) k=0;
     47         else 
     48         {
     49             k++;
     50             if(k>=2)//三顺子达到两组
     51             {
     52                 for(int j=i;j>=i-k+1;j--) sum[j]-=3;
     53                 dfs(x+1);
     54                 for(int j=i;j>=i-k+1;j--) sum[j]+=3;
     55             }
     56         }
     57     }
     58     //带牌
     59     for(int i=2;i<=14;i++)//枚举有3张或4张的牌(这样才能带牌)
     60     {
     61         if(sum[i]<=3)
     62         {
     63             if(sum[i]<=2) 
     64             continue;//三张以下(不含三张)不能带牌
     65             sum[i]-=3;//出掉用来带别人的牌
     66             for(int j=2;j<=15;j++)//带单张
     67             {
     68                 if(sum[j]<=0) 
     69                 continue;//没有牌怎么带??
     70                 sum[j]--;//出掉被带的单张
     71                 dfs(x+1);
     72                 sum[j]++;//回溯
     73             }
     74             for(int j=2;j<=14;j++)//带一对
     75             {
     76                 if(sum[j]<=1) 
     77                 continue;//没有一对怎么带?
     78                 sum[j]-=2;//出掉被带的一对
     79                 dfs(x+1);
     80                 sum[j]+=2;//回溯
     81             }
     82             sum[i]+=3;//回溯
     83         } 
     84         else//大于3可以4带别的也可以3带别的
     85         {
     86             sum[i]-=3;//先用3张带别的
     87             for(int j=2;j<=15;j++) //带单张  //以下原理同上
     88             {
     89                 if(sum[j]<=0) 
     90                 continue;
     91                 sum[j]--;
     92                 dfs(x+1);
     93                 sum[j]++;
     94             }
     95             for(int j=2;j<=14;j++) //带一对
     96             {
     97                 if(sum[j]<=1) 
     98                 continue;
     99                 sum[j]-=2;
    100                 dfs(x+1);
    101                 sum[j]+=2;
    102             }
    103             sum[i]+=3;
    104 
    105             sum[i]-=4; //再用4张带别的 
    106             for(int j=2;j<=15;j++) //带2个单张
    107             {
    108                 if(sum[j]<=0) 
    109                 continue;//自己不能带自己喽
    110                 sum[j]--;//出被带的第一张单张牌
    111                 for (int k=2;k<=15;k++)//找第二张单张
    112                 {
    113                     if(sum[k]<=0)
    114                      continue;
    115                     sum[k]--;//出被带的第二张单张牌
    116                     dfs(x+1);
    117                     sum[k]++;//回溯
    118                 }
    119                 sum[j]++;//回溯
    120             }
    121             for(int j=2;j<=14;j++)//带2个对儿
    122             {
    123                 if(sum[j]<=1) 
    124                 continue;
    125                 sum[j]-=2;//出被带的第一对牌
    126                 for(int k=2;k<=14;k++) 
    127                 {
    128                     if(sum[k]<=1) 
    129                     continue;
    130                     sum[k]-=2;//出被带的第二对牌
    131                     dfs(x+1);
    132                     sum[k]+=2;//回溯
    133                 }
    134                 sum[j]+=2;//回溯
    135             }
    136             sum[i]+=4;//回溯
    137         }
    138     }
    139     //把剩下的牌出完
    140     for(int i=2;i<=15;i++)
    141     {
    142         if(sum[i]) 
    143         {
    144                 x++;
    145         }
    146     } 
    147     ans=min(ans,x);
    148 }
    149 int main() 
    150 {
    151     scanf("%d%d",&T,&n);
    152     while(T--)
    153     {
    154         ans=1<<30;//搞大一点
    155         int x,y;
    156         memset(sum,0,sizeof sum);//多次询问,记得清零
    157         for(int i=1;i<=n;i++)
    158         {
    159             scanf("%d%d",&x,&y);
    160             if (x==0) 
    161             {
    162                 sum[15]++;//把两张王存在一起(但是带牌的时候注意不要做对儿)
    163             }
    164             else 
    165             if(x==1)
    166             {
    167                 sum[14]++;//由于A的牌值大所以往后放
    168             }
    169             else sum[x]++;//其他牌存在相应位置
    170         }
    171         dfs(0);//开始暴搜
    172         printf("%d
    ",ans);
    173     }
    174 }

    然后就可以把这道NOIP的简单题拿满啦!!!!!

    然后,洛谷就有了对这题数据不服的一群大佬搞了这题(传送门

    然后去交了一发,就出事了。。。。。

     能卡成这样,还是有些惊喜的。。。。。

    由此可见:

    • NOIP数据水的可怕----陈彦儒老师最后一天原话
    • 用心出题目,用脚造数据--------zymdalao听完后的感叹

    对于增强版,他主要就把贪心给卡了!!

    对于原题像

    4  4  4  8   8   8  K   K  K

    会出三次三张牌的,然而可以这样出   4 4 4 8 8和  K K K 8这样就只要两步。

    不用贪心,还能用啥?然后去看了看题解,发现了是DP,方程式还不是一般的多(蒟蒻表示推不出来)!!!!然后就在一对DP中发现了还有用贪心的(还是对的!!

    • 同样还是贪心打散牌,这也是不超时的原因。对于上面的贪心就会存在一些问题,增强后要考虑拆牌,王,也算是单排,可以当对子出。
    • 拆牌(重点): 什么时候可以拆呢?
    1. 被四带的时候不能拆,这样可能会多出一次。
    2. 在单牌和对牌很多时,三张和炸弹不可以拆,会多打,可能多很多次。
    3. 反过来,单牌和对牌不是很多时,不就可以拆了吗?当单牌和对牌比三张和炸弹少时,就可以拆了。。。
    4. 证明(没什么用):这时不拆会没得带,例如单牌只能单出,如果把三张拆成一单一对,让其余和炸弹一起走,就会少一步
    5. 举个栗子:  3 3 3   +7    and   9 9 9    and  5 5 5 5   ----->3  3  3  +9  9  and    5 5 5 5+9+7 

                                                        原来       3步                                            后来     2步

             四张是一样的。

     愉快的代码环节(因为尝试新的打法,所以和原题的打法不太一样

      1 #include<iostream>
      2 #include<cstdio>
      3 #include<cstring>
      4 using namespace std;
      5 int n,t,ans;
      6 int pai[20];//存牌 
      7 int san_pai();
      8 void feiji(int step);
      9 void shunzi(int step);
     10 void liandui(int step);
     11 void chupai(int step) {      //开始出牌 
     12     if(step>=ans)
     13     return ;   //最优性剪枝 
     14     int tmp=san_pai();      //打散牌 
     15     ans=min(tmp+step,ans); //更新最优解 
     16     feiji(step);  //飞机 
     17     shunzi(step); //顺子 
     18     liandui(step);//连对 
     19 }
     20 void feiji(int step)//出飞机 
     21 {
     22     int l,end;
     23     for(int st=3;st<=13;++st) //枚举连续牌起始点 
     24     {
     25         l=0;
     26         while(pai[st+l]>=3)
     27         l++;//找出最大长度 
     28         for(int j=l;j>=2;--j) 
     29         {//枚举出牌长度 
     30             end=st+j-1;             
     31             for(int k=st;k<=end;k++)
     32             pai[k]-=3;//出飞机 
     33             chupai(step+1);//继续出牌 
     34             for(int k=st;k<=end;k++)
     35             pai[k]+=3;//搜索回溯 
     36         }
     37     }
     38     return;
     39 }
     40 void liandui(int step) 
     41 {//连对 
     42     int l,end;
     43     for(int st=3;st<=12;++st) 
     44     {//枚举连续牌起始点 
     45         l=0;
     46         while(pai[st+l]>=2)l++;//找出最大长度 
     47         for(int j=l;j>=3;--j) {//枚举出牌长度
     48             end=st+j-1;
     49             for(int k=st;k<=end;k++)
     50             pai[k]-=2;//出连对 
     51             chupai(step+1);//继续出牌
     52             for(int k=st;k<=end;k++)
     53             pai[k]+=2;//搜索回溯
     54         }
     55     }
     56     return;
     57 }
     58 void shunzi(int step) //顺子 
     59 {
     60     int l,end;
     61     for(int st=3;st<=10;++st) 
     62     {//枚举连续牌起始点 
     63         l=0;
     64         while(pai[st+l]>=1)
     65         l++;//找出最大长度 
     66         for(int j=l;j>=5;--j) 
     67         {
     68             end=st+j-1;
     69             for(int k=st;k<=end;k++)
     70             pai[k]-=1;//出顺子 
     71             chupai(step+1);//继续出牌
     72             for(int k=st;k<=end;k++)
     73             pai[k]+=1;//搜索回溯
     74         }
     75     }
     76     return;
     77 }
     78 int san_pai() {//贪心打散牌 
     79     int zs[5],num=0;
     80     memset(zs,0,sizeof(zs));
     81     bool wangzha=false;        
     82     if(pai[1]==2)
     83     wangzha=true;//是否有王炸 
     84     zs[1]+=pai[1];            //王算单牌 
     85     for(int i=2;i<=14;++i)zs[pai[i]]++;//统计个数 
     86     /******  暴力出奇迹 , N!过样例  ******/
     87     while(!zs[3]&&zs[1]==1&&zs[2]==1&&zs[4]>1)
     88     zs[4]-=2,zs[1]--,zs[2]--,num+=2;//神特判 
     89     //把一个炸拆成3张和单牌,再出一组四带二单和三带一 
     90     while(!zs[2]&&zs[1]==1&&zs[4]==1&&zs[3]>1)
     91     zs[3]-=2,zs[1]--,zs[4]--,num+=2;//神特判 
     92     //把一组三张拆成一对和一单,再出一组四带二单和三带二 
     93     if(zs[3]+zs[4]>zs[1]+zs[2])//三四张的比单牌和对牌多,拆着打 
     94         while(zs[4]&&zs[2]&&zs[3])
     95         zs[2]--,zs[3]--,zs[1]++,zs[4]--,num++;//拆三张,4带两对余一单 
     96     if(zs[3]+zs[4]>zs[1]+zs[2])//还多继续拆 
     97         while(zs[4]&&zs[1]&&zs[3])
     98         zs[1]--,zs[3]--,zs[2]++,zs[4]--,num++;//拆三张,4带两单余一对 
     99     while(zs[4]&&zs[1]>1)
    100     zs[4]--,zs[1]-=2,num++;//四带两单 
    101     while(zs[4]&&zs[2]>1)
    102     zs[4]--,zs[2]-=2,num++;//四带两对 
    103     while(zs[4]&&zs[2]  )
    104     zs[4]-- ,zs[2]--,num++;//对看成两单再四带 
    105     if(zs[3]%3==0&&zs[1]+zs[2]<=1)                //三张的太多了拆三张 
    106         while(zs[3]>2)
    107         zs[3]-=3,num+=2;//把一组三张拆成单和对,再出三带一和三带二 
    108     while(zs[3]&&zs[1]  )
    109     zs[3]-- ,zs[1]--,num++;//三带一 
    110     while(zs[3]&&zs[2]  )
    111     zs[3]-- ,zs[2]--,num++;//三带二 
    112     //还剩三张和炸,组合出 
    113     while(zs[4]>1&&zs[3])
    114     zs[3]--,zs[4]-=2,num+=2;//把一个炸拆成一对和两单,再出三带二和四带两单 
    115     while(zs[3]>1&&zs[4])
    116     zs[4]--,zs[3]-=2,num+=2;//把一个炸拆成两对,再出两组三带一对 
    117     while(zs[3]>2)
    118     zs[3]-=3,num+=2;                //同上,把一组三张拆成单和对,再出三带一和三带二
    119     while(zs[4]>1)zs[4]-=2,num++;                //把一个炸拆成两对,再出一组四带两对 
    120     if(wangzha&&zs[1]>=2)//有王炸并且没被带跑 
    121         return num+zs[2]+zs[3]+zs[4]+zs[1]-1;//双王一块出 
    122     else
    123         return num+zs[1]+zs[2]+zs[3]+zs[4];//出剩余的牌,返回答案 
    124 }
    125 int main() 
    126 {
    127     cin>>t>>n;
    128     int a,b;
    129     while(t--) 
    130     {
    131         ans=1<<30;
    132         memset(pai,0,sizeof(pai));
    133         for(int i=1; i<=n; ++i) 
    134         {
    135             cin>>a>>b;
    136             if(a==1)
    137             pai[14]++;    //14代表A 
    138             else 
    139             if(a==0)
    140             {
    141                 pai[1]++;//1代表王
    142             }
    143             else 
    144             pai[a]++;
    145         }
    146         chupai(0);
    147         printf("%d
    ",ans);
    148     }
    149 }

     最后预祝BK201    AK   IOI,还有希望shymm早日攒够350块钱,可以随时去这里水一下

  • 相关阅读:
    Lucene
    coreseek
    Sphinx学习之sphinx的安装篇
    在Hive中使用Avro
    Hive中实现group concat功能(不用udf)
    Fresnel Reflection
    安装Windows更新程序遇到错误:0x80070422
    float4与half4数据类型
    Dijkstra算法(三)之 Java详解
    地图四叉树一般用在GIS中,在游戏寻路中2D游戏中一般用2维数组就够了
  • 原文地址:https://www.cnblogs.com/2529102757ab/p/11298520.html
Copyright © 2011-2022 走看看