动态规划问题(二)

其他篇传送门:

动态规划问题(一)_Wmiracle的博客-CSDN博客

动态规划问题(三)_Wmiracle的博客-CSDN博客

五、背包DP

1、01背包

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

我们令dp[i][j]表示将前 i 个物品放入容量为 j 的背包中(0 \leqslant i \leqslant N,0 \leqslant j \leqslant V)的最大价值,由于每个物品只有放和不放两种选择,因此我们可以分成两种情况:

第一种,第 i 个物品不放入背包中,那么问题就转化成前 i - 1 个物品放入容量为 j 的背包中的最大价值,即dp[i][j] = dp[i - 1][j]

第二种,第 i 个物品放入背包中,那么问题就转化成前 i - 1 个物品放入容量为 j - vi 的背包中的最大价值,即dp[i][j] = dp[i - 1][j - vi] + wi

再考虑边界情况:i = 0, j = 0时表示 0 个物品放入 0 容量的背包中,这种情况下dp[0][0] = 0

i = 0, j \neq 0时表示 0 个物品放入 j 容量的背包中,这种情况下dp[0][j] = 0

i \neq 0, j = 0时表示 i 个物品放入 0 容量的背包中,这种情况下dp[i][0] = 0

综上分析,可以得到状态转移方程为

dp[i][j] = \begin{cases} 0, & i = 0 \; \; or \; \; j = 0 \\ max(dp[i - 1][j], dp[i - 1][j - vi] + wi), & i, j > 0 \end{cases}

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步有关,因此我们可以把二维数组优化成一维数组,将dp[i][j]改成dp[j],第3行就出现了dp[j] = dp[j]可以直接删去,第4行的条件可以直接作为循环的条件,又由于dp[j - vi[i]]是i - 1状态下得到的,j - vi \leqslant j,为了保证我们的递推是由扫到上一个物品时留下的数组中更新过来的,因此使用递减遍历的方法。

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]);

例题:AcWing2 01背包问题

2、完全背包

有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。

第 i 种物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

我们令dp[i][j]表示将前 i 种物品放入容量为 j 的背包中(0 \leqslant i \leqslant N,0 \leqslant j \leqslant V)的最大价值,由于每种物品可以选择不放和放 k 个,因此我们可以分成两种情况:

第一种,第 i 种物品不放入背包中,那么问题就转化成前 i - 1 种物品放入容量为 j 的背包中的最大价值,即dp[i][j] = dp[i - 1][j]

第二种,第 i 种物品放 k 个到背包中,那么问题就转化成前 i - 1 种物品放入容量为 j - vi * k 的背包中的最大价值,即dp[i][j] = dp[i - 1][j - vi * k] + wi * k

完全背包的边界情况和01背包基本相同,即dp[i][0] = dp[0][j] = 0,因此,状态转移方程为

dp[i][j] = \begin{cases} 0, & i = 0 \; \; or \; \; j = 0 \\ max(dp[i - 1][j], dp[i - 1][j - vi * k] + wi * k), & i, j > 0 \end{cases}

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了,因此完全背包的优化是非常有必要的,这里降低时间复杂度的过程就不进行推导了,大家可以在背包九讲中进行学习,最终可以得到优化后的状态转移方程为

dp[i][j] = \begin{cases} 0, & i = 0 \; \; or \; \; j = 0 \\ max(dp[i - 1][j], dp[i][j - vi] + wi), & i, j > 0 \end{cases}

再进行空间优化,方法和01背包基本相同,由于这里dp[j - vi]是状态 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]);

例题:AcWing3 完全背包问题

3、多重背包

有 N 种物品和一个容量是 V 的背包。

第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

我们令dp[i][j]表示将前 i 种物品放入容量为 j 的背包中(0 \leqslant i \leqslant N,0 \leqslant j \leqslant V)的最大价值,由于每种物品可以选择不放和放 k 个,因此我们可以分成两种情况:

第一种,第 i 种物品不放入背包中,那么问题就转化成前 i - 1 种物品放入容量为 j 的背包中的最大价值,即dp[i][j] = dp[i - 1][j]

第二种,第 i 种物品放 k 个到背包中,那么问题就转化成前 i - 1 种物品放入容量为 j - vi * k 的背包中的最大价值,即dp[i][j] = dp[i - 1][j - vi * k] + wi * k

多重背包的边界情况和01背包基本相同,即dp[i][0] = dp[0][j] = 0,因此,状态转移方程为

dp[i][j] = \begin{cases} 0, & i = 0 \; \; or \; \; j = 0 \\ max(dp[i - 1][j], dp[i - 1][j - vi * k] + wi * k), & i, j > 0 \end{cases}(0 \leqslant k \leqslant si)

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,2,4,8,...,2^{k-1}, si - 2^{k} + 1,其中si - 2^k + 1 \geqslant 0,可以证明 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]);

例题:AcWing4 多重背包问题 I

           AcWing5 多重背包问题 II

 4、二维费用背包

有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。

每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
输出最大价值。

