题意

给出两篇文章,分析这两篇文章的文本相似度.分析的时候只统计给出的一个字典中的,且不为停用词的单词.

最后先输出相似度,再输出第一篇文章出现频率最高的前n个单词(同样不统计停用词),频率一样则按字典序排列;再输出第二篇文章中出现频率最高的前n个单词.

写法分析

整体思路

官方给出的数据规模如下:

NIRTcn.png

如此大的字典规模,Trie树就不用想了,肯定慢得飞起,只能选择哈希.快排或者归并O(nlogn)的速度也有些慢,考虑到单个单词出现的次数不会特别的高,改用桶排来计算答案.

在哈希完字典后依次插入两篇文章和停用词,最后遍历一遍整个字典,使用桶排序统计答案.最后计算并输出.

完整版代码贴在了最下面.

哈希部分

字典和文章的哈希过程大同小异,这里只贴出对字典的哈希.

for (now=d_buf;now<end;++now){
    sum=0;  d_pool[++tot]=now;
    while (low(*now))   sum=sum*131u+*now++;
    sum=sum&mod;    *now++='\0';
    Next[tot]=hsd[sum]; hsd[sum]=tot;
}

d_buf是一个字符数组,里面存的是整个字典.now是一个字符指针,end是计算好的字符数组的末尾指针.

通过now遍历整个字典,sum变量是unsigned int类型,用来计算哈希值.d_pool是一个指针数组,负责记录字典中每一个单词的开头在哪里.

哈希函数选择了经典的BKDR哈希,魔数选择了131.哈希表的大小设为了8388608(也就是2^{23}).实际测试结果显示,对于样例中给出的大小为418666的字典,一共会发生10353次碰撞,这个碰撞概率是完全可以接受的.

在哈希结束后,now一定会指向换行符,把这个位置改为\0,就可以通过d_pool数组来完成单个单词的输出了.最后单词的储存和链式前向星很像,就不做过多介绍了.

插入部分

这里摘取插入第一篇文章的部分:

for (now=art;now<end;++now){
    while (!low(*now))  ++now;
    sum=0;  begin=now;
    while (low(*now))   sum=sum*131u+*now++;
    x=sum&mod;  *now='\0';
    poolD=hsd[x];
    if (poolD && !Next[poolD] && *d_pool[poolD]==*begin && *(d_pool[poolD]+1)==*(begin+1))
        Cnt[poolD]++;
    else while (poolD!=0){
        if (strcmp(begin,d_pool[poolD])==0) {
            Cnt[poolD]++;   break;
        }
        poolD=Next[poolD];
    }
}

在读取了文章之后,会先进行一遍扫描,把大写字母转为小写字母.low负责检测字母是不是小写.

哈希过后,要依次和当前哈希值拉出来的那条链上所有的单词进行比对,如果匹配上,则计数器+1.匹配时直接使用自带的strcmp函数就行,测试表明手写的要比自带的慢很多.

在比对的时候有个技巧,如果当前哈希值只存了一个单词,且这个单词和文章中的单词的前两位能匹配上,则直接认为两个单词就是相等的.因为碰撞的单词并不多,如果都要来一遍strcmp的话还是很慢的,这里通过一个if可以减少很多不必要的函数调用.当然这个写法是针对数据试出来的.

停用词的插入则比较独特:

for (now=art;now<end;++now){
    while (!low(*now))  ++now;
    begin=now;  x=stop[sum];    poolD=hsd[x];
    for (;low(*now);now++); *now='\0';  sum++;
    while (poolD!=0){
        if (strcmp(begin,d_pool[poolD])==0) {
            Cnt[poolD]=Sum[poolD]=-1;   break;
        }
        poolD=Next[poolD];
    }
}

可以看到和文章的插入很像,但是没有了计算哈希函数的过程.这是因为,测试数据中的停用词表和给出的停用词表是完全一样的,也就只有320个单词.为了节省计算的时间,提前在本地算好了这些单词的哈希值,直接用就行了.开始丧心病狂了.

