Luogu-P1880 [NOI1995]石子合并

 

题目

题目链接

 

 

测试得分:  100

 

 

主要算法 :  动态规划,区间DP(环状DP)

 

 

 

题干:

    环状区间DP板子题

 

 

应试策略:

  1. 再看你一眼,发现你是区间DP
  2. 手动将数组环状向前旋转一个单位,实现环形区间DP要求(说实话,这个方法有点low)
  3. 确定区间DP操作,需要确定区间两端,又因为要避免重复计算,从后方开始(是这样的吧?( • ̀ω•́ )✧)
  4. 再设置一个过渡位置,在区间两端之间(包括左端点),进行DP状态方程转移f1[i][j]=min(f1[i][j],f1[i][k]+f1[k+1][j]+s[j]-s[i-1]),f2[i][j]=max(f2[i][j],f2[i][k]+f2[k+1][j]+s[j]-s[i-1]); 
  5. 因为需要输出最大值与最小值,所以需要进行两种操作
  6. 最后再每个N单位的区间操作中筛选出答案

  

   代码

 

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define FORa(i,s,e) for(int i=s;i<=e;i++)
#define FORs(i,s,e) for(int i=s;i>=e;i--)

using namespace std;

const int N=100;
int n,min_ans=214748364,max_ans,a[N+1],s[N+1],f1[N+1][N+1],f2[N+1][N+1];//f1[i][j]为i-j这区间合并在一起的最小值,f2[i][j]为i-j这区间合并在一起的最大值
inline int min(int a,int b){return a<b?a:b;}
inline int max(int a,int b){return a>b?a:b;}
int main()
{
    scanf("%d",&n);
    FORa(i,1,n) scanf("%d",&a[i]);
    FORa(k,1,n)
    {
        int p=a[1];
        FORa(i,1,n) a[i-1]=a[i];//将链进行更新操作 
        a[n]=p,s[0]=0;
        FORa(i,1,n) s[i]=s[i-1]+a[i];//s[]为前缀和数组,优化常数
        memset(f1,127/3,sizeof(f1)); 
        memset(f2,0,sizeof(f2));//每一次都需要赋初值 
        FORa(i,1,n) f1[i][i]=0;//初状态的确定 
        FORs(i,n-1,1)
            FORa(j,i+1,n)
                FORa(k,i,j-1)
                    f1[i][j]=min(f1[i][j],f1[i][k]+f1[k+1][j]+s[j]-s[i-1]),f2[i][j]=max(f2[i][j],f2[i][k]+f2[k+1][j]+s[j]-s[i-1]); //状态转移方程 
        min_ans=min(min_ans,f1[1][n]),max_ans=max(max_ans,f2[1][n]);//筛选答案 
    }
    printf("%d\n%d",min_ans,max_ans);
    return 0;
}

 

 

 

 

优化策略:

  • 这是一道区间dp十分经典的模板题,让我们揣测一下,前辈们是如何得到这个状态转移方程的。
  • 首先,要计算合并的最大值、最小值,既然是动态规划,我们需要洞悉其中一些关联且确定的状态。
  • 以下以最大值为例。
  • 既然是最大值,那么求得的结果是否满足每一区间都是该区间所能达得到的的最大值?
  • 显然是这样的。反证法:倘若有一个区间不是,那么换做该区间取得最大值的方案,最终结果将比原得分大。显然必定满足任意区间得分一定是该区间内的最大值。
  • 这样我们可以定义状态f[i][j],表示i到j合并后的最大得分。其中1<=i<=j<=N。
  • 既然这样,我们就需要将这一圈石子分割。很显然,我们需要枚举一个k,来作为这一圈石子的分割线。
  • 这样我们就能得到状态转移方程:
  • f[i][j] = max(f[i][k] + f[k+1][j] + d(i,j));其中,1<=i<=<=k<j<=N。
  • d(i,j)表示从i到j石子个数的和。
  • 那么如何编写更快的递推来解决这个问题?
  • 在考虑如何递推时,通常考虑如下几个方面:
  • 是否能覆盖全部状态?
  • 求解后面状态时是否保证前面状态已经确定?
  • 是否修改了已经确定的状态?
  • 也就是说,在考虑递推顺序时,务必参考动态规划的适应对象多具有的性质,具体参考《算法导论》相关或百度百科或wiki。
  • 既然之前说过我们需要枚举k来划分i和j,那么如果通过枚举i和j进行状态转移,很显然某些k值时并不能保证已经确定过所需状态。
  • 如,i=1 to 10,j=1 to 10,k=1 to 9.当i=1,j=5,k=3时,显然状态f[k+1][j]没有结果。
  • 那么,我们是不是应该考虑枚举k?
  • 但这样i和j就难以确定了。
  • 我们不难得到一个两全的方法:枚举j-i,并在j-i中枚举k。这样,就能保证地推的正确。

 

 

  代码

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define FORa(i,s,e) for(int i=s;i<=e;i++)
#define FORs(i,s,e) for(int i=s;i>=e;i--)

