浅谈求最长公共子串LCP的几种做法

定义:

以这题为例PIPIOJ1476

做法1、动态规划

 

代码:


import java.io.OutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.StringTokenizer;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.InputStream;
 
/**
 * Built using CHelper plug-in
 * Actual solution is at the top
 */
public class Main {
    public static void main(String[] args) {
        InputStream inputStream = System.in;
        OutputStream outputStream = System.out;
        InputReader sc = new InputReader(inputStream);
        PrintWriter out = new PrintWriter(outputStream);
        Task solver = new Task();
        solver.solve(1, sc, out);
        out.close();
    }
 
    static class Task {
    	
        public int calc(String s,String t,int n) {
        	int[][] dp=new int[n+1][n+1];
        	int ans=0;
        	for(int i=1;i<=n;i++) {
        		for(int j=1;j<=n;j++) {
        			if(s.charAt(i)==t.charAt(j))
        				dp[i][j]=dp[i-1][j-1]+1;
        			else
        				dp[i][j]=0;
        			ans=Math.max(ans, dp[i][j]);
        		}
        	}
        	return ans;
        }
    	
        public void solve(int testNumber, InputReader sc, PrintWriter out) {
            int n=sc.nextInt();
            String s=" "+sc.next();
            String t=" "+sc.next();
            out.println(calc(s,t,n));
        }
    }
     
    static class InputReader {
        public BufferedReader reader;
        public StringTokenizer tokenizer;
 
        public InputReader(InputStream stream) {
            reader = new BufferedReader(new InputStreamReader(stream), 32768);
            tokenizer = null;
        }
 
        public String next() {
            while (tokenizer == null || !tokenizer.hasMoreTokens()) {
                try {
                    tokenizer = new StringTokenizer(reader.readLine());
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            return tokenizer.nextToken();
        }
 
        public int nextInt() {
            return Integer.parseInt(next());
        }
 
        public long nextLong() {
            return Long.parseLong(next());
        }
         
        public boolean hasNext() {
            try {
                String string = reader.readLine();
                if (string == null) {
                    return false;
                }
                tokenizer = new StringTokenizer(string);
                return tokenizer.hasMoreTokens();
            } catch (IOException e) {
                return false;
            }
        }
 
    }
}
 

理论上这种做法提交上述的例题返回的结果应该是TLE,但实际返回的是MLE(尴尬hhh),因为这种做法的空间复杂度也是n^2级别的。

做法2:hash+二分

可以考虑多hash因为本题中串的长度过大,单hash的话很容易产生冲突。在对两个串分别完成hash之后,我们可以二分LCP的长度len,然后再对答案进行check。简单提一下check的做法,首先我们记录下S中长度为len的子串的hash值,然后再枚举T串中长度为len的子串的hash值,看是否存在hash值相等的情况。

代码:



import java.io.OutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.StringTokenizer;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.InputStream;
 
/**
 * Built using CHelper plug-in
 * Actual solution is at the top
 */
public class Main {
    public static void main(String[] args) {
        InputStream inputStream = System.in;
        OutputStream outputStream = System.out;
        InputReader sc = new InputReader(inputStream);
        PrintWriter out = new PrintWriter(outputStream);
        Task solver = new Task();
        solver.solve(1, sc, out);
        out.close();
    }
 
    static class Task {
        public HashSet<Long> set1=new HashSet<>();
        public HashSet<Long> set2=new HashSet<>();
         
        public boolean check(int len,Hash hash1,Hash hash2,int n) {
            set1.clear();
            set2.clear();
            for(int i=0;i+len-1<n;i++) {
                set1.add(hash1.get(0, i, i+len-1));
                set2.add(hash1.get(1, i, i+len-1));
            }
            for(int i=0;i+len-1<n;i++) {
                if(set1.contains(hash2.get(0, i, i+len-1))&&set2.contains(hash2.get(1, i, i+len-1)))
                    return true;
            }
            return false;
        }
         
        public void solve(int testNumber, InputReader sc, PrintWriter out) {
            int n=sc.nextInt();
            String s=sc.next();
            String t=sc.next();
            Hash hash1=new Hash(s);
            Hash hash2=new Hash(t);
             
            int l=1;
            int r=n;
            int ans=0;
            while(l<=r) {
                int mid=(l+r)>>1;
                if(check(mid,hash1,hash2,n)) {
                    ans=mid;
                    l=mid+1;
                }else
                    r=mid-1;
            }
            out.println(ans);
        }
    }
     
