区间dp

区间dp

1. 算法分析

算法思想

使用f[l][r]表示对[l, r]这段区间的处理答案,那么f[l, r]可以划分为对f[l, k]和f[k, r]两段区间的处理答案之和

算法时间复杂度

O(n3)

可以处理的问题分类

  1. 合并线性区间问题
  2. 合并环型区间问题
  3. 多边形划分三角形问题
  4. 子树划分问题
  5. 二维分割问题

2. 算法模板

for (枚举区间长度)
    for (枚举左端点)
        for (枚举分割点)
            f[l, r] = f[l, k] + f[k, r] + val(l, r);

3. 典型例题

3.1 线性石子合并问题

acwing282石子合并
题意: 设有N堆石子排成一排,其编号为1,2,3,…,N。
每堆石子有一定的质量,可以用一个整数来描述,现在要将这N堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
例如有4堆石子分别为 1 3 5 2, 我们可以先合并1、2堆,代价为4,得到4 5 2, 又合并 1,2堆,代价为9,得到9 2 ,再合并得到11,总代价为4+9+11=24;
如果第二步是先合并2,3堆,则代价为7,得到4 7,最后一次合并代价为11,总代价为4+7+11=22。
问题是:找出一种合理的方法,使总的代价最小,输出最小代价。
题解:
状态表示:f[l][r]表示从l~r这段区间的最小值
状态划分:中间节点k的位置
状态转移:f[l][r] = f[l][k] + f[k + 1][r] + s[r] - s[l - 1]
入口: f[i][i] = 0, f[l][r] = INF(l != r)
出口: f[1][n]
代码:

#include <bits/stdc++.h>

using namespace std;

int const N = 3e2 + 10, INF = 0x3f3f3f3f;
int f[N][N], n, w[N], s[N];

int main() {
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> w[i];
        s[i] = s[i - 1] + w[i];  // 求前缀和
    }
    
    for (int len = 1; len <= n; ++len) {  // 枚举区间长度
        for (int l = 1; l + len - 1 <= n; ++l) {  // 枚举左端点
            int r = l + len - 1;  // 右端点
            if (len == 1) f[l][r] = 0;   // 区间长度为1时,为入口
            else {  // 区间长度大于1时,表示这段区间需要更新
                f[l][r] = INF;  // 一开始为不合法状态
                for (int k = l; k < r; ++k)  // 枚举分割点
                    f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);  // 更新
            }
        }
    }
    
    cout << f[1][n] << endl;
    return 0;
}

UCF Local Programming Contest 2012(Practice)A.Wall Street Monopoly

题意:
t个样例
给定n座房子,每次可以把相邻的两座房子合并,合并相邻两座房子的花费为 min(第一座房子长,第一座房子宽) * min(第二座房子长, 第二座房子宽) * 100,合并完 形成的新房子长 = 第一座房子长+第二座房子长,宽=max(第一座房子宽,第二座房子宽),求最小的花费。
题解: 区间dp模板题
代码:

#include<bits/stdc++.h>

using namespace std;

typedef long long LL;
int const N = 25, INF = 0x3f3f3f3f;
LL f[N][N], a[N], n, weight[N][N], s[N];  // f[l][r]合并第l~第r座房子的最小花费,weight[l][r] = t:第l~r座房子合并后的新房子的宽为t, a[N]:第i座房子的长

int main() {
    int t;
    cin >> t;
    while (t--) {
        cin >> n;
        
        // 输入,初始化
        for (int i = 1; i <= n; ++i) {
            int t1, t2;
            cin >> t1 >> t2;
            a[i] = t1;
            weight[i][i] = t2;
            s[i] = s[i - 1] + t1;
        }

        for (int len = 1; len <= n; ++len) {  // 枚举区间长度
            for (int i = 1; i + len - 1 <= n; ++i) {  // 枚举左边界
                int l = i, r = i + len - 1;  // 左右边界
                if (len == 1) {
                    f[l][r] = 0;
                }
                else {
                    f[l][r] = INF;
                    for (int k = l; k < r; ++k) {  // 枚举中间节点
                        LL price = min(s[k] - s[l - 1], weight[l][k]) * min(s[r] - s[k], weight[k + 1][r]) * 100;
                        if (f[l][r] > f[l][k] + f[k + 1][r] + price) {  // 只有满足这个条件才能去更新weight数组
                            f[l][r] = f[l][k] + f[k + 1][r] + price;
                            weight[l][r] = max(weight[l][k], weight[k + 1][r]);
                        }
                    }
                }
            }
        }

        printf("The minimum cost for lot #1 is $%lld.\n\n", f[1][n]);
    }
    return 0;
}

