最短带权路径问题的解法::Dijkstra & Floyd

三月 16, 2007 by admin  
类别:生活匣子

  在一个网络中,如果两个结点之间有直接的因果关系,则这两个结点直接连通,在连接
两个结点的弧上标上它的代价或权,值得注意的是这样的代价不一定是对称的,即A到B
的代价不一定等于B到A的代价,实际问题中以行船为例,有顺水和逆水的区别。在图G
中,给出两个结点求这样一条最短的路径,使经过这条路径上的代价之和最小,这就是最短
路径问题。
  如果所有弧上的权都相等,则问题退化为求两个结点间的一条路径使经过的中间结点最
少。比如在一个城市的交通网络中,乘客关心的可能只是旅途中中转的次数,只希望转更少
次数的车。对于这样的问题,运用BFS对图进行层次遍历,可以在O(n)的时间复杂度上
完成搜索,考虑到BFS的层次遍历性质,不难理解,在此不讨论。
  考虑到带权的有向图(无向图可看成特殊的有向图),如何求最短路径呢?
1、 Dijkstra算法
  Dijkstra是最早提出这个算法的大牛:)。这是图论中非常有名的一个算法。
  图采用邻接矩阵的形式描述,M[i][j]表示结点i到结点j间的代价,如果没有直接因果
关系,则为无穷大,计算机中可以用一个很大的数据代替。后面的Floyd算法同样这样描述。
  差点忘了,dijkstra算法只能求出从结点i到其它各结点的最短路径。算法引入这样两
个集合S和T,S是那些已经确定了到I结点的最短路径的结点,在计算过程中为什么可
以确定某些结点已经到达了最短的要求了呢?后面再讲。T为全集U和S的差集,即那些还
未确定最短路径的结点。而且S的初值是{i},T的初值是U-{i}。另外再引入一个标记数组
D[N],其中在某一步D[k]表示当前从i到k的较短路径,D[k]的初值为M[i][k],整个算法
过程如下:
  1、 在T中选择一个D[k]最小的结点k,将k并入S,并从T中去掉,如果T为{}则
  转到3;
  2、 用k结点和T中其余结点进行一遍比较,如果D[i]>D[k]+M[k][i],则用D[k]+M[k][i]
  取代原来的D[i],重复1;
  3、 算法结束,此时D[k]中保存的就是从I到k结点的最短路径。
  算法就以这样非常简单的形式完成了求解,时间复杂度是O(n^2),确定了从I到其余
各结点的最短路径,如果要求记录路径,也很容易,可以引入一个路径标记数组P[k][m],
如果P[k][m]为0则说明从i到k的当前最短路径中不含有m结点作为中间结点,如果为1
则m成为其路径的所经结点。
  就前面提出为什么可以这样一部分一部分的确定解集S,举个例子,第一遍比较,得到
了能够直接抵达的最小代价的结点s0,为什么s0不可能含有其它路径了呢?假设从i到s0
并不是最短路径,则至少含有不是s0的结点k,使i->k->…->s0比原路径更短,而此路径比
i->k大,i->k又比i->s0大,所以不可能比i->s0小,矛盾,得证。再证在算法第k+1步时,
前一次求得了由至多k步可到达的最短路径终结点sk,有i?sk最短,在与后面剩下T中结
点的比较中,如果D[k]+M[k+1][i]更短,则替换,替换的原则是在不能与某条至多m(m<k)
步可到达的最短路径替换,为什么?在前面的过程已经替代了,所以只考虑当前的D[k+1]。
  这只是我的理解,如果需要更加完善的证明还需用数学语言,略之。
与Dijkstra算法平分秋色的还有Floyd算法。
2、 Floyd算法
  如果要求所有两结点间的最短路径,则可以使用dijkstra算法n次完成,时间复杂度是
O(n^3),Floyd还提出过另一个算法,同样是O(n^3)的复杂度,但形式上更简单些。
  要求的解是一个矩阵S[N][N],其中S[i][j]表示结点i到j的最短路径,算法很像动态
规划算法,甚至更简单些,不同的是这里规划的是一个矩阵,而不是简单的数组。
  Floyd算法过程描述如下:
  1、 首先S以边集M初始化,得到所有的直接连通代价;
  2、 依次考虑第k个结点,对于S中的每一个S[i][j],判断是否满足:
  S[i][j]>S[i][k]+S[k][j],如果满足则用S[i][k]+S[k][j]代替S[i][j],此为第k步;
  3、 k循环取遍所有结点,算法结束时,S为最终解。
  这样的求法自然再简单不过了,但要证明其中的正确性,有些难度,为什么不会使结点
