阿翰 剑指offer 之 Day 10 动态规划 3

目录

动态规划

1 翻译数字

1. 动态规划+求余

1.2 空间优化 

2. 字符串string定位前俩数字

3. (大佬实现)细节满满

2 最长不含重复字符的子字符串

1. 滑动窗口 

2. 动态规划+hash表

3. 动态规划+线性遍历 

4. 双指针+hashmap(题解写法)


动态规划

1 翻译数字

剑指 Offer 46. 把数字翻译成字符串icon-default.png?t=LA92https://leetcode-cn.com/problems/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof/

最初的思路是借助栈和求余将数字转换成一个数组,再进行动态规划。

记数字 num 第 i 位数字为 xi,数字 num 的位数为 n ;
例如: num = 12258 的 n = 5 , x1 = 1  

动态规划解析:

  • 状态定义: 设动态规划列表 dp ,dp[i] 代表以 xi 为结尾的数字的翻译方案数量
  • 转移方程: 若 xi​ 和 xi−1​ 组成的两位数字可以被翻译,则 dp[i]=dp[i−1]+dp[i−2] ;否则 dp[i]=dp[i−1] 。

    这里划分的比我实现的要更细致!我是在0到25之间 再限制十位不是0。 

  • 初始状态: dp[0] = dp[1] = 1  ,即 “无数字” 和 “第 1 位数字” 的翻译方法数量均为 1 ;

  • 返回值: dp[n],即此数字的翻译方案数量。

Q: 无数字情况 dp[0] = 1 从何而来?
A: 当 num 第 1, 2 位的组成的数字 ∈[10,25] 时,显然应有 2 种翻译方法,即 dp[2]=dp[1]+dp[0]=2 ,而显然 dp[1] = 1dp[1]=1 ,因此推出 dp[0] = 1dp[0]=1 。

动态规划思想通透了~ 下一步将是把数字转换出每一位来走!俩思想

  • 一个是转成字符串切片,(我是用的转成字符串后charAt定位)
  • 另一个是求余

1. 动态规划+求余

package jzof.Day10;

import org.w3c.dom.stylesheets.LinkStyle;

import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;

/**
 * @author ahan
 * @create_time 2021-11-11-12:29 下午
 * 给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1
 * 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
 *
 */
public class _46 {
    public static void main(String[] args) {
        System.out.println(new _46().translateNum_0(12258));
        System.out.println(new _46().translateNum(220));
        System.out.println(new _46().translateNum(506));
    }
    public int translateNum_0(int num) {
        Deque<Integer> nums = new LinkedList<>();
        int i = 0;
        System.out.print(num%10+ " ");
        nums.push(num%10);
        while(num/10!=0){
            num /= 10;
            nums.push(num%10);
            System.out.print(num%10+ " ");
        }
        System.out.println("\n---");
        int[] numsList = new int[nums.size()];
        int[] dp = new int[nums.size()+1];
        while (!nums.isEmpty()){
            int temp = nums.pop();
            numsList[i++] = temp;
        }
        dp[0] = 1;
        dp[1] = 1;
        for (int j = 2; j < numsList.length+1; j++) {
            int t = numsList[j-1]+numsList[j-2]*10;
            if(numsList[j-2] != 0 && t <= 25 && t >= 0)
                dp[j] = dp[j-1] + dp[j-2];
            else
                dp[j] = dp[j-1];
        }
        for (int j = 0; j < dp.length; j++) {
            System.out.print(dp[j]+" ");
        }
        System.out.println("\n---");
        return dp[numsList.length];
    }
    public int translateNum(int num) {
        Deque<Integer> nums = new LinkedList<>();
        int i = 0;
        nums.push(num%10);
        while(num/10!=0){
            num /= 10;
            nums.push(num%10);
        }
        int[] numsList = new int[nums.size()];
        int[] dp = new int[nums.size()+1];
        while (!nums.isEmpty()){
            int temp = nums.pop();
            numsList[i++] = temp;
        }
        dp[0] = 1;
        dp[1] = 1;
        for (int j = 2; j < numsList.length+1; j++) {
            int t = numsList[j-1]+numsList[j-2]*10;
            if(numsList[j-2] != 0 && t <= 25 && t >= 0)
                dp[j] = dp[j-1] + dp[j-2];
            else
                dp[j] = dp[j-1];
        }
        return dp[numsList.length];
    }
}

