
专栏导读
本专栏收录于《华为OD机试真题(Python/JS/C/C++)》。
刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新。
一、题目描述
某分布式数据库 Q 系统需要将 SQL 操作日志拆分到多个文件中,现给定一组 SQL 语句输入(数组格式),请按要求将 SQL 语句拆分到不同文件中并返回拆分后的文件数量。拆分规则如下:
- 单个数组成员的 SQL 语句(可能包含多条,按
;分隔)按同一行完整保存,不可跨文件拆分,且同一行的语句必须在同一个文件中。 - 部分语句带事务标签
[Tn](n为正整数),如[T1]A;相同事务标签的 SQL 语句必须保存于同一文件中。 - 当单个文件保存的行数超出限制时,需要新建文件;按语句组首次出现的行号顺序处理,优先放入当前文件,超出限制则新建文件;
- 特殊场景约束:若语句 A 与语句 B 必须同文件,语句 B 与语句 C 必须同文件,则语句 A、B、C 必须在同一文件(约束传递性)。约束合并后语句组计数即便超过限制也必须放在同一文件;
- 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 为事务标签数量。
六、Python算法源码
import sys
import re
class DSU:
"""
并查集:
用于维护由于事务标签约束而必须放入同一个文件的 SQL 行。
"""
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
"""
查找当前节点所在集合的根节点。
路径压缩后,后续查询的效率会更高。
"""
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, a, b):
"""
将两个 SQL 行所在的集合进行合并。
"""
root_a = self.find(a)
root_b = self.find(b)
if root_a == root_b:
return
# 按秩合并,尽量降低并查集树的高度
if self.rank[root_a] < self.rank[root_b]:
self.parent[root_a] = root_b
elif self.rank[root_a] > self.rank[root_b]:
self.parent[root_b] = root_a
else:
self.parent[root_b] = root_a
self.rank[root_a] += 1
def split_file_count(split_line, sql_text):
# split_line 非法或者没有 SQL 数据,直接返回 0
if split_line < 1 or split_line > 10000 or not sql_text:
return 0
n = len(sql_text)
# 每一个 sql_text 成员对应一个不可拆分的 SQL 行
dsu = DSU(n)
"""
key : 事务编号,例如 [T10] 对应 10
value : 该事务第一次出现的 SQL 行下标
"""
first_line_by_transaction = {}
# 匹配 [T1]、[T20]、[T1000] 等事务标签
tag_pattern = re.compile(r'\[T(\d+)\]')
for i, line in enumerate(sql_text):
for matcher in tag_pattern.finditer(line):
tag = int(matcher.group(1))
# 事务编号只处理 [1, 1000]
if tag < 1 or tag > 1000:
continue
if tag not in first_line_by_transaction:
# 当前事务第一次出现
first_line_by_transaction[tag] = i
else:
"""
相同事务标签的 SQL 行必须位于同一个文件。
当前行如果同时出现多个事务标签,
会分别执行多次 union,从而自动实现约束传递:
A 与 B 同组,B 与 C 同组
=> A、B、C 最终属于同一个并查集。
"""
dsu.union(
i,
first_line_by_transaction[tag]
)
# group_size[root] 表示一个不可拆分 SQL 组一共有多少行
group_size = [0] * n
for i in range(n):
root = dsu.find(i)
group_size[root] += 1
"""
按原始 SQL 行号从前向后扫描。
第一次遇到某个根节点时,就相当于按照
“该 SQL 组第一次出现的行号”进行处理。
"""
processed = [False] * n
file_count = 0
used_lines = 0
for i in range(n):
root = dsu.find(i)
if processed[root]:
continue
processed[root] = True
size = group_size[root]
if file_count == 0:
# 第一个 SQL 组创建第一个文件
file_count = 1
used_lines = size
elif used_lines + size <= split_line:
# 当前文件能够完整容纳该 SQL 组
used_lines += size
else:
"""
当前文件不能完整容纳该 SQL 组,需要创建新文件。
即使 size 本身大于 split_line,也不能拆分事务组。
"""
file_count += 1
used_lines = size
return file_count
def main():
# splitlines() 可以正确处理 Windows/Linux 换行符
lines = sys.stdin.read().splitlines()
if not lines:
return
split_line = int(lines[0].strip())
sql_text = []
"""
严格保持原 Java main 方法的行为:
后续行如果不包含 ";",立即结束读取;
当前不包含 ";" 的这一行不会加入 sql_text。
"""
for line in lines[1:]:
if ';' not in line:
break
sql_text.append(line)
print(split_file_count(split_line, sql_text))
if __name__ == '__main__':
main()
七、JavaScript算法源码
const fs = require('fs');
class DSU {
constructor(n) {
this.parent = Array.from(
{ length: n },
(_, i) => i
);
this.rank = new Array(n).fill(0);
}
/**
* 查找节点所在集合的根节点。
* 使用路径压缩优化后续查询。
*/
find(x) {
if (this.parent[x] !== x) {
this.parent[x] =
this.find(this.parent[x]);
}
return this.parent[x];
}
/**
* 合并两个 SQL 行所在的集合。
*/
union(a, b) {
let rootA = this.find(a);
let rootB = this.find(b);
if (rootA === rootB) {
return;
}
// 按秩合并,避免树结构过深
if (this.rank[rootA] < this.rank[rootB]) {
this.parent[rootA] = rootB;
} else if (
this.rank[rootA] > this.rank[rootB]
) {
this.parent[rootB] = rootA;
} else {
this.parent[rootB] = rootA;
this.rank[rootA]++;
}
}
}
function splitFileCount(splitLine, sqlText) {
// 参数非法或 SQL 数组为空
if (
splitLine < 1 ||
splitLine > 10000 ||
sqlText.length === 0
) {
return 0;
}
const n = sqlText.length;
const dsu = new DSU(n);
/*
* key : 事务编号
* value : 该事务第一次出现的行号
*/
const firstLineByTransaction = new Map();
// 匹配 [T1]、[T12]、[T1000]
const tagPattern = /\[T(\d+)\]/g;
for (let i = 0; i < n; i++) {
/*
* JavaScript 的全局正则具有 lastIndex 状态,
* 每处理新的一行之前必须重置。
*/
tagPattern.lastIndex = 0;
let matcher;
while (
(matcher = tagPattern.exec(sqlText[i])) !== null
) {
const tag = Number(matcher[1]);
if (tag < 1 || tag > 1000) {
continue;
}
if (!firstLineByTransaction.has(tag)) {
// 当前事务第一次出现
firstLineByTransaction.set(tag, i);
} else {
/*
* 相同事务标签必须同文件,因此合并。
*
* 如果当前 SQL 行同时出现 [T1] 和 [T2],
* 当前行会分别连接 T1、T2 的已有 SQL 行,
* 因此并查集能够自然实现约束的传递性。
*/
dsu.union(
i,
firstLineByTransaction.get(tag)
);
}
}
}
// 统计每个不可拆分 SQL 组的行数
const groupSize = new Array(n).fill(0);
for (let i = 0; i < n; i++) {
const root = dsu.find(i);
groupSize[root]++;
}
/*
* 按原 SQL 行号顺序处理。
* 一个根节点第一次被访问的位置,
* 就是该 SQL 组第一次出现的位置。
*/
const processed = new Array(n).fill(false);
let fileCount = 0;
let usedLines = 0;
for (let i = 0; i < n; i++) {
const root = dsu.find(i);
if (processed[root]) {
continue;
}
processed[root] = true;
const size = groupSize[root];
if (fileCount === 0) {
fileCount = 1;
usedLines = size;
} else if (
usedLines + size <= splitLine
) {
// 当前文件能够完整容纳该 SQL 组
usedLines += size;
} else {
/*
* 当前文件装不下完整 SQL 组,
* 必须创建新文件。
*
* 即使 size > splitLine,
* 根据事务约束仍然不能拆开。
*/
fileCount++;
usedLines = size;
}
}
return fileCount;
}
// 读取标准输入
let input = fs.readFileSync(0, 'utf8');
// 统一 Windows/Linux 换行符
input = input
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n');
let lines = input.split('\n');
/*
* 文件最后通常会有一个换行符。
* split('\n') 会因此产生一个额外的空字符串,
* 这个空字符串并不是 Scanner.nextLine() 实际读取的一行,
* 所以需要删除。
*/
if (
lines.length > 0 &&
lines[lines.length - 1] === ''
) {
lines.pop();
}
if (lines.length > 0) {
const splitLine = Number(lines[0].trim());
const sqlText = [];
/*
* 严格保持 Java 原代码行为:
* 遇到不包含 ";" 的一行,直接停止。
*/
for (let i = 1; i < lines.length; i++) {
if (!lines[i].includes(';')) {
break;
}
sqlText.push(lines[i]);
}
console.log(
splitFileCount(splitLine, sqlText)
);
}
八、C算法源码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#define MAX_LINE_LEN 1105
/**
* 并查集结构。
*/
typedef struct {
int *parent;
int *rankValue;
} DSU;
/**
* 初始化并查集。
*/
void dsuInit(DSU *dsu, int n) {
dsu->parent =
(int *)malloc(
sizeof(int) * n
);
dsu->rankValue =
(int *)calloc(
n,
sizeof(int)
);
for (int i = 0; i < n; ++i) {
dsu->parent[i] = i;
}
}
/**
* 查找根节点,并进行路径压缩。
*/
int dsuFind(DSU *dsu, int x) {
if (dsu->parent[x] != x) {
dsu->parent[x] =
dsuFind(
dsu,
dsu->parent[x]
);
}
return dsu->parent[x];
}
/**
* 合并两个 SQL 行所在集合。
*/
void dsuUnion(
DSU *dsu,
int a,
int b
) {
int rootA = dsuFind(dsu, a);
int rootB = dsuFind(dsu, b);
if (rootA == rootB) {
return;
}
// 按秩合并
if (
dsu->rankValue[rootA] <
dsu->rankValue[rootB]
) {
dsu->parent[rootA] = rootB;
} else if (
dsu->rankValue[rootA] >
dsu->rankValue[rootB]
) {
dsu->parent[rootB] = rootA;
} else {
dsu->parent[rootB] = rootA;
++dsu->rankValue[rootA];
}
}
/**
* 删除 fgets 读取进来的 \\n / \\r。
*/
void removeNewLine(char *str) {
size_t len = strlen(str);
while (
len > 0 &&
(
str[len - 1] == '\n' ||
str[len - 1] == '\r'
)
) {
str[--len] = '\0';
}
}
/**
* 扫描一行字符串中的所有 [Tn] 标签。
*
* C 标准库没有直接对应 Java Pattern 的正则接口,
* 因此直接寻找:
*
* '[' -> 'T' -> 一段数字 -> ']'
*/
void processTags(
const char *line,
int row,
DSU *dsu,
int firstLine[1001]
) {
size_t len = strlen(line);
for (
size_t i = 0;
i + 3 < len;
++i
) {
// 必须以 "[T" 开头
if (
line[i] != '[' ||
line[i + 1] != 'T'
) {
continue;
}
size_t j = i + 2;
// T 后面至少要有一位数字
if (
j >= len ||
!isdigit(
(unsigned char)line[j]
)
) {
continue;
}
long tag = 0;
while (
j < len &&
isdigit(
(unsigned char)line[j]
)
) {
tag =
tag * 10 +
(line[j] - '0');
++j;
}
/*
* 合法事务标签必须:
* 1. 数字后面紧跟 ']'
* 2. 编号范围为 [1,1000]
*/
if (
j < len &&
line[j] == ']' &&
tag >= 1 &&
tag <= 1000
) {
if (firstLine[tag] == -1) {
// 当前事务第一次出现
firstLine[tag] = row;
} else {
/*
* 相同事务标签必须位于同一个文件,
* 因此合并当前行和事务第一次出现的行。
*
* 同一 SQL 行中存在多个标签时,
* 会发生多次合并,实现约束的传递性。
*/
dsuUnion(
dsu,
row,
firstLine[tag]
);
}
i = j;
}
}
}
/**
* 计算需要拆分出的文件数量。
*/
int splitFileCount(
int splitLine,
char **sqlText,
int n
) {
if (
splitLine < 1 ||
splitLine > 10000 ||
n == 0
) {
return 0;
}
DSU dsu;
dsuInit(&dsu, n);
/*
* firstLine[t]:
* 事务 Tt 第一次出现的行号。
*/
int firstLine[1001];
for (int i = 0; i <= 1000; ++i) {
firstLine[i] = -1;
}
for (int i = 0; i < n; ++i) {
processTags(
sqlText[i],
i,
&dsu,
firstLine
);
}
// 统计每个不可拆分 SQL 组的行数
int *groupSize =
(int *)calloc(
n,
sizeof(int)
);
for (int i = 0; i < n; ++i) {
int root =
dsuFind(&dsu, i);
++groupSize[root];
}
/*
* 标记一个并查集是否已经进行过文件分配。
*/
unsigned char *processed =
(unsigned char *)calloc(
n,
sizeof(unsigned char)
);
int fileCount = 0;
int usedLines = 0;
for (int i = 0; i < n; ++i) {
int root =
dsuFind(&dsu, i);
if (processed[root]) {
continue;
}
processed[root] = 1;
int size = groupSize[root];
if (fileCount == 0) {
fileCount = 1;
usedLines = size;
} else if (
usedLines + size <= splitLine
) {
// 当前文件还能完整容纳当前 SQL 组
usedLines += size;
} else {
/*
* 无法完整放入当前文件,创建新文件。
* 即使 size > splitLine,也不允许拆事务组。
*/
++fileCount;
usedLines = size;
}
}
free(dsu.parent);
free(dsu.rankValue);
free(groupSize);
free(processed);
return fileCount;
}
int main(void) {
char buffer[MAX_LINE_LEN];
// 读取 splitLine
if (
fgets(
buffer,
sizeof(buffer),
stdin
) == NULL
) {
return 0;
}
removeNewLine(buffer);
int splitLine = atoi(buffer);
/*
* 动态保存 SQL 行。
* 当容量不足时按 2 倍进行扩容。
*/
int capacity = 16;
int n = 0;
char **sqlText =
(char **)malloc(
sizeof(char *) * capacity
);
while (
fgets(
buffer,
sizeof(buffer),
stdin
) != NULL
) {
removeNewLine(buffer);
/*
* 与 Java 原代码保持一致:
* 当前行只要不包含 ";" 就直接停止。
*/
if (
strchr(buffer, ';') == NULL
) {
break;
}
if (n == capacity) {
capacity *= 2;
sqlText =
(char **)realloc(
sqlText,
sizeof(char *) * capacity
);
}
/*
* 为当前 SQL 行单独申请空间,
* 保存完整内容,不能根据 ';' 拆分。
*/
sqlText[n] =
(char *)malloc(
strlen(buffer) + 1
);
strcpy(sqlText[n], buffer);
++n;
}
printf(
"%d\n",
splitFileCount(
splitLine,
sqlText,
n
)
);
for (int i = 0; i < n; ++i) {
free(sqlText[i]);
}
free(sqlText);
return 0;
}
九、C++算法源码
#include <iostream>
#include <vector>
#include <string>
#include <regex>
#include <unordered_map>
using namespace std;
/**
* 并查集:
* 维护哪些 SQL 行由于事务标签约束而必须同文件。
*/
class DSU {
private:
vector<int> parent;
vector<int> rankValue;
public:
explicit DSU(int n)
: parent(n), rankValue(n, 0) {
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 unite(int a, int b) {
int rootA = find(a);
int rootB = find(b);
if (rootA == rootB) {
return;
}
// 按秩合并,避免并查集退化
if (rankValue[rootA] < rankValue[rootB]) {
parent[rootA] = rootB;
} else if (
rankValue[rootA] > rankValue[rootB]
) {
parent[rootB] = rootA;
} else {
parent[rootB] = rootA;
++rankValue[rootA];
}
}
};
int splitFileCount(
int splitLine,
const vector<string>& sqlText
) {
// split_line 非法或没有 SQL 数据
if (
splitLine < 1 ||
splitLine > 10000 ||
sqlText.empty()
) {
return 0;
}
int n = static_cast<int>(sqlText.size());
DSU dsu(n);
/*
* key : 事务编号
* value : 该事务第一次出现的 SQL 行号
*/
unordered_map<int, int>
firstLineByTransaction;
// 匹配 [T1]、[T10]、[T1000]
regex tagPattern(R"(\[T(\d+)\])");
for (int i = 0; i < n; ++i) {
sregex_iterator begin(
sqlText[i].begin(),
sqlText[i].end(),
tagPattern
);
sregex_iterator end;
for (
auto it = begin;
it != end;
++it
) {
int tag =
stoi((*it)[1].str());
if (tag < 1 || tag > 1000) {
continue;
}
auto pos =
firstLineByTransaction.find(tag);
if (
pos ==
firstLineByTransaction.end()
) {
// 当前事务第一次出现
firstLineByTransaction[tag] = i;
} else {
/*
* 同一个事务标签对应的 SQL 行必须同文件。
*
* 如果一行同时含有多个事务标签,
* 会执行多次 unite,从而通过当前行
* 把不同事务集合连接起来,实现传递约束。
*/
dsu.unite(
i,
pos->second
);
}
}
}
// 统计每个不可拆分 SQL 组的总行数
vector<int> groupSize(n, 0);
for (int i = 0; i < n; ++i) {
int root = dsu.find(i);
++groupSize[root];
}
/*
* 顺序遍历原始 SQL 行。
* 第一次遇到某个根节点时,
* 就是对应 SQL 组第一次出现的位置。
*/
vector<bool> processed(n, false);
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;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string firstLine;
if (!getline(cin, firstLine)) {
return 0;
}
int splitLine = stoi(firstLine);
vector<string> sqlText;
string input;
/*
* 与 Java 代码保持一致:
* 遇到不包含 ";" 的行立即终止读取,
* 且该行不会加入 sqlText。
*/
while (getline(cin, input)) {
if (
input.find(';') == string::npos
) {
break;
}
sqlText.push_back(input);
}
cout
<< splitFileCount(
splitLine,
sqlText
)
<< '\n';
return 0;
}
🏆下一篇:华为OD机试真题 - 简易内存池(Python/JS/C/C++ 新系统 200分)
🏆本文收录于,华为OD机试真题(Python/JS/C/C++)
刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新。

214

被折叠的 条评论
为什么被折叠?



