动态规划_背包问题笔记

https://www.cnblogs.com/31415926535x/p/10403589.html

dp自从知道有这么个东西时,就没有好好的学,,现在一看道dp的题就绕道走,,,但是,很多比赛中的dp问题有很多,,别人都会,自己不会很吃亏啊,,,于是从基础开始一点一点的补ing

背包问题

背包问题是动态规划的经典也是基础,,,下面的东西部分来自 背包九讲

01背包

01背包指的是对于任意的物品只有 取或不取 两种状态,,

状态转移方程

状态转移方程为:

(F[i,j]=max(F[i-1,j], F[i-1,j-c_i]+w_i))

外层循环枚举物品总数:(for i=1 to n)

内层循环枚举背包的容量: (for j=c_i to v)


空间优化后的状态转移方程:

(F[j]=max(F[j], F[j-c_i]+w_i))

外层循环不变,内层循环变为: (for j=v to c_i)

外层循环可以继续优化为: (for j to max(v-sum_i^nw_i, c_i))

初始化

  • 恰好装满背包:(F[0]=0,F[1..v]=-infty)
  • 不必装满: (F[0..v]=0)

初始化F数组就是在没有任何物品可以放入背包时的合法状态,所以,前者只有容量为零的背包什么都不装的情况下是恰好装满的,其他容量的背包都是未定义的状态,无合法解;后者因为不必装满,所以什么都不装的时候就是一个合法解,这时的价值为零。

例题

hud-2602

裸的01背包,,直接做,,,注意判断当前物品是否能放入背包,,再选择放与不放,,

还有内层循环容量的遍历是从0开始

