关于如何评价洗牌质量的猜想
关于如何评价洗牌质量的猜想
洗牌算法是卡牌类游戏中必须使用的算法,本质上说洗牌算法的目的是使某个给定的顺序更加的无序,因此出现了很多种洗牌算法。我们不重点讨论如何洗牌,我们将眼光关注于洗出的牌是否达到我们预期的要求,以及如何衡量洗出的牌无序的程度。首先先看一个简单有效的洗牌算法。
一、一个简单的洗牌算法
一个比较容易实现的洗牌算法是这样的,通过随机选出两张牌进行交换,通过多次这样的重复操作,就能达到洗牌的目的。事实证明这种洗牌方式还是比较可行,最重要的是比较简单,代码如下。
template<class T>
void mess(T data[],unsigned int len)
{
int i=len,j,k;
T t;
srand((unsigned int)time(0));//随机因子
while(i--)//交换牌数次数
{
while(true)//找出两张不一样的牌
{
//随机产生两张牌
j=rand()%len;
k=rand()%len;
if(j!=k)//不是同一个元素
{
//交换
t=data[j];
data[j]=data[k];
data[k]=t;
//退出进行下一次交换
break;
}
}
}
}
这种算法通过对有序的(假设开始洗牌时是有序的,并以此为参考)N张牌进行N次随机交换,事实达到了洗牌的目的。以下是一个20位有序牌几次洗牌后的结果:
虽然得到了我们想要的洗牌效果,但是我们却无法定量的衡量洗出牌的质量。换句话说就是如何确定洗出的牌究竟乱成什么样子?为了验证洗牌的质量,必须给出评价洗出的牌的一个定量的分析。
二、如何评价洗出牌的质量
牌洗出什么样子才算比较好,当然是越乱越好。但是这么说其实并不全面,在实际的卡牌游戏中,我们要达到的目的只需要保证下局游戏时洗出的牌要和洗牌之前的情形变化很大即可,这个就是洗牌算法的本身需要考虑的问题。假如给我们一副新的牌,内部是按照某个顺序排列的,洗牌算法要达到的目的是尽量让它混乱,但是混乱的结果如何呢?所以不论在任何情况下,计算出牌的混乱程度都是必须的,它可以为我们的进行其他流程提供参考。因此,归根结底还是需要讨论洗出牌的混乱程度。为了方便讨论这个问题,我们有个基本假设:如果按照某个顺序,无论是升序还是降序,这种顺序的牌的混乱程度应该定义为0。很明显,有序的牌是不混乱的,那么下边就需要讨论混乱的牌的混乱程度怎么计算。
继续讨论之前,首先我们定义一个新的名词——混乱度[Degree of Chaos](无序度),记为Ch。这个概念类似于物理学中的熵。然后,我们把牌抽象为一个一维数组,数组初始值是按照自然数有序的,即1、2、3、4、5……这样,我们讨论的问题就变成了对一个无序数组的处理并得到一个混乱度的问题了。混乱度如何定义才比较合适呢?结合上述的洗牌算法,我有个大胆的猜想,给出混乱度的定义:
定义:无序序列通过交换两个内部元素还原为有序序列需要的最小次数。
这里定义有两个关键点,一个是通过交换两个元素还原为有序,另一个是最小次数。前者说明了评价无序序列的方式,即通过还原序列为有序进行交换元素,后者说明了若一个序列越混乱则越难还原为有序的序列,需要的次数越多,同时包含了还原为升序和降序序列需要的最小次数。
例如:对于序列A=(1,4,2,3,5),需要交换<3,4>、<2,3>两次才能还原为有序序列A0=(1,2,3,4,5)。当然还有其他的还原步骤,也可以还原为B0=(5,4,3,2,1)。但是不管怎么还原,需要的步数最好是2,即Ch(A)=2。
虽然能按照这种方式得到混乱度,但是如何通过定量的计算得到混乱度呢?因为这直接关系着程序设计的能否实现的问题。
三、混乱度的计算Ch(A)
计算混乱度之前,首先我们需要思考混乱度的定义。混乱度强调最少交换次数变为有序序列。在算法设计中,排序是很经典的问题了,排序算法数不胜数。我们期待的是能否通过排序算法的计算得到混乱度。既然是最少交换次数,我们首先想到的或许是快速排序算法,毕竟它是目前性能最好的排序算法。但是经过验证它并不合适,单纯的将元素按照中轴元素进行分割只会加大交换次数。然后我们尝试经典的排序算法——冒泡排序,事实证明也不行,因为冒泡排序每次都要进行交换,做了很多无用功。最后我们会想到选择排序,它的算法性能并不好,但是经过我们的验证,它是最可能接近我们期待答案的排序算法。为什么呢?改进后的选择排序算法思想大致可以这么描述:通过按序将数组的某一个元素与后边的元素比较,最后拿到最小(大)元素与本身交换,最终达到有序。假设数组大小是N,则根据选择排序算法,最大的交换次数不会大于N-1(最后一个元素不需要交换)。还拿刚才那个例子,A=(1,4,2,3,5),按照选择排序算法,通过交换<4,2>、<4,3>也能达到有序,而且也是2步!看起来选择算法能计算出我们想要的混乱度,通过下边的实验可以求出任意序列的混乱度,并能进一步验证我们的猜想。
首先,我们需要通过选择排序计算混乱度。算法大致如下:
//暂时使用选择排序交换的次数进行计算,捎带验证
template<class T>
int messDegree(const T data[],const unsigned int len)
{
T*upData=new T[len];//升序数据
T*downData=new T[len];//降序数据
int degree;//混乱度
unsigned int i,j,upK,downK,upSum=0,downSum=0;
T t;
//拷贝数据
for(i=0;i<len;i++)
{
upData[i]=downData[i]=data[i];
}
//排序计算
for(i=0;i<len-1;i++)
{
upK=downK=i;
for(j=i+1;j<len;j++)
{
if(upData[j]<upData[upK])//有更小的
upK=j;
if(downData[j]>downData[downK])//有更大的
downK=j;
}
if(upK!=i)
{
t=upData[i];
upData[i]=upData[upK];
upData[upK]=t;
upSum++;//计算升序交换次数
}
if(downK!=i)
{
t=downData[i];
downData[i]=downData[downK];
downData[downK]=t;
downSum++;//计算降序交换次数
}
}
degree=upSum<downSum?upSum:downSum;//取最小值
delete[] upData;
delete[] downData;
return degree;
}
对通过选择排序得到的结果是否就是混乱度进行证明并不是很容易,本人知识有限,希望有高人出现帮我证明这个命题。下边讨论内容的前提是默认选择排序的方法是正确的。
四、最大混乱度Ch(N)
我们得到了混乱度的计算方法,针对有序序列A0=(1,2,3…),混乱度Ch(A0)=0。但是对于N维序列A,它的最大的混乱度Ch(N)是多少,这个能否计算呢?要知道,按照混乱度的定义,最大的混乱度代表着序列的最无序的状态,相应的也就代表牌被洗得不能再洗的情况,所以,最大混乱度有它实际的含义。但是计算最大混乱度目前还没有比较直接的公式可以使用,既然如此,我们就使用计算机枚举所有可能的序列来计算一部分最大混乱度。
我们将计算混乱度的算法嵌入到n阶排列算法中,于是得到所有的n!个排列对应的混乱度。由于我们只关心最大的混乱度Ch(N),所以算法只记录了Ch(N)。
template<class T>
void perm(T data[],int k,int m,int &max,int len)
{
if(k==m)//一个排列出现
{
int t=messDegree(data,len);//计算混乱度
if(max<t)
max=t;//记录最大混乱度
return ;
}
else
{
for(int i=k;i<=m;i++)
{
int t=data[i];
data[i]=data[k];
data[k]=t;
perm(data,k+1,m,max,len);
t=data[i];
data[i]=data[k];
data[k]=t;
}
}
}
对于N维序列,枚举出所有的可能序列是个排列问题,时间复杂度是O(n!)。因此算法的复杂度相当高,受限于机器能力,我只测试到N=11时对应的Ch(N)。由于3个元素一下的序列不存在混乱的问题,很明显Ch(1)=Ch(2)=0。所以讨论N≥3情况下的Ch(N)才有实际意义。以下是枚举所有情况得到的Ch(n):
Ch(3)=1、Ch(4)=3、Ch(5)=4、Ch(6)=4、Ch(7)=5、Ch(8)=7、Ch(9)=8、Ch(10)=8、Ch(11)=9。
得到这个结果其实并不令人满意,本想从中找到一些规律来更好地计算最大混乱度的希望破灭了。
五、总结
本文从洗牌算法中引申出如何评价洗出的牌质量的方法,首先引入概念——混乱度,然后提出通过按照选择排序算法进行计算混乱度的猜想,最后使用穷举的方式求解了简单的序列的最大混乱度Ch(N)。遗憾的是笔者并没有给出选择排序算法计算混乱度是否正确的形式化证明和最大混乱度的简便计算方法。希望基于笔者的想法,有能人异士帮助笔者帮助解决这两个问题,必不胜感激!(——Florian fanzhidongyzby@163.com)
若本文对你有所帮助,您的 关注 和 推荐 是我分享知识的动力!