Nov 23
虽然说最后的结果没有比我预期的差,但是过程还是很囧的。鉴于这里是我自己的博客,废话就多写一点。

总之就是从10月的某个Hi消息开始的,jieyu突然告诉我,搞到了一个福州赛区的名额,和ldl同学一起,组个队伍,参加区域赛。缘由很简单,他想公费回家,顺便打个酱油。于是名叫烤馍馍(OpenKaomomo)的酱油队诞生。

真是很久没有做题了。做题的日子是很快乐的,至少可以把日子过得很简单的,其他七七八八的事情可以暂时放到一边,虽然有些不负责,但是突然觉得一切都变简单了;除了那些迫在眉睫的事情不得不分出时间来处理之外。

于是开始到机房和ldl一起做比赛,并且开始在WOJ上面刷题。大概做了四场比赛吧,WOJ的题目也刷七十多道(当然基本都是简单题),慢慢的找回了大二时的那种感觉(其实就是花大把的时间DEBUG那些BUGGY CODE)。不过之前不是搞得特别清楚的东西,在时间的积淀下,很多都豁然开朗,写起代码来也顺畅多了。

前往福州的旅程是比较曲折的。其实那一堆人里面我是最没有区域赛参赛经验了——因为这是我第一次(估计也是最后一次)参加Regional。所以在出发前两天(周三),我也还等着通知,在群里头问何时何地集合。然后发现原来大家也都在等。于是只好给董哥打电话,问。说是车票买的周五的,不是周四,要和实践办的叶主任一起去。于是给叶主任电话,约好,第二天到他办公室,谈清细节(到BBS上发集合帖),领了400块钱,下午拉上钟安原和FOUR两位同学一起到学院旁的自强去采购了320块钱的食品。真重。

周五下午3点机房集合了,把吃的东西分发,然后13个人(包括甜菜和CW)挤上一辆面包车开往武昌火车站,吃零食,看小说,看电影,睡觉(还是甜菜比较简单,从上车睡到下车),早上10点,抵达福州。真暖和。穿太多了。领着大家在公交车站等了半天308,到宾馆把房间开了,然后在天峰宾馆站等了半天91,然后在福大东门又等了好一会儿96,好不容易到福大,才跟志愿者MM碰上面。顺便说一句,志愿者MM也是福一中的,高中学妹啊。在福大食堂吃饭,jieyu和ldl随后赶到,一起吃饭去参加练习赛。进场的时候就已经迟了一点。

总算切入主题,练习赛。练习赛一共五题,ldl从后往前看,我从前往后看,jieyu看中间。A是简单的字符串处理,ldl速度上去敲,居然WA了。然后是我看的E题,素数筛法+因式分解,ldl上去敲,但是他筛法什么记不太清楚了,于是换我(正好前一阵写了好几题类似的),敲出来没有过样例,打印代码。帮ldl看A题,发现是题意没看清,100个word看成100个字符,AC。然后看自己的代码,发现有两个BUG,改了提交,AC。接着是B题,刚开始觉得最小生成树是错的,后来jieyu仔细考虑,发现如果用kruscal是可以保证正确性的。ldl敲。WA。WA。WA。看了一遍代码以后,发现是找边的循环退出条件位置不对,如果最小生成树只有1~2条边,就会出错,改之,AC。于是发现自己还是很适合搞DEBUG。这是好事还是坏事?考虑C题,SPlay有模板,不过题目的难点不在于SPlay,而在于树的输出,考虑了很久,最后放弃了。D题的图着色,写了暴力算法,不过没交。比赛结束,3题,排名78左右。练习赛的总结是,必须要给出一些极端测试数据,不能乱交。

结束后回去等车等得很痛苦,尤其是福大东门的91路……等了四五十分钟,最后还是让老妈和舅开车过来把大家接到南洋饭店吃了顿好的。晚上睡得不安稳,4点多被噩梦闹醒,然后噩梦不断,迷糊到早上7点出头,被闹钟吵醒。下楼吃了早餐(这个酒店的早餐不给力啊),然后坐车。志愿者说武大的队伍在10号车,上车坐了好一会儿说是错了,应该在14号车,结果走过去又说是13号车,上车后董哥电话问:你们上了3号车没……真是攒足了RP啊。

