【缄*默】 #DP# 各种DP的实现方法(更新ing)

DP =「状态」+「阶段」+「决策」

基本原理 = 「有向无环图」+「最优子结构」+「无后效性」

 

目录

一. 线性DP

{ 1.概念引入 }

{ 2.例题详解 }

【例题1】caioj 1064 最长上升子序列

【例题2】caioj 1068 最长公共子序列

【例题3】洛谷 p1216 数字三角形

【例题4】poj 2279 Picture Permutations

【例题5】最长公共上升子序列(LCIS)

【例题6】三个字符串的最长公共子序列

【例题7】poj 3666 Making the Grade

【例题8】noip 2008 传纸条

二. 背包DP

(1)0/1背包

【思路1】初步分析

【思路2】滚动数组优化空间

【思路3】转化为一维

【例题】poj 1015  Jury Compromise

【 0/1 背包练习题】

(2)完全背包

【具体实现】转化为一维

【例题】TYVJ 1172

【完全背包练习题】

(3)多重背包

【思路1】直接拆分法

【思路2】二进制拆分法

【例题】poj 1742 Coins

【多重背包练习题】

(4)分组背包

【分组背包练习题】

三. 区间DP

{ 1. 概念引入 }

{ 2. 例题详解 }

【例题1】洛谷 p1430 序列取数

【例题2】洛谷 p4170 涂色

【例题3】洛谷 p4342 Polygon

【例题4】洛谷 p1880 石子合并

【例题5】洛谷 p1063 能量项链

【例题6】凸多边形的划分

【例题7】括号配对

【例题8】括号配对(升级版)

【例题9】loj 10151 分离与合并

【例题10】洛谷 p1005 矩阵取数

四. 树形DP

{ 1.经典问题 }

{ 2.相关知识点总结 }

{ 3.例题总结 }

【例题1】洛谷 p1352 没有上司的舞会

【例题2】洛谷 p2014 选课(背包类树形DP)

五. 图论DP

六. 数位DP+状压DP

七. 倍增优化DP

八. 数据结构优化DP

九. 单调队列优化DP

十. 斜率优化

十一. 四边形不等式


 

零. 相关概念辨析

(1)递归和递推

1. 递归从最终目标出发,将复杂问题化为简单问题,逆向解答。

2. 递推从简单问题出发,一步步的向前发展,正向解答。

(2)记忆化搜索

先将数组初始化,再在递归的过程中判断此处是否已经计算过。

(3)刷表法和填表法

填表法:利用方程和上一状态推导现在的状态(已知条件,填答案)。

刷表法:利用当前的状态,把有关联的下一状态都推出来。

 

一. 线性DP

{ 1.概念引入 }

解决一些线性区间上的最优化问题,会用到DP的思想,这种问题可以叫做线性DP。

DP的阶段沿各个维度线性增长,从边界点开始,有方向地向状态空间转移、扩展。

若“决策集合中的元素只增多不减少”,可以维护一个变量记录决策集合信息,避免重复扫描。

需要考虑:一个后续阶段的未知状态,需要哪些前阶段的已知状态去更新。

经典模型:最长上升子序列(LIS)、最长公共子序列(LCS)、最大子序列和、数字三角形等。

那么首先我们从这几个经典的问题出发开始对线性DP的探索。

 

{ 2.例题详解 }

【例题1】caioj 1064 最长上升子序列

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

/*【caioj 1064】最长上升子序列
n个不相同的整数组成的数列a(1)、a(2)、……、a(n)。
求出其中最长的上升序列(LIS)的长度。 */

//【线性DP】【匹配转移】
//状态:f[i]表示以a[i]为结尾的、最长上升子序列的长度。
//方程:f[i]=max{ f[j]+1 (j<i,a[j]<a[i]) };
//边界:f[0]=0; f[i]=1; 目标:max{f[i]};

int a[1009],f[1009];

