3233.统计不是特殊数字的数字数量

目标

给你两个 正整数 l 和 r。对于任何数字 x,x 的所有正因数(除了 x 本身)被称为 x 的 真因数。

如果一个数字恰好仅有两个 真因数,则称该数字为 特殊数字。例如:

  • 数字 4 是 特殊数字,因为它的真因数为 1 和 2。
  • 数字 6 不是 特殊数字,因为它的真因数为 1、2 和 3。

返回区间 [l, r] 内 不是 特殊数字 的数字数量。

示例 1:

输入: l = 5, r = 7
输出: 3
解释:
区间 [5, 7] 内不存在特殊数字。

示例 2:

输入: l = 4, r = 16
输出: 11
解释:
区间 [4, 16] 内的特殊数字为 4 和 9。

说明:

  • 1 <= l <= r <= 10^9

提示:

  • A special number must be a square of a prime number.
  • We need to find all primes in the range [sqrt(l), sqrt(r)].
  • Use sieve to find primes till sqrt(10^9).

思路

返回给定区间 [l, r] 内不是特殊数字的数字个数,所谓特殊数字指除了它本身恰好有两个正因数的数字。

一看到数据范围是 10^9 就不可能使用使用暴力解法去判断每一个数字是不是 lr 的因数,要么使用二分要么预处理。特殊数字是确定的,它除了自身以外只有两个因数,1 一定是一个,即除了 1 和它本身只有一个因数。看了提示说特殊数字是质数的平方,我们需要找到所有在 [√l, √r] 范围内的质数,可以预处理 √10^9 内的质数。二分查找 [l, r] 范围内质数的个数,然后减掉即可。

埃氏筛的基本思想是创建一个 boolean[] 标记查询范围内的数是否为质数,初始时均标记为 true。从 2 开始遍历(01 后面直接过滤掉了),直到 i < √endi * i < end。在循环内部,如果当前值是质数,则将 i * i,i * (i + 1),i * (i + 2),…… 标记为非质数。比如在 2 的循环内,所有大于 2 的偶数都被标为非质数,以此类推,像筛子一样将质数筛选出来。

// todo 最后也可以使用前缀和计算个数

代码


/**
 * @date 2024-11-22 9:07
 */
public class NonSpecialCount3233 {

    public static int[] primes;

    static {
        List<Integer> primeList = new ArrayList<>();
        int end = (int) Math.ceil(Math.sqrt(1000000001));
        boolean[] isPrime = new boolean[end + 1];
        Arrays.fill(isPrime, true);
        for (int i = 2; i * i <= end; i++) {
            if (isPrime[i]) {
                for (int j = i * i; j <= end; j += i) {
                    isPrime[j] = false;
                }
            }
        }
        // 前面没有将 isPrime[0] isPrime[1] 置为false,这里从2开始
        for (int i = 2; i <= end; i++) {
            if (isPrime[i]) {
                primeList.add(i);
            }
        }
        int cnt = primeList.size();
        primes = new int[cnt];
        for (int i = 0; i < cnt; i++) {
            primes[i] = primeList.get(i);
        }
    }

    public int nonSpecialCount_v1(int l, int r) {
        int ceilLeft = (int) Math.ceil(Math.sqrt(l));
        int right = (int) Math.sqrt(r);
        if (ceilLeft > right) {
            return r - l + 1;
        }
        int a = Arrays.binarySearch(primes, ceilLeft);
        if (a < 0) {
            // 获取插入点,说明原来该位置的值大于ceilLeft
            a = -a - 1;
        }
        int b = Arrays.binarySearch(primes, right);
        if (b < 0) {
            // 这里多减了1,因为插入点是第一个大于right的位置,减1则小于 right
            b = -b - 2;
        }
//        return r - l + 1 - (b - a + 1);
        return r - l - b + a;
    }

}

性能

3248.矩阵中的蛇

目标

大小为 n x n 的矩阵 grid 中有一条蛇。蛇可以朝 四个可能的方向 移动。矩阵中的每个单元格都使用位置进行标识: grid[i][j] = (i * n) + j

蛇从单元格 0 开始,并遵循一系列命令移动。

给你一个整数 n 表示 grid 的大小,另给你一个字符串数组 commands,其中包括 "UP"、"RIGHT"、"DOWN" 和 "LEFT"。题目测评数据保证蛇在整个移动过程中将始终位于 grid 边界内。

返回执行 commands 后蛇所停留的最终单元格的位置。

示例 1:

输入:n = 2, commands = ["RIGHT","DOWN"]
输出:3

示例 2:

输入:n = 3, commands = ["DOWN","RIGHT","UP"]
输出:1

说明:

  • 2 <= n <= 10
  • 1 <= commands.length <= 100
  • commands 仅由 "UP"、"RIGHT"、"DOWN" 和 "LEFT" 组成。
  • 生成的测评数据确保蛇不会移动到矩阵的边界外。

