动态规划
1 背包问题九讲
1.1 0-1背包、满包
有N件物品和一个容量为V 的背包。放入第i件物品耗费的空间是Ci,得到的价值是Wi。求解将哪些物品装入背包可使价值总和最大。
//二维
for(int i=1;i<=n;i++)
{
for(int j=1;j<=v;j++)
{
if(j>=volume[i])
dp[i][j] = max(dp[i-1][j], m[i-1][j-volume[i]]+value[i]);
else
dp[i][j] = dp[i-1][j];
}
}
// 一维
for(int i=1;i<=n;i++)
{
for(int j=v;j>=volume[i];j--)
dp[j] = max(dp[j], dp[j-volume[i]]+value[i]);
}
满包
满背包与普通背包唯一不同的点就是恰好装满背包所能取得的最大价值,主要区别在于初始化,满包问题的初始条件: ,
1.2 完全背包
有N种物品和一个容量为V 的背包,每种物品都有无限件可用。放入第i种物品的耗费的空间是Ci,得到的价值是Wi。求解:将哪些物品装入背包,可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。
for(int i=1;i<=n;i++)
{
for(int j=volume[i];j<=v;j++)
dp[j] = max(dp[j], dp[j-volume[i]]+value[i]);
}
这个代码与01背包的代码只有 的循环次序不同而已。为什么这样一改就可行呢?首先想想为什么01背包中要按照 的逆序来循环。这是因为要保证第 次循环中的状态 是由状态 递推而来。换句话说,这正是为了保证每件物品只选一次,在考虑“选入第 件物品”时的策略,是依据一个没有选入第 件物品的子结果 。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第 种物品”时,却正需要一个可能已选入第 种物品的子结果 ,所以就可以并且必须采用 的顺序循环。这就是这个简单的程序为何成立的道理。
1.3 多重背包
给n种物品和一个容量为v的背包,每种物品最多有num[i]件可用,每个物品都有一个体积volume[i]和价值value[i],求背包最多能装多少价值的物品?
二进制优化
把第 种物品换成 件0-1背包中的物品,则得到了物品数为 的01背包问题,直接求解,复杂度仍然是 。
但是我们期望将它转化为0-1背包问题之后能够像完全背包一样降低复杂度。
仍然考虑二进制的思想,我们考虑把第 种物品换成若干件物品,使得原问题中第 种物品可取的每种策略——取 件——均能等价于取若干件代换以后的物品。另外,取超过 件的策略必不能出现。
具体方法是:将第 种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为 且 是满足 的最大整数。
例如,如果 为13,就将这种物品分成系数分别为1,2,4,6的四件物品。分成的这几件物品的系数和为 ,表明不可能取多于 件的第 种物品。另外这种方法也能保证对于 间的每一个整数,均可以用若干个系数的和表示,这个证明可以分 和 两段来分别讨论得出,并不难。这样就将第 种物品分成了 种物品,将原问题转化为了复杂度为 的0-1背包问题,是很大的二进制优化
int count = 0, vval[M], vol[M];
// count存分解后的物品总数
//vval存分解后的每件物品的价值
//vvol存分解完成后每件物品的代价
//二进制分解
for(int i=1;i<=n;i++)
{
int num[i] = read(), val[i] = read(), vol[i] = read();
for(int j=1;j<=num[i];j<<=1)
{
vva[count] = j*val[i];
vol[count] = j*vol[i];
count++;
num[i] -= j;
}
if(num[i]>0)
{
vval[count] = num[i]*val[i];
vol[count] = num[i]*vol[i];
count++;
}
}
// 0-1背包问题
memset(dp, 0, sizeof(dp)) ;
for(int i=1;i<count;i++)
for(int j=v;j>=vol[i];j--)
dp[j] = max(dp[j], dp[j-vol[i]]+val[i]);
简化版本
int dp[M] = {};
for(int i=1;i<=n;i++)
{
int vol = read(), val = read(), num = read();
for(int k=1;k<num;num-=k,k<<=1)
for(int j=v;j>=k*vol;--j)
dp[j] = max(dp[j], dp[j-k*vol]+k*val);
for(int j=v;j>=num*vol;--j)
dp[j] = max(dp[j], dp[j-num*vol]+num*val);
}
单调队列优化
多重背包问题同样有O(VN)的算法。这个算法基于基本算法的状态转移方程,但应用单调队列的方法使每个状态的值可以以均摊O(1)的时间求解。
int dp[M];
for(int i=1;i<=n;i++)
{
int vol = read(), val = read(), num = read();
for(int k=0;k<vol;k++) // 枚举体积的余数
{
int a[M], b[M], head = 0, tail = 0; // 下标,值,队头,队尾
for(int j=k;j<=v;j+=vol)
{
int y = dp[j]-j/vol*val; // 当前体积的贡献值
while(head<tail && y>=b[r-1]) tail--; // 出队
a[tail] = j;
b[tail++] = y;
while(a[head]<j-num*vol]) head++; // 入队
dp[j] = b[head]+j/vol*val;
}
}
}
这里,应先确保搞明白了单调队列,就是在区间移动时动态维护区间的最值
观察多重背包的转移方程:
单调队列优化的主要思想就是分组更新,因为 是成倍增加的 只会更新 (这里是从前往后看的,所以是+)。对于当前 的体积,我们可以按照余数将它分为 组,也就是 . 并且,同一个剩余系的数在一组,比如在模3意义下,1,4,7,10是一组,2,5,8,11是一组,3,6,9,12是一组每组的转移是互不影响的,也就是单独转移
举个例子
让所有 都减去
即
当 一定后,就可以用单调队列来优化了
1.4 混合背包
顾名思义,混合背包就是有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)
最直接的想法:二进制分解。可以取无限次物品我们就记其num=99999,这样同样可以利用二进制分解开来
void binary_split()
{
for(int i=1;i<=n;i++)
{
int k = 1;
while(num[i])
{
ccost[++tp] = k*volume[i];
vvalue[tp] = k*value[i];
num[i] -= k;
k *= 2;
if(num[i]<k)
{
ccost[++tp] = volume[i]*num[i];
vvalue[tp] = value[i]*num[i];
break;
}
}
}
}
binary_split();
for(int i=1;i<=tp;i++)
for(int j=ttime;j>=ccost[i];j--)
dp[j] = max(dp[j], dp[j-ccost[i]]+vvalue[i]);
1.5 二维费用背包
给一个容量为V的背包,你的负重最大只有W,然后有n种物品,每种都有若干个(0个,无限,多个),体积为volume[i],重量为weight[i],价值为value[i].问最多能装多少价值的物品,在不超过体积及负重的情况下?
for(int i=1;i<=n;i++)
for(int j=v;j>=volume[i];j--)
for(int k=w;k>=weight[i];k--)
dp[j][k]=max(dp[j][k],dp[j-volume[i]][k-weight[i]]+value[i]);
3.6 分组背包
有n件物品可以被放入一个容量为v的背包中,每件物品体积为volume[i],价值为value[i].此外,这些物品被分成p组,每组中的物品最多只能选一件,求背包中最多可以装多少价值的物品.
for(int k=1;k<=p;k++)
for(int j=v;j>=0;j–) //此处遍历顺序与物品种类有关
for(int i: part[k])
dp[j]=max(dp[j],dp[j-volume[i]]+value[i]).
注意遍历方式一定是 ,如果是 的话就无法保证每组只选一个了。
先 再 保证了每组内,每个体积只会被一个最优的物品访问到。
1.6 有依赖的背包问题
这种背包问题的物品间存在某种“依赖”的关系。也就是说,物品i依赖于
物品j,表示若选物品i,则必须选物品j。为了简化起见,我们先设没有某个物
品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多
件物品。
for(int i=1;i<=n;i++)
{
memcpy(tmp,dp,sizeof(dp));
for(each attachment k of item i)
for(int j=v;j>=volume[k];j--)
tmp[j]=max(tmp[j],tmp[j-volume[k]]+value[k]);
for(int j=v;j>=volume[i];j--)
dp[j]=max(dp[j],tmp[j-volume[i]]+value[i]);
}
1.7 泛化物品
泛化物品准确来说,不是一类题目,而是一种思想。泛化物品的定义是
考虑这样一种物品,它并没有固定的费用和价值,而是它的价值随着你分
配给它的费用而变化。这就是泛化物品的概念。
or
更严格的定义之。在背包容量为V 的背包问题中,泛化物品是一个定义
域为0 . . . V 中的整数的函数h,当分配给它的费用为v时,能得到的价值就
是h(v)。
or
这个定义有一点点抽象,另一种理解是一个泛化物品就是一个数组h[0 . . . V ],
给它费用v,可得到价值h[v]。
于是,
- 一个0-1背包中的物品(体积 ,价值 ),它的泛化物品模型是 ,
- 一个完全背包中的物品,它的模型是 ,其中k为正整数且 ,
- 一个多重背包中的物品,则是 ,其中 且 ,
- 一个互斥的物品组, , 取遍组中物品的编号, 相同时 取最小值,
泛化物品的和
如果给定两个泛化物品 和 ,现在有体积v来装这两种物品,要求获得最大价值,怎么做?
则 , 取遍 到 ,答案就是
新合成的dp数组,实际上,也是一个泛化物品。
由泛化物品的性质可知,如果将两个泛化物品这样合成一个新的物品,新的物品在问题中完全可以取代原有的两个物品。
1.8 背包问题问法的变化
记录具体的选择
如果需要记录具体的选择,可以另起一个 x[ ] 数组,x[i]=0表示不拿,x[i]=1表示拿。
f[n][c]为最优值,如果f[n][c]=f[n-1][c] ,说明有没有第n件物品都一样,则x[n]=0 ; 否则 x[n]=1。当x[n]=0时,由x[n-1][c]继续构造最优解;当x[n]=1时,则由x[n-1][c-w[i]]继续构造最优解。以此类推,可构造出所有的最优解。
for(int i=n;i>1;i--)
{
if(dp[i][v]==dp[i-1][v]) x[i] = 0;
else
{
x[i] = 1;
v -= volume[i];
}
x[1] = (dp[1][c]>0)?1:0;
}
输出字典序最小的方案
字典序最小是指KaTeX parse error: Undefined control sequence: \cdtsN at position 2: 1\̲c̲d̲t̲s̲N̲号物品的选择方案排列出来以后字典序最小
我们的做法是:先把物品逆序排列一下,然后按照前面的状态转移方程来求值。唯一要注意的是,如果 以及 同时成立,应该按照后者(即选择了物品 )输出
求方案总数
当我们要求装满背包或者将背包装至某一指定容量的方案总数时,一般只需要将转移方程的max改成sum即可。初始条件为
求最优方案的总数
这里与求方案总数不同的是我们要求所装的物品价值最大,于是,结合求最大总价值和方案总数两个问题的思路,最优方案的总数可以这样求: 意义同前述, 表示这个子问题的最优方案的总数,则在求 的同时求
求次优解,第K优解
求次优解往往可以相同的复杂度解决,第K优解则比求最优解的复杂度上多一个系数K。其基本思想是将每个状态都表示成有序队列,将状态转移方程中的max/min转化成有序队列的合并。
这里只讲一下0-1背包的情况:
如果要求第K优解,那么状态 就应该是一个大小为K的数组 。其中 表示前i个物品、背包大小为j时,第k优解的值。“ 是一个大小为K的数组”这一句,熟悉C语言的同学可能比较好理解,或者也可以简单地理解为在原来的方程中加了一维。显然 这K个数是由大到小排列的,所以我们把它认为是一个有序队列。然后原方程就可以解释为: 这个有序队列是由 和 这两个有序队列合并得到的。有序队列f[i−1][j]f[i-1][j]f[i−1][j]即 , 则理解为在 的每个数上加上 后得到的有序队列。合并这两个有序队列并将结果的前KKK项储存到 中的复杂度是 。最后的答案是 。总的复杂度是 。为什么这个方法正确呢?实际上,一个正确的状态转移方程的求解过程遍历了所有可用的策略,也就覆盖了问题的所有方案。只不过由于是求最优解,所以其它在任何一个策略上达不到最优的方案都被忽略了。如果把每个状态表示成一个大小为KKK的数组,并在这个数组中有序的保存该状态可取到的前KKK个最优值。那么,对于任两个状态的max运算等价于两个由大到小的有序队列的合并。