动态规划———打家劫舍

本文介绍了动态规划的基础概念,包括重叠子问题、最优子结构和无后向性,并通过打家劫舍问题举例,详细阐述了如何确定状态转移方程,以及算法的实现过程。强调了动态规划适用于能拆解成重叠子问题且具有最优子结构的问题,并提供了C++代码实现。
摘要由CSDN通过智能技术生成

动态规划———打家劫舍

1.问题描述

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1

输入:[1,2,3,1]

输出:4

解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)

     偷窃到的最高金额 = 1 + 3 = 4

示例 2

输入:[2,7,9,3,1]

输出:12

解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)

     偷窃到的最高金额 = 2 + 9 + 1 = 12

来源:力扣(LeetCode

链接:https://leetcode.cn/problems/house-robber

2.解题思路

背景
dp即dynamic programming,动态规划的英文缩写。

本质上,它是一种多阶段决策的最优解模型,一般用来求最值问题。

多阶段决策(重叠子问题),意味着过程可拆解,每个阶段的子问题重复叠加变成最终问题。

最优解模型(最优子结构),由每个子问题的最优,进而递推到全局最优。

最值问题,比如满足xx情况下使用的最小代价,最大收益等等。

什么情况下考虑用dp解决,或者说什么场景下满足dp:

1、可以拆解成重叠子问题,且能找到最优子结构。

2、无后向性:当前阶段的最优解,只关注前面的数据和产出,不会受到后续内容的影响。

确定了是dp问题之后的核心目标:确定状态转移方程

状态转移方程是求解的关键步骤。

简单版:本阶段的状态是上一个阶段的状态和上一个阶段决策的结果转移的方式。

复杂版:本阶段的状态是前面阶段的状态和前面阶段决策,基于一定的条件筛选,得到的结果转移方式。

转移方程体现形式:dp[i] = func(dp[0], dp[1], dp[2], ... dp[i-1])

比如:dp[i] = dp[i-2] + dp[i-1]

所以dp问题主要分两步:

1、确定是dp问题。

2、确定状态转移方程。

原文链接:https://blog.csdn.net/superperter/article/details/130379767

这个是一个大佬讲的dp的入门的文章,小白可以看看,我在这里节选了两个我认为不错的入门文章,毕竟我是将这一道题,但是考虑有些小白可能不太了解dp所以放上去的

2.2 讲讲dp

内容太多了不作介绍,重点部分是无后效性,重叠子问题,最优子结构。

问S->P1和S->P2有多少种路径数,毫无疑问可以先从S开始深搜两次,S->P1和S->P2找出所有路径数,但是当这个图足够大,那就会超时。

动态规划旨在用空间换时间,我们可以发现S->P2的路上,实际上重复计算了S->P1,然后再去计算P1->P2,如果我们第一次计算S->P1的时候,保留了P1点的路径数,那么就不用再次计算S->P1了。

无后效性:未来的状态不会影响过去的状态,如果我在P1->P2的时候,S->P1多了一条路出来,那么先前保留的路径数就是错误的。
本文为CSDN博主「zzc大魔王」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_17807067/article/details/124373801

2.3 实例分析

dp是实际上可以理解为一种搜索,但是他对比于爆搜最大的不同就是他不是一个一个搜素过去,他是一类一类的进行搜素,所以不是所有的题都能用dp来做,他必须要满足无后效性(简单说就是这一类的影响不能涉及下一类,不能对他造成干扰,比如说就是你前天吃的饭是不能对你今天饿不饿在造成影响)。而这个题中他打劫的前的顺序是没有影响的。

首先dp的题可以按照 状态表示 状态表示的属性 状态转移方程怎么写 状态计算来进行思考

考虑题中的状态他想要偷盗的是这些所有家的金币的最大数量 所以状态的表示就可以表示为第i家都投偷完了然后获得的金币数量(这里的状态表示一般是题中问什么,你就表示成什么)。然后是状态表示的属性,题中说让你求得是最大的数量,所以这个的属性就是最大值(属性一般分为最大值,最小值,数量),所以dp数组中存的就是他得到的最大值(注意这里是dp数组所以它不仅存了一个数,他把从第一家到第i家所能偷到的数量都存了下来)。然后就是最关键的状态转移方程(难点),这里怎么想呢?通常会按照最后一个不同的点来进行思考。比如在这里的第i家,他在偷第i家会有两种选则,一个是前面那家没偷,所以我们要看他第i-2家偷多少,也就是f[i-2](这里存的是第i-2家偷完了所拿到的最的金币的数量),然后在加上这个家里的,一个是前面那家偷了,所以我们现在就不能偷了,所以我们这个状态就是f[i-1],既然两种选择我们都表示了出来,那我们要求的属性是最大值,所以我们就把他的两个状态求最大值就是我们偷到第i家中能拿到的最大的数量。所以状态转移方程就是f[i]=max(f[i-2]+a[i],f[i-1])(f[]数组是dp数组存的是第下标家能偷到的最大的金币的数量,a[]数组存的是第i家中存的金币的数量)。然后就是状态计算,也可以说是怎么来进行枚举,并且更新状态数组(其实感觉dp和前缀和有点像都是依赖一个数组来存前面的一个状态,然后在后面进行应用)那怎么进行枚举呢?我们在前面已经说了,我们表示的是第i家所能偷到的金币的最大数量,所以我们就是从i=1开始枚举枚举到最后一家就可以了,把每一家的最大的数量给他求出来,这样就可以让后面进行转移。(建议先看一遍然后在回头再来看一遍)

       2.4算法实现

2.4.1 定义数组

定义a[i]数组来存各个店铺中存的金币的数量f[i]存的是第i家中所能拿到的最大的数量

开始存入

2.4.2初始化数组

F[0]=第一家的金币,f[2]=第一家和第二家的值取大的那个(这个是按照想想怎么从最开始的状态什么是合法的)(但是我们其实不难发现在转移方程的时候我们其实已经把2给赋值好了,所以这个我们只需要赋值f[1]就可以)

2.4.3 遍历顺序

从小到大,这样能保证前面的数组能用上。也是跟你最开始初始化的时候相关。

2.4.4 输出

  输出最后一个,也就是第i家的

2.5 伪代码

具体的算法如算法4所示。,

cin>>a[i]

f[1]=a[1]

for(int i=1;I<=n;i++){
f[i]=max(f[i-1],f[i-2]+a[i])

cout<<f[n]

算法 4打家劫舍.

3.源代码

打家劫舍-C++语言实现

输入4

1 2 3 1

输出:4

#include<iostream>

using namespace std;

int f[100010];

int a[100010];

int main(){

    int t;

    cin>>t;

    while(t--){

        int n;

        cin>>n;

        for(int i=1;i<=n;i++){

            cin>>a[i];

        }

        f[1]=a[1],f[2]=max(a[1],a[2]);//初始化数组,可以让后面的状态能转移过去

        for(int i=2;i<=n;i++){

            f[i]=max(f[i-1],f[i-2]+a[i]);//状态转移方程

        }

        cout<<f[n]<<endl;//输出第i

    }

    return 0;

}

运行结果

那么我们现在把数组的都输出出来

代码样例解释

f[1]=1(第一家最多拿金币1),f[2]=8(第二家最多可以拿金币8)f[3]=8(第三家最多能拿8)

第一家我们初始化的,第二家是从i-1i-2+a[i]转移过来,第三家是我们在前两种情况中选择的取最大的那个,这样我们就能保证我们后面那个也可以这么转移过来

第二组数据第一家最多拿到10金币,走到第二家最多还是拿到10金币,第三家是16金币,第四家是24,因为他判断出来了应该拿1014,这两个,因为当走到第4家的时候我们可以看到他是在不拿和在第二家的拿到最多的时候加上第四家,这个时候,就是无后效性的体现,因为他在第二家的时候你只能看到他数组的中第二家的最优解,但是这就够了,因为你可以不用管他这是怎么拿到的,你只要知道这是最优的。所以就是一共两种状态,取两种状态中更好的那个,而他后面也不会管他是怎么来的。

4.代码说明

dp问题最重要还是想明白转移方程、遍历顺序、数组的初始化。

我们用数组记录上面的就可以,用一个for来遍历每一个房子,通过这个数组里面存的数字来推出后面的数字,同时因为不用枚举每一个情况所以时间复杂度比爆搜要好的多。

1.转移中用前面的数组中存的数字来确定我们后面的数字,所以它不仅能得出第n家的最优解还可以把钱前面的最优解能得出,这是转移的时候决定的。在转移方程中我们可以从前一家不选或者前两家中选这一家中取最大值而前两家中也是最大值并且对我们并没有影响。

2.遍历顺序 这个的遍历顺序很简单,根据转移方程可以知道,我们必须从前往后去遍历,要不他的数组是记录不了值也就无从谈到转移了。这就是遍历顺序,我们要保证这个转移能进行下去,所以我们要让遍历顺序为转移服务。

3.数组的初始化,这个题里面我们的数组的初始化的值就可以是0,但是有的不是,比如求最小值的时候的一般把除了入口都赋值成无穷大来确保他不。会干扰到我们的转移,那我们我这个题的入口是什么呢?是1,因为你不能让数组越界,所以你的第一值就要附上,因为我们的数组的值就是最优解,所以第一家就是他的本身。

4.最后我们只需要把他想要的那个结果给他从我们的状态数组中拿出来给他输出就好了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值