思路

有一个 n x n 矩阵 grid,初始时位置 (0, 0) 有条蛇,有一系列命令可以操作蛇移到,操作保证在矩阵内移动,问蛇最后停留的位置,格子的标识为 grid[i][j] = (i * n) + j

直接模拟操作就可以了,将操作映射为行列的增减,直接计算位置即可。

最快的解法仅比较操作的第一个字符,并且上下移动直接减加 n,最后不用乘法计算。

代码


/**
 * @date 2024-11-21 0:39
 */
public class FinalPositionOfSnake3248 {

    /**
     * 最快题解
     */
    class Solution {
        public int finalPositionOfSnake(int n, List<String> commands) {
            int ans = 0;
            for (String c : commands) {
                if (c.charAt(0) == 'U') {
                    ans -= n;
                } else if (c.charAt(0) == 'D') {
                    ans += n;
                } else if (c.charAt(0) == 'L') {
                    --ans;
                } else {
                    ++ans;
                }
            }
            return ans;
        }
    }

    public static Map<String, int[]> map = new HashMap<>(4);

    static {
        map.put("UP", new int[]{-1, 0});
        map.put("RIGHT", new int[]{0, 1});
        map.put("DOWN", new int[]{1, 0});
        map.put("LEFT", new int[]{0, -1});
    }

    public int finalPositionOfSnake(int n, List<String> commands) {
        int[] move = new int[2];
        for (String command : commands) {
            move[0] += map.get(command)[0];
            move[1] += map.get(command)[1];
        }
        return move[0] * n + move[1];
    }

}

性能

3244.新增道路查询后的最短距离II

目标

给你一个整数 n 和一个二维整数数组 queries。

有 n 个城市,编号从 0 到 n - 1。初始时,每个城市 i 都有一条单向道路通往城市 i + 1( 0 <= i < n - 1)。

queries[i] = [ui, vi] 表示新建一条从城市 ui 到城市 vi 的单向道路。每次查询后,你需要找到从城市 0 到城市 n - 1 的最短路径的长度。

所有查询中不会存在两个查询都满足 queries[i][0] < queries[j][0] < queries[i][1] < queries[j][1]

返回一个数组 answer,对于范围 [0, queries.length - 1] 中的每个 i,answer[i] 是处理完前 i + 1 个查询后,从城市 0 到城市 n - 1 的最短路径的长度。

示例 1:

输入: n = 5, queries = [[2, 4], [0, 2], [0, 4]]
输出: [3, 2, 1]
解释:
新增一条从 2 到 4 的道路后,从 0 到 4 的最短路径长度为 3。
新增一条从 0 到 2 的道路后,从 0 到 4 的最短路径长度为 2。
新增一条从 0 到 4 的道路后,从 0 到 4 的最短路径长度为 1。

示例 2:

输入: n = 4, queries = [[0, 3], [0, 2]]
输出: [1, 1]
解释:
新增一条从 0 到 3 的道路后,从 0 到 3 的最短路径长度为 1。
新增一条从 0 到 2 的道路后,从 0 到 3 的最短路径长度仍为 1。

说明:

  • 3 <= n <= 10^5
  • 1 <= queries.length <= 10^5
  • queries[i].length == 2
  • 0 <= queries[i][0] < queries[i][1] < n
  • 1 < queries[i][1] - queries[i][0]
  • 查询中不存在重复的道路。
  • 不存在两个查询都满足 i != j 且 queries[i][0] < queries[j][0] < queries[i][1] < queries[j][1]

思路

n 个城市,刚开始每一个城市 i 有一条单项道路通向城市 i + 1,有一个二维数组 queriesqueries[i] 表示增加一条从 queries[i][0]queries[i][1] 的单项道路,返回 answer 数组,answer[i] 表示增加了 queries[i] 之后从城市 0 到城市 n - 1 的最短路径。增加的路径保证不会出现相交的情况。

比昨天的题 3243.新增道路查询后的最短距离I 多了一个条件,新增的路径不会交叉。昨天首先考虑的就是今天这个思路,因为起点与终点是固定的,可以通过查询区间的关系来减小最短路径。当时考虑的差分数组解决不了区间包含的关系。

定义区间数组 intervalinterval[i] = j, 表示 i -> j 有直达道路。如果发现查询的路径 [l, r] 已经被包含,直接略过。否则,循环记录区间 (l, r) 内的路径数,保存下一跳的城市 next = interval[l],将 interval[l] 置为 r,l = next 直到 l >= r。注意最后一跳到 r 是没有计数的,相当于减去了将前面多余的步数,与直达的效果一样。 interval[l] = r 是第一次进入循环必须进行的操作,幸运的是它并不影响我们标记其它区间内的元素,当有更大的查询路径时,直接在 l 处就跳过了。

核心点是如何表示区间,如何判断区间是否重合,如何针对大区间减少的路径计数。

