区间DP问题讲解

区间动态规划问题

1、石子合并

1.题目

n n n 堆石子排成一排,第 i i i 堆石子有 a i a_i ai 颗,每次我们可以选择相邻的两堆石子合并,代价是两堆石子数目的和,现在我们要一直合并这些石子,使得最后只剩下一堆石子,问总代价最少是多少?

输入格式

第一行一个数字 n。

接下来一行 n个整数 a1,a2,…,an。

输出格式

一行一个整数,表示答案。

2.分析1

考虑最后的两堆石子,存在一个分界线 x x x,其中一堆事第 1 1 1 堆石子到第 x x x 堆石子合并得到的结果,另一堆是第 x + 1 x+1 x+1 堆石子到第 n n n 堆石子合并得到的结果

x x x 确定的时候,总代价=合并第 1 1 1 堆到第 x x x 堆石子的代价+合并第 x + 1 x+1 x+1 堆石子到第 n n n 堆石子的代价+总石子数。

不知道分界线在哪,所以需要枚举。

假如我们当前需要合并第 l l l 到 第 r r r 堆石子,并且要求出最小代价,我们只要枚举分界线 m m m 在哪,从 r − l r-l rl 中方案中取代价最小的就可。

需要一直递归,最后只剩下一堆石子即可。

但是因为一直递归, [ l , r ] [l,r] [l,r] 会被重复计算很多次。所以要记录一下。

可以看成记忆化搜索。

3.代码1

#include <bits/stdc++.h>
using namespace std;
int n, a[501], s[501],f[501][501];
int solve(int l,int r)
{
    if(f[l][r]!=-1)
        return f[l][r];
    if(l==r)
        return f[l][r]=0;
    int ans = 1 << 30;
    for (int m = l; m < r;m++)
        ans = min(ans, solve(l, m) + solve(m + 1, r));
    return f[l][r]=ans + s[r] - s[l - 1];
}
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n;i++)
        scanf("%d", &a[i]);
    for (int i = 1; i <= n;i++)
        s[i] = s[i - 1] + a[i];
    memset(f, 255, sizeof(f));
    printf("%d\n", solve(1, n));
}

4.分析2

利用动态规划求解。我们用 f [ i , j ] f[i,j] f[i,j]来表示合并区间 [ i , j ] [i,j] [i,j] 的最小代价,求解 [ i , j ] [i,j] [i,j] 的最小代价,要先计算 [ i , k ] [i,k] [i,k] [ k + 1 , j ] [k+1,j] [k+1,j]

5.代码2

#include <bits/stdc++.h>
using namespace std;
int n, a[501], s[501], f[501][501];
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n;i++)
        scanf("%d", &a[i]);
    for (int i = 1; i <= n;i++)
        s[i] = s[i - 1] + a[i];
    memset(f, 127, sizeof(f));
    for (int i = 1; i <= n;i++)
        f[i][i] = 0;
    for (int i = 1; i < n;i++)//枚举左右端点的差值
        for (int j = 1; j <= n-i;j++)//枚举左端点
            for (int k = j; k < j + i;k++)
                f[j][j + i] = min(f[j][j + i], f[j][k] + f[k + 1][j + i] + s[j + i] - s[j - 1]);
    printf("%d\n", f[1][n]);
}

2、括号序列

1.题目

给定一个长度为 n n n 的字符串 s s s,字符串由 (, ), [, ] 组成,问其中最长的合法子序列有多长?也就是说,我们要找到最大的 m m m,使得存在 i 1 , i 2 , … , i m i_1,i_2,…,i_m i1,i2,,im 满足 1 ≤ i 1 < i 2 < ⋯ < i m ≤ n 1≤i_1<i_2<⋯<i_m≤n 1i1<i2<<imn 并且 s i 1 s i 2 … s i m s_{i_1}s_{i_2}…s_{i_m} si1si2sim 是一个合法的括号序列。

合法的括号序列的定义是:

  • 空串是一个合法的括号序列。

  • 若 A 是一个合法的括号序列,则 (A), [A] 也是合法的括号序列。

  • 若 A, B 都是合法的括号序列,则 AB 也是合法的括号序列。

输入格式

第一行一个整数 n。

接下来一行,一个长度为 n的字符串 s。

输出格式

一个数,表示答案。

2.分析

f [ i ] [ j ] f[i][j] f[i][j] 表示 s i , s i + 1 , … , s j s_i,s_{i+1},\dots,s_j si,si+1,,sj 中最长的合法子序列的长度

3 3 3 种情况:
1. f [ i ] [ j ] = m a x ( f [ i ] [ j ] , f [ i + 1 ] [ j ] ) f[i][j]=max(f[i][j],f[i+1][j]) f[i][j]=max(f[i][j],f[i+1][j])
2. f [ i ] [ j ] = m a x ( f [ i ] [ j ] , f [ i ] [ j − 1 ] ) f[i][j]=max(f[i][j],f[i][j-1]) f[i][j]=max(f[i][j],f[i][j1])
3.如果 s i s_i si s j s_j sj 匹配, f [ i ] [ j ] = m a x ( f [ i ] [ j ] , f [ i + 1 ] [ j − 1 ] + 2 ) f[i][j]=max(f[i][j],f[i+1][j-1]+2) f[i][j]=max(f[i][j],f[i+1][j1]+2)
4.AB:枚举AB的分界线 k k k f [ i ] [ j ] = m a x ( f [ i ] [ j ] , f [ i ] [ k ] + f [ k + 1 ] [ j ] ) f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]) f[i][j]=max(f[i][j],f[i][k]+f[k+1][j])

