区间DP总结

2.1.3 区间DP

2.1.3.1 基本概念

​ 区间DP,顾名思义是在区间上DP,它以“区间长度”作为DP的“阶段”,使用两个坐标(区间的左、右端点)描述每个维度。它的主要思想就是先在小区间进行DP得到最优解,然后再利用小区间的最优解合并求大区间的最优解。

2.1.3.2 核心思路

​ 既然让我求解在一个区间上的最优解,那么我把这个区间分割成一个个小区间,求解每个小区间的最优解,再合并小区间得到大区间即可。所以在代码实现上,我可以枚举区间长度 l e n len len 为每次分割成的小区间长度 ( ( (由短到长不断合并 ) ) ),内层枚举该长度下可以的起点,自然终点也就明了了。然后在这个起点终点之间枚举分割点,求解这段小区间在某个分割点下的最优解。

下面给出代码 : : :

for (int i = 1; i <= n; i++) {
    dp[i][i] = 0;  //初始值
}
for (int i = 2; i <= n; i++) {              //区间长度
    for (int l = 1; l <= n - i + 1; l++) {  //左端点
        int r = l + i - 1;                  //右端点
        for (int j = l; j < r; j++) {       //划分
            dp[l][r] = min(dp[l][r], dp[l][j] + dp[j + 1][r] + w[l][r]);
        }
    }
}
2.1.3.3 石子合并

​ 有 n n n堆石子排成一条直线,每堆石子有一定的重量。现在要合并这些石子成为一堆石子,但是每次只能合并相邻的两堆。每次合并需要消耗一定的体力,该体力为所合并的两堆石子的重量之和。问最少需要多少体力才能将n堆石子合并成一堆石子?

假设只有2堆石子,显然只有1种合并方案。

在这里插入图片描述

​ 不管怎么合并,总之最后总会归结为2堆,如果我们把最后两堆分开,左边和右边无论怎么合并,都必须满足最优合并方案,整个问题才能得到最优解。

m [ i ] [ j ] = { 0 i = j min ⁡ ( m [ i ] [ k ] + m [ k + 1 ] [ j ] ) + w [ i ] [ j ] i < j m[i][j]=\begin{cases}0&i=j\\ \min(m[i][k]+m[k+1][j])+w[i][j]&i<j\end{cases} m[i][j]={0min(m[i][k]+m[k+1][j])+w[i][j]i=ji<j

预处理 w [ i ] [ j ] : w[i][j]: w[i][j]:
先算出前缀和 s [ i ] s[i] s[i] ,然后 w [ i ] [ j ] = s [ j ] – s [ i − 1 ] w[i][j] = s[j] – s[i-1] w[i][j]=s[j]s[i1]
因此, w [ i ] [ j ] w[i][j] w[i][j]可以不事先存放,在DP​时直接 O ( 1 ) O(1) O(1) 计算。

#include <bits/stdc++.h>
using namespace std;
const int M = 1e2 + 5;
int a[M];
long long sum[M], dp[M][M];
int n;
int main() {
    memset(dp, 0x3f3f3f3f, sizeof(dp));
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i]);
        sum[i] = sum[i - 1] + a[i];
    }
    for (int i = 1; i <= n; i++) dp[i][i] = 0;
    for (int i = 2; i <= n; i++) {
        for (int l = 1; l <= n - i + 1; l++) {
            int r = l + i - 1;
            for (int j = l; j < r; j++) {
                dp[l][r] = min(dp[l][r], dp[l][j] + dp[j + 1][r] + sum[r] - sum[l - 1]);
            }
        }
    }
    printf("%lld", dp[1][n]);
    return 0;
}
2.1.3.4 戳西瓜

​ 有 n 个西瓜,编号为 0 0 0 n − 1 n-1 n1 ,每个西瓜上都标有一个数字,这些数字存在数组 n u m s nums nums 中。现在要求你戳破所有的西瓜。每当你戳破一个西瓜 i i i时,你可以获得 n u m s [ l e f t ] ∗ n u m s [ i ] ∗ n u m s [ r i g h t ] nums[left]*nums[i]*nums[right] nums[left]nums[i]nums[right] 个硬币。这里的 l e f t left left r i g h t right right 代表和 i i i 相邻的两个西瓜的序号。注意当你戳破了西瓜 i i i 后,西瓜 l e f t left left 和西瓜 r i g h t right right 就变成了相邻的西瓜。求所能获得硬币的最大数量。

数据范围与提示 : : :你可以假设 n u m s [ − 1 ] = n u m s [ n ] = 1 nums[-1]=nums[n]=1 nums[1]=nums[n]=1 ,但注意它们不是真实存在的西瓜,所以并不能被戳破。

