【题解】P1880 [NOI1995] 石子合并

这篇博客探讨了一种区间动态规划的解决方案,涉及如何合并最小和最大堆的策略。通过枚举所有可能的区间,利用状态转移方程进行计算,并处理环形数据结构。博主介绍了将环形结构转化为链状结构的方法,以覆盖所有情况,并提供了C++代码示例。文章强调了初始化、前缀和以及区间和的计算技巧。

居然是个环= =…

思路

区间dp,要看怎么合并最小和最大,我们自然就想到枚举所有可能的区间!难点就是状态转移方程啦(☞完全不会)。

状态转移方程: dp[i][j] = dp[i][k] + dp[k + 1][j] + sum[i][j] (i <= k < j)

dp[i][j] -> i 到 j 的区间合并,sum[i][j] -> i 到 j 堆的值的和

直接举例,按照样例4 5 9 4这样的四颗石子来!
刚开始的时候每个dp区间都是自己到自己,初始化为 0dp[i][i] = 0
1、 [1] [2]两堆合并dp[1][2] = dp[1][1] + dp[2][2] + sum[1][2],即[1]自己的区间加上[2]自己的区间再加上 1~2 区间的石头的值的和,我们就得出了dp[1][2] = 9
2、 [1] [2] [3]三堆合并,此时存在两种情况:

dp[1][3](情况1) = dp[1][2] + dp[3][3] + sum[1][3]

在这种情况下我们是用[1] [2]先进行合并,而后再合成[3]。

dp[1][3](情况2) = dp[1][1] + dp[2][3] + sum[1][3]

在这种情况下我们是用[2] [3]先进行合并,而后再合成[1]。
同理求得:

dp[2][3] = dp[2][2] + dp[3][3] + sum[2][3] = 14

经过计算:

dp[1][3](情况1) = 9 + 18 = 27
dp[1][3](情况2) = 14 + 18 = 32

这时候其实我们已经可以一下求出两种结果了。
此时的max = 32min = 27
3、重复上述步骤继续合并!直到找区间 i ~ j ( i + j - 1 = n )最大和最小值!

要实现上述的计算要怎样进行枚举呢?

1、 最外面一层 len 从 2 到 n 表示区间[i, j]的长度
2、 第二层枚举起点 i 的位置,i 枚举 从 1 开始到 n - len 结束
3、 因为有了起点 i 和区间的长度len,所以我们的 j 其实就已经可以得出了 j = i + len。
4、 在len长的区间[i , j]枚举每个分割可能的位置 k ,使得 dp[i, j] = dp[i , k] + dp[k + 1, j];所以我们的 k 从 i 开始枚举,到 j 结束( i <= k < j )
5、 需要注意的两点是,First. 由于 i 从 1 开始, j = i + len = 2,此时区间的长度就已经是 2 了,而枚举len = 3时, i(max) = n - 3 = 1时(样例n = 4),此时 j = len + i = 4,所以我们 len 的结尾是 < n 而不是 = n ,因此我们枚举 len 的时候实际是从 1 <= len < n。
Second. 枚举 k 的时候我们从 k = i 开始枚举的,但我们枚举不能到 k = j 的时候。

dp[i, j] = dp[i , k] + dp[k + 1, j];

由这个式子可以看出, dp[k + 1, j]中如果 k = j , 那么k + 1就会大于 j 了!!!所以这就有问题了!因此( i <= k < j )。
6、sum[i][j]我们用来表示区间 i ~ j 的和,而在实际的使用中,我们可以用前缀和来使其简化成一维,方便计算!
我们还是看到样例:

4 5 9 4

sum[0]= 0

sum[1]= 4

sum[2] = sum[1] + 5 = 9

sum[3] = sum[2] + 9 = 18

sum[4] = sum[3] + 4 = 22

我们如果要求sum[1][3]的话我们只要用sum[3] - sum[1 - 1] = sum[3] - sum[0] = 18 就可以得到我们要的结果啦!
由此我们可以得出sum[i][j] = sum[j] - sum[i - 1]
7、这是最要命的一点!!!!!真的超毒的!!!这是个环!环!环!也就是说4 5 9 4可以变成5 9 4 49 4 4 54 4 5 9一共由四种情况!!!那这样要怎么处理呢?
其实也简单,环转链即可,n 变成 2n 把他复制一遍拉长,变成

4 5 9 4 4 5 9 4

这样,我们就区间[i ,j]就能包括所有情况了!,还有求最小值的时候记得把dp[i][j]初始化为一个很大很大的值,不然得出结果就全都是 0 了!
可以用下面这个统一初始化的方法
#include <cstring>
//初始化dp里每个字节为 0x3f,dp[i][j] = 0x3f3f3f3f = 1061109567
memset(dp, 0x3f, sizeof(dp));
//初始化dp里每个字节为0
memset(dp, 0, sizeof(dp));

以上!题解完成!上代码!

#include <iostream>
using namespace std;
const int N = 300;
//相当于1 * 2^30, << 是左移位运算,每次都移动一位都相当于×2
const int INF = 1 << 30;
int n, dp[N][N], dp2[N][N];
int sum[N], s[N];

int main()
{
  cin >> n;
  for (int i = 1; i <= n; i++) 
  {
    cin >> s[i];
    //为n~2n的前缀和做准备
    s[i + n] = s[i];
  }
  // n * 2 是因为要化环为链
  for(int i = 1; i <= n * 2; i++)
  {
    //求前缀和,sum[i, j] -> sum[i][i - j + 1] -> sum[j] - sum[i - 1];
    sum[i] = s[i] + sum[i - 1];
    //虽然已经全局初始化为0了但是题解还是写下吧
    dp[i][i] = 0; dp2[i][i] = 0;
  }
  //len:i 到 j 的距离
  for(int len = 1; len < n; len++)
  {
    //从第 i 堆开始
    for (int i = 1; i <= (n * 2 - len); i++)
    {
      //到第 j 堆结束
      int j = i + len;
      //初始化求最小的区间和为 INF(因为一开始是 0 ,不放大就一直是 0 了)
      dp[i][j] = INF;
      // i 和 j 之间用 k 分割
      for(int k = i; k < j; k++)
      {
        //第 i 堆到第 j 堆的合并
        dp[i][j] = min (dp[i][j], dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]);
        dp2[i][j] = max (dp2[i][j], dp2[i][k] + dp2[k + 1][j] + sum[j] - sum[i - 1]);
      }
    }
  }
  //看化为链的环中哪点为起点时有最大值或最小值
  int ans = INF, ans2 = 0;
  for(int i = 1; i <= n; i++)
  {
    //i为起点 j = i + n - 1 ,len = i + j = n
    ans = min(ans, dp[i][i + n - 1]);
    ans2 = max(ans2,dp2[i][i + n - 1]);
	}
  cout << ans << endl << ans2;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值