int main(){
    int n,ans=0; scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=1;i<=n;i++) f[i]=1;
    for(int i=2;i<=n;i++)
        for(int j=i-1;j>=f[i];j--) 
        //↑↑↑【剪枝】j>=f[i]:如果j小于目前长度,不可能使答案更新
            if(a[j]<a[i]) f[i]=max(f[i],f[j]+1);
    for(int i=1;i<=n;i++) if(ans<f[i]) ans=f[i];
    printf("%d\n",ans); return 0;
}

【例题2】caioj 1068 最长公共子序列

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

/*【caioj 1068】最长公共子序列
给出两个字符串 S1 和 S2 ,求它们最长公共子序列的长度。  */

//【线性DP】【匹配转移】
//状态:f[i][j]表示前缀子串s[1~i]与t[1~j]的最长公共子序列长度。
//方程:f[i][j]=max{ f[i-1][j], f[i][j-1], f[i-1][j-1]+1(if s[i]=t[j]) };
//边界:f[i][0]=f[0][j]=0; 目标:f[n][m];

const int maxn=1009;
char s[maxn],t[maxn]; //两个字符串
int f[maxn][maxn]; //前缀子串s[1~i]与t[1~j]的最长公共子序列长度

int main(){
    scanf("%s%s",s,t);
    int lens=strlen(s),lent=strlen(t);
    memset(f,0,sizeof(f));
    for(int i=1;i<=lens;i++)
        for(int j=1;j<=lent;j++){
            f[i][j]=max(f[i-1][j],f[i][j-1]);
            if(s[i-1]==t[j-1]) //如果元素公共
                f[i][j]=max(f[i][j],f[i-1][j-1]+1); //匹配字符成功
        }
    printf("%d\n",f[lens][lent]);
    return 0;
}

【例题3】洛谷 p1216 数字三角形

  • [ 数字三角形 ] 问题的特点:具有大量重叠子问题。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

/*【洛谷p1216】数字三角形
一个三角形,每一步可以走到左下方的点也可以到达右下方的点。
查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。 */

//【线性DP】【路径转移】
//状态:f[i][j]表示从左上角走到第i行第j列,得到的最大的和。
//方程:f[i][j]=a[i][j]+max{f[i-1][j],f[i-1][j-1](if j>1)};
//边界:f[1][1]=a[1][1];
//目标:max{f[N][j]};

int f[1009][1009],a[1009][1009];

int main(){
    int n,maxx=0; scanf("%d",&n);
    memset(f,0,sizeof(f));
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++){
            scanf("%d",&a[i][j]);
            f[i][j]=a[i][j];
        }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++){
            if(j>1) f[i][j]+=max(f[i-1][j],f[i-1][j-1]);
            else f[i][j]+=f[i-1][j];
        }
    for(int j=1;j<=n;j++) 
        maxx=max(maxx,f[n][j]);
    printf("%d\n",maxx);
    return 0;
}

【例题4】poj 2279 Picture Permutations

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

/*【poj2279】合影
有n个学生,站成k排,给出每排的人数,
要求每排身高从左往右递增,每列身高从前往后递增。
问有多少种排列方式。 */

//【线性DP】【匹配转移】【方案数DP】
//状态:f[a1,a2,a3,a4,a5]表示每排站了a1,a2,a3,a4,a5个人的方案数。
//方程:当a1<N1,f[a1+1,a2,a3,a4,a5]+=f[a1,a2,a3,a4,a5];
//      当a2<N2且a1>a2时,f[a1,a2+1,a3,a4,a5]+=f[a1,a2,a3,a4,a5];
//边界:f[0,0,0,0,0]=1; 目标:f[N1,N2,N3,N4,N5];

long long f[31][31][31][31][31]; //会MLE 
int lim[6]; //每排的人数

