算法竞赛进阶指南 0x53 区间DP

总论

线性DP:从初态开始,沿着阶段的扩张,向某一个方向扩张,知道求出答案。

区间DP是一种特殊的线性DP,同时也与线段树等树形结构具备相同的特征。

阶段:区间的长度(一个转态要从比他小的区间并且包含于他的区间递推过来)

转态:左端点,右端点。

注意:先是阶段,然后状态,最后决策

AcWing\282. 石子合并

设有 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。

问题是:找出一种合理的方法,使总的代价最小,输出最小代价。

输入格式

第一行一个数 N 表示石子的堆数 N

第二行 N 个数,表示每堆石子的质量(均不超过 1000)。

输出格式

输出一个整数,表示最小代价。

数据范围

1≤N≤300

输入样例:

4
1 3 5 2

输出样例:

22

状态表示:在任意时刻,任意一堆石头全部可以使用两个数字来进行唯一标识
L和R,表示这一堆石头是由L到R之间的石头合并而来的。

对于每一堆石头,合并而来的方法有多种,是f[i,j] = f[i,k]和f[k+1, j],其中 i ≤ k ≤ j − 1 i \le k \le j-1 ikj1.

#include <bits/stdc++.h>
using namespace std;
int a[305];
int s[305];
int f[305][350];
int main()
{
    int n;
    scanf("%d", &n);
    memset(f, 0x3f, sizeof(f));
    for(int i = 1; i <= n; i++) f[i][i] = 0;
    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];
    for(int len = 2; len <= n; len++)
        for(int l = 1; l <= n-len+1; l++)
        {
            int r = l+len-1;
            for(int k = 1; k < r; k++)
            {
                f[l][r] = min(f[l][r], f[l][k] + f[k+1][r] + s[r]-s[l-1]);
            }
        }
    printf("%d", f[1][n]);
    return 0;
}

AcWing283. 多边形

“多边形游戏”是一款单人益智游戏。游戏开始时,给定玩家一个具有 N 个顶点 N 条边(编号 1∼N)的多边形,如图 1 所示,其中 N=4。

每个顶点上写有一个整数,每个边上标有一个运算符 +(加号)或运算符 *(乘号)。

1179_1.jpg

第一步,玩家选择一条边,将它删除。

接下来在进行 N−1 步,在每一步中,玩家选择一条边,把这条边以及该边连接的两个顶点用一个新的顶点代替,新顶点上的整数值等于删去的两个顶点上的数按照删去的边上标有的符号进行计算得到的结果。

下面是用图 1 给出的四边形进行游戏的全过程。

1179_2.jpg

最终,游戏仅剩一个顶点,顶点上的数值就是玩家的得分,上图玩家得分为 0。

请计算对于给定的 N 边形,玩家最高能获得多少分,以及第一步有哪些策略可以使玩家获得最高得分。

输入格式

输入包含两行,第一行为整数 N

第二行用来描述多边形所有边上的符号以及所有顶点上的整数,从编号为 1 的边开始,边、点、边…按顺序描述。

其中描述顶点即为输入顶点上的整数,描述边即为输入边上的符号,其中加号用 t 表示,乘号用 x 表示。

输出格式

输出包含两行,第一行输出最高分数。

在第二行,将满足得到最高分数的情况下,所有的可以在第一步删除的边的编号从小到大输出,数据之间用空格隔开。

数据范围

3≤N≤50, 数据保证无论玩家如何操作,顶点上的数值均在 [−32768,32767] 之内。

输入样例:

4
t -7 t 4 x 2 x 5

输出样例:

33
1 2

在完成后了上一道题目之后,这一道题目看似并没有那么难做了。

这一道题目数据这么水,显然是使用暴力来求解。

我可以遍历所有需要删除的边,然后就得到了这n个点并排放置的情况。

同理,任意一个阶段,任意一个石头可以使用左右边界来进行描述。

时间复杂度应该是N4.

but:

在动态规划中一定要注意:

  1. 查看是否满足最优子结构问题。
    在这一道题目中,由于负数以及乘法的存在,导致不一定是最大的乘以最大的是最大的。
    e g .image
    所以,
    x 1 ≤ z ≤ y 1 x 2 ≤ z ′ ≤ y 2 x_1\le z\le y_1\\ x_2\le z'\le y_2\\ x1zy1x2zy2
    根据原来的最大值以及最小值就可以推出来新的正确的最大值以及最小值,满足最优子结构。

  2. 还有一个就是关于环的问题,理论上,这一道题目可以使用N4水过去,但是一定要注意:
    image

