今天,我们来看CSP202303C.LDAP这道题目




题意分析
本题要求实现一个 LDAP 用户匹配算法。给定若干用户,每个用户有唯一的 DN(正整数)和若干属性(属性编号和属性值均为正整数)。再给出多个匹配表达式,每个表达式是由原子表达式通过逻辑操作符(& 和 |)组合而成的树形结构。原子表达式形式为 <属性编号><操作符><属性值>,其中操作符 : 表示断言(属性值相等),~ 表示反断言(属性值不等)。逻辑操作符 & 表示两个子表达式均匹配,| 表示至少一个子表达式匹配。
要求对于每个表达式,输出所有匹配用户的 DN,按升序排列。
数据范围 :用户数 n ≤ 2500,表达式数 m ≤ 500,每个用户属性个数不超过 500,属性编号和值均不超过 10^9,表达式长度不超过 2000。
思路
1. 数据结构设计
由于属性编号可能很大且稀疏,不能直接使用数组下标。我们使用 unordered_map 将原始属性编号映射为内部连续的 ID。对于每个属性 ID,维护:
value_to_users:一个unordered_map<int, vector<int>>,记录该属性下特定属性值对应的所有用户 DN 列表。all_users:一个vector<int>,记录具有该属性的所有用户 DN 列表(不论属性值)。
这些列表在构建后都需要排序,以便后续高效地进行集合运算。
2. 表达式解析与求值
表达式符合 BNF 语法,可以使用递归下降解析器。解析过程中构建表达式树,但为了避免指针和内存管理,我们使用节点池 vector<Node>,每个节点存储子节点的索引。
节点结构:
cpp
struct Node {
bool is_atom;
// 原子表达式信息
int attr;
char op;
int val;
// 逻辑表达式信息
char logic;
int left, right;
};
解析函数 parse_expr 返回节点在 nodes 中的索引。遇到逻辑操作符 & 或 | 时,递归解析左右子表达式;遇到数字则解析为原子表达式。
求值函数 evaluate 递归处理节点:
- 原子节点:根据操作符
:或~获取匹配用户列表。 - 逻辑节点:对左右子结果进行交集(
&)或并集(|)操作。
由于用户列表已排序,我们可以实现有序集合的交、并、差运算,时间复杂度为 O(len1 + len2)。
3. 优化:结果缓存
同一个表达式中可能出现相同的原子表达式(例如 &(1:2)(1:2)),或者多次查询中重复出现。为了避免重复计算,使用 unordered_map 对原子表达式结果进行缓存。缓存键由属性内部 ID、操作符和属性值组成(自定义结构体 AtomKey 及其哈希)。
4. 关键操作实现
- 交集:两个有序列表的公共元素。
- 并集:两个有序列表的合并去重。
- 差集 :在有序列表
a中去除有序列表b中出现的元素,用于反断言操作。
5. 复杂度分析
设每个用户最多有 k 个属性,总属性记录数为 N = n * k。
- 构建属性映射和排序:每个属性列表长度不超过该属性出现次数,总体排序复杂度 O(N log N)。
- 每个表达式求值:
- 解析 O(L),L ≤ 2000。
- 原子操作通过缓存和有序列表直接获取,复杂度 O(len) 或 O(1)(若列表已存在)。
- 逻辑操作合并有序列表,总体复杂度与结果大小相关。最坏情况下所有用户都匹配,合并操作 O(n)。
- 整体复杂度足以通过。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
// 属性数据结构
struct AttrData {
unordered_map<int, vector<int>> value_to_users; // 属性值 -> 用户DN列表
vector<int> all_users; // 所有具有该属性的用户DN
};
// 全局变量
unordered_map<int, int> attr_id_map; // 原始属性编号 -> 内部ID
vector<AttrData> attributes; // 按内部ID存储的属性数据
// 原子表达式缓存键
struct AtomKey {
int attr_id;
char op;
int val;
bool operator==(const AtomKey& other) const {
return attr_id == other.attr_id && op == other.op && val == other.val;
}
};
struct AtomKeyHash {
size_t operator()(const AtomKey& k) const {
size_t h1 = hash<int>()(k.attr_id);
size_t h2 = hash<char>()(k.op);
size_t h3 = hash<int>()(k.val);
return h1 ^ (h2 << 1) ^ (h3 << 2);
}
};
unordered_map<AtomKey, vector<int>, AtomKeyHash> atom_cache;
// 表达式树节点(无指针,用索引表示子节点)
struct Node {
bool is_atom;
// 原子
int attr; // 原始属性编号
char op; // ':' 或 '~'
int val; // 属性值
// 逻辑
char logic; // '&' 或 '|'
int left, right; // 子节点索引,-1表示无
Node() : is_atom(false), attr(0), op(0), val(0), logic(0), left(-1), right(-1) {}
static Node make_atom(int a, char o, int v) {
Node node;
node.is_atom = true;
node.attr = a;
node.op = o;
node.val = v;
return node;
}
static Node make_logic(char l, int lc, int rc) {
Node node;
node.is_atom = false;
node.logic = l;
node.left = lc;
node.right = rc;
return node;
}
};
// 解析表达式,nodes为节点池,pos为当前解析位置,返回节点索引
int parse_expr(const string& s, int& pos, vector<Node>& nodes) {
if (pos >= s.size()) return -1;
char c = s[pos];
if (c == '&' || c == '|') {
char logic = c;
pos++; // 跳过逻辑符
pos++; // 跳过 '('
int left_idx = parse_expr(s, pos, nodes);
pos++; // 跳过 ')'
pos++; // 跳过 '('
int right_idx = parse_expr(s, pos, nodes);
pos++; // 跳过 ')'
int idx = nodes.size();
nodes.push_back(Node::make_logic(logic, left_idx, right_idx));
return idx;
} else {
// 解析属性编号
int start = pos;
while (pos < s.size() && isdigit(s[pos])) pos++;
int attr = stoi(s.substr(start, pos - start));
char op = s[pos]; // ':' 或 '~'
pos++;
// 解析属性值
start = pos;
while (pos < s.size() && isdigit(s[pos])) pos++;
int val = stoi(s.substr(start, pos - start));
int idx = nodes.size();
nodes.push_back(Node::make_atom(attr, op, val));
return idx;
}
}
// 差集:a - b(a, b 有序)
vector<int> difference(const vector<int>& a, const vector<int>& b) {
vector<int> res;
size_t i = 0, j = 0;
while (i < a.size()) {
if (j >= b.size() || a[i] < b[j]) {
res.push_back(a[i++]);
} else if (a[i] > b[j]) {
j++;
} else { // 相等
i++;
j++;
}
}
return res;
}
// 交集(a, b 有序)
vector<int> intersection(const vector<int>& a, const vector<int>& b) {
vector<int> res;
size_t i = 0, j = 0;
while (i < a.size() && j < b.size()) {
if (a[i] < b[j]) i++;
else if (a[i] > b[j]) j++;
else {
res.push_back(a[i]);
i++;
j++;
}
}
return res;
}
// 并集(a, b 有序,无重复)
vector<int> union_set(const vector<int>& a, const vector<int>& b) {
vector<int> res;
size_t i = 0, j = 0;
while (i < a.size() || j < b.size()) {
if (j >= b.size() || (i < a.size() && a[i] < b[j])) {
res.push_back(a[i++]);
} else if (i >= a.size() || b[j] < a[i]) {
res.push_back(b[j++]);
} else { // 相等
res.push_back(a[i]);
i++;
j++;
}
}
return res;
}
// 评估表达式树,返回有序用户列表
vector<int> evaluate(int node_idx, const vector<Node>& nodes) {
const Node& node = nodes[node_idx];
if (node.is_atom) {
// 查找属性内部ID
auto it = attr_id_map.find(node.attr);
if (it == attr_id_map.end()) {
return {}; // 没有任何用户具有该属性
}
int attr_id = it->second;
// 构造缓存键
AtomKey key{attr_id, node.op, node.val};
auto cache_it = atom_cache.find(key);
if (cache_it != atom_cache.end()) {
return cache_it->second; // 命中缓存
}
vector<int> result;
const auto& ad = attributes[attr_id];
if (node.op == ':') {
// 断言:值相等
auto vit = ad.value_to_users.find(node.val);
if (vit != ad.value_to_users.end()) {
result = vit->second; // 拷贝
}
} else { // '~'
// 反断言:值不等
auto vit = ad.value_to_users.find(node.val);
if (vit == ad.value_to_users.end()) {
result = ad.all_users; // 所有用户都匹配
} else {
result = difference(ad.all_users, vit->second);
}
}
atom_cache[key] = result;
return result;
} else {
vector<int> left_res = evaluate(node.left, nodes);
vector<int> right_res = evaluate(node.right, nodes);
if (node.logic == '&') {
return intersection(left_res, right_res);
} else { // '|'
return union_set(left_res, right_res);
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
// 读入用户数据
for (int i = 0; i < n; ++i) {
int dn, k;
cin >> dn >> k;
for (int j = 0; j < k; ++j) {
int attr, val;
cin >> attr >> val;
int attr_id;
auto it = attr_id_map.find(attr);
if (it == attr_id_map.end()) {
attr_id = attributes.size();
attr_id_map[attr] = attr_id;
attributes.emplace_back();
} else {
attr_id = it->second;
}
attributes[attr_id].value_to_users[val].push_back(dn);
attributes[attr_id].all_users.push_back(dn);
}
}
// 对所有列表排序
for (auto& ad : attributes) {
sort(ad.all_users.begin(), ad.all_users.end());
for (auto& pair : ad.value_to_users) {
sort(pair.second.begin(), pair.second.end());
}
}
int m;
cin >> m;
cin.ignore(); // 忽略换行符
for (int i = 0; i < m; ++i) {
string expr;
getline(cin, expr);
if (expr.empty()) {
cout << '\n';
continue;
}
vector<Node> nodes; // 本次表达式的节点池
int pos = 0;
int root_idx = parse_expr(expr, pos, nodes);
vector<int> res = evaluate(root_idx, nodes);
// 输出结果
for (size_t j = 0; j < res.size(); ++j) {
if (j > 0) cout << ' ';
cout << res[j];
}
cout << '\n';
}
return 0;
}
总结
本题是一道典型的 表达式解析 + 集合运算 问题。核心要点包括:
- 属性映射与索引:由于属性编号范围大且稀疏,使用哈希表映射到连续内部 ID 以便高效存储。
- 有序列表维护:预排序用户 DN 列表,使得后续交集、并集、差集操作只需线性时间完成。
- 递归解析表达式:基于 BNF 语法构建表达式树,使用节点池而非指针简化内存管理。
- 结果缓存:对原子表达式结果进行缓存,避免重复计算,提升效率。
通过以上设计,算法能够在大数据范围内高效运行,并保持代码清晰易读。