关于石子合并
有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\n",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!
实际上我们还可以发现,递推的顺序与记忆化搜索的顺序是不一样的,递推还需要进一步的思考,把记忆化搜素分析得有条理
分析成一层一层的才方便我们递推