动态规划问题总结

一、
鉴别:题目里有最优、最长、最大、最小、计数等字眼,要考虑是否是DP问题

二、解决思路
将原问题拆解成子问题

  1. 所有的动归问题基本都可以使用递归求解(想象斐波那契数列),寻找递推公式,这里的设计要考虑无效性,即如果我走了当前这一步,会造成什么后果(比如下一步不能走)。
  2. 但是利用递归会出现大量的冗余计算,这时要想着怎么优化问题——去冗余(一般问题都是离散的,这样把之前计算的结果保存到数组、二维数组、map等数据结构中,就可以防止重复计算)
  3. 递归是自顶向下计算,从n推到0,可以使用循环(自底向上)从0推到N设计,避免递归。

三、经典题

  • 斐波那契数列(上台阶)
  • 机器人在棋盘中的路径
#include <iostream>
#include <vector>
using namespace std;
int uniquePaths(int m, int n);
int main()
    {
        int a=2,b=4;
       int r= uniquePaths(a,b);
        cout<<r<<endl;
    }

int uniquePaths(int m, int n)
{
    int res[m][n];
    res[0][0]=1;
    for(int i=0;i<m;i++)
        res[i][0]=1;
    for(int i=1;i<n;i++)
    {
        res[0][i]=1;
    }
    for(int i=1;i<m;i++)
    {
        for(int j=1;j<n;j++)
        {
            res[i][j]=res[i-1][j]+res[i][j-1];
            if(j==1d&&i==1)
                res[i][j]=0;
        }
    }
    return res[m-1][n-1];
}
  • 排列组合
  • 01背包
#include<iostream>
using namespace std;


int main()
{
    int N,V;
    cin>>N>>V;
    int weight[N];
    int vaule[N];
    for(int i=1;i<=N;i++)
      cin>>weight[i]>>vaule[i];
    int f[N+1][V+1];
    for(int i=0;i<=N;i++)
    {
        for(int j=0;j<=V;j++)
        {
            f[i][j]=0;
        }
    }
    for(int i=1;i<=N;i++)
    {
        for(int j=1;j<=V;j++)
        {
            f[i][j]=f[i-1][j];
            if(j>=weight[i])
            f[i][j]=max(f[i][j],f[i-1][j-weight[i]]+vaule[i]);
        }
        
    }
    cout<<f[N][V];
}
   //////空间复杂度N*V,时间复杂度N*V
   
/****************************************************************/
优化空间复杂度的办法,用一个一维数组存储状态 (通用解法,使用滚动数组)
#include<iostream>
using namespace std;


int main()
{
    int N,V;
    cin>>N>>V;
    int weight[N];
    int vaule[N];
    for(int i=1;i<=N;i++)
      cin>>weight[i]>>vaule[i];
    int f[V+1];
        for(int j=0;j<=V;j++)
        {
            f[j]=0;
        }
    
    for(int i=1;i<=N;i++)
    {
        for(int j=V;j>=weight[i];j--)
        {
            f[j]=max(f[j],f[j-weight[i]]+vaule[i]); ////改成一维数组
        }
        
    }
    cout<<f[V];
}

  • 完全背包问题(leedcode322)
    在这里插入图片描述
    完全背包问题有一个套路魔板
int main()
{
    int N,V;
    cin>>N>>V;
    int weight[N];
    int vaule[N];
    for(int i=1;i<=N;i++)
      cin>>weight[i]>>vaule[i];
    int f[V+1];
        for(int j=0;j<=V;j++)
        {
            f[j]=0;
        }
    
    for(int i=1;i<=N;i++)
    {
    /*******************************************************************************************
     ///主要的不同就在这里,就是01背包的起始范围反转一下,为什么这样做呢?原因是由于范围是从weight[i]~V,从小到
    ///到大运行,这样f[j-weight[i](因为在j之前)在本次循环里就算过了,f[j-weight[i]]这个状态的意义是总数为j-weight[i]
    时,可能的最值,这里可能已经包含了若干个i物品,因为发f[j-weight]这个状态,是在第i次循环里面得到的。从而实现了
    一个i可以被取多次的条件。
    *************************************************************************************************/
        for(int j=weight[i];j<=V;j++) 
        {
            f[j]=max(f[j],f[j-weight[i]]+vaule[i]); ////改成一维数组
        }
        
    }
    cout<<f[V];
}
////复杂度较高的直观写法
 #include<iostream>