1.2 空间优化 

class Solution {
    public int translateNum(int num) {
        Deque<Integer> nums = new LinkedList<>();
        int i = 0;
        nums.push(num%10);
        while(num/10!=0){
            num /= 10;
            nums.push(num%10);
        }
        int[] numsList = new int[nums.size()]; 
        while (!nums.isEmpty()){
            int temp = nums.pop();
            numsList[i++] = temp;
        }
        int pre_1 = 1;
        int pre_2 = 1;
        int cur = 1;
        for (int j = 2; j < numsList.length+1; j++) {
            int t = numsList[j-1]+numsList[j-2]*10;
            if(numsList[j-2] != 0 && t <= 25 && t >= 0)
                cur = pre_1 + pre_2;
            else
                cur = pre_2;
            pre_1 = pre_2;
            pre_2 = cur;
        }
        return cur;
    }
}

2. 字符串string定位前俩数字

class Solution {
    public int translateNum(int num) {
        String s = Integer.toString(num);
        int pre_1 = 1;
        int pre_2 = 1;
        int cur = 1;
        for (int j = 2; j < s.length()+1; j++) {
            int t = (int)s.charAt(j-1)-48+((int)s.charAt(j-2)-48)*10; 
            if(((int)s.charAt(j-2)-48) != 0 && t <= 25 && t >= 0)
                cur = pre_1 + pre_2;
            else
                cur = pre_2;
            pre_1 = pre_2;
            pre_2 = cur;
        }
        return cur;
    }
}

3. (大佬实现)细节满满

class Solution {
    public int translateNum(int num) {
        String s = String.valueOf(num);
        int a = 1, b = 1;
        for(int i = 2; i <= s.length(); i++) {
            String tmp = s.substring(i - 2, i);
            int c = tmp.compareTo("10") >= 0 && tmp.compareTo("25") <= 0 ? a + b : a;
            b = a;
            a = c;
        }
        return a;
    }
} 

此题的动态规划计算是 对称的 ,即 从左向右 遍历(从第 dp[2] 计算至 dp[n] )和 从右向左 遍历(从第 dp[n - 2]计算至 dp[0] )所得方案数一致。从右向左遍历的代码如下所示。

class Solution {
    public int translateNum(int num) {
        String s = String.valueOf(num);
        int a = 1, b = 1;
        for(int i = s.length() - 2; i > -1; i--) {
            String tmp = s.substring(i, i + 2);
            int c = tmp.compareTo("10") >= 0 && tmp.compareTo("25") <= 0 ? a + b : a;
            b = a;
            a = c;
        }
        return a;
    }
} 
  • 上述方法虽然已经节省了 dp 列表的空间占用,但字符串 s 仍使用了 O(N) 大小的额外空间。

空间复杂度优化:

  • 利用求余运算 num%10 和求整运算 num//10 ,可获取数字 num 的各位数字(获取顺序为个位、十位、百位…)。
  • 因此,可通过 求余 和 求整 运算实现 从右向左 的遍历计算。而根据上述动态规划 “对称性” ,可知从右向左的计算是正确的。
  • 自此,字符串 s 的空间占用也被省去,空间复杂度从 O(N) 降至 O(1) 。

复杂度分析:

  • 时间复杂度O(N) : NN 为字符串 ss 的长度(即数字 num 的位数 log(num) ),其决定了循环次数。
  • 空间复杂度 O(1) : 几个变量使用常数大小的额外空间。

 既然从右向左也是可以的求余就更好写了。

