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;
    }

}

性能

540.有序数组中的单一元素

目标

给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。

请你找出并返回只出现一次的那个数。

你设计的解决方案必须满足 O(log n) 时间复杂度和 O(1) 空间复杂度。

示例 1:

输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2

示例 2:

输入: nums =  [3,3,7,7,10,11,11]
输出: 10

说明:

  • 1 <= nums.length <= 10^5
  • 0 <= nums[i] <= 10^5

思路

有序整数数组 nums,除了一个元素仅出现一次,其余每个元素都会出现两次,返回这个只出现一次的元素。

直接循环异或每个元素,时间复杂度为 O(n)。

题目要求时间复杂度是 O(logn) 显然需要使用二分查找,但是问题在于如果当前元素与其前/后元素相等,那么我们应该排除哪边?

这道题使用二分法的关键就是意识到 中间元素下标的奇偶性 与其前后元素的相等关系 可以判断只出现一次的元素在中间元素的哪边。

如果不考虑这个唯一元素,数组元素的排列一定是 a a b b c c ……

  • 如果 mid 为偶数下标,nums[mid] == nums[mid + 1]
  • 如果 mid 为奇数下标,nums[mid - 1] == nums[mid]

现在插入了一个唯一元素,那么该下标 后面 的关系变为:

  • 如果 mid 为偶数下标,nums[mid - 1] == nums[mid]
  • 如果 mid 为奇数下标,nums[mid] == nums[mid + 1]

根据任意一组关系就可以判断唯一元素下标在 mid 的左侧还是右侧了。当 mid 为偶数,比较 nums[mid] == nums[mid + 1],如果不相等说明在左侧,当 mid 为奇数,比较 nums[mid - 1] == nums[mid],如果不等说明在左侧。即相等需要舍弃左边,不等舍弃右边。

官网题解给出了优化细节,省略奇偶性判断,直接比较 midmid^1 两个元素,当 mid 为奇数时,mid^1 = mid - 1,当 mid 为偶数时, mid^1 = mid + 1

官网题解还给出了一种判断方法,由于唯一元素的下标一定是偶数,因此可以二分查找偶数下标,唯一元素以及它右侧的所有偶数下标都满足 nums[mid] != nums[mid + 1],我们只需要找到第一个满足条件的下标即可。查找的过程需要保证 mid 为偶数,这样判断才能成立,通常的做法是得到 mid 之后判断其奇偶性,如果是奇数则减一。这里同样也有个优化细节,即 mid - (mid & 1) 可以保证 mid 为偶数。

代码


/**
 * @date 2024-11-10 14:38
 */
public class SingleNonDuplicate540 {

    public int singleNonDuplicate(int[] nums) {
        int n = nums.length;
        int l = 0;
        int r = n - 1;
        while (l < r) {
            int mid = l + (r - l) / 2;
            mid -= mid & 1;
            if (nums[mid] != nums[mid + 1]) {
                r = mid;
            } else {
                l = mid + 2;
            }
        }
        return nums[l];
    }

}

性能

3255.长度为K的子数组的能量值II

目标

给你一个长度为 n 的整数数组 nums 和一个正整数 k 。

一个数组的 能量值 定义为:

  • 如果 所有 元素都是依次 连续 且 上升 的,那么能量值为 最大 的元素。
  • 否则为 -1 。

你需要求出 nums 中所有长度为 k 的 子数组 的能量值。

请你返回一个长度为 n - k + 1 的整数数组 results ,其中 results[i] 是子数组 nums[i..(i + k - 1)] 的能量值。

示例 1:

输入:nums = [1,2,3,4,3,2,5], k = 3
输出:[3,4,-1,-1,-1]
解释:
nums 中总共有 5 个长度为 3 的子数组:
[1, 2, 3] 中最大元素为 3 。
[2, 3, 4] 中最大元素为 4 。
[3, 4, 3] 中元素 不是 连续的。
[4, 3, 2] 中元素 不是 上升的。
[3, 2, 5] 中元素 不是 连续的。

示例 2:

输入:nums = [2,2,2,2,2], k = 4
输出:[-1,-1]
示例 3:
输入:nums = [3,2,3,2,3,2], k = 2
输出:[-1,3,-1,3,-1]

说明:

  • 1 <= n == nums.length <= 10^5
  • 1 <= nums[i] <= 10^6
  • 1 <= k <= n

思路

有一个整数数组 nums,如果其子数组中的元素是连续且递增的,即公差为 1 的数列,定义子数组的能量值为子数组的最大元素,否则能量为 -1。返回所有长度为 k 的子数组的能量值。

3254.长度为K的子数组的能量值I 相比,数据范围从 1 ~ 500 变成了 1 ~ 10^5。数据范围小可以枚举长度为 k 的子数组,时间复杂度为 O((n - k + 1) * k),即起点个数 * 子数组长度。

对于这个题,如果 n10^5k10^4n - k9 * 10^8 复杂度接近 10^9 超时。

今天换一个解法,记录连续递增的个数。

代码


/**
 * @date 2024-11-07 8:41
 */
public class ResultsArray3255 {

    public int[] resultsArray(int[] nums, int k) {
        int n = nums.length;
        int[] res = new int[n - k + 1];
        Arrays.fill(res, -1);
        int cnt = 1;
        for (int i = 0; i < n; i++) {
            if (i == 0 || nums[i] - nums[i - 1] != 1) {
                cnt = 1;
            } else {
                cnt++;
            }
            if (cnt >= k) {
                res[i - k + 1] = nums[i];
            }
        }
        return res;
    }

}

性能

3254.长度为K的子数组的能量值I

目标

给你一个长度为 n 的整数数组 nums 和一个正整数 k 。

一个数组的 能量值 定义为:

  • 如果 所有 元素都是依次 连续 且 上升 的,那么能量值为 最大 的元素。
  • 否则为 -1 。

你需要求出 nums 中所有长度为 k 的 子数组 的能量值。

请你返回一个长度为 n - k + 1 的整数数组 results ,其中 results[i] 是子数组 nums[i..(i + k - 1)] 的能量值。

示例 1:

输入:nums = [1,2,3,4,3,2,5], k = 3
输出:[3,4,-1,-1,-1]
解释:
nums 中总共有 5 个长度为 3 的子数组:
[1, 2, 3] 中最大元素为 3 。
[2, 3, 4] 中最大元素为 4 。
[3, 4, 3] 中元素 不是 连续的。
[4, 3, 2] 中元素 不是 上升的。
[3, 2, 5] 中元素 不是 连续的。

示例 2:

输入:nums = [2,2,2,2,2], k = 4
输出:[-1,-1]

示例 3:

输入:nums = [3,2,3,2,3,2], k = 2
输出:[-1,3,-1,3,-1]

说明:

  • 1 <= n == nums.length <= 500
  • 1 <= nums[i] <= 10^5
  • 1 <= k <= n

思路

有一个整数数组 nums,如果其子数组中的元素是连续且递增的,即公差为 1 的数列,定义子数组的能量值为子数组的最大元素,否则能量为 -1。返回所有长度为 k 的子数组的能量值。

显然需要使用滑动窗口,关键在于元素移入移出后如何判断是否连续且递增。我们可以使用队列记录不满足规则元素的 前一个 元素下标。当元素移入窗口时,判断元素与前一个元素是否满足规则,不满足则将前一个元素下标加入队列。窗口滑动时,先判断被移出的元素下标是否与队首相同,如果相同将队首元素删除。最后判断队列是否为空,如果为空则能量为新加入的元素,否则为 -1

官网题解使用一个计数器记录连续递增的元素个数,初始化结果数组元素为 -1,当计数器大于等于 k 时,记录 i - k + 1 位置上的能量值为当前元素,否则将计数器重置为 1。

代码


/**
 * @date 2024-11-06 5:50
 */
public class ResultsArray3254 {

