前言:LeetCode或牛客上的题
思考:贪心和动态规划有什么区别?
贪心通过局部最优找到全局最优,同等情况下比动态规划更快。
动态规划包括定义状态,找出状态转移方程、初始化条件
状态如背包容量、最大利润、买卖状态等
动态规划强化
198. 打家劫舍
198. 打家劫舍
图解动态规划的解题四步骤(C++/Java/Python)
二维数组
class Solution {
public int rob(int[] nums) {
int len = nums.length;
int[][] dp = new int[len + 1][len + 1];
// dp[i][j]定义偷i个屋子,遍历到第j个屋子的最大金额
for (int i = 1; i <= len; i++) {
for (int j = 1; j <= len; j++) {
if (j >= 2) {
dp[i][j] = Math.max(dp[i - 1][j - 2] + nums[j-1], dp[i - 1][j - 1]);
//System.out.println(Arrays.toString(dp[i]));
}
else{
dp[i][j] = nums[j-1];
//dp[i][j] = Math.max(nums[j-1], dp[i - 1][j - 1]);
//System.out.println(Arrays.toString(dp[i]));
}
}
//System.out.println();
}
return dp[len][len];
}
}
一维数组,空间可以考虑优化
public int rob(int[] nums) {
int len = nums.length;
int[] dp = new int[len];
if(nums == null || nums.length == 0)
return 0;
if(nums.length == 1)
return nums[0];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for(int i = 2; i < len; i++){
dp[i] = Math.max(dp[i-2] + nums[i], dp[i-1]);
}
return dp[len-1];
}
public int rob1(int[] nums) {
int len = nums.length;
if(nums == null || nums.length == 0)
return 0;
if(nums.length == 1)
return nums[0];
int left = nums[0];
int right = Math.max(nums[0], nums[1]);
for(int i = 2; i < len; i++){
int tmp = Math.max(left + nums[i], right);
left = right;
right = tmp;
}
return right;
}
53. 最大子序和
动态递归,可优化空间
public int maxSubArray(int[] nums) {
if(nums == null || nums.length == 0)
return 0;
if(nums.length == 1)
return nums[0];
int len = nums.length;
int[] dp = new int[len];
dp[0] = nums[0];
int max = dp[0];
for(int i = 1; i < len; i++){
dp[i] = dp[i-1] > 0 ? dp[i-1] + nums[i] : nums[i];
if(dp[i] > max)
max = dp[i];
}
return max;
}
分治,很好的思路
public int maxSubArray(int[] nums) {
return maxSubArrayDivideWithBorder(nums, 0, nums.length-1);
}
private int maxSubArrayDivideWithBorder(int[] nums, int start, int end) {
if (start == end) {
// 只有一个元素,也就是递归的结束情况
return nums[start];
}
// 计算中间值
int center = (start + end) / 2;
int leftMax = maxSubArrayDivideWithBorder(nums, start, center); // 计算左侧子序列最大值
int rightMax = maxSubArrayDivideWithBorder(nums, center + 1, end); // 计算右侧子序列最大值
// 下面计算横跨两个子序列的最大值
// 计算包含左侧子序列最后一个元素的子序列最大值
int leftCrossMax = Integer.MIN_VALUE; // 初始化一个值
int leftCrossSum = 0;
for (int i = center ; i >= start ; i --) {
leftCrossSum += nums[i];
leftCrossMax = Math.max(leftCrossSum, leftCrossMax);
}
// 计算包含右侧子序列最后一个元素的子序列最大值
int rightCrossMax = nums[center+1];
int rightCrossSum = 0;
for (int i = center + 1; i <= end ; i ++) {
rightCrossSum += nums[i];
rightCrossMax = Math.max(rightCrossSum, rightCrossMax);
}
// 计算跨中心的子序列的最大值
int crossMax = leftCrossMax + rightCrossMax;
// 比较三者,返回最大值
return Math.max(crossMax, Math.max(leftMax, rightMax));
}
70. 爬楼梯
70. 爬楼梯
爬楼梯解-矩阵快速幂
递归超时
动态规划或者继续优化空间
121. 买卖股票的最佳时机
一次遍历思路很好
动态规划
class Solution {
public int maxProfit(int[] prices) {
if(prices == null || prices.length < 2)
return 0;
int len = prices.length;
int[] dp = new int[len+1]; // dp[i]表示到第i天所获得的最大利润
if(prices[1] > prices[0])
dp[2] = prices[1] - prices[0]; // 只有第二天才能能完成一次买卖
int min = prices[0]; // 假设第i天前的股票最小值
for(int i = 3; i <= len; i++ ){
if(prices[i-2] < min)
min = prices[i-2];
int tmp = 0; // 到第i天进行卖出的利润为0
if(prices[i-1] > min){
tmp = prices[i-1] - min;
}
dp[i] = Math.max(dp[i-1], tmp);
}
return dp[len];
}
}
分治思路
class Solution {
public int maxProfit(int[] prices) {
if(prices == null || prices.length < 2)
return 0;
int len = prices.length;
return maxProfit(prices, 0, len-1);
}
private int maxProfit(int[] nums, int start, int end) {
if (start >= end) {
// 只有一个元素,也就是递归的结束情况
return 0;
}
// 计算中间值
int center = (start + end) / 2;
int leftMax = maxProfit(nums, start, center); // 计算左侧子序列最大值
int rightMax = maxProfit(nums, center + 1, end); // 计算右侧子序列最大值
// 下面计算横跨两个子序列的最大差值
// 计算包含左侧子序列的最小值
int leftCrossMin = nums[center]; // 初始化一个值
for (int i = center; i >= start; i--) {
leftCrossMin = Math.min(nums[i], leftCrossMin);
}
// 计算包含右侧子序列的最大值
int rightCrossMax = nums[center + 1];
for (int i = center + 1; i <= end; i++) {
rightCrossMax = Math.max(nums[i], rightCrossMax);
}
// 计算跨中心的子序列的最大差值
int crossMax = rightCrossMax - leftCrossMin;
// 比较三者,返回最大值
return Math.max(crossMax, Math.max(leftMax, rightMax));
}
}
494. 目标和
// dfs
class Solution {
private int count = 0;
public int findTargetSumWays(int[] nums, int S) {
dfs(nums, S);
return count;
}
public void dfs(int[] nums, int S){
if(nums.length == 0){
if(S == 0){
count++;
}
return;
}
int len = nums.length;
dfs(Arrays.copyOfRange(nums, 1, len), S-nums[0]);// 前面为加
dfs(Arrays.copyOfRange(nums, 1, len), S+nums[0]); // 前面为减
}
}
// dp
class Solution {
public int findTargetSumWays(int[] nums, int S) {
// dp[i][j] 表示 遍历了数组中前i个元素(包括),组成和为j的方案数
// 若j为0表示不存在当前组合
int len = nums.length;
int[][] dp = new int[len + 1][2001];
dp[0][1000] = 1;
for (int i = 1; i <= len; i++) {
for (int j = -1000; j <= 1000; j++) {
if (dp[i - 1][j + 1000] > 0) {
dp[i][j + nums[i-1] + 1000] += dp[i - 1][j + 1000];
dp[i][j - nums[i-1] + 1000] += dp[i - 1][j + 1000];
}
}
}
return S > 1000 ? 0 : dp[len][S+1000];
}
}
647. 回文子串
416. 分割等和子集
class Solution {
public boolean canPartition(int[] nums) {
if(nums == null || nums.length == 0)
return false;
int sum = 0;
for(int i : nums){
sum += i;
}
if((sum & 1) == 1){
return false;
}
int len = nums.length;
int target = sum/2;
boolean[][] dp = new boolean[len][target+1]; // dp[i][j]表示从[0,i]区间挑选一些整数,使得累加和为j
if(nums[0] <= target){
dp[0][nums[0]] = true;
}
for(int i = 1; i < len; i++){
for(int j = 0; j <= target; 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]];
}
if(dp[i][target]){
// 若以满足挑选元素累加和为target,提前结束
return true;
}
}
}
return dp[len-1][target];
}
}
338. 比特位计数
338. 比特位计数
衍生思路,如何快速计算数字变为二进制上1的个数,x&(x-1)
比特位计数
class Solution {
public int[] countBits(int num) {
int[] dp = new int[num+1];
for(int i = 1; i <= num; i++){
//String numStr = Integer.toBinaryString(i);
//System.out.println(numStr);
//int len = numStr.length();
//int pow = (int) Math.pow(2, len-1);
//dp[i] = dp[i-pow] + 1;
dp[i] = dp[i>>1] + (i&1);
//dp[i] = dp[i & (i-1)] + 1;
}
return dp;
}
}
309. 最佳买卖股票时机含冷冻期
class Solution {
public int maxProfit(int[] prices) {
if(prices.length < 2)
return 0;
int len = prices.length;
int[][] dp = new int[len][3];
// dp[i][j] 表示在下标为i这一天状态为j所取得的最大收益
dp[0][0] = 0;
dp[0][1] = -prices[0];// 第一天买入为负收益
dp[0][2] = 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][2]-prices[i]);
dp[i][2] = dp[i-1][0];
}
int result = Math.max(dp[len-1][0], dp[len-1][2]);
return result;
}
}
279. 完全平方数
广度优先遍历BFS也可以,适合找到最短连接或组合
// dp
class Solution {
public int numSquares(int n) {
int[] dp = new int[n+1]; // dp[i]表示i的完全平方最小个数
for(int i = 1; i <= n; i++){
dp[i] = i; // 最坏情况下全部为1累加
for(int j = 1; j * j <= i; j++ ){
dp[i] = Math.min(dp[i], dp[i-j*j]+1);
}
}
return dp[n];
}
}
// bfs
class Solution {
public int numSquares(int n) {
Queue<Integer> queue = new ArrayDeque<Integer>();
HashSet<Integer> set = new HashSet<>(); // 去重的,相同的结点若第一次出现肯定在当前结点的左边或上层
queue.add(n);
int level = 0;
while(!queue.isEmpty()){
int size = queue.size();
level++; // 完全平方累加层数
while(size > 0 && !queue.isEmpty()){
int cur = queue.poll();
for(int j = 1; j*j <= cur; j++){
int next = cur - j * j;
if(next == 0){
return level;
}
if(!set.contains(next)){
queue.offer(next);
set.add(next);
}
}
size--;
}
}
return level;
}
}
139. 单词拆分
139. 单词拆分
单词拆分
找不出状态就一维dp,切分子问题有些时候也需要遍历任意子问题的可能性
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int len = s.length();
HashSet<String> set = new HashSet<>(wordDict);
boolean[] dp = new boolean[len+1]; // dp[i]表示字符串遍历到i位置前,任意拆分中存在使得各部分为字典里中的字符串
dp[0] = true;
for(int i = 1; i <= len; i++){
for(int j = 0; j < i; j++){
if(dp[j] && set.contains(s.substring(j, i))){
dp[i] = true;
break;
}
}
}
return dp[len];
}
}
// 优化一下,因为每次切分后的部分字符串为空或者恰好处于最长或最短字符串长度中间才有意义
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int len = s.length(), maxw = Integer.MIN_VALUE, minw = Integer.MAX_VALUE;
boolean[] dp = new boolean[len + 1];
dp[0] = true;
Set<String> set = new HashSet();
for(String str : wordDict){
set.add(str);
maxw = Math.max(maxw, str.length());
minw = Math.min(minw, str.length());
}
for(int i = 1; i < len + 1; i++){
for(int j = i-minw; j >= 0 && j >= i - maxw; j--){
if(dp[j] && set.contains(s.substring(j, i))){
dp[i] = true;
break;
}
}
}
return dp[len];
}
}
221. 最大正方形
// 动态规划,更具体
public int maximalSquare(char[][] matrix) {
if(matrix == null || matrix.length == 0 || matrix[0].length == 0)
return 0;
int rows = matrix.length;
int cols = matrix[0].length;
int[][] dp = new int[rows][cols];
int max = 0;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (matrix[i][j] == '0')
continue;
if (i == 0 || j == 0 ) {
dp[i][j] = 1;
} else {
dp[i][j] = Math.min(dp[i-1][j-1],Math.min(dp[i-1][j], dp[i][j-1]))+1; // 状态转移方程
}
max = Math.max(dp[i][j], max);
}
}
return max * max;
}
// 这部分动态规划
public int maximalSquare1(char[][] matrix) {
if(matrix == null || matrix.length == 0 || matrix[0].length == 0)
return 0;
int rows = matrix.length;
int cols = matrix[0].length;
int[][] dp = new int[rows][cols];
int max = 0;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (matrix[i][j] == '0')
continue;
if (i == 0 || j == 0 || matrix[i - 1][j - 1] == '0') {
dp[i][j] = 1;
} else {
int tmp = dp[i - 1][j - 1]; // 左上一个位置最大正方形的边长
dp[i][j] = 1; // 默认范围为1
int width = 1;
while (width <= tmp) {
if (matrix[i-width][j] == '0' || matrix[i][j-width] == '0') {
break;
}
dp[i][j] += 1;
width++;
}
max = Math.max(dp[i][j], max);
}
}
}
return max * max;
}
152. 乘积最大子数组
// 动态递归O(N)
public int maxProduct(int[] nums) {
if(nums == null || nums.length == 0)
return 0;
int len = nums.length;
int[] dpMax = new int[len]; // dpMax[i] 表示以i位置结尾的子数组的最大乘积
int[] dpMin = new int[len]; // dpMin[i] 表示以i位置结尾的子数组的最小乘积
for(int i = 0; i < len; i++){
if(i == 0){
dpMax[i] = nums[i];
dpMin[i] = nums[i];
}else{
dpMax[i] = Math.max(dpMax[i-1]*nums[i], Math.max(nums[i], dpMin[i-1]*nums[i]));
dpMin[i] = Math.min(dpMin[i-1]*nums[i], Math.min(nums[i], dpMax[i-1]*nums[i]));
}
}
int max = Integer.MIN_VALUE;
for(int i : dpMax){
if(i > max)
max = i;
}
return max;
}
// 替换了dp数组
public int maxProduct(int[] nums) {
if(nums == null || nums.length == 0)
return 0;
int len = nums.length;
int dpMax = 1;
int dpMin = 1;
int max = Integer.MIN_VALUE;
for(int i = 0; i < len; i++){
int tmpMax = dpMax; // 会互相干扰,所以需要临时值
int tmpMin = dpMin;
dpMax = Math.max(tmpMax * nums[i], Math.max(nums[i], tmpMin*nums[i]));
dpMin = Math.min(tmpMin*nums[i], Math.min(nums[i], tmpMax*nums[i]));
max = Math.max(max, dpMax);
}
return max;
}
// 暴力枚举
public int maxProduct1(int[] nums) {
if(nums == null || nums.length == 0)
return 0;
int max = Integer.MIN_VALUE;
int len = nums.length;
int[] dp = new int[len]; // dp[i][j]表示取i+1个连续元素,截止下标位置为j的乘积值
for(int i = 0; i < len; i++){
for(int j = len-1; j >= i; j--){
if(i == 0){
dp[j] = nums[j];
}else{
dp[j] = dp[j-1] * nums[j];
}
max = Math.max(max, dp[j]);
}
}
return max;
}
96. 不同的二叉搜索树
class Solution {
public int numTrees(int n) {
int[] dp = new int[n+1]; // dp[i]表示给定整数i组成的数种类
// 选择根结点,分成左右子树
// 卡塔兰数Cn+1=2(2n+1)/(n+2) *Cn
dp[0] = 1;
for(int i = 1; i <= n; i++){
for(int j = i-1; j >= 0; j--){
dp[i] += dp[j] * dp[i-j-1];
}
}
return dp[n];
}
}
300. 最长上升子序列
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums.length < 2)
return nums.length;
int len = nums.length;
int[] dp = new int[len]; // dp[i]表示以第i个元素截止的最长上升子序列的长度
dp[0] = 1;
int minVal = nums[0]; // 保存前面出现的最小值
int maxLength = 1;
for(int i = 1; i < len; i++){
if(nums[i] <= minVal){ // 若出现新的最小值,从新开始
dp[i] = 1;
minVal = nums[i];
continue;
}
for(int j = 0; j < i; j++){
if(nums[j] < nums[i]){
dp[i] = Math.max(dp[j]+1, dp[i]);
}
}
maxLength = Math.max(dp[i], maxLength);
}
return maxLength;
}
}
322. 零钱兑换
322. 零钱兑换
动态规划、使用「完全背包」问题思路、图的广度优先遍历
注意:
1、这里有一个很大的问题值得思考,那就是先容量后物品,还是先物品后容量
2、完全背包问题还有一个容积恰好的情况,即不浪费一寸空间。这个初始值设置需要注意,设置最大值或最小值,或一个稍微合适的值。
3、若容积不够取包,二维数组当前位置还需要赋予上一行的值,而一维数组不变化就可。
// 完全背包问题,后面还需要掌握化为2*N数组以及一维数组
class Solution {
public int coinChange(int[] coins, int amount) {
if (coins == null || coins.length == 0 || coins.length == 1 && amount % coins[0] != 0)
return -1;
if (amount <= 0 )
return 0;
//Arrays.sort(coins); 背包问题不需要排序
int len = coins.length;
int[][] dp = new int[len + 1][amount + 1]; // dp[i][j] 表示取前i个硬币能组合成总金额j的最少硬币个数
for (int j = 1; j <= amount; j++) {
dp[0][j] = amount+1; // 不取硬币,不可能凑成amount+1
}
// 先容积后取包
for (int j = 1; j <= amount; j++) {
for (int i = 1; i <= len; i++) {
dp[i][j] = dp[i - 1][j];
if (j >= coins[i - 1]) {
dp[i][j] = Math.min(dp[i][j], dp[i][j - coins[i - 1]] + 1);
}
}
}
// 先取包后容积
/* for (int i = 1; i <= len; i++) {
for (int j = 1; j <= amount; j++) {
dp[i][j] = dp[i - 1][j];
if (j >= coins[i - 1]) {
dp[i][j] = Math.min(dp[i][j], dp[i][j - coins[i - 1]] + 1);
}
}
//System.out.println(i + " " + Arrays.toString(dp[i]));
}*/
// 若最终结果认为设置的不可能值amount+1,则为-1
int minCoins = dp[len][amount] == amount+1 ? -1 : dp[len][amount];
return minCoins;
}
}
62. 不同路径
class Solution {
public int uniquePaths(int m, int n) {
if(m == 0 || n == 0)
return 0;
if(m == 1 || n == 1)
return 1;
int[][] dp = new int[m][n]; // dp[i][j]表示遍历到i、j位置的所有路径
for(int i = 0; i < m; i++){
dp[i][0] = 1;
}
for(int j = 0; j < n; j++){
dp[0][j] = 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];
}
}
64. 最小路径和
class Solution {
public int minPathSum(int[][] grid) {
int rows = grid.length;
int cols = grid[0].length;
int[][] dp = new int[rows][cols]; // dp[i][j] 表示遍历到grid[i][j]位置的路径上最小累加和
dp[0][0] = grid[0][0];
for(int i = 1; i < rows; i++){
dp[i][0] = dp[i-1][0] + grid[i][0];
}
for(int j = 1; j < cols; j++){
dp[0][j] = dp[0][j-1] + grid[0][j];
}
if(rows == 1)
return dp[0][cols-1];
if(cols == 1)
return dp[rows-1][0];
for(int i = 1; i < rows; i++){
for(int j = 1; j < cols; j++){
dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1])+grid[i][j];
}
}
return dp[rows-1][cols-1];
}
}
5. 最长回文子串
// 暴力破解有些时候也要考虑优化
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() < 2)
return s;
int len = s.length();
//int[][] dp = new int[len+1][len+1]; // dp[i][j] 表示以子字符串长度为i,截止下标为j为回文的长度,这是思路但没必要用这个结构
int maxLength = 0;
String outString = null;
for(int i = 1; i <= len; i++){
for(int j = i; j <= len; j++){
String tmp = s.substring(j-i, j);
if(i > maxLength && judge(tmp) ){ // judge在左边结果超时,但是先去判断是否超过最长再去判断是否回文就可以通过
// 若截取的子串为回文且长度又是一个新最长,保存
maxLength = i;
outString = tmp;
}
}
}
return outString;
}
private boolean judge(String s) {
if(s == null || s.length() < 2)
return true;
int left = 0, right = s.length()-1;
while (left < right){
if(s.charAt(left) != s.charAt(right))
return false;
left++;
right--;
}
return true;
}
}
// 动态规划
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() < 2)
return s;
int len = s.length();
boolean[][] dp = new boolean[len][len]; // dp[i][j] 表示以s[i,j]为true子字符串为回文字符串
int maxLength = 0;
String outString = null;
for (int i = len-1; i >= 0; i--) {
for (int j = i; j < len; j++) {
if (i == j) {
dp[i][j] = true;
} else if (s.charAt(i) == s.charAt(j)) {
if (j - i < 3) { // 字符串长度为2或3、首尾相同即为回文
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1]; // 除去首尾字符的字符串是否为回文
}
}else{
dp[i][j] = false; // 字符串的首尾字符不相同则不为回文
}
if (dp[i][j] && j - i + 1 >= maxLength) { // 若为回文,且字符串长度又是新的最长,保存
// 若相同长度也要取最左边的回文
maxLength = j - i + 1;
outString = s.substring(i, j+1);
}
}
}
return outString;
}
}