1035.不相交的线

目标

在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。

现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足:

  • nums1[i] == nums2[j]
  • 且绘制的直线不与任何其他连线(非水平线)相交。

请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

以这种方法绘制线条,并返回可以绘制的最大连线数。

示例 1:

输入:nums1 = [1,4,2], nums2 = [1,2,4]
输出:2
解释:可以画出两条不交叉的线,如上图所示。 
但无法画出第三条不相交的直线,因为从 nums1[1]=4 到 nums2[2]=4 的直线将与从 nums1[2]=2 到 nums2[1]=2 的直线相交。

示例 2:

输入:nums1 = [2,5,1,2,5], nums2 = [10,5,2,1,5,2]
输出:3

示例 3:

输入:nums1 = [1,3,7,1,7,5], nums2 = [1,9,2,5,1]
输出:2

说明:

  • 1 <= nums1.length, nums2.length <= 500
  • 1 <= nums1[i], nums2[j] <= 2000

思路

在两条平行线上有两个数组,可以将两个数组中的相同元素连线,问最多有多少条不相交的连线。

典型的动态规划题,根据选或不选来进行状态转移,连线之后只能从后面的位置选两点连线。

// todo

代码

性能

2940.找到Alice和Bob可以相遇的建筑

目标

给你一个下标从 0 开始的正整数数组 heights ,其中 heights[i] 表示第 i 栋建筑的高度。

如果一个人在建筑 i ,且存在 i < j 的建筑 j 满足 heights[i] < heights[j] ,那么这个人可以移动到建筑 j 。

给你另外一个数组 queries ,其中 queries[i] = [ai, bi] 。第 i 个查询中,Alice 在建筑 ai ,Bob 在建筑 bi 。

请你能返回一个数组 ans ,其中 ans[i] 是第 i 个查询中,Alice 和 Bob 可以相遇的 最左边的建筑 。如果对于查询 i ,Alice 和 Bob 不能相遇,令 ans[i] 为 -1 。

示例 1:

输入:heights = [6,4,8,5,2,7], queries = [[0,1],[0,3],[2,4],[3,4],[2,2]]
输出:[2,5,-1,5,2]
解释:第一个查询中,Alice 和 Bob 可以移动到建筑 2 ,因为 heights[0] < heights[2] 且 heights[1] < heights[2] 。
第二个查询中,Alice 和 Bob 可以移动到建筑 5 ,因为 heights[0] < heights[5] 且 heights[3] < heights[5] 。
第三个查询中,Alice 无法与 Bob 相遇,因为 Alice 不能移动到任何其他建筑。
第四个查询中,Alice 和 Bob 可以移动到建筑 5 ,因为 heights[3] < heights[5] 且 heights[4] < heights[5] 。
第五个查询中,Alice 和 Bob 已经在同一栋建筑中。
对于 ans[i] != -1 ,ans[i] 是 Alice 和 Bob 可以相遇的建筑中最左边建筑的下标。
对于 ans[i] == -1 ,不存在 Alice 和 Bob 可以相遇的建筑。

示例 2:

输入:heights = [5,3,8,2,6,1,4,6], queries = [[0,7],[3,5],[5,2],[3,0],[1,6]]
输出:[7,6,-1,4,6]
解释:第一个查询中,Alice 可以直接移动到 Bob 的建筑,因为 heights[0] < heights[7] 。
第二个查询中,Alice 和 Bob 可以移动到建筑 6 ,因为 heights[3] < heights[6] 且 heights[5] < heights[6] 。
第三个查询中,Alice 无法与 Bob 相遇,因为 Bob 不能移动到任何其他建筑。
第四个查询中,Alice 和 Bob 可以移动到建筑 4 ,因为 heights[3] < heights[4] 且 heights[0] < heights[4] 。
第五个查询中,Alice 可以直接移动到 Bob 的建筑,因为 heights[1] < heights[6] 。
对于 ans[i] != -1 ,ans[i] 是 Alice 和 Bob 可以相遇的建筑中最左边建筑的下标。
对于 ans[i] == -1 ,不存在 Alice 和 Bob 可以相遇的建筑。

