posts - 195, comments - 34, trackbacks - 0, articles - 1


/*Problem: 1509  User: Uriel 
   Memory: 144K  Time: 16MS 
   Language: C  Result: Accepted
*/
 

#include
<stdio.h>
#include
<string.h>

int min(int a, int b)
{
    
return a <= b ? a : b;
}


int MinimumRepresentation(char *s, int l)
{
    
int i = 0, j = 1, k = 0, t;
    
while (i < l && j < l && k < l)
    
{
        t 
= s[(i + k)%l] - s[(j + k)%l];
        
if (!t) ++ k;
        
else
        
{
            
if (t > 0) i = i + k + 1;
            
else j = j + k + 1;
            
if (i == j) ++j;
            k 
= 0;
        }

    }

    
return min(i,j);
}


int x,len,i,t;
char str[10010];
int main()
{
    scanf(
"%d",&t);
    getchar();
    
while(t--)
    
{
        memset(str,
0x00,sizeof(str));
        scanf(
"%s",str);
        len
=strlen(str);
        x
=MinimumRepresentation(str,len);
        printf(
"%d\n",x+1);
    }

    
return 0;
}


//串的同构是,在若干次循环位移后可以变成相同
    static boolean isIsomorphism(String s1, String s2)
    
{
        
char[] u = (s1+s1).toCharArray();
        
char[] w = (s2+s2).toCharArray();
        
        
int i = 0;
        
int j = 0;
        
int len = s1.length();
        
while(i < u.length && j < w.length)
        
{
            
int k = 0;
            
while((i+k) < u.length && (j+k)<u.length  && u[i+k] == w[j+k])k++;//&& k < len
            System.out.println(k);
            
if(k >= len) return true;
            
            
if(u[i+k] > w[j+k])i = i+k+1;
            
else j = j+k+1;
        }

        
return false;
    }

posted @ 2009-11-02 18:13 小强摩羯座 阅读(403) | 评论 (0)编辑 收藏

