石子合并 -跟着思路coding【暴搜 + 记忆化搜索 + 区间DP】

51 篇文章 0 订阅
22 篇文章 0 订阅

😊😊 😊😊
不求点赞,只求耐心看完,指出您的疑惑和写的不好的地方,谢谢您。本人会及时更正感谢。希望看完后能帮助您理解算法的本质
😊😊 😊😊

题目描述:

小白到进阶各种解法:

一、暴搜:😊

在这里插入图片描述

思路:

首先想一下目标答案的所有子集,因为每次只能合并相邻两堆,那么目标区间 [ i , j ] [i, j] [i,j]必然是由相邻的两堆合并得来的,即存在一个分割点,将整体划分成两堆:
在这里插入图片描述
如图所示的两堆,那么由于中间的 k k k,在区间 [1,n]上有 n-1种取值,而不能取到n种,是因为缺少的那一种就是 n,若 k == n,请问你的区间还能分成相邻两段吗?显然不能。可知合并 [1,n]的区间的子集有:{[1,2], [3,n]}, {[1, 3], [4,n]}…多种。所以我们可以采用暴搜的方式去枚举每种划分方式带来的最小代价。
补充这里的划分方式:其实就是分割点的取值,落在不同的地方,即不同的,就会诞生两个不同的相邻的区间。依次递推下去。因为只有叶子区间才有答案,所以递归到叶子后才开始回溯答案,往上更新!
在这里插入图片描述

  1. 递归的出口:
    合并两堆石子才需要付出代价,所以当递归到只有一堆石子的时候,说明只有自己一堆。合并无需代价:
if (i==j) return 0;
  1. 递归的参数:
    由递归的出口可以知道,这里应该是区间的左右端点,当左端点 == 右端点的时候,说明区间里只有一堆石子。
dfs(int i, int j)
  1. 递归的计算:想想怎么计算哦?
    对于一个大的区间来说,划分为两个子区间后,应该想想子区间怎么计算?由递归的出口可以知道,叶子区间才有答案,所以还要继续划分下去。接下来就是划分的方式枚举了,因为有多种划分方式,所以说,我们需要逐一去枚举所有的划分方式,而划分方式就 k k k 的落脚点,所以说,只需要让 循环 k k k l − ( r − 1 ) l-(r-1) l(r1) 即可!

res记录的是合并当前的区间所需要的最小代价!即当前状态到目标状态的答案!

nt res=1e8;
    for (int k=l; k < r; k ++)  //枚举所有的划分方式!
    {
        res = min(res, dfs(l, k) + dfs(k+1, r) + s[r] - s[l-1]);
    }
    
    return res;

代码:

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

using namespace std;
const int N = 3e2 + 10;
int s[N];
int f[N][N];

int dfs(int l, int r)
{
    if (l == r) return 0;
    int res=1e8;
    for (int k=l; k < r; k ++)  //枚举所有的划分方式!
    {
        res = min(res, dfs(l, k) + dfs(k+1, r) + s[r] - s[l-1]);
    }
    
    return res;
}

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

    for (int i = 1; i <= n; i ++) {
        cin >> s[i];
        s[i] += s[i - 1];
    }
    
    cout << dfs(1, n) << endl;
    return 0;
}

在这里插入图片描述

二、记忆化搜索:先看 [法三、区间DP] 后看它!😊

在这里插入图片描述

思路:回溯性记忆化搜索。

是从目标区间出发,递归到叶子区间后,回溯记录答案,更新区间。因为只有叶子区间才有答案!
目标区间递归搜索到叶子区间。从而通过递归进行区间的划分!
在这里插入图片描述

代码:

#include<iostream>
using namespace std;
const int N = 3e2 + 10;
int s[N];
int f[N][N];

int dfs(int l, int r)
{
	if (l == r) 	//此时已经递归到叶子区间,那么直接返回当前值即可!
		return 0;
	if (f[l][r] != -1)	//表示该区间的最小花费已经计算过了,无需重复,直接返回答案。
		return f[l][r];
	//否则的话就是没有计算 [l, r]区间的最小花费,因为有多种划分方式,每种划分方式
	//都是一个分支,所以递归枚举每个分支!
	//并且
	f[l][r] = 1e8;
	for (int k=l; k < r; k ++)
		f[l][r] = min(f[l][r], dfs(l,k)+dfs(k+1,r)+s[r]-s[l-1]);枚举所有的划分方式,选取代价最小的那种。
	return f[l][r];
}

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

    for (int i = 1; i <= n; i ++) {
        cin >> s[i];
        s[i] += s[i - 1];
    }
    memset (f, -1, sizeof(f));
    cout << dfs(1, n) << endl;
    return 0;
}

在这里插入图片描述

三、本题考察算法:区间DP😊

思路:

区间DP的模板:
区间长度,区间起点,区间终点,长度为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;
		}
		//枚举区间长度为len的情况下,分割点在哪里比较好!
		for (int k=i; k < j; k ++)	
			dp[i][j] = max(dp[i][j], dp[i][k] + dp[k+1][j]);
	}
}

根据模板代码来解释本题的思路:

  1. 枚举区间的长度,从子问题推导原问题,原问题的区间长度是 n n n,而子问题的区间长度为 1 , 2 , 3... n − 1 ) 1,2,3...n-1) 1,2,3...n1),为什么这样一定会推导到答案了?
    因为长度为 n n n 的区间也必然是由相邻两堆合并来的,那么这两堆的区间长度必然是小于等于 n n n 的:
    在这里插入图片描述
    如上图所示,在合并成区间长度为 n n n 之前,有那么多种子集去合并成长度为 n n n 的区间,但是我们并不知道合并的两个子区间的最小代价,所以说我们要从子问题 ⇒ 原问题!比如子区间 [1, 2],你知道合并这两堆的代价是多少吗?[3,4], [5, 6] 呢?都不知道,所以说只有从区间长度为2的开始去递推,逐步扩大区间的长度!

  2. k的作用,同样借用上面的图。
    假设此时区间长度为3,区间为:[1, 2, 3],目标是求出合并三堆石子的最小代价。你知道先合并哪两堆吗?不知道。你会怎么做?
    第一步先划分为两堆:
    [1,1] ,[2,3]。
    [1,2],[3, 3]。
    第二步再将两堆合并:
    合并:[1,1] + [2,3] == [1,3];
    合并:[1,2] + [3, 3] == [1,3];
    那么k的作用也就显现出来:k是枚举分割点的,看分为两堆的话,应该由哪两堆合并而来!

合并从 i  到 j之间的石子
取max的原因是,f[i][j]存在多种合并方式,从所有合并方式中挑选代价最小的方式!d
f[i][j] = min(f[i][j], f[i][k] +  f[k+1][j] + w[i][j]);

代码:

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

using namespace std;
const int N = 3e2 + 10;
int s[N];   //前缀和数组:表示合并 i~j堆的代价!
int f[N][N];    //所有将第 i 堆石子到第j堆石子合并成一堆石子的合并方式!

int main()
{
    int n;
    cin >> n;
    for (int i=1; i <= n; i ++)
    {
        cin >> s[i];
        s[i] += s[i-1];
    }
    
    memset (f, 0x3f, sizeof (f));
    for (int len = 1; len <= n; len ++)
    {
        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; k ++) //实际上就是在枚举长度为len的区间的合并方式了!
            {
                f[i][j] = min(f[i][j], f[i][k] + f[k+1][j] + s[j] - s[i-1]);
            }
        }
    }
    
    cout << f[1][n];

    return 0;
}

在这里插入图片描述

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值