P1880 [NOI1995] 石子合并【暴搜 + 记忆化 + DP】

😊😊 😊😊
详解各种解法,从暴力到终极优化,帮助和我一样的新手快速入门!
😊😊 😊😊

[NOI1995] 石子合并

题目描述

在一个圆形操场的四周摆放 N N N 堆石子,现要将石子有次序地合并成一堆,规定每次只能选相邻的 2 2 2 堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。

试设计出一个算法,计算出将 N N N 堆石子合并成 1 1 1 堆的最小得分和最大得分。

输入格式

数据的第 1 1 1 行是正整数 N N N,表示有 N N N 堆石子。

2 2 2 行有 N N N 个整数,第 i i i 个整数 a i a_i ai 表示第 i i i 堆石子的个数。

输出格式

输出共 2 2 2 行,第 1 1 1 行为最小得分,第 2 2 2 行为最大得分。

样例 #1

样例输入 #1

4
4 5 9 4

样例输出 #1

43
54

提示

1 ≤ N ≤ 100 1\leq N\leq 100 1N100 0 ≤ a i ≤ 20 0\leq a_i\leq 20 0ai20

小白到进阶各种解法:

一、暴搜:😊

在这里插入图片描述

思路:

  1. 合并区间 [ 1 , n ] [1, n] [1,n],每次只能合并相邻两堆,等价于区间 [ 1 , n ] [1,n] [1,n] 是由两个子区间合并而来的。观察区间 [ 1 , n ] [1, n] [1,n]。假设 1 ≤ k ≤ n 1\le k \le n 1kn,则区间 [ 1 , n ] [1,n] [1,n] 一共有 k k k 种划分方式,由于只有一堆石子的时候,区间合并代价为0,所以 1 < k < n 1<k<n 1<k<n,则有 n − 2 n-2 n2 种划分方式。
  2. 所以说枚举 k k k 的取值,即枚举所有的 [ 1 , n ] [1,n] [1,n] 区间的划分方式,然后从所有划分方式中,选取代价最小的划分方式。
  3. [ 1 , n ] [1, n] [1,n]所划分的两个子区间: [ 1 , k ] , [ k + 1 , n ] [1, k], [k+1, n] [1,k],[k+1,n],那他们的代价也同样是由两个更小的子区间分别合并而来的,所以说需要先求子问题,再求原问题。
  4. ⇒ 递归搜索,递归到子问题,再回溯的时候更新原问题!
  5. 第一步先递归到子区间,即递归到区间内元素只有1堆石子的时候返回代价0。
  6. 每次递归到一个区间,对于该区间的处理即为枚举在这个区间内的所有划分方式,即为: ( l , r ) 。 (l,r)。 (lr)
  7. 递归的计算:由于一个区间等于两个子区间的代价和,所以说计算公式为:
             f [ l ] [ r ] = d f s ( l , k ) + d f s ( k + 1 , r ) + s [ r ] − s [ l − 1 ] ; \ \ \ \ \ \ \ \ f[l][r] = dfs(l, k) + dfs(k+1, r) + s[r] - s[l-1];         f[l][r]=dfs(l,k)+dfs(k+1,r)+s[r]s[l1]

总结:递归;
递归的本质是一棵树!从树的角度思考如何设计递归!

  1. 思考一棵树(节点)的分支有哪些(枚举)?即枚举所有的分支,如果分支多,采用循环,分支少采用顺序枚举就行了。一般都是循环枚举
  2. 记住递归都是从上往下递归的,递归到子问题,解决子问题后回溯更新到原问题。
  3. 思考该节点的取值是如何由子分支递推而来的。即我们的计算公式,一般都是回溯的时候更新当前状态。
  4. 递归的出口:要么是处于叶子节点的时候,要么是不合法的状态。

代码:

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

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

int dfs_min (int l, int r)
{
    if(l == r) 
        return 0;    //叶子节点!
    int res=1e8;
    for (int k=l; k < r; k ++)
        res = min(res, dfs_min(l, k) + dfs_min(k+1, r) + s[r] - s[l-1]);
    
    return res;
}

int dfs_max (int l, int r)
{
    if (l==r) 
        return 0;
    int res=0;
    for (int k=l; k < r; k ++)
        res = max(res, dfs_max (l, k) + dfs_max (k+1,r) + s[r] - s[l-1]);
    
    return res;
}


