- 本提依然是以小白的身份学习大佬的代码,参考自:CSP认证202303-3:LDAP (超详细题解)
解题思路
一、数据结构的定义
1.unordered_map
unordered_map
是 C++ 标准库中的一种关联容器,它提供了基于键值对 的高效查找、插入和删除操作。在这里,使用unordered_map
有几个优势:
- 快速查找 :
unordered_map
的查找时间复杂度是 O(1),本题可以用于根据属性名和属性值查找对应的DN。- 高效插入和删除 :
unordered_map
的插入和删除操作同样是O(1)级别的。- 键值对的唯一性 :
unordered_map
中的键是唯一的,这符合属性名-属性值对应用户集合的关系,每个键对应一个用户集合。- 无序性 :
unordered_map
中的键值对是无序存储的,但对于属性名-属性值对应用户集合的映射关系而言,这通常是没有问题的,因为我们是通过属性名和属性值来进行查找,而不是关心它们的顺序。
2.set
set
是 C++ 标准库中提供的一个有序、不重复的容器。它是基于**红黑树(Red-Black Tree)**实现的,这使得插入、删除和查找元素的操作在平均情况下都是 O(log n) 的时间复杂度。
- 有序性(Ordered) :
set
中的元素是按照升序排列的。插入元素时会根据元素的值进行排序。- 不重复性(Unique) :
set
不允许相同的元素存在,每个元素在集合中是唯一的。- 常见操作的时间复杂度:
- 插入(insert):O(log n)
- 删除(erase):O(log n)
- 查找(find):O(log n)
- 遍历:O(n)
3.数据结构定义
unordered_map<int, unordered_map<int, set<int>>> attrName_attrVal_users;
:存储属性名-属性值-用户DN的映射关系。unordered_map<int, set<int>> attrName_users;
:存储每个属性不为NA的用户DN集合。set<int>
:用于存储用户的唯一标识。
二、 字符串转整型函数
-
思路比较简单,套路也很固定,直接记住就行
cppint getNum(string exp) { int ans = 0; for (int i = 0; i < exp.size(); i++) { ans *= 10; ans += (exp[i] - '0'); } return ans; }
三、 原子操作BASE_EXPR
-
AtomOP
的函数,用于处理原子操作BASE_EXPR
。原子操作包括两种情况:属性值等于给定值(:
)和属性值不等于给定值(~
)。函数接受一个字符串exp
作为参数,该字符串表示原子操作的表达式。cppset<int> AtomOP(string exp) { int p = 0; set<int> ans; while (exp[p] != ':' && exp[p] != '~') p++; // 寻找分隔符 ':' 或 '~' 的位置 // 提取属性名和属性值 int a = getNum(exp.substr(0, p)); int b = getNum(exp.substr(p + 1, exp.size())); if (exp[p] == ':') { // 属性值等于给定值的情况,将对应的用户集合插入到结果集中(没有对应元素插入为空) ans.insert(attrName_attrVal_users[a][b].begin(), attrName_attrVal_users[a][b].end()); } else { // 属性值不等于给定值的情况 ans.insert(attrName_users[a].begin(), attrName_users[a].end()); set<int> dead; // dead 集合用于存储需要从结果集中删除的用户。 // 遍历属性名-属性值映射,删除属性值等于给定值的用户 for (auto it : attrName_attrVal_users[a][b]) { // 循环迭代所有具有属性名为 a 且属性值为 b 的用户DN(用户标识) if (ans.count(it)) { dead.insert(it); } } for (auto it : dead) ans.erase(it); } return ans; }
四、表达式EXPR
操作-递归
- 对
EXPR
的处理采用的是递归的思路,即不断递归,直至找到原子操作并进行处理
1. 基本情况(原子操作)-- 直接调用
cpp
if (exp[0] >= '0' && exp[0] <= '9') {
ans = AtomOP(exp);
}
2. 复杂情况 -- 递归
-
对括号的识别类似对算术表达式中括号的识别,由于括号中的内容只可能是
EXPR
或BASE_EXPR
,因此可以使用递归。 -
接下来就是对左右括号的处理。具体思路是,循环内,每当遇到一个左括号 (,就将其推入栈中;每当遇到一个右括号 ),就从栈中弹出一个左括号。当栈为空时,意味着找到了与初始左括号相匹配的右括号,此时 p 的位置标记了第一个子表达式的结束位置。
cppwhile (!br.empty()) { p++; if (exp[p] == '(') { br.push('('); } else if (exp[p] == ')') { br.pop(); } }
-
通过 ExprOP 函数递归处理第一个子表达式,子表达式的范围从 & 符号后的第一个字符开始,到 p - 2 的位置结束(因为 p 位置是第一个子表达式外的右括号,所以需要 -2 来排除 &( 这两个字符)
cppans1 = ExprOP(exp.substr(2, p - 2));
3. 逻辑与运算
- 合并两个子表达式的结果集合 ans1 和 ans2 中的元素(取交集),实现了逻辑与操作(AND)的逻辑。
cpp
for (auto it : ans1) {
if (ans2.count(it)) {
ans.insert(it);
}
}
4. 逻辑或运算
-
同样按照集合的思路,因为set本身就有去重的性质,因此直接两个子表达式的结果集合 ans1 和 ans2 中的元素相加即可(取并集 ),实现了逻辑或操作(OR)的逻辑。
cppans.insert(ans1.begin(), ans1.end()); ans.insert(ans2.begin(), ans2.end());
完整代码
cpp
#include <iostream>
#include <unordered_map>
#include <set>
#include <stack>
using namespace std;
int n; // 用户数量
unordered_map<int, unordered_map<int, set<int>>> attrName_attrVal_users; // 存储 属性名-属性值-用户DN
unordered_map<int, set<int>> attrName_users; // 存储该属性不为NA的用户DN set
// 字符串转整形
int getNum(string exp) {
int ans = 0;
for (int i = 0; i < exp.size(); i++) {
ans *= 10;
ans += (exp[i] - '0');
}
return ans;
}
// 原子操作
set<int> AtomOP(string exp) {
int p = 0;
set<int> ans;
while (exp[p] != ':' && exp[p] != '~') p++; // 寻找分隔符 ':' 或 '~' 的位置
// 提取属性名和属性值
int a = getNum(exp.substr(0, p));
int b = getNum(exp.substr(p + 1, exp.size()));
if (exp[p] == ':') {
// 属性值等于给定值的情况,将对应的用户集合插入到结果集中(没有对应元素插入为空)
ans.insert(attrName_attrVal_users[a][b].begin(), attrName_attrVal_users[a][b].end());
}
else {
// 属性值不等于给定值的情况
ans.insert(attrName_users[a].begin(), attrName_users[a].end());
set<int> dead; // dead 集合用于存储需要从结果集中删除的用户。
// 遍历属性名-属性值映射,删除属性值等于给定值的用户
for (auto it : attrName_attrVal_users[a][b]) { // 循环迭代所有具有属性名为 a 且属性值为 b 的用户DN(用户标识)
if (ans.count(it)) {
dead.insert(it);
}
}
for (auto it : dead) ans.erase(it);
}
return ans;
}
// 表达式操作
set<int> ExprOP(string exp) {
set<int> ans;
// 基本情况(原子操作),直接调用
if (exp[0] >= '0' && exp[0] <= '9') {
ans = AtomOP(exp);
}
// 逻辑 AND 操作
else if (exp[0] == '&') {
int p = 1;
set<int> ans1, ans2; // ans1 和 ans2 来存储两个子表达式的结果集
stack<char> br;
br.push('(');
/*
循环内,每当遇到一个左括号 (,就将其推入栈中;每当遇到一个右括号 ),
就从栈中弹出一个左括号。当栈为空时,意味着找到了与初始左括号相匹配
的右括号,此时 p 的位置标记了第一个子表达式的结束位置。
*/
while (!br.empty()) {
p++;
if (exp[p] == '(') {
br.push('(');
}
else if (exp[p] == ')') {
br.pop();
}
}
/*
通过 ExprOP 函数递归处理第一个子表达式,子表达式的范围从 & 符号后的第
一个字符开始,到 p - 2 的位置结束(因为 p 位置是第一个子表达式外的右
括号,所以需要 -2 来排除 &( 这两个字符)
*/
ans1 = ExprOP(exp.substr(2, p - 2)); // 对每个子表达式递归调用 ExprOP,直到不再出现"("则进入到基本操作
int q = p + 2; // 跳过第一个子表达式的结束括号和一个额外的空格或字符
br.push('(');
while (!br.empty()) {
q++;
if (exp[q] == '(') {
br.push('(');
}
else if (exp[q] == ')') {
br.pop();
}
}
ans2 = ExprOP(exp.substr(p + 2, q - p - 2));
// 合并两个集合 ans1 和 ans2 中的元素,实现了逻辑与操作(AND)的逻辑
for (auto it : ans1) {
if (ans2.count(it)) { // 这一行检查元素 it 是否也存在于集合 ans2 中。count 方法在这里用于检查指定元素是否存在于集合中。如果存在,count 方法返回 1,否则返回 0
ans.insert(it);
}
}
}
else if (exp[0] == '|') {
int p = 1;
set<int> ans1, ans2;
stack<char> br;
br.push('(');
while (!br.empty()) {
p++;
if (exp[p] == '(') {
br.push('(');
}
else if (exp[p] == ')') {
br.pop();
}
}
ans1 = ExprOP(exp.substr(2, p - 2));
int q = p + 2;
br.push('(');
while (!br.empty()) {
q++;
if (exp[q] == '(') {
br.push('(');
}
else if (exp[q] == ')') {
br.pop();
}
}
ans2 = ExprOP(exp.substr(p + 2, q - p - 2));
// 实现逻辑与操作(AND)的逻辑。
ans.insert(ans1.begin(), ans1.end());
ans.insert(ans2.begin(), ans2.end());
}
return ans;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
int dn;
cin >> dn;
int num;
cin >> num;
for (int j = 1; j <= num; j++) {
int attrName;
cin >> attrName;
int attrVal;
cin >> attrVal;
attrName_attrVal_users[attrName][attrVal].insert(dn);
attrName_users[attrName].insert(dn);
}
}
// 输入操作表达式数量
int m;
cin >> m;
// 处理每个操作表达式
for (int i = 1; i <= m; i++) {
string exp;
cin >> exp;
set<int> ans;
ans = ExprOP(exp);
// 输出结果
for (auto it : ans) {
cout << it << " ";
}
cout << endl;
}
return 0;
}