3.2 环型石子合并问题

处理思路: 环型石子合并只需要把原先的石子合并问题从1 ~ n扩展成1 ~ 2n,其中n+1 ~ 2n和1 ~ n相同
acwing1068环型石子合并
题意: 将 n 堆石子绕圆形操场排放,现要将石子有序地合并成一堆。
规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。
请编写一个程序,读入堆数 n 及每堆的石子数,并进行如下计算:

  • 选择一种合并石子的方案,使得做 n−1 次合并得分总和最大。
  • 选择一种合并石子的方案,使得做 n−1 次合并得分总和最小。

题解: 本题在线性石子合并的基础上变成了一个环,转化的技巧是把原先1n的区域变成12n,其中[n+1,2n] = [1,n]
其他的处理方式和原先的一样
求答案的时候需要枚举一下从哪里开始取长度为n的区间
代码:

#include <bits/stdc++.h>

using namespace std;

int const N = 2e2 + 10, INF = 0x3f3f3f3f;
int f1[N * 2][N * 2], w[N * 2], s[N * 2], n, f2[N * 2][N * 2];  // f1记录最大值, f2记录最小值

int main() {
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> w[i];
        w[i + n] = w[i];
    }
    
    for (int i = 1; i <= 2 * n; ++i) s[i] = s[i - 1] + w[i];
    
    // dp更新
    for (int len = 1; len <= n; ++len) {
        for (int l = 1; l + len - 1 <= 2 * n; ++l) {
            int r = l + len - 1;
            if (len == 1) {
                f1[l][r] = 0;
                f2[l][r] = 0;
            }
            else {
                f1[l][r] = -INF;
                f2[l][r] = INF;
                for (int k = l; k < r; ++k) {    
                    f1[l][r] = max(f1[l][r], f1[l][k] + f1[k + 1][r] + s[r] - s[l - 1]);
                    f2[l][r] = min(f2[l][r], f2[l][k] + f2[k + 1][r] + s[r] - s[l - 1]);
                }
            }
        }
    }
    
    // 答案处理,长度区间为n
    int maxv = -1, minv = INF;
    for (int i = 1; i <= n; ++i) {
        maxv = max(maxv, f1[i][i + n - 1]);
        minv = min(minv, f2[i][i + n - 1]);
    }
    
    cout << minv << endl << maxv << endl;
    return 0;
}

acwing320能力项链
题意: 在Mars星球上,每个Mars人都随身佩带着一串能量项链,在项链上有 N 颗能量珠。能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。因为只有这样,通过吸盘(吸盘是Mars人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。如果前一颗能量珠的头标记为m,尾标记为r,后一颗能量珠的头标记为 r,尾标记为 n,则聚合后释放的能量为 m * r * n(Mars单位),新产生的珠子的头标记为 m,尾标记为 n。需要时,Mars人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。
例如:设N=4,4颗珠子的头标记与尾标记依次为(2,3) (3,5) (5,10) (10,2)。
我们用记号⊕表示两颗珠子的聚合操作,(j⊕k)表示第 j,k 两颗珠子聚合后所释放的能量。则
第4、1两颗珠子聚合后释放的能量为:(4⊕1)=10 * 2 * 3=60。
这一串项链可以得到最优值的一个聚合顺序所释放的总能量为((4⊕1)⊕2)⊕3)= 10 * 2 *3 + 10 * 3 * 5 + 10 * 5 * 10 = 710。
题解:
本题是一种环状的区间dp,因此处理环状的dp可以通过复制[1, n]来实现把环状变成线状的区间dp
转移方程为:f[l][r] = max(f[l][r], f[l][k] + f[k][r] + w[l] * w[k] * w[r]);
代码:

#include<bits/stdc++.h>
 
using namespace std;

int n;
int const N = 2e2 + 10;
int f[N][N], w[N];
 
int main() {
    // 读入
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &w[i]);
        w[i + n] = w[i];
    }

    // 更新状态
    for (int len = 3; len <= n + 1; ++len) {  // 枚举区间长度,因为能够处理到最初的位置,所以是n+1
        for (int l = 1; l + len - 1 <= 2 * n; ++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[k] * w[r]);  // 状态转移
            }
        }
    }

    // 找答案
    int res = 0;
    for (int i = 1; i <= n; ++i) res = max(res, f[i][i + n]);
    cout << res << endl;
    return 0;
}

