详细动态规划-一维二维力扣例题

动态规划思路:

1、定义子问题

原问题可由子问题解出来,子问题可有其他子问题解出来“最优子结构”

2、数组的含义

eg:dp[i]表示什么样的子问题的结果

3、计算顺序/状态转移方程

判断到某一个子问题,只有两种可能,要或不要(例如背包问题),动态规划计算顺序有:

-自顶向下,备忘录;

-自底向上,使用dp数组(大部分)

-自底向上(Bottom-up)方法是从‘问题的最小规模’开始,逐步解决较大规模的子问题,直到解决整个问题。这种方法通常使用迭代的方式,将子问题的解保存起来,以供后续使用。自底向上方法的优点是避免了重复计算,因为每个子问题只计算一次。典型的例子是斐波那契数列的计算,可以从最小的斐波那契数开始,逐步计算得到较大的斐波那契数。

-自顶向下(Top-down)方法是从‘问题的整体’出发,将问题分解为较小的子问题,然后递归地解决这些子问题,直到解决最小规模的子问题。这种方法通常使用递归的方式,每次解决一个子问题时,如果该子问题的解已经计算过,则直接使用已经计算的结果。自顶向下方法的优点是可以更直观地理解问题的结构和解决思路,但可能存在重复计算的问题。利用自顶向下的方法解决背包问题,可以通过递归的方式从整体问题开始,将问题划分为较小的子问题。在每个子问题中,可以选择放入当前物品或者不放入当前物品,然后递归地解决剩余的容量和剩余的物品。为了避免重复计算,可以使用一个记忆化数组来保存已经计算过的子问题的解。

状态转移,即子问题之间的递推关系【例如背包问题中Max(money[i]+res[i-1][w-weight[i]],res[i-1][w]);能装得下当前物品,就考虑是装还是不装,比较装和bu'zhuabuzhua那个情况下的值】

目的:利用历史记录,避免重复计算:历史记录存放:一维二维数组

4、空间优化

存在存放历史数据的数组部分数据到后面都不会再用了,造成空间浪费;

例如计算f(n)的时候只用到了f(n-1)和f(n-2)因而只需要两个变量,不断更迭改变存放子问题的解即可。

思路:

1、数组元素的含义?dp[i]

//注意,含义不同,遍历的时候界限也就可能不同

2、数组元素之间的关系式

例如:台阶问题中:d[n]=d[n-1]+d[n-2];

3、找初始值【不能分解的最小的问题】

d[1]=1,d[2]=2【初值要找齐全:不能根据公式2计算出来的都是初值】

例子:

1、二维存储leetcode64题最小路径和

分析:

 //1、数组存储的含义

d[i][j]存储从(0,0)到(i,j)的数字和[包括grid[i,j]]

  //2、数组元素之间的关系

只能从从左/右来:选择两个里面最小的

d[i][j]=Math.min(d[i-1][j],d[i][j-1])+grid[i][j];  

  //3、找初始值,不能分解的最小问题

一直向右走/向下走;无需判断

class Solution {
    public int minPathSum(int[][] grid) {
        int m=grid.length;
        int n=grid[0].length;
        if(m<=0||n<=0)
        {
            return 0;
        }
       // int result=0;
        //1、数组存储的含义
        //2、数组元素之间的关系
        //3、找初始值,不能分解的最小问题
        int[][] d=new int[m][n];//存储含义:记录最小和
        //result=(d[i-1][j]<d[i][j-1])?d[i-1]+grid[i][j]:d[i][j-1]+grid;
        //d[i][j]=result;
        //3、初始值
        d[0][0]=grid[0][0];
        for(int i=1;i<m;i++)
        {
             d[i][0]=d[i-1][0]+grid[i][0];
        }
        for(int j=1;j<n;j++)
        {
            d[0][j]=d[0][j-1]+grid[0][j];
        }
        for(int i=1;i<m;i++)
        {
            for(int j=1;j<n;j++)
            {
                    d[i][j]=Math.min(d[i-1][j],d[i][j-1])+grid[i][j];                
            }
        }
        return d[m-1][n-1];

    }
}
2、leetcode198 打家劫舍
  • 思路

1、存储历史记录

一维数组d[i]存储直至当前房屋的金额总数

2、数组元素之间的关系

当前房屋i的金额等于max(房屋i-2,i-3)+i房屋放的金额;

3、最小问题解

d[0]=0;

d[1]=nums[0];

d[2]=max(nums[0],nums[1]);

