动态规划——用带权的有向图描述状态的转移

前言

此文章的作用:教会读者用简单、快速、稳定的方法推出线性DP的转移方程。

笔者希望通过一些题教会读者做所有线性DP的方法,而不是只教会读者这些题的解法。

先看题:一本通 大盗阿福

我们发现:店铺越多,可以偷的店铺就越多,能得到的现金就越多。

举例:如果只有一家店铺,你就只能偷第一家,只能拿到第一家店铺的现金。但如果有三家店铺,你就可以偷第一和第三家,同时拿到第一家和第三家的现金。

这说明:(最多可以得到的现金)是(店铺数量)的函数

这又说明表示状态中肯定有一个维度是店铺数量。

所以暂时 d p i dp_i dpi :偷前 i i i 家店铺的最大值。

再次观察发现对于每个店铺只有偷和不偷两个选择。

所以设 d p i , [ 0 / 1 ] dp_{i,[0/1]} dpi,[0/1]:对于前 i i i 家店铺, [ [ [ 不偷 ( 0 ) (0) (0) / / / ( 0 ) (0) (0) ] ] ] i i i 家店铺的最大值。

有了状态,就要推状态转移方程了。

第一种画法

回归标题,我们用带权的有向图描述状态的转移,其中点表示状态,边表示状态的转移增量

先画出有哪些状态,如图:
在这里插入图片描述

先考虑 d p i , 0 dp_{i,0} dpi,0 可以由那些状态转移过来

  1. d p i , 0 dp_{i,0} dpi,0 是否可以由 d p i − 1 , 0 dp_{i-1,0} dpi1,0 转移过来?两家店铺都没有偷,不会触发警报,可以转移。因为并当前状态没有选择偷第 i i i 个店铺,所以转移增量为 0 0 0 。所以,有一条边权为 0 0 0 的边从 d p i − 1 , 0 dp_{i-1,0} dpi1,0 连向 d p i , 0 dp_{i,0} dpi,0(边权为 0 0 0 的默认不在图上标出边权)。

现在的图就变成了这样:

在这里插入图片描述

  1. d p i , 0 dp_{i,0} dpi,0 是否可以由 d p i − 1 , 1 dp_{i-1,1} dpi1,1 转移过来?只偷上一家店铺,不偷当前店铺,没有同时偷相邻的两家,不会触发警报,所以可以转移。同样因为并当前状态没有选择偷第 i i i 个店铺,所以转移增量为 0 0 0 。所以,有一条边权为 0 0 0 的边从 d p i − 1 , 0 dp_{i-1,0} dpi1,0 连向 d p i , 0 dp_{i,0} dpi,0(边权为 0 0 0 的默认不在图上标出边权)。

现在的图就变成了这样:

在这里插入图片描述

现在考虑 d p i , 1 dp_{i,1} dpi,1 可以由哪些状态转移过来

  1. d p i , 1 dp_{i,1} dpi,1 是否可以由 d p i − 1 , 0 dp_{i-1,0} dpi1,0 转移过来?只偷当前店铺,不偷上一家店铺,没有同时偷相邻的两家,不会触发警报,所以可以转移,因为并当前状态选择偷第 i i i 个店铺,所以转移增量为 w i w_i wi ,所以,有一条边权为 w i w_i wi 的边从 d p i − 1 , 0 dp_{i-1,0} dpi1,0 连向 d p i , 0 dp_{i,0} dpi,0

现在的图就变成了这样:

在这里插入图片描述

  1. d p i , 1 dp_{i,1} dpi,1 是否可以由 d p i − 1 , 1 dp_{i-1,1} dpi1,1 转移过来?即偷了上一家店铺,也偷了当前店铺,同时偷了相邻的两家,会触发警报,所以不能转移。

第二种画法

这道题对于每个店铺都有 [ [ [ 不偷 ( 0 ) (0) (0) / / / ( 1 ) (1) (1) ] ] ] 两种状态,如图:

