其他篇传送门:
五、背包DP
1、01背包
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
我们令表示将前 i 个物品放入容量为 j 的背包中(
)的最大价值,由于每个物品只有放和不放两种选择,因此我们可以分成两种情况:
第一种,第 i 个物品不放入背包中,那么问题就转化成前 i - 1 个物品放入容量为 j 的背包中的最大价值,即;
第二种,第 i 个物品放入背包中,那么问题就转化成前 i - 1 个物品放入容量为 j - vi 的背包中的最大价值,即;
再考虑边界情况:时表示 0 个物品放入 0 容量的背包中,这种情况下
;
时表示 0 个物品放入 j 容量的背包中,这种情况下
;
时表示 i 个物品放入 0 容量的背包中,这种情况下
。
综上分析,可以得到状态转移方程为
C++未优化部分代码如下:
for(int i = 1; i <= n; i ++)
for(int j = 0; j <= v; j ++){
dp[i][j] = dp[i - 1][j];
if(j >= vi[i]) dp[i][j] = max(dp[i][j], dp[i - 1][j - vi[i]] + wi[i]);
}
由于第i步只与第i - 1步有关,因此我们可以把二维数组优化成一维数组,将改成
,第3行就出现了
可以直接删去,第4行的条件可以直接作为循环的条件,又由于
是i - 1状态下得到的,
,为了保证我们的递推是由扫到上一个物品时留下的数组中更新过来的,因此使用递减遍历的方法。
C++优化部分代码如下:
for(int i = 1; i <= n; i ++)
for(int j = v; j >= vi[i]; j --)
dp[j] = max(dp[j], dp[j - vi[i]] + wi[i]);
2、完全背包
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
我们令表示将前 i 种物品放入容量为 j 的背包中(
)的最大价值,由于每种物品可以选择不放和放 k 个,因此我们可以分成两种情况:
第一种,第 i 种物品不放入背包中,那么问题就转化成前 i - 1 种物品放入容量为 j 的背包中的最大价值,即;
第二种,第 i 种物品放 k 个到背包中,那么问题就转化成前 i - 1 种物品放入容量为 j - vi * k 的背包中的最大价值,即。
完全背包的边界情况和01背包基本相同,即,因此,状态转移方程为
C++未优化部分代码如下:
for(int i = 1; i <= n; i ++)
for(int j = 0; j <= v; j ++){
if(j < vi[i]) dp[i][j] = dp[i - 1][j];
else{
for(int k = 0; k * vi[i] <= j; k ++)
dp[i][j] = max(dp[i][j], dp[i - 1][j - vi[i] * k] + wi[i] * k);
}
}
数据在1000左右基本上已经TLE了,因此完全背包的优化是非常有必要的,这里降低时间复杂度的过程就不进行推导了,大家可以在背包九讲中进行学习,最终可以得到优化后的状态转移方程为
再进行空间优化,方法和01背包基本相同,由于这里是状态 i 下得到的,因此可以直接递增遍历。
C++优化部分代码如下:
for(int i = 1; i <= n; i ++)
for(int j = vi[i]; j <= v; j ++)
dp[j] = max(dp[j], dp[j - vi[i]] + wi[i]);
3、多重背包
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
我们令表示将前 i 种物品放入容量为 j 的背包中(
)的最大价值,由于每种物品可以选择不放和放 k 个,因此我们可以分成两种情况:
第一种,第 i 种物品不放入背包中,那么问题就转化成前 i - 1 种物品放入容量为 j 的背包中的最大价值,即;
第二种,第 i 种物品放 k 个到背包中,那么问题就转化成前 i - 1 种物品放入容量为 j - vi * k 的背包中的最大价值,即。
多重背包的边界情况和01背包基本相同,即,因此,状态转移方程为
C++未优化部分代码如下:
for(int i = 1; i <= n; i ++)
for(int j = 0; j <= v; j ++)
for(int k = 0; k <= si[i] && k * vi[i] <= j; k ++)
dp[i][j] = max(dp[i][j], dp[i - 1][j - vi[i] * k] + wi[i] * k);
暴力dp只能用于数据很小的情况,因此多重背包问题一般就得进行优化,这里我们可以考虑二进制的思想,将第 i 种物品拆分成若干个物品,可以按照2的次幂进行拆分,即,其中
,可以证明 1 到 si 之间的所有整数都能用这些数所表示出来,这样无论 k 取多少,都能进行表示了。
再进行和01背包问题相同的空间优化即可。
C++优化部分代码如下:
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 ++;
vi[cnt] = a * k;
wi[cnt] = b * k;
s -= k;
k *= 2;
}
if(s > 0){
cnt ++;
vi[cnt] = a * s;
wi[cnt] = b * s;
}
}
n = cnt;
for(int i = 1; i <= n; i ++)
for(int j = v; j >= vi[i]; j --)
f[j] = max(f[j], f[j - vi[i]] + wi[i]);
4、二维费用背包
有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。
每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
输出最大价值。
二维费用的背包问题实际上就是在普通的背包问题种加入一个“费用”,这里以二维费用的01背包为例,我们令表示前 i 个物品放入容量为 j 且重量不超过 k 的背包中(
)的最大价值,剩下的步骤和01背包完全相同,只是多了一维而已。
因此,状态转移方程为
同理,可以用01背包的优化方法对二维费用的背包进行优化,C++优化后的部分代码如下:
for(int i = 1; i <= n; i ++)
for(int j = m; j >= vi[i]; j --)
for(int k = v; k >= mi[i]; k --)
dp[j][k] = max(dp[j][k], dp[j - vi[i]][k - mi[i]] + wi[i]);
5、分组背包
有 N 组物品和一个容量是 M 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
我们令表示前 i 组放入容量为 k 的背包中(
)的最大价值,分两种情况来看:
第一种,第 i 组的第 j 个物品不放入背包中,那么问题就转化成前 i - 1 组物品放入容量为 k 的背包中的最大价值,即;
第二种,第 i 组的第 j 个物品放入背包中,那么问题就转化成前 i - 1 组物品放入容量为 k - vij 的背包中的最大价值加上第 i 组第 j 个物品的价值 wij ,即。
边界情况与01背包类似,就不再进行讨论了。
综上分析,可以得到状态转移方程为
再按照01背包的方式进行优化,可以得到C++优化后的部分代码如下:
for(int i = 1; i <= n; i ++)
for(int k = m; k >= 0; k --)
for(int j = 0; j < s[i]; j ++)
if(v[i][j] <= k)
dp[k] = max(dp[k], dp[k - v[i][j]] + w[i][j]);
六、概率DP/期望DP
先来看概率和期望的定义:
概率就是事件发生的可能性,即该事件发生的次数与事件总数的比值。
期望是随机变量的平均值大小,即试验中每次可能结果的概率乘以其结果的总和。
具体定义和性质详见:概率(统计学术语)_百度百科
1.概率DP
N个人坐成一圈玩游戏。一开始我们把所有玩家按顺时针从1到N编号。
首先第一回合是玩家1作为庄家。每个回合庄家都会随机(即按相等的概率)从卡牌堆里选择一张卡片,假设卡片上的数字为X,则庄家首先把卡片上的数字向所有玩家展示,然后按顺时针从庄家位置数第X个人将被处决即退出游戏。然后卡片将会被放回卡牌堆里并重新洗牌。被处决的人按顺时针的下一个人将会作为下一轮的庄家。那么经过N-1轮后最后只会剩下一个人,即为本次游戏的胜者。
现在你预先知道了总共有M张卡片,也知道每张卡片上的数字。现在你需要确定每个玩家胜出的概率。
令表示剩下 i 个人中从庄家开始第 j 个人获胜的概率。
假设剩下 i 个人这次抽到的卡牌为 k,要处决的人就是pos[k] % i,有个特殊状况为当pos[k] % i结果为 0 时,说明被处决的人是 i。令out = pos[k] % i,这次的第 j 个人在下次处决中的位置有两种情况(下次庄家为 out + 1(新的1号位),out - j 为二者的距离,总人数为 i - 1):
(1)out > j,那么 j 的新位置为总人数 - 被处决的编号与 j 的差值,因此 j 在新的一轮中编号为 i - 1 - (out - j) + 1,即;
(2)out < j,那么 j 的新位置为 j 与被处决的编号的差值,即。
由于每轮抽每张卡牌的概率为 1 / m,因此状态转移方程为
C++部分代码如下:
for(int i = 1; i <= m; i++)
cin >> pos[i];
dp[1][1] = 1.0;
for(int i = 2; i <= n; i++)
for(int j = 1; j <= i; j++)
for(int k = 1; k <= m; k++){
int out = (pos[k] % i) == 0 ? i : pos[k] % i;
if(out > j)
dp[i][j] += dp[i - 1][i - out + j] / m;
else
dp[i][j] += dp[i - 1][j - out] / m;
}
2.期望DP
一个软件有 s 个子系统,会产生 n 种 bug。现在要找出所有种类的 bug。
假设某人一天发现一个 bug。一个 bug 属于某个子系统的概率为 1 / s,属于某种分类的概率是 1 / n。
求发现 n 种 bug,且每个子系统都发现 bug 的天数的期望。
令表示已经找到 i 种 bug,并存在于 j 个子系统中,要达到目标状态还需要的期望天数。
从开始,1天找到1个 bug,有4种情况:
(1):发现的一个 bug 属于已有的 i 个分类和 j 个子系统中,概率
;
(2):发现的一个 bug 不属于已有的 i 个分类但属于已有的 j 个子系统中,概率
;
(3):发现的一个 bug 属于已有的 i 个分类但不属于已有的 j 个子系统中,概率
;
(4):发现的一个 bug 既不属于已有的 i 个分类又不属于已有的 j 个子系统中,概率
。
综上分析,可以得到状态转移方程为
移项化简后可以得到
由于表示已经找到 n 种 bug,并存在于 s 个子系统中,要达到目标状态还需要0天,因此
,从该状态倒推回
即为答案。
C++部分代码如下:
for(int i = n; i >= 0; i--)
for(int j = s; j >= 0; j--){
if(i == n && j == s)
dp[n][s] = 0.0;
else
dp[i][j] = (n * s + (n - i) * j * dp[i + 1][j] + i * (s - j) * dp[i][j + 1] + (n - i) * (s - j) * dp[i + 1][j + 1]) / (n * s - i * j);
}