优化原理: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 之了
不得不吐槽一下,写这个东西真好慢的
较上篇题解,版面有所改进
欢迎各位大佬批评指正,溜溜球了先

浙公网安备 33010602011771号