memset(dp, 0, sizeof dp);
        for(int i = 1; i <= n; ++i)
            for(int j = 0; j <= v; ++j)
                if(c[i] <= j)//能放入时,选择放与不放
                    dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - c[i]] + w[i]);
                else
                    dp[i][j] = dp[i - 1][j];
        printf("%d
", dp[n][v]);

空间优化后的方法:

memset(dp, 0, sizeof dp);
        for(int i = 1; i <= n; ++i)
            for(int j = v; j >= 0; --j)
                if(c[i] <= j)//能放入时,选择放与不放
                    dp[j] = max(dp[j], dp[j - c[i]] + w[i]);
        printf("%d
", dp[v]);

hdu-2546

题意是:一个总钱数为m的钱包,在剩余金额大于等于5的情况下可以购买任何东西,即使买了一个东西后剩余钱数为负,然后给你这n个东西的标价,每种东西只能购买一次,,

这道题按01背包做的话,可以将钱包m看成背包的容量,n道菜就是n种物品, 每种物品的价值和花费都是其菜价,,

这是其中一个点,还有为了尽可能的是利益最大,,我们可以先保留5块,为了最后买那个最贵的菜,,对剩下的n-1个菜选择出价值最大的,,,这样就将这道题转化成了容量为m-5的背包选择一些物品使得总价值最大,,,最后的答案在算上那个最贵的菜就行了,,,

int dp[maxn], c[maxn], w[maxn];
int main()
{
//    freopen("233.in" , "r" , stdin);
//    freopen("233.out" , "w" , stdout);
//    ios_base::sync_with_stdio(0);
//    cin.tie(0);cout.tie(0);
    int n;
    while(scanf("%d", &n) && n)
    {
        for(int i = 1; i <= n; ++i)
            scanf("%d", &c[i]);

        int m;scanf("%d", &m);

        if(m < 5)
        {
            printf("%d
", m);
            continue;
        }
        m -= 5;
        sort(c + 1, c + 1 + n);
        memset(dp, 0, sizeof dp);
        for(int i = 1; i <= n - 1; ++i)
            for(int j = m; j >= c[i]; --j)
                dp[j] = max(dp[j], dp[j - c[i]] + c[i]);
        printf("%d
", m + 5 - dp[m] - c[n]);
    }
    return 0;
}

hdu-1171

题意是:有一些设施,每个设施的价值为 (w_i),,然后要分成两堆,这两堆的价值要尽可能的相近

显然分后的价值和 (sum) 就是原来的价值和,,然后肯定一个大于等于均值,一个小于等于,,,所以可以将这道题目看成01背包的模型:一个容量为 (sum/2) 的背包,选择装一些物品,这些物品的价值的和费用相同,,求最大的价值

int dp[maxn], c[maxn], w[maxn];
int main()
{
//    freopen("233.in" , "r" , stdin);
//    freopen("233.out" , "w" , stdout);
//    ios_base::sync_with_stdio(0);
//    cin.tie(0);cout.tie(0);
    int n;
    while(scanf("%d", &n) && n > 0)
    {
        int tot = 0;
        for(int i = 1; i <= n; ++i)
        {
            int a, b;
            scanf("%d%d", &a, &b);
            while(b--)w[++tot] = a;
        }
        memset(dp, 0, sizeof dp);
        int sum = 0;
        for(int i = 1; i <= tot; ++i)sum += w[i];
        for(int i = 1; i <= tot; ++i)
            for(int j = sum / 2; j >= w[i]; --j)
                dp[j] = max(dp[j], dp[j - w[i]] + w[i]);
        printf("%d %d
", sum - dp[sum / 2], dp[sum / 2]);
    }
    return 0;
}

剩下一些其他题,,以后再说

完全背包

完全背包就是在01背包的基础上对于物品的限制解除,,物品不再为只能取一件,而是无限件(实际也不可能是无限件,每一个物品最多取 (lfloor frac{v}{c_i} floor)),,

将完全背包转化为01背包后, 状态转移方程和01背包的类似,,只有对背包容量的枚举也就是内层循环中,完全背包是递增的顺序而01背包的是递减的顺序,,

(for j=c_i to v)

0-1背包和完全背包的不同:

从二维数组上区别0-1背包和完全背包也就是状态转移方程就差别在放第i中物品时,完全背包在选择放这个物品时,最优解是F[i][j-c[i]]+w[i]即画表格中同行的那一个,而0-1背包比较的是F[i-1][j-c[i]]+w[i],上一行的那一个。

从一维数组上区别0-1背包和完全背包差别就在循环顺序上,0-1背包必须逆序,因为这样保证了不会重复选择已经选择的物品,而完全背包是顺序,顺序会覆盖以前的状态,所以存在选择多次的情况,也符合完全背包的题意。状态转移方程都为F[i] = max(F[i],dp[F-c[i]]+v[i])。

例题

hdu-1114

题意是:给你一个存钱罐的总质量个单纯存钱罐的质量(也就是差为钱的质量),,以及n种硬币的面值和质量,然后问你最小的金额是多少

差值可以看作背包的容量,每个硬币的质量为物品的代价,面值为其价值,,然后求最小的价值转移方程里就为min,,初始化再改变一下,,

int dp[maxn], c[maxn], w[maxn];
int main()
{
//    freopen("233.in" , "r" , stdin);
//    freopen("233.out" , "w" , stdout);
//    ios_base::sync_with_stdio(0);
//    cin.tie(0);cout.tie(0);
    int t;scanf("%d", &t);
    while(t--)
    {
        int e, f;scanf("%d%d", &e, &f);
        int v = f - e;
        int k;scanf("%d", &k);
        for(int i = 1; i <= k; ++i)scanf("%d%d", &w[i], &c[i]);
        memset(dp, inf, sizeof dp);
        dp[0] = 0;
        for(int i = 1; i <= k; ++i)
            for(int j = c[i]; j <= v; ++j)
                dp[j] = min(dp[j], dp[j - c[i]] + w[i]);
        if(dp[v] >= inf)
            printf("This is impossible.
");
        else
            printf("The minimum amount of money in the piggy-bank is %d.
", dp[v]);
    }
    return 0;
}

多重背包

多重背包就是完全背包的限制版,,每一种物品不再是无限个,,而是给定的个数,最后还是求背包的最大价值什么的,,,

转化成01背包问题就是对于每一种物品取 (1, 2, 2^2, 2^3,,,2^{k-1},M_i-2^k+1)件,,

一般的多重背包模板:

int dp[maxn], c[maxn], w[maxn], num[maxn];
int n, m, v;//n为物品总数,v为背包容量
//01背包,该物品的代价,价值
void ZeroOnePack(int C, int W)
{
    for(int i = v; i >= C; --i)
        dp[i] = max(dp[i], dp[i - C] + W);
    return;
}
//完全背包,该物品的代价,价值
void CompletePack(int C, int W)
{
    for(int i = C; i <= v; ++i)
        dp[i] = max(dp[i], dp[i - C] + W);
    return;
}
//一次多重背包,该物品的代价,价值,数量
void OneMuitPack(int C, int W, int M)
{
    if(v <= C * M)//物品足够多时用完全背包
    {
        CompletePack(C, W);
        return;
    }
    else        //否则用二进制划分成若干件01背包的物品
    {
        int k = 1;
        while(k < M)
        {
            ZeroOnePack(k * C, k * W);//某一个划分成01背包的物品
            M -= k;
            k <<= 1;
        }
        ZeroOnePack(C * M, W * M);//剩下的一些物品
    }
    return;
}

例题

hdu-2844

题意是:n种面值的硬币,每种硬币的个数限定,问你能够组成几种面值和不超过m的组成方法,

转化成背包问题就是,一个容量为m的背包装一些价值和代价都为面值的物品,其中物品的个数有限制,,问背包内的价值的可能种类

//cf
//#include <bits/stdc++.h>
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <string.h>
#include <algorithm>
#define aaa cout<<233<<endl;
#define endl '
'
#define pb push_back
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, ull> pii;
const int inf = 0x3f3f3f3f;//1061109567
const ll linf = 0x3f3f3f3f3f3f3f;
const double eps = 1e-6;
const double pi = 3.14159265358979;
const int maxn = 1e5 + 5;
const int maxm = 2e5 + 5;
const ll mod = 1e9 + 7;
int dp[maxn], c[maxn], w[maxn], num[maxn];
int n, m, v;//n为物品总数,v为背包容量
//01背包,该物品的代价,价值
void ZeroOnePack(int C, int W)
{
    for(int i = v; i >= C; --i)
        dp[i] = max(dp[i], dp[i - C] + W);
    return;
}
//完全背包,该物品的代价,价值
void CompletePack(int C, int W)
{
    for(int i = C; i <= v; ++i)
        dp[i] = max(dp[i], dp[i - C] + W);
    return;
}
//一次多重背包,该物品的代价,价值,数量
void OneMuitPack(int C, int W, int M)
{
    if(v <= C * M)//物品足够多时用完全背包
    {
        CompletePack(C, W);
        return;
    }
    else        //否则用二进制划分成若干件01背包的物品
    {
        int k = 1;
        while(k < M)
        {
            ZeroOnePack(k * C, k * W);//某一个划分成01背包的物品
            M -= k;
            k <<= 1;
        }
        ZeroOnePack(C * M, W * M);//剩下的一些物品
    }
    return;
}
int main()
{
//    freopen("233.in" , "r" , stdin);
//    freopen("233.out" , "w" , stdout);
//    ios_base::sync_with_stdio(0);
//    cin.tie(0);cout.tie(0);
    while(scanf("%d%d", &n, &m) && n + m)
    {
        for(int i = 1; i <= n; ++i)scanf("%d", &w[i]);
        for(int i = 1; i <= n; ++i)scanf("%d", &num[i]);
        memset(dp, 0, sizeof dp);
        v = m;
        for(int i = 1; i <= n; ++i)
            OneMuitPack(w[i], w[i], num[i]);
        int ans = 0;
        for(int i = 1; i <= m; ++i)if(dp[i] == i)++ans;
        printf("%d
", ans);
    }
    return 0;
}

混合背包

混合背包就是n种物品有的只能取一次,有的能取有限次,有的能取无限次,然后问你对于容量为v的背包的可取最大价值是多少

直接判断每个物品的种类,使用不同的背包类型就行了

例题

codevs-3269

题意就是混合背包的定义,,直接做就行

//cf
//#include <bits/stdc++.h>
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <string.h>
#include <algorithm>
#define aaa cout<<233<<endl;
#define endl '
'
#define pb push_back
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, ull> pii;
const int inf = 0x3f3f3f3f;//1061109567
const ll linf = 0x3f3f3f3f3f3f3f;
const double eps = 1e-6;
const double pi = 3.14159265358979;
const int maxn = 2e5 + 5;
const int maxm = 2e5 + 5;
const ll mod = 1e9 + 7;
int dp[maxn], c[maxn], w[maxn], num[maxn];
int n, m, v;//n为物品总数,v为背包容量
//01背包,该物品的代价,价值
void ZeroOnePack(int C, int W)
{
    for(int i = v; i >= C; --i)
        dp[i] = max(dp[i], dp[i - C] + W);
    return;
}
//完全背包,该物品的代价,价值
void CompletePack(int C, int W)
{
    for(int i = C; i <= v; ++i)
        dp[i] = max(dp[i], dp[i - C] + W);
    return;
}
//一次多重背包,该物品的代价,价值,数量
void OneMuitPack(int C, int W, int M)
{
    if(v <= C * M)//物品足够多时用完全背包
    {
        CompletePack(C, W);
        return;
    }
    else        //否则用二进制划分成若干件01背包的物品
    {
        int k = 1;
        while(k < M)
        {
            ZeroOnePack(k * C, k * W);//某一个划分成01背包的物品
            M -= k;
            k <<= 1;
        }
        ZeroOnePack(C * M, W * M);//剩下的一些物品
    }
    return;
}
int main()
{
//    ios_base::sync_with_stdio(0);
//    cin.tie(0);cout.tie(0);
    scanf("%d%d", &n, &v);
    for(int i = 1; i <= n; ++i)scanf("%d%d%d", &c[i], &w[i], &num[i]);
    memset(dp, 0, sizeof dp);
    for(int i = 1; i <= n; ++i)
    {
        if(num[i] == 1)
            ZeroOnePack(c[i], w[i]);
        else if(num[i] == -1)
            CompletePack(c[i], w[i]);
        else
            OneMuitPack(c[i], w[i], num[i]);
    }
    printf("%d
", dp[v]);
    return 0;
}

二维费用背包

二维费用指的就是相比之前的背包问题侑多了一个费用的影响因素,,对于一个物品有两个不同的代价以及其容量,,做法和前面的一样,dp数组增加一维就行了,,

例题

hdu-2159

转化成背包问题就是代价一是忍耐度,背包容量为m;代价二就是打怪,容量就是s,,求最大的价值(经验值)与n的大小关系,,,

//cf
//#include <bits/stdc++.h>
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <string.h>
#include <algorithm>
#define aaa cout<<233<<endl;
#define endl '
'
#define pb push_back
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, ull> pii;
const int inf = 0x3f3f3f3f;//1061109567
const ll linf = 0x3f3f3f3f3f3f3f;
const double eps = 1e-6;
const double pi = 3.14159265358979;
const int maxn = 1e3 + 5;
const int maxm = 2e5 + 5;
const ll mod = 1e9 + 7;
int dp[maxn][maxn], c[maxn], w[maxn], num[maxn];
int n, m, v;//n为物品总数,v为背包容量

int main()
{
//    freopen("233.in" , "r" , stdin);
//    freopen("233.out" , "w" , stdout);
//    ios_base::sync_with_stdio(0);
//    cin.tie(0);cout.tie(0);
    int n, m, k, s;
    while(~scanf("%d%d%d%d", &n, &m, &k, &s))
    {
        memset(w, 0, sizeof w);
        memset(c, 0, sizeof c);
        memset(dp, 0, sizeof dp);
        for(int i = 1; i <= k; ++i)
            scanf("%d%d", &w[i], &c[i]);
        int ans = inf;
        for(int i = 1; i <= k; ++i)
            for(int j = c[i]; j <= m; ++j)
                for(int k = 1; k <= s; ++k)
                {
                    dp[j][k] = max(dp[j][k], dp[j - c[i]][k - 1] + w[i]);
                    if(dp[j][k] >= n)ans = min(ans, j);
                }
        if(ans > m)printf("-1
");
        else       printf("%d
", m - ans);
    }
    return 0;
}

(loading)

分组背包

分组背包就是:一堆物品被划分成了K组,同一组的物品只能选择一个,或者这组不选,其他的条件和其他背包模型一样,,

解决方法,再加一层对每组背包的枚举

伪代码:

(for k=1 to K)

(for v=V to V)

(for item i in group k)

(F[v]=max(F[v], F[v-C_i]+W_i))

例题

hdu-1712

题意就是有n节课,每一课上几天的价值给你,,一共要上m节课,问最大的价值,,

把这道题看成容量为m的背包,装分为n组的物品最大的价值就行

int dp[maxn];
int main()
{
    int n, m;
    int a[maxn][maxn];
    while(scanf("%d%d", &n, &m) && n + m)
    {
        memset(a, 0, sizeof a);
        for(int i = 1; i <= n; ++i)
            for(int j = 1; j <= m; ++j)
                scanf("%d", &a[i][j]);
        memset(dp, 0, sizeof dp);
        for(int k = 1; k <= n; ++k)//枚举组数
            for(int j = m; j >= 0; --j)//枚举背包的容量
                for(int i = 1; i <= m; ++i)//枚举第k组的物品
                    if(i <= j)//保证能装下
                    dp[j] = max(dp[j], dp[j - i] + a[k][i]);
        printf("%d
", dp[m]);
    }
    return 0;
}

hdu-3033

题意就是一堆鞋子,某一些是一个牌子的,然后每一双鞋有一个价格(看作代价),一个价值,每个牌子至少取一双,问最大的价值,,,

与上一道不同的是每一组的物品不再是最多选一个了,,一组可以选多个,每一组都要选一个,,

dp[i][j]表示的是前i组在容量为j的背包所取的最大价值,,当前状态dp[i][j]可以由 前一状态在本组选一个物品 推来,也可以由 当前状态在本组再取一个物品 推来,,

初始化也不同了,,除了那一组都不选的那一行dp为零,,其他都为负,即未定义状态,,由这个判断是否有解,,

参考1
参考2
参考3

//hdu
//#include <bits/stdc++.h>
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <string.h>
#include <algorithm>
#define aaa cout<<233<<endl;
#define endl '
'
#define pb push_back
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 0x3f3f3f3f;//1061109567
const ll linf = 0x3f3f3f3f3f3f3f;
const double eps = 1e-6;
const double pi = 3.14159265358979;
const int maxn = 1e4 + 5;
const int maxm = 2e5 + 5;
const ll mod = 1e9 + 7;
int dp[11][maxn];
pii a[11][maxn];
int num[11];
int main()
{
//    freopen("233.in" , "r" , stdin);
//    freopen("233.out" , "w" , stdout);
//    ios_base::sync_with_stdio(0);
//    cin.tie(0);cout.tie(0);
    int n, m, K;
    while(~scanf("%d%d%d", &n, &m, &K))
    {
        memset(num, 0, sizeof num);
        for(int i = 1; i <= n; ++i)
        {
            int aa, bb, cc;
            scanf("%d%d%d", &aa, &bb, &cc);
            ++num[aa];
            a[aa][num[aa]].first = bb;
            a[aa][num[aa]].second = cc;
        }
        memset(dp, -1, sizeof dp);
        //for(int i = 0; i <= m; ++i)dp[0][i] = 0;
        memset(dp[0], 0, sizeof dp[0]);
        //不能写成memset(dp[0], 0, sizeof dp);
        for(int k = 1; k <= K; ++k)
            for(int i = 1; i <= num[k]; ++i)
                for(int j = m; j >= a[k][i].first; --j)
                {
                    if(dp[k][j - a[k][i].first] >= 0)
                        dp[k][j] = max(dp[k][j], dp[k][j - a[k][i].first] + a[k][i].second);
                    if(dp[k - 1][j - a[k][i].first] >= 0)
                        dp[k][j] = max(dp[k - 1][j - a[k][i].first] + a[k][i].second, dp[k][j]);
                }
        if(dp[K][m] < 0)printf("Impossible
");
        else            printf("%d
", dp[K][m]);
    }
    return 0;
}

这道题没怎么理解还,,
(loading)

剩下一些其他的内容,暂时先放放,,

原文地址:https://www.cnblogs.com/31415926535x/p/10403589.html