acwing283多边形

题意: “多边形游戏”是一款单人益智游戏。游戏开始时,给定玩家一个具有N个顶点N条边(编号1-N)的多边形,如图1所示,其中N = 4。每个顶点上写有一个整数,每个边上标有一个运算符+(加号)或运算符*(乘号)。
Snipaste_2020-03-10_23-10-49.png
第一步,玩家选择一条边,将它删除。接下来在进行N-1步,在每一步中,玩家选择一条边,把这条边以及该边连接的两个顶点用一个新的顶点代替,新顶点上的整数值等于删去的两个顶点上的数按照删去的边上标有的符号进行计算得到的结果。下面是用图1给出的四边形进行游戏的全过程。
Snipaste_2020-03-10_23-11-42.png
最终,游戏仅剩一个顶点,顶点上的数值就是玩家的得分,上图玩家得分为0。请计算对于给定的N边形,玩家最高能获得多少分,以及第一步有哪些策略可以使玩家获得最高得分。
题解:
本题是环型区间dp的板子题,f[l][r]表示l点 ~ r点合并成一个点时的最大值,g[l][r]示l点 ~ r点合并成一个点时的最小值
f[l][r] = max(f[l][r], f[l][k] ⊙ f[k +1 ][r])(⊙定义为加法或者乘法)
本题需要注意的是两个最大的数字相乘不一定是最大的数,可能两个最小的数字相乘才是最大的数,因此需要维护一个g[l][r]来记录最小的情况
代码:

#include<bits/stdc++.h>

using namespace std;

int const N = 100 + 10, INF = 0x3f3f3f3f;
int f[N][N], point[N], n, g[N][N];  // f[l][r]表示l点~r点合并成一个点时的最大值,g[l][r]示l点~r点合并成一个点时的最小值
char op[N];

int main() {
    cin >> n;
    for (int i = 1; i <= n; ++i) {  // 输入、环型处理
        cin >> op[i] >> point[i];
        point[i + n] = point[i];
        op[i + n] = op[i];
    }

    // 区间dp更新
    for (int len = 1; len <= n; ++len) {
        for (int l = 1; l + len - 1 <= 2 * n; ++l) {
            int r = l + len - 1;
            if (len == 1)  {
                f[l][r] = point[l];
                g[l][r] = point[l];
            }
            else {
                f[l][r] = -INF;  // 初始化
                g[l][r] = INF;

                // 枚举分割点更新
                for (int k = l; k < r; ++k) {
                    int maxl = f[l][k], maxr = f[k + 1][r];
                    int minl = g[l][k], minr = g[k + 1][r];
                    if (op[k + 1] == 't') {  // 加法的情况
                        f[l][r] = max(f[l][r], maxl + maxr);
                        g[l][r] = min(g[l][r], minl + minr);
                    }
                    else {  // 乘法的情况
                        int x1 = maxl * maxr, x2 = maxl * minr, x3 = minl * maxr, x4 = minl * minr;
                        f[l][r] = max(f[l][r], max(max(x1, x2), max(x3, x4)));
                        g[l][r] = min(g[l][r], min(min(x1, x2), min(x3, x4)));
                    }
                }
            }
        }
    }

    int maxv = -INF;
    for (int i = 1; i <= n; ++i) maxv = max(maxv, f[i][i + n - 1]);
    cout << maxv << endl;
    for (int i = 1; i <= n; ++i)
        if (f[i][i + n - 1] == maxv) cout << i << " ";
    return 0;
}

3.3 多边形划分三角形问题

3.3.1 求面积

处理思路: 把一个n个顶点的多边形划分为n-2个三角形,可以去枚举中间那个三角形的底的左右端点,然后整个多边形的面积值就变成最左边那个多边形+中间那个三角形+最右边那个多边形
Screenshot_20200310_143231_com.huawei.notepad.jpg

