LeetCode(动态规划模块 20题) 持续更新

第一题

在这里插入图片描述
这里求数字 n 的二进制数中1的个数的方法巧妙。

class Solution {
    public int[] countBits(int num) {
        int[] res = new int[num+1];
        for(int i = 0 ; i <= num ; i++)
        {
            res[i] = popcount(i);
        }
        return res;
    }
    public int popcount(int x){
        int count;
        for(count = 0 ; x != 0 ; count++)
        {
            x &= x-1;
        }
        return count;
    }
}

第二题

在这里插入图片描述思路是leetcode官网题解:
在这里插入图片描述

class Solution {
    public String longestPalindrome(String s) {
        int n = s.length();
        boolean[][] dp  = new boolean[n+1][n+1];
        String res = "";
        for(int l = 0 ; l < n ; l++)
        {
            for(int i = 0 ; i + l < n ; i++)
            {
                int j = i+l;
                if(j >= n)
                {
                    break;
                }
                if(l == 0)
                {
                    dp[i][j] = true;
                }else if(l == 1)
                {
                    dp[i][j] = (s.charAt(i) == s.charAt(j));
                }else
                {
                    dp[i][j] = (s.charAt(i) == s.charAt(j)) && dp[i+1][j-1];
                }
                if(dp[i][j] && l+1 > res.length())
                {
                    res = s.substring(i,j+1);
                }
            }
        }
        return res;
    }
}

第三题

在这里插入图片描述
方法一:暴力法
枚举所有的可能。

class Solution {
    public boolean checkSubarraySum(int[] nums, int k) {
        int len = nums.length;
        for(int start = 0 ; start < len-1 ; start++)
        {
            for(int end = start+1 ; end < len ; end++)
            {
                int sum = 0;
                for(int temp = start ; temp <= end ; temp++)
                {
                    sum += nums[temp];
                }
                if(sum == k || (k != 0 && sum % k == 0))
                    return true;
            }
        }
        return false;
    }
}

第四题

在这里插入图片描述
上第 n 阶台阶的方法 就等于 = 上(n-1) + 上(n-2) + 上(n-3)台阶的方法之和
所以状态转移方程为: dp[n] = dp[n-1] + dp[n-2] + dp[n-3]
下边取模的时候也需要仔细分析。
取模需要先对三个数中较大的两个数之和进行取模,再对三个数之和取模。
如果直接对每个数取模,假如每个数都距离1000000007 很近,取模之后不变,但是相加之后溢出,结果就为符数
如果直接对三个数之和取模,则三个数之和可能就直接会溢出

class Solution {
    public int waysToStep(int n) {
    int[] dp = new int[n+1];
        
        if(n == 1)
            return 1;
        if(n == 2)
            return 2;
        if(n == 3)
            return 4;
                    dp[1] = 1;
        dp[2] = 2;
        dp[3] = 4;
        // 状态转移方程: dp[n] = dp[n-1] + dp[n-2] + dp[n-3] ;
        for(int i = 4 ; i <= n ; i++)
        {
            dp[i] = (dp[i-1] + (dp[i-2] + dp[i-3]) % 1000000007 ) % 1000000007 ;
        }
        
        return dp[n];
    }
}

第五题

在这里插入图片描述

解题思路:
遍历 s 字符串每一个字符,使用 i 指针指向字符串 t,如果在s中有一个字符在t中找不到的话,那么i指针会一直++,直到不符合while条件(也就是i = t.length),这时候后边再加一次,就会超出 t 的长度,所以s也就不是t的子串。

class Solution {
    public boolean isSubsequence(String s, String t) {
        int i = 0;
        for(char c : s.toCharArray())
        {
            while(i < t.length() && c != t.charAt(i))
            {
                i++;
            }
            i++; // 无论 c 和 t.charAt(i) 是否相等,i都要++,i要向后遍历字符串t
        }
        return i <= t.length() ? true : false;
    }
}

第六题

