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-41−4 来表示;小王的表示方法为 0101 ,大王的表示方法为 0202 。
输出格式
共 TT 行,每行一个整数,表示打光第 ii 组手牌的最少次数。
输入输出样例
8
7 4
8 4
9 1
10 4
11 1
5 1
1 4
1 1
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
说明/提示
样例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\n",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中发现了还有用贪心的(还是对的!!)
- 同样还是贪心打散牌,这也是不超时的原因。对于上面的贪心就会存在一些问题,增强后要考虑拆牌,王,也算是单排,可以当对子出。
- 拆牌(重点): 什么时候可以拆呢?
- 被四带的时候不能拆,这样可能会多出一次。
- 在单牌和对牌很多时,三张和炸弹不可以拆,会多打,可能多很多次。
- 反过来,单牌和对牌不是很多时,不就可以拆了吗?当单牌和对牌比三张和炸弹少时,就可以拆了。。。
- 证明(没什么用):这时不拆会没得带,例如单牌只能单出,如果把三张拆成一单一对,让其余和炸弹一起走,就会少一步
- 举个栗子: 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\n",ans); 148 } 149 }
最后预祝BK201 AK IOI,还有希望shymm早日攒够350块钱,可以随时去这里水一下