class Solution {
    public int translateNum(int num) {
        int a = 1, b = 1, x, y = num % 10;
        while(num != 0) {
            num /= 10;
            x = num % 10;
            int tmp = 10 * x + y;
            int c = (tmp >= 10 && tmp <= 25) ? a + b : a;
            b = a;
            a = c;
            y = x;
        }
        return a;
    }
}

思路有想到。在转化数字的时候卡顿了~ 

2 最长不含重复字符的子字符串

剑指 Offer 48. 最长不含重复字符的子字符串icon-default.png?t=LA92https://leetcode-cn.com/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof/

只想到了滑动窗口,效果不理想~

1. 滑动窗口 

提交前忘记考虑如果abba这种情况,在start和end都移动到2,2,再滑动,start反而会朝前移动,要在移动后判断map里有的信息置为无效(-1)。

package jzof.Day10;

import java.util.*;

/**
 * @author ahan
 * @create_time 2021-11-11-8:29 下午
 * 请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
 */
public class _48 {
    public static void main(String[] args) {
//        String s = new String("abcabcbb");
//        String s = new String("bbbbb");
//        String s = new String("pwwkew");
//        String s = new String("abcabcbb");
        String s = new String("abba");
        System.out.println(new _48().lengthOfLongestSubstring(s));
    }
//    存节点index 对 遇到已存在的 start移动到之前的后一个 然后更新start end继续滑动
    public int lengthOfLongestSubstring(String s) {
        HashMap<Character, Integer> map = new HashMap<>();
        int start = 0;
        int max = 0;
        for (int i = 0; i < s.length(); i++) {
            if(map.get(s.charAt(i)) != null && map.get(s.charAt(i)) != -1){
                start = map.get(s.charAt(i)) + 1;
                for (Map.Entry<Character, Integer> entry: map.entrySet()) {
                    if(entry.getValue() < start){
                        map.put(entry.getKey(), -1);
                    }
                }
            }
            map.put(s.charAt(i), i);
            if(max < (i - start + 1)) max = (i -start + 1);
        }
        return max;
    }

}

    长度为 N 的字符串共有 (1+N)N​ /2个子字符串(复杂度为 O(N^2)  ),判断长度为 N 的字符串是否有重复字符的复杂度为 O(N) ,因此本题使用暴力法解决的复杂度为 O(N^3) 。考虑使用动态规划降低时间复杂度。

动态规划解析:

  • 状态定义: 设动态规划列表 dp ,dp[j] 代表以字符 s[j] 为结尾的 “最长不重复子字符串” 的长度。

  • 转移方程:还是固定右边界 ,设字符 s[j] 左边距离最近的相同字符为 s[i],即 s[i] = s[j]。

    • 当 i < 0 ,即 s[j] 左边无相同字符,则 dp[j] = dp[j-1] + 1;

    • 当 dp[j - 1] < j - i,说明字符 s[i] 在子字符串 dp[j−1] 区间之外 ,则 dp[j] = dp[j - 1] + 1;
    • 当 dp[j−1]≥j−i ,说明字符 s[i] 在子字符串 dp[j-1] 区间之中 ,则 dp[j]的左边界由 s[i] 决定,即 dp[j] = j - i ;

当 i < 0 时,由于 dp[j−1] ≤ j 恒成立,因而 dp[j - 1] < j - i恒成立,因此分支 1. 和 2. 可被合并。

  • 返回值:max(dp) ,即全局的 “最长不重复子字符串” 的长度。

空间复杂度优化:
由于返回值是取 dp 列表最大值,因此可借助变量 tmptmp 存储 dp[j],变量 res 每轮更新最大值即可。 

2. 动态规划+hash表

  • 哈希表统计:遍历字符串 s 时,使用哈希表(记为 dic )统计各字符最后一次出现的索引位置 。
  • 左边界 i 获取方式:遍历到 s[j] 时,可通过访问哈希表 dic[s[j]]  获取最近的相同字符的索引 ii 