在这里插入图片描述

  1. 状态 0 0 0 可以由状态 0 0 0 转移过来,转移增量为 0 0 0

在这里插入图片描述

  1. 状态 0 0 0 可以由状态 1 1 1 转移过来,转移增量为 0 0 0

在这里插入图片描述

  1. 状态 1 1 1 可以由状态 0 0 0 转移过来,转移增量为 w i w_i wi

在这里插入图片描述

  1. 状态 1 1 1 不可以由状态 1 1 1 转移过来。

那么,看着图就很容易写出状态转移方程了。

d p i , 0 = max ⁡ ( d p i − 1 , 0 , d p i − 1 , 1 ) dp_{i,0}=\max(dp_{i-1,0},dp_{i-1,1}) dpi,0=max(dpi1,0,dpi1,1)
d p i , 1 = d p i − 1 , 0 dp_{i,1}=dp_{i-1,0} dpi,1=dpi1,0

代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n,w[1000005],dp[1000005][2];
void work(){
	cin>>n;
	for(ll i=1;i<=n;i++)cin>>w[i];
	for(ll i=0;i<=n;i++)dp[i][0]=dp[i][1]=0;
	for(ll i=1;i<=n;i++){
		dp[i][0]=max(dp[i-1][0],dp[i-1][1]);
		dp[i][1]=dp[i-1][0]+w[i];
	}
	cout<<max(dp[n][0],dp[n][1])<<endl;
}
signed main(){
	ios::sync_with_stdio(false);
	ll T;cin>>T;
	while(T--)work();
	return 0;
}

再次强调声明,题目虽然不难,可能有人不画图也能想出状态转移方程,我希望教会读者的是这做一类题的方法,而不只是这一道题的解法。

再看一道题:Acwing 股票买卖

我们发现:天数越多,可以买卖的股票就越多,能得到的利润就越多。

这说明:(最多能得到的利润)是(天数)的函数

这又说明表示状态中肯定有一个维度是股票数量。

所以暂时 d p i dp_i dpi :对于前 i i i 天 ,可以获得的的最大值。

再次观察发现对于每天只有持有股票和不持有股票两个选择。

所以设 d p i , [ 0 / 1 ] dp_{i,[0/1]} dpi,[0/1] :对于前 i i i 天,第 i i i [ [ [ 未持有(0) / / / 持有(1) ] ] ] 股票的最利润。

第一种画法

先把有哪些状态画出来:

在这里插入图片描述

先考虑 d p i , 0 dp_{i,0} dpi,0 可以由哪些状态转移过来。

  1. d p i , 0 dp_{i,0} dpi,0 可以由 d p i − 1 , 0 dp_{i-1,0} dpi1,0 转移过来。因为都是不持有股票的状态,没有买卖,所以转移增量为 0 0 0

如图:

在这里插入图片描述

  1. d p i − 1 , 0 dp_{i-1,0} dpi1,0 可以由 d p i − 1 , 1 dp_{i-1,1} dpi1,1 转移过来,昨天有票,但今天没票了,这说明当天卖出了股票,卖出股票可以赚钱。所以转移增量为 w i w_i wi

在这里插入图片描述

再考虑 d p i , 1 dp_{i,1} dpi,1 可以由哪些状态转移过来。

  1. d p i , 1 dp_{i,1} dpi,1 可以由 d p i − 1 , 0 dp_{i-1,0} dpi1,0 转移过来,昨天不持有股票,今天持有股票,说明今天买入了股票,买入股票需要花钱,所以转移增量为 − w i -w_i wi

在这里插入图片描述

  1. d p i , 1 dp_{i,1} dpi,1 可以由 d p i − 1 , 1 dp_{i-1,1} dpi1,1 转移过来。因为都是持有股票的状态,没有买卖,所以转移增量为 0 0 0

在这里插入图片描述

第二种画法

先画出有哪些状态:

