• 合并石头(直线+圆圈+强化+优化)


     有N堆石子排成一排,每堆石子有一定的数量。现要将N堆石子并成为一堆。合并的过程只能每次将相邻的两堆石子堆成一堆,每次合并花费的代价为这两堆石子的和,经过N-1次合并后成为一堆。求出总的代价最小值。
     
    输入
    有多组测试数据,输入到文件结束。
    每组测试数据第一行有一个整数n,表示有n堆石子。
    接下来的一行有n(0< n <200)个数,分别表示这n堆石子的数目,用空格隔开
    输出
    输出总代价的最小值,占单独的一行
    样例输入
    3
    1 2 3
    7
    13 7 8 16 21 4 18

    暴力n3算法:
    其中,dp[i][j]代表i到j堆的最优值,sum[i]代表第1堆到第i堆的数目总和。有:dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j])+sum[j]-sum[i-1]。
    #include <fstream>
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <cstdlib>
    #include <cmath>
    using namespace std;
    
    const int N=205;
    const int INF=0x7fffffff;
    int n;
    int a[N],sum[N],dp[N][N];
    
    void f();
    
    int main(){
        //freopen("D:\input.in","r",stdin);
        while(~scanf("%d",&n)){
            sum[0]=0;
            for(int i=1;i<=n;i++){
                scanf("%d",&a[i]);
                sum[i]=sum[i-1]+a[i];
            }
            f();
            printf("%d
    ",dp[1][n]);
        }
        return 0;
    }
    void f(){
        for(int i=1;i<=n;i++) dp[i][i]=0;
        for(int r=1;r<n;r++){
            for(int i=1;i<n;i++){
                int j=i+r;
                if(j>n) break;
                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]);
                }
                dp[i][j]+=sum[j]-sum[i-1];
            }
        }
    }        
    
    224ms
    View Code

    四边行优化:优化:原状态转移方程中的k的枚举范围便可以从原来的(i~j-1)变为(s[i,j-1]~s[i+1,j])。

    #include <fstream>
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <cstdlib>
    #include <cmath>
    using namespace std;
    
    const int N=205;
    const int INF=0x7fffffff;
    int n;
    int a[N],sum[N],dp[N][N],s[N][N];
    
    void f();
    
    int main(){
        //freopen("D:\input.in","r",stdin);
        while(~scanf("%d",&n)){
            sum[0]=0;
            for(int i=1;i<=n;i++){
                scanf("%d",&a[i]);
                sum[i]=sum[i-1]+a[i];
            }
            f();
            printf("%d
    ",dp[1][n]);
        }
        return 0;
    }
    void f(){
        for(int i=1;i<=n;i++) dp[i][i]=0,s[i][i]=i;
        for(int r=1;r<n;r++){
            for(int i=1;i<n;i++){
                int j=i+r;
                if(j>n) break;
                dp[i][j]=INF;
                for(int k=s[i][j-1];k<=s[i+1][j];k++){
                    if(dp[i][j]>dp[i][k]+dp[k+1][j]){
                        dp[i][j]=dp[i][k]+dp[k+1][j];
                        s[i][j]=k;
                    }
                }
                dp[i][j]+=sum[j]-sum[i-1];
            }
        }
    }        
    
    32ms
    View Code

    解释下:

    四边形不等式优化动态规划原理:

    1.当决策代价函数w[i][j]满足w[i][j]+w[i’][j’]<=w[I;][j]+w[i][j’](i<=i’<=j<=j’)时,称w满足四边形不等式.当函数w[i][j]满足w[i’][j]<=w[i][j’] i<=i’<=j<=j’)时,称w关于区间包含关系单调.

    2.如果状态转移方程m为且决策代价w满足四边形不等式的单调函数(可以推导出m亦为满足四边形不等式的单调函数),则可利用四边形不等式推出最优决策s的单调函数性,从而减少每个状态的状态数,将算法的时间复杂度由原来的O(n^3)降低为O(n^2).方法是通过记录子区间的最优决策来减少当前的决策量.令:

    s[i][j]=max{k | ma[i][j] = m[i][k-1] + m[k][j] + w[i][j]}

    由于决策s具有单调性,因此状态转移方程可修改为:

    证明过程: (转载)

    m[i,j]表示动态规划的状态量。

    m[i,j]有类似如下的状态转移方程:

    m[i,j]=opt{m[i,k]+m[k,j]}(ikj)

    如果对于任意的abcd,有m[a,c]+m[b,d]m[a,d]+m[b,c],那么m[i,j]满足四边形不等式。

    以上是适用这种优化方法的必要条件

    对于一道具体的题目,我们首先要证明它满足这个条件,一般来说用数学归纳法证明,根据题目的不同而不同。

    通常的动态规划的复杂度是O(n3),我们可以优化到O(n2)

    s[i,j]m[i,j]的决策量,即m[i,j]=m[i,s[i,j]]+m[s[i,j]+j]

    我们可以证明,s[i,j-1]s[i,j]s[i+1,j]  (证明过程见下)

    那么改变状态转移方程为:

    m[i,j]=opt{m[i,k]+m[k,j]}      (s[i,j-1]ks[i+1,j])

    复杂度分析:不难看出,复杂度决定于s的值,以求m[i,i+L]为例,

    (s[2,L+1]-s[1,L])+(s[3,L+2]-s[2,L+1])…+(s[n-L+1,n]-s[n-L,n-1])=s[n-L+1,n]-s[1,L]n

    所以总复杂度是O(n2)

    s[i,j-1]s[i,j]s[i+1,j]的证明:

    mk[i,j]=m[i,k]+m[k,j]s[i,j]=d

    对于任意k<d,有mk[i,j]md[i,j](这里以m[i,j]=min{m[i,k]+m[k,j]}为例,max的类似),接下来只要证明mk[i+1,j]md[i+1,j],那么只有当s[i+1,j]s[i,j]时才有可能有ms[i+1,j][i+1,j]md[i+1,j]

    (mk[i+1,j]-md[i+1,j]) - (mk[i,j]-md[i,j])

    =(mk[i+1,j]+md[i,j]) - (md[i+1,j]+mk[i,j])

    =(m[i+1,k]+m[k,j]+m[i,d]+m[d,j]) - (m[i+1,d]+m[d,j]+m[i,k]+m[k,j])

    =(m[i+1,k]+m[i,d]) - (m[i+1,d]+m[i,k])

    m满足四边形不等式,∴对于i<i+1k<dm[i+1,k]+m[i,d]m[i+1,d]+m[i,k]

    (mk[i+1,j]-md[i+1,j])(mk[i,j]-md[i,j])0

    s[i,j]s[i+1,j],同理可证s[i,j-1]s[i,j]

    证毕

    扩展:

    以上所给出的状态转移方程只是一种比较一般的,其实,很多状态转移方程都满足四边形不等式优化的条件。

    解决这类问题的大概步骤是:

    0.证明w满足四边形不等式,这里wm的附属量,形如m[i,j]=opt{m[i,k]+m[k,j]+w[i,j]},此时大多要先证明w满足条件才能进一步证明m满足条件

    1.证明m满足四边形不等式

    2.证明s[i,j-1]s[i,j]s[i+1,j]

    GarsiaWachs算法优化+小细节优化:

    #include <fstream>
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <cstdlib>
    #include <cmath>
    using namespace std;
    
    const int N = 205;
    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()
    {
        //freopen("D:\input.in","r",stdin);
        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
    ",ans);
        }
        return 0;
    }        
    
    0ms
    View Code

    解释:

    对于石子合并问题,有一个最好的算法,那就是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 <fstream>
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <cstdlib>
    #include <cmath>
    using namespace std;
    
    const int N=205;
    const int INF=0x7fffffff;
    int n;
    int a[N],sum[N],dp[N][N];
    
    int f();
    
    int main(){
        //freopen("D:\input.in","r",stdin);
        while(~scanf("%d",&n)){
            sum[0]=0;
            for(int i=1;i<=n;i++){
                scanf("%d",&a[i]);
                sum[i]=sum[i-1]+a[i];
            }
            printf("%d
    ",f());
        }
        return 0;
    }
    int f(){
        for(int i=1;i<=n;i++)   dp[i][0]=0;
        for(int r=1;r<n;r++){
            for(int i=1;i<=n;i++){
                int j=i+r;
                dp[i][r]=INF;
                for(int k=1;k<=r;k++){
                    if(i+k>n)   dp[i][r]=min(dp[i][r],dp[i][k-1]+dp[(i+k)%n][r-k]);
                    else    dp[i][r]=min(dp[i][r],dp[i][k-1]+dp[i+k][r-k]);
                }
                if(i+r>n) dp[i][r]+=sum[(i+r)%n]+sum[n]-sum[i-1];
                else    dp[i][r]+=sum[i+r]-sum[i-1];
            }
        }
        int ans=INF;
        for(int i=1;i<=n;i++) ans=min(ans,dp[i][n-1]);
        return ans;
    }
    
    5ms
    View Code

    四边形优化

    #include <fstream>
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <cstdlib>
    #include <cmath>
    using namespace std;
    
    const int N=205;
    const int INF=0x7fffffff;
    int n;
    int a[N],sum[N],dp[N][N],s[N][N];
    
    int f();
    
    int main(){
        //freopen("D:\input.in","r",stdin);
        while(~scanf("%d",&n)){
            sum[0]=0;
            for(int i=1;i<=n;i++){
                scanf("%d",&a[i]);
                sum[i]=sum[i-1]+a[i];
            }
            printf("%d
    ",f());
        }
        return 0;
    }
    int f(){
        for(int i=1;i<=n;i++)   dp[i][0]=0,s[i][0]=1;
        for(int r=1;r<n;r++){
            for(int i=1;i<=n;i++){
                int j=i+r;
                dp[i][r]=INF;
                for(int k=s[i][r-1];k<=s[i+1][r-1]+1;k++){
                    int t=dp[i][k-1]+dp[(i+k-1)%n+1][r-k];
                    if(dp[i][r]>t){
                        dp[i][r]=t;
                        s[i][r]=k;
                    }
                }
                if(i+r>n) dp[i][r]+=sum[(i+r)%n]+sum[n]-sum[i-1];
                else    dp[i][r]+=sum[i+r]-sum[i-1];
            }
        }
        int ans=INF;
        for(int i=1;i<=n;i++) ans=min(ans,dp[i][n-1]);
        return ans;
    }
    
    0ms
    View Code

    还记得经典题石子合并吗?现在小Y将题目加强啦!
    在一个圆形操场的四周摆放着n堆石子,现要将石子有次序地合并成一堆。规定每次只能选取相邻的三堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
    编一程序,读入石子堆数n及每堆的石子数。选择一种合并石子的方案,使得做(n-1)/2次合并,得分的总和最小。

    • Input Format

    第1行一个数,表示石子堆数。
    第2行是顺序排列的各堆石子数(<=1000),每两个数之间用空格分隔。

    • Output Format

    输出合并的最小得分。

    • Sample Input

    5
    1 2 3 4 5

    • Sample Output

    21

    • Hint

    【样例解释】
    先合并(1 2 3),再合并(6 4 5)
    【数据范围】
    对于20%的数据,n=5
    对于60%的数据,n<=80
    对于100%的数据,n<=400


    • 分析

    对于60%的数据,我们可以处理[i,j]这段区间是由[i,k1][k1+1,k2][k2+1,j]这三段区间合并的。复杂度O(kn3)O(kn3)
    对于100%的数据,我们发现合成三堆可以先合并两堆在和第三堆合并。所以我们可以先用F2记录两堆合并成一堆的,然后用F1记录三堆合成的。用SGGhjj两位god的话说,就是2=1+1;3=2+1=1+2;1=3。

    #include <queue>
    #include <stack>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    int n,W[402],a[402],F1[402][402],F2[402][402];
    int main(){
        freopen("merge.in","r",stdin);
        freopen("merge.out","w",stdout);
        scanf("%d",&n);
        for (int i=1;i<=n;i++){
            scanf("%d",&W[i]);
            a[i]=a[i-1]+W[i];
        }
        memset(F1,127/2,sizeof(F1)); memset(F2,127/2,sizeof(F2));
        for (int i=n;i;i--){
            F1[i][i]=0;
            F1[i][i+2]=a[i+2]-a[i-1];
            for (int j=i+3;j<=n;j++){
                for (int k=i;k<j;k++) F2[i][j]=min(F2[i][j],F1[i][k]+F1[k+1][j]);
                for (int k=i;k<j;k++) F1[i][j]=min(F1[i][j],min(F1[i][k]+F2[k+1][j],F2[i][k]+F1[k+1][j])+a[j]-a[i-1]);
            }
        }
        if (n&1) printf("%d",F1[1][n]);
        else printf("Impossible");
        fclose(stdin); fclose(stdout);
        return 0;
    }
    View Code
  • 相关阅读:
    Python引入pandas报错ValueError: numpy.ufunc has the wrong size, try recompiling
    Oracle TNS无法解析ORA-12154报错
    python两个一维list列表合并
    SQL数据表加索引CREATE INDEX
    Python 格式化输出
    Python中三个双引号的作用
    2.认识素描
    如何快速学习Tableau Desktop
    1.怎样学习素描
    正点原子嵌入式Linux笔记3——Ubuntu软件安装
  • 原文地址:https://www.cnblogs.com/shuaihui520/p/9762029.html
Copyright © 2020-2023  润新知