排序部分

for (i=1;i<=tot;++i)
    if (Cnt[i]>0){
        x=Cnt[i];
        if (!bu[x]) bu[x]=i;
        net[tail[x]]=i; tail[x]=i;
    }

这部分非常的简单,就是一个桶排的过程.也是使用了类似链式前向星的方式记录,按顺序访问字典就可以保证相同频率的单词按照字典序插入,之后就不需要对比了.

桶的大小经过测试,设为20000就可以满足需求了.实际最大出现次数应该比这个值还要小上一点,不过20000和10000,哪怕1000的差别并不大,就没有继续优化的必要了.

输入&输出

通过fread将整个文本一次性全部读进去.

FILE *f1=fopen("dictionary.txt","rb");
fseek(f1,0,SEEK_END);
int Size=ftell(f1);
rewind(f1);
fread(d_buf,1,Size,f1);
fclose(f1);
//Read the dictionary

输出部分使用了fwrite函数,也是将结果一次性全部输出出去.输出的处理和答案的计算是混合在一起的:

for (i=M;i;i--)
    for (int j=bu[i];j;j=net[j]){
        s1+=i;  S[j]=-1;    sum++;
        begin=d_pool[j];
        while (*begin!='\0')    *now++=*begin++;
        *now++=' '; x=i;
        while (x)   z[++l]=x%10,x/=10;
        while (l)   *now++=z[l--]+48;
        *now++='\n';
        if (sum>=n) goto END;
    }
END:;

前半部分是在统计出现次数最多的那些单词情况,后面分两部分把输出信息放到字符数组里.先通过一个字符指针遍历要输出的单词,把它整个放进去,然后再对数字进行拆位,倒序转成字符放进去.看上去引入了很多运算和访问,但是本地模拟了400000规模的单词与数字的混合输出,fwrite的用时平均只占fprintf的3.16%.这个数据可能在不同的评测环境下有差异,但足够说明fwrite的高效了.

常数优化

这部分主要介绍下在写法部分没有提到的常数优化方法.

奇葩的主函数:

int main(){
    unsigned size=128u<<22;
    char *p=(char *)malloc(size)+size;
    __asm__ __volatile__
    (
        "movq  %0, %%rsp\n"
        "pushq $exit\n"
        "jmp main2\n"
        :: "r"(p)
    );
}

强行开栈,然后汇编调用main2函数.所有的计算过程全部都位于main2函数之中,所有的变量也都开在了里面.测试显示,全局变量的访问要比局部变量慢一些,开成局部变量要有一些优势.这里是向Kevin大佬学习的.当然main2里面就不能return了,需要写exit(0).整个程序就只有main函数和main2函数,不把哈希函数分出去是为了减少函数调用的时间.

main2里的变量定义:

reg ut sum,i;
const ut mod=8388607;
reg char *now,*begin,*end;
char d_buf[20000010];
char art[20000010];
ut stop[320]={97,7411521,7411637,5370655,8098984,3028091,1632928,4741869,1678873,2217970,4725675,4725677,3820036,1829583,6964010,7682195,12816,6973768,4966101,1291768,7715104,12817,1679127,1974215,1679148,5705872,5825850,1458130,5961455,4085931,1679652,3819685,12822,12823,3886803,12939,7041409,8205530,7281663,5985264,3997132,3955712,5637983,5375262,1642066,6561931,6613561,1203934,6720525,5555164,6408147,4025271,4129281,4240047,1697221,12959,6136074,1711756,3514851,6136344,13080,6997707,1713590,4981325,4617331,1713994,13201,5515047,3878317,13211,236066,237254,1731528,5509721,2242465,13334,1437907,7873949,2254581,2433329,1390570,2197689,1350044,1748556,2603114,2603118,5464259,1672871,1799079,4426880,8251801,1763772,622541,4628254,4629027,4629281,4629806,2526760,4630330,1765077,748269,6488338,7626834,7677510,4733178,4783870,5930225,4834959,6446620,1780930,6878421,13604,1797551,1797566,3133036,600616,13725,3649531,1798089,668736,6278504,575711,576617,7015746,668750,6278001,1798608,2910632,1798614,1799404,7569178,7242022,105,13856,13857,13865,1816414,2637861,3610290,3071548,13870,13871,1817216,8197559,7411317,1203994,2860966,6917779,7023113,1272637,1868684,3450105,3451435,1883377,14380,7331712,238427,3588448,3588703,3692193,1417069,3692339,5087624,3692717,3793197,3795305,14400,5427992,5699375,4207972,3611867,790332,4670930,5769475,5836794,14521,619115,5939760,6375065,1902365,1902367,6129961,1902370,369154,14643,1918335,3902738,14651,8169249,1919382,8170448,8171486,14655,1615652,1935327,149344,1920312,8291355,7734978,1920314,8306812,1920570,1807619,1935377,8014469,4992074,1937475,4343788,15035,162614,1986847,230218,8138669,882480,4992849,3676344,6716008,3279732,1987240,8287883,283021,298723,5749730,4324269,1987390,5923587,15176,402868,1273064,1393042,6325994,706317,252954,565187,5228288,504527,7373251,2410443,2004017,2529269,2529275,2004401,4246630,2529792,6632522,2529793,2744343,4247796,2158148,7825203,3919000,7826109,4102710,4247927,2529804,4314481,2530317,4316439,2530322,4419537,180083,4469186,4649869,928921,2531503,2531894,15307,5667666,2005328,2005329,7336299,4753972,7803517,7837597,2006376,15437,1838916,2114010,15439,4916482,15442,6976206,2038850,2054981,15690,834890,835669,884940,5931258,885458,3588720,1665248,6943719,2413165,997941,998078,998984,7498521,5259812,5546129,7010401,7011577,752085,2055894,6217252,7114543,886767,7115460,2055904,903534,904578,4552123,840944,6176929,2089828,2091139,5503867,7975012,2001603,6835853};
char* d_pool[N];
int Cnt[N],Sum[N],S[N],Next[N],hsd[N],bu[100010],tail[100010],net[450010];
int n=0,tot=0,poolD;

调用频繁的几个变量都开了register.不过据Kevin大佬所说,一次最多开4个,原因好像和内存分配有关.我在上面多放了个i进去,应该没有太大的影响.大段的数字是计算好的停用词的哈希值.mod开成const可能会带来一点点的优化(?)

我知道%运算针对const变量是有优化的,但是%太慢了,后面全部替换成了&和自然溢出,但保险起见,mod还是开成了const类型.因为哈希表的大小是2的次幂,如果&上2的次幂减1的话,就相当于对这个数进行了取模,此时的&运算就和%运算等价.

在哈希时用到的low函数如下:

#define low(x) (x>='a')

可以看到并没有判断和z的大小.经过试验,文章中是有在z之后的字符的.但是字典和停用词表中都没有.为了少一次判断,我在转化小写的时候对z后面的字符做了处理:

for (end=art;*end!='\0';end++){
    if ((*end<='Z') && (*end>='A')) *end=(*end)+32;
    else if ((*end)>'z')    *end='%';
}

这样就可以只和a比对了.如果不做处理,这条判断语句也应该先写和a的比较,再写和z的比较.因为比a大的字符几乎都是小写字母,C语言中,如果&&的前一项为假后就不会再判断后一项了,也就是几乎只有小写字母会参与和z的比较;先写小于z的话....那几乎所有的字符都会再和a比较一次,会慢一点.

在处理字典的时候可以看到开了很多数组,结构体的访问应该是要比数组访问慢一些的,能拆就拆.

当然还有神奇的O3优化:

#pragma GCC optimize(3)

还有一些减少运算次数,减少数组访问的写法,在代码中都看出来.

当然也有一些能加但是因为一些原因没有加的优化,在下面列举一下:

  1. 循环展开

我试着在桶排的时候展开了一下,但是效果好像并不明显,最后去掉了

  1. 停用词预处理