在这里插入图片描述
记忆化搜索和动态递归
记忆化搜索:自顶向下
动态递归:自底向上

使用记忆化搜索解法

class Solution {
    static int[] memo ; // 添加一个数组进行记忆化搜索
    public int integerBreak(int n) {
        memo = new int[n+1];
        return breakInteger(n);
    }

    public int breakInteger(int n){
        // 1 递归终止条件
        if(n == 1)
            return 1; // n 为1的话,已经无法分割,返回1

        if(memo[n] != 0)
        {
            return memo[n];  // 进行记忆化搜索,如果已经有值,就直接返回
        }

        int res = -1;
        // 2 for循环找分割为两部分的乘积最大值
        for(int i = 1 ; i <= n-1 ; i++)
        {
            // 3 这里有三种结果进行比较
            //        1)res本身  2)不对n-i再进行分割 3) 对n-i再进行分割,找n-i的最大乘积
            res = max3(res , i * (n-i) , i * breakInteger(n-i) );
        }
        memo[n] = res;   // 记忆化搜索赋值
        return res;
    }

    public int max3(int a,int b,int c){
        return Math.max(Math.max(a,b),c);
    }
}

使用动态规划解法

class Solution {
    public int integerBreak(int n) {
        // 1 定义动态规划数组
        int[] dp = new int[n+1];
        dp[1] = 1;
        for(int i = 2 ; i <= n ; i++)
             for(int j = 1 ; j <= i-1 ; j++)                
                 // 2 分割为  j  i-j
                 dp[i] = max3(dp[i] , j * (i-j) , j * dp[i-j]);
       
       return dp[n];
   }
   public int max3(int a,int b,int c){
	return Math.max(Math.max(a,b),c);
   }
}

第七题

在这里插入图片描述
解题思路:
状态转移方程: dp[i] = min( dp[i] , dp[i - k] + 1 );
找一个数 n , 组成 n 的平方数的最小的个数,那么只需要知道 n 减去一个平方数之后,组成这个数的平方数的最小个数 ,最后再加一即可!

class Solution {
    public int numSquares(int n) {
        
        // dp[i] = min(dp[i-k] + 1);
        int[] dp = new int[n+1];
        // 给dp数组赋值max_value
        Arrays.fill(dp, Integer.MAX_VALUE);
	
	// 求出n以下所有平方数,存放在数组中,方便遍历
	int square = (int) Math.sqrt(n) + 1;
	int[] squares = new int[square];

	for(int i = 1 ; i < square ; i++)
	{
	    squares[i] = i * i;
	}
	//记得给 dp[0] 赋值
	dp[0] = 0;
	for(int i = 1 ; i <= n ; i++)
	{
	    for(int j = 1 ; j < square ; j++)
	    {
	        if(i < squares[i])
	        	break;
	        dp[i] = Math.min(dp[i] , dp[i-squares[j]] + 1);
	    }
	}
	return dp[n];	
	}
}

第八题

如果看到动态规划题目,没有思路的话,不妨来使用暴力法来有个大致的思路。

在这里插入图片描述

使用记忆化搜索

class Solution {
    static int[] memo ;
    public int rob(int[] nums) {
        int n = nums.length;
        memo = new int[n];
        return tryRob(nums,0);
    }
    public int tryRob(int[] nums , int index){
  	 if(index >= nums.length)
	      return 0;
	 
	 //使用记忆化搜索
	 if(memo[index] != 0)
	 {
	     return memo[index];
	 }
	 
	 int res = 0;
	 for(int i = index ; i < nums.length ; i++)
	 {
	     //  偷取 nums[i] 并考虑偷取 nums[i+2 ... n-1] 范围内
	     res = Math.max( res , nums[i] + tryRob(nums,i+2) );
	 }
	 // 给记忆化数组赋值
	 memo[index] = res;
	 return res;
    }

使用动态规划:

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if(n == 0)
        {
            return 0;
        }
        // dp[i] 表示偷取  i ... n-1 范围内的最多的钱
        int[] dp = new int[n];
        dp[n-1] = nums[n-1];// 偷取 n-1 ... n-1范围内最多的钱就是偷取第n-1个房子
	
