区间dp(环形dp

将 n 堆石子绕圆形操场排放,现要将石子有序地合并成一堆。

规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。

请编写一个程序,读入堆数 n 及每堆的石子数,并进行如下计算:

选择一种合并石子的方案,使得做 n−1 次合并得分总和最大。
选择一种合并石子的方案,使得做 n−1 次合并得分总和最小。
输入格式
第一行包含整数 n,表示共有 n 堆石子。

第二行包含 n 个整数,分别表示每堆石子的数量。

输出格式
输出共两行:

第一行为合并得分总和最小值,

第二行为合并得分总和最大值。

数据范围
1≤n≤200
输入样例:
4
4 5 9 4
输出样例:
43
54

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 440,INF=0x3f3f3f3f;
int f[N][N],g[N][N],n,w[N+N],maxv=-INF,minv=INF;
int sum[2*N];
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>w[i];
        w[i+n]=w[i];
        
    }
    for(int i=1;i<=n*2;i++)sum[i]=sum[i-1]+w[i];
    memset(f,-0x3f,sizeof f);
    memset(g,0x3f,sizeof g);
    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]=g[l][r]=0;
            else{
                for(int k=l;k<r;k++){
                    f[l][r]=max(f[l][r],f[l][k]+f[k+1][r]+sum[r]-sum[l-1]);
                    g[l][r]=min(g[l][r],g[l][k]+g[k+1][r]+sum[r]-sum[l-1]);
                }
            }
            
        }
    }
    for(int i=1;i<=n;i++){
        maxv=max(maxv,f[i][i+n-1]);
        minv=min(minv,g[i][i+n-1]);
    }
    cout<<minv<<endl<<maxv;
}

在 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。

输入格式
输入的第一行是一个正整数 N,表示项链上珠子的个数。

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

至于珠子的顺序,你可以这样确定:将项链放到桌面上,不要出现交叉,随意指定第一颗珠子,然后按顺时针方向确定其他珠子的顺序。

输出格式
输出只有一行,是一个正整数 E,为一个最优聚合顺序所释放的总能量。

数据范围
4≤N≤100,
1≤E≤2.1×109
输入样例:
4
2 3 5 10
输出样例:
710

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 202;
int w[N],dp[N][N];
int n;
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>w[i];
        w[i+n]=w[i];
    }
    //memset(dp,-0x3f,sizeof dp);
    //len表示节点数量
    for(int len=3;len<=n+1;len++){
        for(int L=1;L+len-1<=2*n;L++){
            int R=L+len-1;
            for(int k=L+1;k<=R-1;k++){
                dp[L][R]=max(dp[L][R],dp[L][k]+dp[k][R]+w[L]*w[k]*w[R]);
            }
        }
    }
    int ans=0;
    for(int i=1;i<=n;i++)ans=max(ans,dp[i][i+n]);
    cout<<ans<<endl;
    
}

给定一个具有 N 个顶点的凸多边形,将顶点从 1 至 N 标号,每个顶点的权值都是一个正整数。

将这个凸多边形划分成 N−2 个互不相交的三角形,对于每个三角形,其三个顶点的权值相乘都可得到一个权值乘积,试求所有三角形的顶点权值乘积之和至少为多少。

输入格式
第一行包含整数 N,表示顶点数量。

第二行包含 N 个整数,依次为顶点 1 至顶点 N 的权值。

输出格式
输出仅一行,为所有三角形的顶点权值乘积之和的最小值。

数据范围
N≤50,
数据保证所有顶点的权值都小于109
输入样例:
5
121 122 123 245 231
输出样例:
12214884

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

using namespace std;

typedef long long LL;

const int N = 55, M = 35, INF = 1e9;

int n;
int w[N];
LL f[N][N][M];

void add(LL a[], LL b[])
{
    static LL c[M];
    memset(c, 0, sizeof c);
    for (int i = 0, t = 0; i < M; i ++ )
    {
        t += a[i] + b[i];
        c[i] = t % 10;
        t /= 10;
    }
    memcpy(a, c, sizeof c);
}

void mul(LL a[], LL b)
{
    static LL c[M];
    memset(c, 0, sizeof c);
    LL t = 0;
    for (int i = 0; i < M; i ++ )
    {
        t += a[i] * b;
        c[i] = t % 10;
        t /= 10;
    }
    memcpy(a, c, sizeof c);
}

int cmp(LL a[], LL b[])
{
    for (int i = M - 1; i >= 0; i -- )
        if (a[i] > b[i]) return 1;
        else if (a[i] < b[i]) return -1;
    return 0;
}

