ccf-csp 第34次 文本分词

TUOJ

1.学习经验

1.对于set<string> st,不能直接插入char类型,而应该先把char变成string后再插入

复制代码
for(atuo& c:str){
	string tmp(1,c); // char->string
	st.insert(tmp);
}

以及i+'a'要转换成char

复制代码
string tmp(1,char(i+'a'));

2.题意理解错误:

真实题意:

(1)下一个词汇查找:"接下来,统计词汇表中,两个词汇组成的"词汇对"相连出现的频率"+"从前到后的顺序要求":所以是从左到右依次选取相邻两个词汇,组成词汇对

(2)合并词汇:"同时生成对应的合并规则(即将选出的"词汇对"合并成一个词汇),并按照该规则将所有输入单词的词汇序列按从前到后的顺序依次加以合并(核心) 。"

所以是查找下一个词汇+更新原序列两个词汇变成新词汇(需显式更新 )的重复过程

而我的算法流程并没有显式合并词汇,而是通过最长匹配思路查找词汇表,会出现错误

3.题目提示:

"对于一个给定的文本,可以按照该表格将文本转化为一个数字的序列"+"词汇表包括一系列的字符串,可以用于将输入的文本转换为数字序列 。":所以在处理中,输入单词的词汇序列应该拿数字表示去进行一系列操作,最终在词汇表中通过数字得到字符串,数字就可以拿数字下标表示,也符合顺序插入词汇表的目的

2.题目描述

题目背景

西西艾弗岛大数据中心正在如火如荼地开展大语言模型的研究工作。众所周知,计算机在执行机器学习的任务时,更适合处理数字的数据。对于大语言文本的处理, 最好的方式是将文本转化为数字,然后再进行处理。通常,对输入数据进行整理后,需要将其按照一定的规则进行编码,以便计算机能够更好地处理。

这一转换过程是通过词汇表进行的。词汇表是一个包含了所有可能出现的词的列表。对于一个给定的文本,可以按照该表格将文本转化为一个数字的序列。 而词表也需要根据文本的特点进行设计,以便更好地反映文本的特点。

题目描述

词汇表包括一系列的字符串,可以用于将输入的文本转换为数字序列。这里,我们认为输入文本事先经过一定的处理,将字母统一转换为了小写字母。词汇表的生成过程是一个迭代的过程。首先将文本按照一定的规则切分为单词序列, 并统计全部单词的出现频率。然后将单词拆分为单字母字符串,组成初始的词汇表。接下来根据词汇表中的词汇接连出现在单词中的频率,将词汇反复合并,组成更长的词汇加入到词汇表中。 词汇表的具体生成过程如下:

首先,输入文本中所有的单词和其出现的频率。然后,统计其中所有的字符,将其按照字典序排序,并将这些字符作为单字母字符串加入到词汇表中。同时,将输入的单词相应切分为词汇序列。

例如,输入下列词组和频率:

none 复制代码
cut 15
cute 10
but 6
execute 3

则执行完上述过程后,词汇表中包含了 bcetux 这些单字母字符串,而输入的词组被切分为:

none 复制代码
c u t 15
c u t e 10
b u t 6
e x e c u t e 3

接下来,统计词汇表中,两个词汇组成的"词汇对"相连出现的频率,并选取出现次数最多的那一组拼接为一个字符串加入词汇表中 。如果存在多个这样的"词汇对",则再按照如下优先级顺序选取:

  • 选取拼接后的字符串长度最短的那一组;
  • 选取"词汇对"中前一个词汇长度最短的那一组;
  • 选取拼接后的字符串字典序最小的那一组。

同时生成对应的合并规则(即将选出的"词汇对"合并成一个词汇),并按照该规则将所有输入单词的词汇序列按从前到后的顺序依次加以合并。

例如,在上述单词列表中词汇组合 <c, u> 在单词 cutcuteexecute 中分别出现了 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>,可得到词汇表 bcetuxut。同时,将输入的单词切分为:

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>,可得到词汇表 bcetuxutcut。同时,将输入的单词切分为:

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 和 m,分别表示输入的单词的数量和期望的词汇表长度。

接下来的 n 行,每行包含一个非空字符串 s 和一个正整数 f,表示输入的单词和其出现的频率。其中,s 中只包含小写字母。

输出格式

输出共 m 行,每行包含一个字符串,按照加入词汇表的顺序输出词汇表中所有词汇。