	for(int i = n-2 ; i >= 0 ; i--)
	    for(int j = i ; j < n ; j++)
	        // 原式:dp[i] = max(dp[i] , nums[j] + dp[j+2])
                //                            偷取 j 并考虑偷取 j+2~n-1 的最多的钱
                //   但是因为 dp[j+2]  j+2可能会大于等于n,发生数组越界,所以使用三元运算符判断一下是否越界
                // dp[i] = nums[j] + dp[j+2],但是dp[i]所有遍历的结果中取最大的,所以就也和自己比较就成为了
                // dp[i] = max(dp[i] , nums[j] + dp[j+2])
	        dp[i] = Math.max( dp[i] , nums[j] + ( j+2>=n ? 0 : dp[j+2] ) );

  	return dp[0];
    }
}

第九题

在这里插入图片描述
第i个台阶只与第i-1和i-2个台阶有关,这里注意返回值!

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int n = cost.length;
        int[] dp = new int[n];
        dp[0] = cost[0];
        dp[1] = cost[1];
        for(int i = 2 ; i < n ; i++)
        {
            dp[i] = cost[i] + Math.min(dp[i-1],dp[i-2]);
        }
        return Math.min(dp[n-1] , dp[n-2]);

第十题

简单 0-1背包问题

背包容量C,数组w表示物品大小,数组v表示物品价值,要求在背包中放入最大价值的东西。

递归解法

class Solution{
	public static int main(int[] w,int[] v,int C){
	    int n = w.length();
	    // 0....n-1 件物品最大价值
	    return bestValue(w , v , C , n-1);
	}
	public static int bestValue(int[] w,int[] v,int c,int index){
	    if(index < 0 || c <=  0)
	    {
	         return 0; //如果没有物品选择 或者 容量小于等于0了,那么返回的价值就为0
	    }
	    int res = bestValue(w,v,c,index-1);//不选择
	    if(c >= w[index])
	    {
	        res = Math.max( res , v[index] + bestValue(w,v,c-w[index],index-1) );
	    }
	    return res;
	}
}

第十一题

在这里插入图片描述
解题思路:
如何去寻找上升子序列呢?
遍历数组,使用 dp[i] 记录到 0…i的最长上升子序列的长度。
寻找 nums[i] 之前的数,如果比nums[i] 小,则 nums[i] 就可以跟在其后,所以上升子序列长度+1

class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        int[] dp =  new int[n];
        /*
           动态递归:
             dp[j] = 1+dp[i](if nums[j]>nums[i])
        */
        fill(dp , 1);
        for(int i = 1 ; i < n ; i++)
            for(int j = 0 ; j < i ; j++)
                if(nums[j] < nums[i])
                    dp[i] = Math.max( dp[i] , dp[j] + 1 );
        int res = 0;
        for(int i = 0 ; i < n ; i++)
            res = Math.max(res , dp[i]);
        return res;
    }
    public static void fill(int[] a , int target){
        for(int i = 0 ; i < a.length ; i++)
        {
            a[i] = target ;
        }
    }
}

第十二题

在这里插入图片描述

记忆化搜索
要将数组分成相等的两部分,只需要找到符合 sum/2 的子集即可。

