寶寶也能看懂的 leetcode 周賽 - 170 - 2

1310. 子數組異或查詢

Hi 你們好,我是張小豬。歡迎來到『寶寶也能看懂』系列之 leetcode 周賽題解。git

這裏是第 170 期的第 2 題,也是題目列表中的第 1310 題 -- 『子數組異或查詢』github

題目描述

有一個正整數數組 arr,現給你一個對應的查詢數組 queries,其中 queries[i] = [Li, Ri]shell

對於每一個查詢 i,請你計算從 Li 到 Ri 的 __XOR__ 值(即 arr[Li] xor arr[Li+1] xor ... xor arr[Ri])做爲本次查詢的結果。segmentfault

並返回一個包含給定查詢 queries 全部結果的數組。數組

示例 1:優化

輸入:arr = [1,3,4,8], queries = [[0,1],[1,2],[0,3],[3,3]]
輸出:[2,7,14,8]
解釋:
數組中元素的二進制表示形式是:
1 = 0001
3 = 0011
4 = 0100
8 = 1000
查詢的 XOR 值爲:
[0,1] = 1 xor 3 = 2
[1,2] = 3 xor 4 = 7
[0,3] = 1 xor 3 xor 4 xor 8 = 14
[3,3] = 8

示例 2:spa

輸入:arr = [4,8,2,10], queries = [[2,3],[1,3],[0,0],[0,3]]
輸出:[8,0,4,4]

提示:code

  • 1 <= arr.length <= 3 * 10^4
  • 1 <= arr[i] <= 10^9
  • 1 <= queries.length <= 3 * 10^4
  • queries[i].length == 2
  • 0 <= queries[i][0] <= queries[i][1] < arr.length

官方難度

MEDIUMblog

解決思路

這又是一道很是直白的題目。數據提供了一個 queries 數組,其中每個 query 其實就是在給定的 arr 數組中劃定一個範圍,而後咱們須要作的計算就是把這個範圍內的全部數字進行異或(xor)運算,最終獲得這個 query 的結果。排序

簡單粗暴,沒什麼奇怪的裝飾和描述。那麼就先上直接方案,brute force,奧利給,淦了!

直接方案

其實這裏沒有什麼須要額外分析的,就是根據題目描述淦就完事了。具體流程以下:

  1. 遍歷全部 queries
  2. 針對每一個 query 的範圍,循環執行異或計算
  3. 獲得結果
const xorQueries = (arr, queries) => {
  const ret = new Uint16Array(queries.length);
  for (let i = 0; i < queries.length; ++i) {
    let val = 0;
    for (let j = queries[i][0]; j <= queries[i][1]; ++j) {
      val ^= arr[j];
    }
    ret[i] = val;
  }
  return ret;
};

因爲是 brute force,時間天然不會理想,跑到了 800ms+。
原本想借着和小夥伴出去玩開溜,不過良心是在有點看不下去。摸摸豬鼻子,咱們換個思路再來一次。

換個思路

看着 queries 裏的一大堆範圍,小豬不禁的想到了小時候學校門口的小賣部裏那些好吃的小浣熊乾脆面,以及小賣部的那個小窗戶。等等,小窗戶...窗口...滑動窗口...妙啊,咱們能夠用滑動窗口的思路來解決這個問題。小豬真是個想象力豐富的寶寶,嚶嚶嚶 >.<

先解釋一下這裏滑動窗口的思路吧。假設當前已經基於範圍 [x1, y1] 計算出了咱們的目標值 v1,接下來咱們想計算範圍 [x2, y2] 的目標值,那麼其實徹底能夠不用從新計算全部內容,只須要把當前窗口的左邊界從 x1 移動到 x2,把右邊界從 y1 移動到 y2 便可。具體到針對 v1 值的變化便是配合邊界的移動進行值的運算,而剛好咱們須要作的異或操做是一個執行兩次就至關於撤銷的操做。因而能夠很是方便的進行 v1v2 的計算。

爲了讓咱們的滑動行爲相比於直接計算更加有優點,這時候須要各個目標窗口最好是有必定的順序,這樣就不會出現一會兒很大幅度的滑動,以及很是浪費的來回滑動。因此咱們會先對 queries 進行一個排序。可是最後的返回結果須要是符合題目給定數據的順序,因此咱們不能直接修改 queries 原地排序,只能新開一個空間進行排序。