决赛奇葩地在9点30开始。10题,照例ldl从头,我从后,杰瑜中间。

最初是暴力的j题在100分钟AC的。首先是过不了样例,发现是忘了处理不合法的前导零。然后发现程序还有BUG,数组索引有错。然后提交,WA。jieyu检查发现是因为直接用除法,没有考虑整除。我则认为可能会溢出,改用long long进行乘法。于是AC。

第3个开的,F题是个AC自动机,就是数据量BT了一点。ldl敲完标程,过了样例,但是我出了个很弱的数据就跑错了。修改了以后提交,TLE。TLE。TLE。ldl一直比较紧张,到最后才45min的时候才静下心把问题找出来,AC。

第4个开的题目应该是B题吧。无向图最小割,被我读成了N个最大流,又是我的错-。- 总之就是TLE+TLE,在比赛最后7分钟的时候貌似还提交了一次,TLE,放弃。

H题,这题就是个大杯具,开的第2个题,AC的最后一个题。问题大概应该算在我头上吧,因为最初的时候没有把题意完全看清楚(固定的5分钟间隔),导致思考的时候出现各种偏差,提交了3次,都WA了。最后一小时我提出一个很繁琐的算法:记f[i][j]是表示在i分钟第一次选(0<=i<=4),到第i+5*j分钟可以得到的最大值,for (i = 0; i < 5; i++) f[i][j] = max{ f[i][k]+g(i+5*j) | 0<=k<j }, 其中j是选择的时间点,g(i+5*j)的值是0或1,由一个辅助的数组G[k][1..N]来导出,G[k][t]表示第t个课程在前i+5*k分钟是否被选中(且每次确定G[j][1..N]需要由G[k][1..N]更新)。此算法虽然繁琐且效率低,但是因为每一次往前都能保证最优,可以保证正确性。jieyu大致是从这里得到一点启发,想出正确的贪心算法,终于在269min,也就是最后半小时的时候AC了这题。

我们还在最后半小时的时候开了G题,在DAG上的一个简单DP,编码完全没有难度,感觉比J题还简单。这题是我去写的,思路很清晰,就是最后了比较紧张,出了两个BUG,没能在结束之前通过样例,怨念。关于这题的另一个怨念在于,开场我就看过了这题,之所以没有之前就考虑它,是因为它那OOXX的输入格式很让人纠结,跟jieyu提了一下,他说这题很有意思。然后就撂着了。杯具啊。

除了以上的题目,我们还考虑过E题(四边形费马点)、D题(XOR那题)。E题之所以没有去写,是因为杰瑜读错了题目,他问我Quadrangle这个词什么意思,我说不知道,不过Angle是角,Quadr含有4的意思,于是他认为是四面体。于是这题就废了。(字典啊字典……还是应该带上字典的,ldl同学。。)

比赛完基本上可以确定是打铁了。稍微有点失落,毕竟是第一次也是最后一次参加区域赛吧,没能拿牌还是挺遗憾的。尤其是在发挥正常的情况下至少还有2道题是确定可以出的,并且之前的题目过程太纠结。总之就是不甘+怨念。

比赛完去采购,买了很多东西。其实额外买了一瓶蜂蜜是想送给志愿者mm慰劳一下,不过出来以后才想起除了志愿者mm,还有一个志愿者gg,所以就一直没好意思拿出来~.~ 然后大家在草地上等吃饭,jieyu则干脆先跑去玩了。

大约4点50的时候,该mm说,我们有两只队伍获奖了,应该是Valor和烤馍馍,需要提前去颁奖会场做准备。过山车啊这是。。。后来在饭桌上跟大家提起,说是不对,Forward在封榜前就3题了,不应该在烤馍馍后面的。于是打电话给该mm询问,说是只确定是两支,不确定是哪两只。过山车啊这是。。。看来是确定铁牌了。5点45的时候再跟着志愿者走到颁奖会场,登记的时候神奇地发现,武大有三支队伍获奖,烤馍馍耗尽RP,排在了名单倒数第二的位置。过山车啊这是。。。