排序算法、时间复杂度与信息熵
icon2 Program Impossible | icon4 2008-05-30 13:23| icon317 Comments | 本文内容遵从CC版权协议 转载请注明出自matrix67.com

    在这篇文章里,我们从信息论的角度证明了,基于比较的排序算法需要的比较次数(在最坏情况下)至少为log2(n!),而log(n!)=Θ(nlogn),这给出了比较排序的一个下界。但那里我们讨论的只是最理想的情况。一个事件本身所含的信息量是有大小之分的。看到这篇文章之后,我的思路突然开阔了不少:信息论是非常强大的,它并不只是一个用来分析理论最优决策的工具。从信息论的角度来分析算法效率是一件很有趣的事,它给我们分析排序算法带来了一种新的思路。

    假如你手里有一枚硬币。你希望通过抛掷硬币的方法来决定今天晚上干什么,正面上网反面看电影。投掷硬币所产生的结果将给你带来一些“信息”,这些信息的多少就叫做“信息量”。如果这个硬币是“公正”的,正面和反面出现的概率一样,那么投掷硬币后不管结果咋样,你都获得了1 bit的信息量。如果你事先就已经知道这个硬币并不是均匀的,比如出现正面的概率本来就要大得多,这时我们就说事件结果的不确定性比刚才更小。如果投掷出来你发现硬币果然是正面朝上,这时你得到的信息量就相对更小(小于1 bit);反之如果投掷出来居然反面朝上了,那你就得到了一个相对较大的信息量(大于1 bit)。但平均下来,我们得到的信息量是小于1 bit的,因为前者发生的可能性毕竟要大一些。最极端的情况就是,这是一枚被捣了鬼的魔术硬币,你怎么投都是正面。此时,你投了硬币等于没投,反正结果都是正面朝上,你得到的信息量永远为0。
    这个理论是很符合生活实际的。昨天晚上我出去吃饭时,坐在我后面的那个人是男的还是女的?这种问题就比较有价值,因为大家都猜不到答案究竟是什么;但要问我昨天跟谁一起出去上自习去了,问题的答案所含的信息量就变小了,因为大家都知道如果我破天荒地跑去自习了的话多半是有MM陪着一起去的。如果有网友问我是男的还是女的,那就更不可思议了,因为我不但多次在这个Blog里提到我一直想找一个合适的MM,还在AboutMe里面发了我的照片。如果某人刚操完一个MM,突然扭过头去问“对了,你是男的还是女的呀”,那这个人绝对是一个不折不扣的大傻B,因为这个问题所能带来的信息量几乎为0。
    总之,当每种结果出现的概率都相等,事件的不确定性达到最大,其结果最难预测时,事件的发生将会给我们带来最大的信息量。我们把一个事件的不确定程度叫做“熵”,熵越大表明这个事件的结果越难以预测,同时事件的发生将给我们带来越多的信息。如果在排序算法里每次比较的熵都是最大的,理论上来说这种(基于比较的)排序算法就应当是最优的。但我们一会儿将看到,我们已知的排序算法总是不完美的,每种算法都会或多或少地存在一些价值明显不大的比较。

    首先我们来看三种经典的平方复杂度算法。它们的效率并不高,原因就在于算法过程中会出现越来越多概率严重不均的比较。随着冒泡排序的进行,整个序列将变得越来越有序,位置颠倒的泡泡将越来越少;选择排序的每一趟选择中,你都会不断得到越来越大的数,同时在以后的比较中找到更大的数的概率也越来越低;在插入排序中,你总是把新的数与已经排好的数按从大到小的顺序依次进行比较,可以想到新的数一开始就比前面所有的数中最大的那个还大的概率是相当小的。受此启发,我们可以很自然地想到一个插入排序的改进:处理一个新的数时,为何不一开始就与前面处理过的数中的中位数进行比较?这种比较的熵显然更大,能获取的信息量要大得多,明显更有价值一些。这就是插入排序的二分查找改进。

    下面我们再来看一看几种O(nlogn)的排序算法。在快速排序算法中,比较的信息熵不会因为排序算法的进行而渐渐减小,这就是快速排序比上面几个排序算法更优秀的根本原因。仔细回顾快速排序算法的过程,我们立即看出,每次比较的两种结果出现的概率完全由这一趟划分过程所选择的基准关键字决定:选择的基准关键字刚好是当前处理的数字集合的中位数,则比较结果的不确定性达到最大;如果选择的基准关键字过大或过小,都会出现比较产生的结果不均等的情况,这使得每次比较平均带来的信息量大大减少。因此,快速排序算法是很看人品的:如果基准选的好,算法完全有可能达到理论上的最优;如果基准选的不好,复杂度很容易退化到O(n^2)。
    堆排序所需要的比较次数更多,因为在堆的删除操作中有一种明显不平衡的比较。在删除操作中,我们把根节点用整个堆的最末一个节点来代替,然后不断下沉直到它的儿子都比它大。判断它的两个儿子是否比它大,其信息熵是相当小的,因为这个节点本身就来自堆的底部,除非这个节点已经沉到很底下了,否则儿子比它大的概率是很小的。因此,我们想到了一个堆排序的优化:反正堆建好了以后不需要再插入新元素了,为何不舍弃堆的完全二叉树性质?我们可以直接把根元素改成无穷大,让它沉到底,不用再考虑儿子比它大的问题了,也不再顾及堆的形状。这样的话,堆排序是否就完美了呢?仔细想想你会发现,改进之后的比较操作仍然是不对称的。这种不对称主要来自两个方面:左子树和右子树的节点个数不同,以及被删除的根节点原先是来自左子树还是右子树。比方说,根节点原本就是从右子树提上来的,现在删除了根节点后,左子树的最小值比右子树的最小值更小的概率就偏大一些;此时万一右子树节点本来就比左边少,这样的话这个比较的熵就更小了。
    最后看一下归并排序。在有序队列的合并操作中,绝大多数情况下的比较操作都是比较平衡的。左边一半中的最小值和右边一半中的最小值进行比较,结果显然是等概率的。当然,随后将发生其中一边的最小值与另一边的次小值进行比较,这时的比较操作略微有了一些不平衡,并存在较小的可能使得比较操作变得更加不平衡(最小值与第三小的值相比)。有趣的是,比较越是不平衡,重新归于平衡的概率就越大,就好像归并排序中的信息熵会自动调整一样。这就是归并排序比平方复杂度的排序算法效率更高的原因。当然,完全有可能出现这样的情况:右边的数奇小无比,左边的最小值比右边的所有值都大。结果最后右边的队列都处理完了左边还没开始取数,此时合并操作提前结束,所花费的比较次数出人意料地少。从信息熵的角度来看,这种“比较提前结束”的现象是非常自然的:这种情况毕竟是“出人意料”的,事实越出人意料,获得的信息量就越大,因此算法就提前结束了。但这种情况毕竟是相当罕见的,平均情况下每次比较的信息量仍然不足1 bit。

    最后,为什么线性排序的算法可以达到O(n)的复杂度?这是因为,线性排序算法并不是基于比较的。一次比较事件(假设没有相等的情况)所能产生的信息量最多1 bit,而一次Hash分类可以获得的信息量远远超过了1 bit,因为它可以一次确定出n种等概率的可能情况。