说明:

  • 1 <= heights.length <= 5 * 10^4
  • 1 <= heights[i] <= 10^9
  • 1 <= queries.length <= 5 * 10^4
  • queries[i] = [ai, bi]
  • 0 <= ai, bi <= heights.length - 1

思路

有一个数组 heights 表示建筑的高度,另有一个二维数组,其元素 queries[i] = [ai, bi] 表示 Alice 和 Bob 当前所在建筑的下标,规定可以从左向右移动到比当前建筑高的建筑,求 Alice 和 Bob 可以相遇的建筑下标,如果有多个取最左侧的那个。

这个问题的关键在于求出 [max(ai, bi), n) 范围内,高度大于等于 max(heights[ai], heights[bi]) 的建筑下标最小值。暴力求解会超时,考虑使用单调栈,记录下一个高度大于当前建筑的下标。类似于跳表,从较高的建筑出发,查找第一个下标大于等于max(ai, bi)的建筑即可。它存在的问题是如果(ai, bi)之间有极大值,后面还得遍历查找。最坏的情况下,数据依次递增,而满足条件的值在最后,这时退化为暴力求解。

碰巧过了。// todo 研究一下官网题解

代码

/**
 * @date 2024-08-10 19:56
 */
public class LeftmostBuildingQueries2940 {

    public int[] leftmostBuildingQueries_v1(int[] heights, int[][] queries) {
        int num = heights.length;
        int[] next = new int[num];
        Arrays.fill(next, -1);
        ArrayDeque<Integer> stack = new ArrayDeque<>();
        // 单调栈
        for (int i = 0; i < num; i++) {
            while (!stack.isEmpty() && heights[i] > heights[stack.peek()]) {
                next[stack.pop()] = i;
            }
            stack.push(i);
        }
        int n = queries.length;
        int[] res = new int[n];
        Arrays.fill(res, -1);
        for (int i = 0; i < n; i++) {
            int a = queries[i][0];
            int b = queries[i][1];
            if (a == b) {
                res[i] = a;
                continue;
            } else if (a < b && heights[a] < heights[b]) {
                res[i] = b;
                continue;
            } else if (a > b && heights[a] > heights[b]) {
                res[i] = a;
                continue;
            }
            int leftNext = Math.min(a, b);
            int rightNext = Math.max(a, b);
            if (next[leftNext] > rightNext || next[leftNext] == -1) {
                res[i] = next[leftNext];
                continue;
            }
            int height = Math.max(heights[a], heights[b]);
            while (next[rightNext] != -1) {
                if (heights[next[rightNext]] > height) {
                    res[i] = next[rightNext];
                    break;
                } else {
                    rightNext = next[rightNext];
                }
            }
        }
        return res;
    }
}

性能

3132.找出与数组相加的整数 II

目标

给你两个整数数组 nums1 和 nums2。

从 nums1 中移除两个元素,并且所有其他元素都与变量 x 所表示的整数相加。如果 x 为负数,则表现为元素值的减少。

执行上述操作后,nums1 和 nums2 相等 。当两个数组中包含相同的整数,并且这些整数出现的频次相同时,两个数组 相等 。

返回能够实现数组相等的 最小 整数 x 。

示例 1:

输入:nums1 = [4,20,16,12,8], nums2 = [14,18,10]
输出:-2
解释:移除 nums1 中下标为 [0,4] 的两个元素,并且每个元素与 -2 相加后,nums1 变为 [18,14,10] ,与 nums2 相等。

示例 2:

输入:nums1 = [3,5,5,3], nums2 = [7,7]
输出:2
解释: 移除 nums1 中下标为 [0,3] 的两个元素,并且每个元素与 2 相加后,nums1 变为 [7,7] ,与 nums2 相等。

说明:

  • 3 <= nums1.length <= 200
  • nums2.length == nums1.length - 2
  • 0 <= nums1[i], nums2[i] <= 1000
  • 测试用例以这样的方式生成:存在一个整数 x,nums1 中的每个元素都与 x 相加后,再移除两个元素,nums1 可以与 nums2 相等。