代码


/**
 * @date 2024-11-20 10:10
 */
public class ShortestDistanceAfterQueries3244 {

    public int[] shortestDistanceAfterQueries(int n, int[][] queries) {
        int ql = queries.length;
        int[] res = new int[ql];
        int[] interval = new int[n - 1];
        Arrays.setAll(interval, i -> i + 1);
        int shortestPath = n - 1;
        for (int i = 0; i < ql; i++) {
            int l = queries[i][0];
            int r = queries[i][1];
            while (interval[l] < r) {
                int next = interval[l];
                interval[l] = r;
                l = next;
                shortestPath--;
            }
            res[i] = shortestPath;
        }
        return res;
    }

}

性能

3243.新增道路查询后的最短距离I

目标

给你一个整数 n 和一个二维整数数组 queries。

有 n 个城市,编号从 0 到 n - 1。初始时,每个城市 i 都有一条单向道路通往城市 i + 1( 0 <= i < n - 1)。

queries[i] = [ui, vi] 表示新建一条从城市 ui 到城市 vi 的单向道路。每次查询后,你需要找到从城市 0 到城市 n - 1 的最短路径的长度。

返回一个数组 answer,对于范围 [0, queries.length - 1] 中的每个 i,answer[i] 是处理完前 i + 1 个查询后,从城市 0 到城市 n - 1 的最短路径的长度。

示例 1:

输入: n = 5, queries = [[2, 4], [0, 2], [0, 4]]
输出: [3, 2, 1]
解释:
新增一条从 2 到 4 的道路后,从 0 到 4 的最短路径长度为 3。
新增一条从 0 到 2 的道路后,从 0 到 4 的最短路径长度为 2。
新增一条从 0 到 4 的道路后,从 0 到 4 的最短路径长度为 1。

示例 2:

输入: n = 4, queries = [[0, 3], [0, 2]]
输出: [1, 1]
解释:
新增一条从 0 到 3 的道路后,从 0 到 3 的最短路径长度为 1。
新增一条从 0 到 2 的道路后,从 0 到 3 的最短路径长度仍为 1。

说明:

  • 3 <= n <= 500
  • 1 <= queries.length <= 500
  • queries[i].length == 2
  • 0 <= queries[i][0] < queries[i][1] < n
  • 1 < queries[i][1] - queries[i][0]
  • 查询中没有重复的道路。

思路

n 个城市,刚开始每一个城市 i 有一条单项道路通向城市 i + 1,有一个二维数组 queriesqueries[i] 表示增加一条从 queries[i][0]queries[i][1] 的单项道路,返回 answer 数组,answer[i] 表示增加了 queries[i] 之后从城市 0 到城市 n - 1 的最短路径。

建图,每次增加路径之后使用BFS计算最短路径。注意过滤重复元素,否则过不了。

也可以使用动态规划来做。

定义 dp[i] 表示从城市 i 到城市 n - 1 的最短路径,每添加一条路径,受影响的只有包括起点在内前面的城市。当增加一条道路 [l, r],状态转移方程为 dp[l] = Math.min(dp[r] + 1, dp[l]),表示从 r 到达终点加一步 与 原来值的最小值。我们需要同步更新 l 之前的 dp 值。我们可以倒序遍历 [0, l)dp[j] = Math.min(dp[j], dp[j + 1] + 1) 取其自身与 后面元素 dp 值加一的较小值,同时还要考虑区间内有前面查询新增的道路,比如前面有以 j 为起点的查询,还要再取一个较小值 Math.min(dp[j], dp[end] + 1)end 表示之前增加的道路的终点。

代码


/**
 * @date 2024-11-19 0:47
 */
public class ShortestDistanceAfterQueries3243 {

    public int[] shortestDistanceAfterQueries_v2(int n, int[][] queries) {
        int[] dp = new int[n];
        int ql = queries.length;
        int[] res = new int[ql];
        for (int i = 0; i < n; i++) {
            dp[i] = n - i - 1;
        }
        Map<Integer, List<Integer>> map = new HashMap<>(ql);
        for (int i = 0; i < ql; i++) {
            int l = queries[i][0];
            int r = queries[i][1];
            dp[l] = Math.min(dp[r] + 1, dp[l]);
            for (int j = l - 1; j >= 0; j--) {
                dp[j] = Math.min(dp[j], dp[j + 1] + 1);
                if (map.containsKey(j)) {
                    for (Integer end : map.get(j)) {
                        dp[j] = Math.min(dp[j], dp[end] + 1);
                    }
                }
            }
            res[i] = dp[0];
            map.putIfAbsent(l, new ArrayList<>());
            map.get(l).add(r);
        }
        return res;
    }