int main()
{
    cin >> n;
    for (int i=1; i <= n; i ++)
        cin >> a[i], a[i+n] = a[i];
    
    for (int i=1; i <= 2*n; i ++)
        s[i] = s[i-1] + a[i];
    
    int ans1 = 0x3f3f3f3f, ans2 = -0x3f3f3f3f;
    for (int i=1; i <= n; i ++)
    {
        ans1 = min(ans1, dfs_min(i, i+n-1));
        ans2 = max(ans2, dfs_max(i, i+n-1));
    }
    cout << ans1 << endl << ans2 << endl;
    return 0;
}

在这里插入图片描述

二、记忆化搜索:😊

在这里插入图片描述

思路:待更新!

代码:

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

using namespace std;
const int N = 2e2 + 10;
int a[N];
int s[N];
int n;
int f[N][N];
int g[N][N];

int dfs_min (int l, int r)
{
    if (f[l][r]) return f[l][r];
    if(l == r) 
        return f[l][r] = 0;    //叶子节点
    int res=1e8;
    for (int k=l; k < r; k ++)
        res = min(res, dfs_min(l, k) + dfs_min(k+1, r) + s[r] - s[l-1]);
    
    return f[l][r] = res;
}

int dfs_max (int l, int r)
{
    if (g[l][r]) return g[l][r];
    if (l==r) 
        return g[l][r] = 0;
    int res=0;
    for (int k=l; k < r; k ++)
        res = max(res, dfs_max (l, k) + dfs_max (k+1,r) + s[r] - s[l-1]);
    
    return g[l][r] = res;
}


int main()
{
    cin >> n;
    for (int i=1; i <= n; i ++)
        cin >> a[i], a[i+n] = a[i];
    
    for (int i=1; i <= 2*n; i ++)
        s[i] = s[i-1] + a[i];
    
    int ans1 = 0x3f3f3f3f, ans2 = -0x3f3f3f3f;
    for (int i=1; i <= n; i ++)
    {
        ans1 = min(ans1, dfs_min(i, i+n-1));
        ans2 = max(ans2, dfs_max(i, i+n-1));
    }
    cout << ans1 << endl << ans2 << endl;
    return 0;
}

在这里插入图片描述

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

思路:

  1. 合并区间 [ 1 , n ] [1, n] [1,n],每次只能合并相邻两堆,等价于区间 [ 1 , n ] [1,n] [1,n] 是由两个子区间合并而来的。观察区间 [ 1 , n ] [1, n] [1,n]。假设 1 ≤ k ≤ n 1\le k \le n 1kn,则区间 [ 1 , n ] [1,n] [1,n] 一共有 k k k 种划分方式,由于只有一堆石子的时候,区间合并代价为0,所以 1 < k < n 1<k<n 1<k<n,则有 n − 2 n-2 n2 种划分方式。
  2. 所以说枚举 k k k 的取值,即枚举所有的 [ 1 , n ] [1,n] [1,n] 区间的划分方式,然后从所有划分方式中,选取代价最小的划分方式。
  3. [ 1 , n ] [1, n] [1,n]所划分的两个子区间: [ 1 , k ] , [ k + 1 , n ] [1, k], [k+1, n] [1,k],[k+1,n],那他们的代价也同样是由两个更小的子区间分别合并而来的,所以说需要从子问题推导原问题 ⇒ D P DP DP

代码:

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

using namespace std;
const int N = 2e2 + 10;
int a[N];   //每堆石子的重量!
int s[N];
int f[N][N];    //表示所有合并区间[i,j]的方案里面总分最大的方案!
int g[N][N];    //表示所有合并区间[i,j]的方案里面总分最小的方案!
int n;

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

    memset(f, 0x3f, sizeof(f));
    memset(g, -0x3f, sizeof(g));
    for (int len=1; len <= n; len ++)   //思考下,拆环成链:则最长就长度为n!
    {
        for (int i=1; i + len - 1 <= 2*n; i ++) 
        {
            int j = i + len - 1;
            if (len == 1) //区间长度为1,此时区间只有一堆石子
            {
                g[i][j] = 0;
                f[i][j] = 0;    //合并一堆石子的代价 = 0;
                continue;
            }

            for (int k=i; k < j; k ++)  //这里的k取不到右端点,因为取到右端点就会出现i==j,区间长度=1,合并代价=0;
            {
                f[i][j] = min(f[i][j], f[i][k] + f[k+1][j] + s[j] - s[i-1]);
                g[i][j] = max(g[i][j], g[i][k] + g[k+1][j] + s[j] - s[i-1]);
            }
        }
    }
    
    int resn = 0x3f3f3f3f;
    int resx = -0x3f3f3f3f;
    for (int i=1; i <= n; i ++)
    {
        resn = min(resn, f[i][i+n-1]);
        resx = max(resx, g[i][i+n-1]);
    }
    cout << resn << endl << resx;

    return 0;
}

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值