石子合并 区间dp

石子合并 区间dp

题目链接:http://acm.nyist.edu.cn/JudgeOnline/problem.php?pid=737
题目大意: 有N堆石子排成一排,每堆石子有一定的数量。现要将N堆石子并成为一堆。合并的过程只能每次将相邻的两堆石子堆成一堆,每次合并花费的代价为这两堆石子的和,经过N-1次合并后成为一堆。求出总的代价最小值。
题目分析:区间dp
参考博客
区间dp最简单形式的伪代码

//mst(dp,0) 初始化DP数组  
for(int i=1;i<=n;i++)  
{  
    dp[i][i]=初始值  
}  
for(int len=2;len<=n;len++)  //区间长度  
for(int i=1;i<=n;i++)        //枚举起点  
{  
    int j=i+len-1;           //区间终点  
    if(j>n) break;           //越界结束  
    for(int k=i;k<j;k++)     //枚举分割点,构造状态转移方程  
    {  
        dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]+w[i][j]);  
    }  
}  

石子合并代码:

#include <bits/stdc++.h>
using namespace std;

const int maxn = 500 + 100;
const int INF = 0x3f3f3f3f;

int dp[maxn][maxn], data[maxn], sum[maxn];
int main()
{
    int n;
    while(~scanf("%d", &n)) {
          memset(dp, INF, sizeof(dp));
    for(int i = 1; i <= n; i++) {
        scanf("%d", &data[i]);
        sum[i] = sum[i - 1] + data[i];
    }

    for(int i = 1; i <= n; i++) dp[i][i] = 0;


    for(int len = 2; len <= n; len++) {
        for(int i = 1; i < n; i++) {
            int j = i + len - 1;
            if(j > n) break;

            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]);
            }
        }
    }
    printf("%d\n", dp[1][n]);
    }


}

平行四边形优化
刚刚的代码时间复杂度是n^3,可以优化为n^2。
具体做法:
由于状态转移时是三重循环的,我们想能否把其中一层优化呢?尤其是枚举分割点的那个,显然我们用了大量的时间去寻找这个最优分割点,所以我们考虑把这个点找到后保存下来。
用s[i][j]表示区间[i,j]中的最优分割点,那么第三重循环可以从[i,j-1)优化到【s[i][j-1],s[i+1][j]】。(这个时候小区间s[i][j-1]和s[i+1][j]的值已经求出来了,然后通过这个循环又可以得到s[i][j]的值)。
优化后的代码如下:

#include <bits/stdc++.h>
using namespace std;

const int maxn = 500 + 100;
const int INF = 0x3f3f3f3f;

int dp[maxn][maxn], data[maxn], sum[maxn], s[maxn][maxn];
int main()
{
    int n;
    while(~scanf("%d", &n)) {
          memset(dp, INF, sizeof(dp));
    for(int i = 1; i <= n; i++) {
        scanf("%d", &data[i]);
        sum[i] = sum[i - 1] + data[i];
        s[i][i] = i;
    }

    for(int i = 1; i <= n; i++) dp[i][i] = 0;


    for(int len = 2; len <= n; len++) {
        for(int i = 1; i < n; i++) {
            int j = i + len - 1;
            if(j > n) break;

            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] = dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1];
                s[i][j] = k;
                }

            }
        }
    }
    printf("%d\n", dp[1][n]);
    }
}

对于石子合并问题,有一个有一个最好的算法,那就是GarsiaWachs算法。时间复杂度为O(n^2)。
它的步骤如下:

设序列是stone[],从左往右,找一个满足stone[k-1] <= stone[k+1]的k,找到后合并stone[k]和stone[k-1],再从当前位置开始向左找最大的j,使其满足stone[j] > stone[k]+stone[k-1],插到j的后面就行。一直重复,直到只剩下一堆石子就可以了。在这个过程中,可以假设stone[-1]和stone[n]是正无穷的。