Matrix67原创,转贴请注明出处~~

posted @ 2009-11-02 16:19 小强摩羯座 阅读(301) | 评论 (0)编辑 收藏

传说中效率最高的最大流算法(Dinic)

呵呵,又从DK那偷代码了,好兴奋哈,以下是这个算法的简单介绍,不过我用它去解决HDU的1532 竟然TLE,郁闷.到时候再继续问问DK吧...so 烦躁.

哈哈 终于经过大牛的指点 原来本算法是从0开始标号的......

Dinic是个很神奇的网络流算法。它是一个基于“层次图”的时间效率优先的最大流算法。

层次图是什么东西呢?层次,其实就是从源点走到那个点的最短路径长度。于是乎,我们得到一个定理:从源点开始,在层次图中沿着边不管怎么走,经过的路径一定是终点在剩余图中的最短路。(摘自WC2007王欣上论文)注意,这里是要按照层次走。

那么,MPLA(最短路径增值)的一大堆复杂的证明我就略掉了,有兴趣的请自行参阅WC2007王欣上神牛的论文。

首先我们得知道,Dinic的基本算法步骤是,先算出剩余图,然后用剩余图算层次图,然后在层次图里找增广路。不知道你想到没有,这个层次图找增广路的方法,恰恰就是Ford-Fulkerson类算法的时间耗费最大的地方,就是找一个最短的增广路。所以呢,层次图就相当于是一个已经预处理好的增广路标志图。

如何实现Dinic呢?

首先我们必然要判一下有没有能到达终点的路径(判存在增广路与否),在这个过程中我们顺便就把层次图给算出来了(当然不用算完),然后就沿着层次图一层一层地找增广路;找到一条就进行增广(注意在沿着层次图找增广路的时候使用栈的结构,把路径压进栈);增广完了继续找,找不到退栈,然后继续找有没有与这个结点相连的下一层结点,直到栈空。如果用递归实现,这个东西就很好办了,不过我下面提供的程序是用了模拟栈,当然这样就不存在结点数过多爆栈的问题了……不过写起来也麻烦了一些,对于“继续找”这个过程我专门开了一个数组存当前搜索的指针。

上面拉拉杂杂说了一大堆,实际上在我的理解中,层次图就是一个流从高往低走的过程(这玩意儿有点像预流推进的标号法……我觉得),在一条从高往低的路径中,自然有些地方会有分叉;这就是Dinic模拟栈中退栈的精华。这就把BFS的多次搜索给省略了不说,时间效率比所谓的理论复杂度要高得多。

这里有必要说到一点,网络流的时间复杂度都是很悲观的,一般情况下绝对没有可能到达那个复杂度的。

 