int main(){
  int n,k; while(cin>>n&&n) {
    memset(f,0,sizeof(f)); 
    memset(lim,0,sizeof(lim));
      for(int i=1;i<=n;i++) cin>>lim[i];
      f[0][0][0][0][0]=1;
      for(int i=0;i<=lim[1];i++)
        for(int j=0;j<=lim[2];j++)
          for(int k=0;k<=lim[3];k++)
            for(int a=0;a<=lim[4];a++)
              for(int b=0;b<=lim[5];b++){
                if(i+1<=lim[1]) 
                  f[i + 1][j][k][a][b]+=f[i][j][k][a][b];
                if(j+1<=lim[2]&&j<i) 
                  f[i][j + 1][k][a][b]+=f[i][j][k][a][b];
                if(k+1<=lim[3]&&k<j&&k<i)
                  f[i][j][k + 1][a][b]+=f[i][j][k][a][b];
                if(a+1<=lim[4]&&a<i&&a<j&&a<k)
                  f[i][j][k][a + 1][b]+=f[i][j][k][a][b];
                if(b+1<=lim[5]&&b<i&&b<j&&b<k&&b<a)
                  f[i][j][k][a][b + 1]+=f[i][j][k][a][b];
              }
    cout<<f[lim[1]][lim[2]][lim[3]][lim[4]][lim[5]]<<endl;
  }
  return 0;
}

【例题5】最长公共上升子序列(LCIS)

  • 给出有 n 个元素的数组 a[ ],m 个元素的数组 b[ ] 。
  • 求出它们的最长上升公共子序列的长度。
a[] data:
5
1 4 2 5 -12

b[] data:
4
-12 1 2 4

LCIS is 2
LCIS 所含元素为 1 4

1.确定状态

dp[i][j] 表示以 a 数组的前 i 个整数与 b 数组的前 j 个整数以 b[j] 为结尾构成的公共子序列长度。

需要注意,以 b[j] 结尾构成的公共子序列的长度不一定是最长公共子序列的长度!

2.确定状态转移方程

(1)当 a[i] == b[j] 时,我们只需要在前面找到一个能将 b[j] 接到后面的最长的公共子序列

之前最长的公共子序列在哪呢?首先我们要去找的 dp[ ][ ] 的第一维必然是 i - 1 。

第二维呢?那就需要枚举 b[1]...b[j-1] 了,因为你不知道这里面哪个最长且哪个小于 b[j]

可不可能不配对呢?也就是在 a[i] == b[j] 的情况下,需不需要考虑 dp[i][j] == dp[i-1][j] 的决策呢?

答案是不需要。如果 b[j] 不和 a[i] 配对,就是和之前的 a[1]...a[j-1] 配对,必然没有和 a[i] 配对优越。

(b[j] 和 a[i] 配对之后转移为 max(dp[i-1][k])+1,和前面的 i1 配对则是 max(dp[i1-1][k])+1 。)

(2)当 a[i] != b[j] 时, 因为 dp[i][j] 是以 b[j] 为结尾的LCIS,

如果 dp[i][j] > 0,那么就说明 a[1]..a[i] 中必然有一个整数 a[k] 等于 b[j] 。

因为 a[k] != a[i] ,那么 a[i] 对 dp[i][j] 没有贡献,于是我们不考虑它照样能得出 dp[i][j] 的最优值。

所以在 a[i] != b[j] 的情况下必然有 dp[i][j] == dp[i-1][j]

  • 综上所述,我们可以得到状态转移方程:
  • ① a[i] != b[j], dp[i][j] = dp[i-1][j]。
  • ② a[i] == b[j], dp[i][j] = max(dp[i-1][k]+1) (1 <= k <= j-1 && b[j] > b[k])。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

/* 最长公共上升子序列(LCIS)
给出有 n 个元素的数组 a[ ],m 个元素的数组 b[ ] 。
求出它们的最长上升公共子序列的长度。 */