void print(LL a[])
{
    int k = M - 1;
    while (k && !a[k]) k -- ;
    while (k >= 0) cout << a[k -- ];
    cout << endl;
}

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i ++ ) cin >> w[i];

    LL temp[M];
    for (int len = 3; len <= n; len ++ )
        for (int l = 1; l + len - 1 <= n; l ++ )
        {
            int r = l + len - 1;
            f[l][r][M - 1] = 1;
            for (int k = l + 1; k < r; k ++ )
            {
                memset(temp, 0, sizeof temp);
                temp[0] = w[l];
                mul(temp, w[k]);
                mul(temp, w[r]);
                add(temp, f[l][k]);
                add(temp, f[k][r]);
                if (cmp(f[l][r], temp) > 0)
                    memcpy(f[l][r], temp, sizeof temp);
            }
        }

    print(f[1][n]);

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/124469/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

设一个 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的前序遍历

输入格式
第 1 行:一个整数 n,为节点个数。

第 2 行:n 个用空格隔开的整数,为每个节点的分数(0<分数<100)。

输出格式
第 1 行:一个整数,为最高加分(结果不会超过int范围)。

第 2 行:n
个用空格隔开的整数,为该树的前序遍历。如果存在多种方案,则输出字典序最小的方案。

数据范围
n<30
输入样例:
5
5 7 1 2 10
输出样例:
145
3 1 2 4 5

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int w[100];
int tree[100][100];
int root[100][100];
int n;
int ans;
int dfs(int l,int r){
    if(tree[l][r])return tree[l][r];
    if(l==r)return w[l];
    if(l>r)return 1;
    for(int i=l;i<=r;i++){
        int t=dfs(l,i-1)*dfs(i+1,r)+w[i];
        if(t>tree[l][r]){
            tree[l][r]=t;
            root[l][r]=i;
        }
    }
    return tree[l][r];
}
void dg(int l,int r){
    if(l>r)return;
    if(l==r){printf("%d ",root[l][r]);return;}
    
    printf("%d ",root[l][r]);
    dg(l,root[l][r]-1);
    dg(root[l][r]+1,r);
}

int main(){
    cin>>n;
    for(int i=1;i<=n;i++)cin>>w[i];
    for(int i=1;i<=n;i++)root[i][i]=i;
    cout<<dfs(1,n)<<endl;
    dg(1,n);
}

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

using namespace std;

typedef pair<int, int> PII;

const int N = 50;

int n;
int w[N];
unsigned f[N][N];
int root[N][N];

void dfs(int l, int r)
{
    if (l > r) return;

    int k = root[l][r];
    printf("%d ", k);
    dfs(l, k - 1);
    dfs(k + 1, r);
}

int main()
{
    scanf("%d", &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;

            for (int k = l; k <= r; k ++ )
            {
                int left = k == l ? 1 : f[l][k - 1];
                int right = k == r ? 1 : f[k + 1][r];
                int score = left * right + w[k];
                if (l == r) score = w[k];
                if (f[l][r] < score)
                {
                    f[l][r] = score;
                    root[l][r] = k;
                }
            }
        }

    printf("%d\n", f[1][n]);
    dfs(1, n);
    puts("");

    return 0;
}

作者:yxc
链接:https://www.acwing.com/solution/content/3804/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

将一个 8×8 的棋盘进行如下分割:将原棋盘割下一块矩形棋盘并使剩下部分也是矩形,再将剩下的部分继续如此分割,这样割了 (n−1) 次后,连同最后剩下的矩形棋盘共有 n 块矩形棋盘。(每次切割都只能沿着棋盘格子的边进行)

1191_1.jpg

原棋盘上每一格有一个分值,一块矩形棋盘的总分为其所含各格分值之和。

现在需要把棋盘按上述规则分割成 n 块矩形棋盘,并使各矩形棋盘总分的均方差最小。

均方差formula.png ,其中平均值lala.png ,xi 为第 i 块矩形棋盘的总分。

请编程对给出的棋盘及 n,求出均方差的最小值。

输入格式
第 1 行为一个整数 n。

第 2 行至第 9 行每行为 8 个小于 100 的非负整数,表示棋盘上相应格子的分值。每行相邻两数之间用一个空格分隔。

输出格式
输出最小均方差值(四舍五入精确到小数点后三位)。

数据范围
1<n<15
输入样例:
3
1 1 1 1 1 1 1 3
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 0
1 1 1 1 1 1 0 3
输出样例:
1.633
本题的大意是给定一个 8∗88∗8 的棋盘,对这个棋盘进行 n−1(1<n<15)n−1(1<n<15) 次划分,要求每次划分后,只可以选择两块子矩阵的其中一块继续划分。

完成划分后,一共会得到 nn 块棋盘。

原棋盘上每一格有一个分值,一块矩形棋盘的总分为其所含各格分值之和。

现在需要把棋盘按上述规则分割成 nn 块矩形棋盘,并使各矩形棋盘总分的均方差最小。

显然可以观察到一个二维 区间DP区间DP 的模型 (不对,二维是不是应该叫面积DP(滑稽))

根据题意,我们可以得到的信息是

​ 1)一共可以对棋盘进行 n−1n−1 次划分得到 nn 个子棋盘