class Solution {
public:
    int rob(vector<int>& nums) {
        int d[nums.size()];//直至走到该房屋偷取的最大金额
        d[0]=nums[0];
        if(nums.size()==0)
        return 0;
        else if(nums.size()==1)
        {
            return nums[0];
        }
        else if(nums.size()==2)
            return max(nums[0],nums[1]);
        else 
        {
            //子问题
            d[1]=nums[1];
            d[2]=nums[0]+nums[2];
            int i;
            for(i=3;i<nums.size();i++)
            {
                //元素之间的关系
                d[i]=max(d[i-2],d[i-3])+nums[i];
            }
            return max(d[i-1],d[i-2]);
        }
    }
};

这里注意得判断数组大小;

应用vector:

思路二:

class solution{
public:
    int rob(vector<int>& nums) {
        if (nums.empty())//可以用vector的函数!!
         {
            return 0;
        }
        int size = nums.size();
        if (size == 1) {
            return nums[0];
        }
        vector<int> dp = vector<int>(size, 0);//
        dp[0] = nums[0];
        dp[1] = max(nums[0], nums[1]);
        for (int i = 2; i < size; i++) {
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);//不同的思路:当前i等于max(i-1,i-2+当前nums.i)
}
        return dp[i-1];
}
class Solution {
    public int rob(int[] nums) {
        if(nums.length==0)
        {
            return 0;
        }
        int pre=0;//没有房子
        int cur=nums[0];//只有一家,偷了
        int len=nums.length;
        for(int k=1;k<len;k++)//从第二家房子开始考虑
        {
           int temp=Math.max(cur,pre+nums[k]);
           pre=cur;
           cur=temp;
        }
        return cur;//循环结束的时候,cur表示到最后一个房子偷盗的最大值;



    }
}

3、 一维中等难度leetcode139单词拆分(全排列问题)

 思路:

1、bool d[s.size()]表示字符串的长度为s.size()时,能否被列表中的字符串拼接起来

2、遍历字符串,长度为i的字符串,当列表中存在长度为j的元素,满足i>=j时,说明i长度的字符串可能被拼接,所以要进一步考虑能够拼接成功,所以,去遍历列表,存在长度为j的元素(i>=j)且元素j=s.substr(i-j,j);同时s[0:i-j-1]部分也能被拼接成功的话说明d[i]=true;

即:d[i]=(d[i-j]&&s.substr(i-j,j)==elem);elem为长度为j的列表元素;

3、子问题

当字符串长度为0时,d[0]=true;

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
           //int l=wordDict.size();//元素个数
           //bool d[L+1];
           
            int i=1;
            vector<bool> d(s.size()+1,false);
            d[0]=true;
            //
            for(int i=1;i<=s.size();i++)//背包
            {
                for(string elem:wordDict)//物品
                {
                    int l2=elem.size();
                    if(i>=l2)//能放得下
                    {
                         if(d[i]=(d[i-l2]&&(s.substr(i-l2,l2)==elem)))
                            {
                                 d[i]=true;;//i和l2与s[j]、d[j]的关系!
                                 break;//跳出内层循环
                            }
                    }
                       
                }
            }     
            return d[s.size()];
    }
};

4、leetcode322零钱兑换

class Solution {
    public int coinChange(int[] coins, int amount) {
        //子问题:寻找凑成金额为i的硬币最少数,边界问题是dp[i=0]=0;
        //数组含义dp[k]金额为k。。。
        //计算顺序/状态转移方程:
        // dp[i]=min(dp[i],dp[i-coins[j]+1]);//i表示金额,j表示第j个硬币的金额
        // 对于金额i而言,第j个金币的金额如果小于等于i则考虑要不要加入第j个金币,加入则就是去掉金额为coins[j]的情况下,放入coins[j](需要加1),不放入的话,即dp[i]默认值是金额数+1;
        int max=amount+1;
        int len=coins.length;
        int[] dp=new int[max];
        Arrays.fill(dp,max);//int max=amount+1;:定义一个变量max,用来表示最大的金额数,这里//是amount+1,因为最多需要amount个硬币来凑成金额amount,所以将max初始化为amount+1。
        dp[0]=0;
        dp[0]=0;//边界子问题
        for(int i=1;i<=amount;i++)//子问题:自底向上
        {
            for(int j=0;j<len;j++)
            {
                    if(coins[j]<=i)//能放得了
                    {
                        dp[i]=Math.min(dp[i],dp[i-coins[j]]+1);//i>=coins[j]
                    }
            }
        }
        return (dp[amount]>amount?-1:dp[amount]);//如果dp[amount]是默认值的值,说明没有能够凑成amount的,那么就返回-1
        

    }
}

