优化原理:Luogu P1775 石子合并

题目链接:P1775 石子合并

  这道题将围绕 dfs -> 记忆化 -> DAG图 -> 二维DP(区间DP 这个叫法可能不太准确)这几个大的方面进行讲解。

dfs

 至于dfs,我们大致分以下三个方面走

  • 第一:确定状态参数
      至于参数的确定,一定要合理(废话嘛这不是),怎么着也得方便下一步,往下怎么走,对吧,
    题目中是要求将这个区间的石子合并成一堆所要花费的最小代价,那么,根据经验判断(咳,不知道该怎么扯了)
    我们肯定是要确定一个区间的,至于区间的话,我们只用一个参数肯定是不行的,我们需要左端点和右端点,也就是
    两个参数,l 和 r ,我们设 dfs(l, r) 为合并 从 l 到 r 这一区间内石子所要的最小代价

  • 第二:确定转移方向(接下来往哪儿走 —— 将大问题细分)
      接下来就是转移方向,往哪里走,就是将这个大问题,细分成有限个小问题,至于多有限,这道题的话
    看样子是主要取决于区间长度喽。
      如果我们要求 dfs(l, r),就要给 dfs(l, r),进行细分,我们设一个中间变量 k,并且 l ≤ k < r
    那么 dfs(l, r) = dfs(l, k) + dfs(k + 1, r) + sum(l, r),也就是说,如果我们要求 合并(l, r) 区间内的石子代价
    等于求 合并(l, k)区间内的石子代价 加上 合并(k + 1, r) 区间内的石子代价,再加上此次合并需要花费的代价 sun(l, r)
    其中 sum(l, r) = (a[l] + a[l + 1] + ··· ··· + a[r - 1] + a[r]) ,这个操作,可以用前缀和进行优化,
    由于要求最小代价,所以 dfs(l, r) = min{dfs(l, k) + dfs(k + 1, r)} + sum(l, r) 其中 l ≤ k < r

  • 第三:结束条件
      至于结束条件,我们看上一步,最终结束的时候,就是将 dfs(l, r) 细分到不能再细分,那么,什么时候不能再细分了呢?
    我们上一步不是设置了 k 这个中间变量嘛,而且 k 的取值范围是 l ≤ k < r,我们设置 k 就是为了将大问题细分成小问题,那么,
    这个 k 肯定是能取到具体的值的,那么,当 k 取不到具体的值的时候,这也就意味着,这个问题不能再往下被继续细分了
      什么时候 k 取不到具体值呢,就是当 l == r 时,当 l == r 时,那么 l ≤ k < r --> l ≤ k < l
    不可能 k 比同一个数大于等于,还比同一个数小于对吧,这也就意味着,当 l == r 时,就是结束条件,当 l == r 时,就是一堆具体的
    石子,不用再进行合并,没有代价消耗,直接返回 0 就好了。

那么,接下来,我么就可以瞬间写出代码(ber,瞬间?):

int n;
int a[N];

int sum(int l, int r){
    int res = 0;
    for (int i = l; i <= r; i ++) res += a[i];

    return res;
}

int dfs(int l, int r){
    if (l == r) return 0;

    int res = 2e9;
    for (int i = l; i < r; i ++)
        res = min(res, dfs(l, i) + dfs(i + 1, r) + sum(l, r));

    return res;
}

交一发,一交一个不吱声~~
  数据范围还是比较大的,直接 dfs 一下,寄 ····· 未完待续

考虑优化,记忆化

  在讲优化之前,我们先搞张图:哦吼
(画技拙劣,轻喷轻喷)
  一颗递归搜索树哈这是,这里我画的是求 (1, 4) 这个区间的,当我们暴力搜索的时候,我们应该意识到一个问题,
就是比如一个区间,我们已经搜索过了,但是在后面的搜索过程中,我们可能还会再搜一次或者更多次这个区间
很显然,这是不必要的,因为当我们搜索的这个区间确定的时候,那么最后得到的结果就是确定的,那么,我们可以
记录一下搜素过的区间的结果,再再次进行搜索这个区间的时候,直接返回结果,而不是再搜一次
  那么接下来,我们直接搞一个数组,进行记忆化一下,至于这个数组是几维的,二维就好哈
关嘴放代码

int sum(int l, int r){
    int res = 0;
    for (int i = l; i <= r; i ++) res += a[i];
    return res;
}

int dfs(int l, int r){
    if (l == r) return 0;
    if (memo[l][r]) return memo[l][r];

    int res = 2e9;
    for (int i = l; i < r; i ++)
        res = min(res, dfs(l, i) + dfs(i + 1, r) + sum(l, r));

    return memo[l][r] = res;
}

  这时候,这个代码已经是可以过掉了的,再后面,是进行转 DP 的过程
   写这玩意儿这么费时间吗,呜呜呜,不管了,干饭去先
    干饭人,干饭魂,干饭人吃饭得用盆

okay,王(gan)者(fan)归来

DAG 图

  正常来搞的化,可能我们还要整个高维 DP,不过此处好像没什么太大必要性,那么,我们直接进入有向无环图(DAG 图)环节,
