字节青训营题目之最低花费问题--经典动态规划
动态规划
例题
这三道题有类似的地方,都是求在一个地方选择获取多少补给,最终到达终点的最小花费, 只不过后两题少了一些限制,处理更容易一些,理解了第一题后后面的两题也就是小case了
首先分析第一题,也是最复杂的一题
优化青海湖至景点X的租车路线成本
小F计划从青海湖出发,前往一个遥远的景点X进行旅游。景点X可能是“敦煌”或“月牙泉”,线路的路径是唯一的。由于油价的不断上涨,小F希望尽量减少行程中的燃油成本。车辆的油箱容量为400L,在起始点租车时,车内剩余油量为 200L。每行驶 1km 消耗 1L 油。沿途设有多个加油站,小F可以在这些加油站补充燃油;此外,到达目标景点X还车的时候,需要保证车内剩余的油至少有 200L。
小F需要你帮助他计算,如果合理规划加油站的加油顺序和数量,最小化从青海湖到景点X的旅行成本(元)。
输入:
- distance:从青海湖到景点X的总距离(km),距离最远不超过 10000 km。
- n:沿途加油站的数量 (1 <= n <= 100)
- gas_stations:每个加油站的信息,包含两个非负整数 [加油站距离起始点的距离(km), 该加油站的油价(元/L)]
输出:
- 最小化从青海湖到景点X的旅行成本(元)。如果无法到达景点X,或者到达景点X还车时油料剩余不足 200L,则需要返回
-1告诉小F这是不可能的任务。
题目要点整理
起始油量为200, 油箱容量为400
每行驶 1km 消耗 1L 油
最后要求到终点油量为200
首先我们要找到问题能否转移到子问题,也就决定了本题是否可以用DP, 也就是DP问题的关键->如何设置DP数组
如果设置dp[i][j]为经过了i个加油站, 油量为j的最小成本, 即可完成状态转移
如果不加油: dp[i+1][j-dis] = min(dp[i+1][j-dis], dp[i][j]), 成本不变. 走到下一站用了dis的油的花费和上一站的花费一样
如果加油 dp[i+1][j-dis+add] = min(dp[i+1][j-dis+add], dp[i][j]+add*pirce) 成本需要增加加油的价格
加油必须要至少到达下一个加油站, 所以add+j>=dis, 由于限制了油箱大小,所以add需要限制, j+add<=MAX_CAPACITY
最后一步就是dp初始状态的定义, 油箱状态有0到400,共401种状态, 所以dp.size()=(油站数+1, 油箱容量+1), 初始化全部为INT_MAX
初始状态 dp[0][200-dis] = 0
其他处理:
- 由于给的加油站不一定是按照距离排列的, 所以需要先进行排列, 以保证加油站是按照距离由近到远访问的
- 有些加油站可能比目标距离远, 这部分不需要考虑,需要删除
- 终点需要加入一个加油站,以保证最后达到了终点
- 注意在使用python提交时修改返回值为int类型而不是字符串, Impossible为-1
const int MAX_CAPACITY = 400;
const int INT_MAX = 1000000;
std::string solution(int distance, int n, std::vector<std::vector<int>> gasStations1) {
// Please write your code here
//按照距离排序
std::sort(gasStations1.begin(),gasStations1.end(),[](vector<int>& a, vector<int>& b){return a[0]<b[0];});
//剔除到达不了的加油站
vector<std::vector<int>> gasStations;
for(auto it = gasStations1.begin(); it!=gasStations1.end(); it++)
{
if((*it)[0]<distance)
gasStations.push_back(*it);
else
break;
}
gasStations.push_back({distance, 0}); // 加入终点, 油价无所谓,反正不会用到
int gas_n = gasStations.size();
vector<vector<int>> dp(gas_n,vector<int>(MAX_CAPACITY+1,INT_MAX));
//初始油量到达不了第一个加油站
if(gasStations[0][0]>200)
return "Impossible";
//初始化dp数组
dp[0][200-gasStations[0][0]] = 0;
for(int i = 0; i<gas_n-1; i++)
{
//加油站之间的距离
int dis = gasStations[i+1][0]-gasStations[i][0];
int price = gasStations[i][1];
//每种油量都要转移,可以转移到任意加油数的状态
for(int j = 0; j<=MAX_CAPACITY; j++)
{
if(dp[i][j]==INT_MAX) continue;
//此站可以选择不加油直接到达下一站
if(j>=dis)//不加油
dp[i+1][j-dis] = min(dp[i+1][j-dis],dp[i][j]);
//此站选择加油
for(int add = max(1, dis-j); add+j<=MAX_CAPACITY; add++)
{
dp[i+1][j+add-dis] = min(dp[i+1][j+add-dis],dp[i][j]+add*price);
}
}
}
cout<<"ans: "<<to_string(dp[gas_n-1][200])<<endl;;
return dp[gas_n-1][200]!=INT_MAX? to_string(dp[gas_n-1][200]):"Impossible";
}
递归写法, 当前站的成本由上一站成本转移来, 由于递归dfs(i,j)的写法, 无法像DP数组那样直接对dp[i][j-dis]进行操作,所以要重新考虑转移方程,有一些些绕,但是道理都一样
不加油时:dfs(i,j) = dfs(i-1, j+dis);
加油时:dfs(i,j) = dfs(i-1, origin); 其中origin为上一站的油量, 计算方式为: origin + add - dis = j
其中加油时的转移要进行一些限制, 比如add>0,origin+add<=MAX_CAPACITY, 具体代码如下
std::string solution(int distance, int n,
std::vector<std::vector<int>> gasStations1) {
// Please write your code here
std::sort(gasStations1.begin(), gasStations1.end(),
[](vector<int> &a, vector<int> &b) { return a[0] < b[0]; });
//剔除到达不了的加油站
vector<std::vector<int>> gasStations;
for (auto it = gasStations1.begin(); it != gasStations1.end(); it++) {
if ((*it)[0] < distance)
gasStations.push_back(*it);
else
break;
}
gasStations.push_back({distance, 0}); // 加入终点, 油价无所谓,反正不会用到
int gas_n = gasStations.size();
vector<vector<int>> memo(gas_n, vector<int>(401, -1));
//初始油量到达不了第一个加油站
if (gasStations[0][0] > 200)
return "Impossible";
auto dfs = [&](auto&& dfs, int i, int j)->int
{
//初始状态只有油量为200-gasStations[0][0]的状态
if(i==0)
return j==200-gasStations[0][0]?0:INT_MAX;
int& res = memo[i][j];
if(res != -1)
return res;
int dis = gasStations[i][0] - gasStations[i-1][0];
int price = gasStations[i-1][1];
res = INT_MAX;
//不加油, 此时的状态可由不加油的状态转移过来
if (j+dis<=MAX_CAPACITY)
{
res = min(res, dfs(dfs, i-1, j+dis));
}
//加油了
//遍历原始油量 origin + add - dis = j 原始油量+加油量-消耗油量=下一站的剩余油量
for(int origin = 0; origin<MAX_CAPACITY; origin++)
{
int add = j-origin+dis;
//保证加油的量>0且加油之后不会超过容量
if(add>0&&add+origin<=MAX_CAPACITY)
res = min(res, dfs(dfs, i-1, origin)+add * price);
}
return res;
};
int ans = dfs(dfs, gas_n-1, 200);
cout<<"ans: "<<ans<<endl;
return ans == INT_MAX? "Impossible" : to_string(ans);
徒步旅行中的补给问题
小R正在计划一次从地点A到地点B的徒步旅行,总路程需要 N 天。为了在旅途中保持充足的能量,小R每天必须消耗1份食物。幸运的是,小R在路途中每天都会经过一个补给站,可以购买食物进行补充。然而,每个补给站的食物每份的价格可能不同,并且小R最多只能同时携带 K 份食物。
现在,小R希望在保证每天都有食物的前提下,以最小的花费完成这次徒步旅行。你能帮助小R计算出最低的花费是多少吗?
解答:
理解了第一题后这道题也就迎刃而解了, 这一题同样规定了最多只能携带K个食物, 但是补给点为每一天一个,与上一题类似, 只不过省去了距离dis的判断
int solution(int n, int k, std::vector<int> data) {
vector<vector<int>> memo(n, vector<int>(k+1, -1));
auto dfs = [&](auto&& dfs, int i, int bag) -> int
{
if (i == n) {
return 0;
}
int& res = memo[i][bag];
if (res!=-1) {
return res;
}
int ans = 100000;
// 决定买多少份食物
int j = (bag == 0) ? 1 : 0; // bag == 0 时必须买
for (; j <= k - bag; ++j) {
int current_cost = j * data[i] + dfs(dfs, i + 1, bag + j - 1);
ans = min(ans, current_cost);
}
memo[i][bag] = ans;
return ans;
};
return dfs(dfs, 0, 0);
}
补给站最优花费问题
小U计划进行一场从地点A到地点B的徒步旅行,旅行总共需要 M 天。为了在旅途中确保安全,小U每天都需要消耗一份食物。在路程中,小U会经过一些补给站,这些补给站分布在不同的天数上,且每个补给站的食物价格各不相同。
小U需要在这些补给站中购买食物,以确保每天都有足够的食物。现在她想知道,如何规划在不同补给站的购买策略,以使她能够花费最少的钱顺利完成这次旅行。
M:总路程所需的天数。N:路上补给站的数量。p:每个补给站的描述,包含两个数字A和B,表示第A天有一个补给站,并且该站每份食物的价格为B元。
保证第0天一定有一个补给站,并且补给站是按顺序出现的。
解:
这一题同样类似第一题,只不过没有了最大容量MAX_CAPACITY的限制
int solution(int m, int n, std::vector<std::vector<int>> p) {
int food_index = 0;
p.push_back({m, 0});
std::vector<std::vector<int>> memo(n, std::vector<int>(m,-1));
auto dfs = [&](auto&& dfs, int i, int bag)->int
{
if(i==n)
return 0;
int& res = memo[i][bag];
if(res != -1)
return res;
int price = p[i][1];
int dis = p[i+1][0] - p[i][0];
int temp = 10000;
//不买
if(bag>=dis)
temp = std::min(temp, dfs(dfs, i+1, bag-dis));
//买
for(int buy = 1; buy<=m-p[i][0]; buy++)
{
if(buy + bag >= dis)
temp = std::min(temp, buy*price+dfs(dfs, i+1, bag+buy-dis));
}
res = temp;
return temp;
};
int ans = dfs(dfs, 0, 0);
return dfs(dfs, 0, 0);
}
第一次写题解,有写的不对的的地方大家多多包涵, 只是分享一下自己的解题思路,也想借着这个机会结识更多的志同道合的小伙伴!