二维费用的背包问题实际上就是在普通的背包问题种加入一个“费用”,这里以二维费用的01背包为例,我们令dp[i][j][k]表示前 i 个物品放入容量为 j 且重量不超过 k 的背包中(0 \leqslant i \leqslant N, 0 \leqslant j \leqslant V, 0 \leqslant k \leqslant M)的最大价值,剩下的步骤和01背包完全相同,只是多了一维而已。

因此,状态转移方程为

dp[i][j][k] = \begin{cases} 0, & i = 0 \; \; or \; \; j = 0 \; \; or \; \; k = 0 \\ max(dp[i - 1][j][k], dp[i - 1][j - vi][k - mi] + wi), & i, j, k > 0 \end{cases}

同理,可以用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]);

例题:AcWing8 二维费用的背包问题

5、分组背包

有 N 组物品和一个容量是 M 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。

我们令dp[i][k]表示前 i 组放入容量为 k 的背包中(0 \leqslant i \leqslant N, 0 \leqslant k \leqslant M)的最大价值,分两种情况来看:

第一种,第 i 组的第 j 个物品不放入背包中,那么问题就转化成前 i - 1 组物品放入容量为 k 的背包中的最大价值,即dp[i][k] = dp[i - 1][k]

第二种,第 i 组的第 j 个物品放入背包中,那么问题就转化成前 i - 1 组物品放入容量为 k - vij 的背包中的最大价值加上第 i 组第 j 个物品的价值 wij ,即dp[i][k] = dp[i - 1][k - vij] + wij

边界情况与01背包类似,就不再进行讨论了。

综上分析,可以得到状态转移方程为

dp[i][k] = \begin{cases} 0, & i = 0 \; \; or \; \; k = 0 \\ max(dp[i - 1][k], dp[i - 1][k - vij] + wij), & i, k > 0 \end{cases}

再按照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]);

例题:AcWing9 分组背包问题

六、概率DP/期望DP

先来看概率和期望的定义:

概率就是事件发生的可能性,即该事件发生的次数与事件总数的比值。

期望是随机变量的平均值大小,即试验中每次可能结果的概率乘以其结果的总和。

具体定义和性质详见:概率(统计学术语)_百度百科

                                    数学期望_百度百科

1.概率DP

N个人坐成一圈玩游戏。一开始我们把所有玩家按顺时针从1到N编号。

首先第一回合是玩家1作为庄家。每个回合庄家都会随机(即按相等的概率)从卡牌堆里选择一张卡片,假设卡片上的数字为X,则庄家首先把卡片上的数字向所有玩家展示,然后按顺时针从庄家位置数第X个人将被处决即退出游戏。然后卡片将会被放回卡牌堆里并重新洗牌。被处决的人按顺时针的下一个人将会作为下一轮的庄家。那么经过N-1轮后最后只会剩下一个人,即为本次游戏的胜者。

现在你预先知道了总共有M张卡片,也知道每张卡片上的数字。现在你需要确定每个玩家胜出的概率。 

dp[i][j]表示剩下 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,即j = i - out + j

(2)out < j,那么 j 的新位置为 j 与被处决的编号的差值,即j = j - out

由于每轮抽每张卡牌的概率为 1 / m,因此状态转移方程为

dp[i][j] = \begin{cases} dp[i][j] + dp[i - 1][i - out + j] / m, & out > j \\ dp[i][j] + dp[i - 1][j - out] / m, & out < j \end{cases}

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;
		}

 例题:luoguP2059 卡牌游戏

2.期望DP

一个软件有 s 个子系统,会产生 n 种 bug。现在要找出所有种类的 bug。

假设某人一天发现一个 bug。一个 bug 属于某个子系统的概率为 1 / s,属于某种分类的概率是 1 / n。

求发现 n 种 bug,且每个子系统都发现 bug 的天数的期望。

dp[i][j]表示已经找到 i 种 bug,并存在于 j 个子系统中,要达到目标状态还需要的期望天数。

dp[i][j]开始,1天找到1个 bug,有4种情况:

(1)dp[i][j]:发现的一个 bug 属于已有的 i 个分类和 j 个子系统中,概率p_1 = (i / n) * (j / s)

(2)dp[i + 1][j]:发现的一个 bug 不属于已有的 i 个分类但属于已有的 j 个子系统中,概率p_2 = (1 - i / n) * (j / s)

(3)dp[i][j + 1]:发现的一个 bug 属于已有的 i 个分类但不属于已有的 j 个子系统中,概率p_3 = (i / n) * (1 - j / s)

(4)dp[i + 1][j + 1]:发现的一个 bug 既不属于已有的 i 个分类又不属于已有的 j 个子系统中,概率p_4 = (1 - i / n) * (1 - j / s)

综上分析,可以得到状态转移方程为

dp[i][j] = p_1 * dp[i][j] + p_2 * dp[i + 1][j] + p_3 * dp[i][j + 1] + p_4 * dp[i + 1][j + 1] + 1

移项化简后可以得到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)

由于dp[n][s]表示已经找到 n 种 bug,并存在于 s 个子系统中,要达到目标状态还需要0天,因此dp[n][s] = 0,从该状态倒推回dp[0][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);
    }

例题:poj2096 Collecting Bugs

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值