转载+删改:算法讲解之Dynamic Programing —— 区间DP [变形:环形DP]

Posted pfypfy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了转载+删改:算法讲解之Dynamic Programing —— 区间DP [变形:环形DP]相关的知识,希望对你有一定的参考价值。

发现一篇好文,可惜发现有一些地方有排版问题。于是改了一下,并加了一些自己的内容。

原文链接

对区间DP和其变式环形DP的总结。

首先先来例题。

石子归并

题目描述 Description

有n堆石子排成一列,每堆石子有一个重量w[i], 每次合并可以合并相邻的两堆石子,一次合并的代价为两堆石子的重量和w[i]+w[i+1]。问安排怎样的合并顺序,能够使得总合并代价达到最小。

输入描述 Input Description

第一行一个整数n(n<=100)

第二行n个整数w1,w2...wn (wi <= 100)

输出描述 Output Description

一个整数表示最小合并代价

样例输入 Sample Input

4
4 1 1 4

样例输出 Sample Output

18

区间DP的模型很显然,特征就是所给的数据是一条链或者一条环(环形DP)。

划分问题:

我们所求区间\([1,n]\)的最小合并代价,而\(dp[1][n]\)必然是有两段区间合并而成\(dp[1][n] = {dp[1][k]+dp[k+1][n]+sum[l][r]}\)

重写DP方程:\(dp[1][n] = {dp[1][k]+dp[k+1][n] + sum[n]-sum[0]}(1\le k<n)\)

因为我们不知道\([1,n]\)最终是由哪段合并而来的,所以我们必然要枚举断点\(k\).

我们发现,在计算\([1,n]\)时我们需要已经计算完成了的\([1,k],[k+1,n]\)

所以我们在枚举到\(n\)之前必须先把其前面的区间计算完成。然后再从n开始反向枚举左端点,比如计算\([1,5]\)区间且断点枚举到3的时候,我们必须已经计算出区间\([1,3]\)和区间\([4,5]\)。区间[1,3]好办,因为我们的r是顺着枚举的,区间\([4,5]\)要计算在前面也好办,内层循环反着枚举。(看代码)

我们归纳出一般性的方程\(dp[l][r] = {dp[l][k]+dp[k+1][r]+sum[r]-sum[l-1]}(l\le k\le r)\)

状态定义\(dp[l][r]\):区间\([l,r]\)合并的最小代价,(状态必须是二维,因为我们要表示一个区间,区间的特征是由左界/右界来表现)

代码演示如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
int n,w[101];
int f[101][101];//f[i][j]表示区间[i,j]合并的最小代价
int sum[101] ;//预处理i→j的合并代价 (降低算法复杂度)

inline void dp()
{
    for(int j=2; j<=n; j++)
        for(int i=j-1; i>=1; i--)
        {
            f[i][j]=1e7;//定义一个极大值
            for(int k=i; k<j; k++)
                f[i][j] = min(f[i][j],f[i][k] + f[k + 1][j] + sum[j] - sum[i - 1]);
//状态转移方程 f[i][j] = min{f[i][k] + f[k + 1][j] + sum[j] - sum[i - 1]} | i <= k < j
        }
}

int main()
{
    scanf("%d",&n);
    for(int i=1; i<=n; i++)
    {
        scanf("%d",&w[i]);
        sum[i]=sum[i-1]+w[i];
    }
    dp();
    printf("%d",f[1][n]);
    return 0;
}

当然,我们还有另一种写法。描述一个区间的状态,我们也可以用左端点+长度来表示。当然状态定义还是不变(因为要记录的时候\(dp[l][l+len]\)等价于\(dp[l][r]\)),只是枚举的方式要改变。这里就不写全,只给出核心部分。

下面代码的执行逻辑和上面的有点区别,先算出任意相邻两段的合并代价,然后算任意相邻3段的合并代价。

因为我们在算3段的时候,例如\([1,3]\)我们已经算出了\([1,2]\),\([2,3]\)的合并代价,所以可以直接递推。

for(int len = 2; len <= n; len++) //长度
    for(int l = 1; l+len-1<=n; l++) //左端点
    {
        int r = l+len-1;
        for(int k=l; k<r; k++) //断点
            dp[l][r] = max(dp[l][r],dp[l][k]+dp[k+1][r]+sum[r]-sum[l-1]);
    }

答案是\(dp[1][n]\)

区间DP的写法一般就这两种形式,选一种你喜欢的方式记下即可。状态定义一般也是二维[l,r]

好,然后我们再来看下一道例题

[NOI1995]石子合并

题目描述

