AcWing
  • 首页
  • 活动
  • 题库
  • 竞赛
  • 校园
  • 应用
  • 文章
    • 题解
    • 分享
    • 问答
  • 吐槽
  • 登录/注册

AcWing 282. 石子合并(区间 DP 模版题详解分析)    原题链接    简单

作者: 作者的头像   白马金羁侠少年 ,  2020-05-31 08:39:14 ,  所有人可见 ,  阅读 11052


180


142

原题链接:AcWing 282. 石子合并

题意:合并 N 堆石子,每次只能合并相邻的两堆石子,求最小代价

解题思路:

关键点:最后一次合并一定是左边连续的一部分和右边连续的一部分进行合并

状态表示:$f[i][j]$ 表示将 $i$ 到 $j$ 这一段石子合并成一堆的方案的集合,属性 Min

状态计算:
(1) $i < j$ 时,$f[i][j] = \min\limits_{i\leq k \leq {j - 1}}{f[i][k]+f[k+1][j] + s[j] -s[i - 1]}$
(2)$i = j$ 时, $f[i][i] = 0$ (合并一堆石子代价为 0)

问题答案: $f[1][n]$

区间 DP 常用模版

所有的区间dp问题枚举时,第一维通常是枚举区间长度,并且一般 len = 1 时用来初始化,枚举从 len = 2 开始;第二维枚举起点 i (右端点 j 自动获得,j = i + len - 1)

模板代码如下:

for (int len = 1; len <= n; len++) {         // 区间长度
    for (int i = 1; i + len - 1 <= n; i++) { // 枚举起点
        int j = i + len - 1;                 // 区间终点
        if (len == 1) {
            dp[i][j] = 初始值
            continue;
        }

        for (int k = i; k < j; k++) {        // 枚举分割点,构造状态转移方程
            dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + w[i][j]);
        }
    }
}

本题C++代码

#include <iostream>
#include <cstring>

using namespace std;

const int N = 307;

int a[N], s[N];
int f[N][N];

int main() {
    int n;
    cin >> n;

    for (int i = 1; i <= n; i ++) {
        cin >> a[i];
        s[i] += s[i - 1] + a[i];
    }

    memset(f, 0x3f, sizeof f);
    // 区间 DP 枚举套路:长度+左端点 
    for (int len = 1; len <= n; len ++) { // len表示[i, j]的元素个数
        for (int i = 1; i + len - 1 <= n; i ++) {
            int j = i + len - 1; // 自动得到右端点
            if (len == 1) {
                f[i][j] = 0;  // 边界初始化
                continue;
            }

            for (int k = i; k <= j - 1; k ++) { // 必须满足k + 1 <= j
                f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
            }
        }
    }

    cout << f[1][n] << endl;


    return 0;
}

补充1:从下往上倒着枚举状态

除了按长度枚举,也可以倒着枚举,因为只要保证每种状态都被提前计算即可

从下往上倒着枚举,可以保证你算dp[i][j]时,dp[i][k]和dp[k + 1][j]一定是被提前计算好的,而从上往下枚举则无法保证这一点。所以我们采用倒着枚举

图解:

#include <iostream>

using namespace std;

const int N = 307;

int s[N];
int f[N][N];

int main() {
    int n;
    cin >> n;

    for (int i = 1; i <= n; i++) {
        cin >> s[i];
        s[i] += s[i - 1];
    }

    for (int i = n; i >= 1; i--) {
        for (int j = i; j <= n; j++) {
            if (j == i) {
                f[i][j] = 0;
                continue;
            }
            f[i][j] = 1e9;
            for (int k = i; k < j; k++) {
                f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
            }
        }
    }

    cout << f[1][n] << endl;

    return 0;
}

补充2:记忆化搜索

如果学过后面的记忆化搜索,那也可以用下面的代码。虽然时间会比递推稍微慢一丢丢,但是呢他的思路比较好写

#include <iostream>
#include <cstring>

using namespace std;

const int N = 307;

int a[N], s[N];
int f[N][N];

