剑指offer(二):动态规划

剑指offer(二):

补充知识:

动态规划解题框架

若确定给定问题具有重叠子问题和最优子结构,那么就可以使用动态规划求解。总体上看,求解可分为四步:

  1. 状态定义: 构建问题最优解模型,包括问题最优解的定义、有哪些计算解的自变量
  2. 初始状态: 确定基础子问题的解(即已知解),原问题和子问题的解都是以基础子问题的解为起始点,在迭代计算中得到的;
  3. 转移方程: 确定原问题的解与子问题的解之间的关系是什么,以及使用何种选择规则从子问题最优解组合中选出原问题最优解;
  4. 返回值: 确定应返回的问题的解是什么,即动态规划在何处停止迭代;
    完成以上步骤后,便容易写出对应的解题代码。

示例:斐波那契数列

  • 状态定义:一维 dp列表,设第 ii 个斐波那契数为 dp[i];
  • 初始状态:已知第 0 , 11个斐波那契数分别为 dp[0] = 0 , dp[1] = 1;
  • 转移方程:后一个数字等于前两个数字之和,即

dp[i] = dp[i - 1] + dp[i - 2]

  • 返回值:需求取的第 n个斐波那契数 dp[n] ;

示例:蛋糕最高售价

  • 状态定义:一维dp 列表,设重量为 i 蛋糕的售价为 p(i) ,重量为 i 蛋糕切分后的最高售价为 dp[i];
  • 初始状态:已知重量为 0 蛋糕的最高售价为 0 ,重量为 1 的蛋糕最高售价为 p(1);
  • 转移方程:dp[n]为 n 种切分组合中的最高售价组合,即

dp[n] =max (dp[i] + p(n - i))

  • 返回值:需求取的重量为 nn 的蛋糕最高售价 dp[n]dp[n]

题目一/二:斐波那契数列/青蛙跳台阶问题

class Solution {
    public int fib(int n) {
    int a = 0, b = 1, sum;             
    for (int i = 0; i < n; i++) {  
         sum = (a + b) % 1000000007;
         a = b;
         b = sum;
    }
    return a;                 
} 
}
循环求余法:

大数越界: 随着 n增大, f(n) 会超过 Int32 甚至 Int64 的取值范围,导致最终的返回值错误。

  • 求余运算规则: 设正整数 x, y, p,求余符号为⊙ ,则有 (x + y) ⊙ p = (x⊙p+y⊙p)⊙p 。
  • 解析: 根据以上规则,可推出 f(n)⊙ p = [f(n−1)⊙p+f(n−2)⊙p]⊙p ,从而可以在循环过程中每次计算 sum = (a + b)⊙1000000007,此操作与最终返回前取余等价。

大数阶乘,大数的排列组合等,一般都要求将输出结果对1000000007取模 为什么总是1000000007呢

  • 1000000007是一个质数
  • int32位的最大值为2147483647,所以对于int32位来说1000000007足够大
  • int64位的最大值为2^63-1,对于1000000007来说它的平方不会在int64中溢出
    所以在大数相乘的时候,因为(a∗b)%c=((a%c)∗(b%c))%c,所以相乘时两边都对1000000007取模,再保存在int64里面不会溢出

题目三: 正则表达式

class Solution {
    public boolean isMatch(String s, String p) {
        int m = s.length() + 1, n = p.length() + 1;
        boolean[][] dp = new boolean[m][n];
        dp[0][0] = true;
        // 初始化首行
        for(int j = 2; j < n; j += 2)
            dp[0][j] = dp[0][j - 2] && p.charAt(j - 1) == '*';
        // 状态转移
        for(int i = 1; i < m; i++) {
            for(int j = 1; j < n; j++) {
                if(p.charAt(j - 1) == '*') {
                    if(dp[i][j - 2]) dp[i][j] = true;                                            // 1.
                    else if(dp[i - 1][j] && s.charAt(i - 1) == p.charAt(j - 2)) dp[i][j] = true; // 2.
                    else if(dp[i - 1][j] && p.charAt(j - 2) == '.') dp[i][j] = true;             // 3.
                } else {
                    if(dp[i - 1][j - 1] && s.charAt(i - 1) == p.charAt(j - 1)) dp[i][j] = true;  // 1.
                    else if(dp[i - 1][j - 1] && p.charAt(j - 1) == '.') dp[i][j] = true;         // 2.
                }
            }
        }
        return dp[m - 1][n - 1];
    }
}

