哦对了,我的法线贴图做好了

看看这光影的变化,多漂亮啊:

其实那些凹凸并不在模型上,而只是通过一张图保存了平面的法线方向。

利用法线贴图上的法线信息计算光照的时候需要注意的是得要把它的坐标系(横着是 red, 竖着是 green, 立在平面上的是 blue)转换到空间上对应平面的坐标系中 —— 或者反过来,把光线和视线转换到法线平面的坐标系中(我就是这么做的),然后再那啥。

提一下,从网上搜到的信息来看这个的计算貌似挺复杂的,但是考虑线性变换其实也就是线性方程而已,从法线贴图的坐标系转换到空间坐标系,当前正在渲染的点所在的空间三角形的贴图的 u 坐标的正方向为 a,法向量为 n,不考虑位移(为什么可以不考虑位移?自己想),我们可以列出这么几个式子:

z = cross(a, n);

(1,0,0) -> (a.x, a.y, a.z)
(0,1,0) -> (n.x, n.y, n.z)
(0,0,1) -> (z.x, z.y, z.z)

 

有没有恍然大悟的感觉?

 

没有?再提示,设从法线贴图的坐标空间转换到空间三角形的坐标空间的矩阵为 M, 那么有

1,0,0            a.x, a.y, a.z
0,1,0  *  M ==  n.x, n.y, n.z
0,0,1  z.x, z.y, z.z

好了,点到为止。

 

俺用的漫反射贴图是这样的:

漫反射贴图

法线贴图:

法线贴图

高光贴图:(虽然用了而且计算了,不过光源放的位置似乎不太恰当以至于完全看不到高光的效果,囧~)

高光贴图

可执行文件:

binary

 

源码……还是不给,因为写得丑了……

 

PS. 想要运行着玩玩看的话你得把上面三个贴图和这个程序保存在一起,如果你有更靠谱的贴图当然更好(分别保存为 tex.png, tex_NRM.png, tex_SPEC.png)。

PPS. 在学 DX,所以上面用到的线性变换也是假设向量是行向量,所以对向量的变换就是用行向量左乘矩阵,不过换个方式用矩阵左乘列向量区别也不大。

PPPS. 扫了几本 3D 数学方面的书,发现还是这本比较靠谱: http://book.douban.com/subject/1400419/

PPPPS. 非常感谢老胡当年让我感受到了线性代数的好玩之处,不然现在在公司里可要遭鄙视了。

PPPPPS. 我写的 3dmath.h,如果你有兴趣看的话——含有一个向量类一个矩阵类,若干无趣的函数,唯一比较有意思的东西是透视和投影。

PPPPPPS. 好几次看见自己拍的照片,突然想到,哇,这阴影好逼真啊。

无营养两则

1. 最近在补 3D 知识,上周花很大的精力试着手动逐像素渲染 3D 图形,挺好玩的:

老大的要求是高光贴图和法线贴图也是要的,我也做出来了,只不过几个小时之前突然意识到一个非常重要的坐标转换没有做,所以那个法线贴图的结果其实很不靠谱,代码先不放了,省得丢人,等把法线贴图做对了再说吧。

这是阉割版的程序 —— 如果你有兴趣围观的话:rending.7z

哦对了,这里用到的数学运算俺也都自己实现了一遍,等有时间了会整理一下的,毕竟这个其实挺好玩。

 

 

2. 觉得 mataball 这东西挺有意思,结果就整出了个奇丑的:

任何试图画出有两个眼睛一个嘴巴的面部的尝试都会得到一个青蛙

程序和代码在这儿:metaball.7z

圈圈们默认是会动的,如果你只是想画青蛙,把 render 函数的第二个参数设为 0 就好了。

BTW Fast inverse square root 真是威武,有多威武你自己试试就知道了。

 

用 hough 变换检测抛物线

告诉你某张图中很可能有某个东西长得很像开口向上的抛物线,想把它检测出来,嘿嘿,看上去很难吧?

其实呢,这种事情很适合用 hough 变换 来做:

假设抛物线方程为 y = a ( x - b )2 + c

先把图像边缘检测出来,对于图像边缘上的每一个点 (x,y), 可以有很多条抛物线穿过它,即有很多个 a, b, c 可以使上面的方程成立,于是, 你很暴力的把这些 a, b, c 都记下来! ,对于每个 x,y 点都记下一次,然后看看哪些 a, b, c 的组合被命中的次数比较多就好了。

当然,上面的那个过程有不少地方是可以优化的,比如说对于图片,b 和 c 肯定是整数值,而且 b 很可能在比较偏中心的位置,而 a 的取值范围可以预先设定,比如说如果你想检测下巴,保守的说 a 肯定就是小于 1 的(不然那下巴也太尖了吧!),如此这般等等——而 OpenCV 中用 hough 变换检查圆的代码中显然还有些其他的不那么容易理解的优化,反正我也用不着把它做得很高效,就没有研究了。

下面上图:

(图片均来源于 pixdaus.com ,如果碰巧它侵犯了您的版权,请与我联系)

 

再上代码——

 

(你确定要看吗??!!)

 

 

(警告!魔数出没!)

 

 

 

(警告!没有注释!)

 

 

 

 

(警告!诡异的变量名出没!)

 

 

 

 

 

(警告!你的大脑可能因为看到这种乱七八糟的代码受到永久性的损伤!!)

 

 

 

 

 

 

(你确定依然要看吗??!!!!)

 

 

 

 

 

 

 

// y = a/150*(x-b)^2+c;
struct Parabola {
    int a;
    int b;
    int c;
    Parabola():a(0),b(0),c(0){}
    Parabola(int a, int b, int c):a(a),b(b),c(c){}
    Parabola(Parabola const& p):a(p.a),b(p.b),c(p.c){}
};
struct Record {
    Parabola para;
    int      count;
    Record():para(),count(0){}
    Record(int a, int b, int c, int cnt):para(a,b,c),count(cnt){}
};
struct RecordCmp {
    inline bool operator()(Record const& a, Record const& b) {
        return a.count>b.count;
    }
};
std::vector<Parabola> detectParabolas( cv::Mat const& img,
                                       int minA=-1,
                                       int maxA=-1,
                                       int minB=-1,
                                       int maxB=-1,
                                       int minC=-1,
                                       int maxC=-1 )
{
    cv::Mat edges;
    // cv::imshow("debug", img);
    // cv::waitKey(3000);
    cv::Canny( img, edges, 50, 150 );
    cv::Mat_<uint8_t> img_(edges);
    // cv::imshow("debug", edges);
    // cv::waitKey(3000);
    int ***acc = 0;

    if(minA < 0)
        minA = 1;
    if(minB < 0)
        minB = img.cols/4;
    if(minC < 0)
        minC = img.rows/4;
    if(maxA < 0)
        maxA = 60;
    if(maxB < 0)
        maxB = img.cols/2+img.cols/4;
    if(maxC < 0)
        maxC = img.rows;

    acc = new int**[maxA+1];
    for(int i=0; i<maxA+1; ++i) {
        acc[i] = new int*[maxB+1];
        for(int j=0; j<maxB+1; ++j) {
            acc[i][j] = new int[maxC+1];
            for(int k=minC; k<maxC+1; ++k) {
                acc[i][j][k]=0;
            }
        }
    }

    printf("calculating ...\n");
    int percent=0;
    for(int y=0; y<img.rows; ++y) {
        for(int x=0; x<img.cols; ++x) {
            if(img_(y,x)) {
                for(int a=minA; a<=maxA; ++a){
                    for(int b=minB; b<maxB; ++b) {
                        int c=y+a*(x-b)*(x-b)/150;
                        if(c>=minC && c<=maxC) {
                            ++acc[a][b][c];
                        }
                    }
                }
            }
        }
        int p=int(round(double(y)/img.rows*100.));
        if(p!=percent) {
            percent = p;
            printf("> %02d%%\n", percent);
        }
    }
    printf("done!\n");

    sizedpq<Record, RecordCmp> pq(3);
    for(int i=minA; i<=maxA; ++i) {
        for(int j=minB; j<=maxB; ++j) {
            for(int k=minC; k<=maxC; ++k) {
                pq.insert(Record(-i,j,k, acc[i][j][k]));
            }
        }
    }

    for(int i=0; i<=maxA; ++i) {
        for(int j=0; j<=maxB; ++j) {
            delete[] acc[i][j];
        }
        delete[] acc[i];
    }
    delete[] acc;

    std::vector<Parabola> res;
    for(int i=0; i<3; ++i) {
        res.push_back(pq[i].para);
    }
    return res;
}

上面那段代码最诡异而需要说明的地方是 a ,我希望检测的是开口向上的抛物线,于是在计算机图像的坐标系统下 a 的取值应该是负数,但作为数组下标时它当然得要是正数,所以这里多绕了几个弯,下次我自己看到肯定也会忘记的,不过还好这算法相当简单,下次需要用到我肯定更愿意重写。

sizedpq 类是俺以前做马赛克拼接图像时折腾出来的一个固定大小的优先队列类,其实设计得并不好,不过接口挺好用,所以.. 就拿来用了。

 

PS. 速度与图像边缘数量关系很密切,不过通常情况下处理一张上面展示的那种大小的图像也就需要 3 秒钟左右吧,其实还可以接受。

PPS. 人果然还是逼出来的啊,今早老宋很客气的问我节日快乐,弄得我很不好意思的在两个小时之内把这个做好了……

PPPS. 上周末和室友去西湖边逛了一圈,在柳浪闻莺景区里,我刚拍完一张照,发现旁边飘过了一个长发白衣飘逸出尘的美女背影,我说哇美女,他说其实不是美女我已经观察很久了...... 唉,男人。

PPPPS. 谁能教我玩 DotA 啊!!!

流水帐+意识流 [2]

1. C++ 很变态

去网易面试了,一个 C++ 上的问题把我难到了:“多重继承的时候子类是不是要保存每个父类的虚函数表?”

我回答 “通常是的,但虚继承的时候好像 %#@$%#^&*” —— 其实我就记得虚继承的时候情况很有点乱,忘了究竟有多乱。

回来之后做了个实验:

#include <iostream>
#include <iomanip>
#ifdef _MSC_VER // 老兄,特立独行的永远是你!!
typedef unsigned int uint32_t;
#else
#include <stdint.h>
#endif