举个例子:
186 64 35 32 103
因为35<103,所以最小的k是3,我们先把35和32删除,得到他们的和67,并向前寻找一个第一个超过67的数,把67插入到他后面,得到:186 67 64 103,现在由5个数变为4个数了,继续:186 131 103,现在k=2(别忘了,设A[-1]和A[n]等于正无穷大)234 186,最后得到420。最后的答案呢?就是各次合并的重量之和,即420+234+131+67=852。

基本思想是通过树的最优性得到一个节点间深度的约束,之后证明操作一次之后的解可以和原来的解一一对应,并保证节点移动之后他所在的深度不会改变。具体实现这个算法需要一点技巧,精髓在于不停快速寻找最小的k,即维护一个“2-递减序列”朴素的实现的时间复杂度是O(n*n),但可以用一个平衡树来优化,使得最终复杂度为O(nlogn)。
转自上面的那个博客链接。
代码:

#include <bits/stdc++.h>
using namespace std;

const int N = 4e4+100;
const int INF = 0x7fffffff;

int stone[N];
int n,t,ans;

void combine(int k)
{
    int tmp = stone[k] + stone[k-1];
    ans += tmp;
    for(int i=k;i<t-1;i++)
        stone[i] = stone[i+1];
    t--;
    int j = 0;
    for(j=k-1;stone[j-1] < tmp;j--)
        stone[j] = stone[j-1];
    stone[j] = tmp;
    while(j >= 2 && stone[j] >= stone[j-2])
    {
        int d = t - j;
        combine(j-1);
        j = t - d;
    }
}

int main()
{
    while(~scanf("%d",&n))
    {
        for(int i=1;i<=n;i++)
            scanf("%d",stone+i);
        stone[0]=INF;
        stone[n+1]=INF-1;
        t = 3;
        ans = 0;
        for(int i=3;i<=n+1;i++)
        {
            stone[t++] = stone[i];
            while(stone[t-3] <= stone[t-1])
                combine(t-2);
        }
        while(t > 3) combine(t-1);
        printf("%d\n",ans);
    }
    return 0;
}

石子合并 环形版
题目大意:环形石子合并,即现在有围成一圈的若干堆石子,其他条件跟其那面那题相同,问合并所需最小代价。
题目分析:我们需要做的是尽量向简单的问题转化,可以把前n-1堆石子一个个移到第n个后面,那样环就变成了线,即现在有2*n-1堆石子需要合并。

#include <bits/stdc++.h>
using namespace std;

const int maxn = 500 + 100;
const int INF = 0x3f3f3f3f;

int dp[maxn][maxn], data[maxn], sum[maxn], DP[maxn][maxn];
int main()
{
    int n;
    while(~scanf("%d", &n))
    {
        memset(dp, 0, sizeof(dp));
        memset(DP, INF, sizeof(DP));
        for(int i = 1; i <= n; i++)
        {
            scanf("%d", &data[i]);
            data[i + n] = data[i];
            sum[i] = sum[i - 1] + data[i];
        }
        for(int i = 1; i <= 2 * n; i++)
        {
            sum[i] = sum[i - 1] + data[i];
        }

//        for(int i = 1; i <= 2 * n; i++) printf("%d%c", sum[i], i == 2 * n ? '\n' : ' ');

        for(int i = 1; i <= 2 * n; i++) DP[i][i] = 0;


        for(int len = 2; len <= n; len++)
        {
            for(int i = 1; i <= 2 * n; i++)
            {
                int j = i + len - 1;
                if(j >= 2 * n) break;

                for(int k = i; k < j; k++)
                {
                    dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]);
                    DP[i][j] = min(DP[i][j], DP[i][k] + DP[k + 1][j] + sum[j] - sum[i - 1]);
                }
            }
        }
        int ans1 = INF, ans2 = -INF;
        for(int i = 1; i <= n; i++) {
            ans1 = min(ans1, DP[i][i + n - 1]);
            ans2 = max(ans2, dp[i][i + n - 1]);
        }
        //ans1最小值 ans2最大值
        printf("%d %d\n", ans1, ans2);
    }
}

猜你喜欢

转载自blog.csdn.net/deerly_/article/details/80324460