//【线性DP】【匹配转移】
//状态:dp[i][j]表示【a的前i个】与【b的前j个】且【以b[j]为结尾】构成的LCIS长度。
//方程:① a[i]!=b[j],dp[i][j]=dp[i-1][j]。
//      ② a[i]==b[j],dp[i][j]=max(dp[i-1][k]+1) (1<=k<=j-1 && b[j]>b[k])。
//边界:dp[0][i]=0,dp[i][0]=0;
//目标:中间过程中保存更新的答案ans。
//复杂度:O(N * M^2)

int a[3005],b[3005],dp[505][505]={0};

int lengthOfLCIS(int *a,int n,int *b,int m) {
    int ans=0,max_dp=0; //max_dp记录上一个LCIS长度最大值
    for(int i=0;i<n;++i)
        for(int j=0;j<m;++j){
            dp[i+1][j+1]=dp[i][j + 1];
            if(a[i]==b[j]){
                max_dp=0; //每次匹配时记得清零
                for(int k=0;k<j;++k) //满足上升关系
                    if(b[j]>b[k] && max_dp<=dp[i][k+1])
                        max_dp=dp[i][k+1]; //记录上一个LCIS长度最大值
                dp[i+1][j+1]=max_dp+1; //此处也能匹配
            }
            ans=ans>dp[i+1][j+1]?ans:dp[i + 1][j + 1]; //更新ans
        }
    return ans; //复杂度:O(N * M^2)
}

int main(){
    int n,m,T; cin>>T;
    while(T--){
        scanf("%d",&n);
        for(int i=0;i<n;i++) scanf("%d",&a[i]);
        scanf("%d",&m);
        for(int i=0;i<m;i++) scanf("%d",&b[i]);
        printf("%d\n",lengthOfLCIS(a,n,b,m));
        if(T) printf("\n"); //注意输出格式
    }
    return 0;
}

3.可以发现,这是O(N * M^2) 算法。

【优化1】O(N * M * log(M))算法

分析: a[i]==b[j]时在前面找到一个能将 b[j] 接到后面的最大长度的过程,是可以优化的。

设置辅助数组 len[ i ] = value,代表【长度为 i 的所有LCIS】中、【最后一位数字的最小值】为value。

Q: 为什么要这样设计辅助数组 len[] ?

需要优化的过程是一个查找过程( dp[i][j] = max(dp[i-1][k]+1) (1 <= k <= j-1 && b[j] > b[k]) )

实际上,这个查找过程是在一个一维数组中查找满足条件 b[j] > b[k] 的最大值

可以采用hash或者是二分将查找问题降到一个可以接受的复杂度,大体判断二分似乎更适合。

由于二分查找的对象必须具有单调性,而在最长公共上升子序列中,

所以的子序列是具有单调性的,所以我们可以将子序列的结尾数字作为数组中的值。

维护一个最小值,这样才能够优化判断 b[j] 到底能接在哪个位置。

Q: len[] 更新策略?

用二分查找找到 b[j] 能“嫁接”到之前子序列中的最长长度。

( ind是二分出来的len[]数组下标,根据上面的定义,ind就是以b[j]结尾的最长公共上升子序列长度 )。

a[i] == b[j],这时候 b[j] 将“嫁接”到之前子序列中的合适位置,同时更新 len[ind] = b[j] 。

a[i] != b[j],我们也需要更新 len[] 的信息,因为后面的更新需要前面的数据。

当 b[j] 与 a[i] 适配,需要查看 b[j] 与前 i - 1个数字所构成的以 b[j] 为结尾数字的最长的公共子序列。

如果有多个跟 dp[ i ][ j + 1 ]相同长度的子序列,如果比 d[j] 小则更新 len[dp[i][j + 1]]的值。

// 复杂度 O(N * M * log(M))

int binary_search(int *len, int length, int value) {
    int l = 0, r = length - 1, mid;
    while (l < r) {
        mid = (l + r) >> 1;
        if (len[mid] >= value) r = mid; 
        else l = mid + 1;
    }
    return l; //满足b[j]>len[LCIS长度]的最大LCIS长度
}

