[LeetCode] 923. 3Sum With Multiplicity 三數之和的多種情況

  • 2019 年 11 月 29 日
  • 筆記

給定一個整數數組 A,以及一個整數 target 作為目標值,返回滿足 i < j < k 且 A[i] + A[j] + A[k] == target 的元組 i, j, k 的數量。

由於結果會非常大,請返回 結果除以 10^9 + 7 的餘數。

示例 1:

輸入:A = [1,1,2,2,3,3,4,4,5,5], target = 8 輸出:20 解釋: 按值枚舉(A[i],A[j],A[k]): (1, 2, 5) 出現 8 次; (1, 3, 4) 出現 8 次; (2, 2, 4) 出現 2 次; (2, 3, 3) 出現 2 次。 示例 2:

輸入:A = [1,1,2,2,2,2], target = 5 輸出:12 解釋: A[i] = 1,A[j] = A[k] = 2 出現 12 次: 我們從 [1,1] 中選擇一個 1,有 2 種情況, 從 [2,2,2,2] 中選出兩個 2,有 6 種情況。

提示:

3 <= A.length <= 3000 0 <= A[i] <= 100 0 <= target <= 300

這道題是之前那道 3Sum 的拓展,之前那道題是說沒有重複數字,而這道題卻有大量的重複數字,所以每個組合可能會大量重複出現,讓我們統計出所有組合的總出現次數,並對一個超大數取余。這樣難度就比之前提升了不少,但是本質還是類似,都是使用雙指針來解題。因為有很多重複的數字,所以將相同的數字放在一起便於統計,可以對數組進行排序,然後遍曆數組,先確定一個數字 A[i],則只需要找另外兩個數字,使得其和為 sum = target-A[i]。然後使用兩個指針j和k分別初始化為 i+1 和 n-1,若 A[j]+A[k] 小於 sum,則將j自增1;若 A[j]+A[k] 大於 sum,則將k自減1;若 A[j]+A[k] 正好等於 sum,則此時需要統計重複數字的個數,假設跟 A[j] 相同的數字有 left 個,跟 A[k] 相同的數字有 right 個。若 A[j] 不等於 A[k],那麼直接用 left 乘以 right 就是出現次數了,但如果 A[j] 等於 A[k],則相當於在 left+right 中選兩個數字的不同選法,就是初高中的排列組合的知識了。完事之後j要加上 left,k要減去 right,最終別忘了 res 要對超大數取余,參見程式碼如下:

解法一:

class Solution {  public:      int threeSumMulti(vector<int>& A, int target) {          long res = 0, n = A.size(), M = 1e9 + 7;          sort(A.begin(), A.end());          for (int i = 0; i < n - 2; ++i) {              int sum = target - A[i];              int j = i + 1, k = n - 1;              while (j < k) {                  if (A[j] + A[k] < sum) {                      ++j;                  } else if (A[j] + A[k] > sum) {                      --k;                  } else {                      int left = 1, right = 1;                      while (j + left < k && A[j + left] == A[j]) ++left;                      while (j + left <= k - right && A[k - right] == A[k]) ++right;                      res += A[j] == A[k] ? (k - j + 1) * (k - j) / 2 : left * right;                      j += left;                      k -= right;                  }              }          }          return res % M;      }  };

我們也可以換一種思路來解題,首先使用一個 HashMap 統計出每一個數字和其出現次數之間的映射,注意這裡次數最好設定為長整型 long,因為之後做乘法的時候有可能會超過整型最大值,然後遍歷 HashMap 中所有的任意的兩個數字組合i和j,則第三個數字k可由 target-i-j 計算得到,若k不在 HashMap 中,則說明該組合不存在,直接跳過。若 i,j,k 三個數字相等,相當於在 numCnt[i] 中任選3個數字的方法,還是排列組合的知識;若i和j相等,但不等於k,則是在 numCnt[i] 中任選2個數字的方法個數再乘以 numCnt[k];若三個數字都不相同,則直接用這三個數字 numCnt[i],numCnt[j],numCnt[k] 相乘即可,最終別忘了 res 要對超大數取余,參見程式碼如下:

解法二:

class Solution {  public:      int threeSumMulti(vector<int>& A, int target) {          long res = 0, M = 1e9 + 7;          unordered_map<int, long> numCnt;          for (int num : A) ++numCnt[num];          for (auto a : numCnt) {              for (auto b : numCnt) {                  int i = a.first, j = b.first, k = target - i - j;                  if (!numCnt.count(k)) continue;                  if (i == j && j == k) {                      res += numCnt[i] * (numCnt[i] - 1) * (numCnt[i] - 2) / 6;                  } else if (i == j && j != k) {                      res += numCnt[i] * (numCnt[i] - 1) / 2 * numCnt[k];                  } else if (i < j && j < k) {                      res += numCnt[i] * numCnt[j] * numCnt[k];                  }              }          }          return res % M;      }  };