class Solution {
    public int[][] memo ;
    public boolean canPartition(int[] nums) {
        int n = nums.length;
        int sum = 0;
        for(int i = 0 ; i < n ; i++)
            // 先求出总和,找到满足sum/2的子集即可
            sum += nums[i];
        if(sum % 2 != 0)
            // 如果和是奇数,则不能分割
            return false;
        // 初始化数组
        memo = new int[n][sum/2+1];
        // 把数组都赋值为 -1
        // 这里为什么使用int类型的数组呢?因为我们用到记忆化搜索还要表示未计算的数
        // -1:未计算 0:false 1:true
        fill(memo , -1);
        return check(nums,n-1,sum/2);
    }
    public void fill(int[][] nums , int target){
        for(int i = 0 ; i < nums.length ; i++){
            for(int j = 0 ; j < nums[0].length ; j++)
            {
                nums[i][j] = target;
            }
        }
    }
    public boolean check(int[] nums,int index,int sum){
        //如果总和为0,返回true
        if(sum == 0)
            return true;
        //如果总和小于0 或 index小于0,找不到符合条件的子集,返回false
        if(sum < 0 || index < 0)
        {
            return false;
        }
        //使用记忆化搜索
        if(memo[index][sum] != -1)
        {
            return memo[index][sum] == 1 ? true : false ;
        }
        memo[index][sum] = ( check(nums , index-1 , sum ) 
            || check(nums , index-1 , sum-nums[index]) ) == true ? 1 : 0;
        return memo[index][sum] == 1 ? true : false;
    }
}

动态规划解法:

class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for(int i = 0 ; i < nums.length ; i++)
        {
            sum += nums[i];
        }
        if(sum % 2 != 0)
        {
            return false;
        }
        int C = sum/2 ; //用C表示背包的容量
        int n = nums.length; //用n表示数组的长度
	    //定义一个数组,dp[i]代表容量为i时,背包是否可以被放满
        boolean[] dp = new boolean[C+1];
        for(int i = 0 ; i <= C ; i++)
        {
            // 先对数组进行初始化
            // 看看把数组中的第一个数放进去可不可以满足
            dp[i] = ( nums[0] == i );
        }
        // 因为已经对第一个数进行初始化,所以i从1开始
        for(int i = 1 ; i < n ; i ++)
        {
            for(int j = C ; j > nums[i] ; j--)
            {
                dp[j] = dp[j] || dp[j-nums[i]];
            }
        }
        // 最后返回 dp[C],也就是容量为C时,是否可以放满
        return dp[C];
        
    }
}

第十三题

在这里插入图片描述

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {

        /*
            采用自下向上解法:数组可以看作一下形式
           [
            [2],
            [3,4],
            [6,5,7],
            [4,1,8,3]
            ]
                                        从下边上来较小的数 + 本位置的数
            状态转移方程: dp[i][j] = min(dp[i+1][j],dp[i+1][j+1]) + rows.get[j];
        */

        int n = triangle.size();
        int[][] dp = new int[n+1][n+1];
        
        for(int i = n-1 ; i >= 0 ; i--)
        {
            //从下到上,获取每一行
            List<Integer> rows = triangle.get(i);
            for(int j = 0 ; j < rows.size() ; j++)
            {
                //遍历每一行中的元素
                dp[i][j] = Math.min( dp[i+1][j] , dp[i+1][j+1] ) + rows.get(j);
            }
        }
        return dp[0][0];
    }
}

第十四题

在这里插入图片描述

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        // 特判只有一个格子的情况
        if(m == 1 && n == 1)
        {
            int res = obstacleGrid[0][0] == 1 ? 0 : 1;
            return res;
        }
        // 如果起点有障碍物,返回0
        if(obstacleGrid[0][0] == 1)
            return 0;

        // 行和列各加 1 ,起点从[1][1]开始计算
        int[][] dp = new int[m+1][n+1];
        
        for(int i = 1 ; i <= m ; i++)
        {
            for(int j = 1 ; j <= n ; j++)
            {
                dp[1][1] = 1;
                // 如果有障碍物,就设置为0,不能走,并跳过循环 
                if(obstacleGrid[i-1][j-1] == 1)
                {
                    dp[i][j] = 0;
                    continue;
                }
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        dp[1][1] = 1;
        return dp[m][n];

    }
}

第十五题

在这里插入图片描述
俄罗斯套娃