int lengthOfLCIS(int *a, int n, int *b, int m) {
    int ans = 0;
    int dp[505][505] = {0};
    int len[505];
    for (int i = 0 ; i < n ; ++i) {
        len[0] = INT_MIN;  
        for (int j = 0 ; j < m ; ++j) {
            len[j + 1] = INT_MAX;
            int ind = binary_search(len, j + 2, b[j]);
            if (a[i] != b[j]) {
                if (b[j] < len[dp[i][j + 1]])  
                // 只有当b[j]的值小于当前len[dp[i][j + 1]]的最小值时才能更新
                    len[dp[i][j + 1]] = b[j];
                dp[i + 1][j + 1] = dp[i][j + 1];
            } else {
                dp[i + 1][j + 1] = ind;
                len[ind] = b[j];
            }
            ans = ans > dp[i + 1][j + 1] ? ans : dp[i + 1][j + 1];
        }
    }
    return ans;
}

其他优化请戳 这里 ...


【例题6】三个字符串的最长公共子序列

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

/*【caioj 1073】三个字符串的最长公共子序列
给出三个字符串,求它们最长公共子序列。 */

//【线性DP】【匹配转移】【多维DP】
//状态:a[i][j][k]表示第一个字符串的前i个字符,第二个字符串的前j个字符和
//      第三个字符串的前k个字符的最长公共子序列的长度。

const int MAXN = 101;
int a[MAXN][MAXN][MAXN];

//↓↓↓函数返回三个字符串的最长公共子序列的长度
int commonLen(string strA, string strB, string strC){
    
    int n1 = strA.size();
    int n2 = strB.size();
    int n3 = strC.size();

    for(int i=0;i<=n1;i++) //初始化:某串长度为0的情况
        for(int j=0;j<=n2;j++)
            for(int k=0;k<=n3;k++)
                if(i==0||j==0||k==0) a[i][j][k]=0;

    for(int i=1;i<=n1;i++)
        for(int j=1;j<=n2;j++)
            for(int k=1;k<=n3;k++){
                if(strA[i-1]==strB[j-1]&&strB[j-1]==strC[k-1])
                    a[i][j][k]=a[i-1][j-1][k-1]+1;
                else a[i][j][k]=max(max(a[i-1][j][k],a[i][j-1][k]),a[i][j][k -1]);
            }
    
    return a[n1][n2][n3];
}
 
int main(){
    string str1, str2, str3;
    while(cin >> str1 >> str2 >> str3)
        cout << commonLen(str1, str2, str3) << endl;
    return 0;
}

【例题7】poj 3666 Making the Grade

  • 给出长度为n的整数数列,每次可以将一个数加1或者减1,
  • 最少要多少次可以将其变成不严格的单调递增或者单调递减。

【分析】首先要证明ans最小化的情况下,一定存在一种构造方式,

使得b数组在a数组中全部出现过。证明方法简述:

记原来的数从小到大排序后分别是a1 a2 a3...an,修改后分别是b1 b2 b3...bn。

为了方便描述,在数轴上标出这些点,称为关键点。

  • 假设存在as<bi<=bi+1<=...<=bj<as+1。
  • 情况一:如果这些b都相等,那么把这些b都改成as或者as+1,肯定会有一种更优。
  • 情况二:如果不全相等,那么肯定存在bp bp+1 bp+2...bq,他们的值相等,
  • 那么把他们移到左边的关键点或者右边的关键点,肯定有一种会更优。
  • 不断这样移动,最后就变成情况一了。

综上至少存在一种最优方案,最终的所有数都是原来的某个数。

最优解与两个参数有关:当前位置处理后的最大值mx,和当前情况的处理代价值cost。

显然最大值mx最小最好(这样第i+1个值花费代价较小),cost也是最小最好。

用dp[i][j]表示:前i个数构成的序列,这个序列最大值为j,dp[i][j]的值代表相应的cost。

状态转移方程:dp[i][j]=abs(j-w[i])+min(dp[i-1][k]); (k<=j)

