题解 区间DP之多维度复杂转移-luogu.P1220&P3205
题目
首先给出两个题目。
题意建模
题目都比较复杂,我们一个一个来分析。 按照笔者在DP概述这篇文章中所论述的那样,任何一类DP的问题,都可以采用我们这种最科学的方法去思考和理解,所以具体问题具体分析:
1.P1220
第一步肯定是定义状态,但首先得找到状态,所以就先去看一下。
理解一下题目大意:关灯不需要额外的时间,经过了灯就关了。但是可能折返回去关某一个大灯会比继续往下走关接下来的一个小灯更优,那么可以得到两种状态(沿着当前方向继续往下走,改变方向回去关灯)。既然得到了两种状态,就应该考虑之间的联系,也即如何去转移(思考状态转移方程)。但在这之前,首先得思考一下如何划分阶段:
划分的阶段依据来自于一种拓扑序,在这里找一找拓扑序:只考虑某一次和这一次前面一次的状态,很明显可以知道关了多少个路灯,所以可以将其作为阶段吗?
但是很明显,这是无法写出(准确来说是很难写出)状态转移方程的。但这时注意到,
某一村庄在一条路线上安装了 n 盏路灯,每盏灯的功率有大有小(即同一段时间内消耗的电量有多有少)。老张就住在这条路中间某一路灯旁,他有一项工作就是每天早上天亮时一盏一盏地关掉这些路灯。
这说明了什么?可以考虑将区间的大小作为阶段,正好也包括路灯的信息,这应该是正解了! 所以现在就可以考虑定义状态:
我们思考,朝一个方向走后,我们更新了什么状态呢?答案是左边待开的灯和右边待开的灯的编号,这是很直观的。我们考虑 代表区间
,
的灯已经全部关闭时的时间点已经浪费的电量。继续向下思考,我们发现还漏了一个状态,就是当前这个时间点老张是在i点还是j点。思考为什么会这样?
答案是因为题目中老张每次走到另一个目标点时,都会在这中间的过程中产生额外的代价!思考如何解决这个问题?这好办,加一维就行了则:
代表区间
,
的灯已经全部关闭时老王在
处
或
处
的时间点已经浪费的电量,结合图片可以知道如何去转移。
给出状态转移的方程:
dp[i][j][0]=min(dp[i+1][j][0]+cal(i,i+1,i,j),//从原来的i+1处转移而来
dp[i+1][j][1]+cal(i,j,i,j));//从原来的j处转移而来
dp[i][j][1]=min(dp[i][j-1][0]+cal(i,j,i-1,j-1),//从原来的i处转移而来
dp[i][j-1][1]+cal(j-1,j,i-1,j-1));//从原来的j-1处转移而来
其中, 函数计算从上一个区间转移过来时没关的路灯消耗的电力。这里详细一些的说一下cal()函数。
我们要计算某阶段消耗的电力,则需要知道这一段的路程(即时间)和没有关的电灯的功率之和。想要尽快的计算,我们可以利用前缀和 找到功率之和。 令
为功率的前缀和数组,
存储路灯位置。这里就不给出计算方法,code中很详细。
2.P3205
如何设计状态?
那么我们要怎么设计状态,我们想,每给人进入队伍里,只有 2 种可能:
-
从左边加入;
-
从右边进入。
所以我们的状态是有3个数:
表示的是第
人从左边进来的方案数;
表示的是第
人从右边进来的方案数。
推导状态转移方程
从左边进来肯定前 1 个人比他高,前 1 个人有 2 种情况,要么在 i+1 号位置,要么在 j 号位置。同理,从右边进来肯定前 1 个人比他矮,前 1 个人有 2 种情况,要么在 j−1 号位置,要么在 i 号位置。
那么状态转移方程就出来了:
if(a[i]<a[i+1]) dp[i][j][0]+=dp[i+1][j][0];//从i+1处转移而来
if(a[i]<a[j]) dp[i][j][0]+=dp[i+1][j][1];//从j处转移而来
if(a[j]>a[i]) dp[i][j][1]+=dp[i][j-1][0];//从i处转移而来
if(a[j]>a[j-1]) dp[i][j][1]+=dp[i][j-1][1];//从j-1处转移而来
dp[i][j][0]%=mod;
dp[i][j][1]%=mod;//取模,不再赘述
算法分析
那么现在可以看看算法的组织了。首先分开看两题:
1.P1220
- 全局求最小,初始化肯定最大。
- 什么时候会是边界?肯定是在端点处,并且是最开始的时候,这时
的值肯定是0。
- 最后在哪里找答案?肯定是看看状态的定义,易知是最终的区间两端取一个min。
2.P3205
- 当
的时候显然只有一种方案,所以边界条件是:
for(int i=1;i<=n;i++)f[i][i][0]=1,f[i][i][1]=1; -
然而有点小的细节需要注意:因为,只有一个人的时候方案只有 1 种,可是我们这里却有 2 种方案,所以我们得默认 1 个人的时候,是从左边进来,于是我们就有了正确的边界条件:
for(int i=1;i<=n;i++)f[i][i][0]=1;
参考程序
The Code for T1:
//p1220
#include<iostream>
using namespace std;
const int N=1e2+5,INF=0x3f3f3f3f;
int dp[N][N][2],loc[N],p[N],n,c;
inline int cal(int i,int j,int l,int r) { return (loc[j]-loc[i])*(p[l]+p[n]-p[r]); }
//这个计算的方法其实画一个草图就出来了,前缀和数组注意边界情况
int main()
{
cin>>n>>c;
for(int i=1;i<=n;i++)
{
int a,b;
cin>>a>>b;
loc[i]=a,p[i]=p[i-1]+b;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
dp[i][j][1]=dp[i][j][0]=INF;
dp[c][c][0]=dp[c][c][1]=0;
for(int len=2;len<=n;len++)
for(int i=1,j=i+len-1;j<=n;i++,j++)
{
dp[i][j][0]=min(dp[i+1][j][0]+cal(i,i+1,i,j),dp[i+1][j][1]+cal(i,j,i,j));
dp[i][j][1]=min(dp[i][j-1][0]+cal(i,j,i-1,j-1),dp[i][j-1][1]+cal(j-1,j,i-1,j-1));
}
cout<<min(dp[1][n][0],dp[1][n][1])<<endl;
return 0;
}
The Code for T2:
//P3205
#include<iostream>
using namespace std;
const int N=2e3+5,mod=19650827;
int dp[N][N][2],a[N];
int main()
{
int n; cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) dp[i][i][0]=1;
for(int len=1;len<=n;len++)
for(int i=1,j=i+len;j<=n;i++,j++)
{
if(a[i]<a[i+1]) dp[i][j][0]+=dp[i+1][j][0];
if(a[i]<a[j]) dp[i][j][0]+=dp[i+1][j][1];
if(a[j]>a[i]) dp[i][j][1]+=dp[i][j-1][0];
if(a[j]>a[j-1]) dp[i][j][1]+=dp[i][j-1][1];
dp[i][j][0]%=mod;
dp[i][j][1]%=mod;
}
cout<<(dp[1][n][0]+dp[1][n][1])%mod;
return 0;
}
细节研讨
细节还好不多,秩序注意边界和初始化问题就可以了,这里也就不再赘述。
重点是下面的总结归纳。
总结归纳
两道题其实很像。这也是笔者将这两道题合并为一个题解的原因之一。
像在何处?
- 状态定义很像;
- 状态转移很像;
- 边界条件很像。
为什么会像?
-
都是区间DP问题;
-
都具有信息多的特性,导致了需要多加维度维护所求;
-
转移的都具有分类讨论的特性,也就是说,这里的转移其实是非常典型的,从区间的两个端点转移而来。这应该形成一种思维习惯。
这种区间DP的问题是很典型的,今后再碰到需要反应过来,难点也就是这个想法和状态的转移了。

浙公网安备 33010602011771号