剑指offer(二):
补充知识:
动态规划解题框架
若确定给定问题具有重叠子问题和最优子结构,那么就可以使用动态规划求解。总体上看,求解可分为四步:
- 状态定义: 构建问题最优解模型,包括问题最优解的定义、有哪些计算解的自变量;
- 初始状态: 确定基础子问题的解(即已知解),原问题和子问题的解都是以基础子问题的解为起始点,在迭代计算中得到的;
- 转移方程: 确定原问题的解与子问题的解之间的关系是什么,以及使用何种选择规则从子问题最优解组合中选出原问题最优解;
- 返回值: 确定应返回的问题的解是什么,即动态规划在何处停止迭代;
完成以上步骤后,便容易写出对应的解题代码。
示例:斐波那契数列
- 状态定义:一维 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
- 这道题从传统的动态规划角度考虑,不太好理解。也就是从子问题推dp[i]时,不太好找出状态转移方程;
- 换一个角度考虑:要求一个数组dp[],给定dp第1个元素是1,后面的每个dp值都是前面的值乘以2、3、5得出来的。并且要求dp数组从小到大排序,且无重复元素;
- 基于第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;
}
}