华为OD机试 - SQL记录拆分 - 并查集(Java 新系统 200分)

在这里插入图片描述

华为OD机试 新系统 题库疯狂收录中,刷题点这里

专栏导读

本专栏收录于《华为OD机试(JAVA)真题》

刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新,全天CSDN在线答疑。

一、题目描述

某分布式数据库 Q 系统需要将 SQL 操作日志拆分到多个文件中,现给定一组 SQL 语句输入(数组格式),请按要求将 SQL 语句拆分到不同文件中并返回拆分后的文件数量。拆分规则如下:

  1. 单个数组成员的 SQL 语句(可能包含多条,按 ; 分隔)按同一行完整保存,不可跨文件拆分,且同一行的语句必须在同一个文件中。
  2. 部分语句带事务标签 [Tn]n 为正整数),如 [T1]A;相同事务标签的 SQL 语句必须保存于同一文件中。
  3. 当单个文件保存的行数超出限制时,需要新建文件;按语句组首次出现的行号顺序处理,优先放入当前文件,超出限制则新建文件;
  4. 特殊场景约束:若语句 A 与语句 B 必须同文件,语句 B 与语句 C 必须同文件,则语句 A、B、C 必须在同一文件(约束传递性)。约束合并后语句组计数即便超过限制也必须放在同一文件;
  5. SQL 数组为空时返回 0。

二、输入描述

输入:
参数1:单个文件限定可保存的最大 SQL 语句行数 split_line
参数2:SQL 语句数组 sql_text,每个数组成员可能包含多条 SQL 语句,用符号 ; 分隔;

三、输出描述

SQL 语句数组按规则拆分后的文件个数。

补充说明:

  • split_line 取值范围 [1, 10000],无效值返回 0;
  • sql_text 每行最多 1000 字符,总语句最多 100000 条;
  • 事务标签的范围 [1, 1000]
  • 最后一条语句可以没有分号结尾;
  • 空语句(仅含分号)按 1 行计算。## 四、测试用例

测试用例1:

1、输入

3
[T1]A;
[T2]B;
[T1]C;

2、输出

1

3、说明

第一行同时存在 T1、T2,第二行存在 T1,因此两行属于同一约束组,共 2 行。2 <= 3,只需要 1 个文件。

测试用例2:

1、输入

2
[T1]A;
[T2]B;
[T1]C;
[T2]D;

2、输出

2

3、说明

T1 对应第 1、3 行,共 2 行;T2 对应第 2、4 行,共 2 行。第一个组装满第一个文件,第二个组必须创建第二个文件。

五、解题思路

sql_text 中的每个数组成员看作一个“行节点”。同一数组成员无论包含多少个用 ; 分隔的 SQL,都必须完整保存,因此它始终只占 1 行,不需要按分号再次拆分;像 ;;; 这样的空语句所在数组成员同样按 1 行计算。

核心难点是事务标签产生的传递约束。例如第 1 行含 [T1],第 2 行同时含 [T1][T2],第 3 行含 [T2],那么三行必须全部位于同一个文件。这个问题本质上是在求“必须同文件”关系形成的连通分量,因此使用并查集 DSU。

扫描每一行,用正则提取 [Tn]。用 HashMap 记录每个事务标签第一次出现的行号;后续再次遇到同一标签时,将当前行和第一次出现的行执行 union。若一行包含多个事务标签,该行会同时参与多个 union,从而自然完成传递合并。

所有事务处理完后,统计每个并查集连通分量包含多少行,即一个不可拆分语句组的大小。

接下来必须按照“语句组首次出现的行号”处理。无需排序:直接再次按照原始行号从前往后扫描,某个并查集根节点第一次被访问时,就代表该语句组第一次出现。然后采用贪心策略:当前文件能完整容纳该组就加入;否则新建文件。若某个组自身行数已经超过 split_line,也不能拆分,仍整体占用一个文件。

时间复杂度约为 O(L + N·α(N)),其中 L 为所有 SQL 文本总字符数,N 为数组行数;空间复杂度为 O(N + T),T 为事务标签数量。

六、Java算法源码

public class OdTest {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        String firstLine = scanner.nextLine().trim();

        int splitLine = Integer.parseInt(firstLine);

        List<String> sqlText = new ArrayList<>();

        /*
         * split_line 后面的每一行都视为 sql_text 的一个数组成员。
         *
         * 这里不能根据 ';' 再拆分,因为题目明确规定:
         * 一个数组成员中的 SQL 必须作为完整的一行保存。
         *
         * 空白行本身也可以表示一个数组成员。
         */
        while (scanner.hasNextLine()) {
            String input = scanner.nextLine();
            if(!input.contains(";")){
                break;
            }
            sqlText.add(input);
        }

