【题目】*面试题 17.13. 恢复空格
哦,不!你不小心把一个长篇文章中的空格、标点都删掉了,并且大写也弄成了小写。像句子"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个未识别字符。
提示:
0 <= len(sentence) <= 1000
dictionary中总字符数不超过 150000。
你可以认为dictionary和sentence中只包含小写字母。
【解题思路1】哈希表+ 动态规划
dp数组的定义:dp[i] 表示考虑前 i 个字符最少的未识别的字符数量,从前往后计算 dp 值。
转移方程:每次转移的时候考虑第 j(j≤i) 个到第 i 个字符组成的子串 sentence[j−1⋯i−1] (字符串下标从 0 开始)是否能在词典中找到
- 如果能找到的话
dp[i]=min(dp[i],dp[j−1])
- 如果没有找到的话复用 dp[i−1] 的状态再加上当前未被识别的第 i 个字符,
dp[i]=dp[i−1]+1
主要问题是如何快速判断当前子串是否存在于词典中
class Solution {
public int respace(String[] dictionary, String sentence) {
Set<String> dic = new HashSet<>();
for(String str: dictionary) dic.add(str);
int n = sentence.length();
//dp[i]表示sentence前i个字符所得结果
int[] dp = new int[n+1];
for(int i=1; i<=n; i++){
dp[i] = dp[i-1]+1; //先假设当前字符作为单词不在字典中
for(int j=0; j<i; j++){
if(dic.contains(sentence.substring(j,i))){
dp[i] = Math.min(dp[i], dp[j]);
}
}
}
return dp[n];
}
}
class Solution {
static final long P = Integer.MAX_VALUE;
static final long BASE = 41;
public int respace(String[] dictionary, String sentence) {
Set<Long> hashValues = new HashSet<Long>();
for (String word : dictionary) {
hashValues.add(getHash(word));
}
int[] f = new int[sentence.length() + 1];
Arrays.fill(f, sentence.length());
f[0] = 0;
for (int i = 1; i <= sentence.length(); ++i) {
f[i] = f[i - 1] + 1;
long hashValue = 0;
for (int j = i; j >= 1; --j) {
int t = sentence.charAt(j - 1) - 'a' + 1;
hashValue = (hashValue * BASE + t) % P;
if (hashValues.contains(hashValue)) {
f[i] = Math.min(f[i], f[j - 1]);
}
}
}
return f[sentence.length()];
}
public long getHash(String s) {
long hashValue = 0;
for (int i = s.length() - 1; i >= 0; --i) {
hashValue = (hashValue * BASE + s.charAt(i) - 'a' + 1) % P;
}
return hashValue;
}
}
【解题思路2】字典树Trie + 动态规划
将词典中所有的单词「反序」插入字典树中,然后每次转移的时候我们从当前的下标 i 出发倒序遍历 i−1,i−2,⋯,0。在 Trie 上从根节点出发开始走,直到走到当前的字符 sentence[j] 在 Trie 上没有相应的位置,说明 sentence[j⋯i−1] 不存在在词典中,且它已经不是「任意一个单词的后缀」,此时直接跳出循环即可。否则,需要判断当前的子串是否是一个单词,这里直接在插入 Trie 的时候在单词末尾的节点打上一个 isEnd 的标记即可,这样在走到某个节点的时候就可以判断是否是一个单词的末尾并更新的 dp 值。
使用哈希表的话,同139. 单词拆分,缺点是比起字典树,会多出一些不必要的判断,如果词典:[‘aabc’, ‘babc’, ‘cbc’] ,在倒序枚举的时候检查 dc 这个子串没出现在词典中以后就没必要再接着往前枚举是否有合法的子串了,因为 dc 本身已经不是词典中「任意一个单词的后缀」。
class Solution {
public int respace(String[] dictionary, String sentence) {
int n = sentence.length();
Trie root = new Trie();
for (String word: dictionary) {
root.insert(word);
}
int[] dp = new int[n + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int i = 1; i <= n; ++i) {
dp[i] = dp[i - 1] + 1;
Trie curPos = root;
for (int j = i; j >= 1; --j) {
int t = sentence.charAt(j - 1) - 'a';
if (curPos.next[t] == null) {
break;
} else if (curPos.next[t].isEnd) {
dp[i] = Math.min(dp[i], dp[j - 1]);
}
if (dp[i] == 0) {
break;
}
curPos = curPos.next[t];
}
}
return dp[n];
}
}
class Trie {
public Trie[] next;
public boolean isEnd;
public Trie() {
next = new Trie[26];
isEnd = false;
}
public void insert(String s) {
Trie curPos = this;
for (int i = s.length() - 1; i >= 0; --i) {
int t = s.charAt(i) - 'a';
if (curPos.next[t] == null) {
curPos.next[t] = new Trie();
}
curPos = curPos.next[t];
}
curPos.isEnd = true;
}
}