#include<iostream>
using namespace std;
const long maxn=300;
const long maxm=300000;
const long inf=0x7fffffff;
struct node
{
    
long v,next;
    
long val;
}s[maxm
*2];
long level[maxn],p[maxn],que[maxn],out[maxn],ind;
void init()
{
    ind
=0;
    memset(p,
-1,sizeof(p));
}
inline 
void insert(long x,long y,long z)
{
    s[ind].v
=y;
    s[ind].val
=z;
    s[ind].next
=p[x];
    p[x]
=ind++;
    s[ind].v
=x;
    s[ind].val
=0;
    s[ind].next
=p[y];
    p[y]
=ind++;
}
inline 
void insert2(long x,long y,long z)
{
    s[ind].v
=y;
    s[ind].val
=z;
    s[ind].next
=p[x];
    p[x]
=ind++;
    s[ind].v
=x;
    s[ind].val
=z;
    s[ind].next
=p[y];
    p[y]
=ind++;
}
long max_flow(long n,long source,long sink)
{
    long
ret=0;
    long
 h=0,r=0;
    
while(1)
    {
        
long i;
        
for(i=0;i<n;++i)
            level[i]
=0;
        h
=0,r=0;
        level[source]
=1;
        que[
0]=source;
        
while(h<=r)
        {
            
long t=que[h++];
            
for(i=p[t];i!=-1;i=s[i].next)
            {
                
if(s[i].val&&level[s[i].v]==0)
                {
                    level[s[i].v]
=level[t]+1;
                    que[
++r]=s[i].v;
                }
            }
        }
        
if(level[sink]==0)break;
        
for(i=0;i<n;++i)out[i]=p[i];
        
long q=-1;
        
while(1)
        {
            
if(q<0)
            {
                
long cur=out[source];
                
for(;cur!=-1;cur=s[cur].next)
                {
                    
if(s[cur].val&&out[s[cur].v]!=-1&&level[s[cur].v]==2)
                    {
                        
break;
                    }
                }
                
if(cur>=0)
                {
                    que[
++q]=cur;
                    
out[source]=s[cur].next;
                }
                
else
                {
                    
break;
                }
            }
            
long u=s[que[q]].v;
            
if(u==sink)
            {
                
long dd=inf;
                
long index=-1;
                
for(i=0;i<=q;i++)
                {
                    
if(dd>s[que[i]].val)
                    {
                        dd
=s[que[i]].val;
                        index
=i;
                    }
                }
                ret
+=dd;
                
//cout<<ret<<endl;
                for(i=0;i<=q;i++)
                {
                    s[que[i]].val
-=dd;
                    s[que[i]
^1].val+=dd;    
                }
                
for(i=0;i<=q;i++)
                {
                    
if(s[que[i]].val==0)
                    {
                        q
=index-1;
                        
break;
                    }
                }
            }
            
else
            {
                
long cur=out[u];
                
for(;cur!=-1;cur=s[cur].next)
                {
                    
if(s[cur].val&&out[s[cur].v]!=-1&&level[u]+1==level[s[cur].v])
                    {
                        
break;
                    }
                }
                
if(cur!=-1)
                {
                    que[
++q]=cur;
                    
out[u]=s[cur].next;
                }
                
else
                {
                    
out[u]=-1;
                    q
--;
                }
            }
        }
    }
    
return ret;
}

long m,n;

int main()
{

    
while(scanf("%ld %ld",&m,&n)!=EOF)
    {
        init();
        
for(int i=0;i<n;i++)
        {
            
long from,to,cost;
            scanf(
"%ld %ld %ld",&from,&to,&cost);
            insert(--from,--to,cost);
        }
        
long Start,End;
        scanf(
"%ld %ld",&Start,&End);
        printf(
"%ld\n",max_flow(n,--Start,--End));
    }
    
return 0;
}
« 上一篇:KM算法(转)» 下一篇:字典树

posted @ 2009-10-30 14:37 小强摩羯座 阅读(752) | 评论 (0)编辑 收藏

     摘要: 最短路径 之 SPFA算法 (转载)(2009-05-06 20:41:51) var $tag='spfa,杂谈'; var $tag_code='0c0816ca8a11d99e776ffbef47dd2fd0'; 标签:spfa 杂谈  ...  阅读全文

posted @ 2009-10-30 14:00 小强摩羯座 阅读(2954) | 评论 (0)编辑 收藏

#include  <stdio.h>
int add(int x,int y)  {return x+y;}
int sub(int x,int y)  {return x-y;}
int mul(int x,int y)  {return x*y;}
int div(int x,int y)  {return x/y;}
int (*func[])()={add,sub,mul,div};
int num,curch;
char chtbl[]="+-*/()=";
char corch[]="+-*/()=0123456789";
int getach()  {
    int i;
    while(1)  {
        curch=getchar();
        if(curch==EOF)  return -1;
        for(i=0;corch[i]&&curch!=corch[i];i++);
        if(i<strlen(corch))  break;
    }
    return curch;
}

int getid()  {
    int i;
    if(curch>='0'&&curch<='9')  {
        for(num=0;curch>='0'&&curch<='9';getach())    num=10*num+curch-'0';
        return -1;
    }
    else  {
        for(i=0;chtbl[i];i++) if(chtbl[i]==curch)  break;
            if(i<=5)  getach();
            return i;
    }
}

