Monday, July 6, 2015

Leetcode 30: Substring with Concatenation of All Words/ Leetcode 76: minimal window substring

Leetcode 29: Substring with Concatenation of All Words
You are given a string, s, and a list of words, words, that are all of the same length. Find all starting indices of substring(s) in s that is a concatenation of each word in words exactly once and without any intervening characters.
For example, given:
s: "barfoothefoobarman"
words: ["foo", "bar"]
You should return the indices: [0,9].

本题是找一个长string里面所有由给定words组成的子串的index。这题跟 “find all anagram of a short string in a long string”的题目意思和解法都是类似的,这种题目是string 里面典型的sliding window的题目,sliding window 能将N^2的题目线性化,将时间复杂度降为O(N).
所谓的sliding window算法,是说要求处理的字符串是一个给定长度的string,比如这个题目里面要处理的字符串是words.size() * words[0].size()的字符串,那么窗口的长度就是这个值。初始时,窗口为0,由窗口的右边来匹配字符串,CASE 1: 如果匹配,那么继续移动窗口右边words[0].size()个字符,进行下一次匹配,此时窗口越来越大,当完成第一次完全匹配的时候,窗口到达了指定的size,窗口右边继续匹配,此时窗口的左边也要移动,以保证进行匹配的子串是指定的size,宏观上来说,是将整个窗口进行了移动,窗口左边移出的words要补进来,窗口右边移进的words如果匹配要移出去, 整个窗口移动,移动过程中进行这样的匹配,直到窗口右边到达了长string的最右端,表示一次窗口移动匹配完成。 在移动窗口过程中,CASE 2:如果在窗口内窗口右边只要出现一次不匹配,那么所有变量清零,窗口左边移至下一个待匹配的字符串,窗口右边移至窗口左边,再重新进行下一次匹配。sliding window进行字符匹配的时间复杂度是O(N/L),时间是线性的。
但是本题在base sliding window的基础上加了一点难度,不再只是一次sliding window的匹配,因为窗口左边移动一次要移动1个word的距离,会跳过很多种的情况,(如果是字符匹配,移动窗口是一个字符的距离,一次sliding window就可以了)所以需要对跳过的情况一一进行处理,窗口左边在0-words[0].size的位置,都需要进行sliding window。所以这题的时间复杂度是O(N/L * L)=> O(N) 。
其实这个时间复杂度并不是O(N), 因为在这其中有两次map copy的操作,其实时间复杂度是O(N*M), 这种方法在leetcode运行的时间是很长的,也就是效率很低, 下面更新了一个新的算法。
因为words可能有重复,需要用map来存储。

update:
这种固定窗口大小移动的题目,一定要考虑到窗口大小移到固定大小这个case,因为此时就需要移动left, 并且可能要修改map里面的counter 和match 的值,即 right - left >= Window.size的时候,要对窗口左侧进行处理。 此时不管有没有match完全,可能窗口里面的含有不match的word.

class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        vector<int> rst;
        //sanity check
        if (s.size() < words[0].size() || (s.size() == 0) || (words.size() == 0)) return rst;
        //sling window
        //store words in a map
        map<string, int> wmap;
        for (int i = 0; i < words.size(); i++) {
            wmap[words[i]]++;
        }
        map<string, int> bmap = wmap;
        //sliding window variable
        int slow = 0;//left
        int fast = 0; // right
        int match = 0; //match counter
        for (int i = 0; i < words[0].size(); i++) {
            match = 0;
            bmap = wmap;
            slow = i;
            for (fast = slow; fast < s.size(); fast += words[0].size()) {
                string tmp = s.substr(fast, words[0].size());
                //check if fast match
                if (bmap.find(tmp) != bmap.end()) {
                    bmap[tmp]--;
                    if (bmap[tmp] == 0) match++;
                } else { //not match, move left and all to initial
                    match = 0;
                    slow = fast + words[0].size();
                    bmap = wmap;
                    continue;
                }
                //if all match sliding window
                if (fast - slow >= words.size() * words[0].size()) {
                    //move slow, give up slow match
                    tmp = s.substr(slow, words[0].size());
                    bmap[tmp]++;
                    if (bmap[tmp] == 1) match--;
                    slow += words[0].size();
                }
                //find a whole match
                if (match == wmap.size()) {
                    rst.push_back(slow);
                }
            }
        }
        return rst;
    }
};

