第一章 动态规划 状态机模型

1、什么是状态机模型

可以参考这个文章OI wiki

状态机是有限状态自动机的简称,是现实事物运行规则抽象而成的一个数学模型

状态机描述的是事物的不同状态之间转化的关系。举一个门的例子。门有两种状态,开着和关着。其中开着可以继续保持开着的状态,也可以转换到关着的状态;关着可以继续保持关着的状态,也可以转换到开着状态。
在这里插入图片描述
这样就把门的运行规则抽象成了这样一个模型。这种模型可以很清楚的表示出来不同状态下,门可以做哪些操作,可以转移到什么状态。

2、例题

1. 大盗阿福

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。

2. 分析

按照题目要求,我们确定有两个状态

  1. 抢了状态
    代表抢劫了第i家店铺,根据题意,不可以再抢第i+1个店铺,也就是其的状态不可以指向自己
  2. 没抢状态
    代表没抢第i家店铺,根据题意,其可以继续不抢或者抢第i+1个店铺

那么状态机如下所示
在这里插入图片描述
i每走一步,要走状态机的一条边。每一步走一条边。题目里任何一个抢劫的方案都能对应一个长度为n的走法,任何一个长度为n的状态机走法,也都对应一个抢劫方案。
我们用0代表不抢劫的状态,用1代表抢劫的状态。
在这里插入图片描述
根据状态机,f[i,0]代表从1到i个店铺中选抢劫对象,且不抢劫第i个店铺。f[i,1]同理。f[i,1]可以由f[i - 1,0]和f[i - 1,1]转移过来,而对于f[i,1]只可以由f[i - 1,0]转移过来。
由属性max可以得到状态转移方程
f [ i , 0 ] = m a x ( f [ i − 1 , 0 ] , f [ i − 1 , 1 ] ) f[i,0] = max(f[i - 1,0],f[i - 1,1]) f[i,0]=max(f[i1,0],f[i1,1]) f [ i , 1 ] = f [ i − 1 , 1 ] + w [ i ] f[i,1] = f[i - 1,1] + w[i] f[i,1]=f[i1,1]+w[i]
w[i]代表抢劫第i家店铺的收益。

我自己是这样理解的。如果没有不能连着抢两家的限制,那么每个店铺是否被抢的决定都是独立的。也就是i-1的任何状态都可以转移给i。但是加了这个限制之后,每个店铺的状态就不是任何的前一个状态都能转移的了,只有符合状态机描述的转移是合法的。也就是第i个商店是否被抢是有不同的转移方式的。状态机就是描述这个状态转移的模型。

最后还有个入口的问题。我们初始的时候一定是什么也没抢。也就是虚构一个第0家商铺,并且不抢这个商铺。为了这个目的,我们将f[0][1]设为负收益。以便不能从这个状态向后转移。

3. 代码

#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
const int N = 100010;
int a[N],f[N][2];
int main()
{
    int T;
    cin >> T;
    while(T --)
    {
        int n;
        cin >> n;
        //设置入口条件
        f[0][1] = -0x3f3f3f3f;
        for(int i = 1; i <= n; i ++)
        {
            int w;
            cin >> w;
            f[i][0] = max(f[i - 1][0],f[i - 1][1]);
            f[i][1] = f[i - 1][0] + w;
        }
        cout << max(f[n][0],f[n][1]) << endl;
    }
}

2. 股票买卖IV

1. 题目描述

给定一个长度为 N 的数组,数组中的第 i 个数字表示一个给定股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润,你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。一次买入卖出合为一笔交易。

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

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

数据范围
1≤N≤10^5,
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.

2. 分析

与上题的店铺有多种状态类似,我们看这里每一笔交易也有多种状态。一开始是手中无货,然后买入,然后是手中有货,然后卖出。这样是一笔交易。
在这里插入图片描述
用状态机描述如上图所示。状态机主要说明了状态转移的相关信息。下面我们用0代表空仓状态,1代表持仓状态。
在这里插入图片描述

  1. f[i,j,0]从i-1的持仓卖出来,代表完成了第j笔交易(从空仓到持仓到空仓),所以这部分应该表示为f[i-1,j,1] + w[i](w[i]代表该股票第i天的价格)
  2. f[i,j,0]从i-1的空仓来,应表示为f[i - 1,j,0]
  3. f[i,j,1]从i-1的持仓来,应表示为f[i - 1,j,1]
  4. f[i,j,1]从i-1的空仓来,代表开启了一笔新的交易,买入了股票,也就是j是一笔新的交易(j代表当前正在进行第j笔交易),应表示为f[i - 1,j - 1,0] - w[i]。

所以有
f [ i , j , 0 ] = m a x ( f [ i − 1 , j , 0 ] , f [ i − 1 , j , 1 ] + w [ i ] ) f[i,j,0] = max(f[i - 1,j,0],f[i - 1,j,1] + w[i]) f[i,j,0]=max(f[i1,j,0],f[i1,j,1]+w[i]) f [ i , j , 1 ] = m a x ( f [ i − 1 , j , 1 ] , f [ i − 1 , j − 1 , 0 ] − w [ i ] ) f[i,j,1]=max(f[i - 1,j,1],f[i - 1,j - 1,0] - w[i]) f[i,j,1]=max(f[i1,j,1],f[i1,j1,0]w[i])
对于入口条件,也就是进行第0笔交易的情况,因为要从空仓状态进入状态机,除了f[i][0][0]其他的状态都是非法的。所以要给其他状态赋负权益。

3. 代码

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 100010, M = 110;
int f[N][M][2];