思路

这与找出与数组相加的整数 I类似,只不过需要删除nums1中的两个元素,这样x就可能存在多个值,取其中最小的。

最简单的想法是将这两个数组排序,枚举 nums1 长度为 2 的子序列组合,将其从数组中删去,判断对应位置上的 nums2[i] - nums1[i] 的值是否都等于 x。如果相等则保存,枚举完所有子序列组合后取保存的最小值即可。排序的时间复杂度为 O(nlogn) 代入 n = 200 大约 460.2,子序列可能的组合有 C(200, 2) = 19900,针对每种组合都要遍历数组,循环 200 次,大概需要执行 200 * 19900 ≈ 4 * 10^6 次,应该不会超时。

可以将 nums2nums1 中的元素两两相减然后统计差值的出现次数,如果出现次数小于 nums2.length 那么该差值一定不是 x。因为 nums1.length - 2 == nums2.length,如果 x 存在,那么其出现次数最少为nums2.length。例如,[1,2,3,4][3,4] 的差值 2 1 0 -1 3 2 1 0 2出现了2次,1出现了2次,0出现了2次,取最小的0。

得到了可能的 x 之后,我们需要判断是否合法,例如 [9,9,1,1,1][5,5,5], 差值 -4 出现了 6 次,4 出现了 9 次。但是 -4 是不合法的,因为使用 nums2 - (-4) 还原回去的个数对不上。

看官网题解 // todo

代码

/**
 * @date 2024-08-09 0:30
 */
public class MinimumAddedInteger3132 {

    public int minimumAddedInteger(int[] nums1, int[] nums2) {
        int n = nums2.length;
        Map<Integer, Integer> map = new TreeMap<>();
        Map<Integer, Integer> set = new HashMap<>(nums1.length);
        for (int i : nums1) {
            set.merge(i, 1, Integer::sum);
        }
        for (int v2 : nums2) {
            for (int v1 : nums1) {
                map.merge(v2 - v1, 1, Integer::sum);
            }
        }
        int res = Integer.MAX_VALUE;
        here:
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            Map<Integer, Integer> tmp = new HashMap<>(set);
            if (entry.getValue() >= n) {
                for (int i : nums2) {
                    if (tmp.get(i - entry.getKey()) == null || tmp.get(i - entry.getKey()) <= 0) {
                        continue here;
                    }
                    tmp.merge(i - entry.getKey(), -1, Integer::sum);
                }
                res = Math.min(res, entry.getKey());
            }
        }
        return res;
    }

}

性能

3131.找出与数组相加的整数 I

目标

给你两个长度相等的数组 nums1 和 nums2。

数组 nums1 中的每个元素都与变量 x 所表示的整数相加。如果 x 为负数,则表现为元素值的减少。

在与 x 相加后,nums1 和 nums2 相等 。当两个数组中包含相同的整数,并且这些整数出现的频次相同时,两个数组 相等 。

返回整数 x 。

示例 1:

输入:nums1 = [2,6,4], nums2 = [9,7,5]
输出:3
解释:与 3 相加后,nums1 和 nums2 相等。

示例 2:

输入:nums1 = [10], nums2 = [5]
输出:-5
解释:与 -5 相加后,nums1 和 nums2 相等。

示例 3:

输入:nums1 = [1,1,1,1], nums2 = [1,1,1,1]
输出:0
解释:与 0 相加后,nums1 和 nums2 相等。

说明:

  • 1 <= nums1.length == nums2.length <= 100
  • 0 <= nums1[i], nums2[i] <= 1000
  • 测试用例以这样的方式生成:存在一个整数 x,使得 nums1 中的每个元素都与 x 相加后,nums1 与 nums2 相等。

思路

有两个整数数组 nums1nums2nums1 中的每个元素加上 x 之后可以通过调整元素位置得到 nums2,求 x

nums1nums2 的映射为双射,即单射和满射。为了求 x 我们只需要找到 nums1nums2 的最大值或最小值即可。

