• 动态规划,0/1背包,完全背包(找零钱),套路


    动态规划

    动态规划问题的一般形式就是求最值。
    求解动态规划的核心问题是穷举。
    动态规划的穷举有点特别,因为这类问题存在「重叠子问题」,如果暴力穷举的话效率会极其低下,所以需要「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。
    而且,动态规划问题一定会具备「最优子结构」,才能通过子问题的最值得到原问题的最值。
    关键就是状态转移方程,写出正确的状态转移方程,才能正确地枚举。
    解决问题步骤:
    明确「状态」 -> 定义 dp 数组/函数的含义 -> 明确「选择」(做出选择改变当前状态-> 明确 base case。

    0/1背包问题

    描述:

    有N件物品和一个容量为V的背包。第i件物品的费用(即体积,下同)是w[i],价值是val[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

    思路:

    用动态规划的思路,阶段就是“物品的件数”,状态就是“背包剩下的容量”,那么很显然f [ i , v ] 就设为从前 i 件物品中选择放入容量为 v 的背包最大的价值。那么状态转移方程为:

    f[i][v]=max{ f[i-1][v],f[i-1][v-w[i]]+val[i] }。

    这个方程可以如下解释:只考虑子问题“将前 i 个物品放入容量为 v 的背包中的最大价值”那么考虑如果不放入 i ,最大价值就和 i 无关,就是 f[ i - 1 ][ v ] , 如果放入第 i 个物品,价值就是 f[ i - 1][ v - w[i] ] + val[ i ],我们只需取最大值即可。

    优化:

    上述状态表示,我们需要用二维数组,但事实上我们只需要一维的滚动数组就可以递推出最终答案。考虑到用f[ v ]来保存每层递归的值,由于我们求f[ i ][ v ] 的时候需要用到的是f[ i-1 ][ v] 和 f[ i-1 ][v - w[i] ] 于是可以知道,只要我们在求f[ v ]时不覆盖f[ v - w[i] ],那么就可以不断递推至所求答案。所以我们采取倒序循环,即v = m(m为背包总容积)伪代码如下:

      for i = 1..N
    
       for v = V..0
    
         f[ v ] = max{ f[ v ],f[ v-w[i] ]+val[ i ] };
    

    完全背包问题

    描述:

    有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是w[i],价值是val[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

    思路:

    完全背包问题与0/1背包问题不同之处在于其每个物品是无限的,从每种物品的角度考虑,与它相关的策略就变成了取0件、1件、2件...。我们可以根据0/1背包的思路,对状态转移方程进行改进,令f[i][v]表示前 i 种物品恰放入一个容量为 v 的背包的最大权值。状态转移方程就变成了:

    f[ i ][ v ] = max{ f[ i-1 ][ v-kw[i] ] + kval[ i ]  | 0 <= k*w[i] <=  v}。

    我们通过对0/1背包的思路加以改进,就得到了完全背包的一种解法,这种解法时间复杂度为O(n3),空间复杂度为O(n2)。

    时间优化:

    根据上述f[ i ][ v ]的定义,其为前 i 种物品恰好放入容量为 v 的背包的最大权值。根据上述状态转移方程可知,我们假设的是子结果f[ i-1 ][ v-k*w[i] ]中并没有选入第 i 种物品,所以我们需要逆序遍历(像0/1背包一样)来确保该前提;但是我们现在考虑“加选一件第 i 种物品”这种策略时,正需要一个可能已经选入第 i 种物品的子结果f[ i ][ v-w[i] ],于是当我们顺序遍历时,就刚好达到该要求。这种做法,使我们省去了一层循环,即第 i 种物品放入的件数k,从而时间复杂度优化为O(n^2)。

    空间优化:

    正如0/1背包的空间优化,上述状态转移方程已经优化为:

    f[i][v]=max{f[i-1][v],f[i][v-w[i]]+val[i]}

    将这个方程用一维数组实现,便得到了如下伪代码:

    for i = 1..N
    
       for v = 0..V
    
         f[v] = max{f[v],f[v-w[i]] + val[ i ] };
    

    代码:

    int coinchange(vector<int>& coins, int amount)
    {
    	//所有值初始化为amount+1,因为最多amount枚coin
    	vector<int> dp(amount + 1, amount + 1);
    	for (int i = 1; i <=amount ; i++)
    	{
    		for (int j = 0; j < coins.size(); j++)
    		{
    			if (i - coins[j] >= 0)
    				dp[i] = min(dp[i], dp[i - coins[j]]); //dp[i]为当前最小值
    			
    		}
    	}
    	return dp[amount] == amount + 1 ? -1 : dp[amount];
    }
    

    动态标准套路

    第一步:明确 状态选择

    状态:背包的容量和可选择的物品
    选择:装入背包和不装入背包

    for 状态1 in状态1的所有取值:
        for 状态2 in 状态2的所有取值:
            dp[状态1][状态2]=择优(选择1,选择2)
    
    

    第二步:明确DP数组的定义

    dp数组表示状态。
    dp[i][w]的定义:对于前i个物品,当前背包的容量为w,这种情况下可以装的最大价值为dp[i][w]。
    这样做是为了便于状态转移。
    我们想求的最终答案是dp[N][W]。base case就是dp[0][..]=dp[..][0]=0,因为没有物品或者没有背包空间时,能装的最大价值为0.

    int dp[N+1][W+1]
    dp[0][..]=dp[..][0]=0
    for i in[1..N]:
        for j in [1,w]:
            dp[i][j]=max(i装入背包,不把i装入背包)
    return dp[N][W]
    

    第三步:根据选择。思考状态转移的逻辑。

    要结合dp数组的定义和算法逻辑
    如果没有把第i个物品装入,那么dp[i][w]=dp[i-1][w](不装,继承之前的结果)
    如果把第i个物品装入,那么dp[i][w]=dp[i-1][w-wt[i-1]]+val[i-1];(i从1开始,val和wt取值为i-1.
    dp[i-1][w-wt[i-1]]:在装入第i个物品的前提下,背包能装的最大价值是多少?
    显然应该寻求重量为w-wt[i-1]限制下能装入的最大价值,再加上第i个个物品的价值val[i-1]。

    for i in[1..N]:
        for j in [1,w]:
            dp[i][j]=max(dp[i-1][j],dp[i-1][j-wt[i-1]+val[i-1])
    return dp[N][W]
    

    最后处理边界情况。(j-wt[i-1])可能小于0

    代码:

    int _01(int W, int N, vector<int> & wt, vector<int> & val)
    {
    	vector<vector<int>> dp(N + 1, vector<int>(W + 1,0));
    	for (int i = 1; i <= N; i++)//i从1开始,第i个数据下标是i-1,wt和val数组的下标从0到N-1
    	{
    		for (int j = 1; j <= W; j++)
    		{
    			if (j - wt[i - 1] >= 0)
    				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - wt[i - 1]] + val[i - 1]);
    			else
    				dp[i][j] = dp[i - 1][j];
    
    		}
    	}
    	return dp[N][W];
    }
    

    简单背包问题

    #include <iostream>
    #include <vector>
    using namespace std;
    
    int main()
    {
        int n, s;
        cin >> n >> s;
        int dp[101][1001];
        vector<int> Weight(n, 0);
        for (int i = 0; i < n; i++)
        {
            cin >> Weight[i];
        }
        dp[0][0] = 1;
        dp[0][1] = 0;
        dp[1][0] = 1;
        for(int j=1;j <= s;j++)
        {
            dp[0][j]= 0;
        }
        for(int i = 1;i <= n;i++)
        {
            dp[i][0]= 1;
        }
        for (int i = 1; i <= n; i++)
        {
            for (int j = 1; j <=s; j++)
            {
                if (j - Weight[i - 1] >= 0)
                {
                    dp[i][j] = max(dp[i-1][j-Weight[i - 1]], dp[i-1][j]); //能装下,可以选择装,也可以不装
                }
                else
                {
                    dp[i][j] = dp[i - 1][j]; //装不下,dp[i][j]的状态和dp[i-1][j]一样
                }
            }
        }
        if (dp[n][s] == 1)
        {
            cout << "YES";
        }
        else
        {
            cout << "NO";
        }
    
        return 0;
    }
    
  • 相关阅读:
    数据分析2 numpy(ndarray数组,属性,创建,索引切片,运算,函数,随机数), Pandas(Series[创建,series数据对齐(相加),缺失值处理,特性,索引[整数索引loc和iloc]],DataFrame[索引与切片, 常用属性,数据对齐(相加),缺失值处理,常用方法],时间对象,时间序列), dateutil时间处理
    数据分析1 ipython, jupyter notebook(快捷键), anaconda软件
    CMDB4 总结CMDB,数据展示前后端不分离(xadmin第二种安装方法),前后端分离(vue-element-admin,iview-admin), 画图工具(highcharts,echarts,antv)
    CMDB3 完善采集端代码(ssh方案的多线程采集), 异常处理, 服务端目录结构的设计(django的app), API数据分析比对入库
    CMDB2 采集客户端目录架构设计, 高级配置文件, 可插拔式的采集
    CentOS, CMDB1 Linux命令补充(netstat,ps,kill,service,systemctl,top,wget,Irzsz,vim,磁盘使用情况,cpu情况,tree,history),linux常见的面试题, CMDB
    CentOS centos7的目录结构, 文件系统常用命令(pwd,cd,mkdir,touch,ls,cat,echo,cp,mv,rm), vim编辑器
    CentOS VMware安装CentOS7,配置网卡文件,Xshell5连接,快照,克隆,修改主机名
    flask框架4 表单验证, 表单查询wtforms,flask-migrate
    flask框架3 local对象补充,偏函数,请求上下文,g对象,flask-session组件,flask的信号,自定义信号(了解),用命令启动flask项目(flask-script)
  • 原文地址:https://www.cnblogs.com/void-lambda/p/12444910.html
Copyright © 2020-2023  润新知