Leetcode & Java#面试题17.13 / 309

Posted zz123333

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Leetcode & Java#面试题17.13 / 309相关的知识,希望对你有一定的参考价值。


面试题 17.13 恢复空格【200709每日一题】

方法:字典树trie + 动态规划

这道题真的是要搞死我了…… 先来搞简单的部分—— 动态规划。

(dp[i]) 表示句子里前 i 个字符中最少的未识别字符数,此处前 i 个字符对应的是字符串 (sentence.substring(0, i)) ,也就是由第 0 ~ i - 1 个字符组成的字符串。当我们用字典中的词条去切割句子获得单词时,剩下没有成词的字符就是所有的未识别字符。由于不能确定哪种切割方式能留下最少的未识别字符,所以在求 (dp[i]) 时,需要遍历第 i 个字符前面所有的字符,来比较在哪一处切割能获得最小值。用 j 来表示最后一次成词时的切割位置( (0 <= j < i) ),如果 (sentence.substring(j, i)) 是字典中的单词,则 (dp[j]) (前 j 个字符的最少的未识别字符数)是 (dp[i]) 的一个候选值。

(dp[i]) 的默认值为 $$dp[i] = dp[i-1] + 1$$ ,可以看作是第 i - 1 个字符无法与前面的字符组合成词时的情况。此外,因为所求为最小值,所以如果当前字符串 (sentence.substring(j, i)) 不能组成单词,(dp[j] + i - j) 肯定大于或等于 (dp[i-1] + 1) ,不会是最小值候补,无需考虑。

考虑 (dp[0]) 的情况。显然,(dp[0] = 0),用来表示 sentence 中字符数为 0 或还未开始遍历时未识别字符数为 0 。

动态规划部分的代码如下:

int n = sentence.length();
int[] dp = new int[n + 1];
dp[0] = 0;
for(int i = 1; i < n + 1 ; i++){
	dp[i] = dp[i-1] + 1;
	for(int j = 0; j < i; j++){
		// 检查 sentence.substring(j, i) 是否为字典中的单词
		if(root.inTrie(sentence.substring(j, i), root)){
			dp[i] = Math.min(dp[j],dp[i]);
		}
	}
}

接下来需要解决的问题就是如何存储 & 查询字典中的词条。此处采用的是字典树 trie 的方式。

本质上,字典树将单词中的每一个字符当作一个节点存储在树中。假定单词中只包含小写字母,则理论上一个节点可以后接26个子节点。在二叉树中,我们可以用 node.left 和 node.right 来表示左右子节点,但在字典树中,如果类似地用 26 个字母来表示子节点就太繁琐太复杂了。为了方便表示,我用了哈希表来存储子节点(也可以用数组或者其他map 结构),其中 Key 值表示子节点对应的字符(可以理解为二叉树中的 node.val),Value 值表示字符对应的子节点。

还需要注意的是,在二叉树中,想要判断当前节点是否为叶子节点,方法是检查当前节点的左右子节点是否均为空,但这种做法并不能解决字典树中如何判断是否检索到词尾的问题。举例来说,假设词典中存有 "fun", "funny" 两个词,如何判断词典中收录了 "fun"? 如果还是用有无子节点来判断,显然是不正确的。所以需要一个属性来判断当前节点是否为一个单词的词尾,也就是代码中的 TrieNode.isWord.

字典树 TrieNode 的代码实现如下:

class TrieNode{
    // 字典树
    public boolean isWord;
        // 感觉官方题解里面用 isEnd 表示更贴切
    public Map<Character, TrieNode> next;
        // 也可以令 next 为数组形式
        
    TrieNode(){
        isWord = false;
            //只有在当前字符为单词结尾时才令该值为真
        next = new HashMap<>();
    }
}

在本题中,需要实现字典树的插入和查找操作。首先,要将题目给出的 String[] dictionary 插入到字典中。其实这个 add 函数不难写,只是因为子节点使用 HashMap 表示的,所以需要搞清楚如何在哈希表中查询/插入节点(字符)。最后也不要忘记将词尾处的 isWord 的值改为 true。

public void add(String word, TrieNode root){
	// 插入新词
	TrieNode current = root; 
		// 根节点不表示任何字符,首字母应在 root.next 的 map 中查找
	int n = word.length();
	for(int i = 0; i < n; i++){
            char c = word.charAt(i);
            TrieNode next = current.next.get(c);
                  // 在 current.next 这个 map 中查找,获取当前字符对应的节点
            if(next == null){
                  // 若 next == null,即 map 中没有这个节点 --> 没有这个字符
                  // 在 current.next 这个 map 中添加当前字符 --> 添加<字符,节点>
                  // 此处应创建新的空白节点,等待添加后续字符
                  current.next.put(c, new TrieNode());
            }
                  // 后移 current 至当前字符对应的节点
                  // 为了便于理解,此处类比二叉树中 node = node.left 的写法
                  // 实际可以直接写成 current = next;
            current = current.next.get(c);      
      }
      if(!current.isWord){
            current.isWord = true;
      }
}

字典组建完毕后,就需要实现用于动态规划部分的查找操作了。查找的思路其实和插入是一致的,都是检查:【1】当前字符是否为树中当前节点的子节点【2】词尾处的 isWord 的值,此处就不赘述了。

public boolean inTrie(String word, TrieNode root){
        // 查看 word 是否已在树中
        TrieNode current = root;
        int n = word.length();
        for(int i = 0; i < n; i++){
            char ch = word.charAt(i);
            TrieNode next = current.next.get(ch);
            if(next == null){
                return false;
            }
            current = next;
        }
        return current.isWord;
}