    public int[] resultsArray(int[] nums, int k) {
        int n = nums.length;
        if (k == 1) {
            return nums;
        }
        int[] res = new int[n - k + 1];
        Queue<Integer> q = new ArrayDeque<>();
        int l = 0, i = 0;
        for (int r = 1; r < n; r++) {
            int prev = r - 1;
            if (nums[r] - nums[prev] != 1) {
                q.offer(prev);
            }
            if (r - l + 1 > k) {
                if (!q.isEmpty() && l == q.peek()) {
                    q.poll();
                }
                l++;
            }
            if (r - l + 1 == k) {
                if (q.isEmpty()) {
                    res[i++] = nums[r];
                } else {
                    res[i++] = -1;
                }
            }
        }
        return res;
    }

}

性能

638.大礼包

目标

在 LeetCode 商店中, 有 n 件在售的物品。每件物品都有对应的价格。然而,也有一些大礼包,每个大礼包以优惠的价格捆绑销售一组物品。

给你一个整数数组 price 表示物品价格,其中 price[i] 是第 i 件物品的价格。另有一个整数数组 needs 表示购物清单,其中 needs[i] 是需要购买第 i 件物品的数量。

还有一个数组 special 表示大礼包,special[i] 的长度为 n + 1 ,其中 special[i][j] 表示第 i 个大礼包中内含第 j 件物品的数量,且 special[i][n] (也就是数组中的最后一个整数)为第 i 个大礼包的价格。

返回 确切 满足购物清单所需花费的最低价格,你可以充分利用大礼包的优惠活动。你不能购买超出购物清单指定数量的物品,即使那样会降低整体价格。任意大礼包可无限次购买。

示例 1:

输入:price = [2,5], special = [[3,0,5],[1,2,10]], needs = [3,2]
输出:14
解释:有 A 和 B 两种物品,价格分别为 ¥2 和 ¥5 。 
大礼包 1 ,你可以以 ¥5 的价格购买 3A 和 0B 。 
大礼包 2 ,你可以以 ¥10 的价格购买 1A 和 2B 。 
需要购买 3 个 A 和 2 个 B , 所以付 ¥10 购买 1A 和 2B(大礼包 2),以及 ¥4 购买 2A 。

示例 2:

输入:price = [2,3,4], special = [[1,1,0,4],[2,2,1,9]], needs = [1,2,1]
输出:11
解释:A ,B ,C 的价格分别为 ¥2 ,¥3 ,¥4 。
可以用 ¥4 购买 1A 和 1B ,也可以用 ¥9 购买 2A ,2B 和 1C 。 
需要买 1A ,2B 和 1C ,所以付 ¥4 买 1A 和 1B(大礼包 1),以及 ¥3 购买 1B , ¥4 购买 1C 。 
不可以购买超出待购清单的物品,尽管购买大礼包 2 更加便宜。

说明:

  • n == price.length == needs.length
  • 1 <= n <= 6
  • 0 <= price[i], needs[i] <= 10
  • 1 <= special.length <= 100
  • special[i].length == n + 1
  • 0 <= special[i][j] <= 50
  • 生成的输入对于 0 <= j <= n - 1 至少有一个 special[i][j] 非零。

思路

有一个购物清单 needneed[i] 表示需要购买商品 i 的数量,price[i] 表示商品 i 的单价,此外还有一组大礼包 specialspecial[j][i] 表示大礼包 j 中包含的第 i 件商品的数量,并且 specal[j][n] 表示该大礼包的价格。求购买 need 清单中的商品最少花费多少钱,我们可以购买大礼包任意次,但是购买的总数量不能超过需求的数量,尽管可能价格更低。

完全背包问题是物品有无限个,背包容量有限,求能装下的最大价值/最小价值。如果将题目中的清单视为多个背包容量,单买物品 i,以及购买大礼包 j 中的商品 i 视为不同的商品,那么我们求的是装满所有背包的最小价值。问题在于,大礼包不光有商品 i,还有其它商品,如何处理?

网友题解将单买也看成大礼包,只不过其它商品数量为 0,这样可以统一处理大礼包。

// todo

代码

性能

3259.超级饮料的最大强化能量

目标