using namespace std;

class A
{
public:
    uint32_t x;
    uint32_t y;
    A():x(42),y(24){}
    virtual void f(){
        cout<<"A::f()    [this="<<this
            <<"]; this->x="<<x<<"; this->y="<<y<<";\n";
    }
};

class B:virtual public A
{
public:
    B():A(){}
};

class C:virtual public A
{
public:
    C():A(){x=21,y=12;}
    virtual void f(){
        cout<<"C::f()    [this="<<this
            <<"]; this->x="<<x<<"; this->y="<<y<<";\n";
    }
};

class D:public B, public C
{ 
public:
    D():A(),B(),C(){}
};

template<class T>
void showDetail(T const& v)
{
    int s=sizeof(T)/4;
    uint32_t const* p=reinterpret_cast<uint32_t const*>(&v);
    cout<<"  +-------------+\n";
    for(int i=0;i<s;++i) {
        cout<<"  | "<<setw(11)<<p[i]<<" | ";
        if(p[i]>10000) { // suppose it's a pointer
            cout<<" -> "<<*reinterpret_cast<uint32_t const*>(p[i]);
        }
        cout<<endl;
    }
    cout<<"  +-------------+\n";
}

int main(){
    A a;
    B b;
    C c;
    D d;
    cout<<"a:\n";
    showDetail(a);
    cout<<"b:\n";
    showDetail(b);
    cout<<"c:\n";
    showDetail(c);
    cout<<"d:\n";
    showDetail(d);
    return 0;
}

为的是看看虚继承的时候到底发生了什么,结果又是大出意外,GCC, M$VC, DigitalMars 编译出来的竟各不相同:

GCC

a:  +-------------+
    |     4666904 | -> 4324460
    |          42 |
    |          24 |
    +-------------+

b:  +-------------+
    |     4666924 | -> 0
    |     4666936 | -> 4324460
    |          42 |
    |          24 |
    +-------------+

c:  +-------------+
    |     4666956 | -> 4324764
    |     4666972 | -> 4631892
    |          21 |
    |          12 |
    +-------------+

d:  +-------------+
    |     4666988 | -> 4
    |     4667000 | -> 4324764
    |     4667016 | -> 4631892
    |          21 |
    |          12 |
    +-------------+

M$VC

a:  +-------------+
    |     4305572 | -> 4198672
    |          42 |
    |          24 |
    +-------------+

b:  +-------------+
    |     4305668 | -> 0
    |     4305664 | -> 4198672
    |          42 |
    |          24 |
    +-------------+

c:  +-------------+
    |     4305632 | -> 0
    |           0 |
    |     4305628 | -> 4217088
    |          21 |
    |          12 |
    +-------------+

d:  +-------------+
    |     4305684 | -> 0
    |     4305632 | -> 0
    |           0 |
    |     4305680 | -> 4217088
    |          21 |
    |          12 |
    +-------------+

DigitalMars

a:  +-------------+
    |     4448636 | -> 4205756
    |          42 |
    |          24 |
    +-------------+

b:  +-------------+
    |     4448640 | -> 0
    |     4581960 | -> 5856153
    |     4448636 | -> 4205756
    |          42 |
    |          24 |
    +-------------+

c:  +-------------+
    |     4448660 | -> 4205896
    |     4448648 | -> 0
    |     4448656 | -> 4203244
    |          21 |
    |          12 |
    +-------------+

d:  +-------------+
    |     4448660 | -> 4205896
    |     4448672 | -> 0
    |     4448664 | -> 0
    |     5838868 | -> 4500440
    |     4448680 | -> 4203252
    |          21 |
    |          12 |
    +-------------+

好吧,看这情景,我怎么回答都算不上错,因为哪有正确答案啊。

可惜把 Borland C++ Builder 和 Open Watcom 编译器都删了,不然如果看到 5 种不同的结果岂不是更欢乐~ 唉,难怪 C++ 木有 ABI。

另外,被问到虚函数表在对象的什么部位时我发烧似的答道“在尾部”,囧啊,谁教我的。(不过另一方面,可以诡辩一下的是, C++ 2003 标准甚至没有规定运行期多态一定要以虚函数表的形式实现,只是目前的编译器不约而同的采用了虚函数表而已,所以,嗯...)

Update:

Open Watcom 编译出来的结果是这样的:

a:
  +-------------+
  |          42 |
  |          24 |
  |     4240484 | -> 4198782
  +-------------+
b:
  +-------------+
  |     4240544 | -> 0
  |           8 |
  |          42 |
  |          24 |
  |     4240556 | -> 4198782
  +-------------+
c:
  +-------------+
  |     4240488 | -> 0
  |     4240500 | -> 4198952
  |          12 |
  |          21 |
  |          12 |
  |     4240508 | -> 4198866
  +-------------+
d:
  +-------------+
  |     4240520 | -> 0
  |     4240532 | -> 4198952
  |     4240512 | -> 8
  |          16 |
  |          21 |
  |          12 |
  |     4240540 | -> 4199045
  +-------------+

果然又不同——不过,惊喜啊,它还真把虚函数表放在了尾部。

2. 脑袋短路

还是网易面试,几个算法题都发挥得不够满意,甚至包括生成随机数的题,回头想想也有改进的空间。

嗯...不知道试题有没有涉及到某种需要保密的东西,所以暂时先不谈好了……

3. 春天来了

img_0059.jpg
ps. 实践表明,GCC 编译器会给类的成员函数添一个隐含的 this 参数,通常会自动赋值,然而用奇淫手段弄到了函数地址时也可以手动传对象地址去,就像在 python 中对待 self 那样;然而其他的 C++ 编译器似乎都没这么干,至少你把 void A::f() 当作 void f(A*) 来玩是不行的,可惜了,不然会挺好玩的。

标题不知道怎么取

回家了,睡觉,看书,编程,聚会,无聊的时候还做做 ACM 玩,最近就是这样在过吧……嗯,和 MM 闹别扭了,烦,下面是意识流。

 

稍稍 PS 了一下,作壁纸,可惜的是镜头不给力,闪光灯也不给力,于是乎只好用很高的 ISO 来拍这些玩意儿——于是乎噪点很多,木有办法。

 

 

另外,你应该也会被这样的问题折腾过吧:给你一个容量 3 升的杯子和一个容量 5 升的杯子,想要 4 升水,怎么办;一直觉得这问题完全是闲得蛋疼,但某日在 POJ 上面遇到了,倒是觉得有点好玩了,其实就是一个广度优先的搜索(求最优解),啥技术含量都没有:

杯子 1:(ml)
杯子 2:(ml)
目标:(ml)


您若是很无聊,不妨可以试试 11111, 130, 121 之类的变态数据,我看它有那么多个步骤就觉得很欢乐,可能是幸灾乐祸的感觉吧:叫你拿这玩意忽悠我,看我以后怎么忽悠你。( BTW, 做这种实验的时候建议用 Chrome )

Threat Space Search 其实是浮云啊~

黑先,求 VCT (连续冲三取胜):

 

 

 

 

 

 

 

 

解(之一):

对 threat space search 而言,第一步和第九步肯定都无比艰难吧~

threat space search 为了减少分枝数量做了一个很大胆的设定:如果黑成了三,那么允许白把它的两端都堵上,然后如果黑还能赢,那么黑当然是赢定了。

想法很吸引人,Victoria 的战绩也相当不错,但这算法没有考虑到左也是死,右也是死,但左右夹击就不会死的情况,就如同上面这题,多少感觉有些遗憾——不知道这种状况发生的概率大不大。

另外没记错的话他的论文中提到了算法可以有某种 draw-back 机制,但也只是提了一下而已,真不像话。

悲剧啊,只能混个纪念品玩玩

我的程序在这儿了: http://code.google.com/p/harmless/

很想把他叫做 Mostly Harmless,可是不能太狂啊,它明明就是 TOTALLY harmless。

另外眼下 svn 连接不上 google code, 不出意外的话原因应该就是众所周知的那个吧,我最近也懒得折腾了,所以代码先就在压缩包里看吧。

核心代码在这里: http://code.google.com/p/harmless/source/browse/trunk/main.cpp

Windows / Linux 可执行程序以及源码压缩包在这里: http://code.google.com/p/harmless/downloads/list

 

用到的技术有 alpha-beta, iterative deepening, transposition table, null move heuristic, bit board, threat space search (没写完而且不准的版本), principal variation ,然而这些算法中没有几个是容易调试的,再加上核心的评价函数依然是瞎掰的(试过用进化算法让它进化出一个比较靠谱的参数,然而结果是它进化到了一种自娱自乐的境界,和自己这种风格的AI竞赛倒还好,可实际棋力依旧不行),再加上整体框架上还有好些不成熟的地方,比如说我干嘛要记录下某个空位在四个方位的子力呢,记录下最好的两个方位就OK了啊——反正,改来改去,最后一次练习赛之后的版本棋力竟然越变越差,真伤心。

泥人五子棋里面似乎有好几个可以借鉴的想法,开局库可以多折腾几条记录进去,proof number search + threat space search + 开局库效果应该就会还可以了吧;另外可惜 db-search 接触得太晚了,它的思想也很吸引人。

寒假重写。

 

 

最后,附上两张几天前在厕所玩水的照片:

 

还有好多张,有时间了再传到网易去。

KMP 算法的复杂度和最差情况

此段已经加入到了俺写的 MP/KMP 算法详解 中了;缺少这一段正是它一直保持着草稿状态的主要原因;但由于害怕疏忽,目前也还不敢改变其状态。(囧)

好吧,下面开始正文

6   复杂度分析

至于为什么 KMP 算法的复杂度是线性的,我们再回头看看 另一种表示 一节中的算法主体:

int MP(char const* pattern, char const* text)
{
    int i=0,j=0,m=strlen(pattern);
    int F[m+1];
    calcF (pattern, F);
    for(;text[i];++i,++j) { // j 增大,i 增大
        while(j>=0 && pattern[j]!=text[i]) {
            j = F[j];       // j 减小
        }
        if(j>=m-1) {
            return (i+1-m);
        }
    }
    return -1;
}