    public int[] shortestDistanceAfterQueries_v1(int n, int[][] queries) {
        List<Integer>[] g = new ArrayList[n];
        for (int i = 0; i < g.length; i++) {
            g[i] = new ArrayList<>();
            g[i].add(i + 1);
        }
        int ql = queries.length;
        int[] res = new int[ql];
        for (int i = 0; i < ql; i++) {
            g[queries[i][0]].add(queries[i][1]);
            res[i] = bfs_v1(g);
        }
        return res;
    }

    public int bfs_v1(List<Integer>[] g) {
        int n = g.length;
        List<Integer> list = new ArrayList<>();
        boolean[] visited = new boolean[n];
        list.add(0);
        for (int res = 1; ; res++) {
            List<Integer> tmp = list;
            int size = tmp.size();
            list = new ArrayList<>();
            for (int i = 0; i < size; i++) {
                Integer cur = tmp.get(i);
                for (Integer next : g[cur]) {
                    if (next == n - 1) {
                        return res;
                    }
                    if (!visited[next]) {
                        visited[next] = true;
                        list.add(next);
                    }
                }
            }
        }
    }

    public int[] shortestDistanceAfterQueries(int n, int[][] queries) {
        List<Integer>[] g = new ArrayList[n];
        for (int i = 0; i < g.length; i++) {
            g[i] = new ArrayList<>();
            g[i].add(i + 1);
        }
        int ql = queries.length;
        int[] res = new int[ql];
        for (int i = 0; i < ql; i++) {
            g[queries[i][0]].add(queries[i][1]);
            res[i] = bfs(g);
        }
        return res;
    }

    public int bfs(List<Integer>[] g) {
        int res = 0, n = g.length;
        Queue<Integer> q = new ArrayDeque<>();
        q.offer(0);
        here:
        while (!q.isEmpty()) {
            int size = q.size();
            Set<Integer> set = new HashSet<>();
            for (int i = 0; i < size; i++) {
                int cur = q.poll();
                if (cur == n - 1) {
                    break here;
                }
                set.addAll(g[cur]);
            }
            q.addAll(set);
            res++;
        }
        return res;
    }

}

性能

661.图片平滑器

目标

图像平滑器 是大小为 3 x 3 的过滤器,用于对图像的每个单元格平滑处理,平滑处理后单元格的值为该单元格的平均灰度。

每个单元格的 平均灰度 定义为:该单元格自身及其周围的 8 个单元格的平均值,结果需向下取整。(即,需要计算蓝色平滑器中 9 个单元格的平均值)。

如果一个单元格周围存在单元格缺失的情况,则计算平均灰度时不考虑缺失的单元格(即,需要计算红色平滑器中 4 个单元格的平均值)。

给你一个表示图像灰度的 m x n 整数矩阵 img ,返回对图像的每个单元格平滑处理后的图像 。

示例 1:

输入:img = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[0, 0, 0],[0, 0, 0], [0, 0, 0]]
解释:
对于点 (0,0), (0,2), (2,0), (2,2): 平均(3/4) = 平均(0.75) = 0
对于点 (0,1), (1,0), (1,2), (2,1): 平均(5/6) = 平均(0.83333333) = 0
对于点 (1,1): 平均(8/9) = 平均(0.88888889) = 0

示例 2:

输入: img = [[100,200,100],[200,50,200],[100,200,100]]
输出: [[137,141,137],[141,138,141],[137,141,137]]
解释:
对于点 (0,0), (0,2), (2,0), (2,2): floor((100+200+200+50)/4) = floor(137.5) = 137
对于点 (0,1), (1,0), (1,2), (2,1): floor((200+200+50+200+100+100)/6) = floor(141.666667) = 141
对于点 (1,1): floor((50+200+200+200+200+100+100+100+100)/9) = floor(138.888889) = 138

说明:

  • m == img.length
  • n == img[i].length
  • 1 <= m, n <= 200
  • 0 <= img[i][j] <= 255

思路

将图像中任意像素点的灰度值变为其自身以及周围八个像素灰度值的平均值。

可以使用二维前缀和,prefix[i][j] 表示左上顶点为 (0, 0) 右下顶点为 (i, j) 的矩形内的所有元素和。左上顶点为 (p, q) 右下顶点为 (i, j) 的矩形内所有元素和为 prefix[i][j] - prefix[i][q] - prefix[p][j] + prefix[p][q]。注意这里的坐标是顶点的坐标,而题目中的坐标表示格子的坐标,这样定义的前缀和表示以该格子为右下顶点的所有元素和,包括格子所在行列。

使用前缀和时多初始化一行一列可以大大简化代码逻辑,否则我们需要单独初始化第一行,第一列,并且需要在计算二维前缀和时判断下标越界。定义 prefix[i][j] 表示对角线 (0, 0) ~ (i - 1, j - 1) 矩形内的元素和,对于 m x n 矩阵,prefix[m][n] 表示所有元素和。以 (p, q) 为左上顶点,(i, j) 为右下顶点的前缀和为 prefix[i + 1][j + 1] - prefix[i + 1][q] - prefix[p][j + 1] + prefix[p][q]

