声明:
本文很长,并且最终不会有完整代码,仅提供算法;
本文知识点中等偏难,但是看懂和学会之后有一定好处;
本文绝对不是最优解,并且很多步骤待封装成函数,这里逐一分析;
本文纯属个人刷题方法和总结,如有错误,欢迎指正;
如果你做的时候有些吃力,真正想学点什么,请耐心看完,看懂。
题目如下:
背景:两个人每人发3张牌(各从一副牌中),每张牌包括花色(红桃(Heart)>黑桃(Spade)>方块(Diamond)>梅花(Club))和大小(从小到大依次是:2-10、J、Q、K、A),胜负规则如下:同花顺(3张同花色的连牌,先比大小,再比花色,后同)>炸弹(3张相同大小的牌)>连牌(3张不同花色的连牌)>对子(两张相同大小的牌)>单牌。例如,红桃QKA>黑桃QKA>梅花567>方块234>AAA(红桃、方块、梅花)>AAA(黑桃、方块、梅花)>JQK(红桃、红桃、方块)>JQK(黑桃、红桃、方块)>AA2(梅花黑桃梅花)>QQJ(红桃梅花方块)>JQA(红桃红桃红桃)。注:A23不算连牌。
输入:A的3张牌(未排序)和B的3张牌(未排序)。
输出:A的3张牌的排序后的输出和B的3张牌的排序后的输出,以及A和B谁获胜。
分析:
这里继承了上一题,也就是Poker time的部分内容,这里不多赘述那题的做法,如果有需要的话可以私聊我,我视情况出第一题的算法。
首先是排序,这里的最终排序取决于取决于牌的大小而不是花色(*);其次是胜利条件的判断,有明显的分级,对于每一级内部也有严格的先大小后花色的判断顺序(**);再者,顺子的QKA是否合理,这里没有给出解释,也是留了一手,不然题目的判断会更恶心(***);最后是让人最头疼的10,这个数字在第一题就作为最后一个隐藏示例存雷。
不过有10必有‘1’,只需要找到这个1就行。
这次编写,我不知道隐藏示例都是什么,主要是我一次就过了,除了一个本身就是错误的示例(凡尔赛)。
算法正文:
零、参数声明
这里给出本文主要的参数
char a[20]; // 这里给20,防一下 10 的炸弹
char b[20];
gets(a);
gets(b);
int i=0;
int j=0;
int flag=1; // 循环进行指标
int flag1=1; // 是否双赢指标
int flag2=0; // 判断 A 赢
int flag3=0; // 判断 B 赢
一、建立在排序基础上的整体数组构造以及参数读入
由于最终需要对牌进行先大小后花色的排序,我这里使用的是二位数组
为什么定义成13*4呢? 例如:HA 就在数组 [0][0]的位置
我的思路是:13行存储牌,4列存储花色,数字大、花色大的牌前,最后一次遍历输出牌序。
这里需要将flag的指标置零,如果直接 return 0 也可以。
int As[13][4]={0};
int Bs[13][4]={0};
于是,可以对数组内读入参数,这里只给出其中一个字符串的数据读入
while(a[i]!='\0'&&flag) // A 的数据填充
{
if(a[i]==' ') // 跳过空格
{
i++;
continue;
}
if(a[i]=='H'||a[i]=='S'||a[i]=='D'||a[i]=='C')
{
int m=0; // 列指标
switch(a[i]) // 判断花色
{
case 'H':m=0;break;
case 'S':m=1;break;
case 'D':m=2;break;
case 'C':m=3;break;
}
int n=0; // 行指标
//往后是 i+1 因为现在的 i 指向的是花色,往后以为才是牌的内容
// 向二维数组中填充数据
if(isdigit(a[i+1])||a[i+1]=='A'||a[i+1]=='J'||a[i+1]=='Q'||a[i+1]=='K')
{
if(isdigit(a[i+1])&&a[i+1]!='1') //判断2~9,小的在下往上填
{
n=12-(a[i+1]-'0'-2); // 回忆一下 ascii码 就知道这步在干啥
}
else if(a[i+1]=='A') // 其他牌的读入
{
n=0;
}
else if(a[i+1]=='K')
{
n=1;
}
else if(a[i+1]=='Q')
{
n=2;
}
else if(a[i+1]=='J')
{
n=3;
}
if(a[i+1]=='1'&&a[i+2]!='\0'&&a[i+2]=='0') // 判断是不是 10
{
n=4;
}
i+=n==4?3:2; // 结合 i 的指向,如果不是10就跳过两位,是10就跳过三位
}
else // 不符合的剔除
{
printf("Input Error!\n");
flag=0;
flag1=0;
break;
}
if(As[n][m]==0) // 如果没有重复的牌,那就可以在数组中标记这张牌
{
As[n][m]++;
}
else // 否则报错
{
printf("Input Error!\n");
flag=0;
flag1=0;
break;
}
}
else
{
printf("Input Error!\n");
flag=0;
flag1=0;
break;
}
}
二、建立在先大小后花色基础上的排序
是插入过程的逆,重新输出符合要求的字符串
int k=0;
for(i=0;i<13;i++)
{
for(j=0;j<4;j++)
{
if(As[i][j]==1) //判断有没有这张牌
{
switch (j) // 以下的选择与刚才插入的时候刚好相反,属于反操作
{
case 0:a[k++]='H';break;
case 1:a[k++]='S';break;
case 2:a[k++]='D';break;
case 3:a[k++]='C';break;
}
switch (i)
{
case 0:a[k++]='A';break; // 根据牌所在的行数回推牌的内容
case 1:a[k++]='K';break;
case 2:a[k++]='Q';break;
case 3:a[k++]='J';break;
case 4:a[k++]='1';a[k++]='0';break;
default:a[k++]=14-i+'0'; // 2~9 转回字符
}
a[k++]=' ';
}
else
continue;
}
}
a[--k]='\0'; // 数组结尾,最好加上,免得访问越界
三、建立在规则上的初判断与初步输赢判定
这里初判断 a 和 b 的牌型,如果牌型不同,直接就可以输出唯一胜者
这里粗返回的 0~4 可以初步判断胜者,或者仍需比较
这里返回的0~4可不是乱来的,后面有用到
函数如下:
// 返回差,类似与冒泡排序,前一个与后一个比,根据差调整数组中元素的位置
// 打一下,试一试就会了,这个函数,很容易上手
int cmp(const void*e1,const void*e2) // 比较方式
{
return *(int*)e1-*(int*)e2;
}
int search(int a[13][4]) // 初步判断牌的类型
{
int i=0;
int j=0;
int c[3]={0}; // 创建新的数组,存储牌的 行指标 !!!
int current=0; // 因为 行指标 标记了牌的大小
for(i=0;i<13;i++) // 遍历查找牌
{
for(j=0;j<4;j++)
{
if(a[i][j]==1)
{
// 同花顺,占据连续的一列中的三个,最易判断
if(a[i+1][j]==1&&a[i+2][j]==1&&i<=10) // 这里i防止越界访问
{
return 4;
}
c[current++]=i;
}
else
continue;
}
}
qsort(c,3,sizeof(int),cmp); // 对牌进行排序,这里使用了 qsort 函数
// qsort(数组名,元素个数,元素大小,比较函数)
if(c[0]==c[1]&&c[1]==c[2]) // 炸弹(全部相等)
{
return 3;
}
else if(c[2]-c[1]==1&&c[1]-c[0]==1) // 顺子(等差数列)
{
return 2;
}
else if(c[0]==c[1]||c[1]==c[2]) // 对子
{
return 1;
}
return 0; // 啥也不是,只有单牌
}
判断是否有胜者:
if((flag2>flag3||flag2<flag3)&&flag1) // 唯一胜者,且获胜方式单纯
{
if(flag2>flag3)
{
printf("Winner is A!\n");
}
else if(flag2<flag3)
{
printf("Winner is B!\n");
}
printf("A: %s\n",a); // 打印排序后的牌内容
printf("B: %s\n",b);
}
else if(flag2==flag3&&flag1) // 需要下一步比较
四、建立在再次判断上的函数指针数组及其调用
函数指针数组:一个数组(定语),数组内元素是函数指针(状语)
故名思意:根据牌型,选择不同的函数来判断最终的胜利,也称回调函数
int (*pfarr[4])(char c[15],char d[15])={nmsl,wdnm,cnmd,mdzz};
// *pfarr[4] 指针数组,运算符优先级决定它先与 [4] 结合成为数组
// (char c[15],char d[15]),参数类型,类似于函数的参数声明
// {nmsl,wdnm,cnmd,mdzz},函数名,我乱起的,没别的意思
// 总的意思就是,一个数组,里面四个元素,分别指向一个独立的函数
else if(flag2==flag3&&flag1) // 需要下一步比较
{
if(flag2) // 非零
{
int (*pfarr[4])(char c[15],char d[15])={nmsl,wdnm,cnmd,mdzz};
switch(flag2)
{
case 4: // 这里的调用都是已经排序过的牌的字符串
t=pfarr[0](a,b);break; // 函数调用类似于访问数组的元素
case 3: // 不过记得带上函数的参数调用,就是后面的 (a,b)
t=pfarr[1](a,b);break;
case 2:
t=pfarr[2](a,b);break;
case 1:
t=pfarr[3](a,b);break;
}
if(t==0)
{
printf("Winner is A!\n");
}
else if(t==1)
{
printf("Winner is B!\n");
}
else
{
printf("Draw!\n");
}
}
else // 都是单牌,比较方法就是第一题的全部内容,不说了
{
int count=0;
for(i=0;i<4&&flag;i++) // 对比两个数组判断输赢
{
for(j=0;j<13&&flag;j++)
{
if((As[i][j]==0&&Bs[i][j]==0))
{
continue;
}
else if(As[i][j]==1&&Bs[i][j]==1)
{
count++;
if(count==3)
{
printf("Draw!\n");
goto end;
}
}
else if(As[i][j]>Bs[i][j])
{
printf("Winner is A!\n");
flag=0;
break;
}
else if(As[i][j]<Bs[i][j])
{
printf("Winner is B!\n");
flag=0;
break;
}
}
}
}
五、建立在近似判断机制上的四个函数
刚才我们设定了四个函数,分别是{nmsl,wdnm,cnmd,mdzz},现在需要一一实现他们,达到我们预期的不同情况下的比较效果。
// 同花顺 判断
int nmsl(char a[15],char b[15])
{
int i=0;
if(strlen(a)==8) // 判断有没有万恶的 10,这里显然没有
{
for(i=0;i<15;i++)
{
if(a[i]==b[i]) //相同跳过
{
continue;
}
else
{
if(a[i]=='H'&&b[i]!='H') // 花色碾压
{
return 1;
}
else if(a[i]!='H'&&b[i]=='H')
{
return 0;
}
else if(a[i]=='A'&&b[i]!='A') // 谨记,有A就没有10,这里是大小碾压
{
return 0;
}
else if(a[i]!='A'&&b[i]=='A')
{
return 1;
}
else // 其他牌的大小碾压
{
return b[i]-a[i]?1:0;
}
}
}
}
else // 如果有万恶的10
{
for(i=0;i<15;i++)
{
if(a[i]==b[i])
{
continue;
}
else
{
if(a[i]=='H'&&b[i]!='H')
{
return 1;
}
else if(a[i]!='H'&&b[i]=='H')
{
return 0;
}
else if(a[i]=='1'||b[i]=='1') // 出现了 10 !
{
if(a[i]=='1')
{
return b[i]<=57?0:1; // 10 跟另外一张牌比较,这里57,代表字符'9'
} // 细想一下,是不是也合理?
else
{
return a[i]<=57?1:0;
}
}
else // 其他牌的大小碾压
{
return b[i]-a[i]?1:0;
}
}
}
}
return 2;
}
其余的判断都是建立在刚才判断的基础上的,可以说比较部分几乎完全一致。
这里多说一句,对子的判断,可以先找到是哪一对,创建一个两个元素的数组或者字符串,存下来这一个对子中的一张牌,然后跟另一个对子中的一张牌进行 皇城PK 即可。
这里不给出余下的三个函数了,有心的同学看懂上面的思路一定信手拈来,不就是三个函数嘛?!何况这里已经给出了最核心的比较算法了。
六、写在最后的话
我不否认出这道题的人有很多没有考虑到的地方,题目出的不是很严谨,但是在现有知识点的使用上,这绝对是综合的难题(就我的算法而言)
如果你说你用一次遍历,哈希遍历,等等,那你可以完美的优化这道题的全部算法;
但是就我而言,我希望写可读性更高的代码,不是说我不会写高效代码,hashmap我也可以写,但是这只是独善其身,不是每一个学C的人一开始就可以写0ms,2Mb的代码。
如果你觉得这很难,那很抱歉,我已经尽我的一切可能,写最详细的注释,写最简单的代码了,谁又能刚开始一看就懂呢?
如果你说,这博主怎么这么扣,这也不写,那也不赘述。
不好意思,我都写了,是时候举一反三了。
最后的最后,码字不易,要是本文多少学了点东西的友友们,点个赞让我看一下呗,至少我会觉得我的付出可以让大家获益