一、打家劫舍III
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root
。
除了 root
之外,每栋房子有且只有一个"父"房子与之相连。一番侦察之后,聪明的小偷意识到"这个地方的所有房屋的排列类似于一棵二叉树"。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
父节点和子节点不能同时偷(因为是直接相连的房子)
dp[0]/dp[1]:分别代表该节点上不偷和偷的最大值
思路:在树形结构中,采用后序遍历,左右中的顺序,从下往上求每一个节点的最大价值。
**如果该节点不偷,**那么下一节点可以偷,也可以不偷,dp[0]=下一节点偷或不偷的最大值;
**如果该节点偷,**那么下一节点就不可以偷,dp[1]=root.val+left[0]+right[0];
代码:
class Solution {
public int rob(TreeNode root) {
int[] res=robTree(root);
return Math.max(res[0],res[1]);
}
//递归函数
public int[] robTree(TreeNode root){
int[] result=new int[2];
//终止条件
if(root==null)return result;
//单层递归逻辑
int[] left=robTree(root.left);
int[] right=robTree(root.right);
//该节点不偷 孩子节点就可以偷
result[0]=Math.max(left[0],left[1])+Math.max(right[0],right[1]);
//该节点偷,孩子节点就不能偷
result[1]=root.val+left[0]+right[0];
return result;
}
}
股票问题
二、买卖股票的最佳时机I(一次遍历/动态规划)
一次遍历(贪心):
如果price[i]<minprice,更新minprice;
如果price[i]>=minprice,看price[i]-minprice是否大于maxprofit,大于的话就更新
代码:
class Solution {
public int maxProfit(int[] prices) {
int minprice = Integer.MAX_VALUE;
int maxprofit = 0;
for (int i = 0; i < prices.length; i++) {
if (prices[i] < minprice) {
minprice = prices[i];
} else if (prices[i] - minprice > maxprofit) {
maxprofit = prices[i] - minprice;
}
}
return maxprofit;
}
}
动态规划:
注意:题目要求是买入和卖出只能一次,也就是只能在某一天买入或者某一天卖出
1.dp[i][0/1]:0代表该天不持有股票,1代表该天持有股票。dp[i][0/1]:代表该天可以获得最大利润
2.递推公式:
2.1 dp[i][0]可能是延续上一天的状态,也可能是在这一天把股票卖了。
dp[i][0](第i天不持有股票)=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
2.2 dp[i][1]可能是延续上一天的状态,也可能是在这一天买入股票
dp[i][1]=Math.max(dp[i-1][1],-prices[i]);
3.初始化:dp[0][0]:0 dp[0][1]:-prices;
4.遍历:从1开始到prices.length
代码:
class Solution {
public int maxProfit(int[] prices) {
if(prices==null||prices.length==0)return 0;
// dp[x][0]:第x天不持有股票;dp[x][1]:第x天持有股票
int[][] dp = new int[prices.length][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
//注意dp[i][1]的递推公式 只能买入/卖出一次和每天买入/卖出的区别
for (int i = 1; i < prices.length; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], - prices[i]);
}
return Math.max(dp[prices.length - 1][0], dp[prices.length - 1][1]);
}
}
三、买卖股票的最佳时机II(贪心/动态规划)
可以在某一天买入,同一天卖出。买入卖出允许多次。
贪心:如果prices[i]>prices[i-1] 直接加利润
class Solution {
public int maxProfit(int[] prices) {
int sumProfit=0;
for(int i=1;i<prices.length;i++){
if(prices[i]-prices[i-1]>0){
sumProfit+=prices[i]-prices[i-1];
}
}
return sumProfit;
}
}
动态规划:
这个题和I的区别在于:上一道题只能一天买入,一天卖出。买入卖出的次数仅是1;而这一道题买入卖出的次数可以是多次。因此在状态方程上要发生变化
递推公式:
dp[i][0]可能是延续上一天的状态,也可能是在这一天把股票卖了。
dp[i][0](第i天不持有股票)=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1]可能是延续上一天的状态,也可能是在这一天买入股票
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
代码:
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0)
return 0;
// 定义dp数组
int[][] dp = new int[prices.length][2];
// 初始化dp数组
dp[0][0] = 0;// 不占有股票
dp[0][1] = -prices[0];// 占有股票
for (int i = 1; i < prices.length; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
}
return Math.max(dp[prices.length - 1][0], dp[prices.length - 1][1]);
}
}
四、买卖股票的最佳时机III(只可以买卖两次)
dp[prices.length][5]:dp数组的五个状态
不操作:dp[i][0]=dp[i-1][0];
第一次持有:dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
第一次不持有:dp[i][2]=Math.max(dp[i-1][2],dp[i-1][1]+prices[i]);
第二次持有:dp[i][3]=Math.max(dp[i-1][3],dp[i-1][2]-prices[i]);
第二次不持有:dp[i][4]=Math.max(dp[i-1][4],dp[i-1][3]+prices[i]);
初始化:
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[0][2] = 0;
dp[0][3] = -prices[0];
dp[0][4] = 0;
代码:
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0)
return 0;
// 只能买卖两次 dp五种状态 不操作 第一次持有 第一次不持有 第二次持有 第二次不持有
int[][] dp = new int[prices.length][5];
// dp数组初始化
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[0][2] = 0;
dp[0][3] = -prices[0];
dp[0][4] = 0;
// 遍历数组
for (int i = 1; i < prices.length; i++) {
dp[i][0] = dp[i - 1][0];
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
dp[i][2] = Math.max(dp[i-1][2],dp[i-1][1]+prices[i]);
dp[i][3] = Math.max(dp[i-1][3],dp[i-1][2]-prices[i]);
dp[i][4] = Math.max(dp[i-1][4],dp[i-1][3]+prices[i]);
}
return dp[prices.length-1][4];
}
}
五、买卖股票的最佳时机IV(k次交易)
从III找出规律来,两次交易,dp状态会有五次。k次交易,dp状态就会有2k+1次。
定义dp数组:dp[prices.length][2*k+1];
状态分别是:第一次不操作;第一次占有/第一次不占有;第二次占有/第二次不占有;.....;第k次占有/第k次不占有
dp数组初始化:
dp[0][0] = 0;
dp[0][1] = -prices[0]; dp[0][2] = 0; 第一次占有/不占有
dp[0][3] = -prices[0]; dp[0][4] = 0; 第二次占有/不占有
dp[0][2k-1]=-prices[0]; dp[0][2k]=0; 第k次占有/不占有
dp数组遍历:
不操作:dp[i][0]=dp[i-1][0];
第一次持有:dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
第一次不持有:dp[i][2]=Math.max(dp[i-1][2],dp[i-1][1]+prices[i]);
第二次持有:dp[i][3]=Math.max(dp[i-1][3],dp[i-1][2]-prices[i]);
第二次不持有:dp[i][4]=Math.max(dp[i-1][4],dp[i-1][3]+prices[i]);
找规律:
等于0, =dp[i-1][0];
不等于0,并且是偶数:+prices[i];
不等于0,并且是奇数: -prices[i];
代码:
class Solution {
public int maxProfit(int k, int[] prices) {
if(prices==null||prices.length==0)return 0;
//k次交易 定义dp数组
int[][] dp=new int[prices.length][1+2*k];
//初始化
dp[0][0]=0;//不操作
for(int i=1;i<1+2*k;i++){
if(i%2==0){
dp[0][i]=0;
}else{
dp[0][i]=-prices[0];
}
}
//进行遍历dp数组
for(int i=1;i<prices.length;i++){
for(int j=0;j<1+2*k;j++){
if(j==0){
dp[i][0]=dp[i-1][0];
}else if(j%2!=0){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-1]-prices[i]);
}else if(j%2==0){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-1]+prices[i]);
}
}
}
return dp[prices.length-1][1+2*k-1];
}
}
六、买卖股票的最佳时机(包含冷冻期)
冷冻期:在卖出股票后,无法在第二天在进行买入/卖出(冷冻期为一天)
dp[i][4]:四种状态 表示某一天某种状态下的最大利润
**占有股票:**dp[i][0]=Math.max(dp[i-1][0],dp[i-1][3]-prices[i],dp[i-1][1]-prices[i])
延续上一天占有股票的状态;冷冻期后的买入股票;不占有股票后的买入股票
**不占有股票:**dp[i][1]=Math.max(dp[i-1][1],dp[i-1][3]);
延续上一天不占股票的状态;上一天卖出股票
**卖出股票:**dp[i][2]=dp[i-1][0]+prices[i];
上一天为占有股票
**冷冻期:**dp[i][3]=dp[i-1][2];
上一天为卖出股票
代码:
class Solution {
public int maxProfit(int[] prices) {
//剪枝?
if(prices==null||prices.length==0)return 0;
/**
创建dp数组 0:持有股票 1:不持有股票 2:卖出股票 3:冷冻期
*/
int[][] dp=new int[prices.length][4];
//初始化dp数组
dp[0][0]=-prices[0];
dp[0][1]=0;
dp[0][2]=0;
dp[0][3]=0;
//遍历数组
for(int i=1;i<prices.length;i++){
//延续上一天占有股票的状态;不占有股票后的买入股票;冷冻期后的买入股票
dp[i][0]=Math.max(dp[i-1][0],Math.max(dp[i-1][1]-prices[i],dp[i-1][3]-prices[i]));
//延续上一天的不持有股票;冷冻期后的不持有股票
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][3]);
//上一天持有股票
dp[i][2]=dp[i-1][0]+prices[i];
//上一天为卖出股票
dp[i][3]=dp[i-1][2];
}
return Math.max(dp[prices.length-1][1],Math.max(dp[prices.length-1][2],dp[prices.length-1][3]));
}
}
返回最大利润:要进行比较,因为最大利润可能出现在不持有股票/卖出股票/冷冻期三种情况中。
七、买卖股票的最佳时机含手续费
思路:其实跟II差不多,但是多了一个手续费,这里我们规定在每次卖出的时候需要付手续费。因为卖出肯定是买入之后的,就是买入卖出整个操作需要付手续费。
改变:在dp公式需要作出改变;
不占有股票:dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]-fee);
占有股票:dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
初始化:dp[0][0]=0;dp[0][1]=-prices[0];
代码:
class Solution {
public int maxProfit(int[] prices, int fee) {
//含有手续费 就会限制你进行买卖
int[][] dp=new int[prices.length][2];
//dp[i][0]:不占有股票 dp[i][1]:占有股票 买的时候扣手续费
dp[0][0]=0;
dp[0][1]=0-prices[0];
//初始化dp数组
for(int i=1;i<prices.length;i++){
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]-fee);
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
}
return dp[prices.length-1][0];
}
}
买卖股票的最佳时机总结:
I:只能有一次买入/一次卖出
不持有股票:dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
持有股票:dp[i][1]=Math.max(dp[i-1][1],-prices[i]);
II:可以进行多次买入/卖出
不持有股票:dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
持有股票:dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
III:只能进行两次买入/卖出
状态分为五种:不操作;第一次占有/第一次不占有;第二次占有/第二次不占有
dp[i][0]=dp[i-1][0];
dp[i][1]=dp[i-1][0]-prices[i];
dp[i][2]=dp[i-1][1]+prices[i];
dp[i][3]=dp[i-1][2]-prices[i];
dp[i][4]=dp[i-1][3]+prices[i];
有规律的。
IV:只能进行k次买入/卖出
状态共有:2k+1次。
dp[i][j]=dp[i-1][j-1]±prices[i]; j为偶数+;j为奇数-;
V:包含冰冻期
状态共有四种:
占有股票:dp[i][0]=Math.max(dp[i-1][0],Math.max(dp[i-1][1]-prices[i],dp[i][3]-prices[i]));
不占有股票:dp[i][1]=Math.max(dp[i-1][1],dp[i-1][3]);
卖出股票:dp[i][2]=dp[i-1][0]+prices[i];
冰冻期:dp[i][3]=dp[i-1][2];
VI:含有手续费
在II的基础上,添加手续费。每次在交易完成的时候要付出手续费,在卖出股票时候。
不持有股票:dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]-fee);
持有股票:dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
子序列问题:
八、最长递增子序列
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4
思路:
1.dp[i]:表示从0到i,最长子序列的长度
2.dp[i]=Math.max(dp[i],dp[j]+1);
递推公式如何来的:根据dp[i]的含义,要找到nums[j]<nums[i]的元素,然后在dp[j]的基础上+1;
但是nums[j]不仅仅有一个,因此每次取dp[i],和dp[j]+1的较大的。
3.初始化:所有元素初始化为1
4.遍历顺序从1开始。双层for循环寻找nums[j]<nums[i]的元素,并且更新dp[i]
注意:最大值不是dp[nums.length-1],而是dp数组中的最大值。
代码:
class Solution {
public int lengthOfLIS(int[] nums) {
//定义dp数组
int[] dp=new int[nums.length];
//初始化
Arrays.fill(dp,1);
//双层for循环遍历
int max=1;
for(int i=1;i<nums.length;i++){
for(int j=0;j<i;j++){
if(nums[j]<nums[i]){
dp[i]=Math.max(dp[i],dp[j]+1);
max=Math.max(max,dp[i]);
}
}
}
return max;
}
}
九、最长连续递增序列(比较简单)
不需要双层for循环寻找比nums[i]小的元素了。因为题目要求子序列必须是连续的。
代码:
class Solution {
public int findLengthOfLCIS(int[] nums) {
int[] dp=new int[nums.length];
Arrays.fill(dp,1);
int max=1;
for(int i=1;i<nums.length;i++){
if(nums[i]>nums[i-1]){
dp[i]=dp[i-1]+1;
max=Math.max(max,dp[i]);
}
}
return max;
}
}
十、最长重复子数组
给两个整数数组 nums1
和 nums2
,返回 两个数组中 公共的 、长度最长的子数组的长度。
思路:
1.dp[i][j]:遍历到i-1,j-1的最长公共子数组的长度
2.if(nums[i-1]==nums[j-0])dp[i][j]=dp[i-1][j-1]+1;
3.初始化都为0,因为是从i-1,j-1(-1 -1开始的)。相当于多加了一行一列,这样就不用多用两个循环去初始化原本的第一行第一列。让所有工作交给遍历
4.从i和j分别从1,1开始到nums1.length,nums2.length;
代码:
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int len1=nums1.length;
int len2=nums2.length;
int max=0;
int[][] dp=new int[len1+1][len2+1];
for(int i=1;i<=len1;i++){
for(int j=1;j<=len2;j++){
if(nums1[i-1]==nums2[j-1]){
dp[i][j]=dp[i-1][j-1]+1;
max=Math.max(max,dp[i][j]);
}
}
}
return max;
}
}
十一、最长公共子序列(和上一题类似也有不同)
不同:上一道题要求的是子数组是连续的,因此如果nums1[i]==nums2[j]的时候,dp[i][j]=dp[i-1][j-1]+1;况且只能由[i-1][j-1]而来;
而这一道题不要求序列是连续的,因此当text1.charAt(i-1)==text2.charAt(j-1) 的时候,dp[i][j]=dp[i-1][j-1]+1;如果不相等的话,可以由dp[i-1][j]和dp[i][j-1]得到。
类似:和上一题一样,为了减少初始化时for循环的复杂,多加一行一列,dp[len1+1][len2+1]。
然后初始化由遍历帮助我们完成
代码:
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int len1=text1.length();
int len2=text2.length();
int[][] dp=new int[len1+1][len2+1];
int result=0;
for(int i=1;i<=len1;i++){
for(int j=1;j<=len2;j++){
if(text1.charAt(i-1)==text2.charAt(j-1)){
dp[i][j]=dp[i-1][j-1]+1;
}else{
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
}
result=Math.max(result,dp[i][j]);
}
}
printfDp(dp);
return result;
}
public void printfDp(int[][] dp){
for(int[] arr:dp){
for(int i:arr){
System.out.print(i+" ");
}
System.out.println("");
}
}
}
十二、最大子序和(贪心/动态规划)
贪心:
如果前面的值是负的,那么就不加(加上拖后腿)
如果前面的值是正的,再加(加上更大)
代码:
public int maxSubArray(int[] nums) {
int result=Integer.MIN_VALUE;
int count=0;
for(int i=0;i<nums.length;i++){
count+=nums[i];
if(count>result)result=count;
if(count<0)count=0;
}
return result;
}
动态规划:
1.dp[i]:从0->i,最大的子序列的和
2.if(dp[i-1]>0)dp[i]=dp[i-1]+nums[i];else dp[i]=nums[i]
3.初始化:dp[0]=nums[0];
4.从i=1开始遍历
十三、判断子序列(双指针/动态规划)
双指针:
定义slow,fast指针。slow指向s串,fast指向t串。如果fast遍历到末尾以及之前,slow也可以遍历到末尾,那么就包含。
代码:
class Solution {
public boolean isSubsequence(String s, String t) {
int sLen=s.length();
int tLen=t.length();
if(sLen>tLen)return false;
int slow = 0;
int fast = 0;
while(fast<tLen){
if(slow==sLen)return true;
else{
if(t.charAt(fast)==s.charAt(slow)){
slow++;
}
fast++;
}
}
return slow==sLen;
}
}
注意:如果是slow和fast同时到达末尾的话,那么while(fast<tLen)最后一次就没进去,但此时slow==sLen,就无法判断到。因此返回的时候应该写:return slow==sLen;
动态规划(类似于求最长公共子序列的长度):
如果最长子序列的长度==s串的长度,那么就返回true。
class Solution {
public boolean isSubsequence(String s, String t) {
int sLen = s.length();
int tLen = t.length();
if (sLen > tLen)
return false;
// 定义dp数组
int[][] dp = new int[sLen + 1][tLen + 1];
// 遍历dp数组(初始化dp数组放到遍历中)
int max = 0;
for (int i = 1; i <= sLen; i++) {
for (int j = 1; j <= tLen; j++) {
if (s.charAt(i - 1) == t.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
}
max = Math.max(dp[i][j], max);
}
}
printfDp(dp);
System.out.println(max);
return max == sLen;
}
public void printfDp(int[][] dp){
for(int[] arr:dp){
for(int i:arr){
System.out.print(i+" ");
}
System.out.println(" ");
}
}
十四、不同的子序列(回溯法/动态规划)
回溯法:超时..
代码:
class Solution {
StringBuilder sb = new StringBuilder();
int count = 0;
public int numDistinct(String s, String t) {
if(s.length()<t.length())return 0;
backTracking(s, t, 0);
return count;
}
public void backTracking(String s, String t, int startIndex) {
// 终止条件
if (sb.toString().equals(t)) {
count++;
return;
}
if (startIndex >= s.length())
return;
// 单层递归逻辑
for (int i = startIndex; i < s.length(); i++) {
sb.append(s.charAt(i));
backTracking(s, t, i + 1);
sb.deleteCharAt(sb.length() - 1);
}
}
}
动态规划:
1.dp[i][j]:以i-1为结尾的s中有多少个以j-1为结尾的t (方便操作 不用初始化了)
2.递推公式(难以理解)
if(s.charAt(i)==t.charAt(j))dp[i][j]==dp[i-1][j-1]+dp[i-1]dp[j];
else dp[i][j]=dp[i-1][j];
如何理解:
当 s[i-1]
与 t[j-1]
不相等时
dp[i][j] = dp[i-1][j]
- 解释:此时我们不能用
s[i-1]
来匹配t[j-1]
,所以只能看s[0...i-2]
中有多少个t[0...j-1]
的子序列。
当 s[i-1]
与 t[j-1]
相等时
dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
- 解释:我们有两种选择:
- 用
s[i-1]
来匹配t[j-1]
:此时我们需要看s[0...i-2]
中有多少个t[0...j-2]
的子序列,这就是dp[i-1][j-1]
。 - 不用
s[i-1]
来匹配t[j-1]
:此时我们需要看s[0...i-2]
中有多少个t[0...j-1]
的子序列,这就是dp[i-1][j]
。
- 用
3.初始化:dp[i][0]=1;dp[0][j]=0;
4.遍历顺序 1 1
代码:
class Solution {
public int numDistinct(String s, String t) {
int[][] dp = new int[s.length() + 1][t.length() + 1];
for (int i = 0; i < s.length() + 1; i++) {
dp[i][0] = 1;
}
for (int i = 1; i < s.length() + 1; i++) {
for (int j = 1; j < t.length() + 1; j++) {
if (s.charAt(i - 1) == t.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
}else{
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[s.length()][t.length()];
}
}