字符串哈希

目录

 入门:活动 - AcWing

 引入:[BalticOI 2014 Day1] Three Friends - 洛谷

进阶:hash进阶:使用字符串hash乱搞的姿势_牛客博客

马拉车算法


 入门:活动 - AcWing


题意:给定一个长度为n的字符串,再给定m个询问,每个询问包含四个整数l1,r1,l2,r2l1,r1,l2,r2,请你判断[l1,r1l1,r1]和[l2,r2l2,r2]这两个区间所包含的字符串子串是否完全相同。

字符串中只包含大小写英文字母和数字。


算法:

(字符串哈希) O(n)+O(m)
全称字符串前缀哈希法,把字符串变成一个p进制数字(哈希值), 

   实现不同的字符串映射到不同的数字。
   对形如 X1X2X3⋯Xn−1Xn的字符串,采用字符的ascii 码乘上 P 的次方来计算哈希值。

映射公式 (X1×Pn−1+X2×Pn−2+⋯+Xn−1×P1+Xn×P0)modQ
注意点:
1. 任意字符不可以映射成0,否则会出现不同的字符串都映射成0的情况,比如A,AA,AAA皆为0
2. 冲突问题:通过巧妙设置P (131 或 13331) , Q (264)(264)的值,一般可以理解为不产生冲突。

问题是比较不同区间的子串是否相同,就转化为对应的哈希值是否相同。
求一个字符串的哈希值就相当于求前缀和,求一个字符串的子串哈希值就相当于求部分和。


前缀和公式  h[ i + 1]=h[ i ]× P +s[ i ]   (i∈[ 0, n − 1] ) h为前缀和数组,s为字符串数组
区间和公式   h[ l , r ]=h[ r ]−h[ l − 1 ] × P[ r − l + 1 ]  
区间和公式的理解: ABCDE 与 ABC 的前三个字符值是一样,只差两位,
乘上 P² 把 ABC 变为 ABC00,再用 ABCDE - ABC00 得到 DE 的哈希值。

AcWing 841. 字符串哈希 【公式助理解】 - AcWing


//字符串哈希
typedef unsigned long long ULL;//ULL数组用于存储时,为的就是溢出时有取模效果
const int N = 100010, P = 131; //131 or 13331
int n, m;
char str[N];
ULL h[N], p[N];
// h[i]前i个字符的hash值
//p[i]表示P的i次方
// 字符串变成一个p进制数字,
//体现了字符+顺序,需要确保不同的字符串对应不同的数字

ULL get(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
    scanf("%d%d%s", &n, &m, str + 1);  
    //字符串从1开始编号,h[1]为前一个字符的哈希值
    p[0] = 1;
    h[0] = 0;
    for (int i = 1; i <= n; i++)
    {
        p[i] = p[i - 1] * P;//p进制 把p想象成10就好理解了
        h[i] = h[i - 1] * P + str[i];前缀和求整个字符串的哈希值

    }
    while (m--)
    {
        int l1, r1, l2, r2;
        scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
        if (get(l1, r1) == get(l2, r2))
            puts("Yes");
        else
            puts("No");
    }
    return 0;
}


 引入:[BalticOI 2014 Day1] Three Friends - 洛谷


题目描述
给定一个字符串S,先将字符串S复制一次(变成双倍快乐 ),得到字符串T,然后在T中插入一个字符,得到字符串U。
给出字符串U,重新构造出字符串S。
所有字符串只包含大写英文字母。

输入格式
第一行一个整数N,表示字符串U的长度。
第二行一个长度为N的字符串,表示字符串U。

输出格式
一行一个字符串,表示字符串S。

特别地:

如果字符串无法按照上述方法构造出来,输出NOT POSSIBLE;
如果字符串S不唯一,输出 NOT UNIQUE。
 


题解:

为了减少hash冲突而*31。
因为S翻倍后要插入一个字符,所以始终是2n+1,也就是奇数,所以如果是偶数就输出NOT POSSIBLE,并结束。
插入的字符有2种情况:

1.插入的字符在前面一半之间,则S(也就是答案)就在后面一半,我们就在前面枚举字符的位置,然后根据 hash值来判断是否相等;
2.插入的字符在后面一半之间,则S(也就是答案)就在前面一半,我们就在后面枚举字符的位置,然后根据 hash值来判断是否相等;

“三个朋友”「BalticOI 2014 Day1 T2 Three Friends」【题解】_cqbz_JiangJinBei的博客-CSDN博客


 进制hash板子:

//[l,r]的hash值
ll get(int l, int r)
{
    return (h[r] - h[l - 1] * p[r - l + 1] % mod + mod) % mod;
}

//[l,r]删去x的hash值
ll query(int l, int r, int x)
{
    return (get(l, x - 1) * p[r - x] % mod + get(x + 1, r)) % mod;
}

想解释一下query函数:

注意[l,r]删去x, 前面的get*p[r-x],注意这个r-x是关键,

相当于12345 ,删去3的话,应该是12*100+45。

只有两个0,没有x这一位了噢。

「BalticOI 2014 Day 1」三个朋友(进制hash)_从冬的博客-CSDN博客

 再来说说我看完别人代码之后自己敲这个题时的想法(应该更容易理解一点)

首先你要理解上面的get函数和query,上面已经写过了。

然后再来看这道题:

其实代码里是三种情况,第三种就是这个插入的字符在正中间(mid + 1)。 

前两种体现在代码里就是:

因为n是奇数,mid就会是,比如9/2 = 4,那么假设插入字符是第一种情况,就会是前五个(包含插入字符)减去插入字符的hash值和最后四个的hash值比较,所以我们枚举插入字符为1-mid,(不是mid+1,是因为第三种情况考虑的就是这种情况)。然后前五个的hash值用query函数,后四个用get函数,这样看代码应该就很好理解了叭。


const int N = 2e6 + 10, P = 31;
ll h[N], p[N];
char s[N];

//[l,r]的hash值
ll get(int l, int r)
{
    return (h[r] - h[l - 1] * p[r - l + 1]);
}

//[l,r]删去x的hash值
ll query(int l, int r, int x)
{
    return (get(l, x - 1) * p[r - x] + get(x + 1, r));
}
ll sum = 0;
map<ll, bool> flag;
int main()
{
    int n;
    cin >> n;
    ll ans = 0;
    cin >> s + 1;
    if (n % 2 == 0)
    {
        cout << "NOT POSSIBLE\n";
        return 0;
    }
    p[0] = 1;
    h[0] = 0;
    for (int i = 1; i <= n; i++)
    {
        p[i] = p[i - 1] * P;
        h[i] = h[i - 1] * P + (ll)(s[i] - 'A' + 1);
    }
    ll mid = n / 2;
    for (ll i = 1; i <= mid; i++)
    {
        if (query(1, mid + 1, i) == get(mid + 2, n) && !flag[get(mid + 2, n)])
        {
            sum++;
            ans = i;
            flag[get(mid + 2, n)] = 1;
        }
    }
    if (get(1, mid) == get(mid + 2, n) && !flag[h[mid]])
    {
       
        sum++;
        ans = mid + 1;
        flag[h[mid]] = 1;
    }
    for (ll i = mid + 2; i <= n; i++)
    {
        if (!flag[h[mid]] && get(1, mid) == query(mid + 1, n, i))
        {
           
            sum++;
            ans = i;
            flag[h[mid]] = 1;
        }
    }
    if (!sum)
    {
        cout << "NOT POSSIBLE\n";
    }
    else
    {
        if (sum > 1)
        {
            cout << "NOT UNIQUE\n";
        }
        else
        {
            if (ans <= mid + 1)
            {
                for (ll i = mid + 2; i <= n; i++)
                {
                    cout << s[i];
                }
            }
            else
            {
                for (ll i = 1; i <= mid; i++)
                {
                    cout << s[i];
                }
            }
            cout << endl;
        }
    }
    return 0;
}

 做完上面的题 有了一定的理解,现在来看进阶(更专业的理解哈)

进阶:hash进阶:使用字符串hash乱搞的姿势_牛客博客

 字符串-hash - 随笔分类 - henry_y - 博客园

题目: 

 NUMOFPAL - Number of Palindromes - 洛谷

LOJ#2452. 「POI2010」反对称 Antisymmetry - henry_y - 博客园

马拉车算法

 最长回文子串——马拉车算法详解_HappyRocking的博客-CSDN博客

 马拉车算法(不懂问我)_algsup的博客-CSDN博客_马拉车算法


string Mannacher(string s)
{
    //插入"#"
    string t = "$#";
    for (int i = 0; i < s.size(); ++i)
    {
        t += s[i];
        t += "#";
    }

    vector<int> p(t.size(), 0);
    // mx表示某个回文串延伸在最右端半径的下标,id表示这个回文子串最中间位置下标
    // resLen表示对应在s中的最大子回文串的半径,resCenter表示最大子回文串的中间位置
    int mx = 0, id = 0, resLen = 0, resCenter = 0;

    //建立p数组
    for (int i = 1; i < t.size(); ++i)
    {
        p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;

        //遇到三种特殊的情况,需要利用中心扩展法
        while (t[i + p[i]] == t[i - p[i]])
            ++p[i];

        //半径下标i+p[i]超过边界mx,需要更新
        if (mx < i + p[i])
        {
            mx = i + p[i];
            id = i;
        }

        //更新最大回文子串的信息,半径及中间位置
        if (resLen < p[i])
        {
            resLen = p[i];
            resCenter = i;
        }
    }

    //最长回文子串长度为半径-1,起始位置为中间位置减去半径再除以2
    return s.substr((resCenter - resLen) / 2, resLen - 1);
}

题目:https://loj.ac/p/2452

分析:简单分析后发现n必须是偶数,反对称字串必须是,对称位置不能相同。 

解法一: 马拉车算法 模板改一下判断条件即可。代码如下


ll  Mannacher(string s)
{
    //插入"#"
    ll ans = 0;
    string t = "$#";
    for (int i = 0; i < s.sz; ++i)
    {
        t += s[i];
        t += "#";
    }

    vector<int> p(t.size(), 0);
    // mx表示某个回文串延伸在最右端半径的下标,id表示这个回文子串最中间位置下标
    // resLen表示对应在s中的最大子回文串的半径,resCenter表示最大子回文串的中间位置
    int mx = 0, id = 0;

    //建立p数组
    for (int i = 1; i < t.size(); i += 2)
    {
        p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;

        //遇到三种特殊的情况,需要利用中心扩展法
        while (t[i + p[i]] == t[i - p[i]] && t[i-p[i]] == '#' || t[i + p[i]]-'0' + t[i-p[i]] -'0'== 1)
            ++p[i];

        //半径下标i+p[i]超过边界mx,需要更新
        if (mx < i + p[i])
        {
            mx = i + p[i];
            id = i;
        }
        ans += p[i] / 2 ;
    }
    //最长回文子串长度为半径-1,起始位置为中间位置减去半径再除以2
    return  ans ;
}
int main()
{
    int n ;
    cin>>n;
    string s;
    cin>>s;
    cout<<Mannacher(s)<<endl;
    return 0;
}

 解法二:字符串哈希

LOJ#2452. 「POI2010」反对称 Antisymmetry - henry_y - 博客园

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值