zoukankan      html  css  js  c++  java
  • P1880 [NOI1995]石子合并[环形DP]

     题目来源:洛谷

    题目描述

    在一个圆形操场的四周摆放N堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。

    试设计出1个算法,计算出将N堆石子合并成1堆的最小得分和最大得分

    输入输出格式

    输入格式:

    数据的第1行试正整数N,1≤N≤100,表示有N堆石子.第2行有N个数,分别表示每堆石子的个数.

    输出格式:

    输出共2行,第1行为最小得分,第2行为最大得分.

    输入输出样例

     

    输入样例#1: 
    4
    4 5 9 4
    输出样例#1: 
    43
    54

     

    解析:

    这是一道相当经典的四边形不等式和环形dp,以至于一道NOI级别的题目被研究成了绿题。。。


    首先题目意思很清楚,很容易看出来这就是个首尾相接的环。
     
     
    我们先想一下如果不是环,而是一条链怎么做。
     
    dp的思路很容易想出来,就是区间dp的思想,我们将将要合并的两堆石子的距离当做阶段,合并哪两堆石子作为状态,决策就是选择哪两堆石子合并。
     
    我们定义dp[l][r]表示合并l~r之间的石子所需耗费的最小/大费用,其中l表示左端点的石子,r表示右端点的石子。
     
    我们可以把每个l~r的状态看做两个子问题,假设k为l~r之间的任意一堆石子,我们就要使得l~k的答案和k+1~r的答案最优。因此我们枚举l~r之间的每个位置k,并寻找使得结果最优的一个k。
    对应到动态规划中,就意味着两个较小的区间向一个更长的区间转移,k点不仅是两个小区间的划分点,也是转移的决策。
     
    比方说如果我们求最小值,得出这样的状态转移方程:

    dp[l][r]=min(1<=l<=n-len+1,l<r<=n,l<=k<r){dp[l][k]+dp[k+1][r]+w(l,r)};

    其中w(l,r)函数表示合并l~r之间的石子的花费,len表示区间长度

    这个东西叫二维四边形不等式,常用于优化区间dp,详细情况我这里就不讲了(我懒),如果有需要可以百度或者戳这里看dalao博客。

    PS:关于初始化,我们知道每个石子堆移到它自己上面花费是0,所以。。。

    思考一下为什么每次决策增加的是区间和:移动石子堆的花费是可叠加的,而且费用是会重叠的,如果我们要合并l~r之间的所有石子,那么显然无论它之前是由哪两堆合并而来的,花费都是sum[r]-sum[l-1](sum为前缀和数组)。

    关于w(l,r)函数,我们可以用前缀和优化搞定,于是我们需要一个三重循环来解决此问题,复杂度也就差不多达到了O(n^3),这是无法承受大规模数据的。

    动态规划有一种基本的优化思路,就是如果一个状态的某个维度可以由其他维度推出,那么我们直接在每循环的时候把它算出来就得了。

    在这里,如果我们知道了左端点l和区间长度len的话,那么r就是可求得的,即r=l+len-1;

    至此,链式dp的思路就讲完了。


    你如果拿这个思路做这道题,会发现过不了(WA4个点),这是因为题目说了是环。

    那环跟链有啥区别呢?

    很容易发现,如果是环形的话,某两个点之间的距离可以有两种可能。

    举个例子,我们假设石子总堆数为n,对于第一堆石子到最后一堆石子之间的距离len,如果是链式的话,只有n-1这种可能;而如果是环形的话,我们就有n-1和1两种可能。也就是说,每个石子堆之间都有两个距离(当然可以相等),那我们怎么设计dp算法呢?

    有一种普遍的解法,就是把环拆开成相连的两条相同的链,然后在1~2n上跑dp。具体来说其实就是把上面讲到的两个距离拆出来分开处理,某个点l到第一条链有一个最优解,到第二条链又有一个最优解,我们再取这两个最优解的最优解。

    所以之前的步骤我们可以参考链式dp写出来,而寻找两个最优解中的最优解,我们需要再分析一下。

    链式的解法最优解就在dp[1][n],即把1~n堆石子全部合并。注意,这种情况下有且仅有1~n之间的距离为n-1

    然而如果这是一个环,就有n个n-1的距离,也就是有n个局部最优解,我们还要在这些最优解里面找全局最优解,这也是本题的又一个关键点。

    所以实际上我们扫一遍dp[i][i+n-1](1<=i<=n),找到最优解就行了。

    参考代码:

     1 #include<cstdio>
     2 #include<iostream>
     3 #include<cstring>
     4 #define N 1010
     5 #define INF 0x3f3f3f3f
     6 using namespace std;
     7 int a[N<<1],dp1[N][N],dp2[N][N],s[N];
     8 int main()
     9 {
    10     int n;
    11     cin>>n;
    12     for(int i=1;i<=n;i++) scanf("%d",&a[i]),a[i+n]=a[i];
    13     memset(dp1,0x3f,sizeof(dp1));
    14     memset(dp2,0xff,sizeof(dp2));
    15     for(int i=1;i<=n+n;i++){
    16         dp1[i][i]=0;dp2[i][i]=0;
    17         s[i]=s[i-1]+a[i];
    18     }
    19     for(int len=2;len<=n*2;len++)
    20      for(int l=1;l<=n*2-len+1;l++)
    21      {
    22          int r=l+len-1;
    23          for(int k=l;k<r;k++)
    24          {
    25              dp1[l][r]=min(dp1[l][r],dp1[l][k]+dp1[k+1][r]+(s[r]-s[l-1]));
    26              dp2[l][r]=max(dp2[l][r],dp2[l][k]+dp2[k+1][r]+(s[r]-s[l-1]));
    27         }
    28      }
    29     int maxx=-INF,minn=INF;
    30     for(int i=1;i<=n;i++){
    31         maxx=max(maxx,dp2[i][i+n-1]);
    32         minn=min(minn,dp1[i][i+n-1]);
    33     }
    34     cout<<minn<<endl;
    35     cout<<maxx<<endl;
    36     return 0;
    37 }
  • 相关阅读:
    阿里消息队列中间件 RocketMQ 源码分析 —— Message 拉取与消费(上)
    数据库中间件 ShardingJDBC 源码分析 —— SQL 解析(三)之查询SQL
    数据库分库分表中间件 ShardingJDBC 源码分析 —— SQL 解析(六)之删除SQL
    数据库分库分表中间件 ShardingJDBC 源码分析 —— SQL 解析(五)之更新SQL
    消息队列中间件 RocketMQ 源码分析 —— Message 存储
    源码圈 300 胖友的书单整理
    数据库分库分表中间件 ShardingJDBC 源码分析 —— SQL 路由(一)分库分表配置
    数据库分库分表中间件 ShardingJDBC 源码分析 —— SQL 解析(四)之插入SQL
    数据库分库分表中间件 ShardingJDBC 源码分析 —— SQL 路由(二)之分库分表路由
    C#中Math类的用法
  • 原文地址:https://www.cnblogs.com/DarkValkyrie/p/11066088.html
Copyright © 2011-2022 走看看