来自未来的体育科学家给你两个整数数组 energyDrinkA 和 energyDrinkB,数组长度都等于 n。这两个数组分别代表 A、B 两种不同能量饮料每小时所能提供的强化能量。

你需要每小时饮用一种能量饮料来 最大化 你的总强化能量。然而,如果从一种能量饮料切换到另一种,你需要等待一小时来梳理身体的能量体系(在那个小时里你将不会获得任何强化能量)。

返回在接下来的 n 小时内你能获得的 最大 总强化能量。

注意 你可以选择从饮用任意一种能量饮料开始。

示例 1:

输入:energyDrinkA = [1,3,1], energyDrinkB = [3,1,1]
输出:5
解释:
要想获得 5 点强化能量,需要选择只饮用能量饮料 A(或者只饮用 B)。

示例 2:

输入:energyDrinkA = [4,1,1], energyDrinkB = [1,1,3]
输出:7
解释:
第一个小时饮用能量饮料 A。
切换到能量饮料 B ,在第二个小时无法获得强化能量。
第三个小时饮用能量饮料 B ,并获得强化能量。

说明:

  • n == energyDrinkA.length == energyDrinkB.length
  • 3 <= n <= 10^5
  • 1 <= energyDrinkA[i], energyDrinkB[i] <= 10^5

思路

energyDrinkAenergyDrinkB 两个数组,表示饮料 AB 在第 i 小时可以提供的能量,现在需要每一小时饮用饮料 AB 来获取能量,可以暂停一小时来切换饮料,求能够获得的最大能量。

定义 dp[i][j] 表示第 i 小时选择饮料 j 所能获取的最大能量,假设 j = 0 表示饮料 Aj = 1 表示饮料 B,那么状态转移方程为 dp[i][0] = Math.max(dp[i - 1][1], dp[i - 1][0] + energyDrinkA)dp[i][1] = Math.max(dp[i - 1][0], dp[i - 1][1] + energyDrinkB),最终返回 Math.max(dp[n - 1][0], dp[n - 1][1])。初始条件 dp[0][0] = energyDrinkA[0] dp[0][1] = energyDrinkA[1]

由于只与前面的状态有关,因此可以进行存储优化,使用两个变量保存前一个小时饮用 A B 的最大能量即可。

代码


/**
 * @date 2024-11-01 0:37
 */
public class MaxEnergyBoost3259 {

    public long maxEnergyBoost_v1(int[] energyDrinkA, int[] energyDrinkB) {
        int n = energyDrinkA.length;
        long prevA = energyDrinkA[0];
        long prevB = energyDrinkB[0];
        for (int i = 1; i < n; i++) {
            long curA = Math.max(prevB,  prevA + energyDrinkA[i]);
            long curB = Math.max(prevA,  prevB + energyDrinkB[i]);
            prevA = curA;
            prevB = curB;
        }
        return Math.max(prevA, prevB);
    }

}

性能

3211.生成不含相邻零的二进制字符串

目标

给你一个正整数 n。

如果一个二进制字符串 x 的所有长度为 2 的 子字符串 中包含 至少 一个 "1",则称 x 是一个 有效 字符串。

返回所有长度为 n 的 有效 字符串,可以以任意顺序排列。

示例 1:

输入: n = 3
输出: ["010","011","101","110","111"]
解释:
长度为 3 的有效字符串有:"010"、"011"、"101"、"110" 和 "111"。

示例 2:

输入: n = 1
输出: ["0","1"]
解释:
长度为 1 的有效字符串有:"0" 和 "1"。

说明:

  • 1 <= n <= 18

思路

示例2让人困惑,字符串 0 并没有长度为 2 的子字符串,更别提包含至少一个 1 了,但它是有效字符串。

还是按照题目名称来做吧,生成长度为 n,不含相邻零的二进制字符串。直接回溯即可。

