You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

188 lines
4.9 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

##[$AcWing$ $1023$. 买书](https://www.acwing.com/problem/content/1025/)
**[【总结】背包问题的至多/恰好/至少](https://www.cnblogs.com/littlehb/p/15847138.html)**
### 一、题目描述
小明有 $n$ 块钱全部用来买书,现有 $10$ 元, $20$ 元, $50$ 元, $100$ 元 的书
每本书可以 **购买多次**,求小明有 **多少种** 买书 **方案**,(每种书可购买多本)
**输入格式**
一个整数 $n$,代表总共钱数。
**输出格式**
一个整数,代表选择方案种数。
**数据范围**
$0≤n≤1000$
**输入样例1**
```cpp {.line-numbers}
20
```
**输出样例1**
```cpp {.line-numbers}
2
```
**输入样例2**
```cpp {.line-numbers}
15
```
**输出样例2**
```cpp {.line-numbers}
0
```
**输入样例3**
```cpp {.line-numbers}
0
```
**输出样例3**
```cpp {.line-numbers}
1
```
### 二、分析
一共有 $n$ 个物品,每个物品有体积 $v_i$,价值 $w_i$,每个物品能够选多次
求总体积恰好是$m$的方案数
这是一道 <font color='red' size=4><b>裸的完全背包问题求解方案数</b></font>
#### 闫氏$DP$分析法
状态表示——集合:$f[i][j]$ 表示考虑前$i$个数字,且总数字和 **恰好** $j$的集合下能获得的方案数。
状态表示——属性:因为是求方案数,故为 $count$。
状态计算——集合划分:考虑第 $i$ 个数选不选。
* 不选或选不了(剩余数量不够 $j<a[i]$$f[i1][j]$。
* 选:$f[i][ja[i]]$。
初始状态:$f[0][0]$
目标状态:$f[n][m]$
<center><img src='https://cdn.acwing.com/media/article/image/2021/06/11/55909_41335e2aca-IMG_8A6C4D001CEB-1.jpeg'></center>
### 二、原始朴素版本
时间复杂度:$O(n^2 \times m)$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 5;
const int M = 1010;
int v[N] = {0, 10, 20, 50, 100}; // 每种货币下标从1开始
int n, m; // 货币种类,钱数
int f[N][M]; // 前i种物品体积恰好是j的情况下的最大值
// 完全背包
int main() {
n = 4;
cin >> m;
// 前0种物品体积是0的情况下只有一种方案
// 一般询问方案数的问题f[0]都会设置为1
// Q:那20元钱呢不买买两本10块的每一本20的。三种呀
// A:题目说的全部,钱要花完
f[0][0] = 1;
for (int i = 1; i <= n; i++) // 每个物品
for (int j = 0; j <= m; j++) // 每个体积
for (int k = 0; v[i] * k <= j; k++) // 个数
f[i][j] += f[i - 1][j - v[i] * k];
printf("%d\n", f[n][m]);
return 0;
}
```
### 三、完全背包—经典优化
对 $f(i,j)$ 的 **状态转移方程** 进行变形
尝试找出$f(i,j)$与它的前序$f(i,j-v_i)$之间的关联关系,看看能否实现$f(i,j-v_i) \rightarrow f(i,j)$的迁移:
$$\large f(i,j)=f(i-1,j)+f(i-1,j-v_i)+...+f(i-1,j-s\cdot v_i)①$$
$$\large f(i,j-v_i)=   f(i-1,j-v_i)+...+f(i-1,j-s\cdot v_i)②$$
> <font color='red' size=4><b>注:把体积$j-v_i$代入①式,就可以得到 ②式</b></font>
<font color='blue' size=4><b>$Q:$①和②中的$s$是一个值吗?</b></font>
**答**:是一个值。从事情本质出发,$s\cdot v_i$含义:在$j$这么大的空间限制下,最多可以装多少个$i$物品,当然是同一个个数值$s$了。
由上述两个等式可以获得如下递推式:
$$\large f(i,j)=f(i1,j)+f(i,jv_i)$$
新的 **状态转移方程** ,时间复杂度 $O(n \times m)$
#### 二维优化版本
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int v[5] = {0, 10, 20, 50, 100};
int f[5][N];
int main() {
int m;
cin >> m;
// 前0种物品体积是0的情况下只有一种方案
f[0][0] = 1;
for (int i = 1; i <= 4; i++)
for (int j = 0; j <= m; j++) {
f[i][j] = f[i - 1][j];
if (v[i] <= j) f[i][j] += f[i][j - v[i]];
}
printf("%d\n", f[4][m]);
return 0;
}
```
观察到该 **转移方程** 对于第 $i$ 阶段的状态,只会使用第 $i-1$ 层和第 $i$ 层的状态
因此我们也可以采用 **$01$背包** 的 空间优化方案
时间复杂度:$O(n×m)$
空间复杂度:$O(m)$
#### 一维优化解法
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int v[5] = {0, 10, 20, 50, 100};
int f[N];
// 体积限制是恰好是因此需要初始化f[0][0]为合法解1其他位置为非法解0。
int main() {
int m;
cin >> m;
// 前0种物品体积是0的情况下只有一种方案
f[0] = 1;
for (int i = 1; i <= 4; i++)
for (int j = v[i]; j <= m; j++)
f[j] += f[j - v[i]];
// 输出
printf("%d\n", f[m]);
return 0;
}
```