更多 CSP 认证考试题目题解可以前往:CSP-CCF 认证考试真题题解
原题链接: 202406-3 文本分词
时间限制: 1.5 秒
空间限制: 512 MiB
题目背景
西西艾弗岛大数据中心正在如火如荼地开展大语言模型的研究工作。众所周知,计算机在执行机器学习的任务时,更适合处理数字的数据。对于大语言文本的处理, 最好的方式是将文本转化为数字,然后再进行处理。通常,对输入数据进行整理后,需要将其按照一定的规则进行编码,以便计算机能够更好地处理。
这一转换过程是通过词汇表进行的。词汇表是一个包含了所有可能出现的词的列表。对于一个给定的文本,可以按照该表格将文本转化为一个数字的序列。 而词表也需要根据文本的特点进行设计,以便更好地反映文本的特点。
题目描述
词汇表包括一系列的字符串,可以用于将输入的文本转换为数字序列。这里,我们认为输入文本事先经过一定的处理,将字母统一转换为了小写字母。词汇表的生成过程是一个迭代的过程。首先将文本按照一定的规则切分为单词序列, 并统计全部单词的出现频率。然后将单词拆分为单字母字符串,组成初始的词汇表。接下来根据词汇表中的词汇接连出现在单词中的频率,将词汇反复合并,组成更长的词汇加入到词汇表中。 词汇表的具体生成过程如下:
首先,输入文本中所有的单词和其出现的频率。然后,统计其中所有的字符,将其按照字典序排序,并将这些字符作为单字母字符串加入到词汇表中。同时,将输入的单词相应切分为词汇序列。
例如,输入下列词组和频率:
none
cut 15
cute 10
but 6
execute 3
则执行完上述过程后,词汇表中包含了 b
、c
、e
、t
、u
、x
这些单字母字符串,而输入的词组被切分为:
none
c u t 15
c u t e 10
b u t 6
e x e c u t e 3
接下来,统计词汇表中,两个词汇组成的"词汇对"相连出现的频率,并选取出现次数最多的那一组拼接为一个字符串加入词汇表中。如果存在多个这样的"词汇对",则再按照如下优先级顺序选取:
- 选取拼接后的字符串长度最短的那一组;
- 选取"词汇对"中前一个词汇长度最短的那一组;
- 选取拼接后的字符串字典序最小的那一组。
同时生成对应的合并规则(即将选出的"词汇对"合并成一个词汇),并按照该规则将所有输入单词的词汇序列按从前到后的顺序依次加以合并。
例如,在上述单词列表中词汇组合 <c, u>
在单词 cut
、 cute
和 execute
中分别出现了 15、10 和 3 次。相应统计全部的"词汇对"出现次数如下:
none
c u 28
u t 34
t e 13
b u 6
e x 3
x e 3
e c 3
于是,将 ut
加入词汇表中,并生成合并规则 <u, t>
,可得到词汇表 b
、c
、e
、t
、u
、x
、ut
。同时,将输入的单词切分为:
none
c ut 15
c ut e 10
b ut 6
e x e c ut e 3
上述过程可以重复进行。例如,继续统计"词汇对"出现的频率如下:
none
c ut 28
ut e 13
b ut 6
e x 3
x e 3
e c 3
这时,将 cut
加入词汇表中,并生成合并规则 <c, ut>
,可得到词汇表 b
、c
、e
、t
、u
、x
、ut
、cut
。同时,将输入的单词切分为:
none
cut 15
cut e 10
b ut 6
e x e cut e 3
词汇表的生成,需要重复进行上述过程,直到词汇表达到指定的长度,或者所有输入的单词都被合并为一个词汇。
此外需要注意,一种特殊情况是选取的"词汇对"由两个相同的词汇组成。例如按"词汇对"<a, a>
进行合并时,由于从前到后的顺序要求,序列 a a a
会被合并为 aa a
,而序列 a a a a
则会被合并为 aa aa
。
输入格式
从标准输入读入数据。
输入的第一行包含两个正整数, n n n 和 m m m,分别表示输入的单词的数量和期望的词汇表长度。
接下来的 n n n 行,每行包含一个非空字符串 s s s 和一个正整数 f f f,表示输入的单词和其出现的频率。其中, s s s 中只包含小写字母。
输出格式
输出共 m m m 行,每行包含一个字符串,按照加入词汇表的顺序输出词汇表中所有词汇。
样例1输入
plain
4 8
cut 15
cute 10
but 6
execute 3
样例1输出
plain
b
c
e
t
u
x
ut
cut
样例1解释
该样例即为题目描述中所举的例子。
子任务
对 20% 的数据,有 n ≤ 200 n \leq 200 n≤200, m m m 恰好等于输入单词中出现的所有字母的个数。
对 40% 的数据,有 n ≤ 200 n \leq 200 n≤200, m ≤ 200 m \leq 200 m≤200。
对 80% 的数据,有 n ≤ 2000 n \leq 2000 n≤2000, m ≤ 2500 m \leq 2500 m≤2500。
对 100% 的数据,有 n ≤ 10000 n \leq 10000 n≤10000, m ≤ 5000 m \leq 5000 m≤5000 且大于等于输入单词中出现的所有字母的个数, s s s 的长度 ∣ s ∣ ≤ 25 |s| \leq 25 ∣s∣≤25,输入单词的总频率( f f f 的总和)不超过 1 0 6 10^6 106。文本取材于真实的英文著作。
题解
待补
时间复杂度: O ( n ∣ s ∣ log ( n ∣ s ∣ ) ) \mathcal{O}(n|s|\log(n|s|)) O(n∣s∣log(n∣s∣))。
参考代码(164ms,10344KB)
cpp
/*
Created by Pujx on 2024/7/18.
*/
#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 _ = 1; _ <= n; _++) cin >> a[_]
#define cinstl(a) for (auto& _ : a) cin >> _
#define coutarr(a, n) for (int _ = 1; _ <= n; _++) cout << a[_] << " \n"[_ == n]
#define coutstl(a) for (const auto& _ : a) cout << _ << ' '; cout << endl
#define all(x) (x).begin(), (x).end()
#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 = 2e5 + 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 a[N];
int n, m, t, k, q;
vector<vector<int>> v;
vector<int> cnt;
vector<string> ans;
struct node {
int id1, id2, cnt, vis;
bool operator < (const node& t) const {
if (cnt != t.cnt) return cnt < t.cnt;
if (ans[id1].length() + ans[id2].length() != ans[t.id1].length() + ans[t.id2].length())
return ans[id1].length() + ans[id2].length() > ans[t.id1].length() + ans[t.id2].length();
if (ans[id1].length() != ans[t.id1].length())
return ans[id1].length() > ans[t.id1].length();
return ans[id1] + ans[id2] > ans[t.id1] + ans[t.id2];
}
};
priority_queue<node> pq;
struct Info {
multiset<int> posi;
int cnt, vis;
};
map<pair<int, int>, Info> mpInfo;
Info& getInfo(int id1, int id2) {
if (mpInfo.count({id1, id2})) return mpInfo[{id1, id2}];
else return mpInfo[{id1, id2}] = {multiset<int>(), 0, 0};
}
void work() {
cin >> n >> m;
vector<string> s(n);
vector<int> vis(26, 0);
v.resize(n), cnt.resize(n);
for (int i = 0; i < n; i++) {
cin >> s[i] >> cnt[i];
for (auto& ch : s[i]) vis[ch - 'a'] = 1;
}
for (int i = 0; i < 26; i++) {
if (vis[i])
ans.emplace_back(string(1, 'a' + i));
if (i) vis[i] += vis[i - 1];
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < s[i].length(); j++) {
v[i].emplace_back(vis[s[i][j] - 'a'] - 1);
if (j) {
Info& info = getInfo(v[i][j - 1], v[i][j]);
info.posi.emplace(i), info.cnt += cnt[i], ++info.vis;
pq.emplace((node){v[i][j - 1], v[i][j], info.cnt, info.vis});
}
}
}
while (ans.size() < m) {
while (pq.size() && pq.top().vis != mpInfo[{pq.top().id1, pq.top().id2}].vis) pq.pop();
if (pq.empty()) break;
if (pq.top().cnt == 0) break;
int id1 = pq.top().id1, id2 = pq.top().id2;
dbg(id1, id2);
Info& info = getInfo(id1, id2);
pq.pop();
ans.emplace_back(ans[id1] + ans[id2]);
for (auto i : info.posi) {
int p = -1;
for (int j = 0; j < v[i].size() - 1; j++)
if (v[i][j] == id1 && v[i][j + 1] == id2) {
p = j;
break;
}
if (p == -1) continue;
auto remove = [&] (int id1, int id2, int idx) {
Info& tem = getInfo(id1, id2);
if (&tem.posi == &info.posi) return;
tem.posi.erase(tem.posi.find(idx));
tem.cnt -= cnt[idx], ++tem.vis;
pq.emplace((node){id1, id2, tem.cnt, tem.vis});
};
auto add = [&] (int id1, int id2, int idx) {
Info& tem = getInfo(id1, id2);
if (&tem.posi == &info.posi) return;
tem.posi.emplace(idx);
tem.cnt += cnt[idx], ++tem.vis;
pq.emplace((node){id1, id2, tem.cnt, tem.vis});
};
if (p) remove(v[i][p - 1], v[i][p], i);
if (p + 2 < v[i].size()) remove(v[i][p + 1], v[i][p + 2], i);
v[i].erase(v[i].begin() + p + 1);
v[i][p] = ans.size() - 1;
if (p) add(v[i][p - 1], v[i][p], i);
if (p + 1 < v[i].size()) add(v[i][p], v[i][p + 1], i);
}
info.posi.clear();
info.cnt = 0, ++info.vis;
pq.emplace((node){id1, id2, info.cnt, info.vis});
}
for (auto& str : ans) cout << str << endl;
}
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
函数分开写纯属个人习惯,主要是为了多组数据。