• 字符串相关算法学习总结集合


    HASH
    把复杂问题映射到一个容易维护的值域, 因为值域变简单, 范围变小, 可能会造成两个不同的值被hash函数映射到同一个值上,因此需要处理冲突情况
    开散列:建立一个邻接表结构,以hash函数的值域作为表头数组head, 映射后的值相同的原始信息被分在同一类, 构成一个链表接在对应的表头, 链表的节点保存原始信息和统计数据(大概就是拉链式hash??)
    hash的两个基本操作
    1.计算hash函数的值
    2.定位到对应链表依次遍历,比较
    例:我们要在一个长度为n的随机整数序列A中统计每个数出现了多少次
    一般思路:
    直接数组计数
    hash思路:
    设计一个hash函数为h(x) = (x mod p) + 1, 其中p是一个比较大的质数, 但不超过n。这样,显然,我们把数列A分成了P类, 我们依次考虑数列中的每个数A[i], 定位到hash[h(A[i])]这个表头所指向的链表,如果该链表不包含A[i], 我们就在尾部新插入一个节点A[i], 并在该节点上记录A[i]出现了1次,否则直接找到已经存在的节点A[i],并将其出现次数+1。因为整数序列A是随机的,所以最终所以A[i]会比较均匀的分散在各个表头,整个算法的复杂度接近O(n)

    对于非随机数列,我们可以设计更好的hash函数来保证其时间复杂度。同样的,如果我们需要维护的是比大整数复杂得多的某些性质(如是否存在,出现次数),也可以通过hash解决

    emmmm....放一道基本水题感受下:

     

    emmmm要是x小一点就可以丢到数组那当基本题了,然而很大,显然要是直接用数组会爆内存,所以我们来hash吧,这之后的问题解决就是数学的集合的事情了

    丢份丑陋的代码:

    #include <bits/stdc++.h>
    #define p 2323237
    using namespace std;
    struct node {
        int v, next, num;
    }hash[p];
    int n, sum = 0, a, b, bsum = 0;
    int lin[p], len = 0;
    bool flag1 = 0, flag = 0;
    inline int read() {
        int x = 0, y = 1;
        char ch = getchar();
        while(!isdigit(ch)) {
            if(ch == '-') y = -1;
            ch = getchar();
        }
        while(isdigit(ch)) {
            x = (x << 3) + (x << 1) + ch - '0';
            ch = getchar();
        }
        return x * y;
    }
    inline int getkey(int k) {
    return k % p;}
    inline void insert(int key, int v) {
        hash[++len].next = lin[key];
        hash[len].v = v;
        hash[len].num = 1;
        lin[key] = len;
    }
    inline void hash_(int k, int c) {
        int key = getkey(k);
        if(c == 1)
            insert(key, k);
        else {
            for(int i = lin[key]; i; i = hash[i].next)
                if(hash[i].v == k) {
                    hash[i].num++;
                    flag1 = 1;
                    flag = 1;
                    sum++;
                }
            if(flag == 0) {
                insert(key, k);
                bsum++;
            }
            flag = 0;
        }
    }
    int main(){
        for(int i = 1; i <= 2; i++) {
            n = read();
            int x;
            if(i == 1) a = n;
            else b = n;
            for(int j = 1; j <= n; j++) {
                x = read();
                hash_(x, i);
            }
        }
        if(!flag1) cout << "A and B are disjoint" << endl;
        else {
            if(bsum == 0) {
                if(sum == a) 
                    cout << "A equals B" << endl;
                else
                    cout << "B is a proper subset of A" << endl;
            }
            else if(bsum != 0) {
                if(sum == a)
                    cout << "A is a proper subset of B" << endl;
                else 
                    cout << "I'm confused!" << endl;
            }
        }
        return 0;
    }

    字符型HASH

    字符型hash,即把一个任意长度的字符映射成一个非负整数,并且冲突概率几乎为0
    取一固定值P,把字符串看做P进制数,并分配一个大于0的数值, 代表每种字符。 一般来说, 我们分配的数值都远小于P, 例如对于小写字母构成的字符串,可以令:a = 1, b = 2, c = 3, .....z = 26。取一固定值M,将P对M取模,作为该字符的hash值 。
    一般的说,我们取P = 131或P = 13331, 此时产生冲突的概率极低,只要hash值相同,我们就可以认为原字符串相等的。但是现实是,我们最好还是直接比较字符串是否相同,不然很容易就挂了.jpg,同样拉链式hash很重要.jpg,不然活该被卡(来自被花式卡死的人的怨念。)
    一般我们采用M = 2^64, 即直接使用unsigned long long 类型存储hash值, 在计算时产生算术溢出时相当于直接的2^64取%, 这样可以避免低效的取%运算.jpg
    我们也可以多取一些恰当的P和M的值(例如一些大质数,就比如如果你的企鹅号是质数...),多进行几组hash运算,当结果都相同时才认为与原字符串相等,一般来说,再毒瘤的出题人也很难构造出使这个hash产生错误的数据了,如果不行还是挂了,呵呵,出题人这辈子怕是没有rp了。但是如果你只运行1次,emmm,不被卡才怪。
    对于字符串的各种操作,可以直接对P进制数进行算数运算反映到hash上

    比如我们已知一个字符串S的hash值为Hash(S), 那么在S后添加一个字符c构成新字符S + c的hash值就是
    Hash(S + c) = (Hash(S) * P + value[c]) % M。其中乘P相当于P进制下的左移运算, value数组是我们预先处理的字母的映射数组, value[c]就是我们选定的c的代表数值。

    再如我们已知字符串S的hash值为Hash(S), 字符串S + T的hash值为Hash(S + T),那么字符串T的hash值
    Hash(T) = (Hash(S + T) - Hash(S) * P^length(T)) % M
    其中 Hash(S) * P^length(T)相当于把Hash(S)在P进制下再S后补0的方式进行算术左移,是S的左端与S + T的左端对齐,这样进行相减后得到的就是字符串T的hash值Hash(T)
    例如: S = "abc", c = "d", T = "xyz", value["a, b, c.....z"] = {1, 2, 3, ....26}
    S表示为P进制数为1 2 3
    Hash(S) = 1 * P^2 + 2 * P + 3
    Hash(S + c) = Hash(S) * P + value[c] = (1 * P^2 + 2 * P + 3) * P + 4 = 1 * P^3 + 2 * P^2 + 3 * P + 4
    S + T表示为P进制数为1 2 3 24 25 26
    Hash(S + T) = 1 * P^5 + 2 * P^4 + 3 * P^3 + 24 * P^2 + 25 * P + 26
    Hash(S) * P^length(T) = (1 * P^2 + 2 * P + 3) * P^3 = 1 * P^5 + 2 * P^4 + 3 * P^3
    即Hash(S) * P^length(T)表示为P进制数为 1 2 3 0 0 0
    显然相减以后我们就得到了T的hash值
    即Hash(T) = 1 * P^5 + 2 * P^4 + 3 * P^3 + 24 * P^2 + 25 * P + 26 - (1 * P^5 + 2 * P^4 + 3 * P^3)
    表示为P进制数为 1 2 3 24 25 26 - 1 2 3 0 0 0 = 24 25 26
    也就是说运算出来的Hash(T)表示为P进制数为24 25 26
    Hash(T) = 24 * P^2 + 25 * P + 26
    根据以上两种操作,我们可以通过O(n)的时间预处理字符串甚至所有前缀Hash值,并在O(1)的时间内查询任意子串的hash值

     丢一道题:

    emmm字符串hash,然而切记不要去比较什么hash值,直接比较原字符,顺便把拉链式用上

    不然你就会和我一样,WA掉这道题

    日常丢代码(emmm刚刚发现博客园是可以插入代码的)

    #include <bits/stdc++.h>
    #define maxn 500086
    #define p 131
    #define m 2323237
    #define ull unsigned long long
    using namespace std;
    struct node {
        char c[510];
        int next;
    }hash[50010];
    char ch[maxn];
    int ans[maxn], top = 0;
    int n;
    int lin[3000010], le = 0;
    inline void insert(int key) {
        hash[++le].next = lin[key];
        strcpy(hash[le].c, ch);
        lin[key] = le;
    }
    inline void hash_(int k, int c) {
        register bool flag = 0;
        for(register int i = lin[k]; i; i = hash[i].next)
            if(strcmp(hash[i].c, ch) == 0) {
                ans[++top] = c;
                flag = 1;
                cout << c << "
    ";
            }
        if(!flag) insert(k);
    }
    int main() {
        ios::sync_with_stdio(false);
        cin.tie(NULL);
        cout.tie(NULL);
        cin >> n;
        for(register int i = 1; i <= n; ++i) {
            cin >> ch;
            register int len;
            register ull key = 0;
            len = strlen(ch);
            for(register int j = 0; j < len; ++j) {
                register int h = ch[j] - 'a' + 1;
                key = (key * p + h) % m;
            }
            hash_(key, i);
        }
        return 0;
    }

    KMP

    KMP算法,又称模式匹配算法,能够在线性时间内判定长度为n的字符串A是否为长度为m的字符串B的子串
    O(nm)暴力算法,二重循环枚举,逐个扫描A[1]...A[n]与B[i],B[i + 1]....B[i + n - 1]是否相同,我们把这个比较的过程称为“A与B进行尝试匹配”,
    KMP算法分为两步:
    1.对字符串A进行自我匹配,求出一个数组next,其中next[i]表示“A中以i结尾的非前缀子串”与A的前缀能够匹配的最长长度。
    即:next[i] = max{j}, j < i && a[(i - j + 1) ~ i] = a[1 ~ j]
    2.对字符串A与B进行匹配,求出一个数组f,其中f[i]表示“B中以i结尾的子串”与“A的前缀”能够匹配的最长长度
    即:f[i] = max{j}, j <= i && b[(i - j + 1) ~ i] = a[1 ~ j]
    以字符串abababaac为例
    以i = 7结尾的“非前缀子串有6个”,分别是a[2~7], a[3~7], a[4~7], a[5~7], a[6~7], a[7]
    如果使用暴力算法求出next数组,我们可以枚举下列几种情况
    a[2~7] = “bababa”,它与前缀与a[1~6] = “ababab”不匹配
    a[3~7] = “ababa”,它与前缀a[1~6] = “ababa”匹配,长度为5
    a[4~7] = “baba”, 它与前缀a[1~4] = “abab”不匹配
    a[5~7] = “aba”, 它与前缀a[1~3] = “aba”匹配,长度为3
    a[6~7] = “ba”,它与前缀a[1~2] = “ab”不匹配
    a[7] = “a”, 它与前缀a[1] = “a”匹配,长度为1
    所以,以i = 7结尾, 最多与A的前缀匹配到5,next[7] = 5;

    如何更快的求出next数组?
    我们可以假设next[1~6]已经求出, 按照上述定义,next[6]=4,即a[3~6]与a[1~4]匹配
    接下来a[7] = a[5] = 'a',在该字符上能够继续匹配,有next[6]匹配的长度的最优解为4可知,在a[7]的位置继续匹配,所以next[7] = 5,同理,next[5] = 3
    我们接着考虑next[8],发现a[8] = 'a',与a[6] = 'b'两者不相等,不能把匹配长度从5增长为6.我们只好把匹配长度缩短。以i = 7结尾的匹配长度除了j = 5之外,a[5~7]与a[1~3]还能进行长度为3的匹配,a[7]与a[1]还能进行长度为1的匹配
    我们尝试用这两种稍短的进行匹配,然而我们会发现,a[8]与a[4]或是a[8]与a[2]都不相等,并不能延伸到i = 8,我们只能让i = 8从A字符串开头重新匹配,a[8] = a[1],匹配长度为1,next[8] = 1
    那么我们如何得知要考虑5, 3, 1这些长度的呢。已知next[7] = 5,这说明从7往前5个字符与a[1~5]是相等的,如果存在一个新的j,使得从5往前的j个字符与a[1~j]相等,那么从7往前j个字符与a[1~j]也是相等的。这样的j的最大自然就是next[5]
    同理,考虑完j = next[5] = 3之后,下一个要考虑的匹配长度就是next[3]。

    以下为代码解释

    /*
    假设next[1~(i - 1)]已求出,求next[i] 
    如果相等,就j + 1,如果a[i] != a[j + 1],即扩展失败,令j变为next[j],即从第a[j]的位置再尝试向a[j + 1]扩展 
    直至j等于0(应该从头开始匹配) 
    */
    next[1] = 0;
    for(int i = 2, j = 0; i <= n; i++) {
        while(j > 0 && a[i] != a[j + 1]) j = next[j];
        if(a[i] == a[j + 1]) j++;
        next[i] = j;
    }
    /*
    求解f数组方式
    因为定义的相似性,求解过程基本一致 
    */
    for(int i = 1, j = 0; i <= m; i++) {
        while(j > 0 && (j == n || b[i] != a[j + 1])) j = next[j];//当匹配了n个字符(扩展完成)或是扩展失败时,移到a[j]接着扩展a[j + 1] 
        if(b[i] == a[j + 1]) j++;//字符相等,扩展成功,长度j 加上1 
        f[i] = j;
    }

    最小表示法

    给定一个长度为n的字符串S,我们如果不断把它的最后一个字符放到开头,最终会得到n个字符串,称这n个字符串是循环同构的。这些字符串中字典序最小的一个,称为字符串S的循环同构。这些字符串中字典序最小的一个,称为字符串S的最小表示。
    例如S = “abca”,那么它的循环同构字符串为:abca, bcaa, caab, aabc。这个字符串的最小表示就是aabc
    因为与S循环同构的字符串可以用该字符串在S中的起始下标表示,所以我们用b[i]表示从i开始的循环同构字符串,即:
    s[1~n] + s[1 ~ (i - 1)]
    如何求出一个字符串的最小表示?
    暴力算法:依次比较这n个循环同构的字符串,找到字典序最小的一个。比较两个循环同构字符串b[i]与b[j]时,我们也采用直接向后扫描的方式,依次去k = 0, 1, 2.....,比较b[i + k]与b[j + k]是否相等,直至找到一个不相等的位置,从而确定b[i]与b[j]的大小关系。
    实际上,一个字符串的最小表示可以在O(n)的线性时间内求出。我们首先把S复制一份接在它的尾部得到一个新字符串,我们表示为S2,显然b[i] = s2[i~(i + n - 1)]。
    我们举一个例子:
    S = “bacacabc”,i = 2,j = 4, k = 3
    b a c a c a b c b a c a c a b c
    i i+k
    b a c a c a b c b a c a c a b c
    j j+k
    如果在i+k与j+k处不相等,假设s2[i + k] > s2[j + k],那么我们可以得知b[i]不是S的最小表示。初此之外,我们还可以得知b[i+1],b[i+2]....b[i+k]又都不是S的最小表示,因为对于1<=p<=k,存在比b[i+p]更小的循环同构串b[j+p](i+p与j+p开始向后扫描同样会在p = k时发现不相等),并且s2[i+k]>s2[j+k]
    同理,如果s2[i+k]<s2[j+k],那么b[j],b[j+1],b[j+2]....b[j+k]也都不是S的最小表示,直接跳过这些位置不存在遗漏最小表示的情况。于是我们可以得到以下求最小表示的方法:
    1.初始化i = 1, j = 2.
    2.向后扫描比较b[i]和b[j]两个循环同构串
    (1)如果扫描了n个字符后仍然相等,说明S只由一种字符构成,任意b[i]都是它的最小表示
    (2)如果i+k与j+k处发现不相等
    若s2[i+k]>s2[j+k],令i = i + k + 1。若此时i = j,再令i = i + 1
    若s2[i+k]<s2[j+k],令j = j + k + 1。若此时j = i,再令j = j + 1
    3.若i > n,b[j]为最小表示;若j > n,b[i]为最小表示;否则重复第二步

    int n = strlen(s + 1);
    for(int i = 1; i <= n; i++)
        s[n + i] = s[i];
    int i = 1, j = 2, k;
    while(i <= n && j <= n) {
        for(k = 0; k <= n && s[i + k] == s[j + k]; k++);
        if(k == n) break;
        if(s[i + k] > s[j + k]) {
            i = i + k + 1;
            if(i == j) i++;
        }
        else if(s[i + k] < s[j + k]) .{
            j = j + k + 1;
            if(i == j) j++;
        }
    }
    ans = min(i, j);

  • 相关阅读:
    学习Linux shell脚本中连接字符串的方法
    使用 ffmpeg 转换视频格式
    一点不懂到小白的linux系统运维经历分享
    linux利用scp远程上传下载文件/文件夹
    angular ui-select
    JavaScript sort()方法
    js性能优化
    layer弹出层
    JS复制对象
    某天修改了啥bat批处理
  • 原文地址:https://www.cnblogs.com/ywjblog/p/8857431.html
Copyright © 2020-2023  润新知