#include <bits/stdc++.h>
using namespace std;
#define N 106
int a[N];
char op[N];
int f[N][N];
int g[N][N];
void pro(int x1, int y1, int x2, int y2,char op, int &maxx, int &minn)
{
    if(op == 'x')
    {
        int t1 = x1*x2;
        int t2 = x1*y2;
        int t3 = y1*x2;
        int t4 = y1*y2;
        maxx = max({t1, t2, t3, t4});
        minn = min({t1, t2, t3, t4});
        return;
    }
    else
    {
        maxx = y1+y2;
        minn = x1+x2;
    }
}
int main()
{
    int n;
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
    {
        char buf[12];
        scanf("%s%d",buf, a+i);
        op[i] = buf[0];
    }
    for(int i = 1; i <= n; i++)
    {
        a[i+n] = a[i];
        op[i+n] = op[i];
    }
    memset(f, 0xcf, sizeof(f));
    memset(g, 0x3f, sizeof(g));
    for(int i = 1; i <= 2*n; i++)
    {
        f[i][i] = a[i];
        g[i][i] = a[i];
    }
    for(int len = 1; len <= n; len++)
        for(int l = 1; l <= 2*n-len+1; l++)
        {
            int r = l+len - 1;
            for(int k = l; k < r; k++)
            {
                int maxx;
                int minn;
                pro(g[l][k], f[l][k], g[k+1][r], f[k+1][r], op[k+1], maxx, minn);
                f[l][r] = max(f[l][r], maxx);
                g[l][r] = min(g[l][r], minn);
            }
        }
        int ret = INT_MIN;
        for(int i = 1; i <= n; i++)
            ret = max(ret, f[i][i+n-1]);
        printf("%d\n", ret);
        for(int i = 1; i <= n; i++)
        {
            if(f[i][i+n-1] == ret) printf("%d ", i);//我之前的方法在嘴和一步绕的太多。其实就这样朴素一点就好。
        }

    
    return 0;
}

AcWing\284. 金字塔

(难度: 中等)

虽然探索金字塔是极其老套的剧情,但是有一队探险家还是到了某金字塔脚下。

经过多年的研究,科学家对这座金字塔的内部结构已经有所了解。

首先,金字塔由若干房间组成,房间之间连有通道。

如果把房间看作节点,通道看作边的话,整个金字塔呈现一个有根树结构,节点的子树之间有序,金字塔有唯一的一个入口通向树根。

并且,每个房间的墙壁都涂有若干种颜色的一种。

探险队员打算进一步了解金字塔的结构,为此,他们使用了一种特殊设计的机器人。

这种机器人会从入口进入金字塔,之后对金字塔进行深度优先遍历。

机器人每进入一个房间(无论是第一次进入还是返回),都会记录这个房间的颜色。

最后,机器人会从入口退出金字塔。

显然,机器人会访问每个房间至少一次,并且穿越每条通道恰好两次(两个方向各一次), 然后,机器人会得到一个颜色序列。

但是,探险队员发现这个颜色序列并不能唯一确定金字塔的结构。

现在他们想请你帮助他们计算,对于一个给定的颜色序列,有多少种可能的结构会得到这个序列。

因为结果可能会非常大,你只需要输出答案对109 取模之后的值。

输入格式

输入仅一行,包含一个字符串 S,长度不超过 300,表示机器人得到的颜色序列。

输出格式

输出一个整数表示答案。

输入样例:

ABABABA

输出样例:

5

这道题目考察的知识点:

欧拉序:

与数据结构中的深度优先遍历不同,这里的遍历是指回来也算一个。

image
版权:https://www.acwing.com/video/470/

在题目中也给出了解释,由于每一条边都会被走两次,并且根节点还会有额外的一次,所以序列的长度是 2 n − 1 2n-1 2n1

在树形结构与字符串中建立联系:

在对树的考虑中,常常用到分治,就是考虑它的子树,而每一棵子树都对应着一个区间。这样就和字符串建立了联系。

枚举策略

image

在两个A中间的A可能是根节点A,也可能是字树中的A。

  1. 暴力枚举,but 如果是AAAAAAAAAAAAA需要枚举的时间复杂度就是2n.
  2. 随便(或者把中间的记为有没有划分,然后两边的是可能划分,也可能不划分,这样会产生重复)
  3. 广义表的方式来,左区间仅仅对应一个子树,右区间可能是一个,也可能是多个。

代码:

  • 记忆化搜索(初始值位于终止条件中)
  • 循环(一开始赋一个初始值)

我采用记忆化搜索:

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

#define N 305
const int mod = 1e9;

int f[N][N];
char s[N];

int solve(int l, int r)
{
    if(l > r) return 0;//终止,不合法		这一步其实可以写,也可以不写。
    if(l == r) return 1;//终止,初始条件
    if(f[l][r] != -1) return f[l][r];//终止,已经计算过,无需再次计算
    //必须从2开始,如果从1开始,对于AAAAAA,就会发现左半部分没有子树,显然不符合
    int ret = 0;
    for(int i =l + 2; i <= r; i++)//右边界一定要到r,这个是只有一颗子树的情况。
    {
        if(s[i] == s[r] && s[i] == s[l])//一定要两个全部相等!!!
        {
            ret =( (long long)solve(l+1, i-1) * solve(i, r) + ret)%mod;
        }
    }
    f[l][r] = ret;
    return ret;
}
int main()
{
    scanf("%s", s+1);
    memset(f, -1, sizeof(f));
    int n = strlen(s+1);
    
    printf("%d", solve(1, n));
    return 0;
}

DEBUG记录:

这道题目真的是一波三折。

  1. 本来是scanf("%s", s+1),可是我写成scanf("%s", s)
  2. 必须是左右区间相等然后才可以这样来s[i] == s[r] && s[i] == s[l]
    如果是ABABACA,在第一次脱去A,得到BABAC显然这个不是一棵子树!!!
  3. 注意:我一开始打印的是f[1][n]其实我并没有注意到:当l == r的时候,返回的是1,但是没有更新f[l][r],导致错误。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值