题目大意:
小猫母亲最近生病,但是小猫家很穷没钱来回火车,因此只能在当地移动中心使用SMS发送消息给它母亲,但是最近移动中心的SMS系统故障,它的消息可能夹杂在其他消息或者乱码中(但是它的消息中不含有其他消息或乱码,即消息是完整的,只不过插在了其他消息或乱码当中),为确保母亲能受到有效的消息并能识别,他连续发送两次,这两次个产生一串字符序列,每条序列中都有小猫给母亲的信息(在这两个序列中的位置可能不同,两边的字符可能不同),现请找出小猫可能发送的最长信息的长度(即找出两字符串中的最长公共子序列)。
现只有一个测例,测例中给出两字符串(长度不超过100,000),要求输出最长公共子序列长度。
注释代码:
/*
* Problem ID : POJ 2774 Long Long Message
* Author : Lirx.t.Una
* Language : C
* Run Time : 594 ms
* Run Memory : 3524 KB
*/
#include <string.h>
#include <stdio.h>
//number of characters
//字符的数量
//a ~ z + # + '\0'
//这也是基数排序中元素的初始权重
//由于z最大,因此就以z作为最大初始化权重
//这里是指ASCII码值
#define CHN 'z'
//字符串最大长度 × 2
//最长公共连续子序列用后缀数组解时需要将两个两串字符合并
//并且中间用一个特殊符号(#)隔开
//因此主串的长度就是100,000 × 2 + 2(还包括#、'\0')
#define MAXLEN 200002
#define MAX(x,y) ( (x) > (y) ? (x) : (y) )
#define MIN(x,y) ( (x) < (y) ? (x) : (y) )
//buffer,临时缓冲区
int buf1[MAXLEN];
int buf2[MAXLEN];
int buf3[MAXLEN];
//suffix array,后缀数组
//sa[i]就表示排名为i的后缀的起始元素的下标
int sa[MAXLEN];
//height,height数组,表示以sa[i - 1]和sa[i]为开头的
//后缀的最长公共前缀的长度
int h[MAXLEN];
//rank,基数排序的顺序位置数组
//rank[i]就表示以下标为i的元素开头的后缀的排名
int * rk;
char s[MAXLEN];//string,存放字符串
int
keq( int *v, int x, int y, int l ) {//key equal
//倍增基数排序中比较两个位置的关键字是否相等
//各个位置关键字有两个,一个是第一关键字还有一个是第二关键字
//第一关键字就是v[x]和v[y],第二关键字是v[x + l]和v[y + l]
//其中v是每个位置的值,x和y表示位置,l是combine length,即
//倍增基数排序的当前合并长度
return v[x] == v[y] && v[x + l] == v[y + l];
}
void
bd_sa( char *s, int n, int mv ) {//build SA
//使用倍增基数排序算法构造SA数组
//string,原字符串
//n为string中字符的个数,包括'\0',该元素rank始终为0,即排名最小的
//maximum rank value,最大排名值
//rank value of index i
//s数组中i下标元素的排名值,初始化时就等于该位置字符的ASCII码大小
//随着倍增基数排序的进行,该值(vi[i])会逐渐趋向于rank[i]
//因此vi的长度是MAXLEN,而不是'z'
int * vi;
//sum of rank value/number of rank value
//sv[i]表示排名值为i的元素的个数
//是基数排序的核心部分
//基数排序的核心思想就是利用排名值的个数构造rank数组,从而获得
//各元素的排名而间接对数组排序
int * sv;
//location of rank value
//lv[i]表示排名值为i的元素的下标位置
int * lv;
int * t;//temporary,临时指针变量,用于交换
int v;//rank value,当前排名值
int cl;//combine length,待排序的合并长度
int i, j;//计数变量
//初始化
vi = buf1;
sv = buf2;
lv = buf3;
//以下四行为初始基数排序
//取名的时候利用了向量传递的特性方便记忆
for ( i = 0; i <= mv; i++ ) sv[i] = 0;//计数器清零
for ( i = 0; i <= n; i++ ) sv[ vi[i] = s[i] ]++;//初始化排名值并统计个数
for ( i = 1; i <= mv; i++ ) sv[i] += sv[i - 1];//对排名值个数梯度累加
for ( i = n; i >= 0; i-- ) sa[ --sv[ vi[i] ] ] = i;//间接得出排名sa
//以上只完成了第一轮的第一关键字排名,一下就是倍增算法
//每次先倍增地对第二关键字进行排名
//然后再将第一和第二关键字进行合并并排名,然后就成为了下一轮的第一关键字
//循环操作以上两步,该过程中v的值逐渐趋向于真实rank值,直到v的值完全等于rank值为止
//最直接的表现就是v的最大值等于n
for ( cl = 1, v = 0; v <= n; cl <<= 1, mv = v ) {
//由于上面四行代码完成了一个字符的排名
//因此接下来的合并排序长度就为1
//合并排序完了以后最短的合并后的单位长度就为2了,依次类推合并长度每过一轮就倍增一次
//求第二关键字的lv值
//由于从n - cl + 1的位置开始,与其合并的元素都在外面,因此这些位置的第二关键字都为0
//因此从左往右将其从0开始排名,即lv[0 ~ 1-cl] = n-cl+1 ~ n
for ( j = 0, i = n - cl + 1; i <= n; i++ ) lv[j++] = i;
//接下来对前面的元素进行排序
for ( i = 0; i <= n; i++ ) if ( sa[i] >= cl ) lv[j++] = sa[i] - cl;
//以上已经完成对第二关键字的排名
//接下来又是基数排序的模板算法
//利用同时利用第一和第二关键字完成对两个关键字的合并排名
for ( i = 0; i <= mv; i++ ) sv[i] = 0;
for ( i = 0; i <= n; i++ ) sv[ vi[ lv[i] ] ]++;
for ( i = 1; i <= mv; i++ ) sv[i] += sv[i - 1];
for ( i = n; i >= 0; i-- ) sa[ --sv[ vi[ lv[i] ] ] ] = lv[i];
//但是得到的仅仅是sa数组,还要合并得出vi数组才行
//合并得到vi数组,由于要跟新vi的过程中还要利用vi,但是lv数组已经没用了
//因此可以交换两者的空间,达到重复利用的效果
for ( t = vi, vi = lv, lv = t, vi[ sa[0] ] = 0, v = 1, i = 1; i <= n; i++ )
vi[ sa[i] ] = keq( lv, sa[i - 1], sa[i], cl ) ? v - 1 : v++;
}
rk = vi;//最后vi和rank就完全相等了
}
void
bd_h( char *s, int n ) {//build height
//构造height数组
int i, j;
int cl;//common length,公共连续子序列的长度
//由数学推导的height[ rank[i] ] ≥ height[ rank[i] - 1] - 1
//虽然这里乍一看两个for循环应该是n^2的复杂度,其实由于上面的条件控制
//是的复杂度降到几乎n
//!!注意:s[n] = '\0',因此rank[n] = 0,所以rank[n] - 1 < 0,所以sa[ rk[n] - 1 ]
//可能会导致段错误,因此i < n(第一个for语句中)
for ( cl = 0, i = 0; i < n; h[ rk[i++] ] = cl )
//这里cl为上一轮的height值,--就是上一轮的height值-1(见不等式)
//回顾:height[i] = suffix(sa[i - 1])和suffix(sa[i])的最长公共前缀的长度
for ( cl ? --cl : 0, j = sa[ rk[i] - 1 ]; s[i + cl] == s[j + cl]; cl++ );
}
int
main() {
int len;//第一串字符串的长度
int n;//合并后的总长度(包括'\0')
int ans;
int i;//计数变量
//合并字符串
scanf("%s", s);
len = strlen(s);
s[len] = '#';
scanf("%s", s + len + 1);
n = len + strlen( s + len );
bd_sa( s, n, CHN );
bd_h( s, n );
//最长公共连续子序列长度必然为height中的最大值
//但是注意要剪枝,并不是最大的height就一定符合要求
for ( ans = 0, i = 1; i <= n; i++ )
if ( h[i] > ans )
//只有到两个排名相邻的后缀长度在第一个字符串长度两边时才表名
//这两个后缀中分别包含了前后两个字符串中内容
//否则这两个后缀都只包含一个字符串中的内容,因此无公共子串而言
//注意这里的剪枝
if ( MIN( sa[i - 1], sa[i] ) < len && MAX( sa[i - 1], sa[i] ) > len )
ans = h[i];
printf("%d\n", ans);
return 0;
}
无注释代码:
#include <string.h>
#include <stdio.h>
#define CHN 'z'
#define MAXLEN 200002
#define MAX(x,y) ( (x) > (y) ? (x) : (y) )
#define MIN(x,y) ( (x) < (y) ? (x) : (y) )
int buf1[MAXLEN];
int buf2[MAXLEN];
int buf3[MAXLEN];
int sa[MAXLEN];
int h[MAXLEN];
int * rk;
char s[MAXLEN];
int
keq( int *v, int x, int y, int l ) {
return v[x] == v[y] && v[x + l] == v[y + l];
}
void
bd_sa( char *s, int n, int mv ) {
int * vi;
int * sv;
int * lv;
int * t;
int v;
int cl;
int i, j;
vi = buf1;
sv = buf2;
lv = buf3;
for ( i = 0; i <= mv; i++ ) sv[i] = 0;
for ( i = 0; i <= n; i++ ) sv[ vi[i] = s[i] ]++;
for ( i = 1; i <= mv; i++ ) sv[i] += sv[i - 1];
for ( i = n; i >= 0; i-- ) sa[ --sv[ vi[i] ] ] = i;
for ( cl = 1, v = 0; v <= n; cl <<= 1, mv = v ) {
for ( j = 0, i = n - cl + 1; i <= n; i++ ) lv[j++] = i;
for ( i = 0; i <= n; i++ ) if ( sa[i] >= cl ) lv[j++] = sa[i] - cl;
for ( i = 0; i <= mv; i++ ) sv[i] = 0;
for ( i = 0; i <= n; i++ ) sv[ vi[ lv[i] ] ]++;
for ( i = 1; i <= mv; i++ ) sv[i] += sv[i - 1];
for ( i = n; i >= 0; i-- ) sa[ --sv[ vi[ lv[i] ] ] ] = lv[i];
for ( t = vi, vi = lv, lv = t, vi[ sa[0] ] = 0, v = 1, i = 1; i <= n; i++ )
vi[ sa[i] ] = keq( lv, sa[i - 1], sa[i], cl ) ? v - 1 : v++;
}
rk = vi;
}
void
bd_h( char *s, int n ) {
int i, j;
int cl;
for ( cl = 0, i = 0; i < n; h[ rk[i++] ] = cl )
for ( cl ? --cl : 0, j = sa[ rk[i] - 1 ]; s[i + cl] == s[j + cl]; cl++ );
}
int
main() {
int len;
int n;
int ans;
int i;
scanf("%s", s);
len = strlen(s);
s[len] = '#';
scanf("%s", s + len + 1);
n = len + strlen( s + len );
bd_sa( s, n, CHN );
bd_h( s, n );
for ( ans = 0, i = 1; i <= n; i++ )
if ( h[i] > ans )
if ( MIN( sa[i - 1], sa[i] ) < len && MAX( sa[i - 1], sa[i] ) > len )
ans = h[i];
printf("%d\n", ans);
return 0;
}
单词解释:
frequently:adv, 频繁地,时常
mobile:adj, 机动的,不固定的
append:vt, 附加,追加
redundancy:n, 冗余
issue:n, 问题,故障
charge:vt, 收费
persuade:vt, 说服
dedicate:vt, 致力于,献给
beloved:adj, 心爱的,挚爱的