幸好过山车到站,最后顺利领到牌子……最终成绩是Valor,5题,铜牌第一;Forward,3题,铜牌30左右;烤馍馍,3题,铜牌倒二。

这里顺便提一下颁奖晚会的俩奇葩舞蹈,一个是劲舞(名字不太记得),前面两排mm,大都很漂亮,后面一排gg,大都很帅,不过最中间的那个mm明显不给力,腰围的绝对值比较大,其中一个做俯卧撑的动作,很吃力。另外一个舞蹈叫《女人花》,大多数mm都很不错,但是最核心的那个……腰围更粗,明显赘肉在颤,摆出各种S型,还叼花……我只能联想起当年那个“芙蓉姐姐 叼花”的照片,你去百度图片搜一下,第一张那个……

终于领了证书,坐上回程的车,回到宾馆,至此,福州比赛结束。最终的结果差强人意。虽然早先jieyu说应该拿个silver,但是根据现场的情况来看,正常发挥应该也就是5题,还是bronze,所以就不需要太怨念了。

----分割线----

最后是Felix021@烤馍馍的比赛总结:

· 看清题意。还是太忽视了一点。如果决定开始做一道题目,应该要有两个人看清题意,并且在细节上达成一致,这样才能保证不会出现各种糊涂问题,比如此次的E、H题。

· 每个人都应该把每题都大概看过,至少稍微思考一下,不至于漏掉G那样的简单题。

· 这次比赛对Board的关注太少了,以至于G这样的题目到最后半小时才开始考虑,实在是太过分了。

· 如果某人开了一题,条件允许的情况下,应该有人帮忙出测试数据。这次正式比赛的时候我们这个约定没被贯彻好。

· 大家都太紧张了,应该早点喝红牛的。
Nov 8

RMQ再学习笔记 不指定

felix021 @ 2010-11-8 23:54 [IT » 程序设计] 评论(1) , 引用(0) , 阅读(12513) | Via 本站原创
2年多以前看的算法,当时基本看懂了(实际上还是有问题的),今天再翻出来,感觉基本忘了,重新实现一遍,试着自己把推导过程也写写。非常赞的算法。

RMQ: Range Minimum Query, 区间最小值查询。已知数组在区间 [0, n) 有定义,给出 i, j (0 <= i < j <=n),求 [i, j) 之间的最小值。

朴素算法非常简单,但是显然,每次查询是O(N)的,效率很低:
min = +INFINITE
FOR k = i TO j - 1
  IF min > arr[k]
    min = arr[k]
  END IF
END FOR
RETURN min

由于经常遇到需要多次查询的情况,所以朴素算法不合意。一个比较好的改进是使用线段树/树状数组,首先通过O(NlogN)预处理,让每个分支节点保存了其所有叶子节点的最小值(由底向上递推)。这样每次查询的时候,就可以在O(logN)的时间内得到结果。具体代码稍繁琐,这里不写了。

最好的方法,是用叫做sparse_table的算法,其思路和线段树很像,区别是通过O(NlogN)的预处理,可以让查询的时间复杂度降低到O(1)。其具体思路为:

预处理:
1. 记F[s][p] = min{ arr[i] | s <= i < s + 2^p} = min(arr[s..(s + 2^p - 1)])。例如, F[1][2] = min{ arr[i] | 1<= i < 5} = min(arr[1..4])。
2. 由1,易推知,F[s][0] = min { arr[i] | s <= i < s + 1} = arr[s].
3. 通过由底向上递推的预处理得到所有满足(s + 2^p <= n, 即区间[s, s + 2^p) 包含于[0..n)区间内)的F[s][p] = min(F[s][p-1], F[s+2^(p-1)][p-1])。例如, F[2][3] = min(arr[2..9]) = min(min(arr[2..5]), min(arr[6..9])) = min(F[2][2], F[6][2]) = min(F[2][3-1], F[2 + 2^(3-1)][3-1])。

