单词背诵
题目大意:
简略思路
假设右边界从右往左移动,当一个右边界对应的最小左边界找到后,再向左移动右边界,相应左边界不可能向右移动。以此可以去掉许多多余的循环。
题目要求
给定n个单词和一个长为m的段落,要求在该段落内找出包含给定单词种类数最多且最短的最优段落,求该段落包含给定单词的种类数和段落长度。
思路分析
为了方便叙述,我们约定用数组a[ ]来存储给定的n个单词(将其称为目标单词),用数组b[ ]来按照顺序存储给定的m段内的m个单词。
初步思路:
大致思路应该是很好想的,差不多就是对段落整体扫描一遍来求最优段落。首先用Hash来记录n个给定单词和m个段落内单词,然后对比得出给定段落内包含了多少种目标单词,最后对段落整体扫描一遍来寻得最优段落。
那么重点是,如何对段落整体扫描呢?
我们在Hash时首先对段落扫描做好预先处理:对a[ ]内单词Hash时记录a[ ]的Hash值,为了方便与b[ ]比较,我们将其用bool数组存储(这样就相当于用数组来表示该Hash值是否出现过)。如果这个地方没看懂的话,去看一下下面的代码应该就理解了。对b[ ]内单词Hash时,我们用vis[]数组来对其是否为目标单词做标记,如果该单词的Hash值在a[ ]中已经出现过,说明该单词是目标单词之一,则将其对应的vis[ ]标记为1,同时用cnt记录出现的目标单词的种类数。
完成了预先处理,接下来我们来讨论如何对段落进行扫描求解最优段落。
我们采用枚举区间左右端点的方法来对段落进行扫描。这里我是从最右端向左开始枚举区间。如果觉得从左开始枚举更好理解的话,翻到我的题解底部~
我们首先对左端点进行移动。移动左端点的同时,我们使用appear[]记录一下目标单词在当前l,r包含的段落内出现的次数。如果该单词是初次出现,那么cnt--(或者再重新定义一个变量,出现一种++一下,最后比较是否和cnt相等也可),同时appear[ ]++(不是初次出现也要加,键见代码)。如果cnt=0了,那么说明当前我们枚举的段落已经包含了所有种类的b[ ]内目标单词。这时首先我们更新ans值来存储当前的最优解,然后对右端点进行操作。然后如果当前我们的右端点上的单词不是目标单词,那么我们就把它舍去,即r--;如果右端点上的单词在当前枚举段落内已经重复出现过,那么我们也可将其舍去。如果右端点上的单词是目标单词且仅出现一次,那么我们把它舍去的时候就再将cnt++,然后进行下一次扫描。这个地方是最难理解的,如果想不明白的话手动模拟一下。
最后附上代码(已包含注解):
#include<algorithm> #include<iostream> #include<iomanip> #include<cstring> #include<cstdio> #include<cmath> using namespace std; typedef long long ll; const int p=909043; int base=313; int n,m,cnt,ans=1<<30,l,r; char s[33]; int a[100003],b[100003]; int appear[5000003],vis[5000003],pre[5000003]; //pre[]记录灵梦要背的单词 //vis[]记录灵梦要背的单词在给出的段落中是否出现 //appear[]记录要背的单词在目前搜索到的段落中出现的次数 int hashs(char qwq[]) { int len=strlen(qwq); int sum=0; for(int i=0;i<len;i++) { sum=(sum*base+qwq[i])%p; } return sum; } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) { cin>>s; a[i]=hashs(s); pre[a[i]]=1; } scanf("%d",&m); for(int i=1;i<=m;i++) { cin>>s; b[i]=hashs(s); if(pre[b[i]]&&!vis[b[i]]) { vis[b[i]]=1; cnt++; } } if(cnt==0) { printf("0\n0");//一定要记得讨论这种情况QAQQ return 0; } else printf("%d\n",cnt); l=m;//从右向左扫 r=m; while(true) { if(!cnt)//cnt==0时对答案进行更新 { while(!vis[b[r]]) r--; //如果区间右端点不是灵梦要背的单词就将段落长度缩小 ans=min(ans,r-l);//想想为什么不是(r-l+1)呀qwq if(appear[b[r]]>=1) { if(appear[b[r]]==1) { cnt++; } appear[b[r]]--; r--; } //如果右端点的单词已经在段落里出现过了,就可以缩小段落长度 //如果右端点的单词只出现了一次,那么我们将右端点向左移的同时要cnt++(因为这种单词在当前枚举段落内已经不再出现啦) } else { if(l==0) break; if(vis[b[l]]) { if(!appear[b[l]])//如果这种单词在当前枚举的段落里初次出现,那么就说明当前段落内的种类数加一 { cnt--; } appear[b[l]]++;//该种单词出现的次数增加 } l--; } } printf("%d",ans); return 0; }