Given an integer array `A`, and an integer `target`, return the number of tuples `i, j, k` such that `i < j < k` and `A[i] + A[j] + A[k] == target`.
As the answer can be very large, return it modulo 10^9 + 7
.
Example 1:
Input: A = [1,1,2,2,3,3,4,4,5,5], target = 8
Output: 20
Explanation:
Enumerating by the values (A[i], A[j], A[k]):
(1, 2, 5) occurs 8 times;
(1, 3, 4) occurs 8 times;
(2, 2, 4) occurs 2 times;
(2, 3, 3) occurs 2 times.
Example 2:
Input: A = [1,1,2,2,2,2], target = 5
Output: 12
Explanation:
A[i] = 1, A[j] = A[k] = 2 occurs 12 times:
We choose one 1 from [1,1] in 2 ways,
and two 2s from [2,2,2,2] in 6 ways.
Note:
3 <= A.length <= 3000
0 <= A[i] <= 100
0 <= target <= 300
這道題是之前那道 [3Sum](http://www.cnblogs.com/grandyang/p/4481576.html) 的拓展,之前那道題是說沒有重復數字,而這道題卻有大量的重復數字,所以每個組合可能會大量重復出現,讓我們統計出所有組合的總出現次數,並對一個超大數取余。這樣難度就比之前提升了不少,但是本質還是類似,都是使用雙指針來解題。因為有很多重復的數字,所以將相同的數字放在一起便於統計,可以對數組進行排序,然后遍歷數組,先確定一個數字 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
類似題目:
參考資料:
https://leetcode.com/problems/3sum-with-multiplicity/
https://leetcode.com/problems/3sum-with-multiplicity/discuss/181098/Java-O(n2)-code-Sort-and-Match.
[LeetCode All in One 題目講解匯總(持續更新中...)](https://www.cnblogs.com/grandyang/p/4606334.html)