当然,本题个人认为是不太能直接搞出来 DP,so,我们先搞个 DAG 图看看怎么个事儿,为了方便演示,我们取 n 等于 5,来,上图:

  首先的话,咱先回顾一下第一大环节中,关于 dfs 第三步中的结束条件,是不是 l == r, 对吧,然后我们现在分别将
l --> i   r -- > j,我们可以明确一点,当 l == r 即 i == j 时,dp[i][j] 的这个方格应该是 0 对吧,(我们暂且将 dp[i][j]
认为是代表 (i, j) 这个方格的值),在问题分解的过程中,是不难看出(这个是真不难看出哈)r 是 始终 大于等于 l 的,对应过去就是
j 是始终大于等于 i 的,当 j 小于 i 时,是没有意义的,那么也就是 DAG 图中的下半部分阴影区,没啥子意义,然后我给 划掉了
同时还有第一行,也就是 i == 0 时,也没啥意义,也是给 划掉了,这里除了 i == j 以外,其余都初值为正无穷即可,因为要求最小值
  以上算是我们处理的边界条件,再接下来就是转换关系,我们看图中 (1, 4) 这个格,也就是 dp(1, 4),我们如果将其对应到 dfs(1, 4)
那么 dfs(1, 4) 的计算是不是就依赖于 {(dfs(1, 1) + dfs(2, 4),  dfs(1, 2) + dfs(3, 4),  dfs(1, 3) + dfs(4, 4)} 对吧
然后对应到 DAG 图中就是 {dp[1][1] + dp[2][4],  dp[1][2] + dp[3][4],  dp[1][3] + dp[4][4]},这个我在图中已经标注出来了,当然
最后还要再加上 sum(1, 4) 这一小 tuo
  接下来就是关于计算的先后顺序,根据上面的转换关系分析,我们可以再动手模拟几个,我们不难发现,至于 dp[i][j] 的计算
依赖于其同行的左边的格子   和   同列的下面的格子为了保证在计算 dp[i][j] 时,其所依赖的格子已经被计算过,那么,
我们应该斜着计算,也就是从左上角到右下角,然后计算完成之后,再向右上逐层推进
  经常打代码的小伙伴应该知道,斜着计算,那么会下意识想到直线方程,不经常打代码的小伙伴记得要常打,关于直线方程,这里我们还是做简单介绍
主要还是画一下图哈,数形结合一波:

按照我们关于前面计算顺序介绍的方法,就像是图中斜率一定的直线,不断扫描,然后再向上继续挪动的过程,这里涉及到一点初中数学的之后(正常进度是初中数学卷狗请勿对号入座,谁知道你初中是不是都开始卷微积分了,微距了)这些直线的斜率一定,这些直线的方程都可以以 y = kx + b 来表示,但是不同的就是 b 的取值就拿本题来说,我们发现,处在同一扫描线(就是我们图中画的直线)上时,这些格子 i 和 j 的差值都是固定的,比方说 (1, 2) (2, 3) (3, 4) 这几个格子
他们呢是在同一条扫描线上的,他们的 i 和 j 的差值都是固定为 1
  
简单总结一下:

    为了保证 dp[i][j] 可以正确计算,那么我们应先
以扫描线的形式,将扫描线上的格子进行计算,当这条扫描线上的格子被计算过后

  再斜着向上推进,那么,我们可以通过控制 i 和 j 差值的大小,来控制扫描线的移动通过控制 i 的大小,来控制扫描线上格子计算的顺序
  当然,在同一条扫描线上时,格子的计算顺序是无所谓的,假设 i 和 j 的差值是 d ,我们还有一个对 i 的控制条件,就是 i + d 小于等于 n
  不然的话,就出界了,可能会 RE,然后就是设 k 为中间变量,则此时 i ≤ k < j,然后进行计算,计算关系式就是
    dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum(i, j));
  最后就是初值的设定,初始时 dp 数组全部都赋值为正无穷,然后 dp[i][i] 1 ≤ i ≤ n 初值为 0
   最后结果的输出 dfs(1, n) --> dp[1][n]
接下来是关嘴放代码环节

二维DP

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

using namespace std;

const int N = 310;

int n;
int a[N];
int dp[N][N];

int sum(int l, int r){
    int res = 0;
    for (int i = l; i <= r; i ++) res += a[i];
    return res;
}

int main(){
    cin >> n;

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

    memset(dp, 0x3f, sizeof dp);

    for (int i = 1; i <= n; i ++) dp[i][i] = 0;

    for (int d = 1; d <= n; d ++){
        for (int i = 1; i + d <= n; i ++){
            int j = i + d;
            for (int k = i; k < j; k ++){
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum(i, j));
            }
        }
    }

    cout << dp[1][n] << '\n';

    return 0;
}

那么,现在就是 O 而 K 之了
不得不吐槽一下,写这个东西真好慢的
较上篇题解,版面有所改进
欢迎各位大佬批评指正,溜溜球了先

posted @ 2025-04-26 20:22  Captain_Ceiling  阅读(63)  评论(0)    收藏  举报