int main()
{
    int n,k;
    cin >> n >> k;
    memset(f, -0x3f, sizeof f);
    for (int i = 0; i <= n; i ++ ) f[i][0][0] = 0;
    for(int i = 1; i <= n; i ++)
    {
        int w;
        cin >> w;
        for(int j = 1; j <= k; j ++)
        {
            f[i][j][0] = max(f[i - 1][j][0],f[i - 1][j][1] + w);
            f[i][j][1] = max(f[i - 1][j - 1][0] - w,f[i - 1][j][1]);
        }
    }
    int res = 0;
    for(int i = 0; i <= k; i ++)
    {
        res = max(res,max(f[n][i][0],f[n][i][1]));
    }
    cout << res;
}

3. 股票买卖V

1. 题目

给定一个长度为 N 的数组,数组中的第 i 个数字表示一个给定股票在第 i 天的价格。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

输入格式
第一行包含整数 N,表示数组长度。
第二行包含 N 个不超过 10000 的正整数,表示完整的数组。

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

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

2. 分析

根据本题目的情况,每笔交易被分成了三种状态,第一种是持仓状态,第二种是手中无货的第一天(不可以交易),第三种是手中无货的第二天(可以任意交易)。
按照题目要求我们画出如下的状态机。
在这里插入图片描述
我们用2代表空仓第二天,1代表空仓第一天,0代表持仓状态。
在这里插入图片描述
根据状态机

  1. f[i,2]可以从空仓第一天和空仓第二天来,所以可以表示为max(f[i-1,1],f[i-1,2])
  2. f[i,1]只能从持仓来,所以表示为f[i - 1,0] + w[i]
  3. f[i,0]可以从持仓和空仓第二天来,所以可以表示为max(f[i - 1,0],f[i - 1,2] - w[i])

因为第一天交易不需要冷却时间,所以应当从第0天的空仓第二天转移过去才符合要求。所以需要给f[0][0]和f[0][1]赋予负权益。

3. 代码

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 100010;
int f[N][3];
int main()
{
    int n;
    cin >> n;
    f[0][0] = -0x3f3f3f3f;
    f[0][1] = -0x3f3f3f3f;
    f[0][2] = 0;
    for(int i = 1; i <= n; i ++)
    {
        int w;
        cin >> w;
        f[i][0] = max(f[i - 1][0],f[i - 1][2] - w);
        f[i][1] = f[i - 1][0] + w;
        f[i][2] = max(f[i - 1][1],f[i - 1][2]);
    }
    cout << max(f[n][1],f[n][2]);
}

4. 设计密码(状态机+KMP)

1. 题目

你现在需要设计一个密码 S,S 需要满足:

S 的长度是 N;
S 只包含小写英文字母;
S 不包含子串 T;
例如:abc 和 abcde 是 abcde 的子串,abd 不是 abcde 的子串。
请问共有多少种不同的密码满足要求?
由于答案会非常大,请输出答案模 10^9+7 的余数。

输入格式
第一行输入整数N,表示密码的长度。
第二行输入字符串T,T中只包含小写字母。

输出格式
输出一个正整数,表示总方案数模 10^9+7 后的结果。

数据范围
1≤N≤50,
1≤|T|≤N,|T|是T的长度。

输入样例1:
2
a
输出样例1:
625
输入样例2:
4
cbc
输出样例2:
456924

2. 分析

对于一个n位的密码,他有 2 6 n 26^n 26n中情况(每个位置都可以填一个小写字母),肯定不能暴力枚举。我们使用dp来处理。
对于母串去匹配一个字符串t(设其长度为m)的过程中有m个状态,分别是匹配到第一个字符,匹配到前两个字符,匹配到前i个字符,匹配到前m个字符。对于状态之间的转移,在母串中的i与子串中的j+1相等时,j会向后一位转移,如果不匹配的话,j就会按照next数组一直向前转移( kmp算法可参考 KMP)。对于不同的t,其状态机模型不同。
举一个具体的例子来说
对于子串t = ababc来说,其next数组为[0,0,1,2,0]
在这里插入图片描述
在这里插入图片描述
对于f[i,3]来说

  1. 如果母串第i个字符为b,状态机将会从3走到4
  2. 如果母串第i个字符不为b,他将按照next数组走到1的位置进行对比,如果成功则走到1状态。否则就继续按照next数组进行移动,这里我们知道,就到达0,此时就走到0状态。

3. 代码

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 55, mod = 1e9 + 7;

int ne[N],f[N][N];
char p[N];
int main()
{
    int n,m;
    cin >> n >> p + 1;
    m = strlen(p + 1);
    //预处理next数组
    for(int i = 2, j = 0; i <= m; i ++)
    {
        while(j && p[i] != p[j + 1]) j = ne[j];
        if(p[i] == p[j + 1]) j ++;
        ne[i] = j;
    }
    
    f[0][0] = 1;
    //第i个位置
    for(int i = 0; i < n; i ++)
    {
        //后缀最长匹配的长度
        for(int j = 0; j < m; j ++)
        {
            //枚举k的所有情况
            for(char k = 'a'; k <= 'z'; k ++)
            {
                //ptr实际上就是在寻找下一个状态我要奔赴状态机的哪一个状态
                int ptr = j;
                while(ptr && k != p[ptr + 1]) ptr = ne[ptr];
                if(k == p[ptr + 1]) ptr ++;
                //确定之后,就把f[i][j]的数量加到f[i+1][ptr]上
                f[i + 1][ptr] = (f[i + 1][ptr] + f[i][j]) % mod;
            }
        }
    }
    int res = 0;
    for(int i = 0; i < m; i ++) res = (res + f[n][i]) % mod;
    cout << res;
}

参考资料

  1. 状态机模型简介
  2. KMP
  3. Acwing用户彩虹铅笔题解
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值