i 只有增大的份,所以 ++i 最多执行 n 次,这个很显然。

j 初始值为 0,一共增加了 n 次,而 j>=-1 ,于是 j = F[j] 这一句最多也就执行了 n+1 次(否则就会出现 j<-1 的情况了)。

所以就是线性的了!

7   KMP 算法的最长停顿

为了说明 KMP 算法在文本串上的某一个字符上进行了很多次比较的极限情况(也就是所谓的停顿或者E文的 delay ),我们首先要介绍一下 “费波拉契串” —— 因为,很巧,它就是能使 KMP 算法达到最糟糕状况的模式串,一会儿我们会说到。

提到 “费波拉契” ,相信不少人会直接想到 1, 1, 2, 3, 5, 8, 13, 21, 34 ... ,是的,费波拉契串的定义也十分类似:

设 P 是个费波拉契串,那么:

P[0] = b
P[1] = a
P[i] = P[i-1]P[i-2]

所以:

P[2] = ab
P[3] = aba
P[4] = abaab
P[5] = abaababa
P[6] = abaababaabaab
P[7] = abaababaabaababaababa
P[8] = abaababaabaababaababaabaababaabaab
...

计算出 P[7] 的 F 数组和 Next 数组如下,我们一会儿要用到,你也可以先把它当作找规律的题看看:

P a b a a b a b a a b a a b a b a a b a b a  
n 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
F -1 0 0 1 1 2 3 2 3 4 5 6 4 5 6 7 8 9 10 11 7 8
Next -1 0 -1 1 0 -1 3 -1 1 0 -1 6 0 -1 3 -1 1 0 -1 11 -1 8

费波拉契串有几个性质值得注意一下:

Note

文章前面已经假设了我的世界里费波拉契数列下标是从 0 开始的,这里再强调一遍。

  1. strlen(P[n]) == Fibonacci[n] (这个应该很容易理解吧)

  2. 设函数 c(t) 的作用是交换字符串 t 的最后两个字符,例如 c("abcdef") == "abcdfe",那么当 n>=2 时 P[n-1]P[n-2] == c(P[n-2]P[n-1]) :
    n == 2 时这是显然的;
    当 n>2 时可以用数学归纳法证明:
    c(P[n-2]P[n-1]) = P[n-2]c(P[n-1]) = P[n-2]P[n-3]P[n-2] = P[n-1]P[n-2]
  3. 由上面两条性质我们又可以推导出:
    Next[Fibonacci[n]-2] == F[Fibonacci[n]-2] == Fibonacci[n-1]-2, n>=2
    这是因为,可以把一个费波拉契串分解开:
    P[n] == P[n-1]P[n-2] == P[n-2]P[n-3]P[n-2] == c(P[n-3]P[n-2])P[n-2] == P[n-3]c(P[n-2])P[n-2] == ...
    具体以 P[7] 为例,
    P[7] == ab-ab-ba---ab------ba
    其中省略掉的部分根据 性质2 表现出的规律与前方相等,因此如果在 P[7] 的最后一个字符 b 处发生了不匹配,接下来应该在下列位置重新试着匹配:
    ab-a--b----a-------b-
    它们正好占据着 Fibonacci[2]-2, Fibonacci[3]-2, .. , Fibonacci[7]-2, ... 的位置。

因此,如果在费波拉契串的第 n 位,n == Fibonacci[k]-2 上发生了不匹配,接下来则还需要 k-1 次比较;

又因为 Fibonacci[k-1] == (φk - (-1)kφ-k)/sqrt(5) == round( φk / sqrt(5) ), 于是可以解得 k ~ logφ(n),其中 φ 是黄金比例 (1+sqrt(5))/2 == 1.618...

—— k 便是文本串上的一个字符的最多比较次数。

8   为什么费波拉契串这么神奇

为了证明为何费波拉契串就是使停顿时间最长的模式串,我们再看看 MP 算法的基本思想:将字符串已匹配的一个前缀和一个对应的后缀匹配。

假设字符串 S 有且仅有一个相等的前缀和后缀(设为 a ),那么 S 可以表示为

S = aB = Ca

再假设 a 本身也有且仅有一个相等的前缀和后缀(设为 e ),那么 a 也可以表示为

a = eF = Ge

对应 MP 算法,匹配 Ca 时若在 a 之后失败,则会将 aB 的 a 与其对齐:

Cax
Cay

==>

Cax
 aBy

若在 B 的第一个字符处再次失败,则下一次对齐是这样的:

CGex
 GeBy

==>

CGex
  eFBy

KMP 算法在这里还要求 F 的第一个字符和 B 的第一个字符不等(否则会跳过这一段)

我们很容易可以证明想要 KMP 算法在这个地方停留尽可能长的时间需要满足 |S| <= |e| + |a| :因为若 |e| + |a| > |S|,那么令 d = |e| + |a| - |S| 则 Ca = CGe = aB 算式中, a 和 e 将有长度为 d 的重叠,于是 B 的第一个字符等于 e[d];同理,在 aB = eFB = Ca 算式中,可以得到 F 的第一个字符为 a[d],由 a = eF 可以得到 a[d] = e[d],和 KMP 的要求不符。

于是 |S| == |e| + |a| 是使 KMP 算法的停顿时间达到最长的极限情况——很容易发现,满足这条件的便是费波拉契串了。

聊聊 memcpy

嗯 .. 提到 memcpy 你是不是就条件反射的想到了这样的东西呢:

void * memcpy(void * dst, const void * src, size_t size)
{
    size_t i=0;
    unsigned char * _dst = dst;
    const unsigned char * _src = src;
    for(; i<size; ++i) {
        _dst[i] = _src[i];
    }
    return dst;
}

其实吧,memcpy 里面还是很有学问的 ... 首先从一个新闻说起吧

最近某个由 memcpy 引发的血案似乎引起了不少人关注,事情大概是 glibc 使用了一个号称经过优化的 memcpy 函数,这个函数对源地址和目标地址 重叠 的情况没有处理(虽然实际上 memcpy 函数也不要求处理这种情况),于是 Adobe Flash Player 闹情绪了,原因在于那个 tricky 的 memcpy 函数作风比较诡异,在某些机器上它是把数据 从后往前 拷贝的,对此 Linus 的评价是:

That said - why the heck would you ever do memcpy() backwards? Things like automatic prefetchers usually work better for the "natural" patterns, so a backwards memcpy is generally a bad thing to do unless you have some active reason for it, like doing a memmove()

不过我其实更觉得这完全是 Adobe 的问题,因为如果从后往前拷贝它就会出问题,那么遇到下面这种情况,从前往后拷贝也可以照样整垮它:

A: bbbbbaaaaa
B:      aaaaabbbbb

memcpy(B,A,10);

Anyway,事情到现在已经基本上算是平息了: glibc 相信问题不在于 memcpy 而在于 Adobe 没有正确使用它,问题不再讨论;Linus 则给出了一个暂行方案,他的那个方案已经非常优秀了以至于想想很有可能破坏掉读者的挑战欲望,所以还是留着过一会儿再说吧……

现在我们回到主题上,memcpy,怎么样让它更快些呢。

首先, dst[i] = src[i] 这一句实际上的效率就相当于 *(dst+i) = *(src+i) , 这里的加法显得有点太过分了。稍微修改一下下:

void * memcpy(void * dst, const void * src, size_t size)
{
    unsigned char * _dst = dst;
    const unsigned char * _src = src, * _src_end;
    for(_src_end = _src+size; _src<_src_end; ++_src, ++_dst) {
            * _dst = * _src;
    }
    return dst;
}

Andorid 上面的 libc 似乎就是类似这样的实现了。

但是,其实还不够,拷贝 size 个字节,比较运算就执行了 size 次,可不可以少一点?

答案:Yes!

方案一:Duff's Device

Duff's Device 是个非常 tricky 的东西,以至于你第一眼见到很可能怀疑“这货真能通过编译?”

void * memcpy(void * dst, const void * src, size_t size)
{
    register n=(size+7)/8;
    register unsigned char * _dst = dst;
    register const unsigned char * _src = src;
    switch(size%8){
        case 0:     do{     *_dst++ = *_src++;
        case 7:             *_dst++ = *_src++;
        case 6:             *_dst++ = *_src++;
        case 5:             *_dst++ = *_src++;
        case 4:             *_dst++ = *_src++;
        case 3:             *_dst++ = *_src++;
        case 2:             *_dst++ = *_src++;
        case 1:             *_dst++ = *_src++;
        } while(--n>0);
    }
}

这样,被 switch 一次之后便陷入了 do while 循环;这里 case 0 - case 7 如果不够还可以再加, 此段代码相当于把上面的小于比较省掉了 7/8 。

方案二:不要一个字节一个字节操作,而要一个字一个字的处理:

通常一个机器字有好几个字节长 —— 比如 32 位机器上是 4 个字节,64 位机器上是 8 字节。

将每 n 个字节作为一个整体“超级字符”进行操作,在这种场合算是很常见的应用了:

void * memcpy(void * dst, const void * src, size_t size)
{
    void * orig = dst;
    asm volatile("rep ; movsq"
        :"=D" (dst), "=S" (src)
        :"0" (dst), "1" (src), "c" (size >> 3)
        :"memory");
    asm volatile("rep ; movsb"
        :"=D" (dst), "=S" (src)
        :"0" (dst), "1" (src), "c" (size & 7)
        :"memory");
    return orig;
}

解释一下,这里汇编代码的原理也就是先把 size/8 个字(size>>3)拷贝过去(movsq), 再把后 size%8 个字节(size&7)拷贝过去(movsb)

这个就是 Linus 给出的临时方案了。

另外这里一个字是八个字节,那是因为这里出现症状的机器是 64 位的。

方案三(其实不算方案三):结合以上二者

以字为单位,用 Duff's Device 操作…… 这个想想就觉得挺牛的,Newlib 似乎就是这么玩的了:

/* Nonzero if either X or Y is not aligned on a "long" boundary.  */
#define UNALIGNED(X, Y) \
  (((long)X & (sizeof (long) - 1)) | ((long)Y & (sizeof (long) - 1)))

/* How many bytes are copied each iteration of the 4X unrolled loop.  */
#define BIGBLOCKSIZE    (sizeof (long) << 2)

