目录
1、问题描述
舍友正在健身,每天必须摄入一定食物的营养价值。但是每天摄入食物的总重量是有限制的,否则健身效果很差。那么舍友在摄入食物总重量不超过限制总重量的情况下,在想如何通过食物搭配以获取最大的营养价值,从而达到更好的健身效果!
2、基本思路
根据问题描述,可以将舍友健身营养价值选择问题转化为以下的数学模型,dp[i][w]为:
其中,foods[i-1].weight表示第i个食物的重量,foods[i-1].nutritive_value表示第i个食物的营养价值。
该递推关系的含义是,对于第i个食物,如果其重量小于等于当前限制总重量wlimit_weight,并且放入当前第i个食物的总价值大于不放入当前第i个食物的总价值时,那么选择该食物并将其营养价值加到前i-1个食物放入限制重量为w-foods[i-1].weight时的最大总价值上,得到选择该食物时的最大总价值。否则,其余情况均是不选择该食物,最大总价值不变。
最终的解为dp[foods.size()][limit_weight],表示在foods.size()个食物中,限制重量为limit_weight时的最大总价值。
并且满足如下约束条件:
- 每个食物的重量和营养价值都是非负整数。
- 限制重量limit_weight是非负整数。
- 每个食物只能被选择或不被选择这两种情况,不能部分放入。
- 选择食物的总重量不能超过限制总重量limit_weight。
- 选择的食物要使得总价值最大。
3、问题分析
3.1、动态规划算法
动态规划的基本思想是将求解问题分解成若干互相子问题, 先求解子问题, 然后从子问题的解当中得到原问题的解。 适用于动态规划求解的问题必须具备以下的条件:
- 有最优子结构: 如果问题的最优解所包含的子问题的解也是最优解, 则该问题有最优子结构, 即满足最优解原理。
- 问题中的状态必须满足无后效性: 所谓无后效性是指下一时刻的状态只与当前状态有关, 而与当前状态的之前的状态无关。
- 问题有重叠子问题: 也就是说子问题是不独立的, 一个子问题在下一阶段决策中可能被多次用到。 动态规划算法相较于其他算法的优势正是利用了这种子问题的重叠性质, 对每一个子问题只做一次解, 而后将其解保存在一个表格中, 在以后尽可能多地利用这些子问题的解。
3.2、算法设计与分析
定义食物结构体用来组织和存储相关数据的数据类型,每一个食物均有食物名称、营养价值和食物重量三个属性。代码如下:
#ifndef FOOD_H
#define FOOD_H
#include <string>
struct Food {
std::string name; // 食物名称
int nutritive_value; // 营养价值
int weight; // 食物重量
};
#endif
定义一个二维数组来存储动态规划过程中产生的各种数据,同时定义一个三维数组来存储动态规划过程中所选食物名称。如下:
vector<vector<int>> dp(foods.size() + 1, vector<int>(limit_weight + 1, 0));
vector<vector<vector<string>>>selectedfoods(foods.size()+1, vector<vector<string>>(limit_weight + 1));
动态规划过程的伪代码如下:
for (int i = 1; i <= foods.size(); ++i) {
for (int w = 0; w <= limit_weight; ++w) {
if (foods[i - 1].weight <= w) {
int take = dp[i - 1][w - foods[i - 1].weight] + foods[i - 1].nutritive_value;
int skip = dp[i - 1][w];
if (take >= skip) {
dp[i][w] = take;
selectedfoods[i][w] = selectedfoods[i - 1][w - foods[i - 1].weight];
selectedfoods[i][w].push_back(foods[i - 1].name);
}
else {
dp[i][w] = skip;
selectedfoods[i][w] = selectedfoods[i - 1][w];
}
}
else {
dp[i][w] = dp[i - 1][w];
selectedfoods[i][w] = selectedfoods[i - 1][w];
}
}
}
3.3、时间与空间复杂度分析
- 时间复杂度分析:外层循环 for (int i = 1; i <= foods.size(); ++i) 迭代了 n 次,其中 n 是食物的数量。内层循环 for (int w = 0; w <= limit_weight; ++w) 迭代了 limit_weight 次,其中 limit_weight 是限制的重量。在内层循环中,进行了一些常数时间的操作,如比较、加法、复制等。因此,总体时间复杂度为 O(n * limit_weight),其中 n 是食物的数量,limit_weight 是限制的重量。
- 空间复杂度分析:二维数组 dp 和三维数组 selectedfoods 都具有 foods.size() + 1 行和 limit_weight + 1 列。额外的空间用于存储食物的名称、营养价值和重量。固定的额外空间用于存储循环中的临时变量和输入参数。因此,总体空间复杂度主要由 dp 和 selectedfoods 数组占用的空间决定,为 O(n * limit_weight)。
- 结论:这段代码的时间复杂度为 O(n * limit_weight),空间复杂度也为 O(n * limit_weight)。在处理小规模问题时,这是一个有效的解决方案。但请注意,在 limit_weight 较大的情况下,空间开销可能会变得很大。
3.4 、算法测试
为验证算法的正确性与可靠性,设计了如下两组测试用例, 如表1、 表2所示。测试结果分别如图1, 图2所示。
表1 测试用例(1)
食物名称 | apple | banana | orange |
营养价值 | 4 | 5 | 6 |
食物重量 | 3 | 2 | 4 |
限制总重量 | 5 | ||
期待所选食物 | apple、banana | 最大营养价值 9 |
表2 测试用例(2)
食物名称 | apple | banana | orange | pear |
营养价值 | 4 | 5 | 6 | 2 |
食物重量 | 5 | 4 | 6 | 3 |
限制总重量 | 10 | |||
期待所选食物 | banana、orange | 最大营养价值 11 |
图1 测试用例(1) 测试结果
图2 测试用例(2) 测试结果
4、结语
通过对舍友健身营养价值选择问题进行分析, 建立一个算法模型并进行了算法设计, 实际的算法测试结果证明了算法的有效性。从以上实例分析与设计也可以看出, 动态规划生活实际问题时支配规则的高效性, 并且思路清晰简便, 易于实现。 并且动态规划算法在生活中的应用十分广泛,有最优化、路径规划、字符串处理、组合优化问题、机器学习、经济学和金融、生物学和生物信息学以及游戏理论等各个方面。因此它是值得研究的问题, 有待深入研究。
5 、附录
- Food.h头文件代码如下
#pragma once
#ifndef FOOD_H
#define FOOD_H
#include <string>
struct Food {
std::string name;
int nutritive_value;
int weight;
};
#endif
- dynamicProgramming.cpp文件代码如下
#include <iostream>
#include <vector>
#include <string>
#include "Food.h"
using namespace std;
void input(vector<Food>& foods, int& limit_weight) {
int n;
cout << "请输入食物数量 n 和限制总重量 limit_weight:";
cin >> n >> limit_weight;
cout << endl;
foods.resize(n);
for (int i = 0; i < n; ++i) {
cout << "请输入第" << i + 1 << "种食物的名称、营养价值和重量:";
cin >> foods[i].name >> foods[i].nutritive_value >> foods[i].weight;
cout << endl;
}
}
void dynamicProgramming(const vector<Food>& foods, const int limit_weight) {
vector<vector<int>> dp(foods.size() + 1, vector<int>(limit_weight + 1, 0));
vector<vector<vector<string>>> selectedfoods(foods.size() + 1, vector<vector<string>>(limit_weight + 1));
for (int i = 1; i <= foods.size(); ++i) {
for (int w = 0; w <= limit_weight; ++w) {
if (foods[i - 1].weight <= w) {
int take = dp[i - 1][w - foods[i - 1].weight] + foods[i - 1].nutritive_value;
int skip = dp[i - 1][w];
if (take >= skip) {
dp[i][w] = take;
selectedfoods[i][w] = selectedfoods[i - 1][w - foods[i - 1].weight];
selectedfoods[i][w].push_back(foods[i - 1].name);
}
else {
dp[i][w] = skip;
selectedfoods[i][w] = selectedfoods[i - 1][w];
}
}
else {
dp[i][w] = dp[i - 1][w];
selectedfoods[i][w] = selectedfoods[i - 1][w];
}
}
}
int totalSelectedWeight = 0;
for (const string& foodName : selectedfoods[foods.size()][limit_weight]) {
for (const Food& food : foods) {
if (food.name == foodName) {
totalSelectedWeight += food.weight;
break;
}
}
}
cout << "根据动态规划算法得到所选食物总重量为: " << totalSelectedWeight << endl;
cout << endl;
cout << "根据动态规划算法得到最大营养价值总和: " << dp[foods.size()][limit_weight] << endl;
cout << endl;
cout << "根据最大营养价值总和建议选择如下食物: ";
for (const string& foodname : selectedfoods[foods.size()][limit_weight]) {
cout << foodname << " ";
}
cout << endl;
}
int main() {
vector<Food> foods;
int limit_weight;
while (1) {
cout << "继续执行此程序?(yes or no):";
string interrupt_flag = "no", key_board_interrupt;
cin >> key_board_interrupt;
cout << endl;
if (interrupt_flag == key_board_interrupt)
break;
input(foods, limit_weight);
dynamicProgramming(foods, limit_weight);
cout << "----------------------------------------------------------" << endl;
}
return 0;
}