// 记忆化搜索:dp的记忆化递归实现
int dp(int i, int j) {
    if (i == j) return 0; // 判断边界
    int &v = f[i][j];

    if (v != -1) return v;

    v = 1e8;
    for (int k = i; k <= j - 1; k ++)
        v = min(v, dp(i, k) + dp(k + 1, j) + s[j] - s[i - 1]);

    return v;
}

int main() {
    int n;
    cin >> n;

    for (int i = 1; i <= n; i ++) {
        cin >> a[i];
        s[i] += s[i - 1] + a[i];
    }

    memset(f, -1, sizeof f);

    cout << dp(1, n) << endl;


    return 0;
}

61 评论


用户头像
天郁闷   12天前     回复

醍醐灌顶


用户头像
FanXY   24天前     回复

大佬,这里我想引用你的模板记到我笔记里复习可以吗?


用户头像
宾克斯的酒   24天前     回复

我默认索引是从0开始,怎么都写不出来。求救了。为啥每次索引都是从1开始计算的啊?

用户头像
天郁闷   12天前     回复

因为这里要用到前缀和,y总给的前缀和模板的下标就是从1开始的

用户头像
灰人   11天前     回复

DP问题建议一般都从1开始,因为大部分情况都会涉及到i - 1,i 从 0开始的话 i - 1取到 -1 作为数组下标会导致数组越界


用户头像
咏小新   1个月前     回复

看懂了,感谢大佬!


用户头像
Karshey   2个月前     回复

%%%%%%%%%%


用户头像
白日梦想家_6   2个月前     回复

小姐姐,我对状态转移方程s[j]-s[i-1]这个不理解,这个应该怎么理解?感觉状态转移方程好难想啊

用户头像
不会算法的小菜鸡   2个月前     回复

f[i[[k]代表合成[i~k]这个区间的最小代价,f[k+1][j]代表合成[k+1,j]区间的最小代价
f[i][k] + f[k+1][j]代表的是合成[i~k]这一堆石子和合成[k+1~j]这一堆石子代价
s[j]-s[i-1]代表的合并[i~k] [k+1~j] 这两堆石子的代价

用户头像
白日梦想家_6   2个月前    回复了 不会算法的小菜鸡 的评论     回复

好的,谢谢。感觉dp状态转移方程太难想了

用户头像
不会算法的小菜鸡   2个月前    回复了 白日梦想家_6 的评论     回复

感觉只有多做累计经验

用户头像
ゞ满心欢喜つ   1个月前     回复

前缀和的意思

用户头像
守仁   1个月前     回复

这个是前缀和 就是 j到i所有石子的和

用户头像
天郁闷   12天前    回复了 不会算法的小菜鸡 的评论     回复

这个解释爱了呀


用户头像
叶落无痕   2个月前     回复

我想问一下在循环外面memset(f,0x3f,sizeof(f) )和在循环里面f[i][j] = 0x3f3f3f3f,有什么区别,为什么答案会不一样。

用户头像
白马金羁侠少年   2个月前     回复

需要保证初始化时f[i][i] = 0,我更新了题解,你可以再理解下


用户头像
究极米缸   2个月前     回复

第二个i为什么倒序捏?

用户头像
究极米缸   2个月前     回复

正序貌似啥也不是

用户头像
白马金羁侠少年   2个月前     回复

从下往上倒着枚举是可以保证你算dp[i][j]时,dp[i][k]和dp[k + 1][j]一定是被提前计算好的,而从上往下枚举则无法保证这一点。我更新了图解,你可以看一下。

用户头像
究极米缸   2个月前    回复了 白马金羁侠少年 的评论     回复

好的,谢谢


用户头像
如梦凉生   2个月前     回复

大佬们问一下这个状态转移方程里面在 当前循环的是k=i,但是它需要用f[k+1][j]的值,这个值是什么时候计算出来的呢,也就是现在正在计算的是k=i;k+1肯定就大于i了应该还没计算出来过东西所以f[k+1][j]值是0吗
for (int k = i; k <= j - 1; k ++) { // 必须满足k + 1 <= j
f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
}

用户头像
如梦凉生   2个月前     回复

好像明白了,并不是这么看的,应该是先把区间从2开始全部算完了,然后再计算区间为3的,所以在计算长度较大的区间的时候所有更短的长度的区间已经都计算好了,所以是有数值的!


用户头像
果子666   3个月前     回复

我想问一下i≤k≤j−1中为什么是j-1而不是j?

用户头像
白马金羁侠少年   2个月前     回复

因为k是左半段的结尾,[k + 1, j]是右半段,k + 1必须<= j


用户头像
Tsing_Summer   3个月前     回复

我竟然看懂了orz


用户头像
Odyssey_8   4个月前     回复

orz


用户头像
sjzcacm   4个月前     回复

f[i][k] + f[k + 1][j] + s[j] - s[i - 1] 为什么要有是s[j]-s[i-1]

用户头像
Karshey   4个月前     回复

这是两堆石子合并的代价 用前缀和求出

用户头像
T_87   3个月前    回复了 Karshey 的评论     回复

懂了,赞

用户头像
fanbo   3个月前    回复了 Karshey 的评论     回复

感谢


用户头像
D.devil   7个月前     回复

xd,你最上面的代码模板,min笔误成max了

用户头像
0913   6个月前     回复

他写的是大部分的模板,不是这道题的


用户头像
吹萧望江南   8个月前     回复

orz


用户头像
巴韭特   10个月前     回复

java版本

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        int n = scan.nextInt();
        int[] s = new int[n + 1];
        int[][] f = new int[n + 1][n + 1];
        for (int i = 1; i <= n; i++) s[i] = scan.nextInt();
        for (int i = 1; i <= n; i++) s[i] += s[i - 1];
        for (int len = 2; len <= n; len++) {
            for (int i = 1; i + len - 1 <= n; i++) {
                int l = i, r = i + len - 1;
                f[l][r] = Integer.MAX_VALUE;
                for (int k = l; k < r; k++)
                    f[l][r] = Math.min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
            }
        }
        System.out.println(f[1][n]);
    } 
}
用户头像
大树101   9个月前     回复