还有网友对两个数组求和,相减后再除以数组长度,不过需要注意数据溢出、数据类型的问题。

代码


/**
 * @date 2024-08-08 9:11
 */
public class AddedInteger3131 {

    public int addedInteger(int[] nums1, int[] nums2) {
        int n = nums1.length;
        int max1 = nums1[0];
        int max2 = nums2[0];
        for (int i = 1; i < n; i++) {
            if (max1 < nums1[i]) {
                max1 = nums1[i];
            }
            if (max2 < nums2[i]) {
                max2 = nums2[i];
            }
        }
        return max2 - max1;
    }
}

性能

3129.找出所有稳定的二进制数组 I/II

目标

给你 3 个正整数 zero ,one 和 limit 。

一个 二进制数组 arr 如果满足以下条件,那么我们称它是 稳定的 :

  • 0 在 arr 中出现次数 恰好 为 zero 。
  • 1 在 arr 中出现次数 恰好 为 one 。
  • arr 中每个长度超过 limit 的 子数组 都 同时 包含 0 和 1 。

请你返回 稳定 二进制数组的 总 数目。

由于答案可能很大,将它对 10^9 + 7 取余 后返回。

示例 1:

输入:zero = 1, one = 1, limit = 2
输出:2
解释:
两个稳定的二进制数组为 [1,0] 和 [0,1] ,两个数组都有一个 0 和一个 1 ,且没有子数组长度大于 2 。

示例 2:

输入:zero = 1, one = 2, limit = 1
输出:1
解释:
唯一稳定的二进制数组是 [1,0,1] 。
二进制数组 [1,1,0] 和 [0,1,1] 都有长度为 2 且元素全都相同的子数组,所以它们不稳定。

示例 3:

输入:zero = 3, one = 3, limit = 2
输出:14
解释:
所有稳定的二进制数组包括 [0,0,1,0,1,1] ,[0,0,1,1,0,1] ,[0,1,0,0,1,1] ,[0,1,0,1,0,1] ,[0,1,0,1,1,0] ,[0,1,1,0,0,1] ,[0,1,1,0,1,0] ,[1,0,0,1,0,1] ,[1,0,0,1,1,0] ,[1,0,1,0,0,1] ,[1,0,1,0,1,0] ,[1,0,1,1,0,0] ,[1,1,0,0,1,0] 和 [1,1,0,1,0,0] 。

说明:

  • 1 <= zero, one, limit <= 200 medium
  • 1 <= zero, one, limit <= 1000 hard

提示:

  • Let dp[a][b][c = 0/1][d] be the number of stable arrays with exactly a 0s, b 1s and consecutive d value of c’s at the end.
  • Try each case by appending a 0/1 at last to get the inductions.

思路

zero 个 0 和 one 个 1 组成的数组,满足所有长度大于 limit 的子数组同时包含0和1的数组有多少个。

首先考虑由 zero 个 0 和 one 个 1 组成的数组有多少个。C(zero + one, zero) 从 zero + one 个位置中选出 zero 或者 one 个。根据题目中的范围 1 ~ 200,C(400, 200) 大概在 10^119 ~ 10^120之间。不可能枚举所有组合,更别提枚举每种组合的所有子数组了。我们必须自底向上寻求解决方案。

如何保证 limit + 1 的窗口内一定包含 0 和 1 呢?

看了提示说是四维动态规划,直接放弃了。思考过程中卡住的点是不会构建满足条件的解,想不出怎样才能让子数组中同时包含 0 和 1。其实只要保证不出现连续 limit + 1 个 0 或 1 就可以了。想到了动态规划,但是不知道如何进行状态转移。

代码

性能

600.不含连续1的非负整数

目标

给定一个正整数 n ,请你统计在 [0, n] 范围的非负整数中,有多少个整数的二进制表示中不存在 连续的 1 。

示例 1:

输入: n = 5
输出: 5
解释: 
下面列出范围在 [0, 5] 的非负整数与其对应的二进制表示:
0 : 0
1 : 1
2 : 10
3 : 11
4 : 100
5 : 101
其中,只有整数 3 违反规则(有两个连续的 1 ),其他 5 个满足规则。