​ 2)对所有的子棋盘可以求得的平均数 x¯¯¯=∑ni=1xinx¯=∑i=1nxin
​ 3)要求总分的均方差最小 σ=∑ni=1(xi−x¯¯¯)2n−−−−−−−−−−−−√σ=∑i=1n(xi−x¯)2n
为了使方差 σσ 最小,因此我们首先需要对 σσ 的表达式进行化简

推导过程:

​ σ2=1n(∑ni=1x2i−2∑ni=1xi⋅x¯¯¯+∑ni=1x¯¯¯2)σ2=1n(∑i=1nxi2−2∑i=1nxi⋅x¯+∑i=1nx¯2)
​ σ2=1n∑ni=1x2i−2x¯¯¯1n∑ni=1xi+1n⋅nx¯¯¯2σ2=1n∑i=1nxi2−2x¯1n∑i=1nxi+1n⋅nx¯2
​ σ2=1n∑ni=1x2i−2x¯¯¯2+x¯¯¯2σ2=1n∑i=1nxi2−2x¯2+x¯2
​ σ2=1n∑ni=1x2i−x¯¯¯2σ2=1n∑i=1nxi2−x¯2
由于 x¯¯¯x¯ 是常数,因此显然可以观察到 σσ 与 xixi 是同增的

为了最终方差是最小值,我们需要满足所有子矩阵的值的平方最小

求子矩阵的值的平方,我们会在代码中的get()方法实现

f[x1][y1][x2][y2][k]状态表示:

1) 集合:划分到k−1k−1个的子矩阵,是以(x1,y1)(x1,y1)为左上角,(x2,y2)(x2,y2)为右下角

2) 属性:σ=∑ni=1(xi−x¯¯¯)2n−−−−−−−−−−−−√σ=∑i=1n(xi−x¯)2n最小值 Min

状态计算:

1)集合划分:

/*
1.1) 横着切:
1.1.1)以(x1, y1)为左上角,以(x1, y2)为右下角 或 以(x1+1, y1)为左上角,以(x2, y2)为右下角
1.1.2)以(x1, y1)为左上角,以(x1+1, y2)为右下角 或 以(x1+2, y1)为左上角,以(x2, y2)为右下角

1.1.i)以(x1, y1)为左上角,以(x1+i, y2)为右下角 或 以(x1+i+1, y1)为左上角,以(x2, y2)为右下角

1.1.x2-1)以(x1, y1)为左上角,以(x2-1, y2)为右下角 或 以(x2, y1)为左上角,以(x2, y2)为右下角

1.2) 竖着切:
1.2.1)以(x1,y1)为左上角,以(x2, y1)为右下角 或 以(x1, y1)为左上角,以(x2, y2)为右下角
1.2.2)以(x1,y1)为左上角,以(x2, y1+1)为右下角 或 以(x1, y1+2)为左上角,以(x2, y2)为右下角

1.2.i)以(x1,y1)为左上角,以(x2, y1+i)为右下角 或 以(x1, y1+i+1)为左上角,以(x2, y2)为右下角

1.2.y2-1)以(x1, y1)为左上角,以(x2, y2-1)为右下角 或 以(x1,y2-1)为左上角,以(x2, y2)为右下角
*/
2)状态转移方程

模拟上述集合的划分枚举所有的区间即可
由于dp的方程维数过大,写 55 重迭代太麻烦了,这题采用记忆化搜索

Code

#include <iostream>
#include <cstring>
#include <cmath>
using namespace std;

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

int n, m = 8;
int s[N][N];
double f[N][N][N][N][M];
double X;   //均值x拔

double get(int x1, int y1, int x2, int y2) {
    //根号下,求和符号内的部分
    double sum = s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1] - X;
    //还要平方
    return (double)sum * sum;
}
double dp(int x1, int y1, int x2, int y2, int k) {
    auto &t = f[x1][y1][x2][y2][k];
    if (t >= 0) return t;
    if (k == 1) return get(x1, y1, x2, y2);

    t = INF; //求最小值要初始化成最大值
    //横着切
    for (int i = x1; i < x2; ++i) {
        t = min(t, dp(x1, y1, i, y2, k - 1) + get(i + 1, y1, x2, y2));
        t = min(t, dp(i + 1, y1, x2, y2, k - 1) + get(x1, y1, i, y2));
    }
    //竖着切
    for (int i = y1; i < y2; ++i) {
        t = min(t, dp(x1, y1, x2, i, k - 1) + get(x1, i + 1, x2, y2));
        t = min(t, dp(x1, i + 1, x2, y2, k - 1) + get(x1, y1, x2, i));
    }
    return t;
}

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;
    //记忆化搜索,初始化成NaN
    memset(f, -1, sizeof f);

    //这个就是\sigma的完整计算公式
    printf("%.3lf\n", sqrt(dp(1, 1, m, m, n) / n));
    return 0;
}

作者:彩色铅笔
链接:https://www.acwing.com/solution/content/34369/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值