跳至主要內容

LCCUP'22 力扣杯秋季编程大赛-战队赛/个人赛

大约 10 分钟

LCCUP'22 力扣杯秋季编程大赛-战队赛/个人赛

战队赛

比赛时间 2022-10-07。感谢队友 @ming1ing@kexp,喜提 Rank 112,超过了 97.1% 的队伍!

T1. 最小展台数量(2 分)

解题思路

贪心。对每天各类型的展台数取最大值,求和即可。

时间复杂度:O(n)。其中常数为 26

参考代码

class Solution {
    public int minNumBooths(String[] demand) {
        int[] f = new int[26];
        for (String s : demand) {
            int[] f1 = new int[26];
            for (char ch : s.toCharArray()) {
                f1[ch - 'a']++;
            }
            for (int i = 0; i < 26; i++) {
                f[i] = Math.max(f[i], f1[i]);
            }
        }
        return Arrays.stream(f).sum();
    }
}

T2. 装饰树(4 分)

解题思路

二叉树递归。

时间复杂度:O(n)

参考代码

class Solution {
    public TreeNode expandBinaryTree(TreeNode root) {
        if (root.left != null) {
            TreeNode tmp = root.left;
            root.left = new TreeNode(-1);
            root.left.left = expandBinaryTree(tmp);
        }
        if (root.right != null) {
            TreeNode tmp = root.right;
            root.right = new TreeNode(-1);
            root.right.right = expandBinaryTree(tmp);
        }
        return root;
    }
}

T3. 美观的花束(6 分)

解题思路

双指针。滑动窗口求每个下标贡献,累加即可。

时间复杂度:O(n)

参考代码

class Solution {
    private static final int MAX_N = (int) (1e5 + 5);
    private static final int MOD = (int) (1e9 + 7);

    public int beautifulBouquet(int[] flowers, int cnt) {
        int len = flowers.length;

        // 双指针
        int left = 0;
        int right = 0;
        int[] cntArr = new int[MAX_N];
        long res = 0;
        while (right < len) {
            cntArr[flowers[right]]++;
            right++;

            res += right - left;
            res %= MOD;
            while (right < len && cntArr[flowers[right]] == cnt) {
                cntArr[flowers[left]]--;
                left++;
            }
        }
        return (int) res;
    }
}

T4. Hello LeetCode!(8 分)

解题思路

记忆化搜索。

字符串 helloleetcode 中,e 出现 4 次,l 出现 3 次,o 出现 2 次,h t c d 各出现 1 次,。因此状态共有 A = 5 * 4 * 3 * 2 * 2 * 2 * 2 = 960 种。

  • 首先预处理出 costs[i][mask],表示从 words[i] 的单词里,获取字符状态为 mask 的最小代价。
  • 再用记忆化搜索求获取到完整 helloleetcode 的最小代价。

时间复杂度:O(n·A·m·2^m)。外加大量剪枝。

参考代码

class Solution {
    private static final int INF = Integer.MAX_VALUE / 2;
    private int n;
    private List<Map<Node, Integer>> costs;
    private List<Map<Node, Integer>> memo;