这里的预处理是指把整个停用词表提前存好,可以减少一次文件访问了.但着实有些太丧心病狂了,于是放弃(主要还是因为懒)

  1. 打表

大数据就只有一个测试点,二分答案很快就能出来.这样就可以不用计算答案,直接做输出处理了.可以省下很多统计和计算的步骤.但毕竟打表是不好的行为,面向数据编程不太合适.(停用词表那里不算,因为题面上只说了字典会变大,没说停用词表会变,这算是我的已知条件,不算面向数据编程)

  1. 内嵌汇编优化

这里指拿汇编重写循环什么的.我确实有这个打算,但是网上查到的方法都失败了,遂放弃.(还是学艺不精,回去好好学一下汇编)

  1. mmap重写读入

mmap来读入可以再省一点时间.但是我本地好像没有这个库(经测试OJ上有),配置起来较为繁琐.而且在OJ上测试了一下,目前的代码的时间消耗主要在哈希部分,读入的时间已经很少了,读入优化显得有一点鸡肋.

常数优化基本上就这些了.可能在大佬看来,这份代码还有很大的优化空间,但我个人目前只能做到这一步了.OJ上测试的结果是0.103s,位列第5.第一名0.078s,差距也不是很大.如果把上面的优化全部加上,多交几次,我感觉是有一战之力的.(最后还是因为懒得刷了,这个时间实在是太看评测姬心情了)

代码

#pragma GCC optimize(3)
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define reg register
#define ut unsigned int
#define N 8388608
#define M 100000
#define low(x) (x>='a')

extern int main2(void) __asm__ ("main2");