代码


/**
 * @date 2024-11-18 9:10
 */
public class ImageSmoother661 {

    public int[][] imageSmoother_v1(int[][] img) {
        int m = img.length;
        int n = img[0].length;
        int[][] prefix = new int[m + 1][n + 1];
        for (int r = 1; r <= m; r++) {
            for (int c = 1; c <= n; c++) {
                prefix[r][c] = prefix[r - 1][c] + prefix[r][c - 1] - prefix[r - 1][c - 1] + img[r - 1][c - 1];
            }
        }
        for (int r = 0; r < m; r++) {
            int i = Math.min(m - 1, r + 1);
            int p = Math.max(0, r - 1);
            for (int c = 0; c < n; c++) {
                int j = Math.min(n - 1, c + 1);
                int q = Math.max(0, c - 1);
                int cnt = (i - p + 1) * (j - q + 1);
                img[r][c] = (prefix[i + 1][j + 1] - prefix[p][j + 1] - prefix[i + 1][q] + prefix[p][q]) / cnt;
            }
        }
        return img;
    }

}

性能

825.适龄的朋友

目标

在社交媒体网站上有 n 个用户。给你一个整数数组 ages ,其中 ages[i] 是第 i 个用户的年龄。

如果下述任意一个条件为真,那么用户 x 将不会向用户 y(x != y)发送好友请求:

  • ages[y] <= 0.5 * ages[x] + 7
  • ages[y] > ages[x]
  • ages[y] > 100 && ages[x] < 100

否则,x 将会向 y 发送一条好友请求。

注意,如果 x 向 y 发送一条好友请求,y 不必也向 x 发送一条好友请求。另外,用户不会向自己发送好友请求。

返回在该社交媒体网站上产生的好友请求总数。

示例 1:

输入:ages = [16,16]
输出:2
解释:2 人互发好友请求。

示例 2:

输入:ages = [16,17,18]
输出:2
解释:产生的好友请求为 17 -> 16 ,18 -> 17 。

示例 3:

输入:ages = [20,30,100,110,120]
输出:3
解释:产生的好友请求为 110 -> 100 ,120 -> 110 ,120 -> 100 。

说明:

  • n == ages.length
  • 1 <= n <= 2 * 10^4
  • 1 <= ages[i] <= 120

思路

某社交网站上有 n 个用户,ages[i] 表示用户 i 的年龄,当满足条件 ages[x] >= ages[y] && ages[y] > ages[x] / 2 + 7 时,用户 x 会向 y 发送好友申请。求网站上好友申请的总数。

第三个条件 ages[y] > 100 && ages[x] < 100ages[y] > 100 > ages[x] 与第二个条件重复了,可以不考虑。

ages[x] >= ages[y] > ages[x] / 2 + 7 可以推出 ages[x] > 14 才可以发出好友申请,并且好友申请的对象也要满足 ages[y] > ages[x] / 2 + 7 > 14

将年龄从大到小排序后,使用滑动窗口计算即可。

瓶颈在于排序,可以使用计数排序。

代码


/**
 * @date 2024-11-17 14:46
 */
public class NumFriendRequests825 {

    public int numFriendRequests_v1(int[] ages) {
        int n = ages.length;
        int[] ageCnt = new int[121];
        for (int i = 0; i < n; i++) {
            ageCnt[ages[i]]++;
        }
        int res = 0, windowCnt = 0;
        int l = 15;
        for (int i = 15; i < 121; i++) {
            windowCnt += ageCnt[i];
            if (l * 2 - 14 <= i) {
                windowCnt -= ageCnt[l++];
            }
            if (windowCnt > 0) {
                res += ageCnt[i] * (windowCnt - 1);
            }
        }
        return res;
    }

    public int numFriendRequests(int[] ages) {
        Arrays.sort(ages);
        int n = ages.length;
        int r = n - 1, lowerBounds = ages[r] / 2 + 7;
        int res = 0;
        for (int l = n - 1; l >= 0 && ages[l] > 14; l--) {
            while (ages[l] <= lowerBounds) {
                lowerBounds = ages[--r] / 2 + 7;
            }
            res += r - l;
            int e = l;
            while (e < n - 1 && ages[e] == ages[e + 1]) {
                res++;
                e++;
            }
        }
        return res;
    }
}

性能

3240.最少翻转次数使二进制矩阵回文II

目标

给你一个 m x n 的二进制矩阵 grid 。

如果矩阵中一行或者一列从前往后与从后往前读是一样的,那么我们称这一行或者这一列是 回文 的。

你可以将 grid 中任意格子的值 翻转 ,也就是将格子里的值从 0 变成 1 ,或者从 1 变成 0 。

请你返回 最少 翻转次数,使得矩阵中 所有 行和列都是 回文的 ,且矩阵中 1 的数目可以被 4 整除 。

