zoukankan      html  css  js  c++  java
  • 关于石子合并

    有n堆石子排成一列,每堆石子有一个重量w[i], 每次合并可以合并相邻的两堆石子,一次合并的代价为两堆石子的重量和w[i]+w[i+1]。问安排怎样的合并顺序,能够使得总合并代价达到最小。

    //石子如果能交换顺序的话就是哈夫曼树了

    //但是不能交换的话我们就只能考虑合并的顺序了,由于这个题目有贪心的嫌疑。。我们研究一下相邻两组合并的性质(看原子操作划分元素咯)

    比如说1 2 3

    如果你先合并1 2,sum+=(1+2),然后再合并另一部分。。代价是(1+2)+3,sum+=(1+2)+3,最终sum=9

    如果你先合并2 3,sum+=(2+3),然后再合并另一部分。。代价是1+(2+3),sum+=1+(2+3),最终sum=11

    所以我们找到了这个最简单的情况之后,就能递归地找最小

    因为情况多变。。你永远不知道往哪里走才好。。只有动态决策才行

    但是呢。。这里有一个思考的坑。。或者说还不清晰。。我来实力分析一波。。

    1 2 3 4 

    我先来一波错误思想。。。每次合并最小的

    (1+2) sum+=3

    3 3 4

    (3+3) sum+=6

    6 4

    只剩两个直接合并

    (6+4)sum+=10

    sum=19

    我再来一波探索的思想

    因为最小考虑的单元是3嘛。。所以你现在困惑的应该是1,2,3先合并哪个,2,3,4先合并哪个

    考虑划分1,2,3,前面算过。。是先1+2比较好,然后再合并3,3 ,最后合并6,4  sum=3+6+10

    考虑2,3,4,先加2,3比较好,然后再合并5,4,最后合并1,9 sum+=5+9+10

    没推翻错误想法啊。。再造它一组数据好了

    4 2 1 3

    1 2 4 2 1 3 1 2

    +3+3+3

    3 4 3 3 3

    +6

    3 4 3 6

    发现歧义。。合并哪边呢

    假如是从前往后遍历的第一个最小的

    那么就是

    +7

    7 3 6

    +9

    7 9

    +16

    16

    sum=47

    ====

    3 4 3 6

    合并后面那个4 3的话

    +7

    3 7 6

    +10

    10 6

    +16

    sum=48

    显然这两者的选择造成的结果不同。。而我们的最优决策则认为它们是一样的。。

    这个信息真的是。。你跑过之后才知道。。不跑不知道的。。

    其实主要是 3 4 3 6

    主要是合并成 7 3 6,3 7 6的不同

    一组能取到3,6,一组取不到3,6

     如果你要是写记忆化搜索的话

    它的代码是这个样子

    #include <iostream>
    #include <cstdio>
    using namespace std;
    const int maxn=1e2+7;
    int n;
    int w[maxn];
    int sum[maxn];
    // 1 2 3
    // (1+2)+((1+2)+3)=9
    // (2+3)+((2+3)+1)=11
    //可以看得出这个题的重叠子问题性质非常明显
    int dp[maxn][maxn];
    int dfs(int st,int end){
        int len=end-st+1;
        if(len==1){
            return dp[st][end]=0;
        }
        if(dp[st][end]>0) return dp[st][end];
        if(len==2){
            return dp[st][end]=w[st]+w[end];
        }
        int i,j;
        int ans=~0u>>1;
        for(i=st;i<end;++i){
            //st i sum[i]-sum[st-1];
            //i+1 end sum[end]-sum[i];
            //合并的代价加上前缀和不就好了吗
            int pre1=sum[i]-sum[st-1];
            int pre2=sum[end]-sum[i];
            //实际上pre1+pre2可以消去sum[i],然后这个十字就变成sum[end]-sum[st-1];
            ans=min(ans,dfs(st,i)+pre1+dfs(i+1,end)+pre2);
        }
        return dp[st][end]=ans;
    }
    int main(){
        scanf("%d",&n);
        int i;
        for(i=1;i<=n;++i){
            scanf("%d",&w[i]);
        }
        for(i=1;i<=n;++i){
            sum[i]=sum[i-1]+w[i];
        }
        printf("%d",dfs(1,n));
        
        return 0;
    }

    不加记忆化这里会特别慢。。因为对于这个问题有大量的重叠子问题,优化空间很大,狗哥说不加dp记忆化至少要比不加快5,6倍

    但是根据白神的经验,对于有的问题可能加了记忆化反而会超时,我觉得这是有可能的,对于重叠子问题不太多的搜索,如果你每次都

    记忆化时间复杂度会多出来一个返回值每次回溯的赋值,如果这个操作量很大的话,就会引起超时。所以对于这个问题我们是要具体问题具体分析

    那我们来看一看如何把一个记忆化搜索改成递推,那就是我们要看清楚这棵树的拓扑结构,倒数第二次回溯都依赖哪些状态的计算结果,那么

    我们把这些终止状态找到并且赋值,这个判断的属性都会写到dfs里面所以我们可以枚举他。

    如果我们要先计算出所有某一阶段的值,下一阶段的计算又依赖于上一阶段的计算,那么我们的做法就是把它放到最外面的循环,只有这样

    才能一个阶段算完再算另一个阶段,可以看一下我下面这个枚举len长度进行递推的dp,for循环中枚举的每一维的状态你要清楚

    for循环的先后状态所导致的拓扑结构你也要想清楚,如果稀里糊涂的话,你会wa到妈妈都不认识的

    #include <iostream>
    #include <cstdio>
    
    using namespace std;
    int n;
    const int maxn=1e2+7;
    int w[maxn];
    int dp[maxn][maxn];
    int sum[maxn];
    int main(){
        scanf("%d",&n);
        int i,j,k;
        for(i=1;i<=n;++i){//注意这里的下标与前缀和的求法
            scanf("%d",&w[i]);
            sum[i]=sum[i-1]+w[i];
        }
        for(j=0;j<n;++j){
            //第一维枚举长度
            for(i=1;i<=n&&i+j<=n;++i){
                //第二维枚举起点
                if(j==0){
                    dp[i][i+j]=0;
                    continue;    
                }
                if(j==1){
                    dp[i][i+j]=w[i]+w[i+j];
                    continue;
                }
                int st=i;
                int end=i+j;
                dp[i][i+j]=~0u>>1;
                for(k=i;k<end;++k){
                    //第三维枚举分割点
                    int pre1=sum[k]-sum[i-1];
                    int pre2=sum[i+j]-sum[k];
                    dp[i][i+j]=min(dp[i][i+j],dp[i][k]+pre1+dp[k+1][i+j]+pre2);
                }
            }
        }
        printf("%d
    ",dp[1][n]);
         return 0;
    }
        // for(i=1;i<=n;++i){
        //     for(j=0;i+j<=n;++j){
        //         int cur=i+j;
        //         if(j==0) dp[i][cur]=w[i];
        //         else if(j==1) dp[i][cur]=w[i]+w[cur];
        //         else{
        //             dp[i][cur]=~0u>>1;
        //             for(k=i;k<cur;++k){
        //                 int pre1=sum[k]-sum[i-1];
        //                 int pre2=sum[cur]-sum[k];
        //                 dp[i][cur]=min(dp[i][cur],dp[i][k]+pre1+dp[k+1][cur]+pre2);
    //                    //依赖的东西还没有算完,我们需要一层一层地算
        //             }
        //         }
        //     }
        // }

    所以说,不要看到dp[i][k]和dp[k+1][j]+w[i][j]这种形式就想着把k放到最后一维去枚举。。你这样,太naive!

    实际上我们还可以发现,递推的顺序与记忆化搜索的顺序是不一样的,递推还需要进一步的思考,把记忆化搜素分析得有条理

    分析成一层一层的才方便我们递推

  • 相关阅读:
    372. Super Pow
    224. Basic Calculator + 227. Basic Calculator II
    263. Ugly Number + 264. Ugly Number II + 313. Super Ugly Number
    169. Majority Element
    225. Implement Stack using Queues + 232. Implement Queue using Stacks
    551. Student Attendance Record I + Student Attendance Record II
    765. Couples Holding Hands
    547. Friend Circles
    535. Encode and Decode TinyURL
    87. Scramble String
  • 原文地址:https://www.cnblogs.com/linkzijun/p/6094720.html
Copyright © 2011-2022 走看看