    public int Leetcode(String[] words) {
        n = words.length;

        // 预处理每个单词的每种选择字母的方案所消耗的代币的最小值
        costs = new ArrayList<>();
        for (String word : words) {
            Map<Node, Integer> costMap = new HashMap<>();
            dfs(word, new Node(new int[7]), 0, costMap);
            costs.add(costMap);
        }

        // 记忆化搜索
        memo = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            memo.add(new HashMap<>());
        }
        int res = dfs2(0, new Node(new int[7]));
        return res == INF ? -1 : res;
    }

    private int dfs2(int i, Node mask) {
        if (i == n) {
            // inf 表示不合法,没有选完要求的字母
            return mask.isFull() ? 0 : INF;
        }
        if (memo.get(i).containsKey(mask)) {
            return memo.get(i).get(mask);
        }
        int res = INF;
        for (Map.Entry<Node, Integer> entry : costs.get(i).entrySet()) {
            Node add = entry.getKey();
            int tot = entry.getValue();
            // 剪枝
            if (tot >= res) {
                continue;
            }
            Node m = merge(mask, add);
            if (m.isLess()) {
                res = Math.min(res, tot + dfs2(i + 1, m));
            }
        }
        if (!memo.get(i).containsKey(mask)) {
            memo.get(i).put(mask, res);
        }
        return res;
    }

    private void dfs(String str, Node mask, int tot, Map<Node, Integer> costMap) {
        if (!costMap.containsKey(mask) || costMap.get(mask) > tot) {
            costMap.put(mask, tot);
        }
        for (int k = 0; k < str.length(); k++) {
            char ch = str.charAt(k);
            String nextStr = str.substring(0, k) + str.substring(k + 1);
            int nextTot = tot + k * (str.length() - 1 - k);
            for (int i = 0; i < Node.N; i++) {
                // 可以选 ch
                if (ch == Node.CHARS[i] && mask.cnt[i] < Node.LIMIT[i]) {
                    Node nextMask = new Node(mask.cnt);
                    nextMask.cnt[i]++;
                    dfs(nextStr, nextMask, nextTot, costMap);
                    break;
                }
            }
        }
    }

    // 合并两种选择字母的方案
    private Node merge(Node cur, Node add) {
        Node res = new Node(cur.cnt);
        for (int i = 0; i < Node.N; i++) {
            res.cnt[i] += add.cnt[i];
        }
        return res;
    }

    private static class Node {
        private static final int N = 7;
        private static final char[] CHARS = {'e', 'l', 'o', 'h', 't', 'c', 'd'};
        private static final int[] LIMIT = {4, 3, 2, 1, 1, 1, 1};
        private final int[] cnt;

        public Node(int[] tuple) {
            cnt = new int[N];
            System.arraycopy(tuple, 0, cnt, 0, N);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Node node)) return false;
            return Arrays.equals(cnt, node.cnt);
        }

        @Override
        public int hashCode() {
            return Arrays.hashCode(cnt);
        }

        boolean isFull() {
            return Arrays.equals(cnt, LIMIT);
        }

        boolean isLess() {
            for (int i = 0; i < N; i++) {
                if (cnt[i] > LIMIT[i]) {
                    return false;
                }
            }
            return true;
        }
    }
}

T5. 沙地治理(9 分)

解题思路

构造题。找规律,没啥好说的。

一开始想法是覆盖两条边外加向下三角形即可。然后当 size=5 时不出意料 WA 了。

@ming1ing 提议多 WA 几次通过看更多的用例找规律。实践证明这招非常有用,WA 两发就能看出规律。如下图:

参考代码

class Solution {
    public int[][] sandyLandManagement(int size) {
        List<int[]> list = new ArrayList<>();
        int k = 0;
        list.add(new int[]{1, 1});
        for (int i = size; i > 1; i--) {
            if (k == 0) {
                for (int j = 1; j <= i * 2 - 1; j += 2) {
                    list.add(new int[]{i, j});
                }
            } else if (k == 1) {
                list.add(new int[]{i, 2});
            } else if (k == 2) {
                for (int j = 3; j <= i * 2 - 1; j += 2) {
                    list.add(new int[]{i, j});
                }
            } else {
                list.add(new int[]{i, 1});
            }
            k = (k + 1) % 4;
        }
        return list.toArray(new int[list.size()][]);
    }
}

T6. 集水器(10 分)

解题思路

并查集。

  • 从下往上逐行使用并查集连通,不与外界连通的区域表示可能有水的(也有可能是完全密闭的)。
  • 最后用并查集连通上边界,排除完全密闭的区域。求和即可。

时间复杂度:O(nm·logn)。其中路径压缩并查集最坏时间复杂度为:O(logn)

参考代码

