动态规划专题(白菜的进阶之路)

动态规划专题(持续更新)

目录

动态规划专题(持续更新)

最长递增子序列

0-1背包问题

背包问题的变体--分割等和子集

完全背包问题

高楼扔鸡蛋问题

最长公共子序列


最长递增子序列

leetcode-300(最长递增子序列)

(注意子序列与子串之间的关系,字串一定是连续的,子序列不是连续的但是要保证子序列的稳定性)

1.考虑dp[i]与dp[i-1]之间的关系,在这里dp[i]的意义为以第nums[i]个结尾的子序列的最长递增子序列的长度。

2.得知前i-1个状态,在求解dp[i]个状态时,也就是要比较从nums[i]与前i-1个值得关系,不断更新dp[i]的值。

int dp[nums.size()];
fill(dp,dp + nums.size(),1);//在这里,要将dp数组每个位置上的值初始化为1
//fill函数:fill(array,array + n,number),从array地址开始到array + n,初始化值为number。
for(int j = 0;j < i;j++)
{
    dp[i] = max(dp[i],dp[j] + 1);
}

状态转移方程确定后,便可编写代码

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int dp[nums.size()];
        fill(dp,dp + nums.size(),1);
        int res = 0;
        for(int i = 0;i < nums.size();i++)
        {
            for(int j = 0;j < i;j++)
            {
                if(nums[i] > nums[j])
                {
                    dp[i] = max(dp[i],dp[j] + 1);
                }
            }
        }
        for(int i = 0;i < nums.size();i++)
        {
            res = max(res,dp[i]);
        }
        return res;
    }
};

上述算法的时间复杂度为n^2,至于要求中的时间复杂度的提升要用到二分查找法以及纸牌游戏的思路,leetcode中有很多优秀的题解,下图为我认为比较通俗易懂的一个大佬写的思路。

 

0-1背包问题

0-1背包问题是经典的动态规划问题,给定一个可装重量为w的背包和n个物品,每个物品有重量和价值两个属性,其中第i个物品的重量为wt[i],价值为val[i],现在让你用这个背包装物品,最多能装的价值为多少。

N = 3,W = 4;//有三个物品,总容量为4
wt = [2,1,3];
val = [4,2,3];
这种情况下可装的最大价值为6,选择前两种物品装。

首先我们要明确状态和选择,状态有两个,背包的容量和可选择的物品。选择就是对于每一件物品装或者不装。

其次,明确dp数组的定义,dp[i][w]的定义为:对于前i个物品,当背包的容量为w的时候,所能装下的最大价值是dp[i][w]。

最后,明确状态转移方程。

dp[i][w]的定义已经声明,但是在确定转移方程中要考虑两种结果,如果不把第i个物品装进背包,那么目前背包的总价值为

dp[i - 1][w],如果把第i 个物品装进背包,那么总价值为dp[i - 1][w - wt[i - 1]] + val[i - 1],在这里注意wt和val的下标是从0开始的。

所以结合上述分析,可以将状态转移方程以及算法主体写出。

int the_max_val_backpack(int n,int m,vector<int> wt,vector<int> val)
{
    vector<vector<int>> dp(n + 1,vector<int>(m+1,0));
    //当背包容量为0或者有0个物品的时候,总价值都为0。满足dp[i][w]的定义
    for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= m;j++)
        {
            if(j - wt[i] < 0)
            {
                dp[i][j] = dp[i - 1][j];
            }else
            {
                dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - wt[i - 1]] + val[i - 1]);
            }
        }
    }
    return dp[n][m];
}

 

背包问题的变体--分割等和子集

可能刚开始大家没有办法想到这个和背包问题有什么关系,但是换个思路讲,如果把数组的总和的一半看作是背包的容量,数组中的每一个元素看作体积,如果存在一种存放方法,将数组中的某些物品存放到背包中,能够刚好将这个容量为sum/2的背包装满,那就可以将数组分给成两个元素和想等的子集。

首先分析状态和选择,状态就是背包的容量,选择就是将该物品存放还是不存放

其次,明确dp数组的含义,dp[i][j]的定义为:对于前i 个物品,当背包的容量为j时,能否将背包刚好放满,返回bool值。

最后,明确状态转移方程:首先,对与背包容量为0以及有0个物品来说,返回值应该为true。根据选择情况来讲,存在放与不放两种选择,那么

1.不放第i个元素,那么dp[i][j] = dp[i - 1][j]

2.如果存放第i元素,那么dp[i][j] = dp[i - 1][j - nums[i - 1]]

根据状态转移方程,可以将函数主体写出来了

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        if(nums.size() <= 1)
        {
            return false;
        }
        int sum = 0;
        for(int i = 0;i < nums.size();i++)
        {
            sum += nums[i];
        }
        if(sum%2 != 0)
        {
            return false;//如果说sum值不是一个偶数,那么肯定无法分割成功
        }
        sum = sum/2;

        /*//方法一(空间复杂度高)
        vector<vector<bool>> dp(nums.size() + 1,vector<bool>(sum + 1,false));
        //构建dp
        for(int i = 0;i <= nums.size();i++)
        {
            dp[i][0] = true;//为什么为true已经说明
        }
        for(int i = 1;i <= nums.size();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[nums.size()][sum];
        */
        //会发现,前面那种二维dp中,实际具有意义的只有每一行最后一列元素,所以只存储最后一列元素即可
        vector<bool> dp(sum + 1,false);
        dp[0] = true;
        for(int i = 0;i < nums.size();i++)
        {
            for(int j = sum;j >= 0;j--)//由于之前的每一个元素只能用一次,以免之前的结果影响其他的结果。
            {
                if(j - nums[i] >= 0)
                {
                    dp[j] = dp[j]||dp[j - nums[i]];
                }
            }
        }
        return dp[sum];
    }
};

 