为什么 y总说的最后一步s[j]-s[i-1]我没明白

用户头像
是我会飞呀   9个月前    回复了 大树101 的评论     回复

这里计算的是i到j的和,当前合并需要消耗的代价

用户头像
sjzcacm   4个月前    回复了 是我会飞呀 的评论     回复

那f的集合代表什么呢

用户头像
bying516   3个月前    回复了 sjzcacm 的评论     回复

第i堆石子到第j堆石子合并为一堆式子所消耗的力气 其中的最小值

用户头像
Rain丶bow   1个月前    回复了 大树101 的评论     回复

这里应该计算的是总值,到最后一次还是有两堆,这一步应该是最后一次合并


用户头像
永远热爱   12个月前     回复

那个记忆化搜索挺有用的


用户头像
Tilbur   2021-04-26 23:08     回复

我想知道 为啥这题在初始化 f 数组的时候,不能遍历所有i,j,让f [ i ][ j ]=INF,之前dp的 f 数组初始化都是这样写的,这次这样写却过不了

用户头像
白马金羁侠少年   2021-04-27 00:04     回复

i=j时是0

用户头像
Tilbur   2021-04-27 12:27    回复了 白马金羁侠少年 的评论     回复

谢谢

用户头像
Laurus_1   6个月前     回复

光头哥牛逼

用户头像
超级伞兵   1个月前    回复了 白马金羁侠少年 的评论     回复

为什么i=j时得是0呢,感觉不会用到这种情况啊


用户头像
没有你哪有我   2021-04-03 19:27     回复

小姐姐,能交个朋友嘛,发现你也喜欢算法诶


用户头像
灵茶气艾福   2021-03-14 00:07     回复

楼主建议加个特判:当n==1 时 直接输出, 题目里虽然没有这个测试点,但是我觉得加上更加严谨,毕竟N是从[1~300] 中取的嘛


你确定删除吗?
1024
x

© 2018-2022 AcWing 版权所有  |  京ICP备17053197号-1
用户协议  |  常见问题  |  联系我们
AcWing
请输入登录信息
更多登录方式: 微信图标 qq图标
请输入绑定的邮箱地址
请输入注册信息