(算法提高课)动态规划-状态机模型

1049. 大盗阿福(简单版本,可不用状态机)
1057. 股票买卖 IV

1049. 大盗阿福

阿福是一名经验丰富的大盗。趁着月黑风高,阿福打算今晚洗劫一条街上的店铺。
这条街上一共有 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法,对于某个物品 i i i,有选择和不被选择两种状态,若用 f ( i ) f(i) f(i)来表示前 i i i个物品可以产生的最大收益,则有
f ( i ) = { f [ i − 1 ] ,                           ( 第 i 个物品不被选择 ) f [ 1 − 2 ] + w [ i ] ,                   ( 第 i 个物品被选择 ) f(i) = \begin{cases} f[i - 1],~~~~~~~~~~~~~~~~~~~~~~~~~~(第i个物品不被选择) \\ f[1 - 2] + w[i],~~~~~~~~~~~~~~~~~(第i个物品被选择) \end{cases} f(i)={f[i1]                          (i个物品不被选择)f[12]+w[i],                 (i个物品被选择)

#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int w[N], f[N];

int main()
{
    int T; scanf("%d", &T);
    for(int t = 0;t < T;t ++){
        int n; scanf("%d", &n);
        for(int i = 1;i <= n;i ++){
            scanf("%d", &w[i]);
            if(i > 2)
                f[i] = 0;
        }

        f[1] = w[1]; f[0] = 0;
        f[2] = max(w[1], w[2]);
        //之前做过很多次的模型
        for(int i = 3;i <= n;i ++){
            f[i] = max(f[i - 1], f[i - 2] + w[i]);
        }

        cout << f[n] << endl;
    }
    return 0;
}

本题还可以引入状态机模型做法,使得当前状态只和上一层状态有关
当只关注上层状态时,也即前 i − 1 i-1 i1个物品的状态时,我们会发现第 i − 1 i-1 i1个物品被选择和不被选择的情况被混合在了 f ( i − 1 ) f(i-1) f(i1)里,而第 i − 1 i-1 i1个物品是否被选择影响了当前状态 f ( n ) f(n) f(n)。因此我们可以引入一个状态量来表示最后一个物品是否被选择,将原本被混合的复杂情况分开来

在这种做法中我们用状态机 f ( i , j ) f(i,j) f(i,j)来表示:走到第 i i i步时,当前的状态 j j j。其中 j j j为0或1,0表示最后一个物品未被选择,1表示最后一个物品被选择。由每步的选择可以得到状态机,而由状态机也可以反推出每一步的选择
可以得到如图的关系图:
在这里插入图片描述
(若第 i i i个物品没有被选择,则第 i − 1 i - 1 i1个物品可选择可不被选择;若第 i i i个物品被选择,则第 i − 1 i-1 i1个物品只能不被选择)
这种算法下的代码:

#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int w[N], f[N][2];   //状态机数组
int main()
{
    int T; scanf("%d", &T);
    while(T --){
        int n; scanf("%d", &n);
        for(int i = 1;i <= n;i ++){
            scanf("%d", &w[i]);
        }
        
        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;
}

1057. 股票买卖 IV

给定一个长度为 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.

状态量:当前是否持有物品(股票)

状态量转化:

  • 无物品——>无物品(继续不购入)
  • 无物品——>有物品(购入物品)
  • 有物品——>有物品 (继续持有物品)
  • 有物品——>无物品 (卖出)

同上题一样,在对某个状态做分析的时候情况有很多难以分开,而其中最大的划分就是当前是否持有物品;而某个时刻向下个时刻的转化也可以通过状态量的四种转化来唯一刻画,此时构成一个状态机
在这里插入图片描述
(有种有向图的感觉,每条边是状态量之间的转化,边权根据题意而来,通常是花费或者是消耗)

f ( i , j , k ) f(i,j,k) f(i,j,k)表示行走了 i i i步、正在进行第 j j j次交易、当前状态量为 k k k时,产生的最大收益(其中 k k k为0或1)具体来说, f [ i , j , 0 ] f[i, j, 0] f[i,j,0]表示前 i i i个股票,已经进行完 j j j次交易,且手中没有买入时的最大收益; f [ i , j , 1 ] f[i, j, 1] f[i,j,1]表示前i个股票,已经进行完 j − 1 j - 1 j1次交易,且已经买入第 j − 1 j - 1 j1个股票但还没卖出时的最大收益。状态计算方程如下:
{ f [ i , j ] = m a x ( f [ i , j , 0 ] ,    f [ i , j , 1 ] )           / / 当前状态只有持有和不持有两种状态 f [ i , j , 0 ] = m a x ( f [ i − 1 , j , 0 ] ,    f [ i − 1 , j , 1 ] + w [ i ] f [ i , j , 1 ] = m a x ( f [ i − 1 , j , 1 ] ,    f [ i − 1 , j − 1 , 0 ] − w [ i ] \begin{cases} f[i,j] = max(f[i,j,0], ~~f[i,j,1]) ~~~~~~~~~//当前状态只有持有和不持有两种状态\\ 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] \end{cases} f[i,j]=max(f[i,j,0],  f[i,j,1])         //当前状态只有持有和不持有两种状态f[i,j,0]=max(f[i1,j,0],  f[i1,j,1]+w[i]f[i,j,1]=max(f[i1,j,1],  f[i1,j1,0]w[i]
第二个式子容易出错,注意题目中的条件:一次买入卖出合为一笔交易。所以卖出时(从1到0)其实是在进行某一笔交易的下半部分,但仍然在这笔交易当中;而当我买入的时候,算作开启了一次新的交易,因此此时是j - 1了

#include<bits/stdc++.h>
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]);
    
    //将合法入口全部初始化为0,非合法入口全部初始化为负值
    //比如f[i][0][1] 就是非法的(持有物品至少开始了一次交易)
    memset(f, -0x3f, sizeof f);
    for (int i = 0; i <= n; i ++ ) f[i][0][0] = 0;
    
    //开始dp
    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 ans = 0;
    //如样例,不是进行k次交易就一定会拿到最大值;
    //不能直接输出f[n][k][0],很可能进行不了k次交易,比如样例一,最多进行1次交易,此时输出两次交易的话属于非法值,将会输出-INF
    for(int i = 1;i <= k;i ++) ans = max(ans, f[n][i][0]);
    cout << ans;
    return 0;
}

运行结果如图:
在这里插入图片描述
可优化代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N = 100010, M = 110;
int w[N];
int f[M][2];   //这里简化掉第一维度(物品的N维)

int main()
{
    int n,k; 
    scanf("%d%d", &n, &k);
    for(int i = 1;i <= n;i ++) scanf("%d", &w[i]);
    
    //开始dp
    for(int i = 1;i <= n;i ++){
        for(int j = 1;j <= k;j ++){
            if(i == 1)    f[j][1] = -w[1];   //初始化,这里只需要对持有情况做改变,全局数组下值全部默认为0
            
            else {
            
                f[j][0] = max(f[j][0], f[j][1] + w[i]);
                f[j][1] = max(f[j][1], f[j - 1][0] - w[i]);
            }
        }
    }
    //这里的含义是最多进行K次交易可以产生的最大值?
    cout << f[k][0];
    return 0;
}

运行结果如图:
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值