看题解区学习了一下,整体的思路倒是不难,主要就是一个rolling hash的过程,然后就是关于 MOD 的选取,到现在也不是很清楚怎么选 MOD。
public String longestDupSubstring(String S) { intn= S.length(); int [] nums =newint[n]; for (inti=0; i < S.length(); i++) { nums[i] = S.charAt(i) - 'a'; } longMOD=1L<<32; intB=26; intleft=1; intright= n - 1; intstart= -1, mlen = 0; while (left <= right){ intmid= left + (right - left)/2; inttemp= RabinKarp(mid, B, nums, MOD); if (temp != -1){ if (mid > mlen){ mlen = mid; start = temp; } left = mid + 1; }else{ right = mid - 1; } } return start == -1 ? "" : S.substring(start, start + mlen); }
publicintRabinKarp(int len, int B, int[] nums, long MOD){ //hash(S) = s[0] * B^(len-1) + s[1] * B^(len-2) + ... + s[n-1] *1 //B^(len-1) (移除左端点时需要的值) longBL=1; for (inti=0; i < len - 1; i++){ BL = (BL * B) % MOD; } //[0,len-1] 的 hash 值 longh=0; for (inti=0; i < len; i++){ h = (h * B + nums[i]) % MOD; } HashSet<Long> set = newHashSet<>(); set.add(h); //rolling hash for (inti=1; i <= nums.length - len; i++){ //+MOD 是为了负数取模 h = (h - nums[i - 1] * BL % MOD + MOD) % MOD; h = (h * B + nums[i + len - 1]) % MOD; if (set.contains(h)){ return i; } set.add(h); } return -1; }
解法二
补充一下冲突检测,猜 mod 也太玄学了😂
这个冲突检测的方法有问题,留下做个印证,正确的检测请直接看 解法三
//写一波检测冲突的 public String longestDupSubstring(String S) { intn= S.length(); longMOD= (long) 1e9+7; intB=26; intleft=1; intright= n - 1; intstart= -1, mlen = 0; while (left <= right){ intmid= left + (right - left)/2; inttemp= RabinKarp(mid, B, S, MOD); if (temp != -1){ if (mid > mlen){ mlen = mid; start = temp; } left = mid + 1; }else{ right = mid - 1; } } return start == -1 ? "" : S.substring(start, start + mlen); }
publicintRabinKarp(int len, int B, String S, long MOD){ //hash(S) = s[0] * B^(len-1) + s[1] * B^(len-2) + ... + s[n-1] *1 //B^(len-1) (移除左端点时需要的值) longBL=1; for (inti=0; i < len - 1; i++){ BL = (BL * B) % MOD; } //[0,len-1] 的 hash 值 longh=0; for (inti=0; i < len; i++){ h = (h * B + S.charAt(i) - 'a') % MOD; } //这里肯定不能直接存字符串做冲突检测,太大了会 MLE //存一个起始地址就可以了 len 已知 HashMap<Long,Integer> map = newHashMap<>(); map.put(h, 0); //rolling hash for (inti=1; i <= S.length() - len; i++){ //+MOD 是为了负数取模 h = (h - (S.charAt(i-1) - 'a') * BL % MOD + MOD) % MOD; h = (h * B + (S.charAt(len + i -1) - 'a')) % MOD; Integerstart= map.get(h); if (start != null && S.substring(start, start + len).equals(S.substring(i, i + len))){ return i; } map.put(h, i); } return -1; }
关于这个冲突检测有一个小问题,我尝试减小了MOD的大小,比如 101,这样计算出来的结果在数据量较大的就不对了,得到的字符虽然确实是重复了,但是并不是最长的,按道理写了冲突检测后即使我 MOD 取 1 应该都是可以找出来的啊?======> 在写上面这段话的时候突然想明白了,因为MOD取的太小,导致冲突的概率大大增加,而这里我做冲突检测的时候只保存了一个值,也就是说会有很多值被舍弃掉,也许你舍弃的值可能恰好就是最后的答案,字符越长,发生这种情况的概率就越高,所以说我这里冲突检测做的并不完全,能过也纯属运气,正确的冲突检测应该保存一个List链表,然后在发生冲突的时候在 List 中找有没有和当前字符相等的,这样一来,时间复杂度就会上去(其实这个过程就是设计 Hash 表的过程,链地址法解决冲突)
解法三
下面的应该就没什么问题了,链地址法,时间复杂度会增大,耗时增加到了 500ms+,但是确保了 100%的正确率,这是值得的,这次把 mod 调小也不会出错了(但是可能会 TLE,冲突变大了),总体来说,这几番折腾收获还是挺大的,好事还是要多磨啊
//再写一波检测冲突。 //上面的写的有问题,检测不完全 public String longestDupSubstring(String S) { intn= S.length(); longMOD= (long) 1e9+7; intB=26; intleft=1; intright= n - 1; intstart= -1, mlen = 0; while (left <= right){ intmid= left + (right - left)/2; inttemp= RabinKarp(mid, B, S, MOD); if (temp != -1){ if (mid > mlen){ mlen = mid; start = temp; } left = mid + 1; }else{ right = mid - 1; } } return start == -1 ? "" : S.substring(start, start + mlen); }
publicintRabinKarp(int len, int B, String S, long MOD){ //hash(S) = s[0] * B^(len-1) + s[1] * B^(len-2) + ... + s[n-1] *1 //B^(len-1) (移除左端点时需要的值) longBL=1; for (inti=0; i < len - 1; i++){ BL = (BL * B) % MOD; } //[0,len-1] 的 hash 值 longh=0; for (inti=0; i < len; i++){ h = (h * B + S.charAt(i) - 'a') % MOD; } //这里肯定不能直接存字符串做冲突检测,太大了会 MLE //存一个起始地址就可以了 len 已知 HashMap<Long,List<Integer>> map = newHashMap<>(); map.put(h, newArrayList(){{add(0);}}); //rolling hash for (inti=1; i <= S.length() - len; i++){ //+MOD 是为了负数取模 h = (h - (S.charAt(i-1) - 'a') * BL % MOD + MOD) % MOD; h = (h * B + (S.charAt(len + i -1) - 'a')) % MOD; List<Integer> starts = map.get(h); if (check(starts, i, S, len)){ return i; } if (starts == null){ List<Integer> lis = newArrayList<>(); lis.add(i); map.put(h, lis); }else{ starts.add(i); } } return -1; }
publicbooleancheck(List<Integer> starts, int i, String s, int len){ if(starts == null || starts.size() <= 0){ returnfalse; } for (int left : starts) { if(s.substring(left, left + len).equals(s.substring(i, i + len))){ returntrue; } } returnfalse; }
哦,不!你不小心把一个长篇文章中的空格、标点都删掉了,并且大写也弄成了小写。像句子"I reset the computer. It still didn’t boot!"已经变成了"iresetthecomputeritstilldidntboot"。在处理标点符号和大小写之前,你得先把它断成词语。当然了,你有一本厚厚的词典dictionary,不过,有些词没在词典里。假设文章用sentence表示,设计一个算法,把文章断开,要求未识别的字符最少,返回未识别的字符数。
注意: 本题相对原题稍作改动,只需返回未识别的字符数
示例:
输入: dictionary = ["looked","just","like","her","brother"] sentence = "jesslookedjustliketimherbrother" 输出: 7 解释: 断句后为"jess looked just like tim her brother",共 7 个未识别字符。