zoukankan      html  css  js  c++  java
  • 字符串相关算法学习总结集合

    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);

  • 相关阅读:
    【PAT甲级】1043 Is It a Binary Search Tree (25 分)(判断是否为BST的先序遍历并输出后序遍历)
    Educational Codeforces Round 73 (Rated for Div. 2)F(线段树,扫描线)
    【PAT甲级】1042 Shuffling Machine (20 分)
    【PAT甲级】1041 Be Unique (20 分)(多重集)
    【PAT甲级】1040 Longest Symmetric String (25 分)(cin.getline(s,1007))
    【PAT甲级】1039 Course List for Student (25 分)(vector嵌套于map,段错误原因未知)
    Codeforces Round #588 (Div. 2)E(DFS,思维,__gcd,树)
    2017-3-9 SQL server 数据库
    2017-3-8 学生信息展示习题
    2017-3-5 C#基础 函数--递归
  • 原文地址:https://www.cnblogs.com/ywjblog/p/8857431.html
Copyright © 2011-2022 走看看