示例 1:

输入:grid = [[1,0,0],[0,1,0],[0,0,1]]
输出:3

示例 2:

输入:grid = [[0,1],[0,1],[0,0]]
输出:2

示例 3:

输入:grid = [[1],[1]]
输出:2

说明:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m * n <= 2 * 10^5
  • 0 <= grid[i][j] <= 1

思路

有一个二进制矩阵 grid,每次操作可以翻转任意格子,使 0 变为 1,或者使 1 变为 0。求使得矩阵 所有行 所有列 变成回文,且矩阵中 1 的数目可以被 4 整除 的最少操作次数。

考虑矩阵操作后的最终状态,类似一个 或者 或者 旋转 90 度的 字,每一个 内的元素完全相同,中间线上如果有元素的话也是对称的。

昨天的题 最少翻转次数使二进制矩阵回文I 要求的是 所有行 或者 所有列,当我们保证行是回文的时候,如果遇到不同的元素,翻转哪一个都可以。对于本题,需要同时保证列也是回文的,并且矩阵中 1 的个数能够被 4 整除。在要求操作次数最小的情况下,选择翻转哪一个是有影响的。需要考虑翻转元素后的代价,如果我们在保证 是回文的时候翻转了某个元素导致了该元素所在 的对称位置上的元素不同,那么翻转次数需要多加1,而如果翻转镜像位置的元素刚好可以使所在列对称位置上的元素相同,应该优先选择。实际上矩阵中 1 的数目可以被 4 整除,只需考虑奇数行与奇数列时的中间行与中间列,因为在其它区域出现 1,如果行列是回文的,一定可以被 4 整除。

我们再重新梳理一下,翻转的时候优先选择代价最小的,只需要考虑 四个对称位置上的值哪个占少数就翻转哪个。分别统计中间行与列中 1 的个数,以及变成回文的操作次数。由于中间行、列的翻转是任意的,我们可以计算中间行列中 1 的个数之和 midOneCnt,注意这里需要去除中心元素(即行或列为奇数时的中间元素)后面单独处理。根据 mod = midOneCnt % 4 的值,我们可以确定操作翻转哪个元素:

  • 如果值为 1,将 10
  • 如果值为 2,都行
  • 如果值为 3,将 01

需要的操作次数为 Math.min(mod, 4 - mod)。如果保证回文的操作的次数不够使 1 的个数满足条件,就需要额外的操作。注意,这么做的前提条件是 有足够的格子,如果中线的格子总数不够 4 个,比如 3 个格子,里面都是 1 需要补一个 1 是无法操作的,这时操作次数只能取 mod 了。

最后考虑中心元素,它必须为0,否则 1 的总个数不可能被 4 整除。

代码


/**
 * @date 2024-11-15 11:28
 */
public class MinFlips3240 {

    /**
     * 执行通过
     */
    public int minFlips(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int res = 0;
        int rowMid = m / 2;
        int colMid = n / 2;
        for (int i = 0; i < rowMid; i++) {
            for (int j = 0; j < colMid; j++) {
                int jj = n - j - 1;
                int ii = m - i - 1;
                int sum = grid[i][j] + grid[i][jj] + grid[ii][j] + grid[ii][jj];
                // 取四个元素中的少数元素个数
                res += Math.min(sum, 4 - sum);
            }
        }
        int midRowOneCnt = 0;
        int midRowOptCnt = 0;
        // 计算奇数行时中间行的操作次数与1的个数
        if (m % 2 == 1) {
            for (int j = 0; j < colMid; j++) {
                int jj = n - j - 1;
                midRowOneCnt += grid[rowMid][j] + grid[rowMid][jj];
                midRowOptCnt += grid[rowMid][j] ^ grid[rowMid][jj];
            }
        }
        int midColOneCnt = 0;
        int midColOptCnt = 0;
        // 计算奇数列时中间列的操作次数与1的个数
        if (n % 2 == 1) {
            for (int i = 0; i < rowMid; i++) {
                int ii = m - i - 1;
                midColOneCnt += grid[i][colMid] + grid[ii][colMid];
                midColOptCnt += grid[i][colMid] ^ grid[ii][colMid];
            }
        }
        int midOpt = midRowOptCnt + midColOptCnt;
        res += midOpt;
        int midOneCnt = midRowOneCnt + midColOneCnt;
        int mod = midOneCnt % 4;
        int remainder;
        // 确保格子数量足够
        if (m + n <= 4) {
            remainder = midOpt - mod;
        } else {
            remainder = midOpt - Math.min(mod, 4 - mod);
        }
        // 如果操作次数不够需要补上
        if (remainder < 0) {
            res += -remainder;
        }
        // 处理中心元素
        if (m % 2 == 1 && n % 2 == 1) {
            res += grid[rowMid][colMid];
        }

        return res;
    }

}

性能

3239.最少翻转次数使二进制矩阵回文I