查询[i, j):
1. 令 k = log2(j - i)取整,则 (j - 2^k) <= (i + 2^k),即区间 [i, i + 2^k) 和 [j - 2^k, j) 必定覆盖 [i, j)。例如,区间[3, 8),k = log2(8-3)取整 = 2,则 [3, 7), [4, 8) 覆盖 [3, 8)。
2. RMQ[i, j) = min(F[i][k], F[j - 2^k][k]).

----扩展---
LCA(树中任意两个节点的最近公共祖先)问题,是可以通过RMQ+DFS来完成的,详见LCA问题。ZOJ 1141是一道纯LCA问题。

具体实现和检验代码如下(做了一点改进,F[s][p]中存放的是[s, s + 2^p)中最小值在数组中的索引):
Nov 2
C标准中的qsort函数,可以对任意类型的数组进行快速排序,用起来也还算方便,不过行为有一点诡异。加上之前曾经看过一篇文章(沈大写的,曾经登上过ecom的双周刊,居然在这个youalab.com也等出来了-。-),说是qsort不是线程安全的,所以把glibc的代码翻出来,看看qsort的具体实现。

用Ubuntu的话,找代码就很容易了:
引用
$ apt-get source libc6
$ cd eglibc-xxxx
$ grep -r . -Hne qsort | grep void

可以看到qsort是在stdlib.h里面的,源码在msort.c 302行,调用了qsort_r函数:
void
qsort (void *b, size_t n, size_t s, __compar_fn_t cmp)
{
  return qsort_r (b, n, s, (__compar_d_fn_t) cmp, NULL);                                                     
}
其中__compar_fn_t就是 int (*)(const void *, const void *)类型的函数指针,__compar_d_fn_t则是 int (*)(const void *, const void *, void *)类型的函数指针(为什么还有个void *呢?很神奇~求解)。

qsort_r函数也在msort.c中,约164行起。为了榨干CPU的性能,写了很多代码来优化效率,其流程大致是:

1. 判断额外所需内存大小,如果很小(<1024B),在栈上分配;否则获取系统内存参数——如果比可用内存小,试图在堆上分配;如果所需内存超过物理内存(为防出现不得不使用交换分区的情况致使性能恶化,故不分配),或者分配失败(可用空间不足),使用性能较差的stdlib/qsort.c中的_quicksort函数来排序。
    1.1. _quicksort位于 stdlib/qsort.c,对快排进行了两个改进:1. 将递归转化为递推; 2. 使用插入排序来处理4个元素以内的区间。不过用于交换两个元素的宏SWAP(a, b, size)没有额外的优化,比较意外(本来以为会考虑一次多个字节拷贝,或者使用duff device来减少循环)。

2. 分配成功的情况下,使用额外分配的空间,调用 msort_with_tmp() 函数来进行排序。
    2.1. 如果单个元素的大小超过32个字节,那么使用间接排序,分配空间时就有额外的判断,如果需要间接排序,则额外分配2n个指针的空间,前n个空间用于存放原指针,后n个空间用于按照所指元素大小存放排序好的指针。最后再按照排序好的指针顺序将元素拷贝。(注释中提到了Knuth vol. 3 (2nd ed.) exercise 5.2-10.,应该是指这个算法出自Knuth的《The Art of Computer Programming》卷3吧)
    2.2. 否则使用直接排序。这里也进行了一些优化,主要是为msort_with_tmp()提供p.var参数,完成针对元素的大小进行拷贝的优化:默认是使用gcc内置的__builtin_mempcpy来拷贝。
    2.3. msort_with_tmp对典型的快排也进行了非常赞的优化。
    2.3.1. 从msort_with_tmp的函数名和代码可以看出,这个算法不是就地排序,而是使用了第一步分配的O(N)的额外空间,这样可以让排序时的拷贝效率更高(这个优化应该对CPU的cache命中率也有较大的提高)。
    2.3.2. qsort_r传进来的参数p有个属性var
    (1) 默认情况下p.var = 4,使用mempcpy来进行拷贝。
    (2) 如果单个元素大于32bytes,p.var = 3, 表示是间接排序,拷贝的是指针,不是元素
    (3) 如果数组是按uint32_t对其的,且元素的大小是uint32_t的倍数,则可以进一步优化到每次按照uint32_t/uint64_t/unsigned long来进行拷贝,一次拷贝4或者8个字节,此时p.var = 0,1,2表示按照uint32_t, uint64_t, unsigned long拷贝。

