石子合并系列问题【区间dp,环形,四边不等式优化】

石子合并(一)

   最基础的区间dp

  N堆石子排成一排(n<=100),现要将石子有次序地合并成一堆,规定每次只能选相邻的两堆合并成一堆,并将新的一堆的石子数,记为改次合并的得分,编一程序,由文件读入堆数n及每堆石子数(<=200);

  选择一种合并石子的方案,使得做n-1次合并,得分的总和最少/最多。

   我们首先可以算出两两合并的得分, 通过两两合并的得分我们可以知道相邻三个合并的得分,然后相邻四个,五个······以此类推

  举个栗子,对于 1 2 3 4 5,要把他们合并,假如我们已经知道相邻2个,3个,4个的得分,我们可以枚举两堆石子之间的断点k

  dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);  (sum为前缀和, dp[ i ] [ j ] 表示合并第 i 到 j 堆的最大的分)

  这就是区间dp了,我们可以从小到大枚举区间长度,当然也可以枚举起点终点

  

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
using namespace std;
const int maxn =6e3;
const int inf = 0x7fffffff;
int a[maxn], dp[maxn][maxn], sum[maxn], f[maxn][maxn];
int main(){
    int n; scanf("%d", &n);
    for(int i=1; i<=n; i++){
        scanf("%d", &a[i]);
        sum[i] = sum[i-1] + a[i];//维护一下前缀和便于计算得分
    }
    for(int d=2; d<=n; d++){     //枚举区间长度
        for(int i=1,j; (j=i+d-1)<=n; i++){//区间起点i, 终点j,且保证j不能超出范围
            dp[i][j] = inf;   //保存最小值,初始化
            f[i][j] = 0;          //保存最大值,初始化
            for(int k=i; k<j; k++){//枚举
                dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);//最大
                f[i][j] = max(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);//最小
            }
        }
    }
    printf("%d\n%d\n", dp[1][n],f[1][n]);
    return 0;
}

 石子合并(二)

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

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

比(一)多了一个条件:圆形操场

乍一想很复杂,其实我们可以把这个圆拆成一条链,链的长度为原本的二倍(也就是把(一)的一堆石子复制一下再接到后面)

然后按照(一)中的做法对这条链求长度为n的区间的最大得分,可以覆盖圆中的所有情况,也就是等效的

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
using namespace std;
const int maxn =6e3;
const int inf = 0x3f3f3f3f;
int a[maxn], dp[maxn][maxn], sum[maxn], f[maxn][maxn];
int main(){
    int n; scanf("%d", &n);
    for(int i=1; i<=n; i++){
        scanf("%d", &a[i]);
        a[i+n] = a[i];
    }
    for(int i=1; i<=n*2; i++){
        sum[i] = sum[i-1] + a[i];
    }
    int maxx = 0;
    int minn = inf;
    for(int i=2*n-1; i>=1; i--){
        for(int j=i+1; j<i+n; j++){//这里我直接枚举了起点和终点
            dp[i][j] = inf;
            for(int k=i; k<j; k++){
                dp[i][j] = min(dp[i][j], dp[i][k]+dp[k+1][j]+sum[j] - sum[i-1]);
                f[i][j] = max(f[i][j], f[i][k]+f[k+1][j]+sum[j] - sum[i-1]);
            }
        }
    }
    for(int i=1; i<=n; i++){
        minn = min(minn, dp[i][i+n-1]);//限制区间长度n
        maxx = max(maxx, f[i][i+n-1]);//并求最大/最小值
    }
    printf("%d\n%d\n", minn, maxx);
    return 0;
}

石子合并(三)


题目和(二)完全一致,但数据范围大得多,我们就要考虑时间复杂度了

可以用四边不等式优化,还有一种GarsiaWachs算法(强大又玄学,不会

四边不等式

对于一个数组 f[][] ,   a < b <= c < d, 若它满足四边不等式,则

f[a][c]+f[b][d]<=f[b][c]+f[a][d]

 设 w[ i ][ j ] 表示 i 到 j 的石子数,我们现在证明它满足四边不等式

令 b = a + k, d = c + k,       即 a < a+k <= c < c+k

我们证明 w[a][c] + w[a+k][c+k] <= w[a+k][c] + w[a][c+k];

移项,得 w[a][c] - w[a+k][c] <= w[a][c+k] = w[a+k][c+k];

问题转化成了证明函数f(j)= w[i][j] - w[i+1][j] 递增

然后再证 f[] 满足四边不等式(以后再写)

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
using namespace std;
const int maxn =6e3;
const int inf = 0x3f3f3f3f;
int a[maxn], dp[maxn][maxn], sum[maxn], s[maxn][maxn];
int main(){
    int n; scanf("%d", &n);
    for(int i=1; i<=n; i++){
        scanf("%d", &a[i]);
        a[i+n] = a[i];
    }
    for(int i=1; i<=n*2; i++){
        sum[i] = sum[i-1] + a[i];
    }
    for(int i=1; i<=n; i++){
        s[i][i] = i;
        s[i+n][i+n] = i+n;
    }
    for(int d=2; d<=n; d++){
        for(int i=1,j; (j=i+d-1)<=2*n-1; i++){
            for(int k=s[i][j-1]; k<=s[i+1][j]; k++){
                if(dp[i][j]<dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]){
                    dp[i][j] = max(dp[i+1][j],dp[i][j-1])+sum[j]-sum[i-1];
                }
            }
        }
    }
    int maxx = 0;
    for(int i=1; i<=n; i++){
        maxx = max(maxx, dp[i][i+n-1]);
    }
printf("%d\n",maxx);
    return 0;
}

猜你喜欢

转载自www.cnblogs.com/hzoi-poozhai/p/12666181.html