/* How many bytes are copied each iteration of the word copy loop.  */
#define LITTLEBLOCKSIZE (sizeof (long))

/* Threshhold for punting to the byte copier.  */
#define TOO_SMALL(LEN)  ((LEN) < BIGBLOCKSIZE)

_PTR
_DEFUN (memcpy, (dst0, src0, len0),
        _PTR dst0 _AND
        _CONST _PTR src0 _AND
        size_t len0)
{
#if defined(PREFER_SIZE_OVER_SPEED) || defined(__OPTIMIZE_SIZE__)
  char *dst = (char *) dst0;
  char *src = (char *) src0;

  _PTR save = dst0;

  while (len0--)
    {
      *dst++ = *src++;
    }

  return save;
#else
  char *dst = dst0;
  _CONST char *src = src0;
  long *aligned_dst;
  _CONST long *aligned_src;

  /* If the size is small, or either SRC or DST is unaligned,
     then punt into the byte copy loop.  This should be rare.  */
  if (!TOO_SMALL(len0) && !UNALIGNED (src, dst))
    {
      aligned_dst = (long*)dst;
      aligned_src = (long*)src;

      /* Copy 4X long words at a time if possible.  */
      while (len0 >= BIGBLOCKSIZE)
        {
          *aligned_dst++ = *aligned_src++;
          *aligned_dst++ = *aligned_src++;
          *aligned_dst++ = *aligned_src++;
          *aligned_dst++ = *aligned_src++;
          len0 -= BIGBLOCKSIZE;
        }

      /* Copy one long word at a time if possible.  */
      while (len0 >= LITTLEBLOCKSIZE)
        {
          *aligned_dst++ = *aligned_src++;
          len0 -= LITTLEBLOCKSIZE;
        }

       /* Pick up any residual with a byte copier.  */
      dst = (char*)aligned_dst;
      src = (char*)aligned_src;
    }

  while (len0--)
    *dst++ = *src++;

  return dst0;
#endif /* not PREFER_SIZE_OVER_SPEED */
}

方案四,有请硬件牛

翻出 glibc 2.9 的代码,它的代码看上去倒是不长:

void *
memcpy (dstpp, srcpp, len)
     void *dstpp;
     const void *srcpp;
     size_t len;
{
  unsigned long int dstp = (long int) dstpp;
  unsigned long int srcp = (long int) srcpp;

  /* Copy from the beginning to the end.  */

  /* If there not too few bytes to copy, use word copy.  */
  if (len >= OP_T_THRES)
    {
      /* Copy just a few bytes to make DSTP aligned.  */
      len -= (-dstp) % OPSIZ;
      BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ);

      /* Copy whole pages from SRCP to DSTP by virtual address manipulation,
     as much as possible.  */

      PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len);

      /* Copy from SRCP to DSTP taking advantage of the known alignment of
     DSTP.  Number of bytes remaining is put in the third argument,
     i.e. in LEN.  This number may vary from machine to machine.  */

      WORD_COPY_FWD (dstp, srcp, len, len);

      /* Fall out and copy the tail.  */
    }

  /* There are just a few bytes to copy.  Use byte memory operations.  */
  BYTE_COPY_FWD (dstp, srcp, len);

  return dstpp;
}

从注释可以看出,现在它在玩的都是些硬件相关的 trick 了 (更别提 Linux Kernel 中的那些对应不同处理器的汇编实现),于是我发现我似乎不太适合再继续挖下去了。




文章最后,我想以高中语文考试作文体结尾,非常感谢所有这些 —— hacker 也好 geek 也好 —— 做了这么多有意思又有意义的工作, 让我们有 Linux 这么优秀的操作系统可以用;正是由于这样的细节,这样的竞争气氛,这一点一滴的积累,才有了( 我自己的测试中 )比 Win7 快上近 1/3 的速度,让人没有理由再质疑开源的力量。

最长回文子串 [后缀树应用之三]

为什么有很多人讨论这个问题呢——仅仅因为它的学术价值吗?

 

通过后缀树查找最长回文子串应该说是很容易的:将字符串 s 和 inverse(s) 加入到后缀树中,找出最深的、在正反两个字符串中出现的位置相匹配的节点就是了。

实际玩了一下发现不是那么有意思。

《银河系漫游指南》中的最长回文子串:

mmmmmmmmmmmmmmmmmmmmm

《宇宙尽头的餐馆》 - 

rrrrrrrrrrrr

《生命,宇宙,以及一切》 - 

mmmmm mmm mmmmm

《魔戒》 -

e did i did e

《飘》 - 

t now i won t

《1984》 -

ton did not

...

 

大概就是这样吧。

最长公共子串 [后缀树应用之二]

1. 我干了什么

我写了个小程序,用 1.276 秒找出了《银河系漫游指南》和《宇宙尽头的餐馆》的最长公共子串:

the history of every major galactic civilization tends to pass through three distinct and recognizable phases, those of survival, inquiry and sophistication, otherwise known as the how, why and where phases.

用 1.266 秒找出了《宇宙尽头的餐馆》和《再见,谢谢所有的鱼》的最长公共子串:

in the uncharted backwaters of the unfashionable end of the western spiral arm of the galaxy

用 8.671 秒找出了《魔戒》和《1984》的最长公共子串:

. it was inevitable that they should make

用 6.728 秒找出了《飘》和《基本无害》的最长公共子串:

more than anything else in the world.

...

我发现这是件很好玩的事情~

2. 怎么做的

Again,有了 后缀树 ,这样的工作太简单了:

/*
 * Author  : If
 * Date    : Oct/16th/2010
 * License : Boost License v1

Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license  (the "Software")  to use,  reproduce,  display,  distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:

The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED,  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/

#include "suffixtree.h"
#include <algorithm>
#include <iterator>
#include <iostream>
#include <fstream>
#include <cctype>
#include <cstdio>
#include <string>
#include <vector>
#include <ctime>
using namespace std;

static void ticToc_(bool s)
{
    static clock_t start_ = clock();
    if(s) {
        start_ = clock();
    } else {
        clock_t end_=clock();
        std::cerr
            << (end_ - start_) << " ticks"
            << "    (" << (end_ - start_) * (1000.)/CLOCKS_PER_SEC <<"ms)"
            << std::endl;
    }
}

inline void tic()
{
    ticToc_(true);
}

inline void toc()
{
    ticToc_(false);
}

struct Info
{
    vector<int> belongTo;
};

typedef Node<char, Info>       NodeT;
typedef SuffixTree<char, Info> STree;

// 初始化节点的从属属性。
void initInfo(NodeT& leaf)
{
    NodeT* node = leaf.parent;
    int    nth  = leaf.belongTo;
    while ( node->parent!=node && (
            node->info.belongTo.empty() ||
            node->info.belongTo.back()!=nth )) {
        node->info.belongTo.push_back(nth);
        node=node->parent;
    }
}

void printPath(NodeT const* node)
{
    if(node->parent!=node)
        printPath(node->parent);
    copy(node->str, node->str+node->len,
         ostream_iterator<char>(cout));
}

// 将连续的空白字符转换成单个空格;将大写全转换为小写。
void filter(char * text, int & len)
{
    char*p=text,*q=text;
    for(;q<text+len;++p,++q){
        if(*q>='A'&&*q<='Z') {
            *p=*q-'A'+'a';
        } else if(isspace(*q)) {
            *p=' ';
            while(isspace(*q))
                ++q;
            --q;
        } else {
            *p=*q;
        }
    }
    len = p-text;
}

struct LetsRock
{
    NodeT const*& result;
    int   longestNow;
    int   nStrings;
    LetsRock(int n, NodeT const*& root):
        result(root),
        longestNow(0),
        nStrings(n)
    { }
    void operator()(NodeT const& node) {
        if (node.depth + node.len > longestNow &&
            node.info.belongTo.size()==nStrings) {
            longestNow = node.depth + node.len;
            result     = &node;
        }
    }
};

int main(int c,char**v)
{
    STree tree;
    tic();
    for(int i=1;i<c;++i) {
        cerr<<"reading "<<v[i]<<" ...\n";
        FILE*  file = fopen(v[i],"r");
        string content;
        char   buf[8192];
        int    bytesRead=0;
        int    bytesTotal=0;
        while((bytesRead=fread(buf,1,8192,file))==8192) {
            filter(buf,bytesRead);
            content.append(buf,bytesRead);
            bytesTotal += bytesRead;
        }
        if(bytesRead) {
            filter(buf,bytesRead);
            content.append(buf,bytesRead);
            bytesTotal += bytesRead;
        }
        fclose(file);
        cerr<<"done. ( " << bytesTotal <<" bytes )\n";
        cerr<<"adding to suffix tree ...\n";
        tree.addString(content);
        cerr<<"done.\n";
    }
    NodeT const* ans = tree.root();
    tree.eachLeaf(initInfo);
    LetsRock fun(c-1,ans);
    tree.dfs(fun);
    printPath(ans);
    cout<<endl;
    toc();
    return 0;
}

代码压缩工具原型 [后缀树应用之一]

嗯,对我来说是代码压缩工具原型;对大多数地球生物来说这其实是最长不重叠重复子串问题

1. 我干了什么

我写了个程序,运行时内存占用峰值为 323 MB,耗时 6.582s,从 2,827,372 字节的《魔戒》全本中找出了不算空格、大小写敏感的情况下出现两次或更多的最长子串:

All that is gold does not glitter,
Not all those who wander are lost;
The old that is strong does not wither,
Deep roots are not reached by the frost.
 
From the ashes a fire shall be woken,
A light from the shadows shall spring;
Renewed shall be blade that was broken,
The crownless again shall be king.

耗时 7.022 秒,内存占用峰值 351 MB,从 3,872,493 字节的 sqlite 3.6.22 的合并 c 文件中找到了最长重复子串:(忽略空格)

.c****************//**************Beginfileos_common.h********************
****** *************//***2004May22****Theauthordisclaimscopyrighttothissou
rcecode.Inpla ceof**alegalnotice,hereisablessing:****Mayyoudogoodandnotevi
l.**Mayyoufindforgiv enessforyourselfandforgiveothers.**Mayyousharefreely,
nevertakingmorethanyougive.  *********************************************
*********************************** ****Thisfilecontainsmacrosandalittlebi
tofcodethatiscommonto**alloftheplatform-sp ecificfiles(os_*.c)andis#includ
edintothose**files.****Thisfileshouldbe#includedb ytheos_*.cfilesonly.Itis
nota**generalpurposeheaderfile.*/#ifndef_OS_COMMON_H_#de fine_OS_COMMON_H_
/***AtleasttwobugshaveslippedinbecausewechangedtheMEMORY_DEBUG* *macrotoSQ
LITE_DEBUGandsomeoldermakefileshavenotyetmadethe**switch.Thefollowingc ode
shouldcatchthisproblematcompile-time.*/#ifdefMEMORY_DEBUG#error&quot;TheME
MORY_DEB UGmacroisobsolete.UseSQLITE_DEBUGinstead.&quot;#endif#ifdefSQLITE
_DEBUGSQLITE_PRIVATE intsqlite3OSTrace=0;#defineOSTRACE1(X)if(sqlite3OSTra
ce)sqlite3DebugPrintf(X)#de fineOSTRACE2(X,Y)if(sqlite3OSTrace)sqlite3Debu
gPrintf(X,Y)#defineOSTRACE3(X,Y,Z) if(sqlite3OSTrace)sqlite3DebugPrintf(X,
Y,Z)#defineOSTRACE4(X,Y,Z,A)if(sqlite3OST race)sqlite3DebugPrintf(X,Y,Z,A)
#defineOSTRACE5(X,Y,Z,A,B)if(sqlite3OSTrace)sqli te3DebugPrintf(X,Y,Z,A,B)
#defineOSTRACE6(X,Y,Z,A,B,C)\if(sqlite3OSTrace)sqlite3D ebugPrintf(X,Y,Z,A
,B,C)#defineOSTRACE7(X,Y,Z,A,B,C,D)\if(sqlite3OSTrace)sqlite3D ebugPrintf(
X,Y,Z,A,B,C,D)#else#defineOSTRACE1(X)#defineOSTRACE2(X,Y)#defineOSTRA CE3(
X,Y,Z)#defineOSTRACE4(X,Y,Z,A)#defineOSTRACE5(X,Y,Z,A,B)#defineOSTRACE6(X,
Y, Z,A,B,C)#defineOSTRACE7(X,Y,Z,A,B,C,D)#endif/***Macrosforperformancetra
cing.Norm allyturnedoff.Onlyworks**oni486hardware.*/#ifdefSQLITE_PERFORMAN
CE_TRACE/***hwti me.hcontainsinlineassemblercodeforimplementing**high-perf
ormancetimingroutines.* //**************Includehwtime.hinthemiddleofos_com
mon.h****************//******* *******Beginfilehwtime.h*******************
***********************//***2008May27 ****Theauthordisclaimscopyrighttothi
ssourcecode.Inplaceof**alegalnotice,hereisab lessing:****Mayyoudogoodandno
tevil.**Mayyoufindforgivenessforyourselfandforgiveo thers.**Mayyousharefre
ely,nevertakingmorethanyougive.*************************** ***************
******************************************Thisfilecontainsinlinea smcodefo
rretrieving&quot;high-performance&quot;**countersforx86classCPUs.*/#ifndef
_HWTIME_ H_#define_HWTIME_H_/***Thefollowingroutineonlyworksonpentium-clas
s(ornewer)proce ssors.**ItusestheRDTSCopcodetoreadthecyclecountvalueoutoft
he**processorandreturn sthatvalue.Thiscanbeusedforhigh-res**profiling.*/#i
f(defined(__GNUC__)||defined( _MSC_VER))&amp;&amp;\(defined(i386)||defined
(__i386__)||defined(_M_IX86))#ifdefined(__GN UC__)__inline__sqlite_uint64s
qlite3Hwtime(void){unsignedintlo,hi;__asm____volati le__(&quot;rdtsc&quot;
:&quot;=a&quot;(lo),&quot;=d&quot;(hi));return(sqlite_uint64)hi&lt;&lt;32|
lo;}#elifdefined(_MS C_VER)__declspec(naked)__inlinesqlite_uint64__cdeclsq
lite3Hwtime(void){__asm{rdt scret;returnvalueatEDX:EAX}}#endif#elif(define
d(__GNUC__)&amp;&amp;defined(__x86_64__))_ _inline__sqlite_uint64sqlite3Hw
time(void){unsignedlongval;__asm____volatile__(&quot;r dtsc&quot;:&quot;=A
&quot;(val));returnval;}#elif(defined(__GNUC__)&amp;&amp;defined(__ppc__))
__inline__ sqlite_uint64sqlite3Hwtime(void){unsignedlonglongretval;unsigne
dlongjunk;__asm__ __volatile__(&quot;\n\1:mftbu%1\n\mftb%L0\n\mftbu%0\n\cm
pw%0,%1\n\bne1b&quot;:&quot;=r&quot;(retval) ,&quot;=r&quot;(junk));return
retval;}#else#errorNeedimplementationofsqlite3Hwtime()foryour platform./**
*Tocompilewithoutimplementingsqlite3Hwtime()foryourplatform,**youcan remov
etheabove#errorandusethefollowing**stubfunction.Youwilllosetimingsupportfo
r many**ofthedebuggingandtestingutilities,butitshouldat**leastcompileandru
n.*/SQLI TE_PRIVATEsqlite_uint64sqlite3Hwtime(void){return((sqlite_uint64)
0);}#endif#endi f/*!defined(_HWTIME_H_)*//**************Endofhwtime.h*****
********************** *******************//**************Continuingwherew
eleftoffinos_common.h******** **********/staticsqlite_uint64g_start;static
sqlite_uint64g_elapsed;#defineTIMER_ STARTg_start=sqlite3Hwtime()#defineTI
MER_ENDg_elapsed=sqlite3Hwtime()-g_start#de fineTIMER_ELAPSEDg_elapsed#els
e#defineTIMER_START#defineTIMER_END#defineTIMER_EL APSED((sqlite_uint64)0)
#endif/***IfwecompilewiththeSQLITE_TESTmacroset,thenthefo llowingblock**of
codewillgiveustheabilitytosimulateadiskI/Oerror.This**isusedfort estingthe
I/Orecoverylogic.*/#ifdefSQLITE_TESTSQLITE_APIintsqlite3_io_error_hit=0 ;/
*TotalnumberofI/OErrors*/SQLITE_APIintsqlite3_io_error_hardhit=0;/*Numbero
fnon -benignerrors*/SQLITE_APIintsqlite3_io_error_pending=0;/*Countdowntof
irstI/Oerro r*/SQLITE_APIintsqlite3_io_error_persist=0;/*TrueifI/Oerrorspe
rsist*/SQLITE_APIi ntsqlite3_io_error_benign=0;/*Trueiferrorsarebenign*/SQ
LITE_APIintsqlite3_diskfu ll_pending=0;SQLITE_APIintsqlite3_diskfull=0;#de
fineSimulateIOErrorBenign(X)sqli te3_io_error_benign=(X)#defineSimulateIOE
rror(CODE)\if((sqlite3_io_error_persist &amp;&amp;sqlite3_io_error_hit)\||
sqlite3_io_error_pending--==1)\{local_ioerr();CODE;}st aticvoidlocal_ioerr
(){IOTRACE((&quot;IOERR\n&quot;));sqlite3_io_error_hit++;if(!sqlite3_io _e
rror_benign)sqlite3_io_error_hardhit++;}#defineSimulateDiskfullError(CODE)
\if( sqlite3_diskfull_pending){\if(sqlite3_diskfull_pending==1){\local_ioe
rr();\sqlit e3_diskfull=1;\sqlite3_io_error_hit=1;\CODE;\}else{\sqlite3_di
skfull_pending--;\ }\}#else#defineSimulateIOErrorBenign(X)#defineSimulateI
OError(A)#defineSimulateD iskfullError(A)#endif/***Whentesting,keepacounto
fthenumberofopenfiles.*/#ifdefSQ LITE_TESTSQLITE_APIintsqlite3_open_file_c
ount=0;#defineOpenCounter(X)sqlite3_ope n_file_count+=(X)#else#defineOpenC
ounter(X)#endif#endif/*!defined(_OS_COMMON_H_) *//**************Endofos_co
mmon.h*******************************************//** ************Continui
ngwhereweleftoffinos_ 

正如之前在 后缀树的介绍 中说过的那样,俺想要弄出个能压缩 C 程序代码的工具,而上面的就是原型了:找出出现多次且较长的子串, #define 一下,重复多次,差不多了。

2. 怎么做的

有了后缀树,这样的工作可真是简单啊:

/*
 * Author  : If
 * Date    : Oct/14th/2010
 * Version : 0.0.1
 */