class Solution {
    public int reservoir(String[] shape) {
        int M = shape.length;
        int N = shape[0].length();

        // 增加一层外边界
        int[][] up = new int[M + 2][N + 2];
        int[][] down = new int[M + 2][N + 2];
        int[][] left = new int[M + 2][N + 2];
        int[][] right = new int[M + 2][N + 2];

        // 共 4·M·N 个点 映射到 [0, id)
        int id = 0;
        for (int i = 1; i <= M; i++) {
            for (int j = 1; j <= N; j++) {
                up[i][j] = id++;
                right[i][j] = id++;
                left[i][j] = id++;
                down[i][j] = id++;
            }
        }
        // 左右边界连超级汇点 id
        for (int i = 1; i <= M; i++) {
            right[i][0] = id;
            left[i][N + 1] = id;
        }
        // 下边界连超级汇点 id
        for (int j = 1; j <= N; j++) {
            up[M + 1][j] = id;
        }

        boolean[] mayWater = new boolean[id + 1];
        // 并查集
        DSU dsu = new DSU(id + 1);
        // 从下往上枚举
        for (int i = M; i >= 1; i--) {
            // 连通左右
            for (int j = 0; j <= N; j++) {
                dsu.union(right[i][j], left[i][j + 1]);
            }
            // 连通上下
            for (int j = 1; j <= N; j++) {
                dsu.union(down[i][j], up[i + 1][j]);
            }
            // 连通格子内部
            for (int j = 1; j <= N; j++) {
                char ch = shape[i - 1].charAt(j - 1);
                if (ch == '.') {
                    dsu.union(up[i][j], down[i][j]);
                    dsu.union(up[i][j], left[i][j]);
                    dsu.union(up[i][j], right[i][j]);
                } else if (ch == 'l') {
                    dsu.union(left[i][j], down[i][j]);
                    dsu.union(up[i][j], right[i][j]);
                } else {
                    dsu.union(left[i][j], up[i][j]);
                    dsu.union(down[i][j], right[i][j]);
                }
            }

            // 不连通表示可能有水
            for (int j = 1; j <= N; j++) {
                mayWater[up[i][j]] = (dsu.find(up[i][j]) != dsu.find(id));
                mayWater[down[i][j]] = (dsu.find(down[i][j]) != dsu.find(id));
                mayWater[left[i][j]] = (dsu.find(left[i][j]) != dsu.find(id));
                mayWater[right[i][j]] = (dsu.find(right[i][j]) != dsu.find(id));
            }
        }

        // 第一行连超级汇点
        for (int j = 1; j <= N; j++) {
            dsu.union(up[1][j], id);
        }

        // 统计数量
        int res = 0;
        for (int i = 0; i < id; i++) {
            // 不在闭合区域内可能有水
            if (mayWater[i] && dsu.find(i) == dsu.find(id)) {
                res++;
            }
        }
        return res / 2;
    }

    private static class DSU {
        // 父节点数组/祖先数组
        int[] fa;

        // 初始化
        public DSU(int n) {
            fa = new int[n];
            for (int i = 0; i < n; i++) {
                fa[i] = i;
            }
        }

        // 查找
        int find(int x) {
            // 路径压缩
            if (x != fa[x]) {
                fa[x] = find(fa[x]);
            }
            return fa[x];
        }

        // 合并
        void union(int p, int q) {
            int rootP = find(p);
            int rootQ = find(q);
            if (rootP == rootQ) {
                return;
            }
            fa[rootQ] = rootP;
        }
    }
}

个人赛

历史上参加的三次个人赛,时间全在“月末周六”(2021-09-11、2022-04-16、2022-09-24)。意难平,只能简单做个签到题。

T1. 气温变化趋势(2 分)

解题思路

枚举。令 isEqual[i] 表示第 i 天两地气温变化趋势是否相同。求最大连续区间即可。

时间复杂度:O(n)

参考代码

class Solution {
    public int temperatureTrend(int[] temperatureA, int[] temperatureB) {
        int n = temperatureA.length;

        boolean[] isEqual = new boolean[n];
        for (int i = 1; i < n; i++) {
            if ((temperatureA[i] - temperatureA[i - 1] > 0 && temperatureB[i] - temperatureB[i - 1] > 0)
                    || (temperatureA[i] - temperatureA[i - 1] < 0 && temperatureB[i] - temperatureB[i - 1] < 0)
                    || (temperatureA[i] - temperatureA[i - 1] == 0 && temperatureB[i] - temperatureB[i - 1] == 0)) {
                isEqual[i] = true;
            }
        }

        int max = 0;
        int cnt = 0;
        for (boolean eq : isEqual) {
            if (eq) {
                cnt++;
                max = Math.max(max, cnt);
            } else {
                cnt = 0;
            }
        }
        return max;
    }
}