int main2(){
    reg ut sum,i;
    const ut mod=8388607;
    reg char *now,*begin,*end;
    char d_buf[20000010];
    char art[20000010];
    ut stop[320]={97,7411521,7411637,5370655,8098984,3028091,1632928,4741869,1678873,2217970,4725675,4725677,3820036,1829583,6964010,7682195,12816,6973768,4966101,1291768,7715104,12817,1679127,1974215,1679148,5705872,5825850,1458130,5961455,4085931,1679652,3819685,12822,12823,3886803,12939,7041409,8205530,7281663,5985264,3997132,3955712,5637983,5375262,1642066,6561931,6613561,1203934,6720525,5555164,6408147,4025271,4129281,4240047,1697221,12959,6136074,1711756,3514851,6136344,13080,6997707,1713590,4981325,4617331,1713994,13201,5515047,3878317,13211,236066,237254,1731528,5509721,2242465,13334,1437907,7873949,2254581,2433329,1390570,2197689,1350044,1748556,2603114,2603118,5464259,1672871,1799079,4426880,8251801,1763772,622541,4628254,4629027,4629281,4629806,2526760,4630330,1765077,748269,6488338,7626834,7677510,4733178,4783870,5930225,4834959,6446620,1780930,6878421,13604,1797551,1797566,3133036,600616,13725,3649531,1798089,668736,6278504,575711,576617,7015746,668750,6278001,1798608,2910632,1798614,1799404,7569178,7242022,105,13856,13857,13865,1816414,2637861,3610290,3071548,13870,13871,1817216,8197559,7411317,1203994,2860966,6917779,7023113,1272637,1868684,3450105,3451435,1883377,14380,7331712,238427,3588448,3588703,3692193,1417069,3692339,5087624,3692717,3793197,3795305,14400,5427992,5699375,4207972,3611867,790332,4670930,5769475,5836794,14521,619115,5939760,6375065,1902365,1902367,6129961,1902370,369154,14643,1918335,3902738,14651,8169249,1919382,8170448,8171486,14655,1615652,1935327,149344,1920312,8291355,7734978,1920314,8306812,1920570,1807619,1935377,8014469,4992074,1937475,4343788,15035,162614,1986847,230218,8138669,882480,4992849,3676344,6716008,3279732,1987240,8287883,283021,298723,5749730,4324269,1987390,5923587,15176,402868,1273064,1393042,6325994,706317,252954,565187,5228288,504527,7373251,2410443,2004017,2529269,2529275,2004401,4246630,2529792,6632522,2529793,2744343,4247796,2158148,7825203,3919000,7826109,4102710,4247927,2529804,4314481,2530317,4316439,2530322,4419537,180083,4469186,4649869,928921,2531503,2531894,15307,5667666,2005328,2005329,7336299,4753972,7803517,7837597,2006376,15437,1838916,2114010,15439,4916482,15442,6976206,2038850,2054981,15690,834890,835669,884940,5931258,885458,3588720,1665248,6943719,2413165,997941,998078,998984,7498521,5259812,5546129,7010401,7011577,752085,2055894,6217252,7114543,886767,7115460,2055904,903534,904578,4552123,840944,6176929,2089828,2091139,5503867,7975012,2001603,6835853};
    char* d_pool[N];
    int Cnt[N],Sum[N],S[N],Next[N],hsd[N],bu[100010],tail[100010],net[450010];
    int n=0,tot=0,poolD;
    scanf("%d",&n);
    FILE *f1=fopen("dictionary.txt","rb");
    FILE *f2=fopen("results.txt","w");

    fseek(f1,0,SEEK_END);
    int Size=ftell(f1);
    rewind(f1);
    fread(d_buf,1,Size,f1);
    fclose(f1);
    //Read the dictionary
    tot=0;
    end=d_buf+Size;
    for (now=d_buf;now<end;++now){
        sum=0;  d_pool[++tot]=now;
        while (low(*now))   sum=sum*131u+*now++;
        sum=sum&mod;    *now++='\0';
        Next[tot]=hsd[sum]; hsd[sum]=tot;
    }
    //Insert the dictionary into Hash map

    f1=fopen("article1.txt","rb");
    fseek(f1,0,SEEK_END);
    Size=ftell(f1);
    rewind(f1);
    fread(art,1,Size,f1);
    fclose(f1);
    art[Size]='\0';

    for (end=art;*end!='\0';end++){
        if ((*end<='Z') && (*end>='A')) *end=(*end)+32;
        else if ((*end)>'z')    *end='%';
    }
    // Lower litter

    while (!low(*end))  end--;
    end++;  *end='\0';
    ut x=0;
    for (now=art;now<end;++now){
        while (!low(*now))  ++now;
        sum=0;  begin=now;
        while (low(*now))   sum=sum*131u+*now++;
        x=sum&mod;  *now='\0';
        poolD=hsd[x];
        if (poolD && !Next[poolD] && *d_pool[poolD]==*begin && *(d_pool[poolD]+1)==*(begin+1))
        Cnt[poolD]++;
        else while (poolD!=0){
            if (strcmp(begin,d_pool[poolD])==0) {
                Cnt[poolD]++;   break;
            }
            poolD=Next[poolD];
        }
    }

    //Insert article 1

    f1=fopen("article2.txt","rb");
    fseek(f1,0,SEEK_END);
    Size=ftell(f1);
    rewind(f1);
    fread(art,1,Size,f1);
    fclose(f1);
    art[Size]='\0';

    for (end=art;*end!='\0';end++){
        if ((*end<='Z') && (*end>='A')) *end=(*end)+32;
        else if ((*end)>'z')    *end='%';
    }
    // Lower litter

    while (!low(*end))  end--;
    end++;  *end='\0';
    sum=0;
    for (now=art;now<end;++now){
        while (!low(*now))  ++now;
        sum=0;  begin=now;
        while (low(*now))   sum=sum*131u+*now++;
        x=sum&mod;  *now='\0';
        poolD=hsd[x];
        if (poolD && !Next[poolD] && *d_pool[poolD]==*begin && *(d_pool[poolD]+1)==*(begin+1))
        Sum[poolD]++;
        else while (poolD!=0){
            if (strcmp(begin,d_pool[poolD])==0) {
                Sum[poolD]++;   break;
            }
            poolD=Next[poolD];
        }
    }
    //Insert article 2

    f1=fopen("stopwords.txt","rb");
    fseek(f1,0,SEEK_END);
    Size=ftell(f1);
    rewind(f1);
    fread(art,1,Size,f1);
    fclose(f1);
    art[Size]='\0'; end=art+Size;

    sum=0;
    for (now=art;now<end;++now){
        while (!low(*now))  ++now;
        begin=now;  x=stop[sum];    poolD=hsd[x];
        for (;low(*now);now++); *now='\0';  sum++;
        while (poolD!=0){
            if (strcmp(begin,d_pool[poolD])==0) {
                Cnt[poolD]=Sum[poolD]=-1;   break;
            }
            poolD=Next[poolD];
        }
    }
    // Insert stopwords

    int l=0;
    int z[8];
    double s1=0,s2=0,p1=0,p2=0;
    now=art;

    if (n>tot)  n=tot;
    for (i=1;i<=tot;++i)
        if (Cnt[i]>0){
            x=Cnt[i];
            if (!bu[x]) bu[x]=i;
            net[tail[x]]=i; tail[x]=i;
        }

    sum=0;
    for (i=M;i;i--)
        for (int j=bu[i];j;j=net[j]){
            s1+=i;  S[j]=-1;    sum++;
            begin=d_pool[j];
            while (*begin!='\0')    *now++=*begin++;
            *now++=' '; x=i;
            while (x)   z[++l]=x%10,x/=10;
            while (l)   *now++=z[l--]+48;
            *now++='\n';
            if (sum>=n) goto END;
        }
    END:;
    *now++='\n';

    memset(bu,0,sizeof(bu));
    memset(tail,0,sizeof(tail));
    memset(net,0,sizeof(net));

    for (i=1;i<=tot;++i)
        if (Sum[i]>0){
            x=Sum[i];
            if (!bu[x]) bu[x]=i;
            net[tail[x]]=i; tail[x]=i;
        }

    sum=0;
    for (i=M;i;i--)
        for (int j=bu[i];j;j=net[j]){
            s2+=i;
            if (S[j]==-1)   p1+=Cnt[j],p2+=i;
            begin=d_pool[j];
            while (*begin!='\0')    *now++=*begin++;
            *now++=' '; x=i;
            while (x)   z[++l]=x%10,x/=10;
            while (l)   *now++=z[l--]+48;
            *now++='\n';    sum++;
            if (sum>=n) goto END2;
        }
    END2:;
    *now='\0';
    p1/=s1; p2/=s2;
    if (p1>p2)  p1=p2/p1;   else p1=p1/p2;
    printf("%.5lf",p1);
    fprintf(f2,"%.5lf\n\n",p1);
    fwrite(art,1,now-art,f2);

    exit(0);
}

