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块钱,可以随时去这里水一下

原文地址:https://www.cnblogs.com/2529102757ab/p/11298520.html