https://leetcode-cn.com/problems/regular-expression-matching/solution/zheng-ze-biao-da-shi-pi-pei-by-leetcode-solution/
public boolean isMatch(String s, String p) {
int m = s.length();
int n = p.length();
boolean[][] dp = new boolean[m+1][n+1];
dp[0][0] = true;
for(int i = 0; i <= m; i++){
for(int j = 1; j <= n; j++){
if(p.charAt(j-1) == '*'){
dp[i][j] = dp[i][j-2];
if(matches(s,p,i,j-1)){
dp[i][j] = dp[i][j] || dp[i-1][j];
}
}
else {
if(matches(s,p,i,j)){
dp[i][j] = dp[i-1][j-1];
}
}
}
}
return dp[m][n];
}
public boolean matches(String s, String p, int i, int j){
if(i == 0)
return false;
if(p.charAt(j-1) == '.')
return true;
return s.charAt(i-1) == p.charAt(j-1);
}
public class telnum {
char[][] m = new char[][]{
{},
{},
{'a','b','c'},{'d','e','f'},
{'g','h','i'},{'j','k','l'},{'m','n','o'},
{'p','q','r','s'},{'t','u','v'},{'w','x','y','z'}
};
public List<String> letterCombinations(String str){
List<String> res = new ArrayList<>();
if(str.length() == 0)
return res;
dfs(str, 0, new StringBuilder(), res);
return res;
}
//dfs的递归跑法
//在进入一个dfs之后,如果不满足条件1,则进入条件2,先add,然后继续dfs
//如果满足条件1,return之后返回上一个dfs,执行delete
//如果某一m[]遍历结束,跳回上一层m[]继续dfs
void dfs(String str, int index, StringBuilder sb, List<String> res){
//1.截止条件
if(index == str.length()){
res.add(sb.toString());
return;
}
//2.候选节点
for(char c : m[str.charAt(index) - '0']){
sb.append(c);
dfs(str, index+1, sb, res);
sb.deleteCharAt(sb.length() - 1);
}
}
}
用String
public class GenerateParenthesis {
public List<String> generateParenthesis(int n){
char[] p = new char[]{'(',')'};
int[] pb = new int[]{n,n};
List<String> res = new ArrayList<>();
dfs(n, p, pb, "", res);
return res;
}
void dfs(int n, char[] p, int[] pb, String str, List<String> res){
//截止条件
if(str.length() == 2 * n){
res.add(str);
return;
}
//候选节点
if(pb[0] > 0){
pb[0]--;
dfs(n, p, pb, str + p[0], res);
pb[0]++;
}
if(pb[1] > 0 && pb[0] != pb[1]){
pb[1]--;
dfs(n, p, pb, str + p[1], res);
pb[1]++;
}
}
}
用StringBuilder
public List<String> generateParenthesis(int n){
List<String> res = new ArrayList<>();
char[] p = new char[]{'(',')'};
int[] pb = new int[]{n,n};
dfs(n,p,pb,res,new StringBuilder());
return res;
}
public void dfs(int n, char[] p, int[] pb, List<String> res, StringBuilder sb){
if(sb.length() == n*2){
res.add(sb.toString());
return;
}
if(pb[0] > 0){
pb[0]--;
dfs(n,p,pb,res,sb.append(p[0]));
pb[0]++;
sb.deleteCharAt(sb.length()-1);
}
if(pb[1] > 0 && pb[0] != pb[1]){
pb[1]--;
dfs(n,p,pb,res,sb.append(p[1]));
pb[1]++;
sb.deleteCharAt(sb.length()-1);
}
}
public class Combin {
public List<List<Integer>> combinationSum(int[] p, int t){
List<List<Integer>> res = new ArrayList<>();
dfs(p, t, new ArrayList<>(), res);
return res;
}
void dfs(int[] p, int t, List<Integer> chain, List<List<Integer>> res){
//截止条件
int s = sum(chain);
if(s >= t){
if(s == t){
List<Integer> tmp = new ArrayList<>(chain);
Collections.sort(tmp);
if(!res.contains(tmp)){
res.add(tmp);
}
}
return;
}
//候选节点
for(int i = 0; i < p.length; i++){
int c = p[i];
chain.add(c);
dfs(p, t, chain, res);
chain.remove(chain.size() - 1);
}
}
int sum(List<Integer> chain){
int res = 0;
for(int i : chain){
res += i;
}
return res;
}
}
public class Permute {
public List<List<Integer>> permute(int[] p){
List<List<Integer>> res = new ArrayList<>();
boolean[] pb = new boolean[p.length];
dfs(p, pb, new ArrayList<>(), res);
return res;
}
void dfs(int[] p, boolean[] pb, List<Integer> chain, List<List<Integer>> res){
//截止条件
if(chain.size() == p.length){
res.add(new ArrayList<>(chain));
return;
}
//候选节点
for(int i = 0; i < p.length; i++){
int c = p[i];
//筛选
if(!pb[i]){
chain.add(c);
pb[i] = true;
dfs(p, pb, chain, res);
chain.remove(chain.size() - 1);
pb[i] = false;
}
}
}
}
public class PermuteUnique {
public List<List<Integer>> permuteUnique(int[] px){
HashMap<Integer,Integer> m = new HashMap<>();
for(int i : px){
m.put(i, m.containsKey(i) ? m.get(i) + 1 : 1);
}
int len = m.size();
int[] p = new int[len];
int[] pb = new int[len];
int[] index = new int[1];
m.forEach((k,v) ->{
p[index[0]] = k;
pb[index[0]] = v;
index[0]++;
});
List<List<Integer>> res = new ArrayList<>();
dfs(px.length, p, pb, new ArrayList<Integer>(), res);
return res;
}
void dfs(int size, int[] p, int[] pb, List<Integer> chain, List<List<Integer>> res){
//截止条件
if(chain.size() == size){
res.add(new ArrayList<>(chain));
return;
}
//候选节点
for(int i = 0; i < p.length; i++){
int c = p[i];
//筛选
if(pb[i] > 0){
chain.add(c);
pb[i]--;
dfs(size, p, pb, chain, res);
pb[i]++;
chain.remove(chain.size() - 1);
}
}
}
}
全排列方法改改也可以用
public List<List<Integer>> permuteUnique(int[] p){
List<List<Integer>> res = new ArrayList<>();
boolean[] pb = new boolean[p.length];
dfs(p, pb, new ArrayList<>(), res);
return res;
}
void dfs(int[] p, boolean[] pb, List<Integer> chain, List<List<Integer>> res){
//截止条件
if(chain.size() == p.length){
List<Integer> tmp = new ArrayList<>(chain);
if(!res.contains(tmp)){
res.add(tmp);
}
return;
}
//候选节点
for(int i = 0; i < p.length; i++){
int c = p[i];
//筛选
if(!pb[i]){
chain.add(c);
pb[i] = true;
dfs(p, pb, chain, res);
chain.remove(chain.size() - 1);
pb[i] = false;
}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i5n8i51Z-1596520187453)(/Users/cwq/Documents/全栈工程师的笔记/算法图片/动态规划.png)]
https://leetcode-cn.com/circle/article/lxC3ZB/
https://mp.weixin.qq.com/s/Hew44D8rdXb3pf8mZGk67w
https://labuladong.gitbook.io/algo/di-ling-zhang-bi-du-xi-lie/dong-tai-gui-hua-xiang-jie-jin-jie
中心扩展法
public String longestPalindrome1(String s) {
if (s == null || s.length() == 0) {
return "";
}
int strLen = s.length();
int left = 0;
int right = 0;
int len = 1;
int maxStart = 0;
int maxLen = 0;
for (int i = 0; i < strLen; i++) {
left = i - 1;
right = i + 1;
//如果左边与当前i相同,接着向左边找
while (left >= 0 && s.charAt(left) == s.charAt(i)) {
len++;
left--;
}
//同理
while (right < strLen && s.charAt(right) == s.charAt(i)) {
len++;
right++;
}
//如果i的左右边相同,两边一起找
while (left >= 0 && right < strLen && s.charAt(right) == s.charAt(left)) {
len = len + 2;
left--;
right++;
}
if (len > maxLen) {
maxLen = len;
maxStart = left;
}
//每次找完len归1,最大值记录在maxLen
len = 1;
}
return s.substring(maxStart + 1, maxStart + maxLen + 1);
}
动态规划
状态方程:p(i,j) = p(i+1,j-1) && (Si == Sj)
public String longestPalindrome2(String s) {
if (s == null || s.length() < 2) {
return s;
}
int strLen = s.length();
int maxStart = 0; //最长回文串的起点
int maxEnd = 0; //最长回文串的终点
int maxLen = 1; //最长回文串的长度
boolean[][] dp = new boolean[strLen][strLen];
for (int r = 1; r < strLen; r++) {
for (int l = 0; l < r; l++) {
//这里r-l <= 2 的意思是,如果长度小于2的话,说明其中不存在第二个子串了
//所以dp[l + 1][r - 1])不能用,这是为了给最小的子串赋值
if (s.charAt(l) == s.charAt(r) && (r - l <= 2 || dp[l + 1][r - 1])) {
dp[l][r] = true;
if (r - l + 1 > maxLen) {
maxLen = r - l + 1;
maxStart = l;
maxEnd = r;
}
}
}
}
return s.substring(maxStart, maxEnd + 1);
}
public int maxSubArray(int[] nums) {
//ans是最大值,sum是遍历时计算值
int ans = nums[0];
int sum = 0;
for(int num : nums){
//如果上一次的sum是正数,那么这次必须得计算
//因为下一次有可能得到一个更大的值,之前的最大值保存在ans里了,不用担心
//如果是负数,那么反正都是负数了,加一个正数或负数,不如直接等于它
if(sum > 0){
sum += num;
}else {
sum = num;
}
ans = Math.max(ans,sum);
}
return ans;
}
排列组合
math.factorial(m+n-2) / (math.factorial(m-1) · math.factorial(n-1))
动态规划
状态方程:dp[i][j] = dp[i-1][j] + dp[i][j-1]
//时间复杂度O(m*n)
//空间复杂度O(m*n)
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for(int i = 0; i < n; i++) dp[0][i] = 1;
for(int i = 0; i < m; i++) dp[i][0] = 1;
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
//优化为一维数组,空间复杂度为o(n)
public int uniquePaths(int m, int n) {
int[] dp = new int[n];
Arrays.fill(dp,1);
//滚动数组,相当于将数组从上而下滚动,一维变成二维
//所以dp[j] = dp[j] + dp[j-1]
//这里dp[j]是之前保存的上一行,相当于dp[i-1][j]
//同理,dp[j-1]相当于dp[i][j-1]
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
dp[j] = dp[j-1] + dp[j];
}
}
return dp[n-1];
}
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length, n = obstacleGrid[0].length;
int[] f = new int[n];
f[0] = obstacleGrid[0][0] == 0 ? 1 : 0;
//从第二列开始计算所以有j-1>=0,但是循环得从j=0开始,防止只有一列
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(obstacleGrid[i][j] == 1){
f[j] = 0;
continue;
}
if(j - 1 >= 0 && obstacleGrid[i][j-1] == 0){
f[j] += f[j-1];
}
}
}
return f[n-1];
}
public int minPathSum(int[][] grid) {
int m = grid.length, n = grid[0].length;
int[][] dp = new int[m][n];
int min = 0;
dp[0][0] = grid[0][0];
for(int i = 1; i < m; i++) dp[i][0] = dp[i-1][0] + grid[i][0];
for(int j = 1; j < n; j++) dp[0][j] = dp[0][j-1] + grid[0][j];
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
int one = dp[i-1][j] + grid[i][j];
int two = dp[i][j-1] + grid[i][j];
int three = Math.min(one,two);
dp[i][j] = three;
}
}
return dp[m-1][n-1];
}
public int climbStairs(int n) {
int one = 1, two = 2, sum = 0;
if(n == 1) return 1;
if(n == 2) return 2;
for(int i = 3; i <= n; i++){
sum = one + two;
one = two;
two = sum;
}
return sum;
}
public class NumDecodings {
public int numDecodings(String s){
int n = s.length();
int[] dp = new int[n];
if(s.charAt(0) == '0')
return 0;
dp[0] = 1;
for(int i = 1; i < n; i++){
//如果第i-1个字符与第i个字符匹配在1-26之外
//那么此字符只能作为单个字符出现,所以dp[i]与dp[i-1]一样
if(s.charAt(i) != '0')
dp[i] += dp[i-1];
//如果第i-1个字符与第i个字符匹配在1-26之内
//若i<2,说明此时字符串最多到第二个,那么dp[i]只能加1,
//若i>2,那么相当于在原有方法dp[i-2]的数量上上加两倍
//尾数接两个单字符一种,接两个字符一种
if(s.charAt(i-1) == '1' || (s.charAt(i-1) == '2' && s.charAt(i) <= '6')){
if(i - 2 > 0)
dp[i] += dp[i-2];
else
dp[i]++;
}
}
return dp[n-1];
}
}
G(n): 长度为n的序列能构成的不同二叉搜索树的个数。
F(i, n): 以i为根、序列长度为n的不同二叉搜索树个数(1≤i≤n)。
public int numTrees(int n) {
int[] G = new int[n+1];
G[0] = 1;
G[1] = 1;
for(int i = 2; i <= n; i++){
for(int j = 1; j <= i; j++){
G[i] += G[j-1] * G[i-j];
}
}
return G[n];
}
public List<TreeNode> generateTrees(int n) {
if (n == 0) {
return new LinkedList<TreeNode>();
}
return generateTrees(1, n);
}
public List<TreeNode> generateTrees(int start, int end) {
List<TreeNode> allTrees = new LinkedList<TreeNode>();
if (start > end) {
allTrees.add(null);
return allTrees;
}
// 枚举可行根节点
for (int i = start; i <= end; i++) {
// 获得所有可行的左子树集合
List<TreeNode> leftTrees = generateTrees(start, i - 1);
// 获得所有可行的右子树集合
List<TreeNode> rightTrees = generateTrees(i + 1, end);
// 从左子树集合中选出一棵左子树,从右子树集合中选出一棵右子树,拼接到根节点上
for (TreeNode left : leftTrees) {
for (TreeNode right : rightTrees) {
TreeNode currTree = new TreeNode(i);
currTree.left = left;
currTree.right = right;
allTrees.add(currTree);
}
}
}
return allTrees;
}
public int minimumTotal(List<List<Integer>> triangle) {
int n = triangle.size();
// dp[i][j] 表示从点 (i, j) 到底边的最小路径和。
int[][] dp = new int[n + 1][n + 1];
// 从三角形的最后一行开始递推。
for (int i = n - 1; i >= 0; i--) {
for (int j = 0; j <= i; j++) {
dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle.get(i).get(j);
}
}
return dp[0][0];
}
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> wordDictSet = new HashSet(wordDict);
boolean[] dp = new boolean[s.length()+1];
dp[0] = true;
//每次比较的截止点就是i,比较从0到i是否能匹配到字典
for(int i = 1; i <= s.length(); i++){
for(int j = 0; j < i; j++){
if(dp[j] && wordDictSet.contains(s.substring(j,i))){
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
由于存在负数,那么会导致最大的变最小的,最小的变最大的。因此还需要维护当前最小值imin,当负数出现时则imax与imin进行交换再进行下一步计算。碰到两次负数时,imax存的乘负数就是最大的。
public int maxProduct(int[] nums) {
int max = Integer.MIN_VALUE, imax = 1, imin = 1;
for(int i = 0; i < nums.length; i++){
if(nums[i] < 0){
int tmp = imax;
imax = imin;
imin = tmp;
}
imax = Math.max(imax * nums[i], nums[i]);
imin = Math.min(imin * nums[i], nums[i]);
max = Math.max(imax,max);
}
return max;
}
public int numSquares(int n){
int[] dp = new int[n+1];
dp[0] = 0;
for(int i = 1; i <= n; i++){
dp[i] = i;
for(int j = 1; i - j * j >= 0; j++){
//dp[i-j*j]+1:其实是i减去这个完全平方数j*j
//所需要的总数就是dp[减后的数] + 1(这个完全平方数占一个数)
dp[i] = Math.min(dp[i],dp[i-j*j]+1);
}
}
return dp[n];
}
//数学推导,取3越多越大
public int integerBreak(int n){
if(n <= 3) return n-1;
int a = n / 3;
int b = n % 3;
if(b == 0) return (int) Math.pow(3,a);
if(b == 1) return (int) Math.pow(3,a-1)*4;
return (int) Math.pow(3,a)*2;
}
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/solution/yi-ge-tong-yong-fang-fa-tuan-mie-6-dao-gu-piao-wen/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D879vAqi-1596520187455)(/Users/cwq/Documents/全栈工程师的笔记/算法图片/5.png)]
三种状态:rest(保持)、sell、buy
i是第几天的股票,k是可以买卖几次,0和1是状态(是否持有股票)
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,所以今天我就持有股票了。
状态方程
dp[-1][k][0] = 0
解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0 。
dp[-1][k][1] = -infinity
解释:还没开始的时候,是不可能持有股票的,用负无穷表示这种不可能。
base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -infinity
状态转移方程:
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])
public int maxProfit(int[] prices) {
if(prices.length == 0) return 0;
int len = prices.length;
int[][] dp = new int[len][2];
for(int i = 0; i < len; i++){
if(i-1 == -1){
//相当于第一天没有买股票,钱为0
dp[i][0] = 0;
//买了股票,钱为-prices[i]
dp[i][1] = -prices[i];
continue;
}
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 dp[len-1][0];
}
简化
public int maxProfit(int[] prices) {
if(prices.length == 0) return 0;
int n = prices.length;
int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
// dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
// dp[i][1] = max(dp[i-1][1], -prices[i])
//因为只能买卖一次,所以每次买进股票都是-prices[i]
dp_i_1 = Math.max(dp_i_1, -prices[i]);
}
return dp_i_0;
}
状态方程
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])
public int maxProfit(int[] prices) {
if(prices.length == 0) return 0;
int len = prices.length;
int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
for(int i = 0; i < len; i++){
int tmp = dp_i_0;
dp_i_0 = Math.max(dp_i_0,dp_i_1+prices[i]);
//与上题不同,可以买卖无限次,需要记录持续的买进状态
dp_i_1 = Math.max(dp_i_1,tmp-prices[i]);
}
return dp_i_0;
}
k = 2 和前面题目的情况稍微不同,因为上面的情况都和 k 的关系不太大。要么 k 是正无穷,状态转移和 k 没关系了;要么 k = 1,跟 k = 0 这个 base case 挨得近,最后也没有存在感。
错误示范
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];
其实我们之前的解法,都在穷举所有状态,只是之前的题目中 k 都被化简掉了。比如说第一题,k = 1;所以k = 2时:
public int maxProfit(int[] prices) {
if(prices.length == 0) return 0;
int max_k = 2;
int[][][] dp = new int[prices.length][max_k+1][2];
for(int i = 0; i < prices.length; i++){
for(int k = max_k; k >= 1; k--){
if(i-1 == -1){
dp[i][k][0] = 0;
dp[i][k][1] = -prices[i];
continue;
}
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[prices.length-1][max_k][0];
}
//dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + prices[i])
//dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - prices[i])
//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], -prices[i])
public int maxProfit(int[] prices) {
int dp_i10 = 0, dp_i11 = Integer.MIN_VALUE;
int dp_i20 = 0, dp_i21 = Integer.MIN_VALUE;
for (int price : prices) {
dp_i20 = Math.max(dp_i20, dp_i21 + price);
dp_i21 = Math.max(dp_i21, dp_i10 - price);
dp_i10 = Math.max(dp_i10, dp_i11 + price);
dp_i11 = Math.max(dp_i11, -price);
}
return dp_i20;
}
一次交易由买入和卖出构成,至少需要两天。所以说有效的限制 k 应该不超过 n/2,如果超过,就没有约束作用了,相当于 k = +infinity。这种情况是买卖股票的最佳时机II。
public int maxProfit(int max_k, int[] prices) {
int n = prices.length;
if (max_k > n / 2)
return maxProfit2(prices);
int[][][] dp = new int[n][max_k + 1][2];
for (int i = 0; i < n; i++)
for (int k = max_k; k >= 1; k--) {
if (i - 1 == -1) {
dp[i][k][0] = 0;
dp[i][k][1] = -prices[i];
continue;
}
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][max_k][0];
}
public int maxProfit2(int[] prices) {
int len = prices.length;
int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
for(int i = 0; i < len; i++){
int tmp = dp_i_0;
dp_i_0 = Math.max(dp_i_0,dp_i_1+prices[i]);
dp_i_1 = Math.max(dp_i_1,tmp-prices[i]);
}
return dp_i_0;
}
每次 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 。
public int maxProfit(int[] prices) {
if(prices.length == 0) return 0;
int len = prices.length;
int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
int dp_pre_0 = 0;
for(int i = 0; i < len; i++){
int tmp = dp_i_0;
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
dp_i_1 = Math.max(dp_i_1, dp_pre_0 - prices[i]);
dp_pre_0 = tmp;
}
return dp_i_0;
}
每次交易要支付手续费,只要把手续费从利润中减去即可。改写方程:
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)
解释:相当于买入股票的价格升高了。
在第一个式子里减也是一样的,相当于卖出股票的价格减小了。
public int maxProfit(int[] prices, int fee) {
if(prices.length == 0) return 0;
int len = prices.length;
int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
for(int i = 0; i < len; i++){
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
dp_i_1 = Math.max(dp_i_1, dp_i_0 - prices[i] - fee);
}
return dp_i_0;
}
public int rob(int[] nums) {
if(nums == null || nums.length == 0){
return 0;
}
if(nums.length == 1) return nums[0];
int[] dp = new int[nums.length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
//每次是否要rob看i-1和i-2
for(int i = 2; i < nums.length; i++){
dp[i] = Math.max(dp[i-2]+nums[i], dp[i-1]);
}
return dp[nums.length-1];
}
public int rob1(int[] nums){
if(nums.length == 0) return 0;
if(nums.length == 1) return nums[0];
return Math.max(myrob(Arrays.copyOfRange(nums,0,nums.length-1)),
myrob(Arrays.copyOfRange(nums,1,nums.length)));
}
public int myrob(int[] nums){
//滚动数组,省了dp
int pre = 0, cur = 0, tmp;
for(int num : nums){
tmp = cur;
cur = Math.max(pre+num,tmp);
pre = tmp;
}
return cur;
}
//暴力递归,要么要爷爷和孙子,要么要儿子
public int rob(TreeNode root){
if(root == null) return 0;
int money = root.val;
if(root.left != null)
money += rob(root.left.left) + rob(root.left.right);
if(root.right != null)
money += rob(root.right.left) + rob(root.right.right);
return Math.max(money,rob(root.left) + rob(root.right));
}
//当前节点选择偷时,那么两个孩子节点就不能选择偷了
//当前节点选择不偷时,两个孩子节点只需要拿最多的钱出来就行(两个孩子节点偷不偷没关系)
//我们使用一个大小为 2 的数组来表示 int[] res = new int[2]
//0 代表不偷,1 代表偷
//任何一个节点能偷到的最大钱的状态可以定义为
//当前节点选择不偷:当前节点能偷到的最大钱数 = 左孩子能偷到的钱 + 右孩子能偷到的钱
//当前节点选择偷:当前节点能偷到的最大钱数 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数
public int rob(TreeNode root) {
int[] result = robInternal(root);
return Math.max(result[0], result[1]);
}
public int[] robInternal(TreeNode root) {
if (root == null) return new int[2];
int[] result = new int[2];
//存的相当于result
int[] left = robInternal(root.left);
int[] right = robInternal(root.right);
//偷与不偷的两种形式
result[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
result[1] = left[0] + right[0] + root.val;
return result;
}
public int lengthOfLIS(int[] nums) {
if(nums.length == 0) return 0;
int[] dp = new int[nums.length];
dp[0] = 1;
int maxans = 1;
// maxval记录本次循环最大值,dp[j]记录j这个点之前有几个数比他小
for(int i = 1; i < nums.length; i++){
int maxval = 0;
for(int j = 0; j < i; j++){
if(nums[i] > nums[j]){
maxval = Math.max(dp[j],maxval);
}
}
dp[i] = maxval + 1;
maxans = Math.max(dp[i],maxans);
}
return maxans;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zS6AW2ju-1596520187456)(/Users/cwq/Documents/全栈工程师的笔记/算法图片/2.png)]
//dp[][],行是nums的len,列是nums和sum的一半target
//只要找到某些数字的和等于target即结束
public boolean canPartition(int[] nums) {
int len = nums.length;
if(len == 0) return false;
int sum = 0;
for(int num : nums){
sum += num;
}
//和为奇数判错
if((sum & 1) == 1) return false;
int target = sum / 2;
boolean[][] dp = new boolean[len][target+1];
if(nums[0] <= target){
dp[0][nums[0]] = true;
}
for(int i = 1; i < len; i++){
for(int j = 0; j < target+1; j++){
dp[i][j] = dp[i-1][j];
if(nums[i] == j){
dp[i][j] = true;
continue;
}
if(nums[i] < j){
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];
}
}
}
return dp[len-1][target];
}
//简单优化
public boolean canPartition(int[] nums) {
int len = nums.length;
if(len == 0) return false;
int sum = 0;
for(int num : nums){
sum += num;
}
if((sum & 1) == 1) return false;
int target = sum / 2;
boolean[][] dp = new boolean[len][target+1];
//有这句再通过for循环的dp[i][j] = dp[i-1][j]其实已经把第一列设置为true
dp[0][0] = true;
if(nums[0] < target) dp[0][nums[0]] = true;
if(nums[0] == target) return true;
for(int i = 1; i < len; i++){
for(int j = 0; j < target+1; j++){
dp[i][j] = dp[i-1][j];
if(nums[i] <= j){
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];
}
}
//如果最后一列有值为true,说明已经找到
if(dp[i][target] == true){
return true;
}
}
return dp[len-1][target];
}
在“填表格”的时候,当前行只参考了上一行的值,因此状态数组可以只设置 2 行,使用“滚动数组”的技巧“填表格”即可;
实际上连“滚动数组”都不必,在“填表格”的时候,当前行总是参考了它上面一行 “头顶上” 那个位置和“左上角”某个位置的值。因此,我们可以只开一个一维数组,从后向前依次填表即可。
“从后向前” 写的过程中,一旦 nums[i] <= j 不满足,可以马上退出当前循环,因为后面的 j 的值肯定越来越小,没有必要继续做判断,直接进入外层循环的下一层。相当于也是一个剪枝,这一点是“从前向后”填表所不具备的。
//状态数组压缩到一维
public boolean canPartition(int[] nums) {
int len = nums.length;
if (len == 0) {
return false;
}
int sum = 0;
for (int num : nums) {
sum += num;
}
if ((sum & 1) == 1) {
return false;
}
int target = sum / 2;
boolean[] dp = new boolean[target + 1];
dp[0] = true;
if(nums[0] < target) dp[nums[0]] = true;
if(nums[0] == target) return true;
for (int i = 1; i < len; i++) {
for (int j = target; nums[i] <= j; j--) {
if (dp[target]) {
return true;
}
dp[j] = dp[j] || dp[j - nums[i]];
}
}
return dp[target];
}
状态转移方程
不论从后向前还是从前向后,每次选择字符串找dp数组时,都是会查找dp[i−1][j−当前字符串使用0的个数][k−当前字符串使用1的个数],然后加1,再比较能不能要
其实每个dp[i][j][k]位置相当于使用j和k的个数的0、1所能创建字符串的最大值,每次遍历strs其实就是查找选择这个字符串能不能提高这个最大值
public int[] countZeroAndOne(String str){
int[] cnt = new int[2];
for(char c : str.toCharArray()){
cnt[c - '0']++;
}
return cnt;
}
public int findMaxForm(String[] strs, int m, int n) {
int len = strs.length;
if(strs == null || len == 0) return 0;
int[][][] dp = new int[len+1][m+1][n+1];
for(int i = 1; i < len+1; i++){
int[] cnt = new int[2];
cnt = countZeroAndOne(strs[i-1]);
int zero = cnt[0];
int one = cnt[1];
for(int j = 0; j <= m; j++){
for(int k = 0; k <= n; k++){
//先把上一行抄下来
dp[i][j][k] = dp[i-1][j][k];
//如果给的m和n够这个字符串使用,那么计算一下减去这次使用的个数i和j
//再将上一行的这个位置的个数加一,是否比不加上这个字符串多
if(j >= zero && k >= one){
dp[i][j][k] = Math.max(dp[i-1][j][k], dp[i-1][j-zero][k-one]+1);
}
}
}
}
return dp[len][m][n];
}
优化一下空间,并从后向前赋值
public int[] countZeroAndOne(String str){
int[] cnt = new int[2];
for(char c : str.toCharArray()){
cnt[c - '0']++;
}
return cnt;
}
public int findMaxForm(String[] strs, int m, int n) {
int[][] dp = new int[m + 1][n + 1];
dp[0][0] = 0;
for (String s : strs) {
int[] zeroAndOne = countZeroAndOne(s);
int zeros = zeroAndOne[0];
int ones = zeroAndOne[1];
for (int i = m; i >= zeros; i--) {
for (int j = n; j >= ones; j--) {
dp[i][j] = Math.max(dp[i][j], dp[i - zeros][j - ones] + 1);
}
}
}
return dp[m][n];
}
//递归
int count = 0;
public int findTargetSumWays(int[] nums, int S) {
calculate(nums,0,0,S);
return count;
}
public void calculate(int[] nums, int i, int sum, int S){
if(nums.length == i){
if(sum == S) count++;
}else {
calculate(nums,i+1,sum+nums[i],S);
calculate(nums,i+1,sum-nums[i],S);
}
}
//动态规划
public int findTargetSumWays(int[] nums, int S) {
int[][] dp = new int[nums.length][2001];
dp[0][nums[0]+1000] = 1;
dp[0][-nums[0]+1000] += 1;
for(int i = 1; i < nums.length; i++){
for(int sum = -1000; sum <= 1000; sum++){
if(dp[i-1][sum+1000] > 0){
dp[i][sum+nums[i]+1000] += dp[i-1][sum+1000];
dp[i][sum-nums[i]+1000] += dp[i-1][sum+1000];
}
}
}
return S > 1000 ? 0 : dp[nums.length-1][S+1000];
}
//空间优化
public int findTargetSumWays(int[] nums, int S) {
int[] dp = new int[2001];
dp[nums[0] + 1000] = 1;
dp[-nums[0] + 1000] += 1;
for (int i = 1; i < nums.length; i++) {
int[] next = new int[2001];
for (int sum = -1000; sum <= 1000; sum++) {
if (dp[sum + 1000] > 0) {
next[sum + nums[i] + 1000] += dp[sum + 1000];
next[sum - nums[i] + 1000] += dp[sum + 1000];
}
}
dp = next;
}
return S > 1000 ? 0 : dp[S + 1000];
}
https://leetcode-cn.com/problems/coin-change/solution/322-ling-qian-dui-huan-by-leetcode-solution/
public int coinChange(int[] coins, int amount) {
int max = amount + 1;
int[] dp = new int[max];
//如果只有一块钱零钱,那么最大值就是amount,给dp全赋值amount+1
//那么如果找到比其小的就可填入,dp[0]得赋值0
Arrays.fill(dp,max);
dp[0] = 0;
for(int i = 1; i <= amount; i++){
for(int j = 0; j < coins.length; j++){
if(coins[j] <= i){
dp[i] = Math.min(dp[i],dp[i-coins[j]]+1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lPO0Sen1-1596520187457)(/Users/cwq/Documents/全栈工程师的笔记/算法图片/3.jpg)]
public int change(int amount, int[] coins) {
int[] dp = new int[amount+1];
dp[0] = 1;
for(int coin : coins){
for(int x = coin; x <= amount; x++){
dp[x] += dp[x-coin];
}
}
return dp[amount];
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3dVw7W8N-1596520187458)(/Users/cwq/Documents/全栈工程师的笔记/算法图片/1.png)]
//贼慢
public int longestCommonSubsequence(String text1, String text2) {
int[][] dp = new int[text1.length()+1][text2.length()+1];
for(int i = 1; i <= text1.length(); i++){
for(int j = 1; j <= text2.length(); j++){
if(text1.charAt(i-1) == text2.charAt(j-1)){
dp[i][j] = 1 + dp[i-1][j-1];
}else {
dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
}
}
}
return dp[text1.length()][text2.length()];
}
public class MedianFinder {
private Queue<Integer> A, B;
public MedianFinder() {
// 初始都是小顶堆,保存较大的一半
A = new PriorityQueue<>();
// 这里用了lambda表达式,调用了内部用比较器,将其变成大顶堆
// 保存较小的一半
B = new PriorityQueue<>((x, y) -> (y - x));
}
public void addNum(int num) {
if(A.size() != B.size()) {
//当两个堆元素不等时,需要往小根堆A中加入元素,然后poll出堆顶元素(最小)
//再加入大根堆B中(保证B中保存的一直是较小的一半)
A.add(num);
B.add(A.poll());
} else {
//当两个堆元素相等时,需要往大根堆B中加入元素,然后poll出堆顶元素(最大)
//再加入小根堆A中(保证A中保存的一直是较大的一半)
B.add(num);
A.add(B.poll());
}
}
public double findMedian() {
return A.size() != B.size() ? A.peek() : (A.peek() + B.peek()) / 2.0;
}
}
注意,是要找数组中第几大的数,升序排序完最后一个数是第一大
public int findKthLargest(int[] nums, int k) {
//利用优先队列特性直接搞定
Queue<Integer> queue = new PriorityQueue<Integer>();
for(int num : nums){
queue.add(num);
if(queue.size() > k) queue.poll();
}
return queue.peek();
}
class MyCalendar {
private TreeMap<Integer, Integer> calendar;
public MyCalendar() {
calendar = new TreeMap<>();
}
public boolean book(int start, int end) {
Integer prev = calendar.floorKey(start);
Integer next = calendar.ceilingKey(start);
//因为prev取得是map中key小于等于start的值
//next取得是map中key大于等于start的值
//而如果我们要插入一个正确的start和end,必须不能和它重合
//1. start比map中key都小,所以prev = null,next = map中第一个比start大或等于的key
//这时start小于map中所有key,要保证end小于map中第一个比start大或等于的key
//2. prev = map中第一个比start小或等于的key, next = map中第一个比start大或等于的key
//这时要保证start >= prev的value,end <= next;就是保证start和end在上下界的安全区间内
//3. prev = map中第一个比start小或等于的key, next = null,跟1同理
//4. prev = null , next = null 存在的时候说明map为空,直接插入就行
if((prev == null || calendar.get(prev) <= start) &&
(next == null || end <= next)){
calendar.put(start, end);
return true;
}
return false;
}
}
这次treemap中存的是,每个时间time和其对应的使用次数,如果是start就是从1开始++,如果是end就从-1开始–。用一个计数active来判断是否有三重预定,正常情况下一个start和end相加为0。
如果某次达到了三重预定,那么将其对应的start和end的值分别–和++,如果必要,可remove。
public class MyCalendarTwo {
private TreeMap<Integer, Integer> delta;
public MyCalendarTwo() {
delta = new TreeMap<>();
}
public boolean book(int start, int end) {
delta.put(start, delta.getOrDefault(start, 0) + 1);
delta.put(end, delta.getOrDefault(end, 0) - 1);
int active = 0;
for(int value : delta.values()){
active += value;
if(active >= 3){
delta.put(start, delta.getOrDefault(start, 0) - 1);
delta.put(end, delta.getOrDefault(end, 0) + 1);
if(delta.get(start) == 0)
delta.remove(start);
return false;
}
}
return true;
}
}
与上题差不多,记录一下最大的active就行
public class MyCalendarThree {
private TreeMap<Integer, Integer> delta;
public MyCalendarThree() {
delta = new TreeMap<>();
}
public int book(int start, int end) {
delta.put(start, delta.getOrDefault(start, 0) + 1);
delta.put(end, delta.getOrDefault(end, 0) - 1);
int active = 0, ans = 0;
for(int value : delta.values()){
active += value;
if(active > ans) ans = active;
}
return ans;
}
}
先将所有数按照,key:大小;value:数量,的顺序存入treemap,然后遍历map,每次找到一个最小的card,按照card到card+W-1的大小查询map,如果不符合,说明不成立。
public boolean isNStraightHand(int[] hand, int W) {
int len = hand.length;
if( len % W != 0) return false;
TreeMap<Integer, Integer> count = new TreeMap<>();
for(int card : hand){
if(!count.containsKey(card))
count.put(card,1);
else
count.put(card,count.get(card)+1);
}
while (count.size() > 0){
int first = count.firstKey();
for(int card = first; card < first + W; card++){
if(!count.containsKey(card)) return false;
int n = count.get(card);
if(n == 1)
count.remove(card);
else
count.put(card,n-1);
}
}
return true;
}
当我们要调用 leave§ 函数时,我们只需要把有序集合中的 p 移除即可。当我们要调用 seat() 函数时,我们遍历这个有序集合,对于相邻的两个座位 i 和 j,如果选择在这两个座位之间入座,那么最近的距离 d 为 (j - i) / 2,选择的座位为 i + d。除此之外,我们还需要考虑坐在最左侧 0 和最右侧 N - 1 的情况。
public class ExamRoom {
private TreeSet<Integer> students;
private int N;
public ExamRoom(int N) {
this.N = N;
students = new TreeSet<>();
}
public int seat() {
int student = 0;
if(students.size() > 0){
int dist = students.first();
Integer prev = null;
for(int s : students){
if(prev != null){
int d = (s - prev) / 2;
if(d > dist){
dist = d;
student = prev + d;
}
}
prev = s;
}
if(N - 1 - students.last() > dist){
student = N - 1;
}
}
students.add(student);
return student;
}
public void leave(int p) {
students.remove(p);
}
}