示例 2:

输入: n = 1
输出: 2

示例 3:

输入: n = 2
输出: 3

说明:

  • 1 <= n <= 10^9

思路

给定一个正整数n,求 [0, n] 范围内整数的二进制表示中不含连续1的整数个数。

由于n最大10^9,如果挨个判断整数是否含连续的bit 1,实际复杂度为O(31n),超时。

既然暴力解不行只能考虑其它方法。分析n的二进制表示,将问题转换为一定限制条件下的排列组合问题。求得二进制表示之后可以使用dfs来计算组合数。如果不使用记忆化搜索同样会超时,这里使用状态压缩来记录重复的子问题。状态指方法参数的组合,如果cur为1,则将高位置1与index相与,第二个维度0表示不可以将0改为1,1表示可以将0改为1。

官网题解使用的是递推。

代码

/**
 * @date 2024-08-05 10:20
 */
public class FindIntegers600 {

    public int findIntegers(int n) {
        List<Integer> bitmap = new ArrayList<>(32);
        while (n > 0) {
            bitmap.add(n & 1);
            n >>= 1;
        }
        int[][] mem = new int[(1 << 5) | (bitmap.size() - 1)][2];
        return dfs(0, bitmap, bitmap.size() - 1, false, mem);
    }

    public int dfs(int pre, List<Integer> bitmap, int index, boolean zeroToOne, int[][] mem) {
        int cur = bitmap.get(index);
        if (index == 0) {
            return pre == 0 && (cur == 1 || zeroToOne) ? 2 : 1;
        }
        int res = 0;
        index--;
        int size = bitmap.size();
        int key = (1 << 5) | index;
        int zto = zeroToOne ? 1 : 0;
        if (pre == 1 && cur == 1) {
            // 如果前一个是1,当前也是1,将1改为0,允许后续的0改为1
            if (mem[index][1] == 0) {
                mem[index][1] = dfs(cur - 1, bitmap, index, true, mem);
            }
            res = mem[index][1];
        } else if (pre == 0 && cur == 1) {
            // 如果前一个是0,当前是1,将1改为0,允许后续的0改为1,或者当前不变,后续是否允许0变1取决于zeroToOne
            if (mem[index][1] == 0) {
                mem[index][1] = dfs(cur - 1, bitmap, index, true, mem);
            }
            if (mem[key][zto] == 0) {
                mem[key][zto] = dfs(cur, bitmap, index, zeroToOne, mem);
            }
            res = mem[index][1] + mem[key][zto];
        } else if (pre == 0 && cur == 0) {
            // 如果前一个是0,当前是0,当前不变,后续是否允0变1许取决于zeroToOne,如果当前zeroToOne为true,将0改为1
            if (mem[index][zto] == 0) {
                mem[index][zto] = dfs(cur, bitmap, index, zeroToOne, mem);
            }
            res = mem[index][zto];
            if (zeroToOne) {
                if (mem[key][zto] == 0) {
                    mem[key][zto] = dfs(cur + 1, bitmap, index, zeroToOne, mem);
                }
                res += mem[key][zto];
            }
        } else {
            // 如果前一个是1,当前是0,当前不变,后续是否允许0变1取决于zeroToOne
            if (mem[index][zto] == 0) {
                mem[index][zto] = dfs(cur, bitmap, index, zeroToOne, mem);
            }
            res = mem[index][zto];
        }
        return res;
    }

}

性能

572.另一棵树的子树

目标

给你两棵二叉树 root 和 subRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false 。

二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree 也可以看做它自身的一棵子树。

示例 1:

输入:root = [3,4,5,1,2], subRoot = [4,1,2]
输出:true

示例 2:

输入:root = [3,4,5,1,2,null,null,null,null,0], subRoot = [4,1,2]
输出:false

说明:

  • root 树上的节点数量范围是 [1, 2000]
  • subRoot 树上的节点数量范围是 [1, 1000]
  • -10^4 <= root.val <= 10^4
  • -10^4 <= subRoot.val <= 10^4