在这里插入图片描述

  1. 状态 0 0 0 可以由状态 0 0 0 转移过来,转移增量为 0 0 0

在这里插入图片描述

  1. 状态 0 0 0 可以由状态 1 1 1 转移过来,所以转移增量为 w i w_i wi

在这里插入图片描述
3. 状态 1 1 1 可以由状态 1 1 1 转移过来,转移增量为 0 0 0

在这里插入图片描述

  1. 状态 1 1 1 可以由状态 0 0 0 转移过来,所以转移增量为 − w i -w_i wi

在这里插入图片描述

看着图就很容易写出状态转移方程了。

d p i , 0 = max ⁡ ( d p i − 1 , 0 , d p i − 1 , 1 + w i ) dp_{i,0}=\max(dp_{i-1,0},dp_{i-1,1}+w_i) dpi,0=max(dpi1,0,dpi1,1+wi)
d p i , 1 = max ⁡ ( d p i − 1 , 0 − w i , d p i − 1 , 1 ) dp_{i,1}=\max(dp_{i-1,0}-w_i,dp_{i-1,1}) dpi,1=max(dpi1,0wi,dpi1,1)

在这里再教大家一个确定状态转移方程边界值的方法。

1. 1. 1. 在哪赋边界值?

    ~~~     需要处理的值域范围的最小值往前推一个单位。

    ~~~     就拿这道题举例,我们需要处理的值域范围是 1 1 1~ n n n,那么就给第 0 0 0 天附上边界值。

2. 2. 2. 赋什么值?

    ~~~     如果该状态是非法的,就赋一个极大值或极小值,因为一个状态不会通过一个非法的状态转移过来。

    ~~~     如果该状态是合法的,就赋一个有限的值(通常是 0 0 0)。

    ~~~     还是拿这道题举例举例, d p 0 , 0 dp_{0,0} dp0,0 状态是合法的,附上 0 0 0 d p 0 , 1 dp_{0,1} dp0,1 状态是非法的,赋上极小值。

代码

#include<bits/stdc++.h>
#define ll long long
#define INF 0x3f3f3f3f3f3f3f
using namespace std;
ll n,dp[100005][2],w[100005];
signed main(){
	ios::sync_with_stdio(false);
	cin>>n;
	for(ll i=1;i<=n;i++)cin>>w[i];
	dp[0][1]=-INF;dp[0][0]=0;
	for(ll i=1;i<=n;i++){
		dp[i][0]=max(dp[i-1][0],dp[i-1][1]+w[i]);
		dp[i][1]=max(dp[i-1][0]-w[i],dp[i-1][1]);
	}
	cout<<dp[n][0]<<endl;
	return 0;
}

再来看看股票买卖的升级版 Acwing 股票买卖(k笔交易)

发现这道题与上一题的区别就是多了一个限制:只能有最多 k k k 笔交易。

那我们在状态的表示上是不是就要多一维:当前正在进行第几笔交易。

所以设 d p i , j , [ 0 / 1 ] dp_{i,j,[0/1]} dpi,j,[0/1]:对于前 i i i 天,第 j j j 笔交易 [ [ [ 未持有 ( 0 ) (0) (0) / / / 持有 ( 1 ) (1) (1) ] ] ] 股票的最大利润。

先看第一种画法

先画出有哪些状态

在这里插入图片描述

发现这样画的话过于复杂了,我们需要简化一下。如下图:

在这里插入图片描述

先考虑 d p i , j , 0 dp_{i,j,0} dpi,j,0 可以由哪些状态转移过来。

  1. d p i , j , 0 dp_{i,j,0} dpi,j,0 可以由 d p i − 1 , j − 1 , 0 dp_{i-1,j-1,0} dpi1,j1,0 转移过来吗?不可以,因为两种状态都没有持有股票,也就是没有进行交易,因为没有进行交易,所以当前这笔交易不能从上一笔交易转移过来。

  2. d p i , j , 0 dp_{i,j,0} dpi,j,0 可以由 d p i − 1 , j − 1 , 1 dp_{i-1,j-1,1} dpi1,j1,1 转移过来吗?可以,昨天持有股票,今天不持有股票,说明卖出了一张股票,所以转移增量为 w i w_i wi