/*
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license  (the "Software")  to use,  reproduce,  display,  distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:

The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED,  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/

#include "suffixtree.h"
#include <algorithm>
#include <iterator>
#include <iostream>
#include <string>
#include <cmath>
#include <ctime>
using namespace std;

static void ticToc_(bool s)
{
    static clock_t start_ = clock();
    if(s) {
        start_ = clock();
    } else {
        clock_t end_=clock();
        std::cerr
            << (end_ - start_) << " ticks"
            << "    (" << (end_ - start_) * (1000.)/CLOCKS_PER_SEC <<"ms)"
            << std::endl;
    }
}

inline void tic()
{
    ticToc_(true);
}

inline void toc()
{
    ticToc_(false);
}


struct Info {
    int cv,depth,isLeaf;
    Info():cv(-1),depth(-1),isLeaf(0){}
};

typedef Node<char,Info> NodeT;

void initInfo(NodeT& node) {
    node.info.depth = node.depth + node.len;
    if(node.branches.empty()){
        node.info.isLeaf = 1;
        node.info.cv = node.leafNum;
    } else {
        node.info.isLeaf = 0;
        NodeT* p = node.branches[0];
        if(p->info.cv==-1) {
            initInfo(*p);
        }
        node.info.cv = p->info.cv;
    }
}

void printPath(NodeT const* node)
{
    if(node->parent!=node) {
        printPath(node->parent);
    }
    copy(node->str, node->str+node->len, ostream_iterator<char>(cout));
}

struct LetsHaveFun
{
    NodeT const*& result;
    LetsHaveFun(NodeT const*& root):result(root){}
    void operator()(NodeT& node){
        if(!node.info.isLeaf &&  // more than once
            node.depth != 0 &&   // not root
            node.info.depth > result->info.depth && // better
            abs(node.info.cv - node.branches.back()->info.cv) >=
                node.info.depth  // do not overlap
        ) {
            this->result = &node;
        }
    }
};

int main(int c,char**v) {
    if(c>2)
        return 1;
    SuffixTree<char, Info> stree;
    NodeT const* ans = stree.root();
    LetsHaveFun fun(ans);
    string source;
    if(c==2&&v[1][0]=='-') {
        if(v[1][1]=='S') // do not skip white space
            cin>>noskipws;
        else if(v[1][1]=='s')
            cin>>skipws;
        else
            return 1;
    } else {
        return 1;
    }

    copy(istream_iterator<char>(cin), istream_iterator<char>(),
         back_inserter(source));
    tic();
    stree.addString(source);
    stree.dfs(initInfo);
    stree.dfs(fun);
    printPath(ans);
    cout<<endl;
    toc();
}

3. Question

这次有问题顺便想要请教一下高人们,在上面的代码中有一段让我很不爽:

struct LetsHaveFun
{
    NodeT const*& result;
    ...

我的本意是想用 NodeT const* result ; 然而那么做的话 stree.dfs(fun) 这句话结束之后 fun.result 又回到了 root (LetsHaveFun::operator() 中 result 的确会变的!),百思不得其解,似乎碰到了某个语法的黑暗角落;然而在这个问题上 VC2008 和 GCC 4.5.0 竟然表现得出奇的一致以至于我开始怀疑自己了,谁来救救我?

4. PostScript

suffixtree.h 全部内容在我的 后缀树的介绍 中。

5. Update

Ubuntu 威武,同样的代码,同样的文件系统,同样的计算机配置,同样的编译器,同样的编译选项,在 Ubuntu 上面,此处(Win7)耗时 6.5 秒的程序变得只要 4.5 秒了,Astonishing

Suffix Tree 学习笔记 I

在这篇文章中你将会看到:





* 什么是后缀树

* 后缀树有啥用

* 如何构建后缀树

* 构建后缀树的算法复杂度分析

* 具体实现的源代码

* 以及更多









文章本身很长……还是点进来看吧…… ..more

图:mississippimississippi + iamif 的后缀树

花了很多功夫,俺终于实现了生成后缀树的线性算法,添加多个字符串真麻烦,而且后缀树的结构也真够复杂的,有图为证:


Suffix-Tree(mississippimissippi)+SuffixTree(iamif)

黑线是父子关系,红线是后缀链接,图像是用俺的程序生成 dot 文件、然后用 Graphviz 自动生成的(真丑),程序以及算法下次再解释,那也预计将是俺本学期的最后一篇日志,没时间复习了。

BTW,近期通过 www.if-yu.info 域名访问俺的博客似乎很慢 —— 那么直接访问 http://iamyuguo.appspot.com/ 应该好些

MP/KMP 算法详解 [草稿]

MP/KMP 算法详解

By If

1   Prologue

本篇文章主要针对的是对字符串匹配有兴趣的生物以及被某版本数据结构与算法教材中的 KMP 算法讲解弄得不知所云但与此同时却还难能可贵地保持着旺盛求知欲的不幸生在了错误年代的可怜童鞋,其他生物阅读本文前请慎重考虑因为它 可能对您的大脑(如果有)、小脑甚至包括脊髓都造成严重且不可恢复的创伤

2   Notations

下文可能会提到 “模式串”、“文本串”、“窗口” 这些词,它们的定义如下,如果这些文字使你头晕,请及时做好救治准备。

模式串、文本串
所谓 模式串 ,是指你想要找到(或者得到位置 &etc.)的字符串;而 文本串 ,则是指搜索的目标字符串。
比如说你要在 "lucky dog" 中寻找 "dog" ,那么 "dog" 是模式串, "lucky dog" 则是文本串;
而你若要在 "If is a lucky dog" 中寻找 "lucky dog" ,那么 "lucky dog" 便成了模式串, "If is a lucky dog" 则是文本串。

Understand?

窗口
无论用什么样的搜索算法,在搜索的过程中,总是需要将模式串与文本串进行比较,它们对齐的那部分区域,也就是们关心的那块区域,咱称为 窗口

另外,为了避免让已经适应 C/C++/C#/D/Java/JavaScript/Python/Go/... 语言思维的童鞋多绕一个弯,本文用到的数组下标都以 0 开始 —— 甚至包括费波拉契数列也如此。

3   Main Idea

MP/KMP 字符串搜索算法的思想精华在于利用已经匹配的部分包含的信息,加速搜索的过程。

嗯——已经匹配的部分包含什么信息?

它已经匹配了!

举例说,在某个字符串中查找子串 A B C D A B D A C 时,如果遇到 A B C D A B ,而紧跟其后的不是 D ,这时候我们可以将窗口右移四位(而不是一位),因为既然 A B C D A B 已经匹配了, 那么移动当前窗口之后 已经匹配过的地方 肯定需要保证 依然匹配 ,这里最好的做法即让 A B 相互对齐:

. . A B C D A B ? . . . . . .
    A B C D A B D A C

=>

. . A B C D A B ? . . . . . .
            A B C D A B D A C

因为,看呀,如果只右移一位,那么:

. . . . . . . . . . . . . . . // 先不用管这个字符串
    A B C D A B . . .         // 已经匹配的部分
=>    A B C D A B D A C
      |
    糟糕!

如上面所示, A B C D A B 匹配了,那么移动一位之后,第一个字符 A 就肯定会对着 B ,绝对不可能在这个地方找到匹配

右移两位、三位或者四位时发生的状况可以依此类推;而右移四位时就不同了:

. . . . . . . . . . . . . . . // 暂时还不用管这个字符串
    A B C D A B . . .         // 已经匹配的部分
=>          A B C D A B D A C

这个时候才 可能 成功匹配。

4   MP 算法

4.1   原理

MP 算法基于这样一种观察:

Note

注意了,这里以及下面所说的 前缀后缀 都是指 不包括自身 的“真”前缀或后缀 ( proper prefix/suffix )

发生了不匹配之后,移动窗口时,定要保证 将模式串已匹配部分的一个前缀和一个相同的后缀对齐,并使这个前缀尽可能长

什么意思?

首先让我们列出模式串 A B C D A B 的所有前缀:

0: /*Empty*/
1: A
2: A B
3: A B C
4: A B C D
5: A B C D A

我们再列出它所有的后缀:

0: /*Empty*/
1: B
2: A B
3: D A B
4: C D A B
5: B C D A B

发现前缀 A B == 后缀 A B ,将它们对齐(即,接下来直接从第 3 位开始比较),完美了,前两位不必重复比较了。

原理上说也不难理解: 从左向右移动窗口的过程就是用前缀去匹配后缀的过程 ,而第一次匹配成功的肯定是最长的相同前缀/后缀 —— 在上例中,两个空字串也相等,可是如果将它们对齐的话那可就“移过头了”。

这么看,我们发现,在模式串的每一个位置上,匹配失败之后能最大限度的将窗口移动多少位 —— 即,与什么位置对齐, 只与模式串在该位置前方的子串有关 ,与文本串无关,与模式串在该点之后的字符也无关。

于是,自然而然的就想到了,为什么不把这么个失败后对齐的位置存放在一个数组中呢,这样每次匹配失败之后就按照它的指示进行跳转。

F[i] == max{ j | pattern[0:j) == pattern[i-j+1:i+1) and 0<j<i } ,也就是当前位置上保证前缀等于后缀的最大长度。

对于模式串 A B C D A B D A C , F 数组如下:

Index 0 1 2 3 4 5 6 7 8
Pattern A B C D A B D A C
F 0 0 0 0 1 2 0 1 0

继续扯,在位置 i 匹配失败之后,可以将窗口继续右移一位,并从 F[i-1] 位置开始继续比较模式串与文本串(按照定义,pattern[ 0 : F[i-1] ) 已经保证匹配了),用代码表示的话就是这样:

int MP(char const* pattern, char const* text)
{
    int i=0,j=0,m=strlen(pattern);
    int F[m];
    calcF (pattern, F); // 计算跳转数组
    for(;text[i];++i,++j) {
        while(j>=0 && pattern[j]!=text[i]) {
            // 第一个字符不匹配则右移窗口、从 0 开始比较
            if (j==0) {
                j=-1;
                break;
            }
            j = F[j-1]; // 对齐
        }
        if(j>=m-1) {    // 找到了!
            return (i+1-m);
        }
    }
    return -1;
}

—— 多和谐呀!



对了,内个 F 数组怎么算?

4.2   跳转数组的计算

观察一下,希望求出拥有相同后缀的最长前缀,这个过程不也是一个字符串匹配的过程吗:

1.  A B C D A B D A C
    A B C D A B D A C
    |
    0  // 定义为 0

2.    A B C D A B D A C
    A B C D A B D A C
      |
      0 // 匹配部分的长度

3.      A B C D A B D A C
    A B C D A B D A C
        |
        0

4.        A B C D A B D A C
    A B C D A B D A C
          |
          0

5.6.        A B C D A B D A C
    A B C D A B D A C
            | |
            1 2

7.              A B C D A B D A C
    A B C D A B D A C
                |
                0

8.                A B C D A B D A C
    A B C D A B D A C
                  |
                  1

9.                  A B C D A B D A C
    A B C D A B D A C
                    |
                    0

如上面所示,将模式串向右移动,并与自身做比较,在位置 i 上,pattern[i:] 与 pattern 自身相匹配的部分的长度就是 F[i]

注意第6步到第7步! 为什么可以直接右移两位呢?

—— 因为 F[1] 已经算出来了!于是我们可以将之前 MP 算法中的思想用在这里,聪明的你想到了没有?

用代码来说就是这样的,看不懂的话我会很伤心:

void calcF(char const* pattern, int* F)
{
    int i=0,j=0;
    for(;pattern[i];++i,++j) {
        while(j>0 && pattern[j-1]!=pattern[i]) {
            j = F[j-1];
        }
        F[i] = j;
    }
}

