算法提高课学习——1.动态规划——1.4状态机模型

状态机模型

1.大盗阿福(线性dp|状态机|空间优化O(1))

题目描述

阿福是一名经验丰富的大盗。趁着月黑风高,阿福打算今晚洗劫一条街上的店铺。

这条街上一共有 N 家店铺,每家店中都有一些现金。

阿福事先调查得知,只有当他同时洗劫了两家相邻的店铺时,街上的报警系统才会启动,然后警察就会蜂拥而至。

作为一向谨慎作案的大盗,阿福不愿意冒着被警察追捕的风险行窃。

他想知道,在不惊动警察的情况下,他今晚最多可以得到多少现金?

输入格式
输入的第一行是一个整数 T,表示一共有 T 组数据。

接下来的每组数据,第一行是一个整数 N ,表示一共有 N 家店铺。

第二行是 N 个被空格分开的正整数,表示每一家店铺中的现金数量。

每家店铺中的现金数量均不超过1000。

输出格式
对于每组数据,输出一行。

该行包含一个整数,表示阿福在不惊动警察的情况下可以得到的现金数量。

数据范围
1≤T≤50,
1≤N≤105
输入样例:

2
3
1 8 2
4
10 7 6 14

输出样例:

8
24

样例解释
对于第一组样例,阿福选择第2家店铺行窃,获得的现金数量为8。

对于第二组样例,阿福选择第1和4家店铺行窃,获得的现金数量为10+14=24。

解题思路

这道题可以直接用线性dp来做,也能用自动机来做
1.线性dp:
状态表示f[i]
集合:表示选择前i家店铺行窃所能获得的最大现金数量
属性MAX
状态计算f[i]=max(f[i-2]+w[i],f[i-1])
解释:考虑前i家店铺,对于第i家店铺,我们可以选择行窃或者不行窃,如果行窃第i家店铺,那么根据题意,我们就不能行窃第i-1家,那么最大值就是前i-2家行窃的最大值加上第i家,如果不行窃第i家店铺,那么第i-1家可以行窃,最大价值就是前i-1家行窃的最大值f[i-1],二者取最大就行,最后的结果就是f[n]
2.状态机
状态表示f[i][0],f[i][1]
集合:所有走了i步,当前位于状态j的走法,j0或者1
属性MAX
状态计算f[i][0]=(f[i-1][1],f[i-1][0]), f[i][1]=f[i-1][0]+w[i]
解释:用0表示不行窃,1表示行窃,那么走的方式就可以用一个状态图来表示,如下:
在这里插入图片描述
有三种情况0->1 0->0 1->0,每次我们都只考虑最后一个店铺选与不选,当我们不选最后一个店铺,那么状态就转化成0,而转化成0有两种方式,0->0 1->0,根据状态表示的属性可以得到状态转移方程就是f[i][0]=max(f[i-1][0],f[i-1][1]),同理,当我们选择最后一个店铺,那么状态就转化成1,而转化成1只有一种方式,0->1,根据状态表示的属性可以得到状态转移方程f[i][1]=f[i-1][0]+w[i],最后的最大值应该是max(f[n][0],f[n][1])
这就结束了??no no no 😃
空间也能压缩的哦 从O(N)到O(1)
我们可以发现,第i家店铺行窃或者不行窃,都只跟第i-1家或者第i-2家有关,所以我们可以只记录这两个值!!

代码实现

线性dp:

#include<iostream>
using namespace std;
const int N=100010;
int w[N],f[N];
int main()
{
    int t,n;
    cin>>t;
    while(t--){
        cin>>n;
        for(int i=1;i<=n;i++) cin>>w[i];
        for(int i=1;i<=n;i++) f[i]=max(f[i-2]+w[i],f[i-1]);
        printf("%d\n",f[n]);
    }
    return 0;
}

状态机

#include<iostream>
#include<cstring>
using namespace std;
const int N=100010,NIF=0x3f3f3f3f;
int w[N],f[N][2];
int n,t;
int main()
{
    cin>>t;
    while(t--){
        cin>>n;
       
        for(int i=1;i<=n;i++) cin>>w[i];
        f[0][0]=0,f[0][1]=-NIF;//初始化,前0件状态为0,就是没有行窃,最大价值为0,前0家状态为1是不可能的 没有店铺状态怎么能是行窃呢?莫非..
        for(int i=1;i<=n;i++){
            f[i][0]=max(f[i-1][0],f[i-1][1]);
            f[i][1]=f[i-1][0]+w[i];
        }
        printf("%d\n",max(f[n][0],f[n][1]));
    }
    return 0;
}

空间压缩

#include<iostream>
#include<algorithm>
using namespace std;
const int N=100010;
int w[N];
int main()
{
    int yd=0,byd=0;//第一家 第二家
    int n,t;
    cin>>t;
    while(t--){
        cin>>n;
        for(int i=1;i<=n;i++) cin>>w[i];
        byd=w[1];//如果只有一家店铺 偷它!
        yd=max(w[1],w[2]);//如果只有两家店铺 偷现金多的那个 :)
        for(int i=3;i<=n;i++){
            int tmp=yd;
            yd=max(yd,byd+w[i]);
            byd=tmp;
        }
        cout<<yd<<endl;
    }
    
    return 0;
}

2.股票买卖Ⅳ

题目描述

给定一个长度为 N 的数组,数组中的第 i 个数字表示一个给定股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润,你最多可以完成 k 笔交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。一次买入卖出合为一笔交易。

输入格式
第一行包含整数 N 和 k,表示数组的长度以及你可以完成的最大交易数量。

第二行包含 N 个不超过 10000 的正整数,表示完整的数组。