T2. 交通枢纽(4 分)

解题思路

图论。对每个节点计算入度和出度。交通枢纽即入度为 节点数-1 && 出度为 0 的节点。

时间复杂度:O(n+m)

参考代码

class Solution {
    public int transportationHub(int[][] path) {
        Map<Integer, Integer> inDegrees = new HashMap<>();
        Map<Integer, Integer> outDegrees = new HashMap<>();
        // 出现过的节点
        Set<Integer> seen = new HashSet<>();
        for (int[] tuple : path) {
            int u = tuple[0];
            int v = tuple[1];
            // v入度+1 u出度+1
            inDegrees.put(v, inDegrees.getOrDefault(v, 0) + 1);
            outDegrees.put(u, outDegrees.getOrDefault(u, 0) + 1);
            seen.add(u);
            seen.add(v);
        }

        // 入度为 n-1 && 出度为 0
        int sz = seen.size();
        for (int x : seen) {
            if (inDegrees.getOrDefault(x, 0) == sz - 1 && outDegrees.getOrDefault(x, 0) == 0) {
                return x;
            }
        }
        return -1;
    }
}

T3. 弹珠游戏(6 分)

解题思路

BFS 模拟。本题疑似存在卡常,需从 '.''O',逆向的话会 TLE

时间复杂度:O(nm)

参考代码

class Solution {
    // up left down right
    private static final int[] DX = {-1, 0, 1, 0};
    private static final int[] DY = {0, 1, 0, -1};

    public int[][] ballGame(int num, String[] plate) {
        int M = plate.length;
        int N = plate[0].length();
        char[][] chars = new char[M][N];
        for (int i = 0; i < M; i++) {
            chars[i] = plate[i].toCharArray();
        }

        // [startM, startN, curM, curN, dir]
        Queue<int[]> queue = new LinkedList<>();
        for (int j = 1; j < N - 1; j++) {
            // 上边界往下走
            if (chars[0][j] == '.') {
                queue.add(new int[]{0, j, 0, j, 2});
            }
            // 下边界往上走
            if (chars[M - 1][j] == '.') {
                queue.add(new int[]{M - 1, j, M - 1, j, 0});
            }
        }
        for (int i = 1; i < M - 1; i++) {
            // 左边界往右走
            if (chars[i][0] == '.') {
                queue.add(new int[]{i, 0, i, 0, 1});
            }
            // 右边界往左走
            if (chars[i][N - 1] == '.') {
                queue.add(new int[]{i, N - 1, i, N - 1, 3});
            }
        }

        List<int[]> resList = new ArrayList<>();
        while (!queue.isEmpty() && num >= 0) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                int[] tuple = queue.remove();
                int startM = tuple[0];
                int startN = tuple[1];
                int curM = tuple[2];
                int curN = tuple[3];
                int dir = tuple[4];

                if (chars[curM][curN] == 'O') {
                    resList.add(new int[]{startM, startN});
                    continue;
                } else if (chars[curM][curN] == 'E') {
                    // "E" 表示顺时针转向器(弹珠经过时方向将顺时针旋转 90 度);
                    dir = (dir + 1) % 4;
                } else if (chars[curM][curN] == 'W') {
                    // "W" 表示逆时针转向器(弹珠经过时方向将逆时针旋转 90 度);
                    dir = (dir + 3) % 4;
                }
                int nextM = curM + DX[dir];
                int nextN = curN + DY[dir];
                if (nextM >= 0 && nextM < M && nextN >= 0 && nextN < N) {
                    queue.add(new int[]{startM, startN, nextM, nextN, dir});
                }
            }
            num--;
        }
        return resList.toArray(new int[resList.size()][]);
    }
}

T4. 二叉树灯饰(8 分)

解题思路

树形 DP。令 f(TreeNode node, boolean switch2Odd, boolean switch3) 表示当前节点,父节点 开关2 切换次数是否是奇数次,父节点 开关3 是否切换时,关闭子树所有灯最少需要操作多少次开关。

  • 若当前节点为开灯状态,则 res = min(切换开关1, 切换开关2, 切换开关3, 切换开关123)
  • 若当前节点为关灯状态,则 res = min(不操作, 切换开关12, 切换开关13, 切换开关23)

