題目來源:
來源:力扣(LeetCode)
鏈接:https
文章目錄
動態規劃綱要
主要思想:將問題拆分成多個子問題,然後找到最優的子結構;
自頂向下:遞歸解題思路,可以優化爲記憶化搜索;
自底向上:動態規劃思路;
一、斐波拉切數列
1.爬樓梯(Easy)
第2次
題目鏈接:爬樓梯
題目描述:
假設你正在爬樓梯。需要 n 階你才能到達樓頂。每次你可以爬 1 或 2 個臺階。你有多少種不同的方法可以爬到樓頂呢?
注意:給定 n 是一個正整數。
解題思路:
滿足f(n) = f(n-1)+f(n-2),舉例來說明,比如要爬8階樓梯,可以先爬上7階樓梯,然後再爬一階樓梯,這種方式實現的數目與爬7階樓梯一致;也可以先爬上6階樓梯,然後再一次性爬2階樓梯,這種方式實現的數目與爬6階樓梯一致;所以,f(8) = f(7)+f(6);
解法一:建立長度爲n的動態數組,將得到的方法數量填到對應的數組中,最後返回nums[n-1];
解法二:不用建立數組,創建兩個變量pre1和pre2,在for循環裏創建一個變量cur = pre1 + pre2,然後讓pre1 =cur,pre2 = pre1,巧妙地實現;
解法三:遞歸;
代碼:
解法一:
public int climbStairs(int n) {
int[] nums = new int [n];
if(n < 2){
return n;
}
nums[0] = 1;
nums[1] = 2;
for(int i=2; i < n; i++){
nums[i] = nums[i-1] + nums[i-2];
}
return nums[n-1];
}
解法二:
public int climbStairs(int n) {
if (n <= 2) {
return n;
}
int pre2 = 1, pre1 = 2;
for (int i = 2; i < n; i++) {
int cur = pre1 + pre2;
pre2 = pre1;
pre1 = cur;
}
return pre1;
}
2. 打家劫舍(Easy)
第4次
題目鏈接:打家劫舍
題目描述:
你是一個專業的小偷,計劃偷竊沿街的房屋。每間房內都藏有一定的現金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。給定一個代表每個房屋存放金額的非負整數數組,計算你在不觸動警報裝置的情況下,能夠偷竊到的最高金額。
解題思路:
定義 dp 數組用來存儲最大的搶劫量,其中 dp[i] 表示搶到第 i 個住戶時的最大搶劫量。由於不能搶劫鄰近住戶,如果搶劫了第 i -1 個住戶,那麼就不能再搶劫第 i 個住戶,所以得到如下公式:
代碼:
遞歸解法:
public int rob(int[] nums) {
return fun(nums,0);
}
public int fun(int[] nums,int x){
if(x >= nums.length)
return 0;
int temp = 0;
for(int i = x; i < nums.length; i++){
temp = Math.max(temp,nums[i] + fun(nums,i+2));
}
return temp;
}
記憶化搜索:
public int rob(int[] nums) {
int[] dp = new int[nums.length+1];
Arrays.fill(dp,-1);
return fun(nums,0,dp);
}
public int fun(int[] nums,int x,int[] dp){
if(x >= nums.length)
return 0;
int temp = 0;
if(dp[x] != -1)
return dp[x];
for(int i = x; i < nums.length; i++){
temp = Math.max(temp,nums[i] + fun(nums,i+2,dp));
}
dp[x] = temp;
return temp;
}
動態規劃:
這裏定義的pre2(也即dp[i-2])爲截止到上上一個住戶盜竊的最大值,pre1(也即dp[i-1])爲截止到上一個用戶盜竊的最大值;
參考代碼:
public int rob(int[] nums) {
int pre2 = 0, pre1 = 0;
for (int i = 0; i < nums.length; i++) {
int cur = Math.max(pre2 + nums[i], pre1);
pre2 = pre1;
pre1 = cur;
}
return pre1;
}
我的代碼1:從後向前考慮
public int rob(int[] nums) { //從後向前考慮
int n = nums.length;
if(n == 0)
return 0;
else if(n == 1)
return nums[0];
else if(n == 2){
return Math.max(nums[0],nums[1]);
}
int[] dp = new int[n+1];
dp[n-1] = nums[n-1];
dp[n-2] = Math.max(nums[n-1],nums[n-2]);
for(int i = n-3; i >= 0; i--){
dp[i] = Math.max(nums[i] + dp[i+2],nums[i+1] + dp[i+3]);
}
return dp[0];
}
我的代碼2:從前向後考慮(稍微改進一下,將dp[i-1]和dp[i-2]分別用pre1和pre2表示,則變成參考代碼)
public int rob(int[] nums) { //從前向後考慮
int n = nums.length;
if(n == 0)
return 0;
else if(n == 1)
return nums[0];
int[] dp = new int[n];
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
for(int i = 2; i < nums.length; i++){
dp[i] = Math.max(nums[i] + dp[i-2],dp[i-1]);
}
return dp[n-1];
}
3. 打家劫舍 II(Medium)
第2次
題目鏈接:打家劫舍 II
題目描述:
你是一個專業的小偷,計劃偷竊沿街的房屋,每間房內都藏有一定的現金。這個地方所有的房屋都圍成一圈,這意味着第一個房屋和最後一個房屋是緊挨着的。同時,相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。給定一個代表每個房屋存放金額的非負整數數組,計算你在不觸動警報裝置的情況下,能夠偷竊到的最高金額。
解題思路:由於數組的第一個數和最後一個相鄰,所以只能取其中一個,因此考慮數組拆分成兩種情況,即從0 ~ nums.length - 2 與從1 ~ nums.length - 1,然後這兩部分可以看成是兩條直的街道盜竊,可以採用(2. 打家劫舍)中的方法進行求解;
代碼:
class Solution {
public int rob(int[] nums) {
if(nums.length == 1){
return nums[0];
}
return Math.max(robfind(nums,1,nums.length),robfind(nums,0,nums.length-1));
}
public int robfind(int[] nums, int a, int b){
int pre2 = 0;
int pre1 = 0;
for(int i = a; i < b; i++){
int cur = Math.max(pre2 + nums[i],pre1);
pre2 = pre1;
pre1 = cur;
}
return pre1;
}
}
4. 信件錯排(待補充)
第0次
題目鏈接:來源未知
5. 母牛生產
第0次
題目鏈接:來源未知
題目描述:假設農場中成熟的母牛每年都會生 1 頭小母牛,並且永遠不會死。第一年有 1 只小母牛,從第二年開始,母牛開始生小母牛。每隻小母牛 3 年之後(即達到3歲)成熟又可以生小母牛。給定整數 N,求 N 年後牛的數量。(假設:第2年小母牛可以生小母牛,每年的年初開始生小母牛,到該年結束,小母牛1歲);
解題思路:
dp[i] 表示第i年時的牛數量,dp[i-3]表示在第i年能生育的小母牛,dp[i-1]表示上一年的小母牛數量;
代碼:暫無
二、矩陣路徑
1.最小路徑和(Medium,題號:64)
注意:千萬不要陷入思維旋渦!!!
第1次
題目鏈接:最小路徑和
題目描述:
給定一個包含非負整數的 m x n 網格,請找出一條從左上角到右下角的路徑,使得路徑上的數字總和爲最小。
解題思路:
解法一:使用動態規劃的解法,定義一個動態數組dp[]長度大小爲n,dp[i]表示走到最後一行的第i列所走的最短路徑和,使用雙重for循環,第一層循環指的是走第i行,先走第一行(即i=0),得到一組dp[],然後再走第二行,更新dp[],在走第二行第一列的時候,dp[0]不需要跟別的數比較,直接拿自己加上grid[i][j]就完事,之後dp[j]需要跟dp[j-1]比較(如下圖所示),來決定用哪個去加上grid[i][j],最後返回dp數組的最後一個元素dp[n-1]。
解法二:遞歸,時間超限制
代碼:
解法一:
public int minPathSum(int[][] grid) {
if(grid == null || grid[0] == null){
return 0;
}
int m = grid.length;
int n = grid[0].length;
int[] dp = new int[n];
for(int i= 0; i < m; i++){
for(int j = 0; j < n; j++){
if(j == 0){ //在第一列中,從某行走到相鄰的下一行
dp[j] = dp[j] + grid[i][j];
}
else if(i == 0){ //走第一行時執行的代碼
dp[j] = dp[j-1] + grid[i][j];
}
else {
dp[j] = Math.min(dp[j],dp[j-1]) + grid[i][j];
}
}
}
return dp[n-1];
}
解法二:時間超限制,不推薦
class Solution {
public int minPathSum(int[][] grid) {
if(grid == null){
return 0;
}
return find(grid,0,0);
}
public int find(int[][] grid,int i,int j){
if(i == grid.length - 1 && j == grid[0].length - 1){
return grid[i][j];
}
if(i == grid.length - 1)
return grid[i][j]+find(grid,i,j+1);
else if(j == grid[0].length - 1)
return grid[i][j]+find(grid,i+1,j);
return Math.min(grid[i][j]+find(grid,i+1,j),grid[i][j]+find(grid,i,j+1));
}
}
2.不同路徑(Medium,題號:62)
第0次
題目鏈接:不同路徑
題目描述:
一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記爲“Start” )。機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記爲“Finish”)。
問總共有多少條不同的路徑?
解題思路:
解法一:遞歸
解法二:記憶化搜索,遞歸的改進
解法三:動態規劃,機器人每到一個方格的總路徑數取決於該方格的左側與上側方格的路徑數量,所以動態函數爲dp[i][j] = dp[i][j-1] + dp[i-1][j],可以簡寫爲:dp[j] = dp[j]+dp[j-1]。動態規劃的思想:記住前面的結果來得到後面的結果。
代碼:
解法一:遞歸
public int uniquePaths(int m, int n) {
if(m == 1 || n == 1)
return 1;
if(m == 2 && n == 2)
return 2;
return uniquePaths(m,n-1)+uniquePaths(m-1,n);
}
解法二:記憶化搜索
public int uniquePaths(int m, int n) {
int[][] dp = new int[m+1][n+1];
return quePaths(m,n,dp);
}
public int quePaths(int m, int n,int[][] dp) {
if(m == 1 || n == 1)
return 1;
if(m == 2 && n == 2)
return 2;
if(dp[m][n] != 0)
return dp[m][n];
dp[m][n-1] = quePaths(m,n-1,dp);
dp[m-1][n] = quePaths(m-1,n,dp);
return dp[m][n-1]+dp[m-1][n];
}
解法三:動態規劃
public int uniquePaths(int m, int n) {
int[] dp = new int[n];
Arrays.fill(dp, 1);
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[j] = dp[j] + dp[j - 1];
}
}
return dp[n - 1];
}
3.不同路徑 II(Medium,題號:63)
第1次
題目鏈接:不同路徑 II
題目描述:
一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記爲“Start” )。
機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記爲“Finish”)。
現在考慮網格中有障礙物。那麼從左上角到右下角將會有多少條不同的路徑?
解題思路:
代碼:
動態規劃:自己動手,豐衣足食
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
if(obstacleGrid == null || obstacleGrid[0][0] == 1)
return 0;
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
if(obstacleGrid[m-1][n-1] == 1)
return 0;
int[] dp = new int[n];
for(int i=0; i < m; i++){
for(int j=0; j < n; j++){
if(i == 0){
if(obstacleGrid[0][j] == 1)
break;
else
dp[j] = 1;
}
else if(j == 0){
dp[j] = obstacleGrid[i-1][j] == 1 ? 0 : dp[j];
}
else
dp[j] = (obstacleGrid[i-1][j] == 1 ? 0 : dp[j]) + (obstacleGrid[i][j-1] == 1 ? 0 : dp[j-1]);
}
}
return dp[n-1];
}
4.三角形最小路徑和(Medium,題號:120)
第0次
題目鏈接:三角形最小路徑和
題目描述:
給定一個三角形,找出自頂向下的最小路徑和。每一步只能移動到下一行中相鄰的結點上。
例如,給定三角形:
解題思路:從頂向下地進行考慮,第一行2所對應的可以認爲是dp[0],3所對應的爲上一層的dp[0](也即到上一層3爲止的最短路徑和),4所對應的是上一層的dp[1](也即到上一層4爲止的最短路徑和),求其較小值加上2則得到頂層的dp[0]。
代碼:
public int minimumTotal(List<List<Integer>> triangle) {
int l = triangle.size();
int[] dp = new int[triangle.size()+1];
for(int i = l - 1; i >= 0 ;i--){
List<Integer> temp = triangle.get(i);
for(int j = 0; j < temp.size(); j++){
dp[j] = Math.min(dp[j],dp[j+1]) + temp.get(j);
}
}
return dp[0];
}
三、數組區間
1.區域和檢索 - 數組不可變(Medium)
第0次
題目鏈接:區域和檢索 - 數組不可變
題目描述:
給定一個整數數組 nums,求出數組從索引 i 到 j (i ≤ j) 範圍內元素的總和,包含 i, j 兩點
解題思路:
代碼:
我的垃圾代碼:擊敗了<10%的用戶
class NumArray {
private int[] array;
public NumArray(int[] nums) {
array = nums;
}
public int sumRange(int i, int j) {
int result = 0;
while(i <= j){
result = result + array[i];
i++;
}
return result;
}
參考代碼:擊敗了50%+的用戶
class NumArray {
private int[] sums;
public NumArray(int[] nums) {
sums = new int[nums.length + 1];
for (int i = 1; i <= nums.length; i++) {
sums[i] = sums[i - 1] + nums[i - 1];
}
}
public int sumRange(int i, int j) {
return sums[j + 1] - sums[i];
}
}
2.等差數列劃分(Medium,題號:413)
第0次
題目鏈接:等差數列劃分
題目描述:
等差數列定義:如果一個數列至少有三個元素,並且任意兩個相鄰元素之差相同,則稱該數列爲等差數列。
函數要返回數組 A 中所有爲等差數組的子數組個數。注:要求子數組元素下標連續。
解題思路:
代碼:
四、分割整數
1.整數拆分(Medium,題號:343)
第0次
題目鏈接:整數拆分
題目描述:
給定一個正整數 n,將其拆分爲至少兩個正整數的和,並使這些整數的乘積最大化。 返回你可以獲得的最大乘積。
解題思路:
解法一:記憶化搜索,遞歸方法的改進,自頂向下的解決方案;
解法二:動態規劃,自底向上;
代碼:
解法一:
public int integerBreak(int n) { //記憶化搜索
int[] dp = new int[n + 1];
return Break(n,dp);
}
public int Break(int n, int[] dp) {
if(n == 1)
return 1;
if(dp[n] != 0)
return dp[n];
int res = 0;
for (int j = 1; j <= n - 1; j++) {
res = Math.max(res, Math.max(j * Break(n-j,dp), j * (n - j)));
}
dp[n] = res;
return res;
}
解法二:
public int integerBreak(int n) { //動態規劃
int[] dp = new int[n + 1];
dp[1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= i - 1; j++) {
dp[i] = Math.max(dp[i], Math.max(j * dp[i - j], j * (i - j)));
}
}
return dp[n];
}
2.完全平方數(Medium,題號:279)
第0次
題目鏈接:完全平方數
題目描述:
給定正整數 n,找到若干個完全平方數(比如 1, 4, 9, 16, …)使得它們的和等於 n。你需要讓組成和的完全平方數的個數最少。
解題思路:
解法一:遞歸,超時;
解法二:記憶化搜索,自上而下;
解法三:動態規劃,自下而上;
代碼:
解法一:遞歸
public int numSquares(int n){
if(n == 0){
return 0;
}
if(n == 1){
return 1;
}
int count = Integer.MAX_VALUE;
for(int i = 1; i*i <= n; i++){
count = Math.min(count,numSquares(n - i*i) + 1);
}
return count;
}
解法二:記憶化搜索
public int numSquares(int n) {
int[] dp = new int[n+1];
return generate(n,dp);
}
public int generate(int n,int[] dp){
if(n == 0){
return 0;
}
if(n == 1){
return 1;
}
int count = Integer.MAX_VALUE;
if(dp[n] != 0)
return dp[n];
for(int i = 1; i*i <= n; i++){
count = Math.min(count,generate(n - i*i,dp) + 1);
}
dp[n] = count;
return count;
}
解法三:動態規劃
public int numSquares(int n){
int[] dp = new int[n+1];
if(n == 0){
return 0;
}
if(n == 1){
return 1;
}
for(int i = 1; i <= n; i++){
int min = Integer.MAX_VALUE;
for(int j = 1; j*j <= i; j++){
min = Math.min(min,dp[i - j*j] );
}
dp[i] = min + 1;
}
return dp[n];
}
3.解碼方法(Medium,題號:91)
第0次
題目鏈接:解碼方法
題目描述:
解題思路:
代碼:
五、0-1揹包問題
原始的揹包問題:
題目描述:有一個容量爲C的揹包,現在有n中不同的物品,編號爲0…n-1,其中每一件物品的重量爲w(i),價值爲v(i),問可以向這個揹包中放哪些物品,使得在不超過揹包容量的基礎上,物品的總價值最大?
解題思路:
遞歸:
狀態函數爲:F(i,c) = max(F(i-1 , c) , v(i) + F(i-1, c - w(i)))
記憶化搜索:定義二維記憶化數組dp[n][C+1],數組中每個元素都賦值爲-1,然後記憶化,遍歷到輸入參數n,C對應的dp數組元素值不是-1,則表示已經求得結果,不需要重複求結果,直接返回dp[n][C]即可;
動態規劃:記憶化搜索方法的改進,定義動態數組dp[n][C+1],dp[i][j]表示在揹包容量爲C時,數組w中截止到索引爲i的物品(一共 i+1個物品)判定是否放進揹包獲得的最大值,最後返回dp[n-1][C]表示揹包容量爲C,截止到索引爲n-1的物品是否放進揹包獲得的最大值。
代碼:
遞歸:
public static void main(String[] args) {
int[] v = {6,10,12};
int[] w = {1,2,3};
System.out.print(function(v,w,0,5));
}
public static int function(int[] v,int[] w, int n, int C) {
if(C <= 0 || n >= w.length)
return 0;
int res = Math.max(function(v,w,n+1,C) , (C-w[n] >= 0) ? v[n]+function(v,w,n+1,C-w[n]) : 0);
return res;
}
記憶化搜索:
public static int function(int[] v,int[] w, int n, int C,int [][] dp) {
if(C <= 0 || n >= w.length)
return 0;
if(dp[n][C] != -1)
return dp[n][C];
int res = Math.max(function(v,w,n+1,C,dp) , (C-w[n] >= 0) ? v[n]+function(v,w,n+1,C-w[n],dp) : 0);
dp[n][C] = res;
return res;
}
動態規劃:
二維數組-動態規劃:從前往後遍歷
public static int function(int[] v,int[] w, int C) { //二維數組-動態規劃
int n = w.length;
int[][] dp = new int[n][C+1];
for(int i=0; i < n; i++) {
for(int j=0; j <=C; j++) {
if(i == 0) {
dp[i][j] = (w[i] <= j) ? v[i] : 0;
}
else if(j == 0) {
dp[i][j] = 0;
}
else
dp[i][j] = Math.max(dp[i-1][j], ((j-w[i] >= 0) ? v[i] +dp[i-1][j-w[i]] : 0));
}
}
return dp[n-1][C];
}
一維數組-動態規劃:從後往前遍歷
public static int function(int[] v,int[] w, int C) { //一維數組-動態規劃
int n = w.length;
int[] dp = new int[C+1];
for(int i=0; i < n; i++) {
for(int j=C; j >= w[i]; j--) {
if(i == 0) {
dp[j] = (w[i] <= j) ? v[i] : 0;
}
else
dp[j] = Math.max(dp[j], v[i] +dp[j-w[i]]);
}
}
return dp[C];
}
1.分割等和子集(Medium,題號:416)
第2次
題目鏈接:分割等和子集
題目描述:
給定一個只包含正整數的非空數組。是否可以將這個數組分割成兩個子集,使得兩個子集的元素和相等。
解題思路:
遞歸解法:
記憶化搜索:定義一個n*(c+1)的二維矩陣,初始值都賦值爲-1,然後利用遞歸的思路,記憶化地將dp二維矩陣填滿,最終返回dp[n][c],其值爲1則返回true,爲0則返回false;
動態規劃(二維矩陣):記憶化搜索方案的改進,這時不需要定義int型的二維數組(爲了記憶,要標記爲-1),可以直接定義爲boolean類型的數組dp,dp[1,3]表示nums數組中的前2個數字裝入揹包容量爲3的揹包中,是否剛好裝滿不浪費空間,爲採用雙層for循環,動態函數爲:
動態規劃(一維矩陣):在二維數組的基礎上進行修改,這裏第二層for循環需要從j = c往前遍歷,因爲使用一維數組時需要用到之前的dp[j],這時如果從0往後遍歷的話,會用到當前外層所得到的dp[j],也就是說此時變成用當前層較小的dp[j]得到較大的dp[j],則沒有意義,如果從後往前遍歷,則使用的是上一層的dp[j],才能代替二維矩陣,其動態函數爲:
代碼:
遞歸解法:
記憶化搜索:
public boolean canPartition(int[] nums) {
int n = nums.length;
if(n == 0 || n == 1 )
return false;
int sum = 0;
for(int i = 0; i < nums.length; i++){
sum = sum + nums[i];
}
if(sum%2 != 0)
return false;
int[][] dp = new int[n][sum/2+1];
for(int i=0 ; i < n; i++)
Arrays.fill(dp[i],-1);
return Partition(nums,n-1,sum/2,dp);
}
public boolean Partition(int[] nums,int n,int c,int[][] dp){
if(n < 0 || c <=0 )
return false;
if(nums[n] == c)
return true;
if(dp[n][c] != -1)
return dp[n][c] == 1 ? true : false;
boolean result = Partition(nums,n-1,c,dp);
if(c >= nums[n])
result = (Partition(nums,n-1,c-nums[n],dp) || result);
dp[n][c] = result == true ? 1 : 0;
return result;
}
動態規劃(二維矩陣):
public boolean canPartition(int[] nums) {
int n = nums.length;
if(n <= 1)
return false;
int sum = 0;
for(int i=0; i < n; i++)
sum = sum + nums[i];
if(sum%2 != 0)
return false;
int c = sum/2;
boolean[][] dp = new boolean[n][c+1];
for(int i=0; i < n; i++){
for(int j=0; j <= c; j++){
if(i == 0)
dp[i][j] = (nums[i] == j);
else if(j >= nums[i])
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];
else
dp[i][j] = dp[i-1][j];
}
}
return dp[n-1][c];
}
動態規劃(一維矩陣):
public boolean canPartition(int[] nums) {
int n = nums.length;
if(n <= 1)
return false;
int sum = 0;
for(int i=0; i < n; i++)
sum = sum + nums[i];
if(sum%2 != 0)
return false;
int c = sum/2;
boolean[] dp = new boolean[c+1];
for(int i=0; i < n; i++){
for(int j=c; j > 0; j--){
if(i == 0)
dp[j] = (nums[i] == j);
else if(j >= nums[i])
dp[j] = dp[j] || dp[j-nums[i]];
else
dp[j] = dp[j];
}
}
return dp[c];
}
2.目標和(Medium,題號:494)
第0次
題目鏈接:目標和
題目描述:
給定一個非負整數數組,a1, a2, …, an, 和一個目標數,S。現在你有兩個符號 + 和 -。對於數組中的任意一個整數,你都可以從 + 或 -中選擇一個符號添加在前面。
返回可以使最終數組和爲目標數 S 的所有添加符號的方法數。
解題思路:
遞歸:暴力解法,竟然能通;
動態規劃:
二維數組:
一維數組:
代碼:
遞歸:
public int findTargetSumWays(int[] nums, int S) {
if(nums == null)
return 0;
return TargetSumWays(nums,0,S);
}
public int TargetSumWays(int[] nums,int i, int S){
if(i == nums.length && S == 0)
return 1;
else if(i == nums.length && S != 0)
return 0;
return TargetSumWays(nums,i+1,S-nums[i]) + TargetSumWays(nums,i+1,S+nums[i]);
}
動態規劃:
二維數組:
public int findTargetSumWays(int[] nums, int S) {
if(nums == null)
return 0;
int n = nums.length;
int sum = 0;
for(int i=0; i < nums.length; i++){
sum = sum + nums[i];
}
int c = (sum + S)/2;
if(S > sum || (sum + S)%2 == 1)
return 0;
int[][] dp = new int[n][c+1];
if(nums[0] == 0)
dp[0][0] = 2;
else
dp[0][0] = 1;
for(int i=1;i<=c;i++){
if(nums[0]==i){
dp[0][i]=1;
break;
}
}
for(int i=1; i < n; i++){
for(int j=0;j <= c ; j++){
if(i == 0){
dp[i][j] = (j == 0 || j == nums[i]) ? 1 : 0;
}
if(j >= nums[i])
dp[i][j] = dp[i-1][j-nums[i]] + dp[i-1][j];
else
dp[i][j] = dp[i-1][j];
}
}
return dp[n-1][c];
}
一維數組:
public int findTargetSumWays(int[] nums, int S) {
if(nums == null)
return 0;
int n = nums.length;
int sum = 0;
for(int i=0; i < nums.length; i++){
sum = sum + nums[i];
}
int c = (sum + S)/2;
if(S > sum || (sum + S)%2 == 1)
return 0;
int[][] dp = new int[n][c+1];
dp[0] = 1;
for(int i=1; i < n; i++){
for(int j=c;j >= nusm[i]; j--){
dp[i][j] = dp[i-1][j-nums[i]] + dp[i-1][j];
}
}
return dp[c];
}
3.一和零(Medium,題號:474)
第1次
題目鏈接:一和零
題目描述:
在計算機界中,我們總是追求用有限的資源獲取最大的收益。
現在,假設你分別支配着 m 個 0 和 n 個 1。另外,還有一個僅包含 0 和 1 字符串的數組。
你的任務是使用給定的 m 個 0 和 n 個 1 ,找到能拼出存在於數組中的字符串的最大數量。每個 0 和 1 至多被使用一次。
解題思路:
動態規劃:s定義類似於三維矩陣的二維矩陣,作爲動態數組,橘黃色表示的是將原始數組中第i個物品放入揹包的總數,其與上一個dp[j][k](即未放入揹包的總數)進行比較,取較大者
代碼:
動態規劃:
public int findMaxForm(String[] strs, int m, int n) {
int l = strs.length;
int[][] dp = new int[m+1][n+1];
/*冗餘代碼
int a = count(strs[0],'0');
int b = count(strs[0],'1');
for(int i=0; i <= m; i++){
for(int j=0; j <= n;j++){
if(i >= a && j >= b){
dp[i][j] = 1;
}
}
}
*/
for(int i = 0; i < l; i++){
int m1 = count(strs[i],'0');
int n1 = count(strs[i],'1');
for(int j = m; j >= m1; j--){
for(int k = n; k >= n1; k--)
dp[j][k] = Math.max(dp[j-m1][k-n1] + 1,dp[j][k]);
}
}
return dp[m][n];
}
public int count(String s, char x){
int cnt = 0;
for(int i=0; i < s.length(); i++){
if(s.charAt(i) == x)
cnt++;
}
return cnt;
}
4.零錢兌換(Medium,題號:322)
第0次
題目鏈接:零錢兌換
題目描述:
給定不同面額的硬幣 coins 和一個總金額 amount。編寫一個函數來計算可以湊成總金額所需的最少的硬幣個數。如果沒有任何一種硬幣組合能組成總金額,返回 -1。
解題思路:
代碼:
動態規劃:
public int coinChange(int[] coins, int amount) {
if (amount == 0 || coins == null || coins.length == 0) {
return 0;
}
int[] dp = new int[amount+1];
for(int coin : coins){
for(int j = 0; j <= amount; j++){
if(j < coin)
dp[j] = dp[j];
else if(j == coin)
dp[j] = 1;
else if(dp[j] != 0 && dp[j - coin] != 0)
dp[j] = Math.min(dp[j],dp[j - coin]+1);
else if(dp[j - coin] != 0)
dp[j] = dp[j - coin]+1;
}
}
return dp[amount] == 0 ? -1 : dp[amount];
}
代碼優化:可以直接讓i = coin然後就不需要執行if(j < coin)了
5.零錢兌換 II(Medium,題號:322)
第0次
題目鏈接:零錢兌換 II
題目描述:
給定不同面額的硬幣和一個總金額。寫出函數來計算可以湊成總金額的硬幣組合數。假設每一種面額的硬幣有無限個。
解題思路:
代碼:
動態規劃:
從後往前(我的代碼):垃圾
public int change(int amount, int[] coins) {
if(amount == 0)
return 1;
int[] dp = new int[amount+1];
for(int i=0; i < coins.length; i++){
for(int j = amount; j >= coins[i]; j--){
if(i == 0)
dp[j] = (j % coins[i] == 0 ? 1 : 0);
else {
int k = j;
int temp = 0;
while(k >= coins[i]){
k = k - coins[i];
temp +=dp[k];
if(k == 0)
temp++;
}
dp[j] = dp[j] + temp;
}
}
}
return dp[amount];
}
從前往後(參考代碼):暫時不通
public int change(int amount, int[] coins) {
if (amount == 0 || coins == null || coins.length == 0) {
return 0;
}
int[] dp = new int[amount + 1];
dp[0] = 1;
for (int coin : coins) {
for (int i = coin; i <= amount; i++) {
dp[i] += dp[i - coin];
}
}
return dp[amount];
}
六、最長遞增子序列
1.最長上升子序列(Medium,題號:300)
第1次
題目鏈接:最長上升子序列
題目描述:
給定一個無序的整數數組,找到其中最長上升子序列的長度。
解題思路:
代碼:
2.最長數對鏈(Medium,題號:646)
第0次
題目鏈接:最長數對鏈
題目描述:
給出 n 個數對。 在每一個數對中,第一個數字總是比第二個數字小。
現在,我們定義一種跟隨關係,當且僅當 b < c 時,數對(c, d) 纔可以跟在 (a, b) 後面。我們用這種形式來構造一個數對鏈。
給定一個對數集合,找出能夠形成的最長數對鏈的長度。你不需要用到所有的數對,你可以以任何順序選擇其中的一些數對來構造。
解題思路:先按照數對的第二個數字進行升序排序,得到的一個數對序列,最後進行查找最長字數對時,基本的順序不會改變,只會要某一個數對或者不要某個數對,而不會做順序的調整,這樣就可以轉化爲一個揹包問題,利用上題(1.最長上升子序列)來進行處理即可;
代碼:
public int findLongestChain(int[][] pairs) {
if(pairs == null || pairs.length == 0)
return 0;
int n = pairs.length;
int[] dp = new int[n];
Arrays.sort(pairs, (a, b) -> (a[0] - b[0])); //按照數對的第1個數進行排序,若改成(a[1] - b[1]),則按照數對的第二個數進行升序排序
Arrays.fill(dp,1);
for(int i=0; i < n; i++){
for(int j=0; j < n ; j++){
if(pairs[i][0] > pairs[j][1] && j != i){
dp[i] = Math.max(dp[i],dp[j] + 1);
}
}
}
int temp = dp[0];
for(int i=0; i < n; i++){
if(temp < dp[i])
temp = dp[i];
}
return temp;
}
3.擺動序列(Medium,題號:376)
第1次
題目鏈接:擺動序列
題目描述:
如果連續數字之間的差嚴格地在正數和負數之間交替,則數字序列稱爲擺動序列。第一個差(如果存在的話)可能是正數或負數。少於兩個元素的序列也是擺動序列。
例如, [1,7,4,9,2,5] 是一個擺動序列,因爲差值 (6,-3,5,-7,3) 是正負交替出現的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是擺動序列,第一個序列是因爲它的前兩個差值都是正數,第二個序列是因爲它的最後一個差值爲零。
給定一個整數序列,返回作爲擺動序列的最長子序列的長度。 通過從原始序列中刪除一些(也可以不刪除)元素來獲得子序列,剩下的元素保持其原始順序。
解題思路:擺動序列也就是後一個數減去前一個數的結果的符號一直是正負交替出現,解該題的關鍵在於在遇到連續上升或者連續下降的部分時,dp結果保持不變,也就是dp[i] = dp[i-1],這裏需要使用標記變量fp和fn,利用for循環,從數組第二個數開始遍歷,最後返回dp[n-1];也可以採用兩個變量up和down代替動態數組,遇到連續上升或者下降部分時,所得到的的up和down的值保持不變,此時不需要再使用標記fp和fn了。
代碼:
我的代碼:
public int wiggleMaxLength(int[] nums) {
if(nums.length == 0)
return 0;
int n = nums.length;
boolean fp = true;
boolean fn = true;
int[] dp = new int[n];
dp[0] = 1;
for(int i=1; i < n; i++){
if(nums[i] - nums[i-1] > 0 && fp){
dp[i] = dp[i-1] + 1;
fp = false;
fn = true;
}
else if(nums[i] - nums[i-1] < 0 && fn){
dp[i] = dp[i-1] + 1;
fp = true;
fn = false;
}
else {
dp[i] = dp[i-1];
}
}
return dp[n-1];
}
參考的優秀代碼:
public int wiggleMaxLength(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int up = 1, down = 1;
for (int i = 1; i < nums.length; i++) {
if (nums[i] > nums[i - 1]) {
up = down + 1;
} else if (nums[i] < nums[i - 1]) {
down = up + 1;
}
}
return Math.max(up, down);
}