    static class Hash{
        public static final long[] SEED=new long[] {31,37};
        public static final long[] MOD=new long[] {(long) (1e9+7),998244353};
        long[][] pow;
        long[][] v;
        char[] res;
         
        public Hash(String s) {
            this.res=(" "+s).toCharArray();
            this.pow=new long[MOD.length][s.length()+1];
            this.v=new long[MOD.length][s.length()+1];
            this.init();
        }
         
        public void init() {
            pow[1][0]=pow[0][0]=1;
            for(int i=0;i<MOD.length;i++) {
                for(int j=1;j<res.length;j++) {
                    pow[i][j]=pow[i][j-1]*SEED[i]%MOD[i];
                    v[i][j]=v[i][j-1]*SEED[i]+res[j]-'a'+1;  //防止"aaaaaa"这样的串出现错误
                    v[i][j]%=MOD[i];
                }
            }
        }
         
        public long get(int index,int l,int r) {            //此处的l,r下标从0开始
            l++;
            r++;
            long temp=(v[index][r]-v[index][l-1]*pow[index][r-l+1]%MOD[index]+MOD[index])%MOD[index];             //H(T)=(H(S+T)-H(S)*base^(T_length)+MOD)%MOD
            return (temp+MOD[index])%MOD[index];
        }
    }
     
     
    static class InputReader {
        public BufferedReader reader;
        public StringTokenizer tokenizer;
 
        public InputReader(InputStream stream) {
            reader = new BufferedReader(new InputStreamReader(stream), 32768);
            tokenizer = null;
        }
 
        public String next() {
            while (tokenizer == null || !tokenizer.hasMoreTokens()) {
                try {
                    tokenizer = new StringTokenizer(reader.readLine());
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            return tokenizer.nextToken();
        }
 
        public int nextInt() {
            return Integer.parseInt(next());
        }
 
        public long nextLong() {
            return Long.parseLong(next());
        }
         
        public boolean hasNext() {
            try {
                String string = reader.readLine();
                if (string == null) {
                    return false;
                }
                tokenizer = new StringTokenizer(string);
                return tokenizer.hasMoreTokens();
            } catch (IOException e) {
                return false;
            }
        }
 
    }
}
 

正确通过本题:

但由于常数过大的缘故,还是比较慢的。

做法3:后缀数组

以上截取自IOI2009 国家集训队论文《后缀数组——处理字符串的有力工具》

代码:


import java.io.OutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.StringTokenizer;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.InputStream;

/**
 * Built using CHelper plug-in
 * Actual solution is at the top
 */
public class Main {
    public static void main(String[] args) {
        InputStream inputStream = System.in;
        OutputStream outputStream = System.out;
        InputReader sc = new InputReader(inputStream);
        PrintWriter out = new PrintWriter(outputStream);
        Task solver = new Task();
        solver.solve(1, sc, out);
        out.close();
    }

    static class Task {
        public void solve(int testNumber, InputReader sc, PrintWriter out) {
           while(sc.hasNext()) {
        	   int n=sc.nextInt();
        	   String s=sc.next();
               String t=sc.next();
               String temp=s+"#"+t;
               SuffixArray SA=new SuffixArray(temp);
               SA.getSA();
               SA.getHeight();
               System.out.println(SA.getAns(s.length()));
           }
        }
    }
    
    static class SuffixArray{
    	public static int Max=(int) (1e5+10);
		public char[] s;
		public int[] x;    //x[i]是第i个元素的第一关键字(值)
		public int[] y;    //y[i]表示第二关键字排名为i的数,第一关键字的位置(下标)
		public int[] c;    //桶
		public int[] sa;   //表示字典序第i个后缀的起始下标
		public int[] height;  //height[i]=suffix(sa[i-1])和 suffix(sa[i])的最长公共前缀    排名为i和i-1的后缀的最长公共前缀
		public int[] rank;   //起始位置的下标为i的后缀的排名
		public int[][] ST;
		
		public SuffixArray(String res) {
			s=(" "+res).toCharArray();
			x=new int[s.length+1];
			y=new int[s.length+1];
			c=new int[Math.max(Max, s.length+1)];
			sa=new int[s.length+1];
			height=new int[s.length+1];
			rank=new int[s.length+1];
			//ST=new int[s.length+1][30];
		}
		