这个表格是根据转移方程写出来的 dp 数组。

右边没填充的是因为填充的数字肯定比前面的数字大,无用,

因此,在求 min( dp[ i - 1 ][ k ] ) 时,既然右边更大、则无需考虑。

又从表格中可以看出:这里的 k 无需从1遍历到 j 。

只要在对 j 进行 for 循环的时候不断更新一个 dp[ i - 1 ][ j ] 的最小值:

mn=min(mn,dp[i-1][j]),则 dp[ i ][ j ]=abs( j - w[ i ] ) + mn 。

这样改进之后即可从本来的时候时间复杂度O(NMM)改进为O(NM);

【优化】用离散化思想改进,因为N=2000。远小于 A[ i ] 的最大值。

离散化:将序列排序一下,然后用位置的前后关系来制定其值,这样时间复杂度变成O(N^2)。

#include <bits/stdc++.h> //poj禁用
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

/*【poj 3666】Making the Grade
给出长度为n的整数数列,每次可以将一个数加1或者减1,
最少要多少次可以将其变成不严格的单调递增或者单调递减。 */

//【线性DP】【匹配转移】
//关联因素:当前位置编号(当前最大值编号)mx,和当前情况的代价值cost。
//状态:dp[i][j]表示前i个数,当前最大值编号为j时,相应最小的cost。
//方程:dp[i][j]=abs(j-a[i])+min(dp[i-1][k]); (k<=j)
//优化:for循环记录min(dp[i-1][k]),即mn=min(mn,dp[i-1][j])。
//边界:dp[0][i]=0; 目标:min{dp[n][i]};
//复杂度:O(N^2)(记录mn+离散化之后)

const int N=2005;
int n,a[N],b[N];
long long int dp[N][N];

void solve(){
    for(int i=1;i<=n;i++){
        ll mn=dp[i-1][1];
        for(int j=1;j<=n;j++){
            mn=min(mn,dp[i-1][j]);
            dp[i][j]=abs(a[i]-b[j])+mn;
        }
    }
    long long ans=dp[n][1];
    for(int i=1;i<=n;i++)
        ans=min(ans,dp[n][i]);
    printf("%lld\n",ans);
}

int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]), b[i]=a[i]; //b[i]用于离散化
    sort(b+1,b+n+1); //***注意:此处不需要去重***
    solve(); return 0;
}

【例题8】noip 2008 传纸条

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

/*【洛谷 P1006】传纸条
给出M*N(1<=M,N<=50)的矩阵,在这个矩阵内找出两条从1,1到m,n的路径。
(一条从1,1到m,n;一条从m,n到1,1),且路径之上的权值和k最大。
PS:(1,1)和(m,n)权值为0。 */

const int maxn=60;
int a[maxn][maxn];

int F[2*maxn][maxn][maxn]; 
/* 第一维度维护的是纵坐标与横坐标的和。
   =>在同一斜线上 =>结合纵坐标就可以找到确切位置。
   第二维度维护的是相对在左边的点的纵坐标。
   第三维度维护的是相对在右边的点的纵坐标。*/

//F[sum][i][j]=max{F[sum-1][i][j],F[sum-1][i][j-1],F[sum-1][i-1][j],F[sum-1][i-1][j-1]};