int cal()  {
    int x1,x2,x3,op1,op2,i;
    i=getid();
    if(i==4)    x1=cal();    else  x1=num;
    op1=getid();
    if(op1>=5)  return x1;
    i=getid();
    if(i==4)  x2=cal();    else  x2=num;
    op2=getid();
    while(op2<=4)  {
        i=getid();
        if(i==4)  x3=cal();  else  x3=num;
        if((op1/2==0)&&(op2/2==1))    x2=(*func[op2])(x2,x3);
        else  {
            x1=(*func[op1])(x1,x2);
            x2=x3;
            op1=op2;
        }
        op2=getid();
    }
    return (*func[op1])(x1,x2);
}

void main(void)  {
    int value;
    printf("Please input an expression:\n");
    getach();
    while(curch!='=')  {
        value=cal();
        printf("The result is:%d\n",value);
        printf("Please input an expression:\n");
        getach();
    }
}

posted @ 2009-10-30 00:36 小强摩羯座 阅读(259) | 评论 (0)编辑 收藏

(一)简单的函数指针的应用。
//形式1:返回类型(*函数名)(参数表)
char (*pFun)(int);
char glFun(int a){ return;}
void main()
{
    pFun = glFun;
    (*pFun)(2);
}

        第一行定义了一个指针变量pFun。首先我们根据前面提到的“形式1”认识到它是一个指向某种函数的指针,这种函数参数是一个int型,返回值是char类型。只有第一句我们还无法使用这个指针,因为我们还未对它进行赋值。
        第二行定义了一个函数glFun()。该函数正好是一个以int为参数返回char的函数。我们要从指针的层次上理解函数——函数的函数名实际上就是一个指针,函数名指向该函数的代码在内存中的首地址。
        然后就是可爱的main()函数了,它的第一句您应该看得懂了——它将函数glFun的地址赋值给变量pFun。main()函数的第二句中“*pFun”显然是取pFun所指向地址的内容,当然也就是取出了函数glFun()的内容,然后给定参数为2。
(二)使用typedef更直观更方便。
//形式2:typedef 返回类型(*新类型)(参数表)
typedef char (*PTRFUN)(int);
PTRFUN pFun;
char glFun(int a){ return;}
void main()
{
    pFun = glFun;
    (*pFun)(2);
}

        typedef的功能是定义新的类型。第一句就是定义了一种PTRFUN的类型,并定义这种类型为指向某种函数的指针,这种函数以一个int为参数并返回char类型。后面就可以像使用int,char一样使用PTRFUN了。
        第二行的代码便使用这个新类型定义了变量pFun,此时就可以像使用形式1一样使用这个变量了。
(三)在C++类中使用函数指针。
//形式3:typedef 返回类型(类名::*新类型)(参数表)
class CA
{
 public:
    char lcFun(int a){ return; }
};
CA ca;
typedef char (CA::*PTRFUN)(int);
PTRFUN pFun;
void main()
{
    pFun = CA::lcFun;
    ca.(*pFun)(2);
}

        在这里,指针的定义与使用都加上了“类限制”或“对象”,用来指明指针指向的函数是那个类的这里的类对象也可以是使用new得到的。比如:
CA *pca = new CA;
pca->(*pFun)(2);
delete pca;

        而且这个类对象指针可以是类内部成员变量,你甚至可以使用this指针。比如:
        类CA有成员变量PTRFUN m_pfun;
void CA::lcFun2()

   (this->*m_pFun)(2);
}

        一句话,使用类成员函数指针必须有“->*”或“.*”的调用。

 

 

作者:csumck   更新日期:2004-11-07
来源:CSDN   浏览次数:

posted @ 2009-10-30 00:35 小强摩羯座 阅读(147) | 评论 (0)编辑 收藏

系统的可靠度计算公式 收藏
并联:1-(1-p1)(1-p2)


串联:p1p2


p1,p2分别为部件1和部件2的可靠度.
---------------------------------------------------------------------------
eg:
某计算机系统的可靠性结构是如下图所示的双重串并联结构,若所构成系统的每个部件的可靠度为0.9 ,即R=0.9 ,则系统的可靠度为()?

|---(R)————(R)---|
———| |--
|---(R)----(R)---|

类似于串两个电阻,在并两个电阻的图。问怎样计算?
 