官网题解还提出了一种位运算的解法,主要思想就是将 二进制字符串 视为 数字的二进制表示,问题转化为 0 ~ 2^n - 1 的数字中不含相邻零的个数。由于超出 n 的位数不在我们的考虑范围,为了简化处理,可以直接将数字的低 n 位取反,x ^ ((1 << n) - 1))1 << n0 开始计向左移 n 位,再减 1,得到低 n 位全为 1 的数字,对它取异或相当于低 n 位取反。问题转换为 数字中是否存在连续的 1。针对每一个数字,无需遍历每一位,直接使用 x & (x >> 1) 是否等于 0 来判断是否含有相邻的 1。相当于将每一位与它前面的位相与,如果存在相邻的 1 就会存在某个位相与的结果为 1 使最终结果不为 0

代码


/**
 * @date 2024-10-29 0:23
 */
public class ValidStrings3211 {
    List<String> res;
    char[] path;
    int n;

    public List<String> validStrings(int n) {
        res = new ArrayList<>();
        path = new char[n];
        this.n = n;
        backTracing('1', 0);
        return res;
    }

    public void backTracing(char prev, int i) {
        if (i == n) {
            res.add(new String(path));
            return;
        }
        path[i] = '1';
        int next = i + 1;
        backTracing('1', next);
        if (prev != '0') {
            path[i] = '0';
            backTracing('0', next);
        }
    }
}

性能

684.冗余连接

目标

树可以看成是一个连通且 无环 的 无向 图。

给定往一棵 n 个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1 到 n 中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edges ,edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。

请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回数组 edges 中最后出现的那个。

示例 1:

输入: edges = [[1,2], [1,3], [2,3]]
输出: [2,3]

示例 2:

输入: edges = [[1,2], [2,3], [3,4], [1,4], [1,5]]
输出: [1,4]

说明:

  • n == edges.length
  • 3 <= n <= 1000
  • edges[i].length == 2
  • 1 <= ai < bi <= edges.length
  • ai != bi
  • edges 中无重复元素
  • 给定的图是连通的

思路

有一颗 n 个节点的树,节点编号 1 ~ n。使用 edges 表示向树中两个没有直接连接的节点之间加一条边之后的边的集合,找出一条可以删除的边使得 edges 变为一颗有 n 个节点的树。如果有多种选择,返回 edges 中最后出现的那个,即下标最大的边。

我们可以选择一个根节点,比如从节点 1 出发,使用回溯记录已经访问过的节点,如果发现回到已访问过的非父节点说明出现了环。如果只是寻找环的上的任一条边的话,直接返回即可。

麻烦点在于题目要求返回 edges 中最后出现的边,因此我们需要记录访问的路径,从环开始的节点往后的节点都是在环上的。最后从后向前遍历 edges 找到第一个两端点都在环上的边。

官网题解使用的是并查集。// todo

代码


/**
 * @date 2024-10-27 16:34
 */
public class FindRedundantConnection684 {
    List<Integer>[] g;
    Set<Integer> loop;
    List<Integer> path;
    int start;

    public int[] findRedundantConnection(int[][] edges) {
        int n = edges.length;
        g = new List[n + 1];
        for (int i = 0; i <= n; i++) {
            g[i] = new ArrayList<>();
        }
        for (int[] edge : edges) {
            g[edge[0]].add(edge[1]);
            g[edge[1]].add(edge[0]);
        }
        loop = new HashSet<>(n);
        path = new ArrayList<>();
        dfs(0, 1);
        loop = new HashSet<>();
        for (int i = path.size() - 1; i >= 0; i--) {
            loop.add(path.get(i));
            if (start == path.get(i)) {
                break;
            }
        }
        for (int i = n - 1; i >= 0; i--) {
            if (loop.contains(edges[i][0]) && loop.contains(edges[i][1])) {
                return edges[i];
            }
        }
        return null;
    }

    private boolean dfs(int parent, int current) {
        for (Integer next : g[current]) {
            if (next == parent) {
                continue;
            }
            if (loop.contains(next)) {
                start = next;
                return true;
            } else {
                loop.add(next);
                path.add(next);
                if (dfs(current, next)) {
                    return true;
                }
                path.remove(path.size() - 1);
                loop.remove(next);
            }
        }
        return false;
    }

}

性能