int main(){
    unsigned size=128u<<22;
    char *p=(char *)malloc(size)+size;
    __asm__ __volatile__
    (
        "movq  %0, %%rsp\n"
        "pushq $exit\n"
        "jmp main2\n"
        :: "r"(p)
    );
}

后记

整个大作业连写带优化花了差不多一周的时间吧.我第一次交的时间比较晚,大概是在大作业截止前一周左右,而且第一次交的代码里用的是快排,不知道哪里传参不太对,跑了60多秒233333.最后的优化过程基本上是想到一个点就改一次,交一次.排名也是一点点刷上去的,确实费了一番功夫.

大部分人的时间在1s以内,0.5s附近的人居多.最后的平均分可能能到1.5分左右?(没有算,瞎猜的).果然内卷要从大一抓起.现在看来作业里的那一句"同时,建议同学们用本课程所学到的知识来解决问题。"就像是一个笑话,至少最前面的一批人使用的优化手法肯定涉及到了课外的内容.

个人觉得比卡常的题目最好不要涉及大规模的输入和输出,这样可以把卡常的主要精力放在优化运算上.IO的卡常手法也就那么几个,没有运算卡常有意思.

谨以此文纪念那些为卡常而头秃的夜晚.

说点什么
支持Markdown语法
好耶,沙发还空着ヾ(≧▽≦*)o
Loading...