最佳答案
串联的可靠度P1=R1×R1 =0.81
并联起来时可靠度P2=1-(1-P1)×(1-P1)=0.9639

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/zwhfyy/archive/2009/04/02/4042513.aspx

posted @ 2009-10-29 11:05 小强摩羯座 阅读(820) | 评论 (0)编辑 收藏

     摘要: 背包问题 1.引子   我们人类是一种贪婪的动物,如果给您一个容量一定的背包和一些大小不一的物品,裝到背包里面的物品就归您,遇到这种好事大家一定不会错过,用力塞不一定是最好的办法,用脑子才行,下面就教您如何解决这样的问题,以获得更多的奖品。 2.应用场景   在一个物品向量中找到一个子集满足条件如下 :    1)这个子集加起来的体积大小不能...  阅读全文

posted @ 2009-10-28 21:01 小强摩羯座 阅读(389) | 评论 (0)编辑 收藏

在今年的信息学冬令营上,陈启峰提出了一个自己创造的BST数据结构—Size Balanced Tree。这个平衡二叉树被全世界内的许多网站所讨论,大家讨论的主题也只有一个—SBT能够取代Treap吗?本文详细介绍SBT树的性质,以及一些常用的操作,最后证明SBT是一颗高度平衡的二分查找树。

一.        介绍

众所周知,BST能够快速的实现查找等动态操作。但是在某些情况下,比如将一个有序的序列依次插入到BST中,则BST会退化成为一条链,效率非常之低。由此引申出来很多平衡BST,比如AVL树,红黑树,treap树等。这些数据结构都是通过引入其他一些性质来保证BST的高度在最坏的情况下都保持在O(log n)。其中,AVL树和红黑树的很多操作都非常麻烦,因此实际应用不是很多。而treap树加入了一些随机化堆的性质,实际应用效果非常好,实现起来很简单,一直以来受到很多人的青睐。本文介绍一种新的平衡BST树,实现起来也是非常之简单,并且能够支持更多的操作,实际评测效率跟treap也不差上下。

在介绍SBT之前,先介绍一下BST以及在BST上的旋转操作。

1.      Binary Search Tree

BST是一种高级的数据结构,它支持很多动态操作,包括查找,求最小值,最大值,前驱,后继,插入和删除,能够用于字典以及优先队列。

       BST是一棵二叉树,每个结点最多有2个儿子。每个结点都有个键值,并且键值必须满足下面的条件:

       如果xBST中的一个结点。那么x的键值不小于其左儿子的键值,并且不大于其右儿子的键值。

       对于每个结点t,用left[t]right[t]分别来存放它的两个儿子,ket[t]存放该结点的键值。另外,在SBT中,要增加s[t],用来保存以t为根的子树中结点的个数。

2.      旋转

为了保证BST的平衡(不会退化成为一条链),通常通过旋转操作来改变BST的结构。旋转操作不会影响binary-search-tree的性质!


 

       2.1右旋操作的伪代码

       右旋操作必须保证左儿子存在

       Right-Rotate(t)

              k←left[t]

              left[t]←right[k]

              right[k]←t

              s[k]←s[t]

              s[t]←s[left[t]]+s[right[t]]+1

              t←k

       2.2 左旋操作的伪代码

       左旋操作必须保证右儿子存在

       Left-Rotate(t)

              k←right[t]

              right[t]←left[k]

              left[k]←t

              s[k]←s[t]

              s[t]←s[left[t]]+s[right[t]]+1

              t←k

二.Size Balanced Tree

Size Balanced Tree(简称SBT)是一种平衡二叉搜索树,它通过子树的大小s[t]来维持平衡性质。它支持很多动态操作,并且都能够在O(log n)的时间内完成。

Insert(t,v)

将键值为v的结点插入到根为t的树中

Delete(t,v)

在根为t的树中删除键值为v的结点

Find(t,v)

在根为t的树中查找键值为v的结点

Rank(t,v)

返回根为t的树中键值v的排名。也就是树中键值比v小的结点数+1

Select(t,k)

返回根为t的树中排名为k的结点。同时该操作能够实现Get-min,Get-max,因为Get-min等于Select(t,1),Get-max等于Select(t,s[t])

Pred(t,v)

返回根为t的树中比v小的最大的键值

Succ(t,v)

