求最长的公共子串(NOI’93第一题)
求N个字符串的最长公共子串,N<20,字符串长度不超过255。例如N=3,由键盘
依次输入3个字符串为
Whatislocalbus?
Namesomelocalbuses.
loca1busisahighspeedI/Obusclosetotheprocessor.
则最长公共子串为“localbus”。
此题也是作为第一题出现,同样有很多人在此题上失分。我们都做过求n个数最大公
约数的问题,在那道题中求3个数的最大公约数时,可以先求两个数的最大公约数,再将
此数与第三个数求一次最大公约数。有人从那道题中得到“启发”,设s(p,q)为字符串p
和q的最长公共子串,则p、q、r的最长公共子串为s(s(p,q),r)。这样只需编写一个求两
个字符串的最长公共子串的过程即可。但这种方法对不对呢?看看下面的例子。
三个字符串分别为‘abc’、cab’、‘c’,则s(p,q)=‘ab’,s(s(p。,q),r)=‘’。事实上这
三个字符串有公共子串‘c’。显然上面的算法是错误的,原因在于没有考虑到本题与求最
大公约数那道题在性质上的不同之处。最大公约数可以由局部解得到全局解,而本题却不
能。正确的做法是列举出其中一个字符串的所有子串,找出其中最长的而且是公共的
子串。
FORi:=1TO第一个字符串的长度DO
FORj:=iTO 第一个字符串的长度DO
IF(第i个字符到第j个字符的子串为公共子串
AND(j-i十1>当前找到的最长公共子串的长度max)
THENBEGIN
max←j-i十1;
最长公共子串←此子串;
END;
为了提高效率,我们可以将最短的字符串作为第一个字符串。此题需要考虑的并不像
多项式加法那道题那么多,但是它提醒我们在不清楚题目的性质之前,不能滥用以前的方
法。
4.可重复排列(NOI’94第一题)
键盘输入一个仅由小写字母组成的字符串,输出以该串中任取M个字母的所有排列
及排列总数(输入数据均不需判错)。
此题是由全排列问题转变而来,不同之处在于:一个字符串中可能有相同的字符,导
致可能出现重复的排列。例如从字符串‘aab’中取2个字符的排列只有三种:‘aa’、‘ab’、
‘ba’。如何去掉那些可能重复的排列呢?一种想法就是每产生一种不同的排列就记录下
来,以便让以后产生的排列进行比较判重。这种想法显然没有考虑到随着字符串长度的增
加,排列将会多得无法记录,而且这种判重方法在效率上也会很低。最好有一种方法能在
产生排列的过程中就能将重复的去掉。先看一看全排列的递归过程:
PROCEDUREWork(k);
BEGIN
IFk=m十1
THEN打印结果
ELSEFORi←1TO字符串长度nDO
IFi<>e〔1〕,e[2〕…e[k-1]
THEN
BEGIN
e〔k〕←i;
Workk十1);
END;
END;
让我们来分析产生重复的原因。考虑从字符串‘aab’中取2个字符的排列。当e〔1]从
1变到2时,字符串中的字符却没有变,都是‘a’。这样我们只要在改变e〔k]时,判断其对
应的字符是否也改变。即在上面的过程的循环中加一句判断(设字符串为s):IFs[i]<>
s[e[k]]THEN…;这当然只是一个粗略的想法,我们仅仅用上面的例子就能发现问题:
程序在对e[k〕的每一次赋值之前都要进行一次判断,而不是我们预想的在改变e[k]时才
进行判重。我们用一个布尔型的局部变量First来记录是否是对e[k]进行第一次赋值。修
改后的程序如下:
PROCEDUREWork(k);
BEGIN
First←True;
IFk=m十1
THEN打印结果
ELSEFORi←1TO字符串长度nDO
IF(i<>e〔1〕,e〔2〕…e[k-1]AND
(FirstORs〔i[<>s〔e[k]])
THEN
BEGIN
e[k]←i;
First←False;
Work(k十1);
END;
END;
很多选手的程序到此就为止了,可是它还有一个致命的错误:我们在判重时假定这个
字符串中的字符已经排好顺序,即相同的字符已经连在一起。事实上并不是这样,输入的
字符串中的字符排列是任意的,需要我们在递归之前作一次排序的初始化才能保证程序
运行得正确。
求N个字符串的最长公共子串,N<20,字符串长度不超过255。例如N=3,由键盘
依次输入3个字符串为
Whatislocalbus?
Namesomelocalbuses.
loca1busisahighspeedI/Obusclosetotheprocessor.
则最长公共子串为“localbus”。
此题也是作为第一题出现,同样有很多人在此题上失分。我们都做过求n个数最大公
约数的问题,在那道题中求3个数的最大公约数时,可以先求两个数的最大公约数,再将
此数与第三个数求一次最大公约数。有人从那道题中得到“启发”,设s(p,q)为字符串p
和q的最长公共子串,则p、q、r的最长公共子串为s(s(p,q),r)。这样只需编写一个求两
个字符串的最长公共子串的过程即可。但这种方法对不对呢?看看下面的例子。
三个字符串分别为‘abc’、cab’、‘c’,则s(p,q)=‘ab’,s(s(p。,q),r)=‘’。事实上这
三个字符串有公共子串‘c’。显然上面的算法是错误的,原因在于没有考虑到本题与求最
大公约数那道题在性质上的不同之处。最大公约数可以由局部解得到全局解,而本题却不
能。正确的做法是列举出其中一个字符串的所有子串,找出其中最长的而且是公共的
子串。
FORi:=1TO第一个字符串的长度DO
FORj:=iTO 第一个字符串的长度DO
IF(第i个字符到第j个字符的子串为公共子串
AND(j-i十1>当前找到的最长公共子串的长度max)
THENBEGIN
max←j-i十1;
最长公共子串←此子串;
END;
为了提高效率,我们可以将最短的字符串作为第一个字符串。此题需要考虑的并不像
多项式加法那道题那么多,但是它提醒我们在不清楚题目的性质之前,不能滥用以前的方
法。
4.可重复排列(NOI’94第一题)
键盘输入一个仅由小写字母组成的字符串,输出以该串中任取M个字母的所有排列
及排列总数(输入数据均不需判错)。
此题是由全排列问题转变而来,不同之处在于:一个字符串中可能有相同的字符,导
致可能出现重复的排列。例如从字符串‘aab’中取2个字符的排列只有三种:‘aa’、‘ab’、
‘ba’。如何去掉那些可能重复的排列呢?一种想法就是每产生一种不同的排列就记录下
来,以便让以后产生的排列进行比较判重。这种想法显然没有考虑到随着字符串长度的增
加,排列将会多得无法记录,而且这种判重方法在效率上也会很低。最好有一种方法能在
产生排列的过程中就能将重复的去掉。先看一看全排列的递归过程:
PROCEDUREWork(k);
BEGIN
IFk=m十1
THEN打印结果
ELSEFORi←1TO字符串长度nDO
IFi<>e〔1〕,e[2〕…e[k-1]
THEN
BEGIN
e〔k〕←i;
Workk十1);
END;
END;
让我们来分析产生重复的原因。考虑从字符串‘aab’中取2个字符的排列。当e〔1]从
1变到2时,字符串中的字符却没有变,都是‘a’。这样我们只要在改变e〔k]时,判断其对
应的字符是否也改变。即在上面的过程的循环中加一句判断(设字符串为s):IFs[i]<>
s[e[k]]THEN…;这当然只是一个粗略的想法,我们仅仅用上面的例子就能发现问题:
程序在对e[k〕的每一次赋值之前都要进行一次判断,而不是我们预想的在改变e[k]时才
进行判重。我们用一个布尔型的局部变量First来记录是否是对e[k]进行第一次赋值。修
改后的程序如下:
PROCEDUREWork(k);
BEGIN
First←True;
IFk=m十1
THEN打印结果
ELSEFORi←1TO字符串长度nDO
IF(i<>e〔1〕,e〔2〕…e[k-1]AND
(FirstORs〔i[<>s〔e[k]])
THEN
BEGIN
e[k]←i;
First←False;
Work(k十1);
END;
END;
很多选手的程序到此就为止了,可是它还有一个致命的错误:我们在判重时假定这个
字符串中的字符已经排好顺序,即相同的字符已经连在一起。事实上并不是这样,输入的
字符串中的字符排列是任意的,需要我们在递归之前作一次排序的初始化才能保证程序
运行得正确。