接下來看一種更加簡潔的解法,還是使用一個 HashMap 建立數字跟其出現次數之間的映射,但是這裡並不是建立原數組每個數字跟其出現次數之間的映射,而是建立數組中任意兩個數字之和的出現次數的映射,該數字之和出現了幾次,就說明有多少個不同的兩個數組合。那麼此時遍歷每個數字 A[i],將 numCnt[target-A[i]] 加入結果中,因為和為 target-A[i] 的每個雙數組合跟 A[i] 一起都可以組成符合題意的三數組合。此時由於新加入了數字 A[i],還要從0遍歷到i,將每個數字加上 A[i] 形成新的雙數組合,將其和的映射值加1,這種思路真是碉堡了,參見程式碼如下:

解法三:

class Solution {  public:      int threeSumMulti(vector<int>& A, int target) {          int res = 0, n = A.size(), M = 1e9 + 7;          unordered_map<int, int> numCnt;          for (int i = 0; i < n; ++i) {              res = (res + numCnt[target - A[i]]) % M;              for (int j = 0; j < i; ++j) {                  int sum = A[i] + A[j];                  ++numCnt[sum];              }          }          return res;      }  };

我們也可以使用動態規劃 Dynmaic Programming 來解這道題,使用一個三維的 dp 數組,其中 dp[i][j][k] 表示在範圍 [0, i] 內的子數組使用k個數字能組成和為j的組合個數,則 dp[n][target][3] 即為所求,將 dp[i][0][0] 初始化為1。接下來推導狀態轉移方程,要新加入一個數字 A[i] 的話,那麼不管這個數字能否組成新的組合,之前的所有情況這裡都是繼承的,即 dp[i][j][k] 至少應該加上 dp[i-1][j][k],然後再來看 A[i] 是否能構成新的數字,但假如 j 是小於 A[i] 的,那麼 A[i] 是無法組成和為j的三數之和的,只有當 j 大於等於 A[i] 時才有可能,那麼就還應該加上 dp[i-1][j-A[i]][k-1] 的所有情況,它們可以跟 A[i] 組成和為j的三數組合(注意程式碼中由於i是從1開始的,所以是 A[i-1]),參見程式碼如下:

解法四:

class Solution {  public:      int threeSumMulti(vector<int>& A, int target) {          int n = A.size(), M = 1e9 + 7;          vector<vector<vector<int>>> dp(n + 1, vector<vector<int>>(target + 1, vector<int>(4)));          for (int i = 0; i <= n; ++i) dp[i][0][0] = 1;          for (int i = 1; i <= n; ++i) {              for (int j = 0; j <= target; ++j) {                  for (int k = 1; k <= 3; ++k) {                      dp[i][j][k] = (dp[i][j][k] + dp[i - 1][j][k]) % M;                      if (j >= A[i - 1]) {                          dp[i][j][k] = (dp[i][j][k] + dp[i - 1][j - A[i - 1]][k - 1]) % M;                      }                  }              }          }          return dp[n][target][3];      }  };

我們還可以優化一下空間複雜度,去掉i這一維度,因為i這維度只跟 i-1 相關,可以採用逐層覆蓋的方法,但一定要注意的是,這樣的中間兩個 for 循環應該是從後往前遍歷,不然會累加出錯誤的值,參見程式碼如下:

解法五:

class Solution {  public:      int threeSumMulti(vector<int>& A, int target) {          int n = A.size(), M = 1e9 + 7;          vector<vector<int>> dp(target + 1, vector<int>(4));          dp[0][0] = 1;          for (int i = 1; i <= n; ++i) {              for (int j = target; j >= A[i - 1]; --j) {                  for (int k = 3; k >= 1; --k) {                      dp[j][k] = (dp[j][k] + dp[j - A[i - 1]][k - 1]) % M;                  }              }          }          return dp[target][3];      }  };

Github 同步地址:

https://github.com/grandyang/leetcode/issues/923

類似題目:

3Sum Smaller

3Sum Closest

3Sum

參考資料:

https://leetcode.com/problems/3sum-with-multiplicity/

https://leetcode.com/problems/3sum-with-multiplicity/discuss/181098/Java-O(n2)-code-Sort-and-Match.

https://leetcode.com/problems/3sum-with-multiplicity/discuss/181128/10-lines-Super-Super-Easy-Java-Solution

https://leetcode.com/problems/3sum-with-multiplicity/discuss/181131/C%2B%2BJavaPython-O(N-%2B-101-*-101)

https://leetcode.com/problems/3sum-with-multiplicity/discuss/181125/Knapsack-O(n-*-target)-or-Straightforward-O(n2)