int main(){
    int m,n; scanf("%d%d",&m,&n);
    for(int i=1;i<=m;i++)
        for(int j=1;j<=n;j++) scanf("%d",&a[i][j]);
    memset(F,-1,sizeof(F));//赋初值为-1 
    F[2][1][1]=0; //最初的点,在左上角,好感度为0 [初始化]
    for(int k=3;k<m+n;k++) //纵坐标与横坐标的和
        for(int i=1;i<n;i++)
            for(int j=i+1;j<=n;j++){
                int s=F[k][i][j]; //讨论各种情况,更不更新
                if(F[k-1][i][j]>s) s=F[k-1][i][j];
                if(F[k-1][i-1][j]>s) s=F[k-1][i-1][j];
                if(F[k-1][i][j-1]>s) s=F[k-1][i][j-1];
                if(F[k-1][i-1][j-1]>s) s=F[k-1][i-1][j-1];
                if(s==-1) continue; 
                //↑↑↑当s为-1时,说明四种情况都不能到该点,故不存在
                F[k][i][j]=s+a[k-i][i]+a[k-j][j];
                //↑↑↑该点的值为最大的前一个值与当前F[k][i][j]表示两点的值的和
            }
    printf("%d",F[m+n-1][n-1][n]); //因为i永远小于j,所以右下角的点不会求到
    //但是到右下角只有一种情况,在右下角的上面和右下角的左边,直接输出即可
    return 0;
} 

【例题9】tyvj 1061 移动服务 戳这里

【例题10】SGU 167 I - country 戳这里

【例题11】Cookies 看进阶指南吧Σ( ° △ °|||)

 

二. 背包DP

背包问题的经典讲解可以参见背包详解,还有背包六问

 

(1)0/1背包

问题(0/1背包): 有n个体积和价值分别为vi和wi的物品。
从这些物品中挑出总重量不超过m的物品,求所有挑选方案中价值总和的最大值。

0/1背包:常用于方案数DP,最优值DP。

思想:对于每个物品,比较 [ 放的价值大 ] 还是 [ 不放的价值大 ] 。

转换成方程: f [ i ] [ j ] = Max{ f [ i − 1 ] [ j ] ,f [ i − 1 ] [ j −v [ i ] ] + w [ i ] } ;

 

【思路1】初步分析

//【背包DP】【01背包】初步分析
//状态:dp[i][j]表示前i个物品、选择体积和为j的、所得的最大价值和。
//方程:dp[i][j]=max{dp[i-1][j](不选i),dp[i-1][j-vi]+wi(选i)};
//初值:dp[0][0]=0; 其余均为负无穷。
//目标:max{dp[n][j]}(0<=j<=m); 

memset(dp,0xcf,sizeof(dp)); //-INF
dp[0][0]=0;
for(int i=1;i<=n;i++){
    for(int j=0;j<=m;j++) dp[i][j]=dp[i-1][j];
    for(int j=v[i];j<=m;j++) //注意:从v[i]开始
        dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);
}
int ans=0;
for(int j=0;j<=m;j++) ans=max(ans,dp[n][j]);

 

【思路2】滚动数组优化空间

//【背包DP】【01背包】滚动数组
//优化:滚动数组优化,降低空间开销。
//实现:把阶段i的状态存储在第一维下标为i&1的二维数组中。
//当i为奇数时,i&1=1; 当i为偶数时,i&1=0。
//DP的状态相当于在dp[0][],dp[1][]两个数组中交替转移,节省空间。
//状态:dp[i&1][j]表示前i个物品、选择体积和为j的、所得的最大价值和。
//方程:dp[i&1][j]=max{dp[(i-1)&1][j](不选i),dp[(i-1)&1][j-vi]+wi(选i)};
//初值:dp[0][0]=0; 其余均为负无穷。
//目标:max{dp[n&1][j]}(0<=j<=m); 

int dp[2][max_m+1];
memset(dp,0xcf,sizeof(dp)); //-INF
dp[0][0]=0;
for(int i=1;i<=n;i++){
    for(int j=0;j<=m;j++) dp[i&1][j]=dp[(i-1)&1][j];
    for(int j=v[i];j<=m;j++) //注意:从v[i]开始
        dp[i&1][j]=max(dp[i&1][j],dp[(i-1)&1][j-v[i]]+w[i]);
}
int ans=0;
for(int j=0;j<=m;j++) ans=max(ans,dp[n&1][j]);

 

【思路3】转化为一维

转换成一维方程,需要  逆序 

逆序的目的是:保证后一个 f [ v ] 和 f

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值