滑動窗口常用技巧總結

概述

在解決字串問題時,滑動窗口技巧可能經常會使用,其本身思想並不難理解,難在靈活。因而本文從一個最小覆蓋字串問題入手總結一個通用的算法框架以解決常見的滑動窗口問題。

算法與框架

下邊我們先看一個最小覆蓋子串問題:

image-20201113143843849

題目本身不難理解,主要就是從S(source)中找到包含T(target)中全部字幕的一個子串,順序無所謂,個數相同且子串中一定是所有可能子串中最短的。

最簡單的思路是通過暴力法,通過兩層搜索來解決,但時間複雜度很高,甚至大於O(n^2)。

此類問題實際上我們可以通過滑動窗口的思路來解決。具體思路如下:

  1. 在字符串S中使用雙指針中左右指針的技巧,初始化left = right = 0,把索引區間[left,right]稱之爲一個[窗口]。
  2. 不斷的增加right指針擴大窗口[left,right ],直到窗口中的字符串符合要求(窗口包含T中所有字符)。
  3. 停止增加right,轉而增加left指針,進而縮小窗口直到窗口不再符合要求。同時每增加一個left都要更新一輪結果。
  4. 重複2和3,直到right達到字符串S的盡頭。

整個過程思路並不難,其中第2步相當於在找一個可行解,第3步在優化這個可行解,每輪都進行結果更新,最後找到最優解。

下邊我們結合整下邊的圖來理解算法的整個過程。needs和windows相當於計數器,分別記錄T中字符串出現的次數和窗口中的對應字符出現的次數。

第1步:初始狀態,left和righ都爲0

image-20201113145441247

第2步:向右移動right尋找可行解

image-20201113145521166

第3步:向右移動left,優化可行解

image-20201113145601919

第4步:重複2和3直到,right到達右邊界

image-20201113145635325

上述過程可以簡單寫出如下的代碼框架:

public String slidingWindow(String s, String t) {
	//定義兩個窗口
	Map<Character, Integer> need = new HashMap<>(), window = new HashMap<>();
	// 初始化need窗口
	for (char c : t.toCharArray()) {
	  need.put(c, need.getOrDefault(c, 0) + 1);
	}

	int left = 0, right = 0;
	// 已經和need匹配的字符串個數 
	int valid = 0;
	while (right < s.length()) {
	  char c = s.charAt(right);
	  // move to right
	  right++;
	  // 進行窗口內一系列數據的更新
	  ...
	
	// 判斷左側窗口是否要收縮
	while (window needs shrink) {
	    // d 是將移出窗口的字符
	    char d = s.charAt(left);
	    // 左移窗口
	    left++;
	    // 進行窗口內數據的一系列更新
	    ...
	}
}

其中兩處...表示更新窗口數據的地方,根據不同的問題,進行填充即可。

針對最小覆蓋子串問題,開始套模板,只需要考慮如下四個問題:

  1. 移動right擴大窗口,即加入字符時需要考慮哪些數據?
  2. 什麼條件下,窗口應該暫停擴大,開始移動left縮小窗口?
  3. 當移動left縮小窗口,即移除字符時,應該更新哪些數據?
  4. 我們要的結果應該在擴大窗口時還是縮小窗口時進行更新?

如果一個字符進入窗口,應該增加 window 計數器;如果一個字符將移出窗口的時候,應該減少 window 計數器;當 valid 滿足 need 時應該收縮窗口;應該在收縮窗口的時候更新最終結果。

針對該問題我們將代碼進行填充後得到如何解法:

string minWindow(string s, string t) {
    unordered_map<char, int> need, window;
    for (char c : t) need[c]++;

    int left = 0, right = 0;
    int valid = 0;
    // 記錄最小覆蓋子串的起始索引及長度
    int start = 0, len = INT_MAX;
    while (right < s.size()) {
        // c 是將移入窗口的字符
        char c = s[right];
        // 右移窗口
        right++;
        // 進行窗口內數據的一系列更新
        if (need.count(c)) {
            window[c]++;
            if (window[c] == need[c])
                valid++;
        }

        // 判斷左側窗口是否要收縮
        //必須使用equlals來判斷,不能使用 ==
        while (valid.equals(need.size())) {
            // 在這裏更新最小覆蓋子串
            if (right - left < len) {
                start = left;
                len = right - left;
            }
            // d 是將移出窗口的字符
            char d = s[left];
            // 左移窗口
            left++;
            // 進行窗口內數據的一系列更新
            if (need.count(d)) {
                if (window[d].euqals(need[d])
                    valid--;
                window[d]--;
            }                    
        }
    }
    // 返回最小覆蓋子串
    return len == INT_MAX ?
        "" : s.substr(start, len);
}

應用

接下來我們再看一下另一箇中等難度的題目字符串的排列

image-20201115195023834

題意很好理解,就是判斷s2是否包含s1的某種排列。我們比較容易想到用暴力法。但會發現時間複雜度過高無法通過。然後考慮到是子串問題,嘗試使用滑動窗口方法。

結合模板,考慮兩個問題:

  1. 右側窗口滑動時,做哪些操作
  2. 左側窗口滑動的條件,以及所做操作

針對第一個問題,我們考慮到當右側窗口滑動獲取一個字符時要判斷當前字符是否在need中,如果存在進行windows計數

針對第二個問題,如果窗口的長度大於字符串t的長度,則需要進行窗口左移操作,進行窗口“瘦身”

該問題具體代碼實現如下:

public boolean checkInclusion(String t, String s) {
    if (t.length() > s.length()) {
      return false;
    }
    Map<Character, Integer> need = new HashMap<>();
    Map<Character, Integer> window = new HashMap<>();
    // init need
    for (char c : t.toCharArray()) {
      need.put(c, need.getOrDefault(c, 0) + 1);
    }
    // define variable
    int left = 0, right = 0;
    int valid = 0;
    while (right < s.length()) {
      char c = s.charAt(right);
      right++;
      // update right window
      if (need.containsKey(c)) {
        window.put(c, window.getOrDefault(c, 0) + 1);
        if (need.get(c).equals(window.get(c))) {
          valid++;
        }
      }
      // shrink left window
      // 每一次窗口的尺寸比need的尺寸大的時候都會進行瘦身操作,一直移動到比need的尺寸小1結束
      while (right - left >= t.length()) {
        if (valid == need.size()) {
          return true;
        }
        char d = s.charAt(left);
        left++;
        if (need.containsKey(d)) {
          if (window.get(d).equals(need.get(d))) {
            valid--;
          }
          window.put(d, window.get(d) - 1);
        }
      }
    }
    return false;
  }

總結

簡單來說滑動窗口問題其實只要記下這個框架,大部分類似問題都可迎刃而解。

參考

  1. https://labuladong.gitbook.io/algo/shu-ju-jie-gou-xi-lie/2.5-shou-ba-shou-shua-shu-zu-ti-mu/hua-dong-chuang-kou-ji-qiao-jin-jie
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章