直接在里头做了些注释,有兴趣的话可以更仔细地看看。可以的话自己去下了glibc的源码阅读,更有意思:D
Oct 22

灵魂守恒定律 不指定

felix021 @ 2010-10-22 23:54 [IT » 程序设计] 评论(2) , 引用(0) , 阅读(5731) | Via 本站原创
一切都是从,那道蚂蚁题,开始的
那题中的蚂蚁有20只,爬在长的木棒一根
左边蚂蚁10只向右爬
右边蚂蚁10只向左爬
蚂蚁爬的速度都相同
一碰头各自原速调头
然后就问,这些个蚂蚁,要碰多少次头才会从木棒上都掉下去

一说起这个问题,可能很多人有看过编程之美4.7 蚂蚁爬杆的问题:有一根27厘米长的细长木杆,在第3厘米、7厘米、11厘米、17厘米、23厘米这五个位置 处各有一只蚂蚁。木杆很细,不能同时通过2只蚂蚁。开始时候,蚂蚁的头朝左还是朝右是任意的,他们只会朝前走或者掉头,但是不会后退。当任意2只蚂蚁碰头后时,2只蚂蚁会同时掉转头朝反方向走。架设蚂蚁每秒钟可以走一厘米的距离。编写程序,求所有蚂蚁都离开木杆的最短时间和最长时间。

留一些空间不剧透,有兴趣的同学可以先想想然后再往下看。
Sep 30
看了下百度百科对UTF-8的说明,随手写的,基本能用。

比较诡异的是本来UTF8getchar想用strncpy的,但是这个函数有坑....

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

#define alloc(type, size) ((type *)malloc(sizeof(type) * size))

struct UTF8char
{
    unsigned short length;
    char data[7];
};

unsigned UTF8getcharlen(const char *s)
{
    unsigned char t = (unsigned char) s[0];
    if (t < 0x80) //0xxx xxxx
        return 1;
    else if (t < 0xe0) //110x xxxx
        return 2;
    else if (t < 0xf0) //1110 xxxx
        return 3;
    else if (t < 0xf8) //1111 0xxx
        return 4;
    else if (t < 0xfc) //1111 10xx
        return 5;
    else if (t < 0xfe) //1111 110x
        return 6;
    else //0xff
        return 1;
}

int UTF8getchar(UTF8char *c, const char *s)
{
    int i;
    c->length = UTF8getcharlen(s);
    for (i = 0; i < c->length && s[i] != 0; i++)
        c->data[i] = s[i];
    c->data[i] = 0;
    return (i == c->length);
}

int UTF8cmp(const UTF8char *c, const char *s)
{
    return strncmp(c->data, s, c->length);
}

int main()
{
    char linebuf[4096];
    UTF8char c;

    scanf("%s", linebuf);

    int reti = UTF8getchar(&c, linebuf);
    assert(reti != 0);

    printf("%d %s\n", c.length, c.data);
    return 0;
}
Aug 21

程序员的浪漫 不指定

felix021 @ 2010-8-21 18:11 [IT » 程序设计] 评论(1) , 引用(0) , 阅读(4901) | Via 本站原创
dim arr(3)
arr(1)=-12590
arr(2)=-20306
arr(3)=-15133
str = ""
for i = 1 to 3
    str = str & chr(arr(i))
next
CreateObject("SAPI.SpVoice").Speak str
Aug 21

sdbm hash for PHP 不指定

felix021 @ 2010-8-21 01:59 [IT » 程序设计] 评论(0) , 引用(0) , 阅读(3421) | Via 本站原创
上一篇提到了,使用sdbmhash来生成64bit摘要。这个算法,是需要用在PHP里头的,但是PHP在设计的时候有点囧,用于表示任意变量的 zval 这个struct里头,有一个union是用于存放不同数据类型的,而该union中用于表示整型的变量,就只有一个long。于是很杯具地:

