动态规划(Dynamic Programming, DP)是一种非常经典的算法思想,它通过将问题分解为子问题来递归求解,同时利用记忆化存储中间结果以避免重复计算,从而显著提升效率。在解决一些复杂问题时,动态规划常常需要构建多状态模型,以更好地表示问题的各种约束条件和状态变化。
本文将带你深入了解动态规划的简单多状态模型,帮助你掌握其构建思路和实现方法。
什么是多状态模型?
多状态模型是指在动态规划中,状态不仅仅由单一变量决定,而是由多个变量共同决定。每个状态代表了问题在某个具体条件下的子问题,其转移方程需要考虑所有相关的状态变量。
常见的多状态模型应用场景
-
多维背包问题:需要同时考虑多个背包容量的约束。
-
最短路径问题(带多个约束):路径长度、费用等多种因素需要同时最优。
-
博弈问题:需要跟踪多个玩家的状态。
多状态模型的构建步骤
1. 明确问题的状态表示
在多状态模型中,每个状态可以用一个多维数组表示。例如,二维状态dp[i][j]
可以表示:
-
i
:当前处理到的阶段(如物品、时间等)。 -
j
:当前状态的某一条件值(如容量、费用等)。
2. 确定状态转移方程
根据问题的约束条件,列出当前状态如何从其他状态转移过来。多状态模型通常需要结合多个变量的变化进行转移。
3. 初始化和边界条件
初始化模型中的基础状态,例如当阶段为0时的初始值或无效状态值。
4. 遍历求解和结果提取
按照状态转移的规则,从初始状态递推到最终状态,最后提取目标结果。
案例一 面试题17.16按摩师
题目
一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。
示例 1:
输入: [1,2,3,1] 输出: 4 解释: 选择 1 号预约和 3 号预约,总时长 = 1 + 3 = 4。
示例 2:
输入: [2,7,9,3,1] 输出: 12 解释: 选择 1 号预约、 3 号预约和 5 号预约,总时长 = 2 + 9 + 1 = 12。
算法思路
1.状态表示
dp[i]:表示以i位置为结尾的最长预约时间,但是i位置又可以分为可预约或不预约两种状态,所以可以接着进行细分
f[i]:表示选择到i位置时,nums[i]必选,此时的最长预约时长
g[i]:表示选择到i位置时,nums[i]不选,此时的最长预约时长
2.状态转移方程
如果 [ i ] 位置预约,则 [ i-1 ]不可以预约,所以有f [ i ] = g [ i-1] +nums[ i ];
如果 [ i ] 不预约,则 [ i-1 ]位置有预约和不预约两种状态,所以有 g[ i ] = max( f[ i-1 ], g[ i-1 ]);
3.初始化
由状态转移方程可以知道,f[ 0 ] = nums[0], g[0] = 0;
4.填表顺序
由左到右,两个表一起填
5.返回值
max( f [n-1],g [n-1 ])
编写代码
int massage(vector<int>& nums) {
int n = nums.size();
//注意处理边界条件
if(n==0) return 0;
//创建dp表
vector<int> f(n);
auto g = f;
//初始化
f[0] = nums[0];
g[0] = 0;
//填表
for(int i = 1; i < n; i++)
{
f[i] = g[i-1] + nums[i];
g[i] = max(f[i-1],g[i-1]);
}
//返回值
return max(f[n-1],g[n-1]);
}
案例二213 打家劫舍Ⅱ
题目
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2] 输出:3 解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1] 输出:4 解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
算法思路
因为是围成一圈的,所以先来分析0位置,
如果0位置偷的话,则1和n-1位置不能偷,该模型就转换成 2~n-2位置的打家劫舍Ⅰ问题
如果0位置不偷的话,则该模型转换成 1~n-1位置的打家劫舍Ⅰ问题
1.状态表示
f [i] :偷到 i 位置时,偷nums[ i ], 此时的最大金额
g [ i ]: 偷到 i 位置时,不偷nums[ i ],此时的最大金额
2.状态转移方程
如果i位置偷,则i-1位置不能偷,此时f [i] = g[i-1] + nums[i];
如果i位置不偷, 则 i-1 位置可以偷也可以不偷,所以取两个状态下的最大值
g [ i ] = max( f[i-1], g[i-1] )
3.初始化
f [0] = nums[0] g[0] = 0;
4.填表顺序
由左到右,两个表一起填
5.返回值
max( f [n-1],g [n-1 ])
编写代码
int rob(vector<int>& nums) {
int n = nums.size();
return max(nums[0]+rob1(nums,2,n-2),rob1(nums,1,n-1));
}
int rob1(vector<int>& nums,int left,int right)
{
if(left > right) return 0;
int n = nums.size();
vector<int> f(n);
auto g = f;
f[left] = nums[left];
for(int i = left+1; i < n; i++)
{
f[i] = g[i-1] + nums[i];
g[i] = max(f[i-1],g[i-1]);
}
return max(f[right],g[right]);
}
案例三 740删除并获得点数
题目
给你一个整数数组 nums
,你可以对它进行一些操作。
每次操作中,选择任意一个 nums[i]
,删除它并获得 nums[i]
的点数。之后,你必须删除 所有 等于 nums[i] - 1
和 nums[i] + 1
的元素。
开始你拥有 0
个点数。返回你能通过这些操作获得的最大点数。
示例 1:
输入:nums = [3,4,2] 输出:6 解释: 删除 4 获得 4 个点数,因此 3 也被删除。 之后,删除 2 获得 2 个点数。总共获得 6 个点数。
示例 2:
输入:nums = [2,2,3,3,3,4] 输出:9 解释: 删除 3 获得 3 个点数,接着要删除两个 2 和 4 。 之后,再次删除 3 获得 3 个点数,再次删除 3 获得 3 个点数。 总共获得 9 个点数。
思路分析
对于nums {1, 1, 2, 2, 4, 4, 5, 8, 8, 8}
建立 arr {0, 2, 4, 0, 8, 5, 0, 0, 0, 24} arr[i]:表示“ i ”这个数出现的总和
选择nums[i]之后,则nums[i]-1 和 nums[i]+1 删除,则arr[nums[i] -1] 和arr[nums[i]+1]不选,
即arr[i]的左右不能选,这就转换为打家劫舍问题
预处理:将nums中的数统计到arr中,在arr中做一次打家劫舍问题
1.状态表示
f [i] :选到 i 位置时,nums[i]必选,此时的最大点数
g [ i ]: 选到 i 位置时,nums[i] 不选,此时的最大点数
2.状态转移方程
f[ i ] = g [ i-1 ] + nums[i]
g [ i ] = max( f[i-1], g[i-1] )
3.初始化
f [0] = arr[0] g[0] = 0;
4.填表顺序
由左到右,两个表一起填
5.返回值
max( f [n-1],g [n-1 ])
编写代码
const int N = 10001;
int deleteAndEarn(vector<int>& nums) {
//预处理数组arr
vector<int> arr(N);
for(auto x : nums) arr[x] += x;
//打家劫舍
vector<int> f(N);
auto g = f;
f[0] = arr[0];
for(int i = 1; i < N; i++)
{
f[i] = g[i-1] + arr[i];
g[i] = max(f[i-1], g[i-1]);
}
return max(f[N-1],g[N-1]);
}
案例四 LCR 91 粉刷房子
题目
假如有一排房子,共 n
个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。
当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个 n x 3
的正整数矩阵 costs
来表示的。
例如,costs[0][0]
表示第 0 号房子粉刷成红色的成本花费;costs[1][2]
表示第 1 号房子粉刷成绿色的花费,以此类推。
请计算出粉刷完所有房子最少的花费成本。
示例 1:
输入: costs = [[17,2,17],[16,16,5],[14,3,19]] 输出: 10 解释: 将 0 号房子粉刷成蓝色,1 号房子粉刷成绿色,2 号房子粉刷成蓝色。 最少花费: 2 + 5 + 3 = 10。
示例 2:
输入: costs = [[7,6,2]] 输出: 2
算法思路
1.状态表示
因为每个房子都有三种选择,对应的就有三种状态,所以用一个二维数组表示
dp[i][0]: 粉刷 i 位置,该位置涂成红色的最小花费
dp[i][1]: 粉刷 i 位置,该位置涂成蓝色的最小花费
dp[i][2]: 粉刷 i 位置,该位置涂成绿色的最小花费
2. 状态转移方程
dp[ i ] [0 ]: i 位置涂成红色,花费已经固定,此时需要 0 ~ i-1位置的花费最小
分析i-1位置:只能涂成蓝色或绿色,所需花费分别是dp[ i -1][1] dp[i-1][2]
dp[i][0] = min(dp[ i -1][1], dp[i-1][2]) + costs[i][0];
dp[i][1] = min(dp[ i -1][0], dp[i-1][2]) + costs[i[0]];
dp[i][2] = min(dp[ i -1][0], dp[i-1][2]) + costs[i][0];
3.初始化
添加虚拟节点,但是要注意填值正确和下标映射关系
4.填表顺序
从左向右,一次填写三张表
5.返回值
min(dp[n][0], dp[n][1] dp[n][2]). 三者的最小值
编写代码
int minCost(vector<vector<int>>& costs)
{
int n = costs.size();
vector<vector<int>> dp(n+1,vector<int>(3));
for(int i = 1; i <= n; i++)
{
//注意下标映射关系
dp[i][0] = min(dp[i-1][1],dp[i-1][2]) + costs[i-1][0];
dp[i][1] = min(dp[i-1][0],dp[i-1][2]) + costs[i-1][1];
dp[i][2] = min(dp[i-1][0],dp[i-1][1]) + costs[i-1][2];
}
return min(dp[n][0],min(dp[n][1],dp[n][2]));
}
结语
动态规划的多状态模型为我们解决复杂问题提供了强有力的工具。掌握这种模型的构建方法,不仅能帮助我们高效解决算法题,还能将其应用到现实中的优化问题中。希望本文对你理解动态规划多状态模型有所帮助!