转载+删改:算法讲解之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;
}

原文地址:https://www.cnblogs.com/pfypfy/p/8728950.html

时间: 2024-07-30 01:35:44

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

区间dp与环形dp

区间dp 常见题型 求区间[1,n]XXXXX后的最大/小值,一般分为无要求或只能/最多分成m段两类 做法 如对分段无要求,设dp[i][j]表示序列中[i,j]的最值,最外层循环区间长度,第二层循环左端点,并能确定右端点,第三层枚举断点: for(rint len = 1;len <= n; ++len) {//如果对len == 1初始化了可从2枚举 for(rint i = 1,j = i + len - 1;j <= n;++i,++j) { for(rint k = i;k <

动态规划——DP算法(Dynamic Programing)

一.斐波那契数列(递归VS动态规划) 1.斐波那契数列——递归实现(python语言)——自顶向下 递归调用是非常耗费内存的,程序虽然简洁可是算法复杂度为O(2^n),当n很大时,程序运行很慢,甚至内存爆满. 1 def fib(n): 2 #终止条件,也就是递归出口 3 if n == 0 or n == 1: 4 return 1 5 else: 6 #递归条件 7 return (fib(n-1) + fib(n - 2)) 2.斐波那契数列——动态规划实现(python语言)——自底向上

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

转自http://www.cnblogs.com/uncle-lu/p/5876729.html 全网最详细tarjan算法讲解,我不敢说别的.反正其他tarjan算法讲解,我看了半天才看懂.我写的这个,读完一遍,发现原来tarjan这么简单! tarjan算法,一个关于 图的联通性的神奇算法.基于DFS(迪法师)算法,深度优先搜索一张有向图.!注意!是有向图.根据树,堆栈,打标记等种种神(che)奇(dan)方法来完成剖析一个图的工作.而图的联通性,就是任督二脉通不通..的问题.了解tarja

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

全网最!详!细!tarjan算法讲解. 全网最详细tarjan算法讲解,我不敢说别的.反正其他tarjan算法讲解,我看了半天才看懂.我写的这个,读完一遍,发现原来tarjan这么简单! tarjan算法,一个关于 图的联通性的神奇算法.基于DFS(迪法师)算法,深度优先搜索一张有向图.!注意!是有向图.根据树,堆栈,打标记等种种神(che)奇(dan)方法来完成剖析一个图的工作.而图的联通性,就是任督二脉通不通..的问题.了解tarjan算法之前你需要知道:强连通,强连通图,强连通分量,解答树

算法讲解:二分图匹配

算法讲解:二分图匹配 二分图匹配,自然要先从定义入手,那么二分图是什么呢? 二分图: 二分图又称作二部图,是图论中的一种特殊模型. 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图. 简单的说,一个图被分成了两部分,相同的部分没有边,那这个图就是二分图,二分图是特殊的图. 匹配: 给定一个二分图G,在G的一个子图M中,M的边集{E}中的任意两

通俗易懂--逻辑回归算法讲解(算法+案例)

1.逻辑回归(Logistic Regression) GitHub地址(案例代码加数据) 1.1逻辑回归与线性回归的关系 逻辑回归是用来做分类算法的,大家都熟悉线性回归,一般形式是Y=aX+b,y的取值范围是[-∞, +∞],有这么多取值,怎么进行分类呢?不用担心,伟大的数学家已经为我们找到了一个方法. 首先我们先来看一个函数,这个函数叫做Sigmoid函数: 函数中t无论取什么值,其结果都在[0,-1]的区间内,回想一下,一个分类问题就有两种答案,一种是"是",一种是"否

【转】聚类分析经典算法讲解及实现

本文将系统的讲解数据挖掘领域的经典聚类算法,并给予代码实现示例.虽然当下已有很多平台都集成了数据挖掘领域的经典算法模块,但笔者认为要深入理解算法的核心,剖析算法的执行过程,那么通过代码的实现及运行结果来进行算法的验证,这样的过程是很有必要的.因此本文,将有助于读者对经典聚类算法的深入学习与理解. 4 评论 杨 翔宇, 资深软件工程师, IBM 段 伟玮, 在读博士, IBM 2016 年 7 月 18 日 内容 在 IBM Bluemix 云平台上开发并部署您的下一个应用. 开始您的试用 前言

BZOJ 1901: Zju2112 Dynamic Rankings 区间k大 带修改 在线 线段树套平衡树

之前写线段树套splay数组版..写了6.2k..然后弃疗了.现在发现还是很水的..嘎嘎.. zju过不了,超时. upd:才发现zju是多组数据..TLE一版才发现.然后改了,MLE...手写内存池..尼玛终于过了..附zju2112代码于后. bzoj倒是过了,1A的感觉还是很爽的..可是时间不好看..这就是所谓\(O(nlog^3n)\)的复杂度的可怜之处么? 写挂的地方: insert一定要是传地址指针进去. delete时先把地址指针delete掉,最后把是地址指针指向左儿子or右儿子

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

全网最详细tarjan算法讲解,我不敢说别的.反正其他tarjan算法讲解,我看了半天才看懂.我写的这个,读完一遍,发现原来tarjan这么简单! tarjan算法,一个关于 图的联通性的神奇算法.基于DFS(迪法师)算法,深度优先搜索一张有向图.!注意!是有向图.根据树,堆栈,打标记等种种神(che)奇(dan)方法来完成剖析一个图的工作.而图的联通性,就是任督二脉通不通..的问题.了解tarjan算法之前你需要知道:强连通,强连通图,强连通分量,解答树(解答树只是一种形式.了解即可)不知道怎