第五章——动态规划1

背包问题

01背包问题

有N个·物品和容量是V的背包,每个物品有价值vi和权重(价值)wi属性,每件物品只能用一次(要么用0次,要么用1次),在背包能装得下的情况下,挑一部分物品装入背包,怎样挑才能保证这些物品的价值最大。

f(i,j)集合表示所有选法里面的最大值

f(N,V)表示从前N个物品选,总体积不超过V的集合,状态计算表示的是集合的划分,状态计算是把当前的集合表示成更小的子集,即把当前状态用前面更小的状态表示出来,我们这里采用把f(i,j)所有的选法表示成俩大类,以含i和不含i为划分标准。

划分原则:

  1. 这里划分的时候要做到不重复,即某一个元素不能属于俩个集合,只能属于其中一个。

  1. 不漏,不能漏掉任何一个小的元素(不能让某个元素不属于任何一个集合)

  1. 不漏一定要满足,但是不重有时候可以不用满足。

左边的集合的特点是从1到i-1中选,并且总体积不超过j。用f(i-1.j)表示.

右边集合的特点是所有从1到i中选,总体积不超过j,可以包含i。

步骤:

  1. 右边的选法里面都包含第i个物品,我们在选择的时候先把每种选法里面的第i个物品都去掉,这个不会影响最大值。

  1. 右边也变成了从i到i-1中选,由于没去掉第i个物品时,总体积不超过j,去掉了第i个物品后,总体积不超过j-vi。右边变成f(i-1,j-vi)

  1. 选完之后,我们把第i个物品再加回来,右边这种情况的最大价值是f(i-1,j-vi)+wi

  1. 我们最后将左边和右边价值相比较,取最大值即可

  1. 注意左边集合的这种情况是一定存在的,右边这种情况不一定存在,可能会是空集,当背包体积小于右边vi的时候就是空集。

二维表示

#include<iostream>
#include<algorithm>
using namespace std;
// 01背包问题
const int N = 1010;
int n, m;//n表示物品个数,m表示背包容量
int v[N], w[N];//V表示体积,w表示价值
int f[N][N];  //表示状态
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) cin >> v[i] >> w[i];
    //初始化的时候 要计算所有的状态,f[0~n][0~m]是所有的状态
    //f[0][0~m]表示0件物品,总体积不超过0,价值0~m,这样的情况下最大价值是多少,由于一件物品都没选,所以它是0,由于数组会默认是0,我们就不需要初始化
    //因此我们从1开始枚举
    for (int i = 1; i <= n; ++i)
    {
        for (int j = 0; j <= m; j++)
        {
            f[i][j] = f[i - 1][j];//左边这种情况是存在的
            if (j >= v[i])//右边这种情况不一定存在
                f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
        }
    }
    cout << f[n][m] << endl;
    return 0;
}

一维表示

int f[N];
const int N = 1010;
int n, m;//n表示物品个数,m表示背包容量
int v[N], w[N];//V表示体积,w表示价值
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) cin >> v[i] >> w[i];
    //初始化的时候 要计算所有的状态,f[0~n][0~m]是所有的状态
    //f[0][0~m]表示0件物品,总体积不超过0,价值0~m,这样的情况下最大价值是多少,由于一件物品都没选,所以它是0,由于数组会默认是0,我们就不需要初始化
    //因此我们从1开始枚举
    for (int i = 1; i <= n; ++i)
    {
        for (int j = m; j >= v[i]; j--)//一维数组表示的时候,j从0到vi-1的时候是没有意义的,因为下面有这个判断,
            //所以我们直接从m开始,然后这个判断也可以不要
            //从m开始的另一个原因,要保证出现跟f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);一样的效果
        {
            //if (j >= v[i])//右边这种情况不一定存在
                f[j] = max(f[j], f[j - v[i]] + w[i]);
                
        }
    }
    cout << f[m] << endl;
    return 0;
}

完全背包问题

每件物品有无限个。

状态计算和01背包问题有所不同,完全背包问题,状态计算表示的是集合的划分。

该集合表示的是第i个物品选0个,第i个物品选1个,第i个物品选2个,第i个物品选3个,第i个物品选4个……一直到k个,要注意背包的容量,这里最多选K个。

f[i,j]表示前i个物品,总体积不超过j。

0这个子集表示第i个物品不选。等价于只考虑选前i-1个物品,并且总体积不超过j。

用f[i-1,j]来表示。

我们现在考虑选前i个物品,并且第i个物品选了K个这样选法的集合。

步骤:

  1. 去掉k个物品i,即去掉k个第i个物品

  1. 求max,由于去掉了k个第i个物品,现在变成了max=f[i-1,j-k*v[i]]

  1. 再把去掉的加回来,即+k个物品i

  1. f[i-1,j-k*v[i]]+k*w[i]

