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.

171 lines
5.8 KiB

This file contains ambiguous Unicode 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$ $1252$. 搭配购买](https://www.acwing.com/problem/content/description/1254/)
### 一、题目描述
$Joe$觉得云朵很美,决定去山上的商店买一些云朵。
商店里有 $n$ 朵云,云朵被编号为 $1,2,…,n$,并且每朵云都有一个价值。
但是商店老板跟他说,一些云朵要搭配来买才好,所以买一朵云则与这朵云有搭配的云都要买。
但是$Joe$的钱有限,所以**他希望买的价值越多越好**。
**输入格式**
第 $1$ 行包含三个整数 $nmw$,表示有 $n$ 朵云,$m$ 个搭配,$Joe$有 $w$ 的钱。
第 $2n+1$行,每行两个整数 $c_id_i$ 表示 $i$ 朵云的价钱和价值。
第 $n+2n+1+m$ 行,每行两个整数 $u_iv_i$,表示买 $u_i$ 就必须买 $v_i$,同理,如果买 $v_i$ 就必须买 $u_i$。
**输出格式**
一行,表示可以获得的最大价值。
**数据范围**
$1≤n≤10000,0≤m≤5000,1≤w≤10000,1≤ci≤5000,1≤di≤100,1≤u_i,v_i≤n$
**输入样例**
```cpp {.line-numbers}
5 3 10
3 10
3 10
3 10
5 100
10 1
1 3
3 2
4 2
```
**输出样例**
```cpp {.line-numbers}
1
```
### 二、解题思路
![](https://img2022.cnblogs.com/blog/8562/202204/8562-20220412102228070-811700211.png)
### 三、一维$01$背包解法
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 10010;
int n, m, sum; // 有 n 朵云m 个搭配Joe有 sum 的钱。
int v[N], w[N]; // 表示 i 朵云的价钱和价值
int p[N]; // 并查集数组
int f[N]; // 01背包数组
// 最简并查集
int find(int x) {
if (p[x] != x) p[x] = find(p[x]); // 路径压缩
return p[x];
}
int main() {
scanf("%d %d %d", &n, &m, &sum);
// 初始化并查集
for (int i = 1; i <= n; i++) p[i] = i;
// 读入每个云朵的价钱(体积)和价值
for (int i = 1; i <= n; i++)
scanf("%d %d", &v[i], &w[i]);
while (m--) {
int a, b;
cin >> a >> b; // 两种云朵需要一起买
int pa = find(a), pb = find(b);
if (pa != pb) {
// 集合有两个属性总价钱、总价值都记录到root节点上
v[pb] += v[pa];
w[pb] += w[pa];
p[pa] = pb;
}
}
// 01背包
// 注意这里不能认为一维的能AC二维的替代写法就一定能AC
// 这是因为这里的判断p[i]==i,导致i不一定是连续的
// 所以f[i][j]=f[i-1][j]这句话就不一定对
// 所以看来终极版本的01背包一维解法还是有一定价值的。
for (int i = 1; i <= n; i++)
if (p[i] == i) // 只关心集合代表元素,选择一组
for (int j = sum; j >= v[i]; j--) // 体积由大到小倒序01背包
f[j] = max(f[j], f[j - v[i]] + w[i]);
// 输出最大容量下获取到的价值
printf("%d\n", f[sum]);
return 0;
}
```
### 四、二维$01$背包解法与不能$AC$的理解
采用二维数组的表示法,有以下两个问题:
* 本行结果不一定是从上一行推导过来,因为上一行很可能不是这个家族的族长,只有族长也有资格进行计算。
可以采用$last$变量记录的方法模拟完成二维数组的计算,具体实现见代码。
* 内存超界
过掉$7/11$个数据,无法$AC$
原因分析:$f[N][N]$第一维是可以选择的物品个数,上限是$10000$
第二维是可以支付的钱数,上限也是$10000$
如果按二维思路来处理,确实需要一个$10000*10000$的数组
$10000*10000*8= 800000000 byte$
$800000000/1024/1024= 762MB$ 本题上限是$64MB$,妥妥的超内存,$MLE$~
穷则思变,既然$int$+二维过不了,那么试试$short$吧,因为$short$最大是$65536$,符合题意,并且只占两个$bit$,就是$10000*10000*2= 200000000 byte$
$200000000/1024/1024= 190MB$ 本题上限是$64MB$,妥妥的超内存,$MLE$~
那么一维的为什么可以呢?
一维的只有$10000*8=80000 byte$
$80000/1024/1024=0.076MB$ 本题上限是$64MB$,肯定不会在内存上出问题。
**总结**
**1$01$背包一维相比二维,能够节约非常大的空间,二维特别容易$MLE$。
2$01$背包一维相比二维,不用考虑上一个依赖是不是$i-1$行的问题,不用特殊用$last$方式记录并处理,出错概率小**
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 10010;
int n, m, sum; //有 n 朵云m 个搭配Joe有 sum 的钱。
int v[N], w[N]; //表示 i 朵云的价钱和价值
int p[N];
int f[N][N];
/*过掉7/11个数据无法AC*/
//最简并查集
int find(int x) {
if (p[x] != x) p[x] = find(p[x]); //路径压缩
return p[x];
}
int main() {
cin >> n >> m >> sum;
//实始化并查集
for (int i = 1; i <= n; i++) p[i] = i;
//读入每个云朵的价钱(体积)和价值
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
while (m--) {
int a, b;
cin >> a >> b; //两种云朵需要一起买
int pa = find(a), pb = find(b);
if (pa != pb) {
//集合有两个属性总价钱、总价值都记录到root节点上
v[pb] += v[pa];
w[pb] += w[pa];
p[pa] = pb;
}
}
// 01背包
int last = 0;
for (int i = 1; i <= n; i++)
if (p[i] == i) { //因处理集合的代表元素
for (int j = 1; j <= sum; j++) {
f[i][j] = f[last][j];
if (v[i] <= j)
f[i][j] = max(f[i][j], f[last][j - v[i]] + w[i]);
}
last = i; //依赖的上一个状态
}
printf("%d\n", f[n][sum]);
return 0;
}
```