剑指 Offer | 动态规划 [14-I,14-II,47,63]
42.连续子数组的最大和
class Solution {
// 用tmp[i]表示[0,i]中包括i的最大子数组的和。
// 最后返回的是max i tmp[i]
public int maxSubArray(int[] nums) {
int l = nums.length;
int tmp = nums[0];
int res = tmp;
for(int i=1;i<l;i++){
tmp = Math.max(tmp+nums[i],nums[i]);
res = Math.max(res,tmp);
}
return res;
}
}
63. 股票的最大利润
class Solution {
public int maxProfit(int[] prices) {
if(prices.length==0)
return 0;
int historyMin = prices[0];
int res = 0;
for(int i=1;i<prices.length;i++){
historyMin = Math.min(historyMin,prices[i]);
res = Math.max(res,prices[i]-historyMin);
}
return res;
}
}
14- I. 剪绳子 | 递归+Memoization
思路一:每个最大元素,要么是自己分裂成2块,要么自己分裂一块+别人分裂一块,要么全靠别人分裂。
public int cuttingRope(int n) {
int[] res = new int[n+1]; //res用于记录剪长度为n的绳子最大可能乘积
res[0] = 0;
res[1] = 1;
res[2] = 1;
for(int i=3;i<=n;i++){
res[i] = ((int)(i+1)/2)*((int)i/2); //拆成2个元素,这里用了均值不等式
for(int j=0;j<=i;j++){
res[i] = Math.max(res[i],j*res[i-j]);
res[i] = Math.max(res[i],res[i-j]*res[j]);
}
}
// for(int i=0;i<=n;i++)
// System.out.print(i+"\t");
// System.out.print("\n");
// for(int i=0;i<=n;i++){
// System.out.print(res[i]+"\t");
// }
return res[n];
}
思路二:递归+Memoization
class Solution {
public int memo(int[] m, int n){
// 如果我们已经计算过这个元素了
if(m[n]!=0){
return m[n];
}
for(int i=1;i<n;i++){
m[n] = Math.max(m[n],Math.max(memo(m,n-i)*i,(n-i)*i)); //在分成2块,还是>=3中最大的结果。
}
return m[n];
}
public int cuttingRope(int n) {
int[] m = new int[n+1]; // m[i]用来存储长度为i的绳子的最优解。
m[0] = 0;
m[1] = 1;
m[2] = 1;
memo(m,n);
return m[n];
}
}
14- II. 剪绳子 II 🌟
数学证明参考https://leetcode-cn.com/problems/jian-sheng-zi-lcof/solution/mian-shi-ti-14-i-jian-sheng-zi-tan-xin-si-xiang-by/
class Solution {
public int cuttingRope(int n) {
if(n==2)
return 1;
if(n==3)
return 2;
if(n==4)
return 4;
// 否则,尽可能分成3的小段
long res = 1;
while(n>4){
n -=3;
res = (res*3) % 1000000007;
}
//最后分成 n = 2,3,4三种情况讨论
return (int)(n*res%1000000007);
}
}
47. 礼物的最大价值
这道题感觉和不同路径那一题类似
class Solution {
public int maxValue(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
int[] res = new int[n];
// 初始化第0行
res[0] = grid[0][0];
for(int i=1;i<n;i++)
res[i]=grid[0][i]+res[i-1];
// 更新第1行到最后
for(int i=1;i<m;i++){
// for(int k=0;k<n;k++){
// System.out.print(res[k]+"\t");
// }
// System.out.print("\n");
res[0] += grid[i][0];
for(int j=1;j<n;j++)
res[j] = Math.max(res[j],res[j-1])+grid[i][j];
}
// for(int k=0;k<n;k++){
// System.out.print(res[k]+"\t");
// }
return res[n-1];
}
}
19. 正则表达式匹配 | 倒序思考问题
模式中的字符’.‘表示任意一个字符,而’*'表示它前面的字符可以出现任意次(含0次)。
class Solution {
public boolean isMatch(String s, String p) {
int m = s.length()+1;
int n = p.length()+1;
// dp[i,j]判断 s[0,i-1]和p[0,j-1]是否匹配。 dp[0,0]表示空字符串
boolean[][] dp = new boolean[m][n];
// 初始化dp[0][j]
dp[0][0] = true; // 其余默认false
// 只需要检查偶数位,因为*只可能出现在
for(int j=2;j<n;j++){
dp[0][j] = dp[0][j-2] && (p.charAt(j-1)=='*');
}
// 状态转移
// dp[i,0] = False i>0 因为p此时为空字符串
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
// 这里不用担心j-2数组越界,因为不可能在j=1的时候,即p中第一个元素就出现*
if(p.charAt(j-1)=='*'){
dp[i][j] = dp[i][j-2] || dp[i-1][j] && ( s.charAt(i-1)==p.charAt(j-2) || p.charAt(j-2)=='.');
}
else{
dp[i][j] = dp[i-1][j-1] && (p.charAt(j-1)=='.' || s.charAt(i-1)==p.charAt(j-1));
}
}
}
return dp[m-1][n-1];
}
}
46. 把数字翻译成字符串
dp[i+1] = dp[i] + dp[i-1] (若10*i+(i-1))能构成数字。
需要注意的测试样例 507
如果直接看07=10*0+7是可以转化的,但实际上不行。
class Solution {
// 每一个数字占一位
public int[] intToArray(int num){
int tmp = num;
int l = 0;
while(tmp!=0){
tmp/=10;
l ++;
}
int[] res = new int[l];
for(int i=l-1;i>=0;i--){
res[i] = num %10;
num /= 10;
}
return res;
}
public int translateNum(int num) {
if(num==0)
return 1;
int[] nums = intToArray(num);
int[] dp = new int[nums.length]; // dp[i]表示[0,i]最多翻译的方法,字符串从1开始
dp[0] = 1;
for(int i=1;i<dp.length;i++){
dp[i] = dp[i-1];
if(nums[i-1]!=0 && 0<=(10*nums[i-1]+nums[i]) && (10*nums[i-1]+nums[i])<26){
if(i==1)
dp[i]+=1;
else
dp[i]+= dp[i-2];
}
}
return dp[dp.length-1];
}
}
别人精简的解法:
class Solution {
public int translateNum(int num) {
String s = String.valueOf(num);
int a = 1, b = 1; // i,i-1
for(int i = s.length() - 2; i > -1; i--) {
String tmp = s.substring(i, i + 2);
//若可以组成就返回i-2和i-1的和
int c = tmp.compareTo("10") >= 0 && tmp.compareTo("25") <= 0 ? a + b : a;
b = a;
a = c;
}
return a;
}
}
48. 最长不含重复字符的子字符串
思路一:
- dp[j]表示以s.charAt(j)结尾的最长不含重复字符的子字符串。
- 我们分为两种情况:
- s.charAt(j) 在之前没有出现过,则dp[j]=dp[j-1]+1。(在上一次结果后面直接添加)
- s.charAt(j) 在之前第i位出现过,则dp[j]=Math.min(dp[j-1]+1,j-i)。新的解决一定比dp[j-1]小的原因是如果超过dp[j-1]则按照dp[j-1]的定义,它一定会包含重复字符。
class Solution {
HashMap<Character,Integer> map = new HashMap<>(); //用于记录上一次这个元素何时出现。
public int lengthOfLongestSubstring(String s) {
if(s.length()==0) return 0;
int l = s.length();
int[] dp = new int[l]; // dp[i]表示 [0,i] 中以i为结尾的最长不含重复字符的子字符串数量
dp[0] = 1;
map.put(s.charAt(0),0);
int res = 1; // max(dp[i])
for(int i = 1; i<l;i++){
// 如果这个元素之前出现过1次,则我们不一定能取到dp[i-1]+1,除非上次出现的位置在上一个之前
if(map.containsKey(s.charAt(i))){
// System.out.println("char "+s.charAt(i)+" i = "+i+" map = "+map.get(s.charAt(i)));
dp[i] = Math.min(dp[i-1]+1,i-map.get(s.charAt(i)));
}
else{
dp[i] = dp[i-1]+1; //因为这个元素之前没有出现过,所以可以添加到上一次dp的结果中。
}
map.put(s.charAt(i),i); // 更新这个元素出现的下标
// 每次取最大的值
res = Math.max(res,dp[i]);
}
return res;
}
}
我们注意到我们每次dp的时候只和之前1个元素有关,因此,我们可以用一个tmp代替dp[]
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> dic = new HashMap<>();
int res = 0, tmp = 0, len = s.length();
for(int j = 0; j < len; 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;
}
}
思路二:双指针
每次确保[i+1,j]之间没有重复字符串,如果有重复字符串,就同时将i,j向右移动。因为之前出现过[i+1,j]的结果,所以存在这样长的子字符串没有重复字符。
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> dic = new HashMap<>();
int i = -1, res = 0, len = s.length();
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); // 更新结果
}
return res;
}
}
49. 丑数
思路一:暴力打表
class Solution {
public int nthUglyNumber(int n) {
Queue<Long> pq = new PriorityQueue<>();
HashSet<Long> set = new HashSet<>();
pq.add(1L);
set.add(1L);
long res=1;
while(n>0){
res = pq.poll();
if(!set.contains(2*res)){
pq.add(2*res);
set.add(2*res);
}
if(!set.contains(3*res)){
pq.add(3*res);
set.add(3*res);
}
if(!set.contains(5*res)){
pq.add(5*res);
set.add(5*res);
}
n--;
}
return (int)res;
}
}
思路二:
- dp[i] 代表第 i + 1i+1 个丑数;
- dp[a]首个乘以2后大于 x n x_n xn的丑数,所以如果 d p [ a ] ∗ 2 = = x n + 1 dp[a]*2==x_{n+1} dp[a]∗2==xn+1,要a+1切换到下一个丑数。
- dp[b] 3, dp[c] 4。
- 换言之,我们每次都要保证要下一个要诞生的处于离他最近的*2,*3,*5的元素范围之内。
x n + 1 = { x a ∗ 2 , a ∈ [ 1 , n ] x b ∗ 3 , b ∈ [ 1 , n ] x c ∗ 5 , c ∈ [ 1 , n ] x_{n+1}=\left\{\begin{matrix} x_a*2, a\in[1,n] \\ x_b*3, b\in[1,n]\\ x_c*5, c\in[1,n] \end{matrix}\right. xn+1=⎩⎨⎧xa∗2,a∈[1,n]xb∗3,b∈[1,n]xc∗5,c∈[1,n]
x n + 1 = m i n ( x a ∗ 2 , x b ∗ 3 , x c ∗ 5 ) x_{n+1}=min(x_a*2,x_b*3,x_c*5) xn+1=min(xa∗2,xb∗3,xc∗5)
即下一个丑数由之前某一个丑数的2/3/5倍得到。
另一方面,我们要保证 x a ∗ 2 x_a*2 xa∗2能包含新的丑数,即
所以若 x a ∗ 2 = = x n x_a*2==x_n xa∗2==xn我们要a++。
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];
}
}
注意
(int)(nres%1000000007) ≠ \neq = (int)(nres)%1000000007 后者先计算(int)(n*res)。
Memoization模版
经常利用默认的初始化,如int[] tmp=new int [10]
则tmp中10个元素都是0。
public int memo(int[] m, ...){
// 如果已经有记录了,直接返回
if(m[n]!=0)
return m[n];
// 否则,按照更新方式遍历,取最优的,其中的递归如果已经考虑过了就可以看成是一个常数。
for(int i=1;i<n;i++)
m[n] = Math.max(m[n],Math.max(memo(m,n-i)*i,(n-i)*i)); //在分成2块,还是>=3中最大的结果。
// 返回最优的结果
return m[n];
}