题目链接
题目描述
现有 N
篇文档,每篇文档由若干小写字母单词组成。记第 i
篇文档的单词集合为 (文档编号从 1 开始)。
现给定 M
个查询单词 ,请分别求出每个
所在的文档编号集合。
输入描述:
- 第一行包含一个整数
N
。 - 接下来
N
行,第i
行首先给出整数k_i
(该文档的单词数),随后给出k_i
个单词。 - 接着一行是一个整数
M
(查询次数)。 - 随后
M
行,每行包含一个查询单词。
输出描述:
- 对于每个查询单词,输出一行。
- 按升序序列出所有包含该单词的文档编号,编号间以空格分隔。
- 若该单词未出现在任何文档中,则输出一个空行。
示例: 输入:
3
5 hello world this is test
4 sample test case data
3 test world data
4
test
data
hello
missing
输出:
1 2 3
2 3
1
解题思路
这是一个典型的构建 倒排索引 (Inverted Index) 的问题。倒排索引是一种数据结构,常用于全文搜索,它将内容(在这里是"单词")映射到其所在位置的列表(在这里是"文档编号")。
核心数据结构: 我们需要一个哈希表(Map),它的结构如下:
- 键 (Key): 单词 (string)
- 值 (Value): 一个包含该单词的文档编号的集合 (Set)
使用集合 Set
作为值有两大好处:
- 自动去重: 如果一个单词在同一篇文档中出现多次(如示例2中的"a b a"),集合会自动保证文档编号只被记录一次。
- 有序性: 如果我们使用有序集合(如 C++ 的
std::set
或 Java 的TreeSet
),文档编号在插入时就会自动保持升序,省去了最后排序的步骤。
算法步骤:
- 建立索引:
a. 初始化一个空的哈希表
invertedIndex
。 b. 读取文档总数N
。 c. 循环N
次(docId
从 1 到N
): i. 读取该行文档包含的单词数k
和所有单词。 ii. 对于该文档中的每一个单词word
: - 从invertedIndex
中获取word
对应的集合。如果word
是第一次出现,就创建一个新的空集合。 - 将当前的文档编号docId
加入到这个集合中。 - 处理查询:
a. 读取查询总数
M
。 b. 循环M
次: i. 读取一个查询单词queryWord
。 ii. 在invertedIndex
中查找queryWord
。 iii. 如果找到了对应的文档编号集合: - 遍历该集合(此时已自动有序),并按格式打印所有编号。 iv. 如果没有找到: - 输出一个空行。
代码
#include <iostream>
#include <vector>
#include <string>
#include <map>
#include <set>
#include <sstream>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
map<string, set<int>> invertedIndex;
string line;
getline(cin, line); // Consume the rest of the first line
for (int i = 1; i <= n; ++i) {
getline(cin, line);
stringstream ss(line);
int k;
ss >> k;
string word;
for (int j = 0; j < k; ++j) {
ss >> word;
invertedIndex[word].insert(i);
}
}
int m;
cin >> m;
for (int i = 0; i < m; ++i) {
string queryWord;
cin >> queryWord;
if (invertedIndex.count(queryWord)) {
const auto& doc_ids = invertedIndex[queryWord];
bool first = true;
for (int id : doc_ids) {
if (!first) {
cout << " ";
}
cout << id;
first = false;
}
}
cout << endl;
}
return 0;
}
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
Map<String, NavigableSet<Integer>> invertedIndex = new HashMap<>();
for (int i = 1; i <= n; i++) {
int k = sc.nextInt();
for (int j = 0; j < k; j++) {
String word = sc.next();
invertedIndex.computeIfAbsent(word, key -> new TreeSet<>()).add(i);
}
}
int m = sc.nextInt();
for (int i = 0; i < m; i++) {
String queryWord = sc.next();
Set<Integer> docIds = invertedIndex.get(queryWord);
if (docIds != null && !docIds.isEmpty()) {
StringJoiner sj = new StringJoiner(" ");
for (Integer id : docIds) {
sj.add(id.toString());
}
System.out.println(sj.toString());
} else {
System.out.println();
}
}
sc.close();
}
}
from collections import defaultdict
n = int(input())
# 使用 defaultdict 可以让代码更简洁
inverted_index = defaultdict(set)
for i in range(1, n + 1):
line = input().split()
# 第一个元素是单词数量k,可以忽略,直接遍历单词
words = line[1:]
for word in words:
inverted_index[word].add(i)
m = int(input())
for _ in range(m):
query_word = input()
if query_word in inverted_index:
# sorted() 保证输出升序
result = sorted(list(inverted_index[query_word]))
print(' '.join(map(str, result)))
else:
print()
算法及复杂度
- 算法: 倒排索引 / 哈希表
- 时间复杂度:
,其中
是所有文档中所有单词的总长度(用于构建索引),
是所有查询词的总长度(用于查询)。这可以大致看作与总输入大小成线性关系。
- 空间复杂度:
,其中
是所有唯一单词的总长度,
是单词与文档关联的总数(即
word->docId
映射的总条目数)。