样例1输入

plain 复制代码
4 8
cut 15
cute 10
but 6
execute 3

样例1输出

plain 复制代码
b
c
e
t
u
x
ut
cut

3.分析过程

子任务

子任务1

对 20% 的数据,有 n≤200,m 恰好等于输入单词中出现的所有字母的个数。
分析 :

"m 恰好等于输入单词中出现的所有字母的个数"说明输入字符串统计完所有字符后组成的词汇表就是答案,利用set去重得到字符,然后赋值给vector组成初始词汇表,最终输出。

子任务2

对 40% 的数据,有 n≤200,m≤200。

子任务3

对 80% 的数据,有 n≤2000,m≤2500。
分析 :

1.首先考虑数据结构:

(1)词汇表:tokenId->字符串token,因为是顺序加入,数组实现

(2)每个输入字符串的token序列+频率,所以是words数组,每个数组元素是一个结构体

2.接下来考虑算法流程:

(1)获取下一个token:

  • 遍历每个输入字符串,得到任意相邻两个的tokenId,得到tokenId对的频率(unordered_map实现)
  • 遍历上述map,按照规则找到最好tokenId对(用一组最好值维护)
    (2)更新vocab表,得到新tokenId和token
    (3)更新words数组,即已知当前最好tokenId对和新tokenId,将原先相邻的tokenId对合并为新tokenId

子任务4

对 100% 的数据,有 n≤10000,m≤5000 且大于等于输入单词中出现的所有字母的个数,s 的长度 ∣s∣≤25,输入单词的总频率(f 的总和)不超过 10^6。文本取材于真实的英文著作。
分析 :

(1)上述流程中"遍历每个输入字符串,得到任意相邻两个的tokenId",每次都要这样遍历,导致Time Limit Exceeded

其他

4.代码

子任务1

复制代码
#include <bits/stdc++.h>

using namespace std;

int n,m;
typedef pair<string,int> PSI;
vector<string> res; // 词汇表 
set<string> st; // 初始词汇表,去重 
map<string,int> strToCnt; // 输入字符串 

int main(){
	cin>>n>>m;
	// 输入 
	for(int i=0;i<n;++i){
		string str;
		int x;
		cin>>str>>x;
		strToCnt[str]+=x;
		for(auto& c:str){
			string tmp(1,c);
			st.insert(tmp);
		}
	}
	for(auto& str:st)	res.push_back(str);
	// 子任务1
	if(m==res.size()){
		for(auto& str:res)	cout<<str<<endl;
	} 
	return 0;
}

子任务2,3(学习,20->80)