acwing1069凸多边形的划分
题意: 给定一个具有 N 个顶点的凸多边形,将顶点从 1 至 N 标号,每个顶点的权值都是一个正整数。
将这个凸多边形划分成 N−2 个互不相交的三角形,对于每个三角形,其三个顶点的权值相乘都可得到一个权值乘积,试求所有三角形的顶点权值乘积之和至少为多少。
题解:
f[l][r]表示l点到r点这些点组成的多边形可以划分的三角形的最小面积,那么可以把这个多边形划分为3部分,从l ~ k这些点组成的多边形,l、k、r三个点组成的三角形,k ~ r这些点组成的多边形,因此有状态转移方程:f[l][r] = min(f[l][r], f[l][k] + f[k][r] + w[l] * w[k] * w[r]);
代码:

#include <bits/stdc++.h>
using namespace std;
//大数

// ...大数板子

int const N = 100 + 10;
BigInteger w[N], f[N][N];  // f[l][r]:l点到r点这些点组成的所以点可以划分的三角形的面积之和
int n;

int main() {
    cin >> n;
    BigInteger INF = 1;
    INF = INF.e(30);
    for (int i = 1; i <= n; i++) cin >> w[i];

    for (int len = 3; len <= n; len++)  // 枚举区间长度
        for (int l = 1; l + len - 1 <= n; l++) {  // 枚举左端点
            int r = l + len - 1;  // 右端点
            f[l][r] = INF;  // 一开始为非法状态
            for (int k = l + 1; k < r; k++)  // 枚举分割点
                f[l][r] = min(f[l][r], f[l][k] + f[k][r] + w[l] * w[k] * w[r]);  // dp更新
        }

    cout << f[1][n] << endl;

    return 0;
}

3.3.2 求方案数

2019ICPC沈阳区域赛G-Triangulation
题意: 给定n个点,这n个点是顺时针给定或者逆时针给定,n个点组成一个多边形,求把n-多边形划分成三角形的方案数。
题解: 输入的点可能顺时针也可能逆时针,把这些点变换成顺时针(使用叉乘来判断,a和b叉乘大于0表明b在a的逆时针方向)
状态表示:f[l][r]表示利用l~r这些点组成的多边形可以划分成的方案数
状态划分:分割点k
状态转移:f[l][r] = (f[l][r] + f[l][k] * f[k][r]) % mod
入口:f[i][i + 1] = 1;
出口:f[1][n]
转移的时候要注意判断rl向量在lk向量的逆时针方向(保证转移合法,不会出现凹进去构成的三角形)
代码:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
int const N = 2e3 + 10, MOD = 998244353;
struct POINT {
    LL x, y;
    LL operator *(const POINT &b) const {
        return x * b.y - y * b.x;
    }

    POINT operator -(const POINT &b) const  {
        POINT p;
        p.x = x - b.x;
        p.y = y - b.y;
        return p;
    }
}p[N];
int f[N][N], n;

int main() {
    cin >> n;
    LL s = 0;  // 判断顺时针还是逆时针输入
    for (int i = 1; i <= n; ++i) {
        cin >> p[i].x >> p[i].y;
        s += p[i - 1] * p[i];
    }
    s += p[n] * p[1];

    if (s > 0) {  // 大于0表示逆时针输入
        for (int i = 1; i <= n / 2; ++i)
            swap(p[i], p[n - i + 1]);
    }

    for (int i = 1; i < n; ++i) f[i][i + 1] = 1;  // 入口

    // dp更新
    for (int len = 3; len <= n; ++len) {  // 区间长度
        for (int l = 1; l + len - 1 <= n; ++l) {  // 左端点
            int r = l + len - 1;
            for (int k = l + 1; k < r; ++k)  // 分割点
                if((p[r] - p[l]) * (p[k] - p[l]) > 0)  // 合法转移条件
                    f[l][r] = (f[l][r] + f[l][k] * (LL)f[k][r]) % MOD;  // 转移
        }
    }
    cout << f[1][n] << endl;
    return 0;
}

3.4 子树划分问题

处理思路: 由于树的遍历答案可以划分为左1子树区间、左2子树区间、…右子树区间、根,因此可以使用区间dp来处理
acwing479加分二叉树
题意: 设一个n个节点的二叉树tree的中序遍历为(1,2,3,…,n),其中数字1,2,3,…,n为节点编号。
每个节点都有一个分数(均为正整数),记第i个节点的分数为di,tree及它的每个子树都有一个加分,任一棵子树subtree(也包含tree本身)的加分计算方法如下:     
subtree的左子树的加分 × subtree的右子树的加分 + subtree的根的分数 
若某个子树为空,规定其加分为1。叶子的加分就是叶节点本身的分数,不考虑它的空子树。
试求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树tree。
要求输出: 
(1)tree的最高加分 
(2)tree的前序遍历
题解: f[l][r]表示中序遍历[l, r]的最大值
状态划分使用根节点j
则有状态转移方程:f[l][r] = max(f[l][r], f[l][j - 1] * f[j + 1][r] + w[j]);
进行状态转移即可