f[i,j]=f[i-1,j-v[i]*k]+w[i]*k。

朴素算法

const int N = 1010;
int n, m;
int v[N],w[N];
int f[N][N];
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) cin >> v[i] >> w[i];
    for (int i = 1; i <= n; i++)//枚举所有状态
        for (int j = 0; j <= m; ++j)
            for (int k = 0; k * v[i] <= j; k++)
                f[i][j] = max(f[i][j], f[i - 1][j - v[i]*k] + k * w[i]);
    cout << f[n][m] << endl;
    return 0;
}

优化二维算法

把下列式子展开

橙色部分比红色部分的每一项都多了W

我们再枚举的时候,枚举俩个状态就可以了,第二个状态在体积满足条件的情况下才可以使用

const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) cin >> v[i] >> w[i];
    for (int i = 1; i <= n; i++)//枚举所有状态
        for(int j=0;j<=m;j++)
      {
        f[i][j] = f[i - 1][j];
        if (j >= v[i])
            f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
    }
    cout << f[n][m] << endl;
    return 0;
}

优化一维算法

const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) cin >> v[i] >> w[i];
    for (int i = 1; i <= n; i++)//枚举所有状态
        for(int j=v[i];j<=m;j++)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    cout << f[m] << endl;
    return 0;
}

多重背包问题1

多重背包问题,每件物品的个数是有限制的。

跟完全背包相似,根据第i个物品选多少个,把第i个物品分成若干种类

状态转移方程跟完全背包的朴素算法一样

k从0,1,2一直到s[i]

const int N = 110;
int n, m;
int v[N], w[N], s[N];
int f[N][N];
int main()
{
    cin >> n >>m;
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i] >> s[i];
    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= m; j++)
            for (int k = 0; k <= s[i] && k * v[i] <= j; k++)//注意这里的体积范围,总共需要的体积小于等于给定的体积
                f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);
    cout << f[n][m] << endl;
    return 0;
}

多重背包问题2

跟多重背包问题1相比,数据范围变大了,如果直接按多重背包问题1去做,会超时。

我们发现f[i,j-v]后面多了一项。因此,我们不能直接用完全背包的优化方式,来优化这道题

这里采用二进制的优化方式。

假设某个物品有1023个,s=1023,我们不需要从0枚举到1023,我们可以先把若干个第i个物品打包在一块来考虑,1,2,4,8……512分别代表第一组,第二组,第三组有1,2,4个i物品,即把i物品打包成10组,最多从每组中选1个,然后从选出来的这10个中平凑出0-1023中的任何一个数。

当s=200,我们可以这样打包,最后一组是73,分组规则按照以2为底的质数进行增长,但所有组数加一起的和不能超过某个数的总个数。

第i个物品有s个,我们可以拆分成新的logs组新的物品,新的物品只能用一次。

步骤:

  1. 先把第i个物品拆分

  1. 对拆分出来的新的物品,做一遍01背包即可

时间复杂度为

const int N = 25000, M = 2010;
int n,m;
int v[N], w[N];
int f[N];
int main()
{
    cin >>n >>m;
    int cnt = 0;//表示所有新的物品
    for (int i = 1; i <= n; i++)
    {
        int a, b, s;//当前物品的体积,价值,个数
        cin >> a >> b >> s;
        int k = 1;//从1开始分
        while (k<=s)
        {
            cnt++;//新物品的编号++
            v[cnt] = a * k;//体积
            w[cnt] = b * k;//价值
            s -= k;
            k *= 2;

        }
        if (s > 0)//此时剩下一些需要补上
        {
            cnt++;
            v[cnt] = a * s;
            w[cnt] = b * s;
        }
    }
    n = cnt;//分组完成
    //做01背包问题即可
    for (int i = 1; i <= n; ++i)
        for (int j = m; j >= v[i]; j--)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    cout << f[m] <<endl;
    return 0;
}

分组背包问题

枚举第i组物品,选哪个。

从左到右分别是第i组物品选第0个,第i组物品选第1个……第i组物品选第n个。

从第i组物品选第k个

const int N =110 ;
int n, m;
int v[N][N], w[N][N],s[N];//s存每一组的个数
int f[N];
int main()
{
    cin >> n >>m;
    for (int i = 1; i <= n; i++)
    {
        cin >> s[i];
        for (int j = 0; j < s[i]; j++)
            cin >> v[i][j] >> w[i][j];
    }
    for (int i = 1; i <= n; i++)
        for (int j = m; j >= 0; j--)//从前往后枚举每一组物品,从大到小枚举所有体积
            for (int k = 0; k < s[i]; k++)//枚举所有选择
                if (v[i][k] <= j)//如果第i组,第k件物品<=j的话,我们才能往下进行,即要注意体积大小
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
    cout << f[m] << endl;
    return 0;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

头发没有代码多

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值