1. 32bit OS下面的php只支持32bit整数
2. 不支持无符号整型

那个抑郁啊,于是只好用php的bcmath这个大整数库来实现上一篇的sdbmhash算法:
function sdbmhash($str)
{
    $mul = "65599"; // (1 << 6) + (1 << 16) - 1
    $mod = "18446744073709551616"; // 1 << 64

    $hash = "0";
    for ($i = 0; $i < strlen($str); ++$i)
    {
        $hash = bcmod(bcmul($hash, $mul), $mod);
        $hash = bcmod(bcadd($hash, ord($str{$i})), $mod);
    }
    return $hash;
}

大整数库是用字符串来模拟的,没有对位移的直接支持。于是刚开始的时候用 bcmul($hash, 1<<6) 之类来替代C实现中的位移;然后果断发现自己SB了,直接乘65599更合适。

当然了,由于是用字符串来模拟的,可以想象这段代码效率是很低的。但是有多低呢?在我的Ubuntu虚拟机上测试了一下,1w次对18个字符的hash需要大约2.4s,也就是说一次调用大约需要0.2~0.3ms。宿主机是windows(AMD M320, 2.1GHz),php的效率更低,大约花了3~4s。在同样的时间里,纯C实现,可以进行相同的运算10,000,000+次,效率比大约是PHP:C = 1:1000。

由于0.2~0.3ms这个数量级比较大了,于是决定把它写成一个PHP扩展。参照百度文库的这篇教程 http://wenku.baidu.com/view/044da6f8941ea76e58fa04b1.html 比较快就上手了。

最终实现的效率比是大约1:100。看来PHP的扩展开销还是很大啊。

记得前年曾经和@Sandy讨论过ASP、PHP、JSP的效率,他认为ASP和PHP在同一个数量级,和JSP差距很大(而我当初则认为PHP和JSP差距不大)。这个数据很好地证明了这一点。

最后,附上这个PHP扩展。
下载文件 (已下载 515 次)
Aug 21
前两天在找个摘要算法,要求很简单,冲突少,生成64bit摘要;安全性可以忽略。MD5/SHA-1是好算法,但是它们生成的摘要是128/160bit的。Qining最初提出了个算法,将MD5的128bit分成4个32bit,d[0..3],然后通过以下公式获得64bit摘要:
引用
concat(d[0] XOR d[2], d[1] XOR d[3])
看起来挺有模有样的,但是用0..100,000去测试,发现了3个碰撞,毕竟没有数学理论的支持,胡搞还是不行的。另外再尝试了下,把MD5的前64bit拿出来,效果倒是好,前100,000没有碰撞。只是这种做法毕竟不让人放心,于是到网上搜了一下,发现不少人有此需求,但是没有现成的算法,挺无奈的。还好,有个人一语道破天机:用个字符串hash算法就行了。

于是翻出以前的一篇日志字符串的Hash,从里头把sdbm哈希函数搞出来,测试了一下(跑了好几分钟呢),效果很赞,一亿以内的整数都没有问题,代码如下:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <assert.h>
using namespace std;

typedef unsigned long long ull;

ull sdbmhash(const char *s){
    ull hash = 0;
    while (*s){
        hash = (hash << 6) + (hash << 16) - hash + *s++;
    }
    return hash;
}

int main()
{
    const unsigned N = 100000000;
    ull *all = new(nothrow) ull[N];
    assert(all != NULL);
    unsigned i = 0;
    char tmp[100];
    for (i = 1; i <= N; ++i)
    {
        snprintf(tmp, 100, "%09u", i);
        all[i] = sdbmhash(tmp);
        if (i % (N / 20) == 0) printf("i = %u\n", i);
    }
    sort(all, all + N);
    ull * end = unique(all, all + N);
    printf("%u\n", all + N - end);
    delete[] all;
    return 0;
}
分页: 5/22 第一页 上页 1 2 3 4 5 6 7 8 9 10 下页 最后页 [ 显示模式: 摘要 | 列表 ]