数据结构与算法-国王与金矿
题目:
有一个国家发现了
每座金矿的产量和需要的矿工人数都不相同:
一个输入例子:
w = 10
500金/5人 400金/5人 200金/3人 300金/4人 350金/3人
解答
排列组合
每一座金矿都有挖与不挖两种选择,如果有
代码比较简单就不展示了,时间复杂度也很明显,就是
动态规划
如果仔细思考,可以发现这道题和 01背包问题类似,都可以使用动态规划的方法解答。
动态规划有三个核心元素:最优子结构、边界、状态转移方程式。
下面,我们把金矿数量设为 G[]
,金矿的用工量设为数组 P[]
。对于上面的输入例子:
vector<int> G = {500, 400, 200, 300, 350};
vector<int> P = {5, 5, 3, 4, 3};
那么 5 座金矿和 4 座金矿的最优选择之间存在这样的关系:
最后我们还需要确定一下,这个问题的边界是什么?
边界两种情况:
(1)只有 1 座金矿,也就是 G[0]
。
(2)如果给定的工人数量不够挖取第 1 座金矿,也就是
数据初始化和状态转移
我们先画出如下表格,表格的第一列代表给定前 1-5 座金矿的情况,也就是
- 1金矿时,是400金,需要 5 工人。所以前4个格子都是0,因为人数不够。后面格子都是400,因为只有这一座金矿可挖。
- 第2座金矿有 500 金,需要 5 工人。第二行前 4 个格子是
, 所以 。
- 第 2 行后 6 个格子计算,因为
,所以根据 ,第 5-9 个格子的值是 500 。( 需要注意的是第 2 行第 10 个格子,也就是 的时候, 。)
- 第 3 座金矿有 200 金,需要 3 工人。第 3 行计算方法和前面一样。
- 第4座金矿有300金,需要4工人,计算方法同上。
- 第5座金矿有350金,需要3工人,计算方法同上。
将上面的逻辑转化为代码:
class Solution {
public:
int maxGold(const vector<int> &gold, const vector<int> &man, int w) {
int N = gold.size();
// 下标从 1 开始
vector<vector<int> > dp = vector<vector<int> >(N+1, vector<int>(w+1, 0));
// 初始化第一行
for(int i = 1; i <= w; i++) {
if(i < man[0]) {
dp[1][i] = 0;
} else {
dp[1][i] = gold[0];
}
}
for(int i= 2; i<= N; i++) {
for(int j = 1; j <= w; j++) {
if(j < man[i-1]) {
dp[i][j] = dp[i-1][j];
} else {
dp[i][j] = max(dp[i-1][j], dp[i-1][j-man[i-1]] + gold[i-1]);
}
}
}
return dp[N][w];
}
};
上面的代码有优化的空间,我们可以发现,计算当前行的结果只与前一行相关,不需要保留所有的计算结果。这样可以降低空间复杂度。
class Solution {
public:
int maxGold(const vector<int> &gold, const vector<int> &man, int w) {
int N = gold.size();
vector<int> dp = vector<int>(w+1, 0);
for(int i= 1; i<= N; i++) {
for(int j = w; j >= man[i-1]; j--) {
dp[j] = max(dp[j], dp[j-man[i-1]] + gold[i-1]);
}
}
return dp[w];
}
}
我们换个问法,我们把金矿问题转换为 01背包问题:要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别,这两种问法的实现方法是在初始化的时候有所不同。如果要求恰好装满背包,那么在初始化时除了
如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将
为什么呢?可以这样理解:初始化的
class Solution {
public:
int maxGold(const vector<int> &gold, const vector<int> &man, int w) {
int N = gold.size();
vector<int> dp = vector<int>(w+1, INT_MIN);
dp[0] =0;
for(int i= 1; i<= N; i++) {
for(int j = w; j >= man[i-1]; j--) {
dp[j] = max(dp[j], dp[j-man[i-1]] + gold[i-1]);
if(dp[j] < 0) dp[j] = INT_MIN;
}
}
return dp[w];
}
}
递归法
把状态转移方程式翻译成递归程序,递归的结束的条件就是方程式当中的边界。因为每个状态有两个最优子结构,所以递归的执行流程类似于一颗高度为
class Solution {
public:
int getMostGold(const vector<int> &gold, const vector<int> &man, int w, int n) {
if(n <=1 && w < man[0]) {
return 0;
}
if(n == 1 && w >= man[0]) {
return gold[0];
}
if(n > 1 && w < man[n-1]) {
return getMostGold(gold, man, w, n-1);
}
return max(getMostGold(gold, man, w, n-1), getMostGold(gold, man, w - man[n-1], n-1) + gold[n-1]);
}
}
记忆化搜索法
在简单递归的基础上增加一个 map
备忘录,用来存储中间结果。map
的 Key 是一个包含金矿数 N 和工人数 W 的对象,Value 是最优选择获得的黄金数。
方法的时间复杂度和空间复杂度相同,都等同于备忘录中不同 Key 的数量。
auto pair_hash = [fn=hash<int>()](const pair<int, int> &o) {
return ((fn(o.first) << 16) ^ fn(o.second));
};
unordered_map<pair<int, int>, int, decltype(pair_hash)> mem(0, pair_hash);
class Solution{
int getMostGoldWithMap(const vector<int> &gold, const vector<int> &man, int w, int n) {
if(n <=1 && w < man[0]) {
return 0;
}
if(n == 1 && w >= man[0]) {
return gold[0];
}
if(n > 1 && w < man[n-1]) {
pair<int, int> p(n-1, w);
if(mem.find(p) != mem.end()) {
return mem[p];
}
int v = getMostGoldWithMap(gold, man, w, n-1);
mem[p] = v;
return v;
}
pair<int, int> p1(n-1, w);
pair<int, int> p2(n-1, w - man[n-1]);
int a, b;
if(mem.find(p1) != mem.end()) {
a = mem[p1];
} else {
a = getMostGoldWithMap(gold, man, w, n-1);
mem[p1] = a;
}
if(mem.find(p2) != mem.end()) {
b = mem[p2];
} else {
b = getMostGoldWithMap(gold, man, w-man[n-1], n-1);
mem[p2] = b;
b += gold[n-1];
}
return max(a, b);
}
};
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)