32. 最长有效括号
解法一:动态规划
思路:
dp[i]表示以第i个结尾的最长有效括号个数。
当s[i] == ‘(’ 时,dp[i] = 0;
当s[i] == ‘)’ 时 ,得分两种情况:
- s[i-1] == ‘(’ 时,此时s[i] 只能与s[i-1]组成括号,得dp[i] = dp[i-2] + 2;
- s[i-1] != ‘(’ 时,这说明 s[i-1] 为 ‘)’,可能与前面的dp[i-1] - 1个组成最长有效括号,我们需要回去检查c[i-dp[i-1] -1]是否为‘(’,若是,则可以与c[i] 组成有效括号,否则dp[i]=0。其中 ,若c[i-dp[i-1] -1] 能与c[i] 组成有效括号,此时 dp[i] = dp[i-1] + 2 + dp[i-dp[i-1]-2]; dp[i-dp[i-1]-2]是第i-dp[i-1] -2位的最长有效括号长度。
例子:
class Solution {
public int longestValidParentheses(String s) {
int len = s.length();
if(len==0||len==1) return 0;
int[] dp = new int[len];//dp[i] 表示以字符串i结尾的最长有效括号长度。
char[] c = s.toCharArray();
dp[0] = 0;
if(c[0]=='(' && c[1]==')') dp[1] = 2;
for(int i=2;i<len;i++){
if(c[i]==')'){
if(c[i-1]=='(')
dp[i] = dp[i-2] + 2;
else{
if((i-dp[i-1]-1)>=0&&c[i-dp[i-1]-1]=='('){
if(i-dp[i-1]-2>=0)
dp[i] = dp[i-1] + 2 + dp[i-dp[i-1]-2];
else
dp[i] = dp[i-1] + 2 ;
}
}
}
}
int max = 0;
for(int x : dp)
max = Math.max(max,x);
return max;
}
}
解法二:利用栈判断括号是否优先,并存放下标
撇开方法一提及的动态规划方法,相信大多数人对于这题的第一直觉是找到每个可能的子串后判断它的有效性,但这样的时间复杂度会达到 O(n^3)O(n
3
),无法通过所有测试用例。但是通过栈,我们可以在遍历给定字符串的过程中去判断到目前为止扫描的子串的有效性,同时能得到最长有效括号的长度。具体做法是我们始终保持栈底元素为当前已经遍历过的元素中「最后一个没有被匹配的右括号的下标」,这样的做法主要是考虑了边界条件的处理,栈里其他元素维护左括号的下标:
- 对于遇到的每个 \text{‘(’}‘(’ ,我们将它的下标放入栈中
- 对于遇到的每个 \text{‘)’}‘)’ ,我们先弹出栈顶元素表示匹配了当前右括号:
(1)如果栈为空,说明当前的右括号为没有被匹配的右括号,我们将其下标放入栈中来更新我们之前提到的「最后一个没有被匹配的右括号的下标」
(2)如果栈不为空,当前右括号的下标减去栈顶元素即为「以该右括号为结尾的最长有效括号的长度」
// 具体做法是我们始终保持栈底元素为当前已经遍历过的元素中「最后一个没有被匹配的右括号的下标」,这样的做法主要是考虑了边界条件的处理,栈里其他元素维护左括号的下标:
// 对于遇到的每个 ‘(’ ,我们将它的下标放入栈中
// 对于遇到的每个 ‘)’ ,我们先弹出栈顶元素表示匹配了当前右括号:
// 如果栈为空,说明当前的右括号为没有被匹配的右括号,我们将其下标放入栈中来更新我们之前提到的「最后一个没有被匹配的右括号的下标」
// 如果栈不为空,当前右括号的下标减去栈顶元素即为「以该右括号为结尾的最长有效括号的长度」
class Solution {
public int longestValidParentheses(String s) {
Stack<Integer> stack = new Stack<>();
char[] c = s.toCharArray();
stack.push(-1);
int max = 0;
for(int i=0;i<c.length;i++){
if(c[i]=='(')
stack.push(i);
else{
stack.pop();
if(stack.empty())
stack.push(i);
else{
max = Math.max(max,i-stack.peek());
}
}
}
return max;
}
}
33. 搜索旋转排序数组
这是一道做了好多次的题目,再做还是继续不会,我太菜了
总体思路:根据中间位置划分左半边还是右半边有序,然后在有序的一遍进行查找,若找不到则证明在另一边。
class Solution {
public int search(int[] nums, int target) {
int len = nums.length;
int left = 0;
int right = len-1;
while(left <= right ){
int mid = left + (right - left)/2 ;
if(nums[mid] == target)
return mid;
// 判断哪一边有序
// 左半边有序
if(nums[mid]>=nums[left]){
if(target < nums[mid] && target >= nums[left])//判断target是否在有序的一遍,左半边有序,判断条件为:(target>=nums[left] &&target<nums[mid])
right = mid - 1;
else
left = mid + 1;
}
// 右半边有序
else{
if(target > nums[mid] && target <= nums[right] )
left = mid + 1;
else
right = mid - 1;
}
}
return -1;
}
}
含重复数字的解法
解法:参考《搜索旋转排序数组的值》
不同在于如何去除首尾的重复元素。
class Solution {
public boolean search(int[] nums, int target) {
int len = nums.length;
int left = 0;
int right = len -1;
while(left<=right){
while(right>=1 && nums[right]==nums[right-1])
right--;
int mid = left + (right - left) / 2;
if(nums[mid] == target)
return true;
// 左边有序
if(nums[mid] >= nums[left]){
if(target >= nums[left] && target < nums[mid])
right = mid -1;
else{
left = mid + 1;
}
}
// 右边有序
else{
if(target > nums[mid] &&target <= nums[right])
left = mid + 1;
else
right = mid -1;
}
}
return false;
}
}
寻找旋转排序数组中的最小值
思路:如果nums[mid] > nums[right], 则说明mid左边的值都是小于nums[mid]的,原因是nums[0-mid]的值都大于nums[right]。
class Solution {
public int findMin(int[] nums) {
int len = nums.length;
int left = 0;
int right = len - 1;
while(left<right){
int mid = left + (right - left) / 2;
if(nums[mid] > nums[right])
left = mid + 1;
else
right = mid;
}
return nums[left];
}
}
49. 字母异位词分组
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
HashMap<String, List<String>> map = new HashMap<>();
for(String str : strs){
char[] c = str.toCharArray();
Arrays.sort(c);
String s = String.valueOf(c);
if(!map.containsKey(s)){
List list = new ArrayList<>();
list.add(str);
map.put(s,list);
}
else{
List<String> list = map.get(s);
list.add(str);
map.put(s,list);
}
}
return new ArrayList<>(map.values());
}
}
剑指 Offer 51. 数组中的逆序对
思路:按照归并排序的思想,进行合并时,会进行比较,看看是前一个段的数字还是后一个段的数字放入数组放入临时数组。
这这个过程中,可以统计逆序对的个数。假如前一个数字的大于后一个数字,则说明前一个数字到mid的数字都大一后一个数字。
class Solution {
public int reversePairs(int[] nums) {
int count = merge_sort_count(nums, 0, nums.length - 1);
for(int tmp : nums)
System.out.print(tmp +" ");
return count;
}
public int merge_sort_count(int[] nums,int left, int right){
if(left >= right)
return 0;
int mid = left + (right - left)/2;
int left_count = merge_sort_count(nums, left, mid);
int right_count = merge_sort_count(nums, mid + 1, right);
int count = 0;
int[] tmp_nums = new int[right - left + 1];
int index = 0;
int l = left;
int r = mid + 1;
while(l <= mid && r <= right){
if(nums[l] > nums[r] ){
count += mid - l + 1; //l到mid的个数,这每一个都大于nums[r].
tmp_nums[index++] = nums[r++];
}
else{
tmp_nums[index++] = nums[l++];
}
}
while(l <= mid){
tmp_nums[index++] = nums[l++];
}
while(r <= right){
tmp_nums[index++] = nums[r++];
}
for(int i = 0; i <= right - left; i++)
nums[i + left] = tmp_nums[i];
// System.out.println(left_count);
return left_count + right_count + count;
}
}
寻找第K大的数字
思路:参考快排的锚点。
import java.util.*;
public class Solution {
public int findKth(int[] a, int n, int K) {
// write code here
int x = findK(a,0,n-1,K);
for(int tmp : a)
System.out.print(tmp +" ");
return x;
}
public int findK(int[] a ,int left, int right, int K){
if(left <= right){
int index = quickSort(a,left,right);
if(index == K - 1)
return a[index];
else if( index < K - 1)
return findK(a,index + 1, right, K);
else
return findK(a,left,index - 1, K);
}
return -1;
}
public int quickSort(int[] nums ,int left , int right){
int tmp = nums[left];
while(left < right){
while(left < right && nums[right] < tmp) right--;
nums[left] = nums[right];
while( left < right && nums[left] > tmp) left++;
nums[right] = nums[left];
}
nums[left] = tmp;
return left;
}
}
76. 最小覆盖子串
难点:
- 如何判断滑动窗口是否覆盖字符串
- 如何将窗口左指针右移
思路:
- 使用两个数组,分别记录t字符串与滑动窗口字符串的单词频数
- 利用1的词频,如果winFren[s.charAt(left)] > tFren[s.charAt(left)] 则将左指针右移。
// 滑动窗口
class Solution {
public String minWindow(String s, String t) {
int[] tFren = new int[127];
int[] winFren = new int[127];
for(int i = 0; i < t.length(); i++ )
tFren[t.charAt(i)]++;
int left = 0;
int right = 0;
int start = 0;
int min_len = Integer.MAX_VALUE;
while(right < s.length()){
winFren[s.charAt(right)] ++;
if(s.charAt(right) == 'A')
System.out.println("???");
int count = 0;
for(int i = 0; i < 127; i++){
if(winFren[i] >= tFren[i])
count++;
else
continue;
}
// 如果winFren的频率全部大于等于tFren的频率。
while(count == 127){
System.out.println("left:"+ left+":right:"+ right);
if(min_len > (right - left + 1)){
min_len = right - left + 1;
start = left;
}
if(winFren[s.charAt(left)] <= tFren[s.charAt(left)]){
count -- ;
}
else{
winFren[s.charAt(left)] --;
left ++ ;
}
}
right ++ ;
}
return min_len ==Integer.MAX_VALUE?"" : s.substring(start,start + min_len);
}
}
124. 最大路径和
思路:计算每一个节点的左分支 + 右分支 + 该节点的值。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int max = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
if(root == null)
return 0;
dfs(root);
return max;
}
public int dfs(TreeNode root){
if(root == null)
return 0;
int left = dfs(root.left);
int right = dfs(root.right);
int val = root.val;
left = Math.max(left,0);
right = Math.max(right,0);
max = Math.max(max, left + right + val);;
return Math.max(left,right) + val;
}
}
146. LRU缓存机制
class LRUCache {
// 双向链表的结点
class Node{
public int key,val;
public Node next, prev;
public Node(int k, int v){
this.key = k;
this.val = v;
}
}
// 双向链表的实现
class DoubleList{
// 双向链表头结点与尾结点
Node head = new Node(0,0);
Node tail = new Node(0,0);
int size;
public DoubleList(){
head.next = tail;
tail.prev = head;
size = 0;
}
// 在链表头部增加节点x,时间复杂度为O(1);
public void addFrist(Node x){
Node headNext = head.next;
head.next = x;
headNext.prev = x;
x.prev = head;
x.next = headNext;
size++;
}
// 删除链表中的节点x,(x一定存在)
// 由于是双向链表且给定目标节点,因此时间复杂度是O(1)。
public void remove(Node x){//表示链表中的一个节点,因此有指向上一个节点与下一个节点的指针。
// 只需要修改指针即可删除该节点。
x.prev.next = x.next;
x.next.prev = x.prev;
size--;
}
// 删除链表中最后一个节点且返回该节点
public Node removeLast(){
Node last = tail.prev;
remove(last);
return last;
}
// 返回链表长度
public int size(){
return size;
}
}
HashMap<Integer ,Node> map ;
public DoubleList cache;
int capacity;
public LRUCache(int capacity) {
this.map = new HashMap<>();
this.cache = new DoubleList();
this.capacity = capacity;
}
public int get(int key) {
if(!map.containsKey(key))
return -1;
int val = map.get(key).val;
put(key,val);
return val;
}
public void put(int key, int value) {
Node x = new Node(key,value);
if(map.containsKey(key))//该节点已经存在,则先删除该节点再插入。
{
cache.remove(map.get(key));
cache.addFrist(x);
map.put(key,x);
}
else{
if(capacity==cache.size()){
//已经超过长度,则删除最后一个节点
Node last = cache.removeLast();
map.remove(last.key);
}
cache.addFrist(x);
map.put(key,x);
}
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
使用O(1)的空间复杂度逆序栈
package com.zhiwenwu.proxy;
import java.util.Stack;
/**
* 仅用递归函数和栈逆序一个栈:ReverseStack【2】
*
* 【一个栈依次压入1、2、3,将栈转置,使栈顶到栈底依次是1、2、3,只能用递归函数,不能借用额外的数据结构包括栈】
*
* 算法思想:两个递归函数(getAndRemoveBottom、reverse)
*
* @author xiaofan
*/
public class ReverseStack {
/**
* 返回并移除当前栈底元素(栈内元素<栈底>1、2、3<栈顶>变为2、3<栈顶>).
*/
private int getAndRemoveBottom(Stack<Integer> stack) {
int result = stack.pop();
if (stack.empty()) {
return result;
} else {
int bottom = getAndRemoveBottom(stack);
stack.push(result);
return bottom; // 第一轮时,返回栈底元素1
}
}
/**
* 每层递归取出栈底的元素并缓存到变量中,直到栈空;
*
* 然后逆向将每层变量压入栈,最后实现原栈数据的逆序。
*
* @param stack
*/
public void reverse(Stack<Integer> stack) {
if (stack.empty()) {
return;
}
int i = getAndRemoveBottom(stack); // 依次返回1、2、3
reverse(stack);
stack.push(i); // 依次压入3、2、1
}
/ 测试方法
public static void main(String[] args) {
{
Stack stack = new Stack(); // Stack继承Vector,默认容量是10
stack.push(1);
stack.push(2);
stack.push(3);
ReverseStack rStack = new ReverseStack();
rStack.reverse(stack);
while (!stack.empty()) {
System.out.println(stack.pop());
}
}
}
}
958. 二叉树的完全性检验
给定一个二叉树,确定它是否是一个完全二叉树。
思路:寻找第一个左右节点为空或者是左节点为空右节点不为空的节点,判断该节点之后的节点是否还有孩子,若有孩子则不是完全二叉树。
特别地,如果一个节点左节点为空右节点不为空,则不是二叉树。
使用层次遍历算法
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public boolean isCompleteTree(TreeNode root) {
boolean flag = false;
if(root == null )
return true;
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.add(root);
while(!queue.isEmpty()){
int k = queue.size();
for(int i = 0; i < k; i++){
TreeNode tmp = queue.remove();
if(flag){
if(tmp.left != null || tmp.right != null)
return false;
}
if(tmp.left == null && tmp.right != null)
return false;
if(tmp.left != null && tmp.right == null || tmp.left == null && tmp.right == null){
flag = true;
}
if(tmp.left != null){
queue.add(tmp.left);
}
if(tmp.right != null){
queue.add(tmp.right);
}
}
}
return true;
}
}
79. 单词搜索
思路:回溯法
class Solution {
int[][] directions = {{-1,0},{0,1},{1,0},{0,-1}};
boolean[][] visited ;
public boolean exist(char[][] board, String word) {
int row = board.length;
int column = board[0].length;
visited = new boolean[row][column];
for(int i = 0; i< board.length; i++)
for(int j = 0; j < board[0].length; j++){
if(back(board,word,i,j,0))
return true;
}
return false;
}
boolean back(char[][] board, String word, int i, int j,int index){
if(index == word.length() -1)
return board[i][j]==word.charAt(index);
if(board[i][j] != word.charAt(index))
return false;
else{
visited[i][j] = true;
for(int k = 0; k < 4; k++){
int new_i = i + directions[k][0];
int new_j = j + directions[k][1];
if(new_i >=0&& new_i < board.length &&new_j >= 0 && new_j < board[0].length && !visited[new_i][new_j] )
if(back(board,word,new_i,new_j,index+1))
return true;
}
visited[i][j] = false;
}
return false;
}
}
被围绕的区域
// 思路:曲线救国法
// (1) 从外侧的O入手,寻找内部所有与最外部的O相连的区域。使用回溯法
// (2) 将其修改为其他的符号A。
// (3) 标记为所有的O后,将其修改为你O,然后将剩余的修改为X
class Solution {
int[][] direction = {{-1,0},{0,1},{1,0},{0,-1}};
public void solve(char[][] board) {
int row = board.length;
int column = board[0].length;
for(int i = 0; i < row; i++)
for(int j = 0; j < column; j++){
if(board[i][j] == 'O' && (i == 0 || i == row-1 || j==0 || j == column-1)){
back(board,i,j);
}
}
for(int i = 0; i < row; i++)
for(int j = 0; j < column; j++){
if(board[i][j]=='A')
board[i][j] = 'O';
else
board[i][j] = 'X';
}
}
void back(char[][] board,int i, int j){
board[i][j] = 'A';
for(int k = 0; k < 4; k++ ){
int new_i = i + direction[k][0];
int new_j = j + direction[k][1];
if(new_i>=0&&new_i<board.length&&new_j>=0&&new_j<board[0].length&&board[new_i][new_j]=='O')
back(board,new_i,new_j);
}
}
}
84. 柱状图中最大的矩形
思路:
在这里插入代码片
85. 最大矩形
动态规划
在这里插入代码片
96. 不同的二叉搜索树
// 动态规划法
// dp[i]表示i个节点组成的二叉搜索树个数
// dp[i] = f(1) + f(2) +...+ f(j) + ... + f(i)
// f(j) 表示以j作为根节点的二叉树个数
// 因此 f(j) = dp[j-1] * dp[i-j];
class Solution {
public int numTrees(int n) {
int[] dp = new int[ n + 1];
dp[0] = 1;
dp[1] = 1;
for(int i = 2; i <= n; i++){
for(int j = 1; j <= i; j++){
dp[i] += dp[j-1] * dp[i-j];
}
}
return dp[n];
}
}
98. 验证二叉搜索树
思路:采用中序遍历,节点的左子树只包含小于当前节点的数。节点的右子树只包含大于当前节点的数。
难点,如何判断是否小于或大于根节点
精华在于long pre = Long.MIN_VALUE;
class Solution {
long pre = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
if(root == null )
return true;
if(!isValidBST(root.left))
return false;
if(root.val <= pre)
return false;
pre = root.val;
if(!isValidBST(root.right))
return false;
return true;
}
}
复习“数组中的逆序对”解法
先使用中序遍历保存遍历数组,加入是二叉搜索树,则中序遍历是有序的,此时可以采用数组中逆序对个数这个算法来判断数组是否有序
// 中序遍历是有序的。
// 配合逆序对个数算法,判断数组是否是有序的
class Solution {
List<Integer> res;
int count;
public boolean isValidBST(TreeNode root) {
count = 0;
res = new ArrayList<>();
inorder(root);
int[] arr = new int[res.size()];
for(int i = 0; i < res.size(); i++ )
arr[i] = res.get(i);
for(int i = 0; i < arr.length; i++)
System.out.println(arr[i]);
// 计算逆序对个数
count = mergeSort(arr,0,arr.length-1);
for(int i = 0; i < arr.length; i++)
System.out.println(arr[i]);
return count == 0;
}
public int mergeSort(int[] arr, int left, int right){
if(left >= right)
return 0;
int mid = left + (right - left)/2;
int count = 0;
int left_count = mergeSort(arr,left,mid);
int right_count =mergeSort(arr,mid+1,right);
int[] tmp_arr = new int[right - left + 1];
int i = left;
int j = mid + 1;
int index = 0;
while(i <= mid && j <= right){
if(arr[i] >= arr[j]){
tmp_arr[index++] = arr[j++];
count += mid - i + 1;
}
else{
tmp_arr[index++] = arr[i++];
}
}
while(i <= mid)
tmp_arr[index++] = arr[i++];
while(j <= right)
tmp_arr[index++] = arr[j++];
for(int k = 0; k < right - left + 1; k++){
arr[k + left] = tmp_arr[k];
}
return left_count + right_count + count;
}
public void inorder(TreeNode root){
if(root == null)
return ;
inorder(root.left);
res.add(root.val);
inorder(root.right);
}
}
114. 二叉树展开为链表
不能使用先序遍历存储节点再展开,使用O(1)的空间复杂度。
128. 最长连续序列
// 使用hashmap<Integer,Integer> 分别存储该数字最长序列
class Solution {
public int longestConsecutive(int[] nums) {
HashMap<Integer, Integer> map = new HashMap<>();
int max = 0;
for(int i = 0; i < nums.length; i++){
if(!map.containsKey(nums[i])){
int left = map.getOrDefault(nums[i]-1,0);
int right = map.getOrDefault(nums[i] + 1,0);
max = Math.max(max, 1 + left + right);
map.put(nums[i], 1 + left + right);
map.put(nums[i] - left, 1 + left + right);
map.put(nums[i] + right, 1 + left + right);
}
}
return max;
}
}
152. 乘积最大子数组
// 两个dp数组,一个存储最大,一个存储最小,一个存储正数,一个存储负数
class Solution {
public int maxProduct(int[] nums) {
int len = nums.length;
int[] dp_min = new int[len];
int[] dp_max = new int[len];
dp_max[0] = dp_min[0] = nums[0];
int max = dp_max[0];
for(int i = 1; i < len; i++){
if(nums[i] <= 0){
dp_max[i] = Math.max(nums[i],dp_min[i-1] * nums[i]);
dp_min[i] = Math.min(nums[i],dp_max[i-1] * nums[i]);
}
else{
dp_max[i] = Math.max(nums[i],dp_max[i-1] * nums[i]);
dp_min[i] = Math.min(nums[i],dp_min[i-1] * nums[i]);
}
// dp_max[i] = Math.max(nums[i],Math.max(dp_max[i-1]*nums[i],dp_min[i-1]*nums[i]));
// dp_min[i] = Math.min(nums[i],Math.min(dp_max[i-1]*nums[i],dp_min[i-1]*nums[i]));
max = Math.max(max,dp_max[i]);
}
for(int i = 0; i< len; i++){
System.out.println(dp_min[i]);
}
return max;
}
}
105. 从前序遍历与中序遍历构建二叉树
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
return builder(preorder,0,preorder.length-1,inorder,0,inorder.length-1);
}
public TreeNode builder(int[] preorder, int pre_l, int pre_r, int[] inorder, int in_l, int in_r){
if(pre_l > pre_r ||in_l > in_r)
return null;
int val = preorder[pre_l];
TreeNode root = new TreeNode(val);
int index = -1;
int count = 0;
for(int i = in_l; i < inorder.length; i++){
if(inorder[i] == val){
index = i;
break;
}
else
count++;
}
root.left = builder(preorder,pre_l+1,pre_l+count,inorder,in_l,index-1);
root.right = builder(preorder,pre_l+count+1,pre_r,inorder,index+1,in_r);
return root;
}
}
买卖股票问题的套路模板
这个问题的「状态」有三个,第一个是天数,第二个是允许交易的最大次数,第三个是当前的持有状态(即之前说的 rest 的状态,我们不妨用 1 表示持有,0 表示没有持有)。然后我们用一个三维数组就可以装下这几种状态的全部组合:
dp[i][k][0 or 1]
0 <= i <= n-1, 1 <= k <= K
n 为天数,大 K 为最多交易数
此问题共 n × K × 2 种状态,全部穷举就能搞定。
for 0 <= i < n:
for 1 <= k <= K:
for s in {0, 1}:
dp[i][k][s] = max(buy, sell, rest)
而且我们可以用自然语言描述出每一个状态的含义,比如说 dp[3][2][1] 的含义就是:今天是第三天,我现在手上持有着股票,至今最多进行 2 次交易。再比如 dp[2][3][0] 的含义:今天是第二天,我现在手上没有持有股票,至今最多进行 3 次交易。很容易理解,对吧?
我们想求的最终答案是 dp[n - 1][K][0],即最后一天,最多允许 K 次交易,最多获得多少利润。读者可能问为什么不是 dp[n - 1][K][1]?因为 [1] 代表手上还持有股票,[0] 表示手上的股票已经卖出去了,很显然后者得到的利润一定大于前者。
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
max( 选择 rest , 选择 sell )
解释:今天我没有持有股票,有两种可能:
要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有;
要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
max( 选择 rest , 选择 buy )
解释:今天我持有着股票,有两种可能:
要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。
121. 买卖股票的最佳时机
dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i])
= max(dp[i-1][1][1], -prices[i])
解释:k = 0 的 base case,所以 dp[i-1][0][0] = 0。
现在发现 k 都是 1,不会改变,即 k 对状态转移已经没有影响了。
可以进行进一步化简去掉所有 k:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], -prices[i])
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
int[] by = new int[len];
int[] sell = new int[len];
by[0] = -prices[0];
sell[0] = 0;
for(int i = 1; i < len; i++){
// 比较当天卖出去收益大还是之前的卖出去收益大
sell[i] = Math.max(sell[i-1], by[i-1] + prices[i]);
// 比较哪一天买入的价格最低。
by[i] = Math.max(by[i-1],-prices[i]);
}
return sell[len-1];
}
}
122. 买卖股票的最佳时机 II
如果 k 为正无穷,那么就可以认为 k 和 k - 1 是一样的。可以这样改写框架:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
= max(dp[i-1][k][1], dp[i-1][k][0] - prices[i])
我们发现数组中的 k 已经不会改变了,也就是说不需要记录 k 这个状态了:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
int[][] dp = new int[len][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for(int i = 1; i < len; 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 dp[len-1][0];
}
}
309. 最佳买卖股票时机含冷冻期
每次 sell 之后要等一天才能继续交易。只要把这个特点融入上一题的状态转移方程即可:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])
解释:第 i 天选择 buy 的时候,要从 i-2 的状态转移,而不是 i-1 。
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
if(len < 2)
return 0;
int[][] dp = new int[len][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[1][0] = Math.max(dp[0][0],dp[0][1] + prices[1]);
dp[1][1] = Math.max(dp[0][1],dp[0][0] - prices[1]);
for(int i = 2; i < len; 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-2][0] - prices[i]);
}
return dp[len-1][0];
}
}
714. 买卖股票的最佳时机含手续费
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)
解释:相当于买入股票的价格升高了。
在第一个式子里减也是一样的,相当于卖出股票的价格减小了。
class Solution {
public int maxProfit(int[] prices, int fee) {
int len = prices.length;
int[][] dp = new int[len][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for(int i = 1; i < len; 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[len-1][0];
}
}
123. 买卖股票的最佳时机 III
原始的动态转移方程,没有可化简的地方
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
int k = 2;
int[][][] dp = new int[n][k + 1][2];
for (int i = 0; i < n; i++)
if (i - 1 == -1) { /* 处理一下 base case*/ }
dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
}
return dp[n - 1][k][0];
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
int[][][] dp = new int[len][3][2];
dp[0][1][0] = dp[0][0][0] = 0;
dp[0][2][1] = dp[0][1][1] = dp[0][0][1] = -prices[0];
for(int i = 1; i < len ; i++)
for(int k = 2; k >= 1; k--){
dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
}
return dp[len-1][2][0];
}
}
188. 买卖股票的最佳时机 IV
class Solution {
public int maxProfit(int k, int[] prices) {
int len = prices.length;
if(len < 2)
return 0;
int[][][] dp = new int[len][k+1][2];
for(int i = 0; i <= k; i++){
dp[0][i][0] = 0;
dp[0][i][1] = -prices[0];
}
for(int i = 1; i < len; i++)
for( int j = k; j >= 1; j--){
dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1] + prices[i]);
dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i]);
}
return dp[len-1][k][0];
}
}
416. 分割等和子集
思路:参考01背包算法
class Solution {
public boolean canPartition(int[] nums) {
int len = nums.length;
if(len == 0)
return true;
Arrays.sort(nums);
int sum = 0;
for(int i = 0; i < len; i++)
sum += nums[i];
if(sum % 2 != 0)
return false;
if(nums[len-1] > sum/2)
return false;
// 有len件物品,恰好装满容量为sum/2的背包
boolean[][] dp = new boolean[len+1][sum/2+1];
// 边界条件,容量为0,一定能装满
for(int i = 0; i < len; i ++)
dp[i][0] = true;
// 元素为0,则一定装不满
for(int i = 0; i <= sum/2; i++)
dp[0][i] = false;
for(int j = 1; j <= sum/2; j++){
for(int i = 1; i <= len; i++){
if(nums[i-1] > j){//第i件大于j,放不进去
dp[i][j] = dp[i-1][j];
}
else{
dp[i][j] = dp[i-1][j] || dp[i-1][j - nums[i-1]];
}
}
}
return dp[len][sum/2];
}
}
617. 合并二叉树
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if(root1 == null && root2 == null)
return null;
TreeNode root = new TreeNode();
if(root1 != null && root2 != null){
root = new TreeNode(root1.val + root2.val);
root.left = mergeTrees(root1.left,root2.left);
root.right = mergeTrees(root1.right,root2.right);
}
else if(root1 != null){
root = root1;
}
else
root = root2;
return root;
}
}
621. 任务调度器
class Solution {
public int leastInterval(char[] tasks, int n) {
int[] buckets = new int[26];
for(int i = 0; i < tasks.length; i++){
buckets[tasks[i] - 'A']++;
}
Arrays.sort(buckets);
int maxTimes = buckets[25];
int maxCount = 1;
for(int i = 25; i >= 1; i--){
if(buckets[i] == buckets[i - 1])
maxCount++;
else
break;
}
int res = (maxTimes - 1) * (n + 1) + maxCount;
return Math.max(res, tasks.length);
}
}
93 复原IP地址
class Solution {
List<String> res;
public List<String> restoreIpAddresses(String s) {
int len = s.length();
res = new ArrayList<>();
if(len < 4 || len > 12)
return res;
back(s,0,new LinkedList<String>());
return res;
}
public void back(String s, int start, LinkedList<String> queue){
if(start == s.length()){
if(queue.size() == 4){
res.add(String.join(".",queue));
}
}
if((s.length() - start) < (4 - queue.size()) || (s.length() - start) > 3*(4 - queue.size()))
return;
for(int i = 1; i <= 3; i++){
int end = start + i;
if(end > s.length())
break;
String tmp = s.substring(start,end);
if(judge(tmp)){
queue.addLast(tmp);
back(s,end,queue);
queue.removeLast();
}
}
}
public boolean judge(String s){
int len = s.length();
if(len > 1 && s.charAt(0) == '0')
return false;
int tmp = Integer.parseInt(s);
if(tmp < 0 || tmp > 255)
return false;
return true;
}
}
不同的二叉搜索树2
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<TreeNode> generateTrees(int n) {
return build(1,n);
}
public List<TreeNode> build(int start, int end){
List<TreeNode> res = new ArrayList<TreeNode>();
if(start > end){
res.add(null);
return res;
}
for(int i = start; i <= end; i++){
List<TreeNode> left = build(start,i -1);// 返回一个list,里面是左子树不同的种类
List<TreeNode> right = build(i + 1, end);// 返回一个list,里面是由子树不同的种类
for(TreeNode tmpleft : left)//两个for循环
for(TreeNode tmpright : right){
res.add(new TreeNode(i,tmpleft,tmpright));
}
}
return res;
}
}