在这里插入图片描述

题目四:连续子数组的最大和

//解法一:完全使用暴力的方式,时间复杂度为O(n^3),完全超出了时间限制,抛弃 
public static int maxSubArray(int[] nums) {
        int n = nums.length;
        int max = nums[0];
        for (int i = 1; i <= n; i++) {
            for (int j = i; j <= n; j++) {
                int sum = sum(nums, i, j);
                if (sum>max)max=sum;
            }
        }
        return max;
    }
    public static int sum(int[] nums, int i, int j){
        int sum = 0;
        for (int k = j-i; k < j; k++) sum+=nums[k];
        return sum;
    }

//解法二:使用动态规划的方法来做,也太精简了
   public static int maxSubArray(int[] nums) {
        int res = nums[0];
        for(int i = 1; i < nums.length; i++) {
            //如果产生负影响,则抛弃
            nums[i] += Math.max(nums[i - 1], 0);
            res = Math.max(res, nums[i]);
        }
        return res;
    }
}

在这里插入图片描述

tips:
  • 蛋糕销售(钢条切割),连续子数组的最大和这一类问题相比于斐波那契数列,在每一层都进行了一次最优解的选择,这个段语句就是最优解选择,这里上一层的最优解与下一层的最优解相关。
  • 比如蛋糕问题
 f_n = max(f_n, max_cake_price(i, price_list) + price_list[n - i])
  • 连续子数组的最大和问题
num[i] += Math.max(num[i-1],0)

题目五:把数字翻译成字符串

//代码一:自己写的,稍微有点复杂
class Solution {
        public static int translateNum(int num) {
            if(num<10)return 1;
        //越界处理
        // 将数字转换成为数组
        Stack<Integer> integers = new Stack<>();
        while (num != 0) {
            int last = num % 10;
            integers.push(last);
            num = (num - last) / 10;
        }
        int[] intlist = new int[integers.size()];
        int i = 0;
        while (!integers.empty()) intlist[i++] = integers.pop();

        //进行动态规划的实现
        int[] dp = new int[intlist.length];
        dp[0] = 1;
        if (intlist[0] == 2 && intlist[1] <= 5) dp[1] = 2;
        else {
            if (intlist[0] == 1) dp[1] = 2;
            else dp[1] = 1;
        }
        for (int j = 2; j < intlist.length; j++) {
            if (intlist[j - 1] == 2 && intlist[j] <= 5) dp[j] = dp[j - 2] + dp[j - 1] ;
            else {
                if (intlist[j - 1] == 1) dp[j] = dp[j - 2] + dp[j - 1];
                else dp[j] = dp[j - 1];
            }
        }
        return dp[intlist.length-1];
    }
}

//对比简单的方法
class Solution {
    public int translateNum(int num) {
        int a = 1, b = 1, x, y = num % 10;
        while(num > 9) {
            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;
}

//答案的解法,字符串的方法
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;
    }
}

tips:String的方法
  • 如果第一个字符和参数的第一个字符不等,结束比较,返回第一个字符的ASCII码差值。
  • 如果第一个字符和参数的第一个字符相等,则以第二个字符和参数的第二个字符做比较,以此类推,直至不等为止,返回该字符的ASCII码差值。 如果两个字符串不一样长,可对应字符又完全一样,则返回两个字符串的长度差值。
valueOf(int i): //返回 int 参数的字符串表示形式。

在这里插入图片描述