复杂度分析:

  • 时间复杂度 O(N): 其中 NN 为字符串长度,动态规划需遍历计算dp 列表。
  • 空间复杂度 O(1) : 字符的 ASCII 码范围为 0 ~ 127 ,哈希表 dicdic 最多使用 O(128) = O(1)大小的额外空间。
class Solution {
    public int lengthOfLongestSubstring(String s) {
        Map<Character, Integer> dic = new HashMap<>();
        int res = 0, tmp = 0;
        for(int j = 0; j < s.length(); j++) {
            int i = dic.getOrDefault(s.charAt(j), -1); // 获取索引 i
            dic.put(s.charAt(j), j); // 更新哈希表
            tmp = tmp < j - i ? tmp + 1 : j - i; // dp[j - 1] -> dp[j]
            res = Math.max(res, tmp); // max(dp[j - 1], dp[j])
        }
        return res;
    }
}

3. 动态规划+线性遍历 

  • 左边界 i 获取方式: 遍历到 s[j]s[j] 时,初始化索引 i = j - 1 ,向左遍历搜索第一个满足 s[i] = s[j] 的字符即可 。

复杂度分析:

  • 时间复杂度 O(N^2) : 其中 N 为字符串长度,动态规划需遍历计算 dp 列表,占用 O(N) ;每轮计算 dp[j] 时搜索 i 需要遍历 j 个字符,占用 O(N)  。
  • 空间复杂度 O(1) : 几个变量使用常数大小的额外空间。
class Solution {
    public int lengthOfLongestSubstring(String s) {
        Map<Character, Integer> dic = new HashMap<>();
        int res = 0, tmp = 0;
        for(int j = 0; j < s.length(); j++) {
            int i = j - 1;
            while(i >= 0 && s.charAt(i) != s.charAt(j)) i--; // 线性查找 i
            tmp = tmp < j - i ? tmp + 1 : j - i; // dp[j - 1] -> dp[j]
            res = Math.max(res, tmp); // max(dp[j - 1], dp[j])
        }
        return res; 
    }
}

4. 双指针+hashmap(题解写法)

本质上与方法一类似,不同点在于左边界 ii 的定义。

  • 哈希表 dic 统计: 指针 j 遍历字符 s ,哈希表统计字符 s[j] 最后一次出现的索引 。
  • 更新左指针 i : 根据上轮左指针 i 和 dic[s[j]] ,每轮更新左边界 i ,保证区间 [i + 1, j]内无重复字符且最大。

      

     

    复杂度分析:

    • 更新结果  res: 取上轮 resres 和本轮双指针区间 [i + 1,j][i+1,j] 的宽度(即 j - ij−i )中的最大值。
    • 时间复杂度 O(N) : 其中 NN 为字符串长度,动态规划需遍历计算 dp 列表。
    • 空间复杂度 O(1) : 字符的 ASCII 码范围为 0 ~ 127 ,哈希表 dicdic 最多使用 O(128) = O(1)大小的额外空间。
class Solution {
    public int lengthOfLongestSubstring(String s) {
        Map<Character, Integer> dic = new HashMap<>();
        int i = -1, res = 0;
        for(int j = 0; j < s.length(); j++) {
            if(dic.containsKey(s.charAt(j)))
                i = Math.max(i, dic.get(s.charAt(j))); // 更新左指针 i
            dic.put(s.charAt(j), j); // 哈希表记录
            res = Math.max(res, j - i); // 更新结果
        }
        return res;
    }
} 

这里更新左指针,会遇到abba这种情况,我解决的策略是如果之前有过 就置为-1,在更新前判断是不是不等于-1,存在且不等于-1就更新,但是这样多了O(N^2)的遍历,

题解巧妙的根据,前面有过的肯定比新的索引小这一条件,在更新左指针时,判断map内的和当前的索引哪个大,存大的索引。

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值