		public void getSA() {
			int n=s.length-1;
			int m=200;              //字符个数    排序的过程会发生改变
			for(int i=1;i<=n;i++)
				c[x[i]=s[i]]++;
			for(int i=2;i<=m;i++)
				c[i]+=c[i-1];    做c的前缀和,我们就可以得出每个关键字最多是在第几名
			for(int i=n;i>=1;i--)
				sa[c[x[i]]--]=i;
			for(int k=1;k<=n;k<<=1) {        
				int num=0;
				for(int i=n-k+1;i<=n;i++)   //y[i]表示第二关键字排名为i的数,第一关键字的位置	
					y[++num]=i;             //第n-k+1到第n位是没有第二关键字的 所以排名在最前面
				for(int i=1;i<=n&&num<n;i++) {
					if(sa[i]>k)
						y[++num]=sa[i]-k;     //如果满足(sa[i]>k) 那么它可以作为别人的第二关键字,就把它的第一关键字的位置添加进y就行了
				}
				for(int i=0;i<=m;i++)
					c[i]=0;
				for(int i=1;i<=n;i++)
					c[x[i]]++;
				for(int i=2;i<=m;i++)
					c[i]+=c[i-1];    //第一关键字排名为1~i的数有多少个
				for(int i=n;i>=1;i--) {    //因为y的顺序是按照第二关键字的顺序来排
					sa[c[x[y[i]]]--]=y[i];  //第二关键字靠后的,在同一个第一关键字桶中排名越靠后
					y[i]=0;
				}
				
				int[] temp=x;       //此时y中存的是值
				x=y;
				y=temp;
				
				x[sa[1]]=1;    //对关键字(值)重新编号
				num=1;
				for(int i=2;i<=n;i++) {
					 x[sa[i]]=(y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k]) ? num : ++num;    
				}
				if(num==n)      //排序完成 ,所有关键字已经两两不同
					break;
				m=num;    
			}
		}
		
		public void getHeight() {
			int k=0;
			int n=s.length-1;
			for(int i=1;i<=n;i++)
				rank[sa[i]]=i;
			for (int i=1; i<=n; ++i) {                       //h[i]=height[rank[i]],也就是 suffix(i)和在它前一名的后缀的最长公共前缀。
		        if (rank[i]==1) continue;//第一名height为0
		        if (k>0) 
		        	--k;//h[i]>=h[i-1]-1;
		        int j=sa[rank[i]-1];
		        while (j+k<=n && i+k<=n && s[i+k]==s[j+k])   //增量法  每次求的都是当前的h[i]
		        	++k;
		        height[rank[i]]=k;//h[i]=height[rank[i]];
		    }
		}
		
		public int getAns(int n) {
			int ans=0;
			
			for(int i=2;i<height.length;i++) {
				if((sa[i]<=n&&sa[i-1]>n+1)||(sa[i-1]<=n&&sa[i]>n+1))
					ans=Math.max(ans, height[i]);
			}
			return ans;
		}
		
	/*	public void initLCP() {
			for(int i=0;i<ST.length;i++)
				Arrays.fill(ST[i], Integer.MAX_VALUE);
			for(int i=2;i<ST.length;i++)
				ST[i][0]=height[i];
			int t=(int) (Math.log((double)height.length-1)/Math.log(2.0)+1);
			for(int j=1;j<t;j++) {
				for(int i=1;i+(1<<j)-1<ST.length;i++)
					ST[i][j]=Math.min(ST[i][j-1], ST[i+(1<<(j-1))][j-1]);
			}
		}
		
		public int getLCP(int l,int r) {
			if(l==r)
				return s.length-l;
			l++;
			int k=(int) (Math.log((double)(r-l+1))/Math.log(2.0));
			return Math.min(ST[l][k], ST[r-(1<<k)+1][k]);
		}*/
	}

    static class InputReader {
        public BufferedReader reader;
        public StringTokenizer tokenizer;

        public InputReader(InputStream stream) {
            reader = new BufferedReader(new InputStreamReader(stream), 32768);
            tokenizer = null;
        }

        public String next() {
            while (tokenizer == null || !tokenizer.hasMoreTokens()) {
                try {
                    tokenizer = new StringTokenizer(reader.readLine());
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            return tokenizer.nextToken();
        }

        public int nextInt() {
            return Integer.parseInt(next());
        }

        public long nextLong() {
            return Long.parseLong(next());
        }
        
        public boolean hasNext() {
            try {
                String string = reader.readLine();
                if (string == null) {
                    return false;
                }
                tokenizer = new StringTokenizer(string);
                return tokenizer.hasMoreTokens();
            } catch (IOException e) {
                return false;
            }
        }

    }
}

可以看到这种做法要前面远优于两种做法。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值