题目传送门

一、数据样例解析

第二行是\(N\)个用空格隔开的正整数,所有的数均不超过 \(1000\),第 \(i\) 个数为第 \(i\) 颗珠子的头标记,当 \(i<N\) 时,第 \(i\) 颗珠子的尾标记应该等于第 \(i+1\) 颗珠子的头标记,第 \(N\) 颗珠子的尾标记应该等于第 \(1\) 颗珠子的头标记。

上面这段话,就是告诉我们,其实,是一个首尾相连接的

样例: \(2\) \(3\) \(5\) \(10\)
表示有\(4\)个珠子:\(①[2,3]\), \(②[3,5]\),\(③[5,10]\),\(④[10,2]\)

如果我们先合并的\(④\)\(①\),最终结果就是\(⑤[10,3]\),释放能量\(10*2*3=60\)点,剩下\(②③⑤\)
\(⑤\)再与\(②\)合并,结果就是\(⑥[10,5]\),释放能量\(10*3*5=150\)点。剩下\(③\)\(⑥\)
\(⑥\)\(③\)合并,结果就是\(⑦[10,10]\),释放能量\(10*5*10=500\)点。剩下\(⑦\),只剩下一个了,完成!
一共合并\(3\)次,总的能量就是\(60+150+500=710\)

二、理解题意

本题是上一题环形石子合并问题的变形,或者说更类似于矩阵连乘问题。按照上一题一样的处理环形问题的方法,将序列的长度翻倍。

状态表示
\(f[l][r]\)表示第\(l\)到第\(r\)个数合并释放的最大能量

这里注意比如\(len\)\(3\)时,\(2\) \(3\) \(5\)实际上只是两颗珠子包含的信息,即\((2,3)\)\((3,5)\)因此合并\(2\) \(3\) \(5\)释放的能量等于\(2 * 3 *5\)

最后剩下一棵珠子的头尾吸盘也需要合并到一起的,比如\(2\) \(3\) \(5\) \(10\)合并成为了\((2,10)\),尽管此时合并成了一串,但项链的首尾仍需要连接,所以还需要合并\((2,10)\)\((10,2)\),本题就转化成了求区间长度为\(n + 1\)的石子合并问题的最大值。

以往的 区间DP 我们是把区间 \([a,b]\) 拆分为 \([a,k]\)\([k+1,b]\)

因为 同一个石子 只会被合并到 一个石子堆

但本题合并魔法石时,分割点 \(k\) 要被分到 左侧石子堆的右端点右侧石子堆的左端点

因此,参数 \(k\) 要作为两个区间的共同端点来使用,即 \([a,k]\)\([k,b]\)
此外我们原来只需要合并 \(n\) 个石头,这样转换后就要合并 \(n+1\) 个石头了

三、dfs实现代码

#include <bits/stdc++.h>

using namespace std;
const int N = 210;

int n;
int a[N];
int f[N][N];  //记忆化结果

//计算l~r之间的结果值
int dfs(int l, int r) {
    if (r <= l)return 0;                            //区间内有一个数字
    if (r == l + 1) return (a[l] * a[r] * a[r + 1]);//区间内有两个数字
    //a[r+1]就是破环成链的妙用

    int &v = f[l][r];//准备记录结果
    if (v)return v;  //如果计算过,则返回已经有的结果
    //枚举倒数第一个可能的结束位置
    //区间      l..........(i-1).i..................r
    //分成      |<------------->|<------------------>|
    //新区间为   l..(i-1)           i..r
    //即        (l,l+1)..(i-1,i) 和 (i,i+1)..(r,r+1) 注意最后是r+1 !!
    for (int i = l + 1; i <= r; i++)
        v = max(v, dfs(l, i - 1) + dfs(i, r) + a[l] * a[i] * a[r + 1]);
    return v;
}

int main() {
    cin >> n;
    //破环成链
    for (int i = 1; i <= n; i++)cin >> a[i], a[i + n] = a[i];
    int res = 0;
    //以每个位置为起点,跑一遍dfs,找出最大值
    for (int i = 1; i <= n; i++) res = max(res, dfs(i, i + n - 1));
    printf("%d", res);
    return 0;
}

四、DP实现代码I

#include <bits/stdc++.h>

//把合并n颗珠子的问题转化为合并(n+1)个数合并的问题,只不过有一个数是公用的,注意不要间断分割
//区间长度为n+1
using namespace std;
const int N = 210;//开两倍的空间
int n;
int w[N];
int f[N][N];    //左,右端点间的能量最大值
int main() {
    cin >> n;
    //环形DP的标准路线,双倍长度,w[i+n]=w[i]
    for (int i = 1; i <= n; i++) {
        cin >> w[i];
        w[i + n] = w[i];
    }
    //因为最后需要扣头,所以区间长度是n+1
    //长度是3才能构成两个珠子,才能对接释放能量
    for (int len = 3; len <= n + 1; len++)           //枚举有效区间长度
        for (int l = 1; l + len - 1 <= n * 2; l++) { //枚举左端点
            int r = l + len - 1;                     //计算右端点
            for (int k = l + 1; k < r; k++)          //枚举分界点,模拟最后一次是在哪个位置合并的
                f[l][r] = max(f[l][r], f[l][k] + f[k][r] + w[l] * w[r] * w[k]);
        }
    //枚举所有区间长度是n+1的区间,取一个最大值
    int res = 0;
    for (int i = 1; i <= n; i++)//枚举每个左端点,长度为n+1的区间,能量最大值是多少
        res = max(res, f[i][i + n]);
    //输出
    printf("%d", res);
    return 0;
}

五、DP实现代码II

#include <bits/stdc++.h>

using namespace std;
//直接当成珠子来看待即可,模拟合并过程。
//区间长度为n
const int N = 210;
const int INF = 0x3f3f3f3f;

int n;
int a[N];
int f[N][N];

int main() {
    cin >> n;
    //破环成链
    for (int i = 1; i <= n; i++) cin >> a[i], a[i + n] = a[i];

    for (int len = 2; len <= n; len++)
        for (int l = 1; l + len - 1 <= n * 2; l++) {
            int r = l + len - 1;
            for (int k = l; k < r; k++)
                f[l][r] = max(f[l][r], f[l][k] + f[k + 1][r] + a[l] * a[k + 1] * a[r + 1]);
            //这里的过程同石子合并,这里不难想到若将l到k的珠子合并之后会变成一个首是l而尾k+1的珠子;
            //同理若将k+1到r的珠子合并之后会变成一个首是k+1而尾r+1的珠子;
        }
    int res = -INF;
    for (int i = 1; i <= n; i++) res = max(f[i][i + n - 1], res);//区间长度为n
    cout << res << endl;
    return 0;
}

相关文章: