习题课4-2(子串(动态规划)、前缀串问题(trie树))

习题课4-2

回文串(二分len数组)

  • 比较正反hash值

  • 线性时间计算出hash值?

  • hash值是可以利用后缀和计算的,因此我们可以在经过O(n)时间的预处理后,O(1)的时间求一个串的hash值。

  • h a s h ( { a x . . y } = ( ∑ i = x n a i B i − x − B y − x + 1 ∑ i = y + 1 n a i B i − y − 1 )   m o d   m o hash(\{a_{x..y}\} = (\sum_{i=x}^na_iB^{i-x}-B^{y-x+1}\sum_{i=y+1}^na_iB^{i-y-1})\space mod\space mo hash({ ax..y}=(i=xnaiBixByx+1i=y+1naiBiy1) mod mo

  • 因此可以在每个位置i上,二分出len(i)数组的值

  • 时间复杂度O(nlogn)

  • 解法:先用mnanacher算法O(n)处理出 s1串的最长回文子串长度 L,那么最后的答案肯定是L,L-2,L-4 ~ 0,并且我们已经求出了 p[i] 数组(以i为中心的最长回文半径),用于后面二分答案的判断。

    这里有个坑点:二分时奇偶回文要分开二分。因为如何有2k+2长度的公共回文。肯定也会有2k长度的公共回文,但是不保证也有2k+1的长度的公共回文。。. 如果不想奇偶分开讨论的话,可以用马拉车的技巧,转化为全部都是奇回文。

  • #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <cmath>
    #include <queue>
    #include <vector>
    #include <map>
    #include <set>
    #include <stack>
    #include <string>
    #include <bits/stdc++.h>
    using namespace std;
     
    typedef unsigned long long ull;
    #define lson l,mid,rt<<1
    #define rson mid+1,r,rt<<1|1
    #define mem(a,b) memset(a,b,sizeof(a))
    #define lowbit(x) x&-x
    const int maxn = 2e6 + 5;
    const int mod = 1e9 + 7;
    const double eps = 1e-6;
    const double pi = acos(-1.0);
    char s[maxn<<1],ss[maxn<<1],s1[maxn],s2[maxn],str[maxn<<1];
    int p[maxn<<1];
    ull hash1[maxn<<1],hash2[maxn<<1],po[maxn<<1];
    int len1,len2;
    ull geth1(int l,int r){
          
          
        if(l == 0) return hash1[r];
        return (ull)(hash1[r]-hash1[l-1]*po[r-l+1]);
    }
    ull geth2(int l,int r){
          
          
        if(l == 0) return hash2[r];
        return (ull)(hash2[r]-hash2[l-1]*po[r-l+1]);
    }
    int manacher(int len){
          
          
        int id = 0,mx = 0;
        int res = 0;
        for(int i = 2; i < len; i++){
          
          
            p[i] = mx>i?min(p[2*id-i],mx-i):1;
            while(str[i+p[i]] == str[i-p[i]]) p[i]++;
            if(i+p[i] > mx){
          
          
                mx = i+p[i];
                id = i;
            }
            res = max(res,p[i]);
            //printf("%d ",p[i]);
        }
        //cout<<res-1<<endl;
        //cout<<endl;
        return res-1;
    }
     
    map<ull,int>mp;
    bool judge(int len,int sz){
          
          
        mp.clear();
        for(int i = 2; i+len-1 < sz; i+=2){
          
           //枚举回文中心
            if(p[i]-1>=len){
          
           //以i为中心的回文子串长度大于二分的长度
                int ra = len/2;  
                int id = i/2-1; //对应回原串回文中心
                mp[geth1(id-ra,id+ra)] = 1;
                //cout<<1;
            }
        }
        for(int i = 0; i+len-1 < len2; i++){
          
          
            if(mp.count(geth2(i,i+len-1))) return true;
        }
        return false;
    }
    int main(){
          
          
        //freopen("in.txt","r",stdin);
        //freopen("out.txt","w",stdout);
        po[0] = 1;
        for(int i = 1; i < maxn*2; i++){
          
          
            po[i] = po[i-1]*31;
        }
        while(~scanf("%s%s",s1,s2)){
          
          
            /*预处理将字符串中的回文串全部变成奇回文*/
            len1 = len2 = 0;
            s[len1++] = 'A';
            for(int i = 0; s1[i]!='\0'; i++){
          
          
                s[len1++] = s1[i];
                s[len1++] = 'A';
            }
            ss[len2++] = 'A';
            for(int i = 0; s2[i]!='\0'; i++){
          
          
                ss[len2++] = s2[i];
                ss[len2++] = 'A';
            }
            /*...........................*/
            /*manacher的预处理*/
            int tot = 0;
            str[tot++] = '$';
            str[tot++] = '#';
            for(int i = 0; i < len1; i++){
          
          
                str[tot++] = s[i];
                str[tot++] = '#';
            }
            str[tot] = '\0';
            /*...................*/
            /*hash处理*/
            hash1[0] = s[0];
            for(int i = 1; i < len1; i++)
                hash1[i] = hash1[i-1]*31+s[i];
            hash2[0] = ss[0];
            for(int i = 1; i < len2; i++)
                hash2[i] = hash2[i-1]*31+ss[i];
            /*..........*/
            int l = 0, r = manacher(tot)/2;  
            /*由于之前的奇回文处理,现在的r是s串的(最长回文串长度-1)/2,其实就是原串的最长回文串长度,因为s串加倍处理过*/
            int ans = 0;
            while(l <= r){
          
          
                int mid = (l+r)>>1; //二分答案长度
                if(judge(mid<<1|1,tot)){
          
           
                    l = mid+1;
                    ans = mid*2+1;
                }else
                    r = mid-1;
            }
            printf("%d\n",ans/2);
        }
        return 0;
    }
    
  • #include<cstdio>
    #include<iostream>
    #include<string>
    #include<vector>
    #include<algorithm>
    using namespace std;
    typedef long long ll;
    const ll mod=1000000007;
    const ll p=10000019;
    const ll maxn=200010;
    ll powp[maxn],h1[maxn],h2[maxn];
    
    void init(){
          
          
    	powp[0]=1;
    	for(int i=1;i<maxn;i++){
          
          
    		powp[i]=(powp[i-1]*p)%mod;
    	}
    }
    	
    void calh(ll h[],string &str){
          
          
    	h[0]=str[0];
    	for(int i=1;i<str.length();i++){
          
          
    		h[i]=(h[i-1]*p+str[i])%mod;
    	}
    }
    
    int calsinglesubh(ll h[],int i,int j){
          
          
    	if(i==0) return h[j];
    	return ((h[j]-h[i-1]*powp[j-i+1])%mod+mod)%mod;
    }
    
    int binarysearch(int l,int r,int len,int i,int iseven){
          
          
    	while(l<r){
          
          
    		int mid=(l+r)/2;
    		int h1l=i-mid+iseven,h1r=i;
    		int h2l=len-1-(i+mid),h2r=len-1-(i+iseven);//难!
    		int hashl=calsinglesubh(h1,h1l,h1r);
    		int hashr=calsinglesubh(h2,h2l,h2r); 
    		if(hashl!=hashr) r=mid; //说明回文半径≤mid 
    		else l=mid+1;
    	}
    	return l-1;
    }
    
    int main(){
          
          
    	init();
    	string str;
    	getline(cin,str);
    	calh(h1,str);
    	reverse(str.begin(),str.end());
    	calh(h2,str);
    	int ans=0;
    	//奇回文 
    	for(int i=0;i<str.size();i++){
          
          
    		int maxlen=min(i,(int)str.length()-1-i)+1;
    		int k=binarysearch(0,maxlen,str.length(),i,0);
    		ans=max(ans,k*2+1);
    	}
    	//偶回文 
    	for(int  i=0;i<str.length();i++){
          
          
    		int maxlen=min(i+1,(int)str.length()-1-i)+1;
    		int k=binarysearch(0,maxlen,str.length(),i,1);
    		ans=max(ans,k*2);
    	}
    	printf("%d\n",ans);
    	return 0; 
    }
    	
    
    

邓老师数

  • 求出所有不超过n的质数

  • 求出所有不超过n的邓老师数

  • 质数是除了1与自身不再有其他因数的正整数

  • 邓老师数:合数,正因数除了1与自身外,全都是质数

  • 质数用筛法求解

代码解析

  • i从2到n枚举

  • 把它的2倍到若干倍但是小于等于n的数标记为合数

  • 筛掉质数倍数的数(质数倍数的数不是质数,但是要判断一下)

  • static class Task {
          
          
    
            // 本函数求解质数或邓老师数(将这两个功能合并在了一起)
            // n, k:意义均与题目描述相符
            // 返回值:如果 k=0,则将所求的质数按从小到大的顺序放入返回值中;如果 k=1,则将所求的邓老师数按从小到大的顺序放入返回值中。
            List<Integer> getAnswer(int n, int k) {
          
          
                
                boolean[] isPrime = new boolean[n+1];
                boolean[] isDeng = new boolean[n+1];
                Arrays.fill(isPrime,true);
                Arrays.fill(isDeng,true);
                List<Integer> ans = new ArrayList<>();
    
                for (int i = 2; i <= n; i++) {
          
          
                    // 筛质数
                    if (isPrime[i]) {
          
          
                        isDeng[i] = false;
                    }
                    // 添加质数
                    if (k==0 && isPrime[i]){
          
          
                        ans.add(i);
                    }
                    // 添加邓老师数
                    if (k==1 && isDeng[i]){
          
          
                        ans.add(i);
                    }
                    // 每个数的倍数
                    for (int j = i+i; j <= n; j += i) {
          
          
                        isPrime[j] = false;
                        // 筛掉合数的倍数
                        if (!isPrime[i]) {
          
          
                            isDeng[j] = false;
                        }
                    }
    
                }
                return ans;
            }
    }
    

子序列

  • aaa的子串是a,aa,aaa,不能重复计算

解法1

  • O(2^n)的时间吧所有的子序列求出来,然后去重

解法2(动态规划)

  • 递推,在竞赛中也称为动态规划

  • f(i)表示s(i)所拥有的不同子序列个数

  • pre(i)表示字符s(i)在s(i-1)最后出现的位置

  • f ( i ) = { f ( i − 1 ) + f ( i − 1 ) + 1 , p r e ( i ) = 0 f ( i − 1 ) + f ( i − 1 ) − f ( p r e ( i ) − 1 ) , p r e ( i ) ≠ 0 f(i)=\begin{cases} f(i-1)+f(i-1)+1 , pre(i)=0\\ f(i-1)+f(i-1)-f(pre(i)-1), pre(i)\neq0\end{cases} f(i)={ f(i1)+f(i1)+1pre(i)=0f(i1)+f(i1)f(pre(i)1)pre(i)=0

  • pre(i)=0

    • 此时s(i)在前面s(i-1)没有出现过

    • 第一个f(i-1):f(i-1)的子序列也是f(i)的子序列

    • 第二个f(i-1):将前i-1的子序列后面都加新字符也可以构成f(i-1)个子序列

    • 单独的字符

  • pre(i)不等于0

    • s(i)在前面s(i-1)中出现过

    • 第一个f(i-1)同上

    • 第二个f(i-1)同上

    • 由于字符已经出现过了,所以第二次加了重复的,减去在pre(i)这个位置以前的字符串子串,加上字符s(i)结尾的子序列

  • for (int i = 1; i <= n; i++) {
          
          
        if (p[i]==0){
          
          
            f[i] = f[i-1] + f[i-1] +1;
        }else {
          
          
            f[i] = f[i-1] + f[i-1] - f[p[i] - 1] + mo;
        }
    }
    
    return f[n]%mo;
    
  • 为什么要加一个mo,因为对mo取余后,做减法的时候可能为负

  • static class Task {
          
          
    
            final int N = 500005, mo = 23333;
    
            /* 全局变量 */
        	// 表示前i个字符形成的本质不同的子序列个数
            int[] f = new int[N];
        	// 表示字符s[i]最后出现的位置
            int[] p = new int[N];
        	// 表示字符i最后出现的位置
            int[] last = new int[26];
    
            // 为了减少复制开销,我们直接读入信息到全局变量中
            // s:题目所给字符串,下标从1开始
            // n:字符串长度
            int n;
            char[] s;
    
            // 求出字符串s有多少不同的子序列
            // 返回值:s不同子序列的数量,返回值对mo取模
            int getAnswer() {
          
          
                
                for (int i = 1; i <= n; i++) {
          
          
                    // 获取0-25的编号
                    int a = s[i] - 'a';
                    p[i] = last[a];
                    // 最后出现的位置
                    last[a] = i;
                }
    
                for (int i = 1; i <= n; i++) {
          
          
                    if (p[i]==0){
          
          
                        f[i] = (f[i-1] + f[i-1] +1)%mo;
                    }else {
          
          
                        f[i] = (f[i-1] + f[i-1] - f[p[i] - 1])%mo;
                    }
                }
    
                return f[n]%mo;
    
    
            }
    
           
    }
    

前缀

  • 给定n个字符串,询问m次,每个询问给出一个字符串,求出这个字符串是n个字符串里,多少个串的前缀
  • 前缀:从头开始的一段连续子串。比如字符串ab是字符串abcd的前缀,也是字符串ab的前缀,但是不是bab的前缀

解法1

  • 将每个字符串的前缀全部拉出来,然后丢进hash表

  • 时间复杂度是O(L^2+mk)

  • const int base1 = 233;
    const int mo1 = 998244353;
    
    const int base2 = 31;
    const int mo2 = 19260817;
    
    // set是可重复的
    // multiset是不可重复的
    multiset<pair<int,int>> Hash;
    
    
    void add(char *s){
    	int h1 = 0;
    int h2 = 0;
    	for(;*s!=0;++s){
            // 需要从1开始
    		int a = *s - 'a' +1;
    		h1 = ((long long)h1*base1 +a)%mo1;
    		h2 = ((long long)h2*base2 +a)%mo2;
    		Hash.insert(pair<int,int>(h1,h2);
    	}
    }
    

解法2

  • trie树

代码解析

  • trie树上的边,c(x)(y)表示从x节点出发(x从1开始),字符为y的边(y从0到25),

  • trie树的根节点是0,是起点,但没有字符的点

  • 遍历一个字符时,如果最终停止在终止节点,说明这个字符出现过

  • 在trie中添加新字符串

  • int cnt = 0;
    void add(String s) {
          
          
                int x = 0;
                for (int i = 0; i < s.length(); i++) {
          
          
                    int y = s.charAt(i) - 'a';
                    if (c[x][y] == 0) {
          
          
                        c[x][y] = ++cnt;
                    }
                    x = c[x][y];//将该字符标记为某节点(从1开始)
                }
                ++sz[x];//最终终止的节点
            }
    
  • 最终是终止节点,所以终止节点的数目+1

  • 对于非终止节点,如何计算size

  • void dfs(int x) {
          
          
                for (int y = 0; y < 26; y++) {
          
          
                    int z = c[x][y];
                    if (z!=0){
          
          
                        dfs(z);// 没有终止,继续向下递归
                        sz[x] += sz[z];
                    }
                }
            }
    
  • 把x的子节点的所有sz都算出来,然后都加到x身上

  • int walk(String s) {
          
          
                int x = 0;
                for (int i = 0; i < s.length(); i++) {
          
          
                    int y = s.charAt(i) - 'a';
                    if (c[x][y] == 0) {
          
          
                        return 0;//不存在该条边,直接返回0
                    }
    
                    x = c[x][y];
                }
                return x;//返回最终停止在哪个节点上
            }
    

标程

  • static class Task {
          
          
    
          
    
            final int M = 505, L = 1000005;
    
            // c:trie树上的边,c[x][y]表示从节点x出发(x从1开始),字符为y的边(y范围是0到25)
            // sz:sz[x]表示x节点的子树中终止节点的数量(子树包括x自身)
            // cnt:trie树上节点的数目
       		// 通过统计终止节点个数可以知道字符串数目
            int[][] c = new int[L][26];
            int[] sz = new int[L];
            int cnt;
    
            // 将字符串s加入到trie树中
            // s:所要插入的字符串
            void add(String s) {
          
          
                
                int x = 0;
                for (int i = 0; i < s.length(); i++) {
          
          
                    // 形成0-25
                    int y = s.charAt(i) - 'a';
                    // 边不存在,新建一个节点
                    if (c[x][y] == 0) {
          
          
                        c[x][y] = ++cnt;
                    }
                    // 沿着边继续往下走
                    x = c[x][y];
                }
                // 此时x是终止节点
                ++sz[x];
            }
    
    
            // 用于计算sz数组
            // x:当前节点
        	// 可以把这一堆去掉,把{ ++sz[x];}这一句上移到for循环里面就可以了
            void dfs(int x) {
          
          
                
                for (int y = 0; y < 26; y++) {
          
          
                    int z = c[x][y];
                    if (z!=0){
          
          
                        dfs(z);
                        sz[x] += sz[z];
                    }
                }
            }
        
    
            // 用字符串s沿着trie树上走,找到相应的节点
            // s:所给字符串
            // 返回值:走到的节点
            int walk(String s) {
          
          
                
                int x = 0;
                for (int i = 0; i < s.length(); i++) {
          
          
                    int y = s.charAt(i) - 'a';
                    if (c[x][y] == 0) {
          
          
                        return 0;
                    }
    
                    x = c[x][y];
                }
                return x;
            }
     		void solve(InputReader in, PrintWriter out) {
          
          
                int n = in.nextInt(), m = in.nextInt();
                for (; n-- != 0; )
                    add(in.next());
                dfs(0);
                sz[0] = 0;
                for (; m-- != 0; )
                    out.println(sz[walk(in.next())]);
            }
    }
    

猜你喜欢

转载自blog.csdn.net/Markland_l/article/details/109282722
今日推荐