动态规划(Dynamic Programming)dp,这周主要学习了背包问题、线性dp
dp问题的核心在于状态表示与状态计算。
一般先思考整个问题需要用几维的状态来表示,每一个状态的含义是什么,再考虑如何将每一步的状态计算出来。状态表示,如二维状态f(i,j),需要理解它表示的集合是什么(每一个状态都用一个集合表示,如在背包问题中表示的是所有的选法,只从前i个物品中选,且总体积<=j),本身的值的属性是什么(常见有最大值、最小值、数量等);状态计算,表示的是集合的划分,划分为若干个子集,使每个子集都可以用前面更小的子集计算出来,如背包问题中f(i,j)分为两大类,一类是不包含第i个物品,另一类是包含第i个物品(一般需要保证每个元素不重、不漏)。
dp问题的优化一般是对dp代码或方程做一个等价变形。一般先写出dp再考虑优化。
关于下标问题,一般i是从0开始,如果表达式中涉及i-1的话,则设i从1开始。
时间复杂度分析:状态数量*转移的计算量(需要计算的数量)
边界问题的考虑:可以根据题意先初始化所有状态为INF或-INF(与prim、dijkstra相似)
背包问题
01背包(每个物品只能用一次)
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。输入格式
第一行两个整数,N,V用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
状态表示:
f[i][j]表示选择第i件物品,容量不超过j的最大价值。
状态计算:
将f[i]表示的所有选法分成两大类
1. 选法中不含 i , 即从 1 ~ i-1中选,且总体积不超过j,即 f[i-1]
2. 选法中包含 i ,即从 1 ~ i 中选,包含 i,且总体积不超过 j
可以先把第 i 个物品拿出来,即从第 1 ~ i-1中选,且总体积不超过 j-v[i],即f[i-1][j-v[i]]+w[i]
得到状态转移方程:
f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i]);
dp代码
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
f[i][j] = f[i - 1][j];//默认为选i件超过容量
if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
}
优化代码(没看懂,待解决,滚动数组)
(更新一下,已解决,详见01背包问题再探__qingche的博客-CSDN博客)
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]);
}
}
完全背包(每件物品有无限个)
有 N 种物品和一个容量是 V的背包,每种物品都有无限件可用。
第 i种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
状态表示:f[i][j],所有只考虑前i个物品,且总体积不大于j的所有选法,值表示最大价值。
状态计算:
集合的划分,01背包问题是按第i个物品选与不选分为两大类,完全背包问题每个物品都有无限个,按照第i个物品选多少个来分,可以分成很多组,但由于有最大容量的限制,第i个物品可以从0选到k个。
不选第i件物品f[i-1][j]
选k个第i件物品:
1.去掉k个物品i
2.求去掉后的max,f[i-1][j-k*v[i]]
3.加上k个物品i的最大值,f[i-1][j-k*v[i]]+k*w[i]
得到状态转移方程
f[i][j]=max(f[i-1][j],f[i-1][j-k*v[i]]+k*w[i])
dp代码
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-1][j],f[i-1][j-k*v[i]]+k*w[i]);
优化代码(没看懂,待解决,与上面相同也是滚动数组)
(更新,已解决,可以先看01背包优化理解后再看这个推导方程)
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]);
多重背包(每件物品的数量不一样)
有 N 种物品和一个容量是 V 的背包。
第 ii 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。输入格式
第一行两个整数,N,V用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
状态表示:f[i][j],所有只考虑前i个物品,且总体积不大于j的所有选法,值表示最大价值。
状态计算:类似于完全背包,枚举k从0到s[i],循环条件为不大于背包容量且不多于i物品的数量即可
状态转移方程
f[i][j]=max(f[i-1][j],f[i-1][j-k*v[i]]+k*w[i])//k从0到s[i]
dp代码
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-1][j],f[i-1][j-k*v[i]]+k*w[i]);
优化代码(二进制优化,没看懂待解决)
cin >> n >> m;
int cnt = 0; //分组的组别
for(int i = 1;i <= n;i ++)
{
int a,b,s;
cin >> a >> b >> s;
int k = 1; // 组别里面的个数
while(k<=s)
{
cnt ++ ; //组别先增加
v[cnt] = a * k ; //整体体积
w[cnt] = b * k; // 整体价值
s -= k; // s要减小
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;
分组背包(不同组互斥,每组选一)
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行有两个整数 N,V用空格隔开,分别表示物品组数和背包容量。
接下来有 NN 组数据:
- 每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量;
- 每组数据接下来有 Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;
输出格式
输出一个整数,表示最大价值。
状态表示:f[i][j],只从前i组物品中选,且总体积不大于j的所有选法,值表示价值最大值。
状态计算:状态计算可以理解为集合的划分过程,多重背包问题是枚举第i个物品选几个,分组背包问题是枚举第i组物品选哪个(选第i组的第1个物品、第2个物品...)
若不选第i组的物品f[i-1][j];若选第i组物品中的第k个f[i-1][j-v[i][k]]+w[i][k]
得到状态转移方程
f[j]=max(f[j],f[j-v[i][k]]+w[i][k])
从前往后枚举每一组,从大到小枚举所有体积,再枚举所有的选择,得到dp代码
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
f[i][j]=f[i-1][j];
for(int k=0;k<s[i];k++){
if(j>=v[i][k])
f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);
}
}
}
优化代码(滚动数组,思想跟01背包、完全背包一致,待解决)
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)
f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
线性dp
线性dp是指状态转移方程存在一个线性的递推关系。可以是一维线性、二维线性等
数字三角形
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
输入格式
第一行包含整数 n,表示数字三角形的层数。
接下来 n 行,每行包含若干整数,其中第 i行表示数字三角形第i 层包含的整数。
输出格式
输出一个整数,表示最大的路径数字和。
状态表示:f[i][j]表示所有从起点走到(i,j)的路径,值为路径上数字之和的最大值
状态计算(集合的划分,如何分类):直观来看考虑分成左上路径和右上路径两类,f[i-1][j-1]+a[i][j]表示从起点到目标点左上的路径数字最大值,f[i-1][j]+a[i][j]表示从起点到目标点右上的路径数字最大值
得到状态转移方程
f[i][j]=max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j])
初始化时需注意要把上方两侧都初始化(j从0到i+1)
for(int i=1;i<=n;i++){
for(int j=0;j<=i+1;j++){
f[i][j]=-INF;
}
}
f[1][1]=a[1][1];
dp代码
for(int i=2; i<= n; i++ )
for(int j=1; j<= i; j++ )
f[i][j] = max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j]);
int res=-INF;
for(int i=1;i<=n;i++) res=max(res,f[n][i]);
cout<<res<<endl;
最长上升子序列
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N。
第二行包含 N 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
子段:属于集合且要求连续 子序列:属于集合,可以不连续
状态表示:f[i]表示所有以第i个数结尾的上字子序列,值为以i为结尾的所有上升子序列长度的最大值
状态计算:考虑所有以i为结尾的子序列如何分类,状态转移方程需要存在一种递推关系,状态计算表示的是集合的划分,划分为若干个子集,使每个子集都可以用前面更小的子集计算出来。以i为结尾的最长上升子序列都是i,以第i-1个数来分类,分成0、1...i-1。若a[j]<a[i],则满足条件使最长上升子序列为f[j]+1。
得到状态转移方程
a[i]=max(f[j]+1),满足a[j]<a[i],j从0到i-1
dp代码
for(int i=1;i<=n;i++){
f[i]=1;//最小值(默认)最长上升子序列为自身,长度是1
for(int j=1;j<i;j++)
if(f[j]<f[i]) f[i]=max(f[i],f[j]+1);
}
//枚举一遍f[i]找出最大值即可
额外思考一下,如何将这个最长的上升子序列保存下来。
for(int i=1;i<n;i++){
f[i]=1;
g[i]=0;
for(int j=1;j<i;j++)
if(f[i]<f[j]+1){
f[i]=f[j]+1;
g[i]=j;//记录f[i]是从哪个状态转移过来的
}
}
int k=1;
for(int i=0;i<=n;i++)
if(f[k]<f[i])
k=i;
for(int i=0,len=f[k];i<len;i++){
cout<<a[k]<<" ";
k=g[k];//根据记忆数组可以得知k是从哪个状态转移过来的,效果是逆序输出最长上升子序列
}
最长公共子序列
给定两个长度分别为 N 和 M 的字符串 A 和 B,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少。
输入格式
第一行包含两个整数 N 和 M。
第二行包含一个长度为 N 的字符串,表示字符串 A。
第三行包含一个长度为 M 的字符串,表示字符串 B。
字符串均由小写字母构成。
输出格式
输出一个整数,表示最大长度。
状态表示: 因为有两个序列,考虑用二维表示。f[i][j]表示所有在第一个序列的前i个字母中出现,且在第二个序列的前j个字母中出现的子序列,值为所有公共子序列长度的最大值
状态计算:划分依据是两个序列末尾的字符是否相等。若相等,最大值为f[i-1][j-1]+1,不相等的话,两个字符一定有一个可以抛弃,对f[i-1][j]和f[i][j-1]取max即可表示(f[i-1][j]表示a[i]不包含在内,f[i][j-1]表示b[j]不包含在内),而f[i-1][j-1]两个字符都抛弃的情况包含在f[i][j-1]和f[i-1][j]中,所以不必再额外讨论。
得到状态转移方程
f[i][j]=f[i-1][j-1]=1,条件为a[i]==b[j]
f[i][j]=max(f[i][j-1],f[i-1][j]),条件为a[i]!=b[j]
i从1遍历到n,j从1遍历到m
dp代码
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i] == b[j]) f[i][j] = f[i - 1][j - 1] + 1;
else f[i][j] = max(f[i - 1][j], f[i][j - 1]);
}
}
下周计划学习区间dp、树形dp等,套路基本摸清楚了,需要阅读更多博客多见题。背包问题算是提前预习了,等老师再讲的时候应该会有更深的理解和体会。期末课少了也有更多空闲时间来看博客了,很充实。