完全背包问题

有一个背包,最大容量为amount,有一系列coins,每个物品的重量为coins[i],每个物品的数量是无限的,请问有多少种方法,恰好能将背包装满。

分析:

状态:状态就是背包的容量以及可选的物品,选择:就是将该物品装入或者不装人。

dp数组的定义:dp[i][j]为当前选择i个物品,将容量为j 的背包装满有多少种方法。

状态转移方程:

首先明确,dp[0][j] = 0,没有物品只用0种方法可以装,dp[i][0],背包容量为0的话就只有一种方法,不装。

然后状态转移方程有两种选择:

1.不装入第i个物品,也就是dp[i][j] = dp[i - 1][j];

2.装入第i个物品,也就是dp[i][j] = dp[i][j - coins[i - 1]];

以leetcode零钱问题||为例:

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<vector<int>> dp(coins.size() + 1,vector<int>(amount + 1,0));
        for(int i = 0;i <= coins.size();i++)
        {
            dp[i][0] = 1;
        }
        for(int i = 1;i <= coins.size();i++)
        {
            for(int j = 1;j <= amount;j++)
            {
                if(j >= coins[i - 1])
                {
                    dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]];
                }else
                {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[coins.size()][amount];
    /*
    当然可以减少空间的损耗,有上述转移方程发现,dp[i][]只与dp[i-1][]有关
    所以可以使用一个一维数组统计信息
    vector<int> dp[amount + 1];
    dp[0] = 1;
    for(int i = 0;i < coins.size();i++)
    {
        for(int j = 1;j < amount;j++)
        {
            if(j - coins[i] >= 0)
            {
                dp[j] = dp[j] + dp[j - coins[i]];
            }
        }
    }
    return dp[amount];
    */
    }
};

 

高楼扔鸡蛋问题

有一栋从1到N共N层楼,然后给你K个鸡蛋,K至少为1,现在确定存在楼层0<=F<=N,在这层楼将鸡蛋扔下去,鸡蛋恰好没摔碎,现在问你,最坏情况下,至少要仍几次鸡蛋,才能确定这个楼层F呢?

最坏情况就是,鸡蛋破碎一定发生在搜索区间穷尽时,至少就是,以最少得次数达到搜索区间穷尽。

分析:

状态:当前拥有得鸡蛋数K和需要测试得楼层数N,随着测试的进行,鸡蛋树可能会减少,楼层的搜索数会减小,这就是状态变化。

选择:选择哪层楼去扔鸡蛋,如果鸡蛋数为1,那么就只能一层一层的去仍,如果鸡蛋数大于1,那就要进行状态选择。

dp数组的定义:dp[k][i],就是当前拥有k 个鸡蛋,楼层数为N,最坏情况下至少仍dp[k][N]次,能找到楼层F。

所以算法结构为:

dp[k][N] = m,dp数组代表的含义,当前状态为K个鸡蛋时,面对N层楼,至少仍鸡蛋的次数为m.
for(int i = 0;i <= N;i++)
{
   res = min(res,max(dp[k - 1][i - 1],dp[k][N - i]) + 1)
}
dp[k - 1][i - 1],代表第i楼鸡蛋破了,所以k-1,从第0到i - 1丢
dp[k][N - i],代表第i楼鸡蛋没破,所以从N-i,到N丢。

第二种方法
dp[k][m] = N,dp数组代表的含义,当前状态为K个鸡蛋时,扔m次鸡蛋,能够确定最高的楼层数。
dp[k][m] = dp[k - 1][m - 1] + dp[k][m - 1] + 1;
dp[k - 1][m - 1],楼下的层数;
dp[k][m - 1],楼上的层数。
while(dp[k][m] < N)
{
    m++;
    for(int k = 1;k <= K;k++)
    {
        dp[k][m] = dp[k - 1][m - 1] + dp[k][m - 1] + 1;
    }    
}

 

最长公共子序列

首先明确最长公共子序列是否可用动态规划来求,str1和str2的公共子序列随着下标的前进可能会增加,也可能会不变。

在这种不能明确的找出状态与选择的题目中,首先尝试看看能不能用dp表来将问题表达出来,在这个问题中,dp[i][j]代表从串1的起始位置到串1的i 位置,串2的起始位置到串2的j位置,公共子串的长度为多少。

将基本情况定义一下,dp[0][j]和dp[i][0]都应该初始化为0。

状态转移方程为:(用两个指针i,j,分别从串1和串2的末端开始向前进,若相等则该字符肯定在公共子序列中)

若str1[i] == str2[j],则dp[i][j] = dp[i - 1][j - 1] + 1;反之,dp[i][j] = max(dp[i - 1][j - 1],dp[i - 1][j],dp[i][j - 1]);

但是,由于dp[i - 1][j - 1]肯定是小于等于其余两项的,所以在这里可以去掉。

int find_the_maxsize_common_str(string &str1,string &str2)
{
    vector<vector> dp(str1.size() + 1,vector<int>(str2.size() + 1,0));
    for(int i = 1;i <= str1.size();i++)
    {
        for(int j = 1;j <= str2.size();j++)
        {
            if(str1[i - 1] == str2[j - 1])
            {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            }else
            {
                dp[i][j] = max(dp[i - 1][j],dp[i][j - 1]);
            }
        }
    }
    return dp[str1.size()][str2.size()];
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值