class Solution {
    public int maxEnvelopes(int[][] envelopes) {
        Arrays.sort(envelopes,new Comparator<int[]>(){
            public int compare(int[] arr1 , int[] arr2){
                if(arr1[0] == arr2[0])
                { 
                    //如果宽相等,按照高从大到小排序
                    return arr2[1] - arr1[1];
                }else
                {
                    //按照宽从小到大排序
                    return arr1[0] - arr2[0];
                }
            }
        });
        /*
		    先按照宽,从小到大排序
		    (但是有一种复杂的情况,如果宽相等的话,即使高相差较多,也无法放入)
		    所以,就需要考虑宽相等的情况
		    当宽相等时,把高按照从大到小的顺序排序
		    最后结果就是求高的最长上升子序列。
		*/
        int n = envelopes.length;
        int[] res = new int[n];
        for(int i = 0 ; i < n ; i++)
        {
            res[i] = envelopes[i][1];
        }
        return lengthOfLIS(res);
    }
    // 求最长上升子序列
    public int lengthOfLIS(int[] nums){
        int n = nums.length;
        int[] dp = new int[n];
	 	// 将数组初始化为1
        fill(dp,1);

        for(int i = 1 ; i < n ; i++)
        {
            for(int j = 0 ; j < i ; j++)
            {
                if(nums[j] < nums[i])
                {
                    dp[i] = Math.max( dp[i] , dp[j]+1 );
                }
            }
        }
        int res = 0;
        for(int i = 0 ; i < n ; i++)
        {
            res = Math.max(res,dp[i]);
        }
        return res;
    }
    public void fill(int[] nums , int target){
        for(int i = 0 ; i < nums.length ; i++)
        {
            nums[i] = target;
        }
    }
}

十六题

在这里插入图片描述

class Solution {
    public int[] countBits(int num) {
        int[] f = new int[num+1];
        f[0] = 0;
        for(int i = 1 ; i <= num ; i++)
        {
            //  f[2] = [0,1,1]
            //  将每个数和 1 与,并除以2 也就是(i>>1)
            f[i] = f[i >> 1] + i&1;
        }
        return f;
    }
}

第十七题

错误代码

class Solution {
    public int waysToChange(int n) {

        int[] coins = new int[]{1,5,10,25};

        int[] dp = new int[n+1];

        dp[0] = 1;

        for(int i = 1 ; i <= n ; i++)
        {
            for(int coin : coins)
            {
            	/*
				  i = 1  dp[1] = dp[1] + dp[0] = 1
				  i = 2  dp[2] = dp[2] + dp[2 - 1] = 1
				  i = 3  dp[3] = dp[3] + dp[3-1] = 1
				  i = 4  dp[4] = dp[4] + dp[4-1] = 1
				  i = 5  dp[5] = dp[5] + dp[5-1] = 1
				         dp[5] = dp[5] + dp[5-5] = 2
				         
				  i = 6  dp[6] = dp[6] + dp[6-1] = 2
				         dp[6] = dp[6] + dp[6-5] = 3
				  
				*/
                if(i-coin < 0)    break;
                dp[i] = (dp[i] + dp[i-coin]) % 1000000007;
            }
        }
        return dp[n];
    }
}

上述代码错在哪里呢?

我们来看一下dp[6]的情况,也就是组成6元硬币的情况
coin = 1:
dp[6] = dp[6] + dp[5] 也就是 0 + dp[5] = 2
coin = 5:
dp[6] = dp[6] + dp[1] 也就是 2 + dp[1] = 3
所以组成6元硬币的情况有3种
但是实际上只有两种[1,1,1,1,1,1] 和 [1,5]
我们计算出来了3中是因为把[1,5] 和 [5,1]重复计算了

正确代码

class Solution {
    public int waysToChange(int n) {

        int[] coins = new int[]{1,5,10,25};

        int[] dp = new int[n+1];

        dp[0] = 1;

        for(int coin : coins)
        {
            //对代码进行改进
            for(int i = coin ; i <= n ; i++)
            {
                dp[i] = (dp[i] + dp[i-coin]) % 1000000007;
            }
        }
        return dp[n];
    }
}

猜你喜欢

转载自blog.csdn.net/qq_45260619/article/details/106238928