返回根为t的树中比v大的最小的键值

SBT树中的每个结点都有leftrightkey以及前面提到的size域。SBT能够保持平衡性质是因为其必须满足下面两个条件:

对于SBT中的每个结点t,有性质(a)(b)

(a). s[right[t]]≥s[left[left[t]]],s[right[left[t]]]

(b). s[left[t]]≥s[right[right[t]]],s[left[right[t]]]


 

即在上图中,有s[A],s[B]≤s[R]&s[C],s[D] ≤s[L]

三.              Maintain

假设我们要在BST中插入一个键值为v的结点,一般是用下面这个过程:

Simple-Insert(t,v)

        If t=0 then

            t←NEW-NODE(v)

              Else

                     s[t]←s[t]+1

                     If v<key[t] then

                            Simple-Insert(left[t],v)

                     Else

                            Simple-Insert(right[t],v)

执行完操作Simple-Insert后,SBT的性质(a)(b)就有可能不满足了,这是我们就需要修复(Maintain)SBT

Maintain(t)用来修复根为tSBT,使其满足SBT性质。由于性质(a)(b)是对称的,下面仅讨论对性质(a)的修复。

Case 1s[left[left[t]]]>s[right[t]]


这种情况下可以执行下面的操作来修复SBT

执行Right-Rotate(T)

 

有可能旋转后的树仍然不是SBT,需要再次执行Maintain(T)

由于L的右儿子发生了变化,因此需要执行Maintain(L)

Case 2s[right[left[t]]]>s[right[t]]

这种情况如下图所示:


 

需要执行一下步骤来修复SBT

执行Left-Rotate(L)。如下图所示


 

执行Right-Rotate(T)。如下图所示


 

当执行完(1)(2)后,树的结构变得不可预测了。但是幸运的是,在上图中,A,E,F,R子树仍然是SBT。因此我们可以执行Maintain(L)Maintain(T)来修复B的子树。

Case 3

这种情况和case 1是对称的

Case 4

这种情况和case 2是对称的

Maintain操作的伪代码:

Maintain过程中,用一个变量flag来避免额外的检查。当flagfalse时,代表case 1case 2需要被检查,否则case 3case 4需要被检查。

Maintain (t,flag)