4.3   另一种表示

对了有没有人觉得 MP 算法中对于第一个字符不匹配时的特殊处理感觉到很生硬的?

嗯,其实呢,考虑到第一个字符失败时的特殊情况其实也不怎么特殊,不如干脆把这种情况也放到 F 数组中去统一处理好了:

Index 0 1 2 3 4 5 6 7 8 9
Pattern A B C D A B D A C  
F -1 0 0 0 0 1 2 0 1 0

这样,MP 算法表达起来更简单了:

int MP(char const* pattern, char const* text)
{
    int i=0,j=0,m=strlen(pattern);
    int F[m+1];
    calcF (pattern, F); // 计算跳转数组
    for(;text[i];++i,++j) {
        while(j>=0 && pattern[j]!=text[i]) {
            j = F[j];   // 对齐
        }
        if(j>=m-1) {    // 找到了!
            return (i+1-m);
        }
    }
    return -1;
}

calcF 也并没有因此变复杂:

void calcF(char const* pattern, int* F)
{
    int i=0,j=-1;
    F[0]=-1;
    for(;pattern[i];++i,++j) {
        while(j>=0 && pattern[j]!=pattern[i]) {
            j = F[j];
        }
        F[i+1] = j+1;
    }
}

理解之后就会觉得,这种表示方法比较 “省事”;下面咱就都用这种表示得了。

Nevertheless, 其实它们是一码事。

4.4   可是...

我们再停下来看看 “暴力” 搜索算法 —— 有没有可能暴力算法比 MP 算法还快呢?

答案是 “Yes!”

( 哈哈!你输了吧! )




让我们想象一下在一个字符集很大的串 —— 比如说 UTF-16 字符串吧,中寻找一段模式串;而模式串的第一个字符出现在文本串中的频率根本就不大,那么看看第一次匹配失败时它们两者工作的流程吧:

MP 算法 暴力算法
  1. i>=n?
  2. j>=0? ( 此时 j == 0 )
  3. 比较 pattern[j] 与 text[i]
  4. j = F[j]
  5. j>=0? ( 此时 j == -1 )
  6. j = j+1
  7. j >= m?
  8. i = i+1
  9. 到第 1 步
  1. i>=n?
  2. j = 0
  3. j < m?
  4. 比较 pattern[j] 与 text[i]
  5. i = i+1
  6. 到第 1 步

嗯,所以说,这个地方是有优化空间的,Knuth、Morris、Pratt 的论文 [1] 中有提到,俺就不展开了 —— 因为真正牛B的优化在下面:




5   KMP 算法

其实 MP 算法的效率还有提升的空间,不过从模式串 A B C D A B D A C 中看不明显;我们试试这样一个模式串: A B A B A B C

假设在 A B C A B C A B A B A B C A C 中查找 A B A B A B C ,按照 MP 算法的思想,先算出 F 数组:

Pattern A B A B A B C  
F -1 0 0 1 2 3 4 0

于是查找的过程就是这样的:

1.  A B C A B C A B A B A B C A C
    A B A . . . .

2.  A B C A B C A B A B A B C A C
        A . . . . . .

3.  A B C A B C A B A B A B C A C
          A B A . . . .

4.  A B C A B C A B A B A B C A C
              A . . . . . .

5.  A B C A B C A B A B A B C A C
                A B A B A B C

从第 1 步到第 2 步、从第 3 步到第 4 步,我们发现,字符 AC 的不匹配导致了第一次失败,然后紧接着又直接导致了第二次失败。

如此,我们又惊喜的发现,在 A B 之后若是遇到不是 A 的字符,我们完全可以跳三步!因为跳两步的话算是把 A 对齐了 —— 可是它们会被对齐到一个不是 A 的、将会导致匹配失败的字符上面去。

这样的规则有什么规律呢?我直接放出代码吧:

// 这是我们计算 MP 算法中的 F 数组的函数:
void calcF(char const* pattern, int* F)
{
    int i=0,j=-1;
    F[0]=-1;
    for(;pattern[i];++i,++j) {
        while(j>=0 && pattern[j]!=pattern[i]) {
            j = F[j];
        }
        F[i+1] = j+1;
    }
}

睁大眼睛准备找茬喽:

// 只需要改一句话:
void calcF(char const* pattern, int* F)
{
    int i=0,j=-1;
    F[0]=-1;
    for(;pattern[i];++i,++j) {
        while(j>=0 && pattern[j]!=pattern[i]) {
            j = F[j];
        }
        // !这里!
        F[i+1] = pattern[j+1] == pattern[i+1]?
                   F[j+1]:
                   j+1;
    }
}

为什么呢?因为对于同一个字符导致的失败,失败在前面应该跳到哪里,到后面就还是应该跳到哪里。

另外,这个时候咱似乎就比较喜欢把这个数组称作 next 数组了 —— 其实还是同一回事。

那么, A B A B A B Cnext 数组如下,请您欣赏:

Index 0 1 2 3 4 5 6  
Pattern A B A B A B C  
Next -1 0 -1 0 -1 0 4 0

6   复杂度分析

至于为什么 KMP 算法的复杂度是线性的,我们再回头看看 另一种表示 一节中的算法主体:

int MP(char const* pattern, char const* text)
{
    int i=0,j=0,m=strlen(pattern);
    int F[m+1];
    calcF (pattern, F);
    for(;text[i];++i,++j) { // j 增大,i 增大
        while(j>=0 && pattern[j]!=text[i]) {
            j = F[j];       // j 减小
        }
        if(j>=m-1) {
            return (i+1-m);
        }
    }
    return -1;
}

i 只有增大的份,所以 ++i 最多执行 n 次,这个很显然。

j 初始值为 0,一共增加了 n 次,而 j>=-1 ,于是 j = F[j] 这一句最多也就执行了 n+1 次(否则就会出现 j<-1 的情况了)。

所以就是线性的了!

7   KMP 算法的最长停顿

为了说明 KMP 算法在文本串上的某一个字符上进行了很多次比较的极限情况(也就是所谓的停顿或者E文的 delay ),我们首先要介绍一下 “费波拉契串” —— 因为,很巧,它就是能使 KMP 算法达到最糟糕状况的模式串,一会儿我们会说到。

提到 “费波拉契” ,相信不少人会直接想到 1, 1, 2, 3, 5, 8, 13, 21, 34 ... ,是的,费波拉契串的定义也十分类似:

设 P 是个费波拉契串,那么:

P[0] = b
P[1] = a
P[i] = P[i-1]P[i-2]

所以:

P[2] = ab
P[3] = aba
P[4] = abaab
P[5] = abaababa
P[6] = abaababaabaab
P[7] = abaababaabaababaababa
P[8] = abaababaabaababaababaabaababaabaab
...

计算出 P[7] 的 F 数组和 Next 数组如下,我们一会儿要用到,你也可以先把它当作找规律的题看看:

P a b a a b a b a a b a a b a b a a b a b a  
n 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
F -1 0 0 1 1 2 3 2 3 4 5 6 4 5 6 7 8 9 10 11 7 8
Next -1 0 -1 1 0 -1 3 -1 1 0 -1 6 0 -1 3 -1 1 0 -1 11 -1 8

费波拉契串有几个性质值得注意一下:

Note

文章前面已经假设了我的世界里费波拉契数列下标是从 0 开始的,这里再强调一遍。

  1. strlen(P[n]) == Fibonacci[n] (这个应该很容易理解吧)

  2. 设函数 c(t) 的作用是交换字符串 t 的最后两个字符,例如 c("abcdef") == "abcdfe",那么当 n>=2 时 P[n-1]P[n-2] == c(P[n-2]P[n-1]) :
    n == 2 时这是显然的;
    当 n>2 时可以用数学归纳法证明:
    c(P[n-2]P[n-1]) = P[n-2]c(P[n-1]) = P[n-2]P[n-3]P[n-2] = P[n-1]P[n-2]
  3. 由上面两条性质我们又可以推导出:
    Next[Fibonacci[n]-2] == F[Fibonacci[n]-2] == Fibonacci[n-1]-2, n>=2
    这是因为,可以把一个费波拉契串分解开:
    P[n] == P[n-1]P[n-2] == P[n-2]P[n-3]P[n-2] == c(P[n-3]P[n-2])P[n-2] == P[n-3]c(P[n-2])P[n-2] == ...
    具体以 P[7] 为例,
    P[7] == ab-ab-ba---ab------ba
    其中省略掉的部分根据 性质2 表现出的规律与前方相等,因此如果在 P[7] 的最后一个字符 b 处发生了不匹配,接下来应该在下列位置重新试着匹配:
    ab-a--b----a-------b-
    它们正好占据着 Fibonacci[2]-2, Fibonacci[3]-2, .. , Fibonacci[7]-2, ... 的位置。

因此,如果在费波拉契串的第 n 位,n == Fibonacci[k]-2 上发生了不匹配,接下来则还需要 k-1 次比较;

又因为 Fibonacci[k-1] == (φk - (-1)kφ-k)/sqrt(5) == round( φk / sqrt(5) ), 于是可以解得 k ~ logφ(n),其中 φ 是黄金比例 (1+sqrt(5))/2 == 1.618...

—— k 便是文本串上的一个字符的最多比较次数。

8   为什么费波拉契串这么神奇

为了证明为何费波拉契串就是使停顿时间最长的模式串,我们再看看 MP 算法的基本思想:将字符串已匹配的一个前缀和一个对应的后缀匹配。

假设字符串 S 有且仅有一个相等的前缀和后缀(设为 a ),那么 S 可以表示为

S = aB = Ca

再假设 a 本身也有且仅有一个相等的前缀和后缀(设为 e ),那么 a 也可以表示为

a = eF = Ge

对应 MP 算法,匹配 Ca 时若在 a 之后失败,则会将 aB 的 a 与其对齐:

Cax
Cay

==>

Cax
 aBy

若在 B 的第一个字符处再次失败,则下一次对齐是这样的:

CGex
 GeBy

==>

CGex
  eFBy

KMP 算法在这里还要求 F 的第一个字符和 B 的第一个字符不等(否则会跳过这一段)