题目六:最长不含重复字符的子字符串

在这里插入图片描述

class Solution {
    public static int lengthOfLongestSubstring(String s) {
        int tem = 0, res = 0, len = s.length();
        Map<Character,Integer> dic = new HashMap<>();
        for (int i = 0; i < s.length(); i++) {
            Integer j = dic.getOrDefault(s.charAt(i), -1);
            dic.put(s.charAt(i), i);
            tem = tem<i-j?tem+1:i-j;
            res = Math.max(res,tem);
        }
        return res;
    }
}
关键问题:
  • 如何确定子数组的左边界,使用map存着,相对的key会变换value。
// 线性遍历
for(int j = 0; j < len; j++) {
            int i = j - 1;
            while(i >= 0 && s.charAt(i) != s.charAt(j)) i--; // 线性查找 i

//双指针
  for(int j = 0; j < len; 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); // 更新结果
  • getOrDefault() 方法获取指定 key 对应对 value,如果找不到 key ,则返回设置的默认值。

题目七:丑数

在这里插入图片描述

class Solution {
    public int nthUglyNumber(int n) {
        int a = 0, b = 0, c = 0;
        int[] dp = new int[n];
        dp[0] = 1;
        for(int i = 1; i < n; i++) {
            int n2 = dp[a] * 2, n3 = dp[b] * 3, n5 = dp[c] * 5;
            dp[i] = Math.min(Math.min(n2, n3), n5);
            if(dp[i] == n2) a++;
            if(dp[i] == n3) b++;
            if(dp[i] == n5) c++;
        }
        return dp[n - 1];
    }
}

tips

  1. 这道题从传统的动态规划角度考虑,不太好理解。也就是从子问题推dp[i]时,不太好找出状态转移方程;
  2. 换一个角度考虑:要求一个数组dp[],给定dp第1个元素是1,后面的每个dp值都是前面的值乘以2、3、5得出来的。并且要求dp数组从小到大排序,且无重复元素;
  3. 基于第2点就很好考虑了,设置3个标志a、b、c分别表示某个元素dp[j]的2倍、3倍、5倍是否已经添加到dp[]数组。那么初始时a、b、c都指向dp[0]说明dp[0]的2倍、3倍、5倍都还没有用到,那就选其中一个最小的放在dp[1]位置上,a指针++。(这时很明确的是,同一个数的a、b、c这3个标记,一定会先使用到a标记)。

题目八:n 个骰子的点数

把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。

第一步理解:添加一个新的为旧的6种情况想加

在这里插入图片描述
在这里插入图片描述
第二步理解:逆向变为正向

以上递推公式是 “逆向” 的,即为了计算 f(n, x) ,将所有与之有关的情况求和;而倘若改换为 “正向” 的递推公式,便可解决越界问题。
在这里插入图片描述

class Solution {
    public double[] dicesProbability(int n) {
        double[] dp = new double[6];
        Arrays.fill(dp, 1.0 / 6.0);
        for (int i = 2; i <= n; i++) {
            double[] tmp = new double[5 * i + 1];
            for (int j = 0; j < dp.length; j++) {
                for (int k = 0; k < 6; k++) {
                    tmp[j + k] += dp[j] / 6.0;
                }
            }
            dp = tmp;
        }
        return dp;
    }
}

题目九:股票的最大利润

假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
在这里插入图片描述

// 暴力穷举,不太行
    public static int maxProfit(int[] prices) {
        int m = prices.length;
        int res = 0;
        int[][] dp = new int[m][m];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < i; j++) {
                int tem = prices[i]-prices[j];
                res = Math.max(res,tem);
            }
        }
        return res;
    }

//动态优化方法好
class Solution {
    public int maxProfit(int[] prices) {
        int cost = Integer.MAX_VALUE,profit = 0;
        for (int price : prices) {
            //保存最小值
            cost = Math.min(price,cost);
            profit = Math.max(profit,price-cost);
        }
        return profit;  
}
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值