更多 CSP 认证考试题目题解可以前往:CSP-CCF 认证考试真题题解
原题链接: 202303-3 LDAP
时间限制: 12.0s
内存限制: 1.0GB
题目背景
西西艾弗岛运营公司是一家负责维护和运营岛上基础设施的大型企业,拥有数千名员工。公司内有很多 IT 系统。为了能够实现这些 IT 系统的统一认证登录,公司 IT 部门决定引入一套 LDAP 系统来管理公司内的用户信息。轻型目录访问协议(Lightweight Directory Access Protocol,LDAP)是一种用于访问和维护目录服务的应用层协议,基于它的数据库可以用树形结构来组织和存储数据。每一笔数据,都包含了一个唯一的标识符(DN,Distinguished Name),以及一系列的属性(Attribute)。
不同的 IT 系统,允许访问的用户是不相同的。每个信息系统都有一个表达式,用来描述允许访问的用户。
这个表达式可以按照某一个属性的值作为条件来匹配用户,也可以用多个条件的逻辑组合来匹配用户。
小 C 被安排来实现这样一个算法,给定一个 IT 系统的匹配表达式,找到所有与之匹配的用户的 DN。
问题描述
为了简化该问题,我们约定,每个用户的 DN 是一个正整数,且不会重复。有若干种用户的属性,用正整数编号。每个用户可以具有这些属性中的若干个,且每个属性只能有一个值。每个属性的值也是一个正整数。例如,假定有两个用户:用户 1 和用户 2,他们的 DN 分别是 1 和 2。一共有 3 种属性。用户 1 具有属性 1 和属性 2,且属性 1 的值为 2,属性 2 的值为 3;但不具有属性 3。用户 2 具有属性 2 和属性 3,且属性 2 的值为 3,属性 3 的值为 1;但不具有属性 1。如下表所示:
DN | 属性 1 | 属性 2 | 属性 3 |
---|---|---|---|
1 | 2 | 3 | N/A |
2 | N/A | 3 | 1 |
一个匹配表达式可以是一个属性的值,也可以是多个匹配表达式的逻辑组合。只匹配一个属性的值的表达式称为原子表达式,原子表达式的形式为 <属性编号><操作符><属性值>。其中操作符有两种:断言与反断言。断言操作符为 :,表示匹配具有该属性且值与之相等的用户;反断言操作符为 ~,表示匹配具有该属性且值与之不等的用户。例如,表达式 1:2 可以与上述用户 1 相匹配,但不能与用户 2 相匹配;而表达式 3~1则不能与任何一个用户相匹配。
表达式可以进行逻辑组合,其语法是:<操作符>(表达式 1)(表达式 2)。其中操作符有两种:与(&)和或(|)。如果操作符为与,则当且仅当两个表达式都与某一用户相匹配时,该表达式与该用户相匹配;如果操作符为或,则当且仅当两个表达式中至少有一个与某一用户相匹配时,该表达式与该用户相匹配。例如,表达式 &(1:2)(2:3) 可以与用户 1 相匹配,但不能与用户 2 相匹配;而表达式 |(1:2)(3:1) 则可以与两个用户都相匹配。
形式化地,上述语法用 BNF 范式表示如下:
NON_ZERO_DIGIT = "1" / "2" / "3" / "4" /
"5" / "6" / "7" / "8" / "9"
DIGIT = "0" / NON_ZERO_DIGIT
NUMBER = NON_ZERO_DIGIT / (NON_ZERO_DIGIT DIGIT*)
ATTRIBUTE = NUMBER
VALUE = NUMBER
OPERATOR = ":" / "~"
BASE_EXPR = ATTRIBUTE OPERATOR VALUE
LOGIC = "&" / "|"
EXPR = BASE_EXPR / (LOGIC "(" EXPR ")" "(" EXPR ")")
EASY_EXPR = BASE_EXPR /
(LOGIC "(" BASE_EXPR ")" "(" BASE_EXPR ")")
输入格式
从标准输入读入数据。
输入的第一行包含一个正整数 n n n,表示用户的数目。
接下来 n n n 行,每行包含空格分隔的若干个正整数,第一个正整数表示该用户的 DN,第二个正整数表示该用户具有的属性个数,此后的每两个正整数表示该用户具有的一个属性及其值。这些属性按照属性编号从小到大的顺序给出。
接下来一行包含一个正整数 m m m,表示匹配表达式的数目。
接下来 m m m 行,每行包含一个匹配表达式。
输出格式
输出到标准输出。
输出 m m m 行,每行包含零个或多个正整数,用空格分隔,表示与对应的匹配表达式相匹配的用户的 DN,由小到大排序。
样例输入1
2
1 2 1 2 2 3
2 2 2 3 3 1
4
1:2
3~1
&(1:2)(2:3)
|(1:2)(3:1)
样例输出1
1
1
1 2
样例解释
本组输入是题目描述中的例子。
子任务
对于 20% 的输入,有 1 ≤ n ≤ 100 1 \le n \le 100 1≤n≤100, 1 ≤ m ≤ 10 1 \le m \le 10 1≤m≤10,每个用户的属性个数不超过 10,全部属性编号不超过 100,且表达式是原子表达式,即符合 BNF 语法 BASE_EXPR。
对于 40% 的输入,有 1 ≤ m ≤ 100 1 \le m \le 100 1≤m≤100,每个用户的属性个数不超过 10,全部属性编号不超过 100,且表达式中至多含有两个原子表达式的逻辑组合,即符合 BNF 语法 EASY_EXPR。
对于 70% 的输入,有全部属性编号不超过 500。
对于全部输入,有 1 ≤ n ≤ 2500 1 \le n \le 2500 1≤n≤2500, 1 ≤ m ≤ 500 1 \le m \le 500 1≤m≤500,每个用户的属性个数不超过 500,全部属性编号、属性值和 DN 均不超过 1 0 9 10^9 109,每个表达式语句都符合题设语法,且语句字符长度不超过 2000。
题解
对于一个表达式,根据 BNF 范式的规定,第一个字符只有三种情况:&
、|
和数字。
- 如果是数字,则已经到达原子表达式;
- 如果是
&
或|
,则为表达式的逻辑组合。
运用递归的算法实现,定义函数 bitset<N> calc(int l, int r)
表示从语句字符的第
l
l
l 位到第
r
r
r 位中选中的用户,返回值为一个 bitset
,如果第
i
i
i 位为
1
1
1,则代表第
i
i
i 位用户被选中,否则没选中。
对于原子表达式,直接遍历所有有该属性值的用户,判断是否满足条件。满足条件的给对应的位置上置 1 1 1。
对于表达式的逻辑组合,首先要将两个表达式拆分开来。由于表达式是由括号括起来的,可以在读入语句字符串后先对串执行括号匹配算法,找到相对应的括号,这样很容易把表达式拆分开来。拆分完后直接递归调用函数即可,并将两个函数的返回值按照要求进行 bitset
的与或操作。
由于 DN 的值很大,而
n
n
n 的值很小,可以先将读入顺序作为 DN 找到最后输出的是哪些,最后输出前再转换为 DN 即可,记得要 sort
。
时间复杂度:
O
(
m
n
2
k
w
)
\mathcal{O}\left(\dfrac{mn^2k}{w}\right)
O(wmn2k),其中
k
k
k 为 BNF 范式规约过程中出现的 EXPR
的个数,
w
w
w 为计算机位数(通常为
32
32
32 或者
64
64
64)。
参考代码(4.984s,70.63MB)
/*
Created by Pujx on 2024/3/19.
*/
#pragma GCC optimize(2, 3, "Ofast", "inline")
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
//#define int long long
//#define double long double
using i64 = long long;
using ui64 = unsigned long long;
using i128 = __int128;
#define inf (int)0x3f3f3f3f3f3f3f3f
#define INF 0x3f3f3f3f3f3f3f3f
#define yn(x) cout << (x ? "yes" : "no") << endl
#define Yn(x) cout << (x ? "Yes" : "No") << endl
#define YN(x) cout << (x ? "YES" : "NO") << endl
#define mem(x, i) memset(x, i, sizeof(x))
#define cinarr(a, n) for (int i = 1; i <= n; i++) cin >> a[i]
#define cinstl(a) for (auto& x : a) cin >> x;
#define coutarr(a, n) for (int i = 1; i <= n; i++) cout << a[i] << " \n"[i == n]
#define coutstl(a) for (const auto& x : a) cout << x << ' '; cout << endl
#define all(x) (x).begin(), (x).end()
#define md(x) (((x) % mod + mod) % mod)
#define ls (s << 1)
#define rs (s << 1 | 1)
#define ft first
#define se second
#define pii pair<int, int>
#ifdef DEBUG
#include "debug.h"
#else
#define dbg(...) void(0)
#endif
const int N = 2500 + 5;
//const int M = 1e5 + 5;
const int mod = 998244353;
//const int mod = 1e9 + 7;
//template <typename T> T ksm(T a, i64 b) { T ans = 1; for (; b; a = 1ll * a * a, b >>= 1) if (b & 1) ans = 1ll * ans * a; return ans; }
//template <typename T> T ksm(T a, i64 b, T m = mod) { T ans = 1; for (; b; a = 1ll * a * a % m, b >>= 1) if (b & 1) ans = 1ll * ans * a % m; return ans; }
int dn[N];
int n, m, t, k, q;
unordered_map<int, int> a[N];
unordered_map<int, vector<int>> have;
string s;
int x[N];
bitset<N> calc(int l, int r) {
if (s[l] == '&' || s[l] == '|') {
auto L = calc(l + 2, x[l + 1] - 1);
auto R = calc(x[r] + 1, r - 1);
if (s[l] == '&') return L & R;
else return L | R;
}
else {
int posi = -1;
for (int i = l; i <= r; i++)
if (s[i] == ':' || s[i] == '~') posi = i;
int x = stoi(s.substr(l, posi - l)), y = stoi(s.substr(posi + 1, r - posi));
bitset<N> bt(0);
for (auto i : have[x]) {
if (s[posi] == ':' && a[i][x] == y) bt[i] = 1;
if (s[posi] == '~' && a[i][x] != y) bt[i] = 1;
}
return bt;
}
}
void work() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> dn[i] >> t;
while (t--) {
int x, y;
cin >> x >> y;
a[i][x] = y;
have[x].emplace_back(i);
}
}
cin >> m;
while (m--) {
cin >> s;
stack<int> st;
for (int i = 0; i < s.length(); i++) {
if (s[i] == '(') st.push(i);
else if (s[i] == ')') x[st.top()] = i, x[i] = st.top(), st.pop();
}
auto bt = calc(0, s.length() - 1);
vector<int> ans;
for (int i = bt._Find_first(); i < N; i = bt._Find_next(i))
ans.emplace_back(dn[i]);
sort(ans.begin(), ans.end());
coutstl(ans);
}
}
signed main() {
#ifdef LOCAL
freopen("C:\\Users\\admin\\CLionProjects\\Practice\\data.in", "r", stdin);
freopen("C:\\Users\\admin\\CLionProjects\\Practice\\data.out", "w", stdout);
#endif
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int Case = 1;
//cin >> Case;
while (Case--) work();
return 0;
}
/*
_____ _ _ _ __ __
| _ \ | | | | | | \ \ / /
| |_| | | | | | | | \ \/ /
| ___/ | | | | _ | | } {
| | | |_| | | |_| | / /\ \
|_| \_____/ \_____/ /_/ \_\
*/
关于代码的亿点点说明:
- 代码的主体部分位于
void work()
函数中,另外会有部分变量申明、结构体定义、函数定义在上方。#pragma ...
是用来开启 O2、O3 等优化加快代码速度。- 中间一大堆
#define ...
是我习惯上的一些宏定义,用来加快代码编写的速度。"debug.h"
头文件是我用于调试输出的代码,没有这个头文件也可以正常运行(前提是没定义DEBUG
宏),在程序中如果看到dbg(...)
是我中途调试的输出的语句,可能没删干净,但是没有提交上去没有任何影响。ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
这三句话是用于解除流同步,加快输入cin
输出cout
速度(这个输入输出流的速度很慢)。在小数据量无所谓,但是在比较大的读入时建议加这句话,避免读入输出超时。如果记不下来可以换用scanf
和printf
,但使用了这句话后,cin
和scanf
、cout
和printf
不能混用。- 将
main
函数和work
函数分开写纯属个人习惯,主要是为了多组数据。