using namespace std;


int main()
{
    int N,V;
    cin>>N>>V;
    int weight[N];
    int vaule[N];
    for(int i=1;i<=N;i++)
        cin>>weight[i]>>vaule[i];
    int dp[V+1];
    
        for(int j=0;j<=V;j++)
            dp[j]=0;
    
    
    for(int i=1;i<=N;i++)
    {
        for(int j=V;j>=0;j--)  ///从后往前便利是为了变一维数组时,用到的是i-1时刻的状态
        {
            for(int k=0;k<=j/weight[i];k++)
            {
                dp[j] = max(dp[j], dp[j-weight[i]*k] + k*vaule[i]);
            }
        }
    }
    cout<< dp[V]<<endl;
    return 0;
}

  • 多重背包问题
    多重背包是限制单个物品个数的完全背包问题,其实就是在01背包上在套一个循环用来限制单个物体的取样次数。
#include<iostream>
using namespace std;
int main()
{
    int N,V;
    cin>>N>>V;
    int weight[N];int value[N];int count[N];
    for(int i=0;i<N;i++)
        cin>>weight[i]>>value[i]>>count[i];
    int dp[V+1];
    for(int i=0;i<V+1;i++)
        dp[i]=0;
    for(int i=0;i<N;i++)
    {
        for(int j=V;j>=0;j--)
        {
            for(int k=0;(k<=count[i])&&(k*weight[i]<=j);k++)
               dp[j]=max(dp[j],dp[j-k*weight[i]]+k*value[i]);  ////这里比01背包多加了一层循环,用来约束单个物体的取样次数问题
        }
    }
    cout<<dp[V];
    return 0;
}

优化解法(二进制优化)
多重背包的解法出现了三层循环,可以优化成两层循环,具体的优化思想是,每个物体可以取s次,可以把这s次当成一样的s个独立的物体,这样,多重背包问题就转化成了01背包问题,但是这里面有一个问题,就是将每个物体很多次这个事儿看成独立的物体,s的总次数如果比较多,就会出现较高的复杂度,于是使用取log的方式优化,举个例子,物体a可以选10次
那么现在把10拆分:
10=1+2+4 10-(1+2+4)=3 那么现在将10分成1,2,4,3这四组,我们可以发现,这四组,0~10都可以由这4个数通过某种方式组合得到,所以10次现在就等价于1,2,4,3次,这样就把原本需要拆分10次的问题变成了拆分4次,降低了时间消耗。

#include<iostream>
#include<vector>
using namespace std;
int main()
{
    struct Good
    {
        int w,v;
    };
    vector<Good> Goods;
    int N,V;
    cin>>N>>V;
    int dp[V+1];
    for(int i=0;i<V+1;i++)
      dp[i]=0;
    for(int i=0;i<N;i++)
    {
        int w,v,s;
        cin>>w>>v>>s;
        for(int k=1;k<=s;k*=2)
        {
            s-=k;
            Goods.push_back({w*k,v*k});
        }
        if(s>0)
            Goods.push_back({w*s,v*s});
    }
    for(auto i:Goods)
    {
        for(int j=V;j>=i.w;j--)
        {
            dp[j]=max(dp[j],dp[j-i.w]+i.v);
        }
        
    }
    cout<<dp[V];
    return 0;

}

优化解法2(单调队列解法) ///先放一放,去看看滑动窗口。。。