目标

给你一个 m x n 的二进制矩阵 grid 。

如果矩阵中一行或者一列从前往后与从后往前读是一样的,那么我们称这一行或者这一列是 回文 的。

你可以将 grid 中任意格子的值 翻转 ,也就是将格子里的值从 0 变成 1 ,或者从 1 变成 0 。

请你返回 最少 翻转次数,使得矩阵 要么 所有行是 回文的 ,要么所有列是 回文的 。

示例 1:

输入:grid = [[1,0,0],[0,0,0],[0,0,1]]
输出:2
解释:
将高亮的格子翻转,得到所有行都是回文的。

示例 2:

输入:grid = [[0,1],[0,1],[0,0]]
输出:1
解释:
将高亮的格子翻转,得到所有列都是回文的。

示例 3:

输入:grid = [[1],[0]]
输出:0
解释:
所有行已经是回文的。

说明:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m * n <= 2 * 10^5
  • 0 <= grid[i][j] <= 1

思路

有一个二进制矩阵 grid,每次操作可以翻转任意格子,使 0 变为 1,或者使 1 变为 0。求使得矩阵 所有行 或者 所有列 变成回文的最少操作次数。

由于是所有行 所有列,每种情况下的翻转次数是确定的,直接返回其最小值即可。

优化点:

  • 使用 ⌊n/2⌋ 缩小循环范围,使用 n - 1 - j 代替尾部指针
  • 是使用异或运算代替条件判断

代码


/**
 * @date 2024-11-15 9:05
 */
public class MinFlips3239 {

    public int minFlips(int[][] grid) {
        int rowOpts = 0;
        int colOpts = 0;
        int m = grid.length;
        int n = grid[0].length;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n / 2; j++) {
                rowOpts += grid[i][j] ^ grid[i][n - 1 - j];
            }
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m / 2; j++) {
                colOpts += grid[j][i] ^ grid[m - j - 1][i];
            }
        }
        return Math.min(rowOpts, colOpts);
    }

}

性能

3249.统计好节点的数目

目标

现有一棵 无向 树,树中包含 n 个节点,按从 0 到 n - 1 标记。树的根节点是节点 0 。给你一个长度为 n - 1 的二维整数数组 edges,其中 edges[i] = [ai, bi] 表示树中节点 ai 与节点 bi 之间存在一条边。

如果一个节点的所有子节点为根的 子树 包含的节点数相同,则认为该节点是一个 好节点。

返回给定树中 好节点 的数量。

子树 指的是一个节点以及它所有后代节点构成的一棵树。

示例 1:

输入:edges = [[0,1],[0,2],[1,3],[1,4],[2,5],[2,6]]
输出:7
说明:
树的所有节点都是好节点。

示例 2:

输入:edges = [[0,1],[1,2],[2,3],[3,4],[0,5],[1,6],[2,7],[3,8]]
输出:6
说明:
树中有 6 个好节点。上图中已将这些节点着色。

示例 3:

输入:edges = [[0,1],[1,2],[1,3],[1,4],[0,5],[5,6],[6,7],[7,8],[0,9],[9,10],[9,12],[10,11]]
输出:12
解释:
除了节点 9 以外其他所有节点都是好节点。

说明:

  • 2 <= n <= 10^5
  • edges.length == n - 1
  • edges[i].length == 2
  • 0 <= ai, bi < n
  • 输入确保 edges 总表示一棵有效的树。

思路

树中的任一节点,如果以它的子节点为根的子树包含相同的节点数量,则称该节点为好节点。注意没有要求子节点是好节点,只统计子树整体的节点个数。求给定树的好节点个数。

dfs 获取子树节点数目,判断各子树的节点个数是否相同。叶子节点没有子树,可认为子树节点个数为 0 也是好节点。

代码


/**
 * @date 2024-11-14 9:32
 */
public class CountGoodNodes3249 {
    int res = 0;
    List<Integer>[] g;

    public int countGoodNodes(int[][] edges) {
        g = new List[edges.length + 1];
        int n = g.length;
        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<>();
        }
        for (int i = 0; i < edges.length; i++) {
            g[edges[i][0]].add(edges[i][1]);
            g[edges[i][1]].add(edges[i][0]);
        }
        dfs(-1, 0);
        return res;
    }

    public int dfs(int parent, int cur) {
        int num = 1;
        int prev = -1;
        boolean equal = true;
        for (Integer next : g[cur]) {
            if (next == parent) {
                continue;
            }
            int childNum = dfs(cur, next);
            if (prev != -1 && prev != childNum) {
                equal = false;
            }
            prev = childNum;
            num += childNum;
        }
        if (equal) {
            res++;
        }
        return num;
    }

}

性能

3261.统计满足K约束的子字符串数量II

目标

给你一个 二进制 字符串 s 和一个整数 k。

另给你一个二维整数数组 queries ,其中 queries[i] = [li, ri] 。