在这里插入图片描述

  1. d p i , j , 0 dp_{i,j,0} dpi,j,0 可以由 d p i − 1 , j , 0 dp_{i-1,j,0} dpi1,j,0 转移过来吗?可以,因为两种状态都没有持有股票,也就是没有进行交易,所以当前这笔交易可以从昨天的同一笔交易转移过来。因为没有买卖,所以转移增量为 0 0 0

在这里插入图片描述

  1. d p i , j , 0 dp_{i,j,0} dpi,j,0 可以由 d p i − 1 , j , 1 dp_{i-1,j,1} dpi1,j,1 转移过来吗?当然不可以,你买了股票不可能退回去吧。

再来考虑 d p i , j , 1 dp_{i,j,1} dpi,j,1 可以由哪些状态转移过来。

  1. d p i , j , 1 dp_{i,j,1} dpi,j,1 可以由 d p i − 1 , j − 1 , 0 dp_{i-1,j-1,0} dpi1,j1,0 转移过来吗?不可以,因为不能直接跳过上一笔交易的持有股票状态和当前这一笔交易的未持有股票状态。

  2. d p i , j , 1 dp_{i,j,1} dpi,j,1 可以由 d p i − 1 , j − 1 , 1 dp_{i-1,j-1,1} dpi1,j1,1 转移过来吗?不可以,因为上一笔交易股票必须要先买了才能买当前股票(题目要求)。

  3. d p i , j , 1 dp_{i,j,1} dpi,j,1 可以由 d p i − 1 , j , 0 dp_{i-1,j,0} dpi1,j,0 转移过来吗?可以,从这一笔交易不持有股票到持有股票,说明买入了股票,买股票要花钱,所以转移增量为 − w i -w_i wi

在这里插入图片描述

  1. d p i , j , 1 dp_{i,j,1} dpi,j,1 可以由 d p i − 1 , j , 1 dp_{i-1,j,1} dpi1,j,1 转移过来吗?可以,因为两种状态都持有股票,也就是没有进行交易,所以当前这笔交易可以从昨天的同一笔交易转移过来。因为没有买卖,所以转移增量为 0 0 0

在这里插入图片描述

再来看第二种画法

每天的每笔交易只有两个状态 [ [ [ 未持有股票 ( 0 ) (0) (0) / / / 持有股票 ( 1 ) (1) (1) ] ] ]

在这里插入图片描述

先考虑状态 0 0 0 可以由哪些状态转移过来。

  1. 状态 0 0 0 可以由昨天的同一笔交易的状态 0 0 0 转移过来,转移增量为 0 0 0

在这里插入图片描述

  1. 状态 0 0 0 可以由昨天的上一笔交易的状态 1 1 1 转移过来,转移增量为 w i w_i wi

在这里插入图片描述

再虑状态 1 1 1 可以由哪些状态转移过来。

  1. 状态 1 1 1 可以由昨天的同一笔交易的状态 1 1 1 转移过来,转移增量为 0 0 0

在这里插入图片描述

  1. 状态 1 1 1 可以由昨天的同一笔交易的状态 0 0 0 转移过来,转移增量为 − w i -w_i wi

在这里插入图片描述

接下来就是看着图推状态转移方程

d p i , j , 0 = max ⁡ ( d p i − 1 , j − 1 , 1 + w i , d p i − 1 , j , 0 ) dp_{i,j,0}=\max(dp_{i-1,j-1,1}+w_i,dp_{i-1,j,0}) dpi,j,0=max(dpi1,j1,1+wi,dpi1,j,0)
d p i , j , 1 = max ⁡ ( d p i − 1 , j , 0 − w i , d p i − 1 , j , 1 ) dp_{i,j,1}=\max(dp_{i-1,j,0}-w_i,dp_{i-1,j,1}) dpi,j,1=max(dpi1,j,0wi,dpi1,j,1)