using namespace std;

const int N=100;
int n,min_ans=214748364,max_ans,a[2*N+2],s[2*N+2],f1[2*N+2][2*N+2],f2[2*N+2][2*N+2];//f1[i][j]为i-j这区间合并在一起的最小值,f2[i][j]为i-j这区间合并在一起的最大值 
inline int min(int a,int b){return a<b?a:b;}
inline int max(int a,int b){return a>b?a:b;}
int main()
{
    scanf("%d",&n);
    FORa(i,1,n) scanf("%d",&a[i]),a[i+n]=a[i],s[i]=s[i-1]+a[i];//化环为链, 
    FORa(i,n+1,2*n) s[i]=s[i-1]+a[i];//s[]为前缀和数组,优化常数
    memset(f1,127/3,sizeof(f1));
    FORa(i,1,2*n) f1[i][i]=0;
    FORs(i,2*n-1,1)
        for(int j=i+1;j<=2*n&&j-i<=n;j++)
            FORa(k,i,j-1)
                f1[i][j]=min(f1[i][j],f1[i][k]+f1[k+1][j]+s[j]-s[i-1]),f2[i][j]=max(f2[i][j],f2[i][k]+f2[k+1][j]+s[j]-s[i-1]);//状态转移方程 
    FORa(i,1,n) min_ans=min(min_ans,f1[i][i+n-1]),max_ans=max(max_ans,f2[i][i+n-1]);//筛选答案 
    printf("%d\n%d",min_ans,max_ans);
    return 0;
}

 

#include<stdio.h>
#include<stdlib.h>
#define FORa(i,s,e) for(int i=s;i<=e;i++)
#define FORs(i,s,e) for(int i=s;i>=e;i--)

using namespace std;

const int N=200,INF=1047483647;
int n,a[2*N+2],f[2*N+2][2*N+N][2],sum[N+1],min_ans=INF,max_ans=0;
/*f[i][j][0]表示的是合并i-j这几堆石子的最小费用     f[i][j][1]表示的是合并i-j这几堆石子的最大费用
    sum[i]表示的是1-i的前缀和 
*/
inline int max(int fa,int fb){return fa>fb?fa:fb;}
inline int min(int fa,int fb){return fa<fb?fa:fb;}
int main()
{
    scanf("%d",&n);
    FORa(i,1,n) scanf("%d",&a[i]),a[i+n]=a[i],sum[i]=sum[i-1]+a[i];
    for(int i=n+1,in=2*n;i<=in;i++) sum[i]=a[i]+sum[i-1];
    //化环为链,初始化DP数组 
/*    错误思想,没有考虑到最优子结构 
    for(int i=1,in=2*n-1;i<=in;i++)
    {
        for(int j=i+1,jn=2*n;j<=jn&&j-i+1<=n;j++)//枚举区间 
        {
            f[i][j][0]=INF;
            for(int k=i;k<j;k++)
            {
                f[i][j][0]=min(f[i][j][0],f[i][k][0]+f[k+1][j][0]+sum[j]-sum[i-1]);//我们注意看到f[k+1][j]可能是全新的没有处理,不符合最优子结构 
                f[i][j][1]=max(f[i][j][1],f[i][k][1]+f[k+1][j][1]+sum[j]-sum[i-1]);
                printf("%d %d\n",f[i][j][0],f[i][j][1]); 
            }
        }    
        min_ans=min(min_ans,f[i][i+n-1][0]);
        max_ans=max(max_ans,f[i][i+n-1][1]);
    }    */ 
    FORa(l,2,n)
    {
        for(int i=1,in=2*n-l+1;i<=in;i++)
        {
            f[i][i+l-1][0]=INF;
            for(int k=i,j=i+l-1;k<j;k++)
            {
                f[i][j][0]=min(f[i][j][0],f[i][k][0]+f[k+1][j][0]+sum[j]-sum[i-1]);//保证了最优子结构 
                f[i][j][1]=max(f[i][j][1],f[i][k][1]+f[k+1][j][1]+sum[j]-sum[i-1]);//保证了最优子结构
            }
            if(l==n) min_ans=min(min_ans,f[i][i+n-1][0]),max_ans=max(max_ans,f[i][i+n-1][1]);
        }    
    }    
    printf("%d\n%d",min_ans,max_ans);
    return 0;
}
/*4
4 5 9 4*/

 

 

 

总结:

  化繁为简,确定DP本身的性质

 

 

 

 

 

 

 

posted @ 2019-07-18 17:57  SeanOcean  阅读(220)  评论(0编辑  收藏  举报