If flag=false then

             If s[left[left[t]]>s[right[t]] then

                    Right-Rotate(t)

             Elseif s[right[left[t]]>s[right[t]] then

                    Left-Rotate(left[t])

                    Right-Rotate(t)

             Else exit

      Elseif s[right[right[t]]>s[left[t]] then

             Left-Rotate(t)

      Elseif s[left[right[t]]>s[left[t]] then

             Right-Rotate(right[t])

             Left-Rotate(t)

      Else exit

      Maintain(left[t],false)

Maintain(right[t],true)

Maintain(t,false)

      Maintain(t,true)

四.常用操作

插入操作

SBT和插入操作和BST的基本相同,只是在插入之后需要执行下Maintain操作。

Insert (t,v)

If t=0 then

t←NEW-NODE(v)

Else

s[t] ←s[t]+1

If v<key[t] then

Simple-Insert(left[t],v)

Else

Simple-Insert(right[t],v)

Maintain(t,v≥key[t])

删除操作

如果没有找到要删除的结点,那么就删除最后一个访问的结点并记录。

Delete (t,v)

If s[t]2 then

record←key[t]

t←left[t]+right[t]

Exit

s[t] ←s[t]1

If v=key[t] then

Delete(left[t],v[t]+1)

Key[t] ←record

Maintain(t,true)

Else

If v<key[t] then

Delete(left[t],v)

Else

Delete(right[t],v)

Maintain(t,v<key[t])

另外,由于SBT的平衡性质是靠size域来维护的,而size域本身(子树所含节点个数)对于很多查询算法都特别有用,这样使得查询集合里面的譬如第n小的元素,以及一个元素在集合中的排名等操作都异常简单,并且时间复杂度都稳定在O(log n)。下面仅介绍下上表提到的select(t,k)操作和rank(t,v)操作。

       由于SBT的性质(结点t的关键字比其左子树中所有结点的关键字都大,比其左子树中所有的关键字都小),理解下面的算法非常容易。

3Select操作

Select(t,k)

       If k=s[left[t]]+1 then

              return key[t]

       If k<=s[left[t]] then

              return Select(left[t],k)

       Else

              return Select(right[t],k-1-s[left[t]])

4Rank操作

Rank(t,v)

       If t=0 then

              return 1

       If v<=key[t] then

              return rank(left[t],v)

       Else

              return s[left[t]]+1+rank(right[t],v)

同样,求前驱结点的操作Pred和后继结点的操作都很容易通过size域来实现。

 

五.相关证明分析

显然Maintain操作是一个递归过程,可能你会怀疑它是否会结束。下面我们可以证明Maintain操作的平摊时间复杂度为O(1)

1.关于树的高度的分析

f[h]表示高度为hSBT中结点数目的最小值,则有

                                                               (h=0)

f[h]=                                         (h=1)

           f[h-1]+f[h-2]+1                  (h>1)

a.证明:

(1)       很明显f[0]=1,f[1]=2

(2)       首先,对于任意h>1,我们假设t是一颗高度为hSBT的根结点,则这颗SBT包含一颗高度为h-1的子树。不妨假设t的左子树的高度为h-1,根据f[h]的定义,有

s[left[t] ]≥f[h-1],同样的,左子树中有一颗高度为h-2的子树,换句话说,左子树中含有一颗结点数至少为f[h-2]的子树。由SBT的性质(b),可知s[right[t]] ≥f[h-2]。因此我们有s[t]=s[left[t]]+s[right[t]]+1≥f[h-1]+f[h-2]+1。

另外一方面,我们可以构造一颗高度为h,并且结点数正好为f[h]SBT,称这样的SBTtree[t]。可以这样来构造tree[h]

                             含有一个结点的SBT                                    (h=0)

tree[h]=     含有2个结点的任意SBT                             (h=1)

         左子树为tree[h-1],右子树为tree[h-2]SBT    (h>1)

f[h]的定义可知f[h] f[h-1]+f[h-2]+1(h>1)。因此f[h]的上下界都为f[h-1]+f[h-2]+1,因此有f[h]=f[h-1]+f[h-2]+1

b.最坏情况下的高度

事实上f[h]是一个指数函数,通过f[h]的递推可以计算出通项公式。


 

定理:

含有n个结点的SBT在最坏情况下的高度是满足f[h] n的最大的h值。

假设maxh为含有n个结点的SBT的最坏情况下的高度。由上面的定理,有

                                  

于是很明显SBT的高度为O(logn),是一颗高度平衡的BST

2.对Maintain操作的分析

通过前面的计算分析我们能够很容易分析出Maintain操作是非常高效的。

首先,有一个非常重要的值来评价一颗BST的好坏:所有结点的平均深度。它是通过所有结点的深度之和SD除以结点个数n计算出来的。一般来说,这个值越小,这颗BST就越好。由于对于一颗BST来说,结点数n是一个常数,因此我们期望SD值越小越好。

现在我们集中来看SBTSD值,它的重要性在于能够制约Maintain操作的执行时间。回顾先前提到的BST中的旋转操作,有个重要的性质就是:每次执行旋转操作后,SD值总是递减的!

由于SBT树的高度总是O(log n),因此SD值也总是保持在Olog n)。并且SD仅在插入一个结点到SBT后才增加,因此(TMaintain操作中执行旋转的次数)


 

Maintain操作的次数等于T加上不需要旋转操作的Maintain操作的次数。由于后者为O(nlogn)+O(T),因此Maintain的平摊分析时间复杂度为:


 

对各个操作时间复杂度的分析

现在我们知道了SBT的高度为O(log n),并且 Maintain操作的平摊分析时间复杂度为O(1),因此对于所有的常用操作,时间复杂度都稳定在O(log n)

posted @ 2009-10-28 01:07 小强摩羯座 阅读(475) | 评论 (0)编辑 收藏

     摘要: 目前最快的数独求解程序 - 实现了Knuth的Dancing Links+Algorithm X算法 C++语言: 目前最快的数独求解程序 - 实现了Knuth的Dancing Links+Algorithm X算法 //from: http://code.google.com/p/klsudoku/source/checkout //半瓶墨水修改于 2009 Sept 18 //R...  阅读全文

posted @ 2009-10-26 23:11 小强摩羯座 阅读(1198) | 评论 (0)编辑 收藏

仅列出标题
共20页: 上一页 1 2 3 4 5 6 7 8 9 下一页 Last