文章思路来源公众号:labuladong
借助labuladong整理自己DP算法
动态规划一般是求最优问题
- 先确定「状态」,也就是原问题和子问题中变化的变量
- 状态转移方程,你把 f(n) 想做一个状态 n,这个状态 n 是由状态 n - 1 和状态 n - 2 相加转移而来,这就叫状态转移
- 然后确定「选择」并择优,也就是对于每个状态,可以做出什么选择改变当前状态
1、0-1 背包问题
2、#416. 分割等和子集
0-1背包问题的变体
class Solution {
/**
* 416. 分割等和子集 :给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
* 每个数组都是sum/2
* 类似于背包问题:给一个可装载重量为sum/2的背包和N个物品,每个物品的重量为nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满?
* 状态:dp[i][j] i:当前重量,j:当前容量
* 选择:如果当前i选择不装 dp[i][j] = dp[i-1][j]
* 装 ; dp[i][j] = dp[i-1][j-nums[i]]
* @param nums
* @return
*/
public boolean canPartition(int[] nums) {
if (nums.length<=0) return false;
int sum=0;
for (int i : nums) sum+=i;//对数组元素求求和
if (sum%2==1) return false;//如果和是奇数,不可以
sum/=2;
int n = nums.length;//共有n个数1.2.3...n
boolean[][] dp = new boolean[n+1][sum+1];
// dp[0][...] = false;dp[..][0]=true;
for (int i = 0;i<=n;i++){
dp[i][0] = true;
}
//dp[0][...] = false不需要初始化
for (int i =1;i<=n;i++){
for (int j=1;j<=sum;j++){
if (j-nums[i-1]<0){//表示当前背包容量装不下当前重量;
dp[i][j] = dp[i-1][j];
}else {
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]];
}
}
}
return dp[n][sum];
}
}
3、#518.零钱兑换 II
完全背包问题
若只使用coins中的前i个硬币的面值,若想凑出金额j,有dp[i][j]种凑法。
如果你不把这第i个物品装入背包,也就是说你不使用coins[i]这个面值的硬币,那么凑出面额j的方法数dp[i][j]应该等于dp[i-1][j],继承之前的结果。
如果你把这第i个物品装入了背包,也就是说你使用coins[i]这个面值的硬币,那么dp[i][j]应该等于dp[i][j-coins[i-1]]。
class Solution {
public int change(int amount, int[] coins) {
int n = coins.length;
int[][] dp = new int[n+1][amount+1];
//dp[0][..]=0;dp[..][0]=1;
for (int i=0;i<=n;i++){
dp[i][0]=1;
}
for (int i=1;i<=n;i++){
for (int j=1;j<=amount;j++){
if (j-coins[i-1]>=0){
dp[i][j] = dp[i-1][j]+dp[i][j-coins[i-1]];
}else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][amount];
}
}
4、详解一道腾讯面试题:编辑距离
5、#887. 鸡蛋掉落
对我来说,难,先放在这里,还不会做
打家劫舍系列问题
6、198. 打家劫舍
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if(n==0) return 0;
int[] dp = new int[n+2];
for (int i=n-1;i>=0;i--){//从后开始往前抢
/* 状态只和当前第 i 和房子有关
每次选择到第i个房子,nums[i]代表第i个房子的现金
假设第i个房子抢劫,那么 i+1 个房子不抢 dp[i]=dp[i+2]+nums[i-1];
第i个房子不抢, dp[i]=dp[i+1];
*/
dp[i] = Math.max(dp[i+1],dp[i+2]+nums[i]);
}
return dp[0];
}
}
7、213. 打家劫舍 II
class Solution {
public int rob(int[] nums) {
//分为两种情况,要么第一家不偷,要么最后一家不偷
int n = nums.length;
if(n==0) return 0;
if(n==1) return nums[0];
/*
最后一家不偷:从 1 到 n-1
dp[i] = Math.max(dp[i-2]+nums[i-1],dp[i-1]);
*/
/*
第一家不偷:从 2 到 n
*/
return Math.max(robHelp(nums,0,n-2),robHelp(nums,1,n-1));
}
public int robHelp(int[] nums,int start,int end) {
int[] dp = new int[nums.length+2];
for (int i=end;i>=start;i--){
dp[i] = Math.max(dp[i+1],dp[i+2]+nums[i]);
}
return dp[start];
}
}
8、337. 打家劫舍 III
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
Map<TreeNode,Integer> map = new HashMap<>();
public int rob(TreeNode root) {
//回溯:如果根访问了,就不能访问他的儿子,只能访问孙子;
//如果根没有访问,可以访问他的儿子
if (root==null) return 0;
if (map.containsKey(root)){
return map.get(root);
}
int doIt = root.val + (root.left==null?0:rob(root.left.left)+rob(root.left.right))
+(root.right==null?0:rob(root.right.left)+rob(root.right.right));
int notDo = rob(root.left)+rob(root.right);
int res = Math.max(doIt,notDo);
map.put(root,res);
return res;//这样做相同的的节点做了重复的运算,可以采用备忘录消除重叠子问题 map<>
}
}
动态规划之 KMP 算法详解
原文:https://mp.weixin.qq.com/s?__biz=MzAxODQxMDM0Mw==&mid=2247484731&idx=2&sn=d9d6b24c7f94d5e43e08666e82251984&chksm=9bd7fb33aca0722548580dd27eb49880dc126ef87aeefedc33aa0f754f54691af6b09b41f45f&scene=21#wechat_redirect
先在开头约定,本文用pat表示模式串,长度为M,txt表示文本串,长度为N。KMP 算法是在txt中查找子串pat,如果存在,返回这个子串的起始索引,否则返回 -1
KMP 算法永不回退txt的指针i,不走回头路(不会重复扫描txt),而是借助dp数组中储存的信息把pat移到正确的位置继续匹配,时间复杂度只需 O(N),用空间换时间
KMP kmp = new KMP("aaab");
int pos1 = kmp.search("aaacaaab"); //4
int pos2 = kmp.search("aaaaaaab"); //4
public class KMP {
private int[][] dp;
private String pat;
public KMP(String pat) {
this.pat = pat;
int M = pat.length();
// dp[状态][字符] = 下个状态
dp = new int[M][256];
// base case
dp[0][pat.charAt(0)] = 1;
// 影子状态 X 初始为 0
int X = 0;
// 构建状态转移图(稍改的更紧凑了)
for (int j = 1; j < M; j++) {
for (int c = 0; c < 256; c++)
dp[j][c] = dp[X][c];
dp[j][pat.charAt(j)] = j + 1;
// 更新影子状态
X = dp[X][pat.charAt(j)];
}
}
public int search(String txt) {
int M = pat.length();
int N = txt.length();
// pat 的初始态为 0
int j = 0;
for (int i = 0; i < N; i++) {
// 计算 pat 的下一个状态
j = dp[j][txt.charAt(i)];
// 到达终止态,返回结果
if (j == M) return i - M + 1;
}
// 没到达终止态,匹配失败
return -1;
}
}
股票买卖问题
121. 买卖股票的最佳时机
/** 只能买一支股票
* 2个状态:第i天,,是否持有股票0,1,
* 选择:
* @param prices
* @return
*/
public int maxProfit(int[] prices) {
int n = prices.length;
int k=1;
int[][] dp = new int[n+1][2];
dp[0][0] = 0;
dp[0][1] = Integer.MIN_VALUE;
for (int i=1;i<=n;i++){
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i-1]);//i-1天没有股票,i-1天有股票,第i天卖掉了
dp[i][1] = Math.max(dp[i-1][1],-prices[i-1]);//第i天手上有股票,要么是i-1天的股票,要么是第i天买的股票
}
return dp[n][0];
}
122. 买卖股票的最佳时机 II
int n = prices.length;
int k=1;
int[][] dp = new int[n+1][2];
dp[0][0] = 0;
dp[0][1] = Integer.MIN_VALUE;
for (int i=1;i<=n;i++){
//i-1天没有股票,i-1天有股票,第i天卖掉了
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i-1]);
//第i天手上有股票,要么是i-1天的股票,要么是i-1天没有股票,i天买的股票
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i-1]);
}
return dp[n][0];
根据题目的意思,当天卖出以后,当天还可以买入,所以其实可以第三天卖出,第三天买入,第四天又卖出((5-1)+ (6-5) === 6 - 1)。所以算法可以直接简化为只要今天比昨天大,就卖出。
public int maxProfit(int[] prices) {
int ans = 0;
for (int i=1;i<prices.length;i++){
if (prices[i]>prices[i-1]){
ans+=(prices[i]-prices[i-1]);
}
}
return ans;
}
123. 买卖股票的最佳时机 III
class Solution {
/**
* 你最多可以完成 两笔 交易。
* 3个状态:第i天,第k次交易,是否持有股票0,1,
* @param prices
* @return
*/
public int maxProfit(int[] prices) {
int n = prices.length;
int K = 2;
int[][][] dp = new int[n+1][K+1][2];
for (int k=0;k<=K;k++){
dp[0][k][1] = Integer.MIN_VALUE;
}
for (int i=1;i<=n;i++){
for (int k=1;k<=K;k++){
//第i天第k次交易手上没有股票,,,记买入股票为一次交易
dp[i][k][0] = Math.max(dp[i-1][k][0],dp[i-1][k][1]+prices[i-1]);
dp[i][k][1] = Math.max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i-1]);
}
}
return dp[n][K][0];
}
}
309. 最佳买卖股票时机含冷冻期
/**
* 你可以尽可能地完成更多的交易(多次买卖一支股票)
* 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
* @param prices
* @return
*/
public int maxProfit4(int[] prices) {
int n = prices.length;
int k=1;
int[][] dp = new int[n+2][2];
dp[0][0] = 0;
dp[0][1] = Integer.MIN_VALUE;
for (int i=1;i<=n;i++){
//i-1天没有股票,i-1天有股票,第i天卖掉了
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i-1]);
//第i天手上有股票,要么是i-1天的股票,要么是i-2天没有股票,i天买的股票
//dp[i][1] = Math.max(dp[i-1][1],dp[i-2][0]-prices[i-1]);
if (i-2<0){
dp[i][1] = Math.max(dp[i-1][1],0-prices[i-1]);
}else {
dp[i][1] = Math.max(dp[i-1][1],dp[i-2][0]-prices[i-1]);
}
}
return dp[n][0];
}
188. 买卖股票的最佳时机 IV
class Solution {
/**
当k大于等于数组长度一半时, 问题退化为贪心问题此时采用 买卖股票的最佳时机 II
的贪心方法解决可以大幅提升时间性能, 对于其他的k, 可以采用 买卖股票的最佳时机 III
的方法来解决, 在III中定义了两次买入和卖出时最大收益的变量, 在这里就是k租这样的
变量, 即问题IV是对问题III的推广, t[i][0]和t[i][1]分别表示第i比交易买入和卖出时
各自的最大收益
**/
public int maxProfit(int k, int[] prices) {
int n = prices.length;
if (k>n/2){
return greedy(prices);
}
int[][][] dp = new int[n+1][k+1][2];
for (int j=1;j<=k;j++){
dp[0][j][1] = Integer.MIN_VALUE;
}
for (int i=1;i<=n;i++){
for (int j=1;j<=k;j++){
//第i天第k次交易手上没有股票,,,记买入股票为一次交易
dp[i][j][0] = Math.max(dp[i-1][j][0],dp[i-1][j][1]+prices[i-1]);
dp[i][j][1] = Math.max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i-1]);
}
}
return dp[n][k][0];
}
public int greedy(int[] prices) {
int ans = 0;
for (int i=1;i<prices.length;i++){
if (prices[i]>prices[i-1]){
ans+=(prices[i]-prices[i-1]);
}
}
return ans;
}
}
714. 买卖股票的最佳时机含手续费
class Solution {
public int maxProfit(int[] prices, int fee) {
int n = prices.length;
int k=1;
int[][] dp = new int[n+1][2];
dp[0][0] = 0;
dp[0][1] = Integer.MIN_VALUE;
for (int i=1;i<=n;i++){
//i-1天没有股票,i-1天有股票,第i天卖掉了
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i-1]);
//第i天手上有股票,要么是i-1天的股票,要么是i-1天没有股票,i天买的股票
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i-1]-fee);
}
return dp[n][0];
}
}
子序列问题
300. 最长上升子序列
class Solution {
/**
* 300. 最长上升子序列
* 输入: [10,9,2,5,3,7,101,18]
* 输出: 4
* 解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
* dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。
* 我们只要找到前面那些比nums[i]小的子序列,然后把 nums[i]接到最后,就可以形成一个新的递增子序列,而且这个新的子序列长度加一。
* @param nums
* @return
*/
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp,1);
// 先求出每一个dp[i],最后求出最大dp
// 当前nums[i]的dp值为比nums[i]小的num值得dp+1;dp[i] = max(dp[i],dp[j]+1)
int res=0;
for (int i=0;i<n;i++){
for (int j=0;j<i;j++){
if (nums[j]<nums[i]){
dp[i]=Math.max(dp[i],dp[j]+1);
}
}
res = Math.max(res,dp[i]);
}
return res;
}
}
1143.最长公共子序列
dp[i][j]的含义是:对于s1[1…i]和s2[1…j],它们的 LCS 长度是dp[i][j]。
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
//dp[i][j]表示第i个,第j个字符的最长公共子序列长
int m = text1.length();
int n = text2.length();
int [][] dp = new int[m+1][n+1];
for (int i=1;i<=m;i++) {
for (int j = 1; j <= n; 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]);
}
}
}
return dp[m][n];
}
}
516. 最长回文子序列
class Solution {
public int longestPalindromeSubseq(String s) {
int n = s.length();
int[][] dp = new int[n][n];
for(int i=0;i<n;i++) {
dp[i][i] = 1;
}
for (int i=n-1;i>=0;i--){
for (int j=i+1;j<n;j++){
if (s.charAt(i)==s.charAt(j)){
dp[i][j]=dp[i+1][j-1]+2;
}else {
dp[i][j] = Math.max(dp[i][j-1],dp[i+1][j]);
}
}
}
return dp[0][n-1];
}
}
354. 俄罗斯套娃信封问题(最长上升子序列之信封嵌套)添加链接描述
class Solution {
/*
354. 俄罗斯套娃信封问题
输入: envelopes = [[5,4],[6,4],[6,7],[2,3]]
输出: 3
解释: 最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。
先对宽度w进行升序排序,如果遇到w相同的情况,则按照高度h降序排序。之后把所有的h作为一个数组,在这个数组上计算 LIS 的长度就是答案。
*/
public int maxEnvelopes(int[][] envelopes) {
if (envelopes.length==0){
return 0;
}
Arrays.sort(envelopes, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[0]==o2[0]?o2[1]-o1[1]:o1[0]-o2[0];
}
});
int n = envelopes.length;
int[] high = new int[n];
for (int i=0;i<n;i++){
high[i] = envelopes[i][1];
}
return lengthOfLIS(high);
}
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp,1);
// 先求出每一个dp[i],最后求出最大dp
// 当前nums[i]的dp值为比nums[i]小的num值得dp+1;dp[i] = max(dp[i],dp[j]+1)
int res=0;
for (int i=0;i<n;i++){
for (int j=0;j<i;j++){
if (nums[j]<nums[i]){
dp[i]=Math.max(dp[i],dp[j]+1);
}
}
res = Math.max(res,dp[i]);
}
return res;
}
}
博弈问题
正则表达式
class Solution {
public boolean isMatch(String s, String p) {
//分三种情况:
/*
1: 第 i 个字符 和 j 字符相等时,跳过 i和j
2: j = ‘.' ,跳过 i ,j
3: j+1 字符为 ’*’ 时, 因为 * 表示匹配 0 个或者 多个,所以可能有两种情况: 匹配 0 次;的情况或者 根据 j 个字符匹配多次的情况
*/
int m = s.length();
int n = p.length();
boolean[][] dp = new boolean[m+2][n+2];
dp[m][n] = true;
for (int i= m;i>=0;i--){
for (int j=n-1;j>=0;j--){
boolean first = i<m && (p.charAt(j)== s.charAt(i))||(p.charAt(j)=='.');
if (j+1<p.length()&&p.charAt(j+1)=='*'){
dp[i][j] = (dp[i][j+2] || (first && dp[i+1][j]));//当j=n-1时,当前i=m
}else {
dp[i][j] = first&&dp[i+1][j+1];
}
}
}
return dp[0][0];
}
}