updated: 
将整个string 分成 N - num*len个 num*len的小段,分别对每一个小段进行处理,看这个小段是不是完全匹配,如果能够完全匹配,那么就讲start 的index 存进去, 这种思路跟strStr的思路是很像的,时间复杂度: O((N- len*num)*(num))
总体来说这个算法的时间更少,省去了一些map 复制的操作,也没有太多的substr的操作, 时间上比上面那个好, 但是其实基本的思想差不多,也都是一个固定长度的window在string s上面移动。
对下面这个方法的正确理解是: 对string上面的每一个start position, 都进行words匹配,如果以这个start为起始的words[0].size() * words.size()的string可以匹配,那么就找到了一个匹配的,如果以这个start position 匹配的不成功, start position往前移一位,进行下一次的匹配。 这个思想跟sliding window不同,更加简单一些。 感觉有点小暴力的意思。

class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        vector<int> rst;
        if (s.size() < words.size() * words[0].size()) return rst;
        int start = 0;
        int num = words.size();
        int len = words[0].size();
        unordered_map<string , int> smap;
        for (string st : words) {
            smap[st]++;
        }
        unordered_map<string, int> buf = smap;
        for (; start + len * num <= s.size(); start++) {
            smap = buf;
            int j = 0;//the number of words to match
            for (; j < num; j++) {
                string tmp = s.substr(start + j*len, len);
                if (smap.find(tmp) != smap.end()) {
                    smap[tmp]--;
                    if (smap[tmp] < 0) break;
                } else {
                    break;
                }
            }
            if (j == num) rst.push_back(start);
        }
        return rst;
    }
};



leetcode中还有一题sliding window做的题目:

Leetcode 76: Minimum Window Substring

Given a string S and a string T, find the minimum window in S which will contain all the characters in T in complexity O(n).
For example,
S = "ADOBECODEBANC"
T = "ABC"

Minimum window is "BANC".
Note:
If there is no such window in S that covers all characters in T, return the emtpy string "".
If there are multiple such windows, you are guaranteed that there will always be only one unique minimum window in S.

本题是找长字符串中能包含短字符串中字符的最小substring。 也是利用sliding window的思想,分析后发现,本题和上面那题的区别在于,一般的sliding window中的窗口大小是固定的,窗口内是待匹配字符串,窗口右边用于匹配字符,左边移动控制窗口大小,但是这题中的窗口大小显然是变化的,而且窗口内可以有不匹配的子串,但是也是右边用于匹配字符,左边移动控制窗口最小化。 

这题的算法是,移动窗口右边,使其达到第一次完全匹配,并且记录此时的substring 长度,因为一次匹配窗口中可能会存在很多不匹配字符,所以通过窗口左边移动来去除不匹配字符,达到缩小窗口的作用。 当达到一次完全匹配的时候,固定窗口右侧,此时右侧一定是一个匹配字符,然后移动左侧,如果移出的字符为一个匹配字符,修改map中的counter,直至match数目变化,以至于窗口内的字符串不够形成一次匹配为止。此时再移动窗口右边,进行下一次完全匹配。 在每次完全匹配的时候需要更新最小窗口长度。
时间复杂度: O(n)
class Solution {
public:
    string minWindow(string s, string t) {
        //sanity checki
        if (s.size() < t.size() || s.size() == 0 || t.size() == 0) return "";
        //store t in map
        map<char, int> tmap;
        for (int i = 0; i < t.size(); i++) {
            tmap[t[i]]++;
        }
        int slow = 0;
        int fast = 0;
        int match = 0;
        int begin = 0;
        int end = 0;
        int minr = s.size()+1;
        bool flag = false; // move slow
        for (;fast < s.size(); fast++) {
            char c = s[fast];
            flag = false;
            if (tmap.find(c) != tmap.end()) {
                tmap[c]--;
                if (tmap[c] == 0) match++;
            }
            while (match == tmap.size()) { //get one whole match
                if (minr > fast - slow+1) {
                    begin = slow;
                    end = fast;
                    minr = fast - slow + 1;
                }
                if ( tmap.find(s[slow]) != tmap.end() ) {
                    tmap[s[slow]]++;
                    if (tmap[s[slow]] == 1) match--;
                }
                slow++;
            }
        }
        if (minr > s.size()) return "";
        return s.substr(begin, minr);
    }
};


sliding window题目两类: 1. window 大小固定,找所有匹配; 2. window大小可变,找最小窗口。 


No comments:

Post a Comment