那麼具體流程以下:

  1. 複製原始 queries 數組,並按照範圍的開始點和結束點來進行排序
  2. 初始化當前窗口位置和運算值
  3. 遍歷已排序過的數組,進行窗口的滑動,並記錄每個窗口的計算值

    • 左邊界移動到新的左邊界
    • 右邊界移動到新的右邊界
    • 移動過程當中維護運算值
  4. 從新根據原始 queries 數組的順序賦值計算值

基於以上流程,咱們能夠實現相似下面的代碼:

const xorQueries = (arr, queries) => {
  const ret = new Uint32Array(queries.length);
  const map = new Map();
  // 複製原始數組,並按照左邊界從小到大排序,若是左邊界相同,再按照右邊界從小到大排序
  const sorted = [...queries].sort((a, b) => a[0] === b[0] ? a[1] - b[1] : a[0] - b[0]);
  let val = left = right = 0;
  for (let i = 0; i < sorted.length; ++i) {
    const [start, end] = sorted[i];
    // 移動左邊界
    while (left < start) val ^= arr[left++];
    // 移動右邊界,須要區分兩種狀況,由於是基於左邊界排序的,因此新的右邊界可能比以前的右邊界小
    while (right <= end) val ^= arr[right++];
    while (right > end + 1) val ^= arr[--right];
    map.set(left + '-' + (right - 1), val);
  }
  for (let i = 0; i < queries.length; ++i) {
    ret[i] = map.get(queries[i][0] + '-' + queries[i][1]);
  }
  return ret;
};

這個代碼的時間大約能跑到 400ms+,說明咱們的優化思路確實起到了做用,不過還不夠。We need more!

再換個思路

上面的思路已經提到了一點,即咱們需求的異或操做,針對這個操做咱們能夠看看它的一些特性:

(4) === (3 ^ 4 ^ 3)
(4 ^ 5) === (3 ^ 4 ^ 5 ^ 3)
(4 ^ 5) === (2 ^ 3 ^ 4 ^ 5 ^ 2 ^ 3)

不知道這樣寫完小夥伴們有沒有發現一件事情,也就是咱們的目標範圍 [x, y] 的運算值其實能夠轉化爲 [start, x) ^ [start, y]

而後咱們再看,若是從 0 開始遍歷 arr,咱們能夠很容易的獲得從 0 開始的不斷累積各個數組值的異或運算值。換句話說就是咱們能夠很容易的計算出 [0, n] 這個範圍的值。那麼結合上面的那個轉化,對於 [x, y] 這個範圍其實能夠經過 [0, x) ^ [0, y] 來計算獲得。

到此,咱們能夠整理出這個思路的具體流程:

  1. 遍歷 arr 獲得各個從 0 開始的範圍的目標運算值
  2. 遍歷 queries,針對每一個具體的 query 範圍,根據上面的轉化方式求得運算值

是否是一會兒簡單了好多。而且這裏還有個小優化,咱們能夠直接在 arr 數組中記錄從 0 開始的累積運算值,從而不須要額外的儲存空間。

基於以上流程,咱們能夠實現相似下面的代碼:

const xorQueries = (arr, queries) => {
  const ret = new Uint32Array(queries.length);
  for (let i = 1; i < arr.length; ++i) {
    arr[i] ^= arr[i - 1];
  }
  for (let i = 0; i < queries.length; ++i) {
    ret[i] = arr[queries[i][1]];
    queries[i][0] !== 0 && (ret[i] = arr[queries[i][0] - 1] ^ ret[i]);
  }
  return ret;
};

到這裏,咱們的時間複雜度下降到了 O(n),額外的空間使用下降到了 O(1)。應該已經到比較極限啦。

總結

這也是一道內容簡單粗暴的題,兩次的思路轉換都是基於一些題目數據的特性進行的。在實際的生產環境中,其實相似的狀況還有不少,即根據具體需求的一些特性,咱們每每能找到更優秀的處理方法。

相關連接

qrcode_green.jpeg

相關文章
相關標籤/搜索