输出格式
输出一个整数,表示最大利润。

数据范围
1≤N≤105,
1≤k≤100
输入样例1:

3 2
2 4 1

输出样例1:

2

输入样例2:

6 2
3 2 6 5 0 3

输出样例2:
7

样例解释
样例1:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

样例2:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。共计利润 4+3 = 7.

解题思路

状态机用来表示两种状态,用1表示手中有股票,0表示手中无股票,转换如下图:
在这里插入图片描述
状态表示f[i][j][0] f[i][j][1]
集合f[i][j][0] 表示从前i件股票中选,正在进行第j次交易且手中无货的状态,f[i][j][1] 表示从前i件股票选,正在进行第j次交易且手中有货的状态。
属性MAX
状态计算f[i][j][0]=max(f[i-1][j][0],f[i-1][j][1]+w[i]) f[i][j][1]=max(f[i-1][j][1],f[i-1][j-1][0]-w[i])
解释:有四种转换 先看转换为有货的两个状态:从有货到有货:也就是说我们没有第 i 次我们没有进行任何交易,所以交易花费 0,状态表示为为f[i][j][1]->f[i-1][j][1]从无货到有货,也就是第i次我们买入了股票,花费为w[i],状态表示为:f[i][j][0]=f[i-1][j-1]-w[i]。然后再看转换为无货的两种状态:从无货到无货,也就是我们没有进行任何交易,所以交易花费0,状态表示为f[i][j][0]->f[i-1][j][0]从有货到无货:也就是说我们将股票卖出了,状态表示为f[i][j][1]->f[i-1][j-1][0]+w[i]
最后的答案是枚举所有可能的交易f[n][i][0],(i从0~k),然后取最大,因为我们不确定交易多少次是最大值,而且最后一定是手中无货才能取最大值

代码实现

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;

const int N=100010,M=110;
int w[N];
int f[N][M][2];
int main()
{
    int n,k;
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++) scanf("%d",&w[i]);
    memset(f,-0x3f,sizeof f);//初始状态肯定是手中无货,f[i][0][1]一定是不可能的
    for(int i=0;i<=n;i++) f[i][0][0]=0;//前i个股票 ,0次交易 手中一定无货,价值为0
    for(int i=1;i<=n;i++){
        for(int j=1;j<=k;j++){
            f[i][j][0]=max(f[i-1][j][0],f[i-1][j][1]+w[i]);
            f[i][j][1]=max(f[i-1][j][1],f[i-1][j-1][0]-w[i]);
        }
        
    }
    int res=0;
    for(int j=0;j<=k;j++) res=max(res,f[n][j][0]);
    printf("%d\n",res);
    return 0;
}

3.股票买卖Ⅴ

题目描述

给定一个长度为 N 的数组,数组中的第 i 个数字表示一个给定股票在第 i 天的价格。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
输入格式
第一行包含整数 N,表示数组长度。

第二行包含 N 个不超过 10000 的正整数,表示完整的数组。

输出格式
输出一个整数,表示最大利润。

数据范围
1≤N≤105
输入样例:

5
1 2 3 0 2

输出样例:

3

样例解释
对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出],第一笔交易可得利润 2-1 = 1,第二笔交易可得利润 2-0 = 2,共得利润 1+2 = 3。

解题思路

这题有三个状态,其实就是将上一题无货的状态拆分成两个状态,无货第一天和无货大于等于2天
状态表示f[i][j]
集合:表示考虑到第i个股票,状态为j的利润最大值,j可以取0(有货),1(无货第一天),2(无货大于等于两天)
属性MAX
状态计算f[i][0]=max(f[i-1][0],f[i-1][2]-w[i]) f[i][1]=f[i-1][0]+w[i] f[i][2]=max(f[i-1][1],f[i-1][2])
解释,这次的状态机有三种状态,如下图:
在这里插入图片描述
我们只考虑最后一次交易:
如果最后一次交易是手中有货f[i][0],那么它的最大值有两个来源,也就是状态机里有两个转换可以到这个状态手中有货到手中有货,也就是交易花费为0,状态表示为f[i-1][0]手中无货大于等于2天到手中有货,那么一定是买入股票,花费为 w[i],状态表示为 f[i-1][2]-w[i]两者取最大
如果最后一次交易是手中无货第一天f[i][1],那么它的最大值只有一个来源,就是从手中有货到手中无货第一天,此时股票一定卖出,收益w[i],状态表示为f[i-1][0]+w[i]
如果最后一次交易是手中无货第n天(n>=2)即f[i][2],那么它的最大值有两个来源,也就是状态机有两个转换可以到这个状态手中无货第一天到手中无货第二天,状态表示为f[i-1][1]手中无货第n天到手中无货第n天,状态表示为f[i-1][2],二者取最大即可
最后的最大值是在max(f[n][1],f[n][2]),因为有两个出口

代码实现

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=100010;
int w[N];
int f[N][3];//0表示手中有货 1表示手中无货第一天 2表示手中无货天数大于等于2
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++) cin>>w[i];
    
    f[0][0]=-0x3f3f3f3f;
    for(int i=1;i<=n;i++){
        f[i][0]=max(f[i-1][2]-w[i],f[i-1][0]);
        f[i][1]=f[i-1][0]+w[i];
        f[i][2]=max(f[i-1][1],f[i-1][2]);
    }
    printf("%d\n",max(f[n][1],f[n][2]));
    return 0;
}

还有一个比较难的(我觉得)状态机的题 设计密码大家有兴趣可以去做 我看了一个多小时 理解不了 所以肯定讲不清楚 时间不多了 只能先放弃

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值