- 混合背包问题
第一类物品只能用1次
第二类物品可以用无限次
第三类物品可以用多次
混合背包问题其实就是01背包、完全背包、多重背包的组合题,只需要根据条件带入不同的情况就行了


时间复杂度较高的写法(主要是因为多重背包问题造成的,可以使用二进制优化)

#include<iostream>
using namespace std;
int main()
{
    int N,V;
    cin>>N>>V;
    int dp[V+1];
    /*struct Thing
    {
        int kind;
        int w;
        int V;
    };*/
    for(int i=0;i<V+1;i++)
        dp[i]=0;
    for(int i=0;i<N;i++)
    {
        int w,v,s;
        cin>>w>>v>>s;
        if(s==-1)
        {
            for(int j=V;j>=w;j--)
                dp[j]=max(dp[j],dp[j-w]+v);
        } 
        else if(s==0)
        {
            for(int j=w;j<=V;j++)
            {
                dp[j]=max(dp[j],dp[j-w]+v);
            }
        }
        else
            for(int j=V;j>=0;j--)
            {
                for(int k=0;k*w<=j&&k<=s;k++)
                    dp[j]=max(dp[j],dp[j-k*w]+k*v);
            }
    }
     cout<<dp[V];
        return 0;
}

二进制优化(把所有的数据放一个结构体里)

#include<iostream>
#include<vector>
using namespace std;
int main()
{
    int N,V;
    cin>>N>>V;
    int dp[V+1];
    struct Thing
    {
        int w;
        int v;
        int kind;
    };
    vector<Thing>Things;
    for(int i=0;i<V+1;i++)
        dp[i]=0;
    for(int i=0;i<N;i++)
    {
        int w,v,s;
        cin>>w>>v>>s;
        if(s==-1)
        {
            Things.push_back({w,v,s});
        }
        else if(s==0)
        {
            Things.push_back({w,v,s});
        }
        else
        {
            for(int k=1;k<=s;k*=2)
            {
                s-=k;
                Things.push_back({k*w,k*v,-1});
            }
            if(s>0)
                Things.push_back({s*w,s*v,-1});
        }
    }
    for(auto i:Things)
    {
        if(i.kind==-1)
        {
            for(int j=V;j>=i.w;j--)
                dp[j]=max(dp[j],dp[j-i.w]+i.v);
        }
        else
        {
            for(int j=i.w;j<=V;j++)
            {
                dp[j]=max(dp[j],dp[j-i.w]+i.v);
            }
        }
    }
     cout<<dp[V];
        return 0;
}
  • 二维背包问题
    一个物体不仅有体积,还有重量
    用一个二维数组保存状态dp[j][k]表示j体积,k重量下的最大值
#include<iostream>
using namespace std;
int main()
{
    int N,V,M;
    cin>>N>>V>>M;
    int dp[V+1][M+1];
    for(int i=0;i<V+1;i++)
    {
        for(int j=0;j<M+1;j++)
        dp[i][j]=0;
    }
    for(int i=0;i<N;i++)
    {
        int w,m,v; ///体积、重量、价值
        cin>>w>>m>>v;
        for(int j=V;j>=w;j--)
        {
            for(int k=M;k>=m;k--)
                dp[j][k]=max(dp[j][k],dp[j-w][k-m]+v);
        }
    }
    cout<<dp[V][M];
    return 0;

}
  • 分组背包问题

