cqbzoj3474 信仰(hineven模拟赛t3,Hash出奇迹)

本文介绍了一种利用Hash算法高效解决字符串匹配问题的方法。通过离线处理和排序技巧,实现了快速查询具有相同前缀的第k小字符串。文章详细解释了如何通过Hash减少字符串比较的时间复杂度,并给出了具体实现步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

时间限制: 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]);
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值