前两点不用考虑,因为第四条包含了。

3.代码

#include <bits/stdc++.h>
using namespace std;
int n, f[501][501];
char str[511];
int main()
{
    scanf("%d%s", &n, str + 1);
    memset(f, 0, sizeof(f));
    for (int i = 1; i < n; i++)
        for (int j = 1; j <= n - i; j++)
        {
            if ((str[j] == '(' && str[j + i] == ')') || (str[j] == '[' && str[j + i] == ']'))
                f[j][j + i] = f[j + 1][j + i - 1] + 2;
            for (int k = j; k < j + i; k++)
                f[j][j + i] = max(f[j][j + i], f[j][k] + f[k + 1][j + i]);
        }
    printf("%d\n", f[1][n]);
}

3.石子合并2

1.题目

n n n 堆石子围成一个圈,第 i i i 堆石子有 a i a_i ai 颗,每次我们可以选择相邻的两堆石子合并,代价是两堆石子数目的和,现在我们要一直合并这些石子,使得最后只剩下一堆石子,问总代价最少是多少?

输入格式

第一行一个数字 n。

接下来一行 n个整数 a1,a2,…,an。

输出格式

一行一个整数,表示答案。

2.分析

考虑一条长度为 2 n 2n 2n 的链,是由两个 a a a 数组连接起来,也就是说, > n >n >n 的位置 i i i 对应了原来的第 i − n i-n in 堆石子。

3.代码

#include <bits/stdc++.h>
using namespace std;
int n, a[501], f[501][501],s[501];
int main()
{
    cin >> n;
    for (int i = 1; i <= n;i++){
        cin >> a[i];
        a[n + i] = a[i];
    }
    n *= 2;
    for (int i = 1; i <= n;i++)
        s[i] = s[i - 1] + a[i];
    memset(f, 127, sizeof f);
    for (int i = 1; i <= n;i++)
        f[i][i] = 0;
    for (int i = 1; i < n / 2;i++)
        for (int j = 1; j <= n - i;j++)
            for (int k = j; k < j + i;k++)
                f[j][j + i] = min(f[j][j + i], f[j][k] + f[k + 1][j + i] + s[j + i] - s[j - 1]);
    int ans = 1 << 30;
    for (int i = 1; i <= n / 2;i++)
        ans = min(ans, f[i][i + n / 2 - 1]);
    printf("%d\n", ans);
}

四、凸多边形的划分

1.题目

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

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

输入格式

第一行包含整数 N,表示顶点数量。

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

输出格式

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

2.分析

划分出新的三角形,两侧是没有关系的。

f [ L , R ] f[L,R] f[L,R] 表示所有将 ( L , L + 1 ) , ( L + 1 , L + 2 ) , . . . , ( R − 1 , R ) , ( R , L ) (L,L+1),(L+1,L+2),...,(R-1,R),(R,L) (L,L+1),(L+1,L+2),...,(R1,R),(R,L)这个多边形划分成三角形的方案。就等于 f [ L , K ] + f [ K , R ] + w [ L ] ∗ w [ K ] ∗ w [ R ] f[L,K]+f[K,R]+w[L]*w[K]*w[R] f[L,K]+f[K,R]+w[L]w[K]w[R]

这道题需要高精度。如果不用高精的话状态转移方程为 f [ l ] [ r ] = m i n ( f [ l ] [ r ] , f [ l ] [ k ] + f [ k ] [ r ] + w [ l ] ∗ w [ k ] ∗ w [ r ] ) f[l][r] = min(f[l][r], f[l][k] + f[k][r] + w[l] * w[k] * w[r]) f[l][r]=min(f[l][r],f[l][k]+f[k][r]+w[l]w[k]w[r])

3.代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
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;
}

五、加分二叉树

1.题目

设一个 n n n 个节点的二叉树 tree 的中序遍历为 ( 1 , 2 , 3 , … , n ) (1,2,3,…,n) 1,2,3,,n,其中数字 1 , 2 , 3 , … , n 1,2,3,…,n 1,2,3,,n 为节点编号。

每个节点都有一个分数(均为正整数),记第 i i i 个节点的分数为 d i d_i di,tree 及它的每个子树都有一个加分,任一棵子树 subtree(也包含tree本身)的加分计算方法如下:

subtree的左子树的加分 × subtree的右子树的加分 + subtree的根的分数

若某个子树为空,规定其加分为 1 1 1。叶子的加分就是叶节点本身的分数,不考虑它的空子树。

试求一棵符合中序遍历为 ( 1 , 2 , 3 , … , n ) (1,2,3,…,n) 1,2,3,,n且加分最高的二叉树tree。

要求输出:

(1)tree的最高加分

(2)tree的前序遍历

输入格式

第 1 行:一个整数 n,为节点个数。

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

输出格式

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

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

2.分析

f [ L , R ] f[L,R] f[L,R] 表示所有中序遍历是 [ L , R ] [L,R] [L,R] 这一段的二叉树的集合。根节点为 k k k 的数,左右两边怎么选是没关系的,所以左右两边分别取最大值。 g [ L , R ] g[L,R] g[L,R] 记录一下根节点。

如果要输出字典序最小的,递归的时候只要去找到最左侧的最大值。

3.代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 30;
int n;
int w[N];
int f[N][N], g[N][N];
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++)
        cin >> 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] = w[l];
                g[l][r] = l;
            }
            else
            {
                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 (f[l][r] < score) // 确保按字典序
                    {
                        f[l][r] = score;
                        g[l][r] = k;
                    }
                }
            }
        }
    cout << f[1][n] << endl;
    dfs(1, n);
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值