代码改变世界
[登录 · 注册]
  • [LeetCode] 923. 3Sum With Multiplicity 三数之和的多种情况

  • 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:

    1. 3 <= A.length <= 3000
    2. 0 <= A[i] <= 100
    3. 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


    类似题目:

    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)


    [LeetCode All in One 题目讲解汇总(持续更新中...)](https://www.cnblogs.com/grandyang/p/4606334.html)
  • 上一篇:[LeetCode] 924. Minimize Malware Spread 最大程度上减少恶意软件的传播
    下一篇:[LeetCode] 922. Sort Array By Parity II 按奇偶排序数组之二
  • 【推广】 阿里云小站-上云优惠聚集地(新老客户同享)更有每天限时秒杀!
    【推广】 云服务器低至0.95折 1核2G ECS云服务器8.1元/月
    【推广】 阿里云老用户升级四重礼遇享6.5折限时折扣!
  • 原文:https://www.cnblogs.com/grandyang/p/11818151.html
走看看 - 开发者的网上家园