leetcoder题目java_【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 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 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;

}

// 本题中只涉及字典树的插入/查找操作

}

数据结构与算法(十一)Trie字典树 :

本题中只涉及字典树的插入/查找操作,这篇文章还给出了字典树删除操作的实现方法,对字典树的讲解非常详细,五星好评:-)

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

方法:动态规划

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

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

在第 i 天结束后,目前手中持有股票:

--> 可能是第 i 天买入的(则第 i-1 天结束后必处于状态 3),也可能是继承了第 i-1 天持有的股票;

在第 i 天结束后,不持有股票,且目前处于冷冻期:

--> 第 i 天卖出了手中的股票,所以第 i-1 天必然持有股票;

在第 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]);

}

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值