为了找出前序遍历,可以维护一个g数组,g[i][j]表示[i, j]序列的跟节点
然后做根左右的dfs即可
为了维护字典序最小的前序遍历,我们只要保证当两个f[l][r]相等时,取j小的那个,那么只需要把分割点j按从小到大枚举即可
代码:

#include<bits/stdc++.h>
 
using namespace std;

int n;
int const N = 50;
int f[N][N], w[N], g[N][N];  // f[l][r]区间[l, r]的最小值,g[l][r] = t 区间[l, r]的根节点为t
 
// 得到前序遍历:根左右
void dfs(int l, int r) {
    if (l > r) return;
    int root = g[l][r];
    cout << root << " ";
    dfs(l, root - 1);
    dfs(root + 1, r);
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; ++i) scanf("%d", &w[i]);

    for (int len = 1; len <= n; ++len) {  // 枚举区间长度
        for (int l = 1; l + len - 1 <= n; ++l) {  // 左端点
            int r = l + len - 1;  // 右端点
            if (len == 1) {   // 如果区间长度为1,说明是叶节点
                f[l][r] = w[l];
                g[l][r] = l;  // 根节点是自己
            }  
            else {   // 区间长度不为1
                for (int j = l ; j <= r; ++j) {  // 枚举中点
                    int left = j == l? 1: f[l][j - 1];
                    int right = j == r? 1: f[j + 1][r];
                    int score = left * right + w[j];
                    if (score > f[l][r]) {  // 只有score>f[l][r]时才能转移,这样才能得到字典序最小的前序遍历
                        f[l][r] = score;
                        g[l][r] = j;
                    }
                }
            }
        }
    }
    cout << f[1][n] << endl;  // 输出最高得分
    dfs(1, n);  // 输出前序遍历结果
    return 0;
}

acwing284金字塔
题意: 虽然探索金字塔是极其老套的剧情,但是有一队探险家还是到了某金字塔脚下。
经过多年的研究,科学家对这座金字塔的内部结构已经有所了解。
首先,金字塔由若干房间组成,房间之间连有通道。
如果把房间看作节点,通道看作边的话,整个金字塔呈现一个有根树结构,节点的子树之间有序,金字塔有唯一的一个入口通向树根。
并且,每个房间的墙壁都涂有若干种颜色的一种。
探险队员打算进一步了解金字塔的结构,为此,他们使用了一种特殊设计的机器人。
这种机器人会从入口进入金字塔,之后对金字塔进行深度优先遍历。
机器人每进入一个房间(无论是第一次进入还是返回),都会记录这个房间的颜色。
最后,机器人会从入口退出金字塔。
显然,机器人会访问每个房间至少一次,并且穿越每条通道恰好两次(两个方向各一次), 然后,机器人会得到一个颜色序列。
但是,探险队员发现这个颜色序列并不能唯一确定金字塔的结构。
现在他们想请你帮助他们计算,对于一个给定的颜色序列,有多少种可能的结构会得到这个序列。
因为结果可能会非常大,你只需要输出答案对109 取模之后的值。
题解:
状态表示:f[l][r]:从l~r这段区间的方案
状态划分:最后一颗子树的起始位置
状态转移:f[l][r] = (f[l][r] + (LL)f[l][k] * f[k + 1][r - 1]) % MOD;
入口:f[l][l] = 1
出口:f[1][n]
dfs序列有几个性质:

  1. 长度为2(n-1)+1 = 2n - 1
  2. [l, r]中,str[l] == str[r]
    本题判断是时候必须满足输入序列长度为奇数
    枚举最后一颗子树的起始位置:k=l~r-2,同时要满足str[k] == str[r]才能转移
    Screenshot_20200311_004836.jpg

代码:

#include<bits/stdc++.h>

using namespace std;

typedef long long LL;
int const N = 3e2 + 10, MOD = 1e9;
char str[N];
int n, f[N][N];