重复存在于同一路径?在替换的时候,S[i][k]和S[k][j]中如果同时含有m结点,则这样的替
换显然是错误的,相当于i–>m–>k–>m–>j,可以简单用这有这样连通关系的例子试一下,会
发现如果在一条路径中有结点冗余,则不可能是最短路径,显然上面这条有冗余结点的路径
肯定要比i–>m–>j要长,所以算法在要求最短的同时已经排除了这种情况,这样使得任意一
条路径都不冗余。

  最后想提一下旅行商问题,旅行商问题非常类似于最短路径问题,不同的是所求的是一
个环路,且环路还是要包含图中所有结点的,同样的要求是代价和最短。但旅行商问题是一
个NP问题,用动态规划的方法只能达到O(2^n)的复杂度。
  不过就实际应用来说,我觉得最短路径更实用,对于旅行商问题只需要求到某种近似程
度的近似解已经够用了,如果降低总程代价1%需要已经花费的时间的几倍,这样的努力是
多么的廉价。

                                                                   算法驿站  (rickone 的 BLOG)

满足任意概率密度函数分布的随机变量生成算法

三月 16, 2007 by admin  
类别:生活匣子

概率密度的定义
  概率简单地说就是在空间某处附近一确定的范围内发生某事件的可能性,而概率密度则是概率与空间范围之比的极限。在讨论更复杂一些的空间之前,可以从一维的线性空间出发定义它们。
  在一维空间中,用数轴表示所有的点,定义Ф(x)为:随机变量i,当i<x时的概率,因此得Ф(-∞)=0,Ф(+∞)=1(因为全概率为1),这个条件是直接由定义得出,称为归一化条件。
  由此当x取数轴上的点时,随机变量i取到[x,x+△x](△x>0)中时的概率应该为Ф(x+△x)-Ф(x),则定义i取到这个区间的平均概率密度p^=(Ф(x+△x)-Ф(x))/ △x,此时如果极限:
  lim(Ф(x+△x)-Ф(x))/ △x=p(x) 存在
  △ x->0
  则称极限值为这一点的概率密度,由数学知识这正是导数的定义,于是有p(x)=dФ/dx,于是有△Ф=∫p(x)dx,积分区域为△,虽然这是从一维情况推出,这是概率的一般形式定义。于是归一化条件就相应为∫p(v)dv=1,积分区域为全空间。
  由此概率密度函数就唯一决定了随机变量的分布,反过来我们也说随机变量服从某种概率密度函数的分布。

如何由p函数模拟随机变量的生成
  首先我们要有一个这样的随机函数,C的原型如下:
double random();/*返回一个大于等于0,小于1之间的等概率随机双精度小数*/
  
–>规格化的随机变量<–
  这个算法的实现将在以后给出算法,事实上笔者最开始学习编程时就是学的这种随机函数,它是BASIC里的标准函数,这里只是为了讨论的需要,因为由它可以非常方便的得到任意范围内的随机数,包括浮点数或整数。
  然后我们再拿来这样一个任意的概率密度函数p(x),且满足归一化条件∫p(x)dx=1,积分区域为[0,1)。最后再给出这个函数在[0,1)上的上限(最大值)h。然后用下面的方法生成服从该分布的随机变量。
  首先得到一个等概率的随机的二维的点(x,y),它落在矩形区域[0,1;0,h]内,同时我们还要求这样一个条件y<p(x),如果得到的点不满足这个条件则重新取点,也就是说把那些不满足此条件的点‘去掉’。最后我说这里的x满足p(x)的分布。证明如下:

证明
  首先由算法过程将得到在y=0,x=0,x=1,y=p(x)四条曲线围成的二维区域内的等概率随机点。
  任取x属于[0,1),则在x的一个邻域内[x,x+△x](取△x,使这个邻域也在[0,1)内),上述随机变量i取到这个邻域的概率记为△Ф(x),由几何性质△Ф(x)=S'/S,其中S'为这一区域围成的曲顶梯形的面积,而S为整个y=p(x)曲线围成的面积,由归一化条件马上知S=1于是△Ф(x)就是那块曲顶梯形的面积,由中值定理,有△Ф(x)=p(x') △x,其中x'为上面那个邻域内的某一点,现令△x->0,则由夹逼定理x'->x,于是得p(x)=dФ(x)/dx,这正是p(x)的定义,于是产生的随机变量即满足p(x)的分布。证毕。

C++语言算法描述
double random_rickone(double (*p)(double),double h)
{
   double x=random(),y=random()*h;
   if(y<p(x))return x
   return random_rickone(p,h);
}

算法驿站  (rickone 的 BLOG)

A*高效搜索算法

三月 16, 2007 by admin  
类别:生活匣子

    了解了基本搜索算法,下面就来看A*,神奇的A*。(—>搜索方法小结

A*是一种启发式搜索,一种有序搜索,它之所以特殊完全是在它的估价函数上,如果我要求的是从初始结点到目的结点的一个最短路径(或加权代价)的可行解,那对于一个还不是目标结点的结点,我对它的评价就要从两个方面评价:第一,离目标结点有多近,越近越好;第二,离起始结点有多远,越近越好。记号[a,b]是表示结点a到结点b的实际最短路径代价。设起始结点为S,当前结点为n,目标结点为G,于是n的实际代价应该是f*(n)=g*(n)+h*(n),其中g*(n)=[S,n],h*(n)=[n,G],对于是g*(n)是比较容易得到的,在搜索的过程中我们可以按搜索的顺序对它进行累积计算,当然按BFS和DFS的不同,我们对它的估价g(n)可以满足g(n)>=g*(n),大多可以是相等的。但是对于h*(n)我们却了解得非常少,目标结点正是要搜索的目的,我们是不知道在哪,就更不知道从n到目标结点的路径代价,但是或多或少我们还是可以估计的,记估价函数f(n)=g(n)+h(n)。

    我们说如果在一般的图搜索算法中应用了上面的估价函数对OPEN表进行排序的,就称A算法。在A算法之上,如果加上一个条件,对于所有的结点x,都有h(x)<=h*(x),那就称为A*算法。如果取h(n)=0同样是A*算法,这样它就退化成了有序算法。

A*算法是否成功,也就是说是否在效率上胜过蛮力搜索算法,就在于h(n)的选取,它不能大于实际的h*(n),要保守一点,但越接近h*(n)给我们的启发性就越大,是一个难把握的东西。

A*算法流程:
    首先将起始结点S放入OPEN表,CLOSE表置空,算法开始时:
1、如果OPEN表不为空,从表头取一个结点n,如果为空算法失败
2、n是目标解吗?是,找到一个解(继续寻找,或终止算法);不是到3
3、将n的所有后继结点展开,就是从n可以直接关联的结点(子结点),如果不在CLOSE表中,就将它们放入OPEN表,并把S放入CLOSE表,同时计算每一个后继结点的估价值f(n),将OPEN表按f(x)排序,最小的放在表头,重复算法到1

最短路径问题,Dijkstra算法与A*
A*是求这样一个和最短路径有关的问题,那单纯的最短路径问题当然可以用A*来算,对于g(n)就是[S,n],在搜索过程中计算,而h(n)我想不出很好的办法,对于一个抽象的图搜索,很难找到很好的h(n),因为h(n)和具体的问题有关。只好是h(n)=0,退为有序搜索,举一个小小的例子:

与结点写在一起的数值表示那个结点的价值f(n),当OPEN表为空时CLOSE表中将求得从V0到其它所有结点的最短路径。考虑到算法性能,外循环中每次从OPEN表取一个元素,共取了n次(共n个结点),每次展开一个结点的后续结点时,需O(n)次,同时再对OPEN表做一次排序,OPEN表大小是O(n)量级的,若用快排就是O(nlogn),乘以外循环总的复杂度是O(n^2logn),如果每次不是对OPEN表进行排序,因为总是不断地有新的结点添加进来,所以不用进行排序,而是每次从OPEN表中求一个最小的,那只需要O(n)的复杂度,所以总的复杂度为O(n*n),这相当于Dijkstra算法。在这个算法基础之上稍加改进就是Dijkstra算法。OPEN表中常出现这样的表项:(Vk,fk1)(Vk,fk2)(Vk,fk3),而从算法上看,只有fk最小的一个才有用,于是可以将它们合并,整个OPEN表表示当前的从V0到其它各点的最短路径,定长为n,且初始时为V0可直接到达的权值(不能到达为INFINITY),于是就成了Dijkstra算法。

    另外一个问题就是八数码难题,一个A*的好例子。
问题描述为有这样一个3*3方阵格子:

格子上有1-8八个数字外加一个空格,每次只能把与空格相临的一个数字移到空格内,移动一次算作一步,给出初始状态和目标状态,求如何以最少的步数完成移动?

设计A*算法时,g(n)就取当前已移动的步数,h(n)取各个数字到目标状态中对应数字的位置的最短距离之和,这样选取的原因是,对于每一次移动,只能使一个数字改变一个相临位置,所以h(n)步是至少需要的,所以满足h(n)<=h*(n)。

   A*的成功之处就是在选择好的h(n),如果实在没办法令它为0也是可以求得问题的解的。

                                                                                                                                                            算法驿站  (rickone 的 BLOG)

Fibonacci数列的计算

三月 16, 2007 by admin  
类别:生活匣子

Fibonacci数列F(n)递归地定义为:

        1                n<=2
F(n)={
        F(n-1)+F(n-2)     n>2

列出前几项:1,1,2,3,5,8,13,21,34。。。

    注意到这些数字没有,很多都是在大自然中出现的,我知道的少,不过你可以在互联网上搜一下,关键词:黄金分割。

没错,这个数列中体现了黄金分割,用数学方法很容易证明。首先它是发散的,但是不妨假设F(n+1)/F(n)是可以收敛的,设e=limF(n+1)/F(n),由递推方程:F(n)=F(n-1)+F(n-2),两边同除F(n-1)得(在极限情况下):e=1+1/e,解出来就得e=1.618…,同时也得知它近似地相当于指数数列1.618^n,至少会以这样的增涨率增涨。

    以下总结一下Fibonacci数列的计算方法。

1,直接递归计算。

int fibonacci(int n)
{
            if(n<=2)
                        return 1;
            return fibonacci(n-1)+fibonacci(n-2);
}

    它的计算效率同它的数值增涨效率一样是指数型的(O(1.618^n)),因为它会在递归中重复计算子问题,改进的方法就是‘打表’,把已经计算过的F(n)记录下来,在以后的计算中只管用就是了,也叫备忘录方法,也是DP算法的一个组成部分。

    2、备忘录方法。

int fibonacci(int n)
{
            if(mem(n)>0)
                        return mem(n);
            return mem(n)=fibonacci(n-1)+fibonacci(n-1);
}

    也可以直接用递推法,先开一个表f(n),然后由f(1)=f(2)=1开始计算。

inf fibonacci(int n)
{
            if(n<=2)
                        return 1;
            int *f=new int[n+1];
            f[1]=f[2]=1;
            for(int i=3;i<=n;++i)
                        f[i]=f[i-1]+f[i-2];
            int result=f[n];
            delete[] f;
            return result;
}

    前者是自顶向下,后者是自底向上,其效率是一样的,都是O(n)。从指数型复杂度到线性复杂度,是多大的提高啊。然而效率还可以提高。

    3、矩阵的方法。

思想来源上上次PKU的acm warmup
http://acm.pku.edu.cn/JudgeOnline/problem?id=3070

{{F(n+1),F(n)},
{F(n),F(n-1)}}

=

{{1,1}      ^n
{1,0}}

    我当时就想,用加法算都只能是O(n)的,换成n个矩阵的连乘不是更废劲吗?其实不然,乘法方法有比加法更好的性质,用加法只能利用前两个的值,而乘法却不同,因为乘法有结合律,可以大幅度下降算法的耗时。

令A(n)表示一个矩阵序列

A(n)=

{{F(n),F(n-1)},
{F(n-1),F(n-2)}

那么A(2)={{1,1},{1,0}},由那个表达式得到:

A(n)=A(2)^(n-1),A(2)是己知的2*2矩阵,现在的问题就是如何求

A(2)^n

   因为方阵的乘法有结合律,所以

A(2)^n=A(2)^(n/2)*A(2)^(n/2),不妨设n是偶数

    所以求A(n)就可以化成求A(n/2)并作一次乘法,所以递归方程是:

T(n)=T(n/2)+O(1)

它的解是O(lbn),不可思议吧。

这是我AC的代码:
#include <iostream>
using namespace std;

struct Matrix22
{
 int a,b,c;
 void operator*=(const Matrix22 &m);
 void display();
};

void Matrix22::operator *=(const Matrix22 &m)
{
 int p=b*m.b;
 int aa=a*m.a+p;
 int bb=a*m.b+b*m.c;
 int cc=p+c*m.c;
 a=aa%10000;
 b=bb%10000;
 c=cc%10000;
}

void Matrix22::display()
{
 printf("{%d,%d,%d},\n",a,b,c);
}
int main(void)
{
 Matrix22 base[31]={
{1,1,0},{2,1,1},{5,3,2},
{34,21,13},{1597,987,610},{4578,8309,6269},
{7565,7723,9842},{3954,4261,9693},{237,9867,370},
{3858,9269,4589},{8525,5243,3282},{4674,4101,573},
{4477,7947,6530},{8338,2629,5709},{3885,9563,4322},
{4194,3541,653},{8317,3227,5090},{6018,4389,1629},
{9645,2683,6962},{4514,6581,7933},{5757,3707,2050},
{4898,549,4349},{1805,6603,5202},{7634,7221,413},
{797,7387,3410},{2978,7109,5869},{6365,3323,3042},
{5554,9461,6093},{7437,2267,5170},{8258,69,8189},
{9325,4843,4482}
 };
 int n;
 while(1)
 {
  scanf("%d",&n);
  if(n==-1)
   break;
  if(n<2)
   printf("%d\n",n);
  else
  {
   Matrix22 x={1,0,1};
   –n;
   for(int i=0;n;++i,n>>=1)
   {
    if(n & 0×1)
     x*=base[i];
   }
   printf("%d\n",x.a);
  }

 }
 return 0;
}

    最后我们要观察这样的矩阵方法可否推广到一般情形,我们暂且称这样的递推序列为‘c阶线性递推方程’吧,它的意思就是说F(n)由与n相距c远的前面的一些F(m)组成的线性组合,比如Fibonacci数列的最大距离就是2,所以这样的递推式需要确定一个序列至少要知道最初的c个己知初值,我们的目的就是要求这样的递归式的一般算法:

F(n)={F(n-1),F(n-2),…F(n-c)}*K
{F(1),F(2),,,F(c)}^T=C

其中K={x1,x2,…xc}^T,C={c1,c2,…cc}^T都是非0的常向量。

我们构造A(n)表示一个c*c的矩阵序列,且形式为

A(n)=

{{F(n),F(n-1),F(n-2),…F(n-c+1)},
{F(n-1),F(n-2),F(n-3),…F(n-c)},
……
{F(n-c+1),F(n-c),F(n-c-c),…F(n-2c+2)}}

为了使它有意义,n-2c+2>=1,所以n>=2c-1,所以A(n)的初始矩阵是A(2c-1),可以按递归式直接构造它,然后我们考虑A(n)的生成矩阵会是什么样子,设为G,于是应满足:

A(n)=A(n-1)G

观察A(n-1)的第一行,它刚好就是{F(n-1),F(n-2),…F(n-c)},由递推式,知G的第一列应为K,而第二列设为x,则要满足{F(n-1),F(n-2),…F(n-c)}x=F(n-1),很简单,x={1,0,0,…},同样第三列是{0,1,0,0,…},所以右边应该是一个(c-1)*(c-1)的单位矩阵I再在最下面补上一行0,于是G可以表示为:

G={K,{I,0}^T}

于是A(n)的通项就是

A(n)=A(2c-1)*G^(n-2c+1)

比如Fibonacci数列的K={1,1}^T,所以
G={K,{I,0}^T}=
{{1,1},
 {1,0}}

    再举一个例子,也是以前做过的一个题,母牛生小牛的题,在一个农场上有很多母牛,第1年有一只母牛,而且每过4年,母牛会开始生小母牛,小母牛过4年后也会开始生,此后就每年生一只小母年,问第n年时有多少只母牛?

简单例出前几个数字:1,1,1,2,3,4,6,。。。会发现一个规律,就是第n年的母牛数F(n)恰等于前一年的母牛数F(n-1),以及前三年的母牛数F(n-3)之和,其实直接得到这个递推式也不难,因为F(n-3)到F(n)刚好4年(第4年),所以F(n-3)都是一些可以生仔的牛数,它们会添加F(n-3)个小牛,同时F(n-1)是所有的牛妈妈,那当然有F(n)=F(n-1)+F(n-3)。

它的K={1,0,1},c=3,生成矩阵G=
{{1,1,0},
 {0,0,1},
 {1,0,0}}

所以有A(n)=A(3)*G^(n-2)

A(3)=
{{1,1,1},
 {1,1,0},
 {1,0,0}}

    从O(2^n)到O(n),再到O(lbn),效率提高得也太猛烈了,不过一点要提出来,不管什么样的算法,Fiboacci数列本身数值的递增是指数型的,所以要想算很高阶的结果,也近乎不太可能,所以经常会在一个模数范围内考虑。

                                                                                                                                              rickone 2006/11/06 算法驿站  (rickone 的 BLOG)

搜索方法小结

三月 16, 2007 by admin  
类别:生活匣子

散列排序法

三月 16, 2007 by admin  
类别:生活匣子

    原来写过一篇文章在笔记本里,后来中病毒我把它格了,也没上网没机会发表出来,主要内容是关于一种比较另类的排序方法的,我叫它散列排序法。凭我的记忆把算法的主要思想写下来。

    排序法总体上可以分两大类,一类是基于‘比较’的,主要有大家非常熟悉的:选择排序,交换排序,插入排序,归并排序等,其算法的复杂度也是用‘比较’的次数衡量的,其中有非常高效和优秀的‘快速排序’,可以说是他们中间的佼佼者,无论从时间还是空间上说都有很好的性能;另外一类也就自然是不基于‘比较’的,《数据结构》上介绍过一种叫‘基数排序’,我觉得也很经典,今天我要向大家介绍的跟基数排序很类似,原理也非常简单。

    和基数排序的方法非常类似,也是采用分配和收集,而我管它叫‘散列’,因为就和hash散列表一样,而且我可以说当数据比较均匀的离散分布的时候,效率是非常高的,可以花很少的几次散列就得到有序结果。

    [写在前面,也跟基数排序一样只适用于整数排序,因为不基于比较就只能从元素,即数的本身出发了。]

先举个例子,设待排序的数是:
4,2,5,1,3

我们排序的任务就是让上面一列数有序,看看我们要做的结果是什么:
1,2,3,4,5

于是我们的任务就是,把前面一列数各位数放到‘正确’的位置上去,使得它有序。而一个数和它的正确位置就是一个映射关系,于是我们就是要找到一个函数f(x),使f(x)->‘x的正确位置’!

上面的例子非常简单,f(x)=x,但实际情况非常复杂,其实像这样的f(x)是不可能有很好的数学表达式的,别指望在这里做更多的努力。于是我着手找近似的f(x),我的想法是,只要让它不会错太多就行了,不完善的可以慢慢做得完善。

实际上f(x)就是一个散列函数,只不过它不是用来检索而是用来排序,我给出一个f(x)=[(x-min)*(n-1)/(max-min)],其中min是这列数的最小值,max是这列数的最大值,n是这列数的个数,那值域就会在[0,n-1],再用这些值找散列存储位置。

在这样一个近似的f(x)函数下,会出些‘意外’的情况,首先可能会有两个元素得到相同的f(x)值,也就是‘冲突’,冲突如何解决?可以采用拉链法,类似于hash表的冲突处理。

为什么要用拉链法,其实可以从集合的角度看这个算法,实际上我是把整个数列看成一个集合,然后我着手把它分成更小一些的子集,同时使这些子集‘集合间有序’,所谓集合间有序是指:
如果 集合A<集合B
那么 对于任意的x,x属于A,又对于任意的y,y属于B,都成立x<y

然后不断往下分,直到被分的集合中只含有一个元素为止,排序工作就完成了。

再来个例子:
1,4,3,7,3,2
于是
min=1,max=7,n=6
f(x)=[(x-1)*5/6]
第一次划分的结果是:
收集链表ID:0 1 2 3 4 5
[元素]      1 3 4     7
            2 3
就得到了四个子集,设对应ID的子集记为[ID],那么它们是
{[0],[1],[2],[5]}   (它们也是第一次划分f(x)的值域)
它们是‘集合间有序’的。

算法到此全部描述完了,另外我还想说点具体实现的过程,虽然没源代码了,还有些细节问题。

1、f中的min,max该如何求?
给我们的排序任务,只给出数据地址和数据元素,并没有给出min和max,是不是需要先求一下呢?如果要求一下,又回到了比较的方法,于是整个算法就不是很好的不基于比较的算法。我的看法是,具体排序的时候如果能给出来可以尽量给出(比如,按一个城市所有人的年龄排序,范围可以是0~150),如果真给不出来,没关系,我们取那种类型的数据的最小值和最大值。但是这不会影响到效率吗?后面再讨论效率。
另外再提出个概念,叫‘区分度’,区分度就是指一次划分的时候,得到的子集内的元素的最大值和最小值之差的最大值;这样吧,可能不好理解。这样记,我记一次划分为:
div(min,max):(这样记是因为,一次划分是跟min和max分不开的,确定了min和max才能划分)
{parent} –> {[i]}
那这次划分的区分度就是
max{elem1-elem2|elem1,elem2属于[i]}
在前面的例子中就是1,因为[0]中2-1最大。
为什么要提这个概念,在你划分一次后如果没有得到结果,那必须再对子集进行划分,这时的子划分中的max-min正是前一次的区分度。如果还知道min,那子划分就确定了。而min是可以线性求出来的,于是子划分就完全确定下来了。
区分度约为(max-min)/n *

2、这个算法的效率如何?
首先从时间上考虑。整个排序的过程可以看成是把一个有n个元素的集合分成n个只有一个元素的子集的过程。而就以划分的次数来衡量的话,最差的情况是,连续几次划分都没有分成功,原有集合本身做为子集,但是这样的情况不会持续很久,随着更加细致的划分,区分度会不断变小,区分度的迭代递推式是:d'=d/n,实际上是非常快速的收敛。其次,如果每一次划分只能分成两个子集,效率会如何呢?注意到划分出的子集是集合间有序的,这会联想到快速排序的一次划分,没错快速排序的一次划分能且只能将元素划分成两个部分,这两个部分有序,所以应该和快速排序的效率相当。最后,如果每一次划分能分成两个子集以上,效率会更高吗?不用我说了,应该会更高吧~~
实际上对于离散分布比较均匀(等概率分布)的无序数,排起序来是非常高效的~!
另外从空间上考虑。这就比快速排序差太多了,整个辅助空间需要得很大,基本上需要和原有数据量一样多的空间,应该是O(n),当n很大时,运行的实际耗时基本上都花在内存空间分配和回收上了。

                                                                                                                                                       算法驿站  (rickone 的 BLOG)

18位身份证号码验证算法代码

三月 15, 2007 by admin  
类别:生活匣子

介绍18位身份证号码最后一位校验码的计算方法

公民身份号码是特征组合码,由十七位数字本体码和一位校验码组成。排列顺序从左至右依次为:六位数字地址码,八位数字出生日期码,三位数字顺序码和一位数字校验码。

校验方法:
(1)十七位数字本体码加权求和公式
S = Sum(Ai * Wi), i = 0, … , 16 ,先对前17位数字的权求和
Ai:表示第i位置上的身份证号码数字值
Wi:表示第i位置上的加权因子
Wi: 7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2

(2)计算模
Y = mod(S, 11)

(3)通过模得到对应的校验码
     Y: 0 1 2 3 4 5 6 7 8 9 10
校验码: 1 0 X 9 8 7 6 5 4 3 2

下面是C程序代码:

//        char szSrc1[]="11010519491231002X";
//        DoVerify(szSrc1);
//        char szSrc2[]="440524188001010014";
//        DoVerify(szSrc2);

char DoVerify(const char* pszSrc)
{
    
int iS = 0;
    
int iW[]={7910584216379105842};
    
static char szVerCode[]="10X98765432";
    
int i;
    
for(i=0;i<17;i++)
    
{
        iS 
+= (int)(pszSrc[i]-'0'* iW[i];
    }

    
int iY = iS%11;
//    printf("%d %% 11 = iY = %d ",iS, iY);
//    printf("%c  ",szVerCode[iY]);
    return szVerCode[iY];

}

冒泡排序 Bubble Sort

三月 12, 2007 by admin  
类别:生活匣子

   最简单的排序方法是冒泡排序方法。这种方法的基本思想是,将待排序的元素看作是竖着排列的“气泡”,较小的元素比较轻,从而要往上浮。在冒泡排序算法中我们要对这个“气泡”序列处理若干遍。所谓一遍处理,就是自底向上检查一遍这个序列,并时刻注意两个相邻的元素的顺序是否正确。如果发现两个相邻元素的顺序不对,即“轻”的元素在下面,就交换它们的位置。显然,处理一遍之后,“最轻”的元素就浮到了最高位置;处理二遍之后,“次轻”的元素就浮到了次高位置。在作第二遍处理时,由于最高位置上的元素已是“最轻”元素,所以不必检查。一般地,第i遍处理时,不必检查第i高位置以上的元素,因为经过前面i-1遍的处理,它们已正确地排好序。这个算法可实现如下。

引用内容:
procedure Bubble_Sort(var L:List);
var
i,j:position;
begin
1 for i:=First(L) to Last(L)-1 do
2  for j:=First(L) to Last(L)-i do
3     if L[j]>L[j+1] then 
4           swap(L[j],L[j+1]);   //交换L[j]和L[j+1]
end;

上述算法将较大的元素看作较重的气泡,每次最大的元素沉到表尾。其中First(L)和Last(L)分别表示线性表L的第一个元素和最后一个元素的位置,swap(x,y)交换变量x,y的值。上述算法简单地将线性表的位置当作整数用for循环来处理,但实际上线性表可能用链表实现;而且上述算法将线性表元素的值当作其键值进行处理。不过这些并不影响表达该算法的基本思想。今后如果不加说明,所有的算法都用这种简化方式表达。

容易看出该算法总共进行了n(n-1)/2次比较。如果swap过程消耗的时间不多的话,主要时间消耗在比较上,因而时间复杂性为O(n2)。但是如果元素类型是一个很大的纪录,则Swap过程要消耗大量的时间,因此有必要分析swap执行的次数。

显然算法Bubble_Sort在最坏情况下调用n(n-1)/2次Swap过程。我们假设输入序列的分布是等可能的。考虑互逆的两个输入序列L1=k1,k2,..,kn和L2=kn,kn-1,..,k1。我们知道,如果ki>kj,且ki在表中排在kj前面,则在冒泡法排序时必定要将kj换到ki前面,即kj向前浮的过程中一定要穿过一次ki,这个过程要调用一次Swap。对于任意的两个元素ki和kj,不妨设ki>kj,或者在L1中ki排在kj前面,或者L2在中ki排在kj前面,两者必居其一。因此对于任意的两个元素ki和kj,在对L1和L2排序时,总共需要将这两个元素对调一次。n个元素中任取两个元素有Cn2 种取法,因此对于两个互逆序列进行排序,总共要调用Cn2 =n(n-1)/2次Swap,平均每个序列要调用n(n-1)/4次Swap。那么算法Bubble_Sort调用Swap的平均次数为n(n-1)/4。

可以对冒泡算法作一些改进,如果算法第二行的某次内循环没有进行元素交换,则说明排序工作已经完成,可以退出外循环。可以用一个布尔变量来记录内循环是否进行了记录交换,如果没有则终止外循环。

冒泡法的另一个改进版本是双向扫描冒泡法(Bi-Directional Bubble Sort)。设被排序的表中各元素键值序列为:

483 67 888 50 255 406 134 592 657 745 683

对该序列进行3次扫描后会发现,第3此扫描中最后一次交换的一对纪录是L[4]和L[5]:

50 67 255 134 | 406 483 592 657 683 745 888

显然,第3次扫描(i=3)结束后L[5]以后的序列都已经排好序了,所以下一次扫描不必到达Last(L)-i=11-4=7,即第2行的for 循环j不必到达7,只要到达4-1=3就可以了。按照这种思路,可以来回地进行扫描,即先从头扫到尾,再从尾扫到头。这样就得到双向冒泡排序算法:

引用内容:
procedure Bi-Directional_Bubble_Sort(var L:List);
var
low,up,t,i:position;
begin
1  low:=First(L);up:=Last(L);
2  while up>low do
    begin
3     t:=low;
4     for i:=low to up-1 do
5       if L[i]>L[i+1] then
          begin
6           swap(L[i],L[i+1]);
7           t:=i;
          end;
8     up:=t;
9     for i:=up downto low+1 do
10      if L[i]< L[i-1] then
          begin
11          swap(L[i],L[i-1]);
12          t:=i;
          end;
13    low:=t;   
    end; 
end;

算法利用两个变量low和up记录排序的区域L[low..up],用变量t 记录最近一次交换纪录的位置,4-7行从前向后扫描,9-12行从后向前扫描,每次扫描以后利用t所记录的最后一次交换记录的位置,并不断地缩小需要排序的区间,直到该区间只剩下一个元素。

直观上来看,双向冒泡法先让重的气泡沉到底下,然后让轻的气泡浮上来,然后再让较大气泡沉下去,让较轻气泡浮上来,依次反复,直到排序结束。

双向冒泡排序法的性能分析比较复杂,目前暂缺,那位朋友知道请告诉我。

冒泡排序法和双向冒泡排序法是原地置换排序法,也是稳定排序法,如果算法Bubble_Sort中第3行的比较条件L[j]>L[j+1]改为L[j]>= L[j+1],则不再是稳定排序法。

« 上一页下一页 »