时间限制: 10 Sec 内存限制: 1024 MB
题目描述
七位探索者与Hineven 来到了第三块天石旁--这也是最大, 最重要的封印石。密密麻麻的裂痕从下方爬满了天石, 天石光泽黯淡, 微微地歪向一边。九百九十七年的
守护, 和魔王Ender持续了九百九十七年的消耗战, 已经让天石的灵性所剩无几, 镌刻的文字也完
全不可辨识了。
七名探索者和Hineven 决定, 他们不仅仅要修复天石, 而且还要用信仰为天石赋予新的
灵性。
Hineven 说: 大家拿出自己的材料吧!
Elaine 拔起身旁的一颗线段树。
Farmer 从剑鞘里抽出了字符串。
XJC 截取了Farmer 的字符串的前后缀。
SunIsMe 端来内存池。
Lsy 选择了一些后缀, 并且进行了翻转。
Miracle 的哈希表丢失了, 于是他只能展开他的邻接表。
JeremyGuo 把一种叫`常数'的杂质从大家的材料中剥离出来, 扔了下山。
Hineven 一一检查了大家的材料并且加入自己的一点东西。
最后, 7 位探索者和他们的向导Hineven 一起点亮了他们的信仰。
于是Hineven 郑重的托起键盘, 顿了顿, 写下:
给N 个字符串, 有M 个询问, 每次询问匹配一个前缀s 的全部字符串中翻转串第k 大的
字符串编号。
在这里, 字符串s 的翻转串指s 串倒序后的字符串, 字符串大小比较使用ASCII 码和字
典序比较规则。
但是Hineven 发现这道题数据生成奇难无比(真的超级难! 比写标算难多了!), 她忙着
生成数据, 于是写标程的任务就只能交给探索者们了。
输入
第一行包括两个正整数, N 和M。
接下来N 行, 第i 行表示编号为i 的字符串。
保证这N 个字符串没有任意两个字符串相同。
接下来M 行, 每行一个字符串s 与一个正整数k, 分别表示询问的前缀与询问的k。
注意, 所有输入中出现过的字符串都由大小写字母和10 个阿拉伯数字组成。
输出
输出一共M 行。
第i 行输出一个正整数表示你对第i 个询问的答案, 即字符串编号。
如果没有这样的字符串, 输出0。
提示
设输入的N 个字符串总长度为L1, 查询的前缀总长度为L2, 对于100%的数据,
1 ≤ N,M ≤ 10^5, N ≤ L1 ≤ 5*10^6, M ≤ L2 ≤ 5*10^6, 1 ≤ k ≤ 10^9
附:(hineven自创)
"要有模板题"
"要有搜索分"
"思维上高度"
"代码控长度"
"难度升序排"
"暴力梯度分"
"实现不能太复杂"
"题面不要太艰深"
"常数稍微宽一点"
"内存放得开一些"
"撰写文档多仔细"
"构造背景需走心"
"样例卡出正解千万傻逼错"
"数据斩尽贪心各路骗分神"
结语:
"搞OI, 要有信仰"
回到正题:
首先,略微的说一下这道题的正解,其实题面已经给了许多信息,那就是:线段树+启发式合并+字典树
要开内存池,不然炸内存,要优化常数,不然炸时间,不能用vector,要用邻接表,不然既炸内存又炸时间
看到这些东西就不想写。
JeremyGuo大佬用Hash将这道题给搞定了,代码简短易懂,空间时间均比较理想。
在我这个弱鸡盯着正解代码苦苦思索不得其要领之时,这个解法简直就是救命稻草。
所以这个故事告诉我们:Hash出奇迹。
题解:
先抛去Hash,考虑暴力的做法,我们要查询前缀相同的第k小字符串,在线做是找死,但并
不是代表不可以做(如果你像Ender一样会一个叫主席树的数据结构的话),所以考虑离线
离线下来后,将每一个询问按照字典序排序,再将输入的所有字符串翻转后排序,在保证了
这两个东西的单调性后,我们可以暴力地找到输入的所有字符串中第一个前缀与当前查询前
缀相同的字符串并且因为输入的字符串也排了序,所以往下直接跳k个再比较前缀即可找到是
否有满足条件的字符串,因为有单调性,所以下一次操作可以不用返回到1,而从当前节点开
始。在忽略掉字符串比较的时间复杂度是O(m+n)的。而Hash恰恰就能做到忽略掉字符串比较
的时间复杂度。所以Hash在理论上是非常优秀的。
但是这样也会带来一些问题,比如,处理出Hash的时间复杂度是否可以承受?可以发现,每个
输入的字符串的每个前缀都是需要Hash的,这个的内存和时间是5000000,询问也都要Hash下来,
但可以忽略不计,所以可以承受。然后是用Hash值排序后是否能保证单调性,这个其实是不能
保证的,但是它可以保证前缀相同的一定相邻,这就够了。
上面思路有点混乱,因为这个解法是由暴力而来,而在将暴力做法修改成Hash时是很不一样的
整理一下:
首先将所有的字符串按翻转后的字典序从小到大排序,然后用Hash将所有的字符串前缀都处理
出来,将这些所有处理出来的Hash值按照从小到大排序。然后离线询问,并处理处所有询问的
前缀的Hash值,依然是从小到大排序。然后枚举一下就可以了。
细节看代码吧。。。
#include<iostream>
#include<cstdlib>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
using namespace std;
typedef unsigned long long ULL;
const int MAXL=5000005;
const int MAXN=100005;
const int seed=233;
int n,m,vcnt,ans[MAXN];
struct Data{
string s; int id;
}p[MAXN];
struct Hash1{
ULL v; int rk,id;
}Hash_v[MAXL];
struct Hash2{
ULL v; int k,id;
}Hash_q[MAXN];
char tmp[MAXL];
namespace ib {char b[10];}
inline void print(int x){
if(x==0) {putchar(48); return;}
if(x<0) {putchar('-'); x=-x;}
char *s=ib::b;
while(x) *(++s)=x%10, x/=10;
while(s!=ib::b) putchar((*(s--))+48);
putchar(10);
}
bool cmp1(const Data &a,const Data &b){
return a.s<b.s;
}
bool cmp2(const Hash1 &a,const Hash1 &b){
if(a.v!=b.v) return a.v<b.v;
else return a.rk<b.rk;
}
bool cmp3(const Hash2 &a,const Hash2 &b){
return a.v<b.v;
}
inline void Read(int &Ret){
char ch;bool flag=0;
for(;ch=getchar(),!isdigit(ch);)if(ch=='-')flag=1;
for(Ret=ch-'0';ch=getchar(),isdigit(ch);Ret=Ret*10+ch-'0');
flag&&(Ret=-Ret);
}
int main()
{
Read(n); Read(m);
for(int i=1;i<=n;i++)
{
scanf("%s",tmp);
reverse(tmp,tmp+strlen(tmp));
p[i]=(Data){(string)(tmp),i};
}
sort(p+1,p+n+1,cmp1);
for(int i=1;i<=n;i++)
{
ULL val=0;
for(int j=p[i].s.size()-1;j>=0;j--)
{
val=val*seed+(ULL)p[i].s[j];//这个seed常数很玄学,要尽量大于后面的那个变量且尽量是素数,这样可以降低重复率
Hash_v[++vcnt]=(Hash1){val,i,p[i].id};
}
}
sort(Hash_v+1,Hash_v+vcnt+1,cmp2);
for(int i=1;i<=m;i++)
{
scanf("%s",tmp); Read(Hash_q[i].k);
Hash_q[i].id=i; Hash_q[i].k--;
int len=strlen(tmp);
for(int j=0;j<len;j++)
Hash_q[i].v=Hash_q[i].v*seed+(ULL)tmp[j];
}
sort(Hash_q+1,Hash_q+m+1,cmp3);
int pos=1;
for(int i=1;i<=m;i++)
{
while(pos<=vcnt&&Hash_v[pos].v<Hash_q[i].v) pos++;
if(pos>vcnt) break;
if(Hash_v[pos].v!=Hash_q[i].v) continue;
if(pos+Hash_q[i].k>vcnt) continue;
if(Hash_v[pos+Hash_q[i].k].v==Hash_q[i].v)
ans[Hash_q[i].id]=Hash_v[pos+Hash_q[i].k].id;
}
for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
}