给代码赋初值的方法和上一道题一样,这里再拿这道题举个例子

1. 1. 1. 在哪赋边界值?

    ~~~     我们需要处理的值域范围是 1 1 1~ n n n,那么就给第 0 0 0 天附上边界值。

2. 2. 2. 赋什么值?

    ~~~     d p 0 , ( 1 ≤ j ≤ k ) , 0 dp_{0,(1\le j\le k),0} dp0,(1jk),0 状态是合法的,附上 0 0 0 d p 0 , ( 1 ≤ j ≤ k ) , 1 dp_{0,(1\le j\le k),1} dp0,(1jk),1 状态是非法的,赋上极小值。

注意:因为我们要求的是做完 k k k 笔交易后的利润,所以要在代码中的 k k k 加上 1 1 1

还有一件事:这道题卡空间,需要滚动数组优化。为了方便读者理解,我会放两版代码,一版是滚动数组优化前的,一版是滚动数组优化后的。

代码

滚动数组优化前的:

#include<bits/stdc++.h>
#define ll long long
#define INF 0x3f3f3f3f3f3f3f
using namespace std;
ll n,k,w[100005],dp[100005][105][2];
signed main(){
	ios::sync_with_stdio(false);
	cin>>n>>k;k++;
	for(ll i=1;i<=n;i++)cin>>w[i];
	for(ll j=1;j<=k;j++)dp[0][j][1]=-INF;
	for(ll i=0;i<=n;i++)dp[i][0][1]=-INF;
	for(ll i=1;i<=n;i++){
		for(ll j=1;j<=k;j++){
			dp[i][j][0]=max(dp[i-1][j][0],dp[i-1][j-1][1]+w[i]);
			dp[i][j][1]=max(dp[i-1][j][1],dp[i-1][j][0]-w[i]);
		}
	}
	cout<<dp[n][k][0];
	return 0;
}

滚动数组优化后的:

#include<bits/stdc++.h>
#define ll long long
#define INF 0x3f3f3f3f3f3f3f
using namespace std;
ll n,k,w[100005],dp[2][105][2];
signed main(){
	ios::sync_with_stdio(false);
	cin>>n>>k;k++;
	for(ll i=1;i<=n;i++)cin>>w[i];
	for(ll j=1;j<=k;j++)dp[0][j][1]=-INF;
	for(ll i=1;i<=n;i++){
		dp[0][0][1]=-INF;
		for(ll j=1;j<=k;j++){
			dp[1][j][0]=max(dp[0][j][0],dp[0][j-1][1]+w[i]);
			dp[1][j][1]=max(dp[0][j][1],dp[0][j][0]-w[i]);
		}
		for(ll j=1;j<=k;j++){
			dp[0][j][0]=dp[1][j][0];
			dp[0][j][1]=dp[1][j][1];
		}
	}
	cout<<dp[1][k][0];
	return 0;g
}

再来看最后一道题

股票买卖的另一种升级版 Acwing [股票买卖(含冷冻期)]

这道题原题面不知道为什么打不开,但是我们可以在LeetCode上看题面

发现这道题与股票买卖的差别就是卖完股票后不能立刻买股票,那我们往前多考虑一位就可以了。

还是设 d p i , [ 0 / 1 ] dp_{i,[0/1]} dpi,[0/1] 为对于前 i i i 天,第 i i i [ [ [ 未持有股票 ( 0 ) (0) (0) / / / 持有股票 ( 1 ) (1) (1) ] ] ] 能获得的最大利润。

画法一

先画出有哪些状态。

在这里插入图片描述