在一个圆形操场的四周摆放N堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。

试设计出1个算法,计算出将N堆石子合并成1堆的最小得分和最大得分.

输入输出格式

输入格式:

数据的第1行试正整数N,1≤N≤100,表示有N堆石子.第2行有N个数,分别表示每堆石子的个数.

输出格式:

输出共2行,第1行为最小得分,第2行为最大得分.

输入输出样例

输入样例#1:

4
4 5 9 4

输出样例#1:

43
54

数据由链变成了环,难道我们就没法处理了?

这里有一种技巧叫做 “断环为链”

将链复制一倍 a[i+n] = a[i]

然后我们控制枚举的长度n即可。看代码实现。

最后算答案的时候再枚举一遍起点和固定长度\(n\)\(dp[l][l+n-1](1\le l\le n)\)

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>

using namespace std;

int n,w[101];
int f[201][201];//f[i][j]表示区间[i,j]合并的最小代价
int g[201][201];//f[i][j]表示区间[i,j]合并的最大代价
int sum[201] ;//预处理i→j的合并代价 (降低算法复杂度)
int ans_min=1e7;
int ans_max=0;

inline void dp()
{
    for(int j=2; j<=n+n; j++)
        for(int i=j-1; i>=1&&j-i<n; i--)
        {
            f[i][j]=1e7;//定义一个极大值
            for(int k=i; k<j; k++)
            {
                f[i][j] = min(f[i][j],f[i][k] + f[k + 1][j] + sum[j] - sum[i - 1]);
//状态转移方程 f[i][j] = min{f[i][k] + f[k + 1][j] + sum[j] - sum[i - 1]} | i <= k < j
                g[i][j] = max(g[i][j],g[i][k] + g[k + 1][j] + sum[j] - sum[i - 1]);
            }
            if(ans_max<g[i][j])ans_max=g[i][j];
        }
}

int main()
{
    scanf("%d",&n);
    for(int i=1; i<=n; i++)
    {
        scanf("%d",&w[i]);
        w[i+n]=w[i];
    }
    for(int i=1; i<=2*n; i++)
    {
        sum[i]=sum[i-1]+w[i];
    }
    dp();
    for(int i=1; i<=n; i++)
    {
        if(ans_min>f[i][i+n-1])ans_min=f[i][i+n-1];
    }
    printf("%d\n",ans_min);
    printf("%d\n",ans_max);
    return 0;
}

上面代码是用第一种方式写的,你也很容易用第二种(左端点+长度)的方式写出来。这里就不给出代码了。

啊,顺手就写了一份,好吧,还是把代码贴出来吧。不过希望大家认真思考,DP的题看到代码和方程可能很容易懂,但难度在于想到代码和方程,或者是定义状态。初学者可以先学习,过一段时间后再来做。复习的人就不要看代码啦。

#include<cstdio>
#include<iostream>
#include<cmath>
#include<algorithm>
#include<cstring>

using namespace std;
int n;
int w[220],sum[220];
int f[220][220];
int g[220][220];

int main()
{
    scanf("%d",&n);
    for(int i=1; i<=n; i++)
    {
        scanf("%d",&w[i]);
        w[i+n] = w[i];
    }
    for(int i=1; i<=2*n; i++)
        sum[i]=w[i]+sum[i-1];
    for(int len = 2; len <= n; len++)
    {
        for(int l=1; l<=2*n-len+1; l++) //这里要注意
        {
            int r = l+len-1;
            f[l][r]=1e9;
            for(int k=l; k<r; k++)
            {
                f[l][r] = min(f[l][r],f[l][k]+f[k+1][r]+sum[r]-sum[l-1]);
                g[l][r] = max(g[l][r],g[l][k]+g[k+1][r]+sum[r]-sum[l-1]);
            }
        }
    }
    int ans_max = 0,ans_min = 1e9;
    for(int i=1; i<=n; i++)
    {
        ans_min = min(ans_min,f[i][i+n-1]);
        ans_max = max(ans_max,g[i][i+n-1]);
    }
    cout<<ans_min<<endl;
    cout<<ans_max;
    return 0;
}

以上是关于转载+删改:算法讲解之Dynamic Programing —— 区间DP [变形:环形DP]的主要内容,如果未能解决你的问题,请参考以下文章

Dynamic对实体进行增删改查

数据挖掘分类之Naïve Bayes(转载)

全网最!详!细!tarjan算法讲解。——转载自没有后路的路

转载:一文讲解图像插值算法原理

转载全网最!详!细!tarjan算法讲解。

转载:EM算法的最精辟讲解