思路

判断给定的树是否是另一颗树的子树。

刚开始想以某种顺序(先/中/后)遍历给定的树并记录访问节点,然后遍历另一棵树,如果节点值等于给定树的根节点则以同样的顺序记录其子树,再比较记录是否相同。但是这并不能保证结构是相同的。

只能逐个节点比较。进一步优化可以计算树的高度,只比较高度相同的。//todo

看了官网题解可以引入两个null值,进行串匹配的时候可以使用 KMP 或者 Rabin-Karp 算法。//todo

也提供了将树映射为hash值来比较的思路。//todo

代码

/**
 * @date 2024-08-04 15:36
 */
public class IsSubtree572 {

    public boolean isSubtree(TreeNode root, TreeNode subRoot) {
        if (root == null) {
            return false;
        }
        if (root.val == subRoot.val) {
            if (compare(root, subRoot)) {
                return true;
            }
        }
        return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot);
    }

    public boolean compare(TreeNode root, TreeNode subRoot) {
        if (root.val != subRoot.val) {
            return false;
        }
        boolean res = false;
        if (root.left != null && subRoot.left != null) {
            res = compare(root.left, subRoot.left);
        } else if (root.left == null && subRoot.left == null){
            res = true;
        }
        if (root.right != null && subRoot.right != null) {
            res = compare(root.right, subRoot.right) && res;
        } else if (root.right == null && subRoot.right == null) {
        } else {
            res = false;
        }
        return res;
    }

}

性能

3143.正方形中的最多点数

目标

给你一个二维数组 points 和一个字符串 s ,其中 points[i] 表示第 i 个点的坐标,s[i] 表示第 i 个点的 标签 。

如果一个正方形的中心在 (0, 0) ,所有边都平行于坐标轴,且正方形内 不 存在标签相同的两个点,那么我们称这个正方形是 合法 的。

请你返回 合法 正方形中可以包含的 最多 点数。

注意:

  • 如果一个点位于正方形的边上或者在边以内,则认为该点位于正方形内。
  • 正方形的边长可以为零。

示例 1:

输入:points = [[2,2],[-1,-2],[-4,4],[-3,1],[3,-3]], s = "abdca"
输出:2
解释:边长为 4 的正方形包含两个点 points[0] 和 points[1] 。

示例 2:

输入:points = [[1,1],[-2,-2],[-2,2]], s = "abb"
输出:1
解释:边长为 2 的正方形包含 1 个点 points[0] 。

示例 3:

输入:points = [[1,1],[-1,-1],[2,-2]], s = "ccd"
输出:0
解释:任何正方形都无法只包含 points[0] 和 points[1] 中的一个点,所以合法正方形中都不包含任何点。

说明:

  • 1 <= s.length, points.length <= 10^5
  • points[i].length == 2
  • -10^9 <= points[i][0], points[i][1] <= 10^9
  • s.length == points.length
  • points 中的点坐标互不相同。
  • s 只包含小写英文字母。

思路

坐标平面上有一些点points,每个点都有一个标签字母,points[i] 对应的标签为给定字符串s相应位置上的字母 s.charAt(i)。问以坐标轴原点为中心的正方形最多能覆盖多少标签不同的坐标点。

坐标平面内的点如果想要被以原点为中心的正方形覆盖,那么正方形的边长应大于等于max(|x|,|y|),即切比雪夫距离。又要使坐标点的标签不同,可以将坐标点的 切比雪夫距离 从小到大排序,然后依次遍历并将其标签加入哈希表,直到遇到重复的为止。需要注意,应将该边长上的所有已经加入的坐标点删除,为此我们可以将距离相同的坐标放入一个集合,要么都计入,要么结束循环。

看了题解,网友还提供了两种解法,一个是使用二分法枚举边长范围,看正方形内是否有重复坐标;另一个是建立 字母 <---> 最小距离 的映射,同时计算最小的 次小距离,统计映射中的最小距离 小于 最小 次小距离 的坐标点个数即可。