我们很容易可以证明想要 KMP 算法在这个地方停留尽可能长的时间需要满足 |S| <= |e| + |a| :因为若 |e| + |a| > |S|,那么令 d = |e| + |a| - |S| 则 Ca = CGe = aB 算式中, a 和 e 将有长度为 d 的重叠,于是 B 的第一个字符等于 e[d];同理,在 aB = eFB = Ca 算式中,可以得到 F 的第一个字符为 a[d],由 a = eF 可以得到 a[d] = e[d],和 KMP 的要求不符。

于是 |S| == |e| + |a| 是使 KMP 算法的停顿时间达到最长的极限情况——很容易发现,满足这条件的便是费波拉契串了。

9   PS

对了,如果我要找出模式串在文本串中所有的出现怎么办?

提示一下:目前为止 F 数组(或者 next 数组)的最后一个元素我们还没有用到过是不是?

10   References

[1]KNUTH D.E., MORRIS (Jr) J.H., PRATT V.R., 1977, Fast pattern matching in strings, SIAM Journal on Computing 6(1):323-350.
[2]http://www-igm.univ-mlv.fr/~lecroq/string/node8.html#SECTION0080
[3]http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=stringSearching

Last update: 2010/11/23 18:53:11

About KMP

发现很多地方把 KMP 算法介绍得不完全正确,说

text:    a b a b b a b b b . . .
pattern: a b a b a c b

这一次匹配尝试失败之后下一步尝试是:

text:    a b a b b a b b b . . .
pattern:     a b a b a c b

不过 KMP,至少根据 “FAST PATTERN MATCHING IN STRINGS” 论文本身的说法以及 这里 的介绍,比较聪明——它会发现右移两位之后出现在不匹配位置的还是同样一个不匹配的字符。

 所以,实际上可以右移五位:

text:    a b a b b a b b b . . .
pattern:           a b a b . . .

这才是 KMP。

而上面的,貌似是 MP 算法。

 

 

PS,强烈建议看看 Fast Pattern Matching in Strings 这篇论文,写得可好了——18页之后还有个大大的惊喜: Knuth 爷爷在 Postscript 中很详细的介绍并分析了 Boyer-Moore 算法。

 

非空 Young 氏矩阵:O(log(n)^2) Can Do

<UPDATE>
说明,由于数据规模是 n*m, 所以复杂度是 O(log(n) * log(m*n));不妨设 n >= m,则 O(log(m*n)) = O(log(n*n)) = O(2*log(n)) = O(log(n)) —— 突然想到 m 时我还以为自己错了呢,其实没错,诶。
</UPDATE>

 
正在长大的忆向童鞋某次讨论了关于判断 Young 氏矩阵 中元素存在性的问题,当时我就觉得可以在 O(log(n)) 时间内得出结论——后来发现错了,但 O(log(n)^2) 也还是够了。
 
先抄一段关于 Young 氏矩阵的介绍,介绍得不好别怪我因为不是我写的。

一个 m*n 的 Young 氏矩阵(Young tableau) 是一个 m*n 的矩阵, 其中每一行的数据都从左到右升序, 每一列的数据都从上到下升序. 所以, Young 氏矩阵可以用来存放 r<= mn 个有限的元素.

 

我们要做的事情是,在一个 Young 氏矩阵中找出某个值的位置,或者判断它是否存在。
 
搜了一下,发现还真没有谁提出可以用亚线性的时间复杂度完成这样的工作。
 
为了证明俺是对的,我把代码给码出来了,然后实际操作了一下,先看结果:
yo
横坐标:矩阵规模(为了方便画图,这里用的是 n x n 矩阵)
纵坐标:比较次数
红线:直线 y = x
绿线:实际“有效”的比较次数
蓝线:加上参数合法检验等判断在内总共的比较次数
 
说下思路:在对角线上面找出和要找的数(设为 x 吧)最接近且小于等于 x 的一个值,如果这个值不等于x,那么可以这个值为界,丢掉包括它在内的左上角和不包括它的右下角,原因很显然嘛——然后递归在剩下的部分中找就是了。
——于是一次能丢掉 1/2 左右的数据。
上面在对角线上寻找最接近值的过程又可以用二分法以 O(log(n)) 复杂度得到。
于是总体复杂度就是 O(log(n)*log(n)) = O(log(n)^2) 了。
 

简单拟合了一下, 绿线: 5 ln(n)^2,紫线: 15 ln(n)^2 —— 多么和谐啊!
 
代码如下,感兴趣的拿去玩吧,版权还没没想好。

from numpy import *
from random import *

def genRandomYoung( rows, cols ):
    y = zeros( (rows, cols), int );
    t = randrange( 0, 10 );

    for c in range( cols ):
        t = y[0][c] = randrange( t, t+10 );
    for r in range( 1, rows ):
        t = y[r-1][0];
        for c in range( cols ):
            if t < y[r-1][c]:
                t = y[r-1][c];
            t = y[r][c] = randrange( t, t+10 );
    #print(y);
    return y;

def mySuperBinarySearch( young, rect, what ):
    x = rect['x'];
    y = rect['y'];
    rows = rect['rows'];
    cols = rect['cols'];
    if rows==0 or cols==0:
        return None;
    k = (cols+0.0)/(rows+0.0);
    pos = lambda n: (int(y+n), int(x+n*k));
    if (k > 1):
        pos = lambda n: (int(y+n/k), int(x+n));
    get = lambda p: young[p[0]][p[1]];
    n = rows if k<1 else cols;
    if n==1 and get(pos(0)) != what:
        return None;
    #for i in range(n):
    #    print( get(pos(i)) )
    f = 0;
    b = n;
    m = 0;
    m2 = -1;
    while m != m2:
        m2 = m;
        w = get(pos(m))
        if( what < w ):
            b = m;
        elif( what > w ):
            f = m;
        else:
            return pos(m);
        m = (f+b)/2;
    m = (f+b)/2;
    if( get(pos(m)) > what and m>0):
        m = m-1;

    p = pos(m);
    w = get(p);
    if( w == what ):
        return p;
    rect = {'x': x, 'y': p[0]+1, 'rows': y+rows-p[0]-1, 'cols': p[1]-x+1 };
    r = mySuperBinarySearch(young, rect, what);
    if r!=None:
        return r;
    rect = {'x': p[1]+1, 'y': y, 'rows': p[0]-y+1, 'cols': x+cols-p[1]-1 };
    return mySuperBinarySearch(young, rect, what);

class youngMatrixSearcher:
    def __init__(self):
        self.compareOp = 0;
        self.algebraicOp = 0;
        self.call = 0;
        self.binSearchCmp = 0;

    def search( self, young, rect, what ):
        x = rect['x'];
        y = rect['y'];
        rows = rect['rows'];
        cols = rect['cols'];
        if rows==0 or cols==0:
            return None;
        self.compareOp += 2;

        k = (cols+0.0)/(rows+0.0);
        self.algebraicOp +=2;

        pos = lambda n: (int(y+n), int(x+n*k));
        if (k > 1):
            pos = lambda n: (int(y+n/k), int(x+n));
        get = lambda p: young[p[0]][p[1]];
        n = rows if k<1 else cols;
        self.compareOp += 1;

        if n==1 and get(pos(0)) != what:
            return None;
        self.compareOp +=2;

        f = 0;
        b = n;
        m = 0;
        m2 = -1;
        self.algebraicOp += 4;
        while m != m2:
            m2 = m;
            w = get(pos(m))
            if( what < w ):
                b = m;
            elif( what > w ):
                f = m;
            else:
                return pos(m);
            m = (f+b)/2;
            self.compareOp += 4;
            self.algebraicOp += 9;
            self.binSearchCmp += 2;

        m = (f+b)/2;
        self.algebraicOp += 2;

        if( get(pos(m)) > what and m>0):
            m = m-1;
            self.algebraicOp +=2;
        self.compareOp +=2;

        p = pos(m);
        w = get(p);
        self.algebraicOp += 5;

        if( w == what ):
            return p;
        self.compareOp += 1;

        rect = {'x': x, 'y': p[0]+1, 'rows': y+rows-p[0]-1, 'cols': p[1]-x+1 };
        self.algebraicOp += 6;

        r = self.search(young, rect, what);
        self.call += 1;

        if r!=None:
            return r;
        self.compareOp += 1;
        rect = {'x': p[1]+1, 'y': y, 'rows': p[0]-y+1, 'cols': x+cols-p[1]-1 };
        self.algebraicOp += 6;

        r = self.search(young, rect, what);
        self.call += 1;
        return r;

def test():
    rows = 12;
    cols = 12;
    y = genRandomYoung( rows, cols );
    print(y);
    rect = {'x':0,'y':0,'rows':rows,'cols':cols}
    searchFor = y[randrange(0,rows)][randrange(0,cols)];
    print "searching for", searchFor;
    s = youngMatrixSearcher();
    p = s.search( y, rect, searchFor );
    print "find at", p;
    print "with ", s.algebraicOp, "algebraic operations; ";
    print "     ", s.compareOp, "comparisions; ";
    print "     ", s.binSearchCmp, "comparisions purely for searching;";
    print "     ", s.call, "calls.";

    if( y[p[0]][p[1]] != searchFor ):
        print 'oops';
    else:
        print ' - am i genius?'

def benchMark():
    r=range(20,201);
    ns = array([i for i in r]);
    cs = array([0 for i in r]);
    bcs = array([0 for i in r]);
    for n in r:
        avgc = 0;
        avgbc = 0;
        for i in range(10):
            y = genRandomYoung(n,n);
            w = y[randrange(0,n)][randrange(0,n)];
            s = youngMatrixSearcher();
            rect = { 'x':0, 'y':0, 'rows':n, 'cols':n };
            s.search(y,rect,w);
            avgc += s.compareOp;
            avgbc += s.binSearchCmp;
        avgc /= 10;
        avgbc /= 10;
        cs[n-ns[0]] = avgc;
        bcs[n-ns[0]] = avgbc;
    print ns;
    print cs;
    print bcs;

#test();
benchMark();

不靠谱啊不靠谱

这是方差最小的:
the man from earth

 

这是直方图最接近的:
chaos

计算直方图的相似度试过两种算法,都一样的不靠谱:

方法一:

方法二:

 

另外,这是均值最接近的:
the man from earth