如果一个 二进制字符串 满足以下任一条件,则认为该字符串满足 k 约束:

  • 字符串中 0 的数量最多为 k。
  • 字符串中 1 的数量最多为 k。

返回一个整数数组 answer ,其中 answer[i] 表示 s[li..ri] 中满足 k 约束 的 子字符串 的数量。

示例 1:

输入:s = "0001111", k = 2, queries = [[0,6]]
输出:[26]
解释:
对于查询 [0, 6], s[0..6] = "0001111" 的所有子字符串中,除 s[0..5] = "000111" 和 s[0..6] = "0001111" 外,其余子字符串都满足 k 约束。

示例 2:

输入:s = "010101", k = 1, queries = [[0,5],[1,4],[2,3]]
输出:[15,9,3]
解释:
s 的所有子字符串中,长度大于 3 的子字符串都不满足 k 约束。

说明:

  • 1 <= s.length <= 10^5
  • s[i] 是 '0' 或 '1'
  • 1 <= k <= s.length
  • 1 <= queries.length <= 10^5
  • queries[i] == [li, ri]
  • 0 <= li <= ri < s.length
  • 所有查询互不相同

思路

定义二进制字符串满足 k 约束的条件是字符串中 0 的个数 或者 1 的个数都不超过 k。求给定字符串满足 k 约束的子字符串数量。子字符串 是字符串中 连续非空 字符序列。

这道题与昨天的 3258.统计满足K约束的子字符串数量I 相比多了一个查询数组,并且字符串的长度也来到了 10^5,返回结果是 long[],暴力枚举会超时。

滑动窗口的时间复杂度为 O(n),不可能对每一次查询都重新滑动一遍。显然需要在滑动的过程中记录下查询的结果。每次滑动我们可以得到一个区间 [l, r],这个区间的所有子数组是合法的。

使用一维数组记录这个区间,使用下标与值的映射,我们有两种选择:

  • 记录的左端点的最大右端点,即 left[l] = r
  • 记录的右端点的最小左端点,即 right[r] = l

对于查询区间 [ql, qr],它与我们已知的合法区间存在两种关系,被包含或者部分相交。

  • 如果是被包含的关系,那么查询区间的所有子数组均合法,子数组个数为 (qr - ql + 1) * (qr - ql + 2) / 2
  • 如果是相交的关系,说明 ql < r[ql, r] 的所有子数组是合法的,剩下的 [r + 1, qr] 的合法子数组如何求?可以在滑动过程中记录以每个元素为终点的合法子数组个数,以前缀和的形式保存。

这里区间的划分与结果集的构成是非常有讲究的。前缀和记录的是以元素为 终点 的合法子数组,如果我们在滑动的过程中根据查询区间终点匹配当前元素,即 qr == r,那么可能的情况为 ql >= l 查询区间被完全包含。如果 ql < l 则查询区间与合法区间相交。如果这时使用前缀和计算 [ql, l],使用公式计算 [l, r] 就错了。因为后面区间使用公式计算的子数组并不包括以前面区间内的元素为起点的子数组,并且前缀和记录的子数组的起点可能在查询的左边界之外。而反过来前面使用公式计算,后面使用前缀和计算,被减去的那部分子数组个数中就包含了以 前面区间所有元素 为终点的子数组,也就是前面使用公式计算的子数组个数。我们不用担心后面通过前缀和计算的子数组的起点超出左边界,如果超出的话,一定是被包含的关系。

核心点在于想清楚这两部分集合的关系, [i, j] 的所有子数组包括了以 b ∈ [i, j] 为终点,a ∈ [i, b] 为起点的子数组。而使用前缀和相减计算的是所有以 c ∈ [m, n] 为终点的合法子数组,起点可以不在该区间,但是不会超出 ql

代码


/**
 * @date 2024-11-13 0:25
 */
public class CountKConstraintSubstrings3261 {

    public long[] countKConstraintSubstrings_v1(String s, int k, int[][] queries) {
        int n = s.length();
        char[] chars = s.toCharArray();
        int[] cnt = new int[2];
        long[] res = new long[queries.length];
        long[] prefix = new long[n + 1];
        int[] right = new int[n];
        Arrays.fill(right, n);
        int l = 0;
        for (int i = 0; i < n; i++) {
            cnt[chars[i] - '0']++;
            while (l <= i && cnt[0] > k && cnt[1] > k) {
                right[l] = i;
                cnt[chars[l++] - '0']--;
            }
            prefix[i + 1] += prefix[i] + i - l + 1;
        }
        for (int i = 0; i < queries.length; i++) {
            int ql = queries[i][0];
            int qr = queries[i][1];
            int r = Math.min(right[ql], qr + 1);
            res[i] = (r - ql + 1L) * (r - ql) / 2 + prefix[qr + 1] - prefix[r];

        }
        return res;
    }

}

性能