复制代码
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
// 单个字符串 
struct Word{
	vector<int> tokens; // 词汇数字序列
	ll freq; // 频率 
};
vector<string> vocab; // 词汇表
vector<Word> words; // 字符串数组 
int n,m;
// 初始化 vocab和 words
void input(){
	cin>>n>>m;
	unordered_map<string,ll> strToCnt; // 输入字符串频率 
	vector<bool> used(26,false);
	for(int i=0;i<n;++i){
		string str;
		ll f;
		cin>>str>>f;
		strToCnt[str]+=f;
		for(char& c:str)	used[c-'a']=true;
	}
	vector<int> charId(26,-1); // 字符id 
	// 统计单个字符,更新词汇表 
	for(int i=0;i<26;++i){
		if(used[i]){
			charId[i]=(int)vocab.size();
			string str=string(1,char('a'+i));
			vocab.push_back(str);
		}
	} 
	// 更新words 
	for(auto& tmp:strToCnt){
		string str=tmp.first;
		ll f=tmp.second;
		Word w;
		w.freq=f;
		for(char& c:str){
			int id=charId[c-'a'];
			w.tokens.push_back(id);
		}
		words.push_back(move(w)); // move()更快 
	}
}
// 寻找下一个token
pair<int,int> get_nxt(){
	map<pair<int,int>,ll> tokenPairs;
	// 更新 tokenPairs
	for(auto& w:words){
		auto& tokens=w.tokens;
		ll f=w.freq;
		for(int i=0;i+1<tokens.size();++i){
			pair<int,int> curPair={tokens[i],tokens[i+1]};
			tokenPairs[curPair]+=f;
		}
	}
	if(tokenPairs.empty())	return {-1,-1}; // 异常处理 
	// 查找bestPair
	int bestTokenA=-1,bestTokenB=-1;
	ll bestF=LLONG_MIN,bestLenA=0,bestLen=0; 
	string bestStr;
	for(auto& tmp:tokenPairs){
		auto curTokens=tmp.first;
		ll f=tmp.second;
		int tokenA=curTokens.first,tokenB=curTokens.second;
		string strA=vocab[tokenA],strB=vocab[tokenB];
		string str=strA+strB;
		ll lenA=strA.size(),lenB=strB.size();
		ll len=lenA+lenB;
		if(bestF==LLONG_MIN){
			bestTokenA=tokenA;
			bestTokenB=tokenB;
			bestF=f;
			bestLen=len;
			bestLenA=lenA;
			bestStr=str;
		}
		bool isBest=false;
		if(f>bestF){ // 出现次数多 
			isBest=true;
		}
		else if(f==bestF){
			if(len<bestLen){ // 字符串长度短 
				isBest=true;
			}
			else if(len==bestLen){ // 前一个词汇长度短 
				if(lenA<bestLenA){
					isBest=true;
				}
				else if(lenA==bestLenA){
					if(str<bestStr){ // 字典序更小 
						isBest=true;
					}
				}
			}
		}
		if(isBest){
			bestTokenA=tokenA;
			bestTokenB=tokenB;
			bestF=f;
			bestLen=len;
			bestLenA=lenA;
			bestStr=str;
		}
	}
	return {bestTokenA,bestTokenB};
}
// 合并words中词汇对变成新词汇 
void merge(pair<int,int> bestPair,int newToken){
	int tokenA=bestPair.first,tokenB=bestPair.second;
	for(auto& w:words){
		auto& oldTokens=w.tokens;
		vector<int> newTokens;
		for(int i=0;i<oldTokens.size();){
			if(i+1<oldTokens.size() 
			&& oldTokens[i]==tokenA 
			&& oldTokens[i+1]==tokenB){
				newTokens.push_back(newToken);
				i+=2;
			}
			else{
				newTokens.push_back(oldTokens[i]);
				i+=1;
			}
		}
		w.tokens.swap(newTokens);
	}
}
// 建立vocab
void buildVocab(){
	while((int)vocab.size()<m){
		pair<int,int> bestPair=get_nxt();
		int tokenA=bestPair.first,tokenB=bestPair.second;
		if(tokenA==-1)	break;
		// 插入vocab
		string strA=vocab[tokenA],strB=vocab[tokenB];
		string newStr=strA+strB;
		int newToken=(int)vocab.size();
		vocab.push_back(newStr);
		// 合并words中词汇对变成新词汇
		merge(bestPair,newToken);
	}
} 
int main(){
	input();
	//testOutput();
	buildVocab();
	for(auto& str:vocab){
		cout<<str<<endl;
	}
	return 0;
}

优化点

(1)临时对象放入数组用move

复制代码
vector<words> vecs;
Word w;
vecs.push_back(move(w));

(2)结构体中数组更新用swap

复制代码
vector<int> newTokens;
w.tokens.swap(newTokens);

(3)数组和一些字符串(map的key不行)加引用&

相关推荐
Elias不吃糖5 小时前
LeetCode每日一练(209, 167)
数据结构·c++·算法·leetcode
铁手飞鹰5 小时前
单链表(C语言,手撕)
数据结构·c++·算法·c·单链表
悦悦子a啊5 小时前
项目案例作业(选做):使用文件改造已有信息系统
java·开发语言·算法
小殊小殊6 小时前
【论文笔记】知识蒸馏的全面综述
人工智能·算法·机器学习
无限进步_6 小时前
C语言动态内存管理:掌握malloc、calloc、realloc和free的实战应用
c语言·开发语言·c++·git·算法·github·visual studio
im_AMBER6 小时前
AI井字棋项目开发笔记
前端·笔记·学习·算法
Wadli6 小时前
项目2 |内存池1|基于哈希桶的多种定长内存池
算法
TT哇6 小时前
【BFS 解决拓扑排序】3. ⽕星词典(hard)
redis·算法·宽度优先
橘颂TA6 小时前
【剑斩OFFER】算法的暴力美学——判定字符是否唯一
算法·c/c++·结构与算法
ModestCoder_6 小时前
PPO-clip算法在Gymnasium的Pendulum环境实现
人工智能·算法·机器人·具身智能