        System.out.println(splitFileCount(splitLine, sqlText));
    }

    /**
     * 并查集:
     * 用于维护哪些 SQL 行由于事务约束而必须放在同一个文件中。
     */
    private static class DSU {
        int[] parent;
        int[] rank;

        DSU(int n) {
            parent = new int[n];
            rank = new int[n];

            for (int i = 0; i < n; i++) {
                parent[i] = i;
            }
        }

        /**
         * 查找节点所属集合的根节点。
         * 使用路径压缩降低后续查询开销。
         */
        int find(int x) {
            if (parent[x] != x) {
                parent[x] = find(parent[x]);
            }
            return parent[x];
        }

        /**
         * 合并两个 SQL 行所在的集合。
         */
        void union(int a, int b) {
            int rootA = find(a);
            int rootB = find(b);

            if (rootA == rootB) {
                return;
            }

            // 按秩合并,避免并查集退化成链表
            if (rank[rootA] < rank[rootB]) {
                parent[rootA] = rootB;
            } else if (rank[rootA] > rank[rootB]) {
                parent[rootB] = rootA;
            } else {
                parent[rootB] = rootA;
                rank[rootA]++;
            }
        }
    }

    /**
     * 计算最终需要拆分成多少个文件。
     */
    public static int splitFileCount(int splitLine, List<String> sqlText) {

        // split_line 不合法,或者 SQL 数组为空,直接返回 0
        if (splitLine < 1 || splitLine > 10000
                || sqlText == null || sqlText.isEmpty()) {
            return 0;
        }

        int n = sqlText.size();

        // 每个数组成员都是一个不可拆分的“SQL 行节点”
        DSU dsu = new DSU(n);

        /*
         * key   : 事务编号,例如 [T10] 中的 10
         * value : 该事务第一次出现在哪一行
         *
         * 后续再次遇到同一个事务时,只需要和第一次出现的行合并。
         */
        Map<Integer, Integer> firstLineByTransaction = new HashMap<>();

        // 匹配 [T1]、[T23]、[T1000] 等事务标签
        Pattern tagPattern = Pattern.compile("\\[T(\\d+)\\]");

        for (int i = 0; i < n; i++) {
            String line = sqlText.get(i);

            if (line == null) {
                line = "";
            }

            Matcher matcher = tagPattern.matcher(line);

            while (matcher.find()) {
                int tag;

                try {
                    tag = Integer.parseInt(matcher.group(1));
                } catch (NumberFormatException e) {
                    continue;
                }

                // 题目规定事务标签范围为 [1, 1000]
                if (tag < 1 || tag > 1000) {
                    continue;
                }

                Integer firstLine = firstLineByTransaction.get(tag);

                if (firstLine == null) {
                    // 当前事务第一次出现
                    firstLineByTransaction.put(tag, i);
                } else {
                    /*
                     * 同一事务的 SQL 行必须保存在同一个文件中,
                     * 因此将当前行和该事务第一次出现的行合并。
                     *
                     * 如果当前一行同时出现 [T1]、[T2],
                     * 那么当前行会分别和 T1、T2 对应的行执行 union,
                     * 从而自动完成 A-B、B-C => A-B-C 的传递约束。
                     */
                    dsu.union(i, firstLine);
                }
            }
        }

        /*
         * 统计每个连通分量包含多少个数组成员。
         * 每个连通分量就是一个绝对不能拆开的 SQL 语句组。
         */
        int[] groupSize = new int[n];

        for (int i = 0; i < n; i++) {
            int root = dsu.find(i);
            groupSize[root]++;
        }

        /*
         * 题目要求按照“语句组首次出现的行号”处理。
         *
         * 不需要额外排序:
         * 直接按照原数组 0、1、2... 顺序扫描,
         * 某个根节点第一次被遇到的位置,
         * 天然就是该语句组首次出现的位置。
         */
        boolean[] processed = new boolean[n];

        int fileCount = 0;
        int usedLines = 0;

        for (int i = 0; i < n; i++) {
            int root = dsu.find(i);

            // 该事务组之前已经处理过
            if (processed[root]) {
                continue;
            }

            processed[root] = true;

            int size = groupSize[root];

            if (fileCount == 0) {
                // 第一个语句组创建第一个文件
                fileCount = 1;
                usedLines = size;
            } else if (usedLines + size <= splitLine) {
                /*
                 * 当前文件能够完整容纳整个语句组,
                 * 根据题目“优先放入当前文件”的要求直接加入。
                 */
                usedLines += size;
            } else {
                /*
                 * 当前文件装不下整个组,只能创建新文件。
                 *
                 * 注意:如果 size 本身已经大于 splitLine,
                 * 根据题目规则也不能拆分,仍然整体放进一个新文件。
                 */
                fileCount++;
                usedLines = size;
            }
        }

        return fileCount;
    }
}

七、效果展示

1、输入

2
;
A;
;;
B;
C;

2、输出

3

3、说明

共有 5 个数组成员,每个成员都按 1 行计算。; 和 ;; 虽然是空语句,但所在数组成员仍然占 1 行。每个文件最多 2 行,因此需要 2 + 2 + 1,共 3 个文件。

在这里插入图片描述


🏆下一篇:华为OD机试 - 简易内存池 - 逻辑分析(Java 新系统 200分)

🏆本专栏收录于《华为OD机试(JAVA)真题》

刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新,全天CSDN在线答疑。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哪 吒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值