动态规划算法----简单多状态模型

动态规划(Dynamic Programming, DP)是一种非常经典的算法思想,它通过将问题分解为子问题来递归求解,同时利用记忆化存储中间结果以避免重复计算,从而显著提升效率。在解决一些复杂问题时,动态规划常常需要构建多状态模型,以更好地表示问题的各种约束条件和状态变化。

本文将带你深入了解动态规划的简单多状态模型,帮助你掌握其构建思路和实现方法。


什么是多状态模型?

多状态模型是指在动态规划中,状态不仅仅由单一变量决定,而是由多个变量共同决定。每个状态代表了问题在某个具体条件下的子问题,其转移方程需要考虑所有相关的状态变量。

常见的多状态模型应用场景

  1. 多维背包问题:需要同时考虑多个背包容量的约束。

  2. 最短路径问题(带多个约束):路径长度、费用等多种因素需要同时最优。

  3. 博弈问题:需要跟踪多个玩家的状态。


多状态模型的构建步骤

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]));

    }

结语

动态规划的多状态模型为我们解决复杂问题提供了强有力的工具。掌握这种模型的构建方法,不仅能帮助我们高效解决算法题,还能将其应用到现实中的优化问题中。希望本文对你理解动态规划多状态模型有所帮助!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值