代码


/**
 * @date 2024-08-03 21:44
 */
public class MaxPointsInsideSquare3143 {

    public int maxPointsInsideSquare(int[][] points, String s) {
        int n = points.length;
        Map<Integer, List<Integer>> map = new TreeMap<>();
        for (int i = 0; i < n; i++) {
            int key = Math.max(Math.abs(points[i][0]), Math.abs(points[i][1]));
            map.putIfAbsent(key, new ArrayList<>());
            map.get(key).add(i);
        }
        Set<Character> labelSet = new HashSet<>();
        int res = 0;
        here:
        for (Map.Entry<Integer, List<Integer>> entry : map.entrySet()) {
            for (Integer i : entry.getValue()) {
                if (labelSet.contains(s.charAt(i))) {
                    break here;
                }
                labelSet.add(s.charAt(i));
            }
            res += entry.getValue().size();
        }
        return res;
    }

}

性能

3128.直角三角形

目标

给你一个二维 boolean 矩阵 grid 。

请你返回使用 grid 中的 3 个元素可以构建的 直角三角形 数目,且满足 3 个元素值 都 为 1 。

注意:

  • 如果 grid 中 3 个元素满足:一个元素与另一个元素在 同一行,同时与第三个元素在 同一列 ,那么这 3 个元素称为一个 直角三角形 。这 3 个元素互相之间不需要相邻。

示例 1:

0 1 0
0 1 1
0 1 0

输入:grid = [[0,1,0],[0,1,1],[0,1,0]]
输出:2
解释:有 2 个直角三角形。

示例 2:

1 0 0 0
0 1 0 1
1 0 0 0

输入:grid = [[1,0,0,0],[0,1,0,1],[1,0,0,0]]
输出:0
解释:没有直角三角形。

示例 3:

1 0 1
1 0 0
1 0 0

输入:grid = [[1,0,1],[1,0,0],[1,0,0]]
输出:2
解释:有两个直角三角形。

说明:

  • 1 <= grid.length <= 1000
  • 1 <= grid[i].length <= 1000
  • 0 <= grid[i][j] <= 1

思路

有一个二进制矩阵,如果元素本身为1,且同行和同列存在其它元素也为1,则称这三个元素组成了直角三角形。求矩阵中直角三角形的数目。

常规的解法是从左到右从上到下遍历矩阵元素,如果当前元素为1,则将其视为可能的直角顶点,然后向左右、向上下查找,看是否有元素为1。如果纵向为1的元素个数为 m,横向为1的元素个数为 n,那么以当前元素为直角顶点的直角三角形有 m * n 个。矩阵元素个数最多 10^6 个,然后遍历上下、左右元素最多 2 * 10^3,这个解法肯定会超时。

我们希望遍历到某个节点时可以直接获取到上下、左右的可选元素个数。为此,我们可以将各行各列元素值为1的元素个数先求出来,然后判断当前节点是否为1,为1就拿相应行列的个数减1相乘。这样时间复杂度可降为为 O(mn)

从leetcode结果来看,这里面有几个可以提高性能的操作:

  1. 行、列求和写在同一个循环内可以大幅提高性能
  2. 循环内部判断元素是否为1可以省去,因为是二进制数组,可以直接拿当前元素值相乘,可以大幅提高效率
  3. 将行和数组改为局部变量可以稍微提高性能

代码

/**
 * @date 2024-08-02 14:37
 */
public class NumberOfRightTriangles3128 {

    public long numberOfRightTriangles(int[][] grid) {
        long res = 0;
        int m = grid.length;
        int n = grid[0].length;
        int[] colSum = new int[n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                colSum[j] += grid[i][j];
            }
        }
        for (int i = 0; i < m; i++) {
            int rowSum = 0;
            for (int j = 0; j < n; j++) {
                rowSum += grid[i][j];
            }
            for (int j = 0; j < n; j++) {
                res += grid[i][j] * (rowSum - 1) * (colSum[j] - 1);
            }
        }
        return res;
    }
}

性能