题目描述
这个问题借用了“鱼与熊掌不可兼得”的典故,但设定了一个场景:有些人可以同时拥有多种物品(比如鱼和熊掌)。
我们需要处理以下信息:
1. 有 n
个人和 m
种不同的物品。
2. 给出了每个人所拥有的物品清单。
3. 接下来有 Q
次查询。
4. 每次查询会给出两种物品的编号(例如 x
和 y
)。
5. 对于每次查询 (x, y)
,我们需要计算出有多少人同时拥有物品 x
和物品 y
。
输入格式:
第一行:人数 n
和物品种类数 m
。
接下来 n
行:第 i
行描述第 i
个人(编号从 1 到 n,但代码中通常按 0 到 n-1 处理)拥有的物品。格式为 K M[1] M[2] ... M[K]
,K
是数量,M
是物品编号。
再下一行:查询次数 Q
。
最后 Q
行:每行是两个物品编号 x
和 y
。
输出格式:
* 对每个查询 (x, y)
,输出一行,包含一个整数,即同时拥有物品 x
和 y
的人数。
数据范围:
1≤n≤105 (人数)
2≤m≤105 (物品种类)
0≤K≤103 (每人拥有的物品数量)
所有人 K 的总和 ≤106 (总拥有关系数)
* 1≤Q≤100 (查询次数)
样例
输入样例:
4 8 // 4 个人, 8 种物品
3 4 1 8 // 人 1 (索引 0): 拥有物品 4, 1, 8
4 7 1 8 4 // 人 2 (索引 1): 拥有物品 7, 1, 8, 4
5 6 5 1 2 3 // 人 3 (索引 2): 拥有物品 6, 5, 1, 2, 3
4 3 2 4 8 // 人 4 (索引 3): 拥有物品 3, 2, 4, 8
3 // 3 次查询
2 3 // 查询: 多少人同时拥有物品 2 和 3?
7 6 // 查询: 多少人同时拥有物品 7 和 6?
8 4 // 查询: 多少人同时拥有物品 8 和 4?
输出样例:
2
0
3
样例解释:
查询 (2, 3): 人 3 (索引 2) 拥有 {6, 5, 1, 2, 3};人 4 (索引 3) 拥有 {3, 2, 4, 8}。共有 2 人。
查询 (7, 6): 人 2 (索引 1) 拥有 {7, 1, 8, 4};人 3 (索引 2) 拥有 {6, 5, 1, 2, 3}。没有人同时拥有 7 和 6。共有 0 人。
* 查询 (8, 4): 人 1 (索引 0) 拥有 {4, 1, 8};人 2 (索引 1) 拥有 {7, 1, 8, 4};人 4 (索引 3) 拥有 {3, 2, 4, 8}。共有 3 人。
算法1
(模拟查询 + Set 优化)
思路:
解决这个问题的最直接方法是模拟查询过程:
1. 存储信息: 首先,需要一种高效的方式来存储每个人拥有的物品清单,以便后续快速查询某个人是否拥有特定物品。
2. 处理查询: 对于每一次查询 (x, y)
,遍历所有 n
个人。对每个人,检查他/她是否同时拥有物品 x
和物品 y
。如果满足条件,则计数器加一。遍历完所有人后,输出该查询的计数结果。
数据结构选择:
代码中选择了 vector<set<int>> s(n);
来存储信息。这是一个包含 n
个元素的 vector
,其中每个元素 s[i]
是一个 set<int>
。
vector
的索引 i
(从 0 到 n-1) 代表了第 i+1
个人。
set<int>
用于存储第 i+1
个人拥有的所有物品的编号。选择 set
的优点在于:
* 自动去重:即使输入中同一个人有重复物品(虽然题目保证没有),set
也只会存储一份。
* 快速查找:set
内部通常基于红黑树实现,检查一个元素是否存在(使用 count()
或 find()
方法)的时间复杂度是对数级别的,即 O(log K),其中 K 是该集合中的元素数量(即该人拥有的物品数量)。这比在无序列表(如 vector
)中查找(O(K))要快得多。
代码逻辑详解:
-
读取输入 (人员和物品):
- 读取人数
n
和物品种类数m
。 - 创建
vector<set<int>> s(n);
。 - 循环
n
次,对于第i
个人:- 读取他拥有的物品数量
k
。 - 再循环
k
次,读取每个物品编号x
。 s[i].insert(x);
:将物品x
加入第i
个人的set
中。插入操作的时间复杂度是 O(log K_i),K_i 是当前s[i]
的大小。
- 读取他拥有的物品数量
- 读取人数
-
读取输入 (查询):
- 读取查询次数
q
。
- 读取查询次数
-
处理查询:
- 外层循环
q
次,处理每个查询。 - 读取当前查询的两个物品编号
x
和y
。 - 初始化当前查询的计数器
ans = 0
。 - 内层循环
n
次,遍历编号为j
(从 0 到 n-1) 的每个人。 - 核心判断:
if (s[j].count(x) and s[j].count(y))
s[j].count(x)
:检查第j
个人的set
中是否存在物品x
。count()
返回 1 (存在) 或 0 (不存在)。时间复杂度 O(log K_j)。s[j].count(y)
:同理检查物品y
。时间复杂度 O(log K_j)。and
:逻辑与操作,只有当两个count()
都返回 1 时,条件才为真,表示这个人同时拥有x
和y
。
- 如果条件为真,
ans ++
。 - 内层循环结束后,
ans
就是同时拥有x
和y
的总人数。 cout << ans << "\n";
输出结果。
- 外层循环
时间复杂度
-
读取和存储物品信息:
- 设
TotalK
为所有人拥有的物品总数 (Sum of all K),题目保证TotalK <= 10^6
。 - 设
K_i
为第i
个人拥有的物品数,maxK = max(K_i) <= 1000
。 - 总的插入时间约为 Σ (K_i * log K_i) for i=0 to n-1。
- 一个宽松的上界是
TotalK * log(maxK)
。代入数据:10^6 * log(1000)
≈10^6 * 10
=10^7
。这部分是可接受的。
- 设
-
处理查询:
- 有
Q
次查询 (Q <= 100
)。 - 每次查询需要遍历
n
个人 (n <= 10^5
)。 - 对每个人
j
,进行两次set::count
操作,每次耗时 O(log K_j)。 - 单次查询的总时间约为 Σ O(log K_j) for j=0 to n-1。其上界为
n * log(maxK)
。 Q
次查询的总时间复杂度约为Q * n * log(maxK)
。- 代入数据:
100 * 10^5 * log(1000)
≈100 * 10^5 * 10
=10^8
。
- 有
总时间复杂度 主要由查询部分决定,约为 O(TotalK * log(maxK) + Q * n * log(maxK))。虽然 O(10^8) 看起来比较大,但考虑到 log(maxK)
是个小常数(大约 10),且 Q
只有 100,这个算法在实践中通常可以通过。如果 Q
很大,这个算法可能就会超时。
空间复杂度
- 主要空间开销是存储
vector<set<int>> s
。 - 所有
set
中总共存储了TotalK
个整数。 set
本身有额外开销(例如存储树结构的指针)。- 总空间复杂度大致为 O(n + TotalK)(
n
是 vector 的大小,TotalK
是所有 set 中元素的总数)。代入数据:O(10^5 + 10^6) = O(10^6),这是完全可接受的。
C++ 代码
#include <bits/stdc++.h> // 引入所有标准库头文件
using namespace std; // 使用 std 命名空间
using i64 = long long; // 定义 i64 为 long long 的别名(本题未使用)
int main() {
// 优化 C++ 输入输出流速度
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m; // n: 人数, m: 物品种类数
cin >> n >> m; // 读取 n 和 m
// 创建一个大小为 n 的 vector,每个元素是一个 set<int>
// s[i] 将存储第 i 个人(索引从 0 开始)拥有的物品编号集合
vector<set<int>> s(n);
// 循环读取 n 个人的物品清单
for (int i = 0; i < n; i ++ ) {
int k; // 当前这个人拥有的物品数量
cin >> k; // 读取 k
// 循环读取 k 个物品编号
for (int j = 0; j < k; j ++ ) {
int x; // 物品编号
cin >> x; // 读取 x
s[i].insert(x); // 将物品 x 插入到第 i 个人的 set 中 (自动去重且保持有序)
// 插入操作复杂度 O(log k'),k' 是当前 set 大小
}
}
int q; // 查询次数
cin >> q; // 读取 q
// 循环处理 q 次查询
for (int i = 0; i < q; i ++ ) {
int x, y; // 当前查询的两种物品编号
cin >> x >> y; // 读取 x 和 y
int ans = 0; // 初始化计数器,用于统计同时拥有 x 和 y 的人数
// 循环遍历每一个人 j (索引从 0 到 n-1)
for (int j = 0; j < n; j ++ ) {
// 检查第 j 个人是否同时拥有物品 x 和物品 y
// s[j].count(item) 检查 item 是否在集合 s[j] 中,存在返回 1,否则返回 0
// 时间复杂度 O(log K_j),K_j 是第 j 个人拥有的物品数
if (s[j].count(x) and s[j].count(y)) {
ans ++ ; // 如果同时拥有,计数器加 1
}
}
// 输出本次查询的结果
cout << ans << "\n";
}
return 0; // 程序正常结束
}