假设 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示开区间 ( i , j ) (i,j) (i,j) 内你能拿得最多的硬币。

那么在这个情况下 ( i , j ) (i,j) (i,j)开区间得到的金币可以由 d p [ i ] [ j ] dp[i][j] dp[i][j] d p [ k ] [ j ] dp[k][j] dp[k][j] 进行转移,如果你此刻选择戳爆 K K K ,那么你得到的金币数量就是 d p [ i ] [ k ] + n u m s [ i ] ∗ n u m s [ k ] ∗ n u m s [ j ] + d p [ k ] [ j ] dp[i][k]+nums[i]*nums[k]*nums[j]+dp[k][j] dp[i][k]+nums[i]nums[k]nums[j]+dp[k][j]

#include <bits/stdc++.h>
using namespace std;
const int M = 505;
int a[M], dp[M][M];
int n;
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    a[0] = a[n + 1] = 1;
    int m = n + 2;
    for (int i = 2; i < m; i++) {
        for (int l = 0; l + i < m; l++) {
            int r = l + i;
            for (int j = l + 1; j < r; j++)
                dp[l][r] = max(dp[l][r], dp[l][j] + dp[j][r] + a[l] * a[j] * a[r]);
        }
    }
    printf("%d", dp[0][m - 1]);
    return 0;
}
2.1.3.5 「COCI 2010.03.06」ZUMA

Mirko 将 n n n 颗弹子排成一排,依次编号为 1... N 1...N 1...N。 号弹子的颜色为 c [ i ] c[i] c[i] 。他发现,如果他触摸 ≥ k \ge k k 颗连续的弹子,且这些弹子的颜色相同,魔法会使这些弹子消失;此后,这 k k k 颗弹子前面的弹子便与这 k k k 颗弹子后面的弹子相邻。

Mirko 家里有很多弹子,他想在这 颗弹子之间(也可以在开头的弹子前面或末尾的弹子后面)插入尽可能少的弹子,使得这 k k k 颗弹子+插入的所有弹子消失。

这到题略微有一点变化。

我们设 d p [ i ] [ j ] [ s u m ] dp[i][j][sum] dp[i][j][sum] 表示消除区间 [ i , j ] [i,j] [i,j] 在左边添加了 s u m sum sum 个珠子的总添加珠子数。

答案就是 d p [ 1 ] [ n ] [ 0 ] dp[1][n][0] dp[1][n][0]

转移方程如下 : : :

  1. s u m < k − 1 sum<k−1 sum<k1 (不能消)时再加一个,即 d p [ i ] [ j ] [ s u m ] = m i n ( f [ i ] [ j ] [ s u m ] , f [ i ] [ j ] [ s u m + 1 ] + 1 ) dp[i][j][sum]=min(f[i][j][sum],f[i][j][sum+1]+1) dp[i][j][sum]=min(f[i][j][sum],f[i][j][sum+1]+1)
  2. s u m = k − 1 sum=k−1 sum=k1 时直接消掉,即 d p [ i ] [ j ] [ s u m ] = d p [ i + 1 ] [ j ] [ 0 ] dp[i][j][sum]=dp[i+1][j][0] dp[i][j][sum]=dp[i+1][j][0]
  3. i i i i + 1 i+1 i+1 的颜色相同时,可以把 i i i 加到 i + 1 i+1 i+1 的整体中,即 d p [ i ] [ j ] [ s u m ] = d p [ i + 1 ] [ j ] [ s u m + 1 ] dp[i][j][sum]=dp[i+1][j][sum+1] dp[i][j][sum]=dp[i+1][j][sum+1]
#include <bits/stdc++.h>
using namespace std;

int a[105], dp[105][105][105];
int n, k;

int dfs(int l, int r, int sum) {
    if (l > r)  //边界
        return 0;
    if (dp[l][r][sum] != -1)  //记忆化
        return dp[l][r][sum];
    int ans = 0x3f3f3f3f;
    dp[l][r][sum] = 0x3f3f3f3f;
    if (sum < k - 1)  //第一种情况
        ans = min(ans, dfs(l, r, sum + 1) + 1);
    else if (sum == k - 1)  //直接消掉
        ans = dfs(l + 1, r, 0);
    for (int i = l + 1; i <= r; i++)
        if (a[i] == a[l])  //加入后面形成一个整体
            ans = min(ans, dfs(l + 1, i - 1, 0) + dfs(i, r, min(k - 1, sum + 1)));
    dp[l][r][sum] = ans;
    return ans;
}

int main() {
    scanf("%d %d", &n, &k);
    memset(dp, -1, sizeof(dp));
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    dfs(1, n, 0);
    printf("%d", dp[1][n][0]);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值