int main() {
    cin >> str + 1;
    n = strlen(str + 1);

    if (n % 2 == 0) printf("0\n");  // 长度必须为奇数
    else {
        for (int len = 1; len <= n; ++len) {  // 枚举区间长度
            for (int l = 1; l + len - 1 <= n; ++l) {  // 枚举左端点
                int r = l + len - 1;  // 右端点
                if (len == 1) f[l][r] = 1;  // 入口
                else if (str[l] == str[r]) {  // 必须满足这个条件才可能存在子树
                    for (int k = l; k < r - 1; k += 2) {  //  枚举最后一颗子树的左端点
                        if (str[k] == str[r])  // 必须满足这个条件才可能存在子树
                            f[l][r] = (f[l][r] + (LL)f[l][k] * f[k + 1][r - 1]) % MOD;  // 转移
                    }
                }
            }
        }
        cout << f[1][n] << endl;
    }
    return 0;
}

3.5 二维分割问题

处理思路: 区间dp也可以处理对一个二维区间进行分割的情况,因为每个二维区间可以分成从(x1, y1) ~ (x2, y2)的区间,然后可以枚举分割线,变成从(x1, y1) ~ 分割线,分割线 ~ (x2, y2)的两个子状态之和
acwing321棋盘分割
题意: 将一个8*8的棋盘进行如下分割:将原棋盘割下一块矩形棋盘并使剩下部分也是矩形,再将剩下的部分继续如此分割,这样割了(n-1)次后,连同最后剩下的矩形棋盘共有n块矩形棋盘。(每次切割都只能沿着棋盘格子的边进行)
Snipaste_2020-03-10_15-38-57.png
原棋盘上每一格有一个分值,一块矩形棋盘的总分为其所含各格分值之和。
现在需要把棋盘按上述规则分割成n块矩形棋盘,并使各矩形棋盘总分的均方差最小。
均方差
Snipaste_2020-03-10_15-39-44.png
,其中平均值Snipaste_2020-03-10_15-40-21.png ,xi为第 i 块矩形棋盘的总分。
请编程对给出的棋盘及n,求出均方差的最小值。
题解: 本题是二维的区间dp,f[x1][y1][x2][y2][k]:表示{(x1, y1), (x2, y2)}的这个矩形切成k块需要的代价
以最后一刀的位置来划分,最后一刀可以是横的,也可以是竖的,枚举过程可以采用记忆化搜索来优化时间复杂度。如果不用记忆化搜索,那么需要写6个for循环,所以直接记忆化搜索,二维的情况就可以使用记忆化搜索
代码:

#include<bits/stdc++.h>

using namespace std;

const int N = 15, M = 9;
const double INF = 1e9;

int n, m = 8;
int s[M][M];
double f[M][M][M][M][N];
double X;

// 求二维前缀和
int get_sum(int x1, int y1, int x2, int y2) {
    return s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1];
}

// 求计算的公式答案
double get(int x1, int y1, int x2, int y2) {
    double sum = get_sum(x1, y1, x2, y2) - X;
    return (double)sum * sum / n;
}

// 当前要分割的区域为(x1, y1)~(x2, y2),还需分割k块
double dp(int x1, int y1, int x2, int y2, int k) {
    double &v = f[x1][y1][x2][y2][k];
    if (v >= 0) return v;  // 如果枚举过,那么直接返回
    if (k == 0) return v = get(x1, y1, x2, y2);  // 如果k等于0,表示不需要切割了

    v = INF;  // 记忆化搜索的结构是一棵树,枚举最小值时初始化为INF

    // 枚举横的
    for (int i = x1; i < x2; i ++ ) {
        v = min(v, get(x1, y1, i, y2) + dp(i + 1, y1, x2, y2, k - 1));
        v = min(v, get(i + 1, y1, x2, y2) + dp(x1, y1, i, y2, k - 1));
    }

    // 枚举竖的
    for (int i = y1; i < y2; i ++ ) {
        v = min(v, get(x1, y1, x2, i) + dp(x1, i + 1, x2, y2, k - 1));
        v = min(v, get(x1, i + 1, x2, y2) + dp(x1, y1, x2, i, k - 1));
    }

    return v;
}

int main() {
    cin >> n;

    // 预处理前缀和
    for (int i = 1; i <= m; i ++ )
        for (int j = 1; j <= m; j ++ ) {
            cin >> s[i][j];
            s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
        }

    X = (double)s[m][m] / n;
    memset(f, -1, sizeof f);
    printf("%.3lf\n", sqrt(dp(1, 1, 8, 8, n - 1)));

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值