leetcode30 Substring with Concatenation of All Word

来源:互联网 发布:js兄弟节点 编辑:程序博客网 时间:2024/06/05 08:19

You are given a string, S, and a list of words, L, that are all of the same length. Find all starting indices of substring(s) in S that is a concatenation of each word in L exactly once and without any intervening characters.

For example, given:
S"barfoothefoobarman"
L["foo", "bar"]

You should return the indices: [0,9].

(order does not matter).

这是一个很不错的问题,可以用sliding window做,可以用hash table做,可以用Rabin-Karp algorithm算法做。可惜我一种都没想出来,只用了permutation number+KMP算法,方法有局限性;用KMP+set/multiset,超时。这两种方法后面会分析为什么出错。

声明

函数头vector<int> findSubstring(string S, vector<string> &L)

n是S的长度,m是L中字符串的个数,len是L中每个word的长度。

brute-force算法

首先来看剪枝brute-force算法。这个算法的主要思想是对L的所有子串进行统计,统计后的结果存于一个map<string,int> table中,对S的所有子串进行遍历比较,如果比较结果和table相同,说明S的这个子串是满足返回性质的。如果简单利用这种性质对S中的字符逐个进行比较的话,算法的时间复杂度是n*len*m,因为对于每个字符都要往前比较m*len个字符,得到结果后在S中前进一个字符。这种方法明显过于简单,而且超时,我们需要进行剪枝。

L, that are all of the same length, 是一条值得利用的性质。此条性质可以让我们每次进行一个word的比较,因为我们知道每次前进len个字符是一个word,用map进行查找,可以降低时间复杂度len的参数。但是,每次前进len个字符存在一个问题,这种初始设定限制我们在S中所有比较的word都是从len的倍数开始,比如S是“abbbfara”,L是"bbb","far",那对于S,只能比较"abb""bfa""ra"。为了避免这个问题,我们在S的遍历循环外加一个循环,for (j = 0; j < len; ++j)设置S中不同的起始位置。

对于方法的说明如下:

1 对L进行遍历,得到L的word table,便于进行比较。并设置一个临时变量tableTmp,用来记录S中的遍历结果。

2 设置一个外循环for (j = 0; j < len; ++j),初始化S中不同的起始位置

   在外循环内部,对S进行for(i=j;i<n;i+=len)的遍历,i+=len就是每次比较一个word长度。对当前i,取string strTmp=S.substr(i,len)得到当前word:

   2.1 如果strTmp在table中,说明这个word符合要求,我们会对tableTmp进行++tableTmp[strTmp]的处理,并++count说明有效word的个数。但是L的限制使得我们必须处理如下几种情况

   2.1.1如果遇到strTmp之前,tableTmp[strTmp]<table[strTmp],说明此strTmp有效,进行上述设置。

   2.1.2如果遇到strTmp之前,tableTmp[strTmp]==table[strTmp],说明最开始的strTmp是多余的(比如,S="farboofarboofarman",L={"far","far","boo","man"},start=0),再遇到S中的第二个"boo"时,明显从0开始的子串中到此为止存在2个"boo",这超出了L的限制,我们需要处理这种情况:对start进行增加,每次增加len长度,直到遇到一个"boo"并将其从之前的tableTmp/count中剔除,然后从这个start(这里是6)开始的字符串满足L的要求。

   2.1.3如果处理完2.1.1的情况后,满足count==m,这时候是满足如下限制:a. S中从start开始的字符串中每个word的个数不超过L的限制; b. 这个字符串的word个数又满足L的长度,这就说明这个字符串是L中word的组合,即start有效。可以在返回的vector<int>中插入start,但是后续的情况是a little tricky的:a. start不可能从i+len开始进行遍历,因为如果S="aabbccaabbcc",L={"aa","bb"},明显S中存在0/2/4/6四个有效起始位置,在i==4时候出现第一个valid start,这时候如果从i+len进行遍历,将忽略2~7之间的有效字符,所以start要设置为从start+len开始遍历;b.但是 i 下一个将要遍历6,如果把tableTmp/count全部清空,并将 i 也设置为从2开始进行计数,那先前的工作明显被重复执行,所以将在tableTmp中减去那个start~start+len的word,count减1,start前进len大小,这样避免了工作的重复。

   2.2 如果strTmp不在table中,就说明S中从start起始的子串不满足L的concatenation without any intervening chars,就说明之前的所有结果全部作废,就对start/tableTmp/count全部重新设置,进行下一个word的比较。

时间复杂的分析:如上所示,对于每个j,i遍历所有的word,遍历次数为n/len,j为len大小,所以整体时间复杂度是O(len* n/len)=O(n)。代码如下:

<span style="font-size:14px;">vector<int> findSubstring(string S, vector<string> &L) {if (S.size() < L.size()*L[0].size()) return vector<int>();vector<int> ret;map<string, int> table, tableTmp;int start, n = S.size(), m = L.size(), len = L[0].size(), i, j, count = 0, num = L.size();string strTmp;for (i = 0; i < m; ++i)++table[L[i]];for (j = 0; j < len; ++j){start = j;count = 0;tableTmp.clear();for (i = j; i < n; i += len){strTmp = S.substr(i, len);if (!table.count(strTmp)){start = i + len;tableTmp.clear();count = 0;continue;}if ((++tableTmp[strTmp]) <= table[strTmp]){++count;}else{--tableTmp[strTmp];string sb = S.substr(start, len);while (sb != strTmp){--count;start += len;--tableTmp[sb];sb = S.substr(start, len);}start += len;}if (count == num){string sb = S.substr(start, len);ret.push_back(start);start += len;--tableTmp[sb];--count;}}}return ret;}</span>


错误的思想

下面说说我上面提到的自己写的错误的方法,

第一种是产生一个permutation number,就是L中的字符串如果有n个,就生成一个n!的排列组合,把L中的所有字符串的可能排列结果生成一个字符串,然后用KMP算法在S中查找匹配,这种方法的缺点在于,如果L中有15个word(可能重复),那生成一个15!的排列组合,可能这15个word长度不长,也就几十个字符,但是15! 是一个恐怖的数字,为了记录这些排列组合序列所消耗的内存足以 memory exceeds。

第二种方法是对L中的所有word进行kmp匹配,记录所有的有效起始位置,然后排序,看看是否有一个range能够覆盖所有的word,但是这种方法kmp匹配需要时间为O(n),m个word就是O(mn),再加上set的计算时间,runtime error.


0 0
原创粉丝点击