最后给出完整代码:

class Solution {
    public int respace(String[] dictionary, String sentence) {
        int n = sentence.length();    
        int[] dp = new int[n + 1];
        dp[0] = 0;
        TrieNode root = new TrieNode();
        if(n != 0){
            for(int i = 0; i < dictionary.length; i++){
                root.add(dictionary[i], root);
            }            
        }

        for(int i = 1; i < n + 1 ; i++){
            dp[i] = dp[i-1] + 1;
            for(int j = 0; j < i; j++){
                if(root.inTrie(sentence.substring(j, i), root)){
                    dp[i] = Math.min(dp[j],dp[i]);
                }
            }
        }
        return dp[n];
    }
}

class TrieNode{
    // 字典树
    public boolean isWord;
        // 感觉官方题解里面用 isEnd 表示更贴切
    public Map<Character, TrieNode> next;
        // 也可以令 next 为数组形式
        
    TrieNode(){
        isWord = false;
            //只有在当前字符为单词结尾时才令该值为真
        next = new HashMap<>();
    }
    
    public void add(String word, TrieNode root){
        // 插入新词
        TrieNode current = root; 
        int n = word.length();
        for(int i = 0; i < n; i++){
            char c = word.charAt(i);
            TrieNode next = current.next.get(c);
                // 在 current.next 这个 map 中查找,获取当前字符对应的节点
            if(next == null){
                // 若 next == null,即 map 中没有这个节点 --> 没有这个字符
                // 在 current.next 这个 map 中添加当前字符 --> 添加<字符,节点>
                // 此处应创建新的空白节点,等待添加后续字符
                current.next.put(c, new TrieNode());
            }
                //后移 current 至当前字符对应的节点
            current = current.next.get(c);
                // 可以简写成 current = next;
        }
        if(!current.isWord){
            current.isWord = true;
        }
    }

    public boolean inTrie(String word, TrieNode root){
        // 查看 word 是否已在树中
        TrieNode current = root;
        int n = word.length();
        for(int i = 0; i < n; i++){
            char ch = word.charAt(i);
            TrieNode next = current.next.get(ch);
            if(next == null){
                return false;
            }
            current = next;
        }
        return current.isWord;
    }
// 本题中只涉及字典树的插入/查找操作
}


309.最佳买卖股票时机含冷冻期【200710每日一题】

方法:动态规划

刷 leetcode 这些天已经遇到了不少动态规划的题。这一道中等难度的题算是一道质检吧。这道题看起来很复杂,但如果套用到动态规划的思路中,就可以顺利地解决了。

在本题中,我们要求解在最后一天结束后,所能获得的累计最大利润。首先,需要分析出在第 i 天结束后,共有几种可能的状态。根据题意,一共有 3 种可能:

  1. 在第 i 天结束后,目前手中持有股票:
    --> 可能是第 i 天买入的(则第 i-1 天结束后必处于状态 3),也可能是继承了第 i-1 天持有的股票;
  2. 在第 i 天结束后,不持有股票,且目前处于冷冻期:
    --> 第 i 天卖出了手中的股票,所以第 i-1 天必然持有股票;
  3. 在第 i 天结束后,不持有股票,且可以买入:
    --> 第 i 天没有执行买卖操作,且第 i-1 天也不持有股票,但不一定是在第 i - 1 天卖出的.

分别用 (dp[i][0], dp[i][1], dp[i][2]) 来表示在第 i 天结束后,且处于上述 3 种对应状态时所积累的最大利润。根据分析,可以得到状态转移方程为:

[dp[i][0] = MAX(dp[i - 1][2] - prices[i], dp[i - 1][0]), ]

[dp[i][1] = dp[i - 1][0] + prices[i], ]

[dp[i][2] = MAX(dp[i - 1][1], dp[i - 1][2]). ]

得到状态转移方程后,考虑边界情况。显然,当 (i = 0) 时, 若不执行买入操作,那么累计的最大利润默认值为 0;若执行买入操作,最大利润需要在默认值基础上减去当天的股票价格。因此,当 (i = 0) 时,各状态的初始值为:

[dp[0][0] = -prices[0], ]

[dp[0][1] = dp[0][2] = 0. ]

至此,基本解题思路已完成。但在观察状态转移方程时,可以发现 (dp[i][...]) 的值只和第 i-1 天的结果有关。因此,可以使用动态数组来降低 dp 数组维度,实现空间优化。

具体代码如下:

class Solution {
    public int maxProfit(int[] prices) {
        int[] dp = new int[3];
        if(prices.length > 0){
            dp[0] = -prices[0];
            dp[1] = 0;
            dp[2] = 0;
        } 
        for(int i = 1; i < prices.length; i++){
            int temp0 = dp[0];
            dp[0] = Math.max(dp[0], dp[2] - prices[i]);
            dp[2] = Math.max(dp[1], dp[2]);
            dp[1] = temp0 + prices[i];      
        }
        return Math.max(dp[1], dp[2]);
    }
}




以上是关于Leetcode & Java#面试题17.13 / 309的主要内容,如果未能解决你的问题,请参考以下文章

我用java刷 leetcode 面试题 17.16. 按摩师

Leetcode & Java & SQL#面试题16.11 / 175 / 176 / 181 / 182

LeetCode 面试题 17.11单词距离[数组 双指针] HERODING的LeetCode之路

LeetCode 面试题17.11.单词距离

leetcode 面试题 17.13. 恢复空格

Leetcode -面试题17.04.消失的数字 -189.轮转数组