浅谈区间类动态规划

\(\mathcal{Before\ Writing}\)

为了加深印象,写下这则学习笔记 .

若文中有错误之处还请指出,感激不尽 .

\(\mathcal{PS:}\) 菜鸡笔者水平有限,没写好不要喷我哦qwq

\(\mathcal{Come\ into\ subject}\)

我们先从一个问题入手。

\(\mathcal{Problem\ Link\ :P1090}\)

题解就是将 \(n\) 堆果子任意两堆合并最终合并成一堆所需要的最小的体力耗费值。很容易想到贪心的方法,即每次合并耗费最小的两堆,可以通过维护一个小根堆实现。这里就不给出代码了,请读者自己实现。

接下来,考虑如果把题目改为每次只能合并相邻的两堆(保证果子都在一条线上,即第1堆和第 \(n\) 堆不算相邻)那么很明显贪心的思路是行不通的。因此需要换一种算法。

\(\mathcal{Solution}\)

我们可以这么想,无论怎么合并这些果子堆,最终都是要合并成一堆的。

举个例子:假设有5堆果子(编号从1到5)需要合并,那么当她们最终合并成一堆时,一定是由 ①--②③④⑤ | ①②--③④⑤ | ①②③--④⑤ | ①②③④--⑤ 这四种合并方法中的某一种合并得到的( \(a\) -- \(b\) 表示 \(a\)\(b\) 合并)。

也就是我们只需要知道最后一堆是由哪两堆合并而来的,问题也就迎刃而解了。而对于最终解需要最优,也就是合并而来的两堆也要是最优的,这就符合了动态规划最优子结构的性质,而且两堆合并的情况也是独立唯一的,不存在后效性,因此我们可以考虑用动态规划来解决这个问题。

约定\(H(a,b)\) 表示将编号从 \(a\)\(b\)所有堆合并后得到的一个堆

描述状态

\(dp[i][j]\) 表示将编号从 \(i\)\(j\) 的所有果子堆合并能得到的最小体力耗费值(也就是 \(H(i,j)\) 的最小值)。因为 \(H(i,j)\) 是由 \(H(i,k)\)\(H(k+1,j)\) 合并得到的,即上述例子中
\(H(1,5)=min\begin{cases}H(1,1)+H(2,5)\\H(1,2)+H(3,5)\\H(1,3)+H(4,5)\\H(1,4)+H(5,5)\end{cases}\)

由此我们可以得到状态转移方程

\(dp[i][j]=min\{\ dp[i][k]+dp[k+1][j]\ \}+cost(i,j)\) \(\ \ \ \ \ (1\leq i \leq j \leq n,i\leq k \leq j-1)\)

所以我们去枚举 \(i,j,k\) 就好了,最终的解即为 \(dp[1][n]\) 。也就是:

初始化 dp[i][i]=0;

for(int i=1;i<=N;++i)
  for(int j=i;j<=N;++j){
    for(int k=i;k<=j-1;++k)
      dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+cost(i,j));
    /* dp[i][j]+=cost(i,j);  状态转移方程中的 +cost(...) 操作也可以放到循环外面来做,对于此题没有影响 
       cost可以是一个计算花费的函数,也可以是一个数组,具体看题目而定,而在本题中表示 H(i,j) 的值,可以使用
       前缀和,即将函数cost(i,j)用来计算sum[j]-sum[i-1] 
    */
}

Res=dp[1][N];
    

通常 \(dp\) 数组的第一维是阶段,第二维是状态。但如果你对动态规划的概念非常清楚,那么就会发现这么做其实是有问题的,因为这里的 \(i\) 并不能作为阶段

还是拿之前的例子来说,对于动态规划的最优子结构性质,我们要求当做到 \(dp[1][5]\) 时,她的子问题 \(dp[1][1],\) \(dp[1][2]\) \(...\) \(dp[4][5]\) 必须都是已经确定且最优的,但放入我们写的循环里看,做到 \(dp[1][5]\) 时,需要用到的 \(dp[2][3]\) \(,dp[2][4]...\) \(dp[4][5]\) 全都还没有确定。