方法二:自顶向下/记忆化搜索思路来源:

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class Solution {
    int[] memo;
    public int coinChange(int[] coins, int amount)
    {
        //记忆化搜索/自顶向下
        if(coins.length==0)
        {
            return -1;
        }
        memo=new int[amount+1];//存储1->amount
        return findway(coins,amount);
    }
    public int findway(int[] coins,int amount)
    {
        if(amount<0)
        {
            return -1;
        }
        if(amount==0)
        {
            return 0;//边界条件,当待凑金额为0的时候,需要0个硬币
        }
         // 记忆化的处理,memo[n]用赋予了值,就不用继续下面的循环
        // 直接的返回memo[n] 的最优值
        if(memo[amount]!=0)
        {
            return memo[amount];//
        }
        int min =Integer.MAX_VALUE;
        for(int i=0;i<coins.length;i++)
        {
            int res=findway(coins,amount-coins[i]);//子问题
            if(res>=0&&res<min)
            {
                min=res+1;//1是加上coins[i]

            }
        }
        memo[amount]=(min==Integer.MAX_VALUE?-1:min);
        return memo[amount];
    }
}

5、leetcode最长递增子序列

注意这里并不是要求连续的递增序列,要区别!

        //1、子问题dp[i]=?从第i个元素开始开始的,子串的递增最长长度;

        //2、自底向上,dp[1]=1;//起始为1的字串,最少的最长递增序列长为1

         //3、递推关系;dp[i]=max(nums[i]>nums[j]?dp[j]+1,dp[i]);

class Solution {
    public int lengthOfLIS(int[] nums) {
        int len=nums.length;
        if(len==1)
            return 1;
        //1、子问题dp[i]=?i从第i个元素开始开始的,子串的递增最长长度;
        //2、自底向上,dp[1]=1;//起始为1的字串,最少最长递增序列长为1
        //3、递推关系;dp[i]=max(nums[i]>nums[j]?dp[j]+1,dp[i]);

        int[] dp=new int[len];
        int maxl=1;
        for(int i=0;i<len;i++)//判断到i位置,递增序列有多长
        {
            dp[i]=1;//初始值//边界
            
            for(int j=0;j<i;j++)//从0->i,有多少递增的
            {
                if(nums[i]>nums[j])
                {
                    // dp[i]=dp[j]+1;//[i]>[j],说明在dp[j]的基础上多了一个递增
                    // 不用担心的d[j]=0,因为j<i.
                     dp[i] = Math.max(dp[i], dp[j] + 1);//注意为什么这里要加max!!
                    //  [i]>[j],则对于j而言,序列加1,[i]>[j],dp[i]=dp[j]+1;
                    // [i]>[j+k],则对于j+k而言,序列加1,dp[i]=dp[j+k]+1;
                    // 那么到底要给dp[i]赋予那个值,那么就要进行比较,所以用max();
                }
            }
            maxl=Math.max(maxl,dp[i]);
        }
        return maxl;
        // int maxl=1;
        // for(int i=0;i<len-1;i++)//起始点
        // {
        //     dp[i]=1;//初始化边界值;
        //     int base=nums[i];
        //     for(int j=i+1;j<len;j++)
        //     {
                
        //         if(nums[j]>base)
        //         {
        //             dp[i]++;
        //             base=nums[j];
        //         }

              
        //     }
        //     maxl=Math.max(dp[i],maxl);
        // }
        // return maxl;


    }
}

 思路二:动态规划+二分查找

遍历数组里面的元素,对每一个元素用二分查找,找到一个数组里面合适的位置插入:

找到第一个>=num的位置,比所有元素都小的话,就覆盖第一个元素,比有的小,有的大,那么就覆盖掉比它大的,因为较小的元素,后面有比它大的可能才更大。

如果里面的元素都比他小,那么说明二分查找的右边指针没有动过,说明判断的num要添加到数组的后面,递增序列长度+1.

class Solution {
    public int lengthOfLIS(int[] nums) {
        int len=nums.length;
        int[] tails=new int[len];//递增数组
        int res=0;
        for(int num:nums)
        {
            int i=0,j=res;//每个元素判断
            while(i<j)//二分查找:有序查找
            {
                int m=(i+j)/2;//
                if(tails[m]<num)//中间的值小于当前数据
                {
                    i=m+1;//向右边找,
                }
                else if(tails[m]>=num){
                    j=m;//否则向左边找
                }
                // else{
                //     break;
                // }
            }//i>=j跳出
// 整个过程可以理解为在一个有序数组tails中查找第一个【大于等于】num的元素的位置。
            tails[i]=num;
            if(res==j)//说明都比目标值num小,//新序列长度+1
            {
                res++;
            }
        }
        return res;

    }
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值