d p i , 0 dp_{i,0} dpi,0 可以由 d p i − 1 , 0 dp_{i-1,0} dpi1,0 d p i − 1 , 1 dp_{i-1,1} dpi1,1 转移过来,之前已经讲过,这里就不赘述了。

在这里插入图片描述

d p i , 1 dp_{i,1} dpi,1 可以由 d p i − 1 , 1 dp_{i-1,1} dpi1,1 转移过来,同样上面讲过, d p i , 1 dp_{i,1} dpi,1 要由 d p i − 2 , 0 dp_{i-2,0} dpi2,0 转移,因为至少要隔一天。

在这里插入图片描述

画法二

过程大家都很熟悉了,直接上最终版。

在这里插入图片描述

状态转移方程:

d p i , 0 = max ⁡ ( d p i − 1 , 0 , d p i − 1 , 1 + w i ) dp_{i,0}=\max(dp_{i-1,0},dp_{i-1,1}+w_i) dpi,0=max(dpi1,0,dpi1,1+wi)
d p i , 1 = max ⁡ ( d p i − 2 , 0 − w i , d p i − 1 , 1 ) dp_{i,1}=\max(dp_{i-2,0}-w_i,dp_{i-1,1}) dpi,1=max(dpi2,0wi,dpi1,1)

这道题因为原题面有问题我就不放代码了,大家理解了就行。

总结: 用带权的有向图描述状态的转移

如果你还有问题,可以私信我,如果是在工作日,我会尽量在 12 12 12 小时内回复。

比起点赞收藏和关注,我更希望你们可以把不懂的地方告诉我。

正文结束

附:

这个方法我是看了这位老师的讲解学会的。

废话

啊啊啊啊啊啊啊啊啊啊啊终于写完了啊啊啊啊啊啊啊。

为了大家有一个更流畅的阅读体验,还特意压缩了图片大小(虽然可能还是有点卡)

要是A股可以这样,我还刷什么题呀。

作为一个正在准备NOIP的初三信竞生,时间真的不多,所以我要断更一周。

无向带权图可以使用邻接矩阵来存储,邻接矩阵是一个二维数组,其中第i行第j列的元素表示第i个顶点到第j个顶点的边的权重(如果两个顶点之间没有边,则对应的元素为0)。以下是C语言中无向带权图邻接矩阵的建立代码: ```c #define MAX_VERTEX_NUM 100 // 最大顶点数 #define INF 65535 // 表示正无穷 typedef struct { int vertex[MAX_VERTEX_NUM]; // 存储顶点 int arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; // 存储边的权值 int vexnum, arcnum; // 存储图的顶点数和边数 } MGraph; void CreateMGraph(MGraph *G) { int i, j, k, w; printf("请输入顶点数和边数:\n"); scanf("%d%d", &(G->vexnum), &(G->arcnum)); printf("请输入顶点信息:\n"); for (i = 0; i < G->vexnum; i++) { scanf("%d", &(G->vertex[i])); } for (i = 0; i < G->vexnum; i++) { for (j = 0; j < G->vexnum; j++) { G->arcs[i][j] = INF; // 初始化为正无穷 } } printf("请输入边的信息:\n"); for (k = 0; k < G->arcnum; k++) { printf("请输入第%d条边的起点、终点和权值:\n", k+1); scanf("%d%d%d", &i, &j, &w); G->arcs[i-1][j-1] = w; G->arcs[j-1][i-1] = w; // 无向图需要对称存储 } } ``` 在上述代码中,我们首先定义了一个结构体MGraph,包含了一个顶点数组、一个邻接矩阵和图的顶点数和边数。然后,我们使用CreateMGraph函数来创建无向带权图的邻接矩阵。在函数中,我们首先输入图的顶点数和边数,然后输入每个顶点的信息,接着初始化邻接矩阵为正无穷,最后输入每条边的起点、终点和权值,并将其存储到邻接矩阵中。需要注意的是,无向图需要对称存储。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值