分组背包问题是把物体分成若干组,每一组里只能选一个物体
比如当前循环到了第i组,需要从第i组中挑出一个物体
for(int a=0;a<第i组的物体个数;a++)
{
for(j=V;j>a这个物体的体积;j++) ////这里能体现出同一组物体之间的互斥关系
dp[j]=max{dp[j],dp[j-a的体积]+a的价值
}

#include<iostream>
using namespace std;
int main()
{
    int N,V;
    cin>>N>>V;
    int dp[V+1];
    for(int i=0;i<V+1;i++)
        dp[i]=0;
    int weight[101]{0};
    int value[101]{0};
    for(int i=0;i<N;i++)
    {
        int a; ///某组有几个物品
        cin>>a;
        for(int j=0;j<a;j++)
        {
           cin>>weight[j]>>value[j];
        }
        for(int k=V;k>=0;k--)
        {
            for(int r=0;r<a;r++)
            {
                if(weight[r]<=k)
                 dp[k]=max(dp[k],dp[k-weight[r]]+value[r]);
            }
               
        }
    }
    cout<<dp[V];
    return 0;
}
  • 背包问题求方案数
    最优选择一共有多少种不同的方案
#include<iostream>
using namespace std;
int main()
{
    int mod=100000007;
    int N,V;
    cin>>N>>V;
    int f[V+1];  
 /*****************************************************************************
注意这里与01背包的区别,01背包的f数组表示体积小于等于j时的最大价值,但是j并不一定全用了,比如体积是10的时候最大价值是5,但是可能在体积是9的时候最大价值就已经是5了,体积是10的时候的最大价值
其实和体积为9的时候是一样的。那么,如何让数组f[j]表示的是正好体积是j的时候的方案数呢?在初始化f[j]
数组的时候,如果f[0]等于0,其他的都初始化为负无穷,这样所有的状态都是从f[0]转换过来的,这样就可以
了。
如果像01背包那样,需要把f[0]数组全部初始化为0
************************************************************************************/
    int g[V+1]; ///开一个g数组,表示最大价值正好是j时,总的方案数是多少
    for(int i=0;i<V+1;i++)
    {
        f[i]=INT32_MIN;
        g[i]=0;
    }
    g[0]=1;
    for(int i=0;i<N;i++)
    {
        int w,v;
        cin>>w>>v;
        for(int j=V;j>=w;j--)
        {
            int t=max(f[j],f[j-w]+v);
            int s=0;
            if(t==f[j]) s=s+g[j];
            if(t==f[j-w]+v) s=s+g[j-w]; ////看t是从哪个状态转移过来的
            if(s>=mod) s-=mod;
            f[j]=t;
            g[j]=s;
        }
    }
    int maxv=0;
    for(int i=0;i<=V;i++)
    {
        maxv=max(maxv,f[i]);  ///找出最优的方案
    }
    int res=0;
    for(int i=0;i<=V;i++)
    {
        if(f[i]==maxv)
        {
            res+=g[i];     ////统计最优方案的方案种数
            if(res>=mod)res-=mod;
        }
    }
    cout<<res;
    return 0;
}
  • 求背包问题的一个具体方案(输出字典排序最小的一个)
#include <iostream>
using namespace std;
int main() {
    int N, V;
    cin >> N >> V;
    int w[N + 1];
    w[0] = 0;
    int v[N + 1];
    v[0] = 0;
    int dp[N + 2][V + 2];
    for (int i = 0; i < N + 2; i++) {
        for (int j = 0; j < V + 2; j++)
            dp[i][j] = 0;
    }
    for (int i = 1; i < N + 1; i++) {
        cin >> w[i] >> v[i];
    }
////从第N个物品开始取,直到取到第一个物体,为什么这么取呢,因为我们想要字典排序最小的输出,所以比
如我们需要先确定第一个物体要不要选,如果要选,把他输出出来,而不是先找第N个物体要不要选。
    for (int i = N; i >=1; i--) {
        for (int j = 0; j <= V; j++) {
            dp[i][j] = dp[i+1][j];
            if (j >= w[i])
                dp[i][j] = max(dp[i][j], dp[i+1][j - w[i]] + v[i]);
        }
    }
    int vol=V;
    for(int i=1;i<=N;i++) 
    {
        if(vol>=w[i]&&dp[i][vol]==dp[i+1][vol-w[i]]+v[i])
        {
            cout<<i<<" ";
            vol-=w[i];
        }
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值