时间复杂度:O(n)

参考代码

class Solution {
    private Map<TreeNode, Map<Integer, Integer>> memo;

    public int closeLampInTree(TreeNode root) {
        memo = new HashMap<>();
        return f(root, 0);
    }

    // f(TreeNode node, boolean switch2Odd, boolean switch3)
    // 表示当前节点,父节点开关 2 切换次数是否是奇数次,父节点开关 3 是否切换时,关闭子树所有灯最少需要操作多少次开关
    // f(node, 0/1, 0/1) => f(node, mask) 方便进行记忆化 mask = switch2Odd << 1 + switch3
    private int f(TreeNode node, int mask) {
        if (node == null) {
            return 0;
        }
        // 记忆化搜索
        if (memo.containsKey(node) && memo.get(node).containsKey(mask)) {
            return memo.get(node).get(mask);
        }

        int res = Integer.MAX_VALUE;
        int bit0 = mask & 1;
        int bit1 = (mask >> 1) & 1;

        // 灯 = 开,且 switch2_odd, switch3 抵消,最终还是开
        // 灯 = 关,且 switch2_odd, switch3 不抵消,最终还是开
        if ((node.val == 1) == (bit1 == bit0)) {
            res = Math.min(res, f(node.left, bit1 << 1) + f(node.right, bit1 << 1) + 1);
            res = Math.min(res, f(node.left, (1 - bit1) << 1) + f(node.right, (1 - bit1) << 1) + 1);
            res = Math.min(res, f(node.left, (bit1 << 1) + 1) + f(node.right, (bit1 << 1) + 1) + 1);
            res = Math.min(res, f(node.left, ((1 - bit1) << 1) + 1) + f(node.right, ((1 - bit1) << 1) + 1) + 3);
        } else {
            res = Math.min(res, f(node.left, bit1 << 1) + f(node.right, bit1 << 1));
            res = Math.min(res, f(node.left, (1 - bit1) << 1) + f(node.right, (1 - bit1) << 1) + 2);
            res = Math.min(res, f(node.left, (bit1 << 1) + 1) + f(node.right, (bit1 << 1) + 1) + 2);
            res = Math.min(res, f(node.left, ((1 - bit1) << 1) + 1) + f(node.right, ((1 - bit1) << 1) + 1) + 2);
        }
        memo.computeIfAbsent(node, key -> new HashMap<>()).put(mask, res);
        return res;
    }
}

T5. 舒适的湿度(10 分)

解题思路

CF 原题:https://codeforces.com/problemset/problem/1579/Gopen in new windowDiv. 3 的最后一题,rating2200

动态规划。令 f[i][j] 表示前 i 个数,最右端点纵坐标与折线图最低端点纵坐标差值为 j 时,折线图最大最小值差值的最小值。

时间复杂度:O(n·max(operate[i]))

参考代码

class Solution {
    public int unSuitability(int[] operate) {
        int len = operate.length;
        // 答案显然不会超过最大 operate[i] 的 2 倍
        int max = Arrays.stream(operate).max().orElseThrow() * 2 + 1;

        // f[i][j] 表示前 i 个数,最右端点纵坐标与折线图最低端点纵坐标差值为 j 时,折线图最大最小值差值的最小值
        int[][] f = new int[len + 1][max];
        for (int i = 0; i < len + 1; i++) {
            Arrays.fill(f[i], Integer.MAX_VALUE);
        }
        f[0][0] = 0;
        for (int i = 0; i < len; i++) {
            int x = operate[i];
            for (int j = 0; j < max; j++) {
                if (f[i][j] == Integer.MAX_VALUE) {
                    continue;
                }
                if (j + x < max) {
                    f[i + 1][j + x] = Math.min(f[i + 1][j + x], Math.max(f[i][j], j + x));
                }
                if (j >= x) {
                    f[i + 1][j - x] = Math.min(f[i + 1][j - x], f[i][j]);
                } else {
                    f[i + 1][0] = Math.min(f[i + 1][0], f[i][j] - j + x);
                }
            }
        }
        return Arrays.stream(f[len]).min().orElseThrow();
    }
}

(全文完)