你也可以这么想,对于上面的状态转移方程的变量范围 \(\ 1\leq i \leq j \leq n\ ,\ i\leq k \leq j-1\ \ \\)
很明显,结合转移方程来看,\(dp[k+1][j]\) 这个数组的阶段 \(k+1\) 是会大于 \(i\) 这个阶段的,也就是阶段 \(i\) 的所有状态还没确定好,就用到了后面等待确定的阶段 \(k+1\) 中的状态,这显然是错误的。

下面我们就要来考虑到底以什么来作为阶段了。

我们可以发现,对于 \(H(1,5)\) ,她是由5个堆合并得到的,而她的子问题 \(H(1,2),\) \(H(2,4)\) 等,都是属于由2~4个的堆合并得到的,即要确定由 \(n\) 个堆合并而成的 \(H(a,a+n-1)\) 时,我们要做的是确定由2至 \(n-1\) 个堆合并而成的 \(H(a,a+k-1)\) 。因为 \(k\) 一定小于 \(n\) ,所以我们可以将合并的堆数作为阶段。发现合并的堆数知道了,只要知道合并操作的起始堆的位置,就能够算出终止堆的位置。

下面给出正确的伪代码

初始化 dp[i][i]=0; 

for(int L=2;L<=N;++L) //这里合并的堆数从2开始,也就是最少两个堆合并
  for(int i=1;i+L-1<=N;++i){  //枚举起点
    int j=i+L-1;  //由起点计算出终点
    for(int k=i;k<=j-1;++k)
      dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+cost(i,j));
}

Res=dp[1][N];

至此,问题已经得到解决。

\(end\)

上述代码就是区间类动态规划的思想,基本套路就是1.枚举区间长度 2.枚举起点,算出终点 3.枚举过渡点 4.转移状态

但是现在广大毒瘤出题人当然不会给你太裸的模型来做,所以他们会在一些 描述状态、转移状态、或者 在进行 \(dp\) 前的处理 等方面做文章。其中环形区间动规最为经典,本文就以此为例进行分析。

我们可以对之前的题目再次进行修改:将 \(n\) 堆果子围成一圈,每次只能合并相邻的两堆,其他约定与原题一致,问最少的体力耗费值。

\(\mathcal{Solution}\)

我们来看一张图

可以看出,环状分布解法其实就相当于从环上取一条长为 \(n\) 的链,而我们做前面题目的解 \(dp[1][N]\) 只是在这个环上取链的其中一种情况。

较朴素的做法就是在原先的循环最外层再套一层循环,枚举链的起点,还请读者自行尝试。这里主要介绍一种环形区间动规的经典解法。

割环

显然,既然是环,我们可以考虑在环上“切”一刀,使其变成一条链。这样由环转链,是解决环形区间动规较常用的一种方法,对于图示中的环,我们就可以将其表示为: 123451234 也就是将长度为 \(n\) 的环变成了长度为 \(2n-1\) 的链。那么最优解就在 12345 23451 34512 45123 51234 中。

你会发现,接下来的操作便和之前的写法完全一样了

伪代码如下

初始化 dp[i][i]=0;

for(int i=1;i<=N;++i) a[i]=a[i+N]=read();  由环转链 

for(int L=2;L<=N;++L)
  for(int i=1;i+L-1<=2*N-1;++i){
    int j=i+L-1;
    for(int k=i;k<=j-1;++k)
      dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+cost(...));
}

Res=min{dp[i][i+N-1]}  1<=i<=N

理解了之后,就可以尝试做这两道环形区间动规的经典题目。(如果还有例题可以提出来哦)

\(\mathcal{Problem\ Link : P1880}\)

\(\mathcal{Problem\ Link : P1063}\)

\(end\)

\(\mathcal{After\ Writing}\)

最后,如果你认真看了这篇文章,或多或少一定会有点收获吧(dalao请忽略qwq)。如果你还有什么问题请发在讨论区或者私信我,我会不定时解疑的。

另外笔者文化课水平欠佳,有些抽象的意思实在不能解释的很清楚,还请你们自行画图列表帮助理解哦。

\({End}\)

猜你喜欢

转载自www.cnblogs.com/SuYii/p/10988769.html
今日推荐