1. 程式人生 > >石子合並

石子合並

max ava 新的 程序 設有 方式 不可 線型 動態規劃

[問題描述]:
  設有n堆石子排成一排,其編號為1、2、3、…、n(n<=100)。每堆石子的數量用:a[1]、a[2]、…、a[n] 表示,現將這n堆石子歸並成一堆,歸並的規則:

每次只能將相鄰兩堆歸並成一堆,即:第 1 堆石子 a[1] 只能與第 2 堆石子 a[2] 歸並,最後一堆石子 a[n] 只能與 a[n-1] 歸並,中間的石子 a[i] 只能與 a[i-1] 或 a[i+1] 歸並;

每次歸並的代價是兩堆石子的重量之和。
  我們假如5堆的石子,其中石子數分別為7,6,5,7,100

按照貪心法,合並的過程如下:
每次合並得分
第一次合並 7 6 5 7 100 =11
  第二次合並 7 11 7 100=18
  第三次合並 18 7 100 =25
第四次合並 25 100 =125

總得分=11+18+25+125=179

另一種合並方案

每次合並得分
  第一次合並 7 6 5 7 100 ->13
第二次合並 13 5 7 100->12
第三次合並 13 12 100 ->25
第四次合並 25 100 ->125

總得分=13+12+25+125=175

顯然利用貪心來做是錯誤的,貪心算法在子過程中得出的解只是局部最優,而不能保證使得全局的值最優。

   

如果N-1次合並的全局最優解包含了每一次合並的子問題的最優解,那麽經這樣的N-1次合並後的得分總和必然是最優的。

   因此我們需要通過動態規劃算法來求出最優解。

一:任意版
  有N堆石子,現要將石子有序的合並成一堆,規定如下:每次只能移動任意的2堆石子合並,合並花費為的一堆石子的數量。設計一個算法,將這N堆石子合並成一堆的總花費最小(或最大)。
  此類問題比較簡單,就是哈夫曼編碼的變形,用貪心算法即可求得最優解。即每次選兩堆最少的,合並成新的一堆,直到只剩一堆為止。證明過程可以參考哈夫曼的證明過程。
所用的數據結構:
1、 是堆,取兩次堆頂的最小元素,相加後再加入堆中,重復n-1次即可。
2、 兩個隊列,一個是原始的從小到大排序後的石子序列A。
       一個合並後的石子生成的序列B,
註意:這兩個序列都是有序的(從小到大),總是從它們中取出最小的兩個相加到序列B。



二:直線版
  在一條直線上擺著N堆石子,現要將石子有序的合並成一堆,規定如下:每次只能移動相鄰的2堆石子合並,合並花費為將的一堆石子的數量。設計一個算法,將這N堆石子合並成一堆的總花費最小(或最大)。
  如果熟悉矩陣連乘對這類問題肯定非常了解。矩陣連乘每次也是合並相鄰兩個矩陣(只是計算方式不同)。那麽石子合並問題可用矩陣連乘的方法來解決。
那麽最優子結構是什麽呢?如果有N堆,第一次操作肯定是從n-1個對中選取一對進行合並,第二次從n-2對中選取一對進行合並,以此類推……

  分析:我們熟悉矩陣連乘,知道矩陣連乘也是每次合並相鄰的兩個矩陣,那麽石子合並可以用矩陣連乘的方式來解決。

  設dp[i][j]表示第i到第j堆石子合並的最優值,sum[i][j]表示第i到第j堆石子的總數量。那麽就有狀態轉移公式:
  dp[i, j] = 0; (i=j)
  dp[i, j] = min{ dp[i, k] + dp[k+1, j] } + sum[i, j]; (i != j)

代碼:

java:

import java.util.Scanner;
import java.lang.Math;

public abstract class StoneAdd {
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		int m = sc.nextInt();
		int a[] = new int[m];//存放m堆石子中各堆石子數量
		int f[] = new int[m];//存放從第一堆到第i堆的石子的總數
		int sum[][] = new int[m][m];//存放從第i堆到第j堆石子的總數
		int b[][] = new int[m][m];//吧b[i][j]即為在i~j的區間內石子合並的最優值
		
		a[0] = sc.nextInt();
		f[0] = a[0];                 //輸入各堆石子數,並得到第一堆到第i堆的石子的總數
		for (int i = 1; i < a.length; i++) {
			a[i] = sc.nextInt();
			f[i] = a[i] +f[i-1];
		}
		
		for (int i = 0; i < sum.length; i++) {   //得到第i堆到第j堆石子的總數
			for (int j = i+1; j < sum[i].length; j++) {
				if(i==0) {
					sum[i][j] = f[j];
				}else {
					sum[i][j] = f[j]-f[i-1];
				}
			}
		}
		
		for (int i = 0; i < b.length; i++) {   //初始化b[][]數組為無窮大,並且當i==j的時候b[][]=0
			for (int j = i; j < b[i].length; j++) {
				b[i][j] = Integer.MAX_VALUE;
				if(i==j) {
					b[i][j] = 0;
				}
			}
		}
		
		for (int i = 1; i < b.length; i++) {   
			for (int j = 0; j < b.length-i; j++) {
				for (int k = j; k < j+i; k++) {
					b[j][j+i] = Math.min(b[j][j+i], b[j][k]+b[k+1][j+i]+sum[j][j+i]);
				}
			}
		}
		
		System.out.println(b[0][m-1]);
	}
}

  

三、加強版

  • 描述

    還記得經典題石子合並嗎?現在小Y將題目加強啦!
    在一個圓形操場的四周擺放著n堆石子,現要將石子有次序地合並成一堆。規定每次只能選取相鄰的三堆合並成新的一堆,並將新的一堆的石子數,記為該次合並的得分。
    編一程序,讀入石子堆數n及每堆的石子數。選擇一種合並石子的方案,使得做(n-1)/2次合並,得分的總和最小。

  • 輸入

    第1行一個數,表示石子堆數。
    第2行是順序排列的各堆石子數(<=1000),每兩個數之間用空格分隔。

  • 輸出

    輸出合並的最小得分。

  • 例子輸入

    5
    1 2 3 4 5

  • 例子輸出

    21

c:

#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;
}

  

四、圓形版

  如果石子是排成圓形,其余條件不變,那麽最優值又是什麽呢?
  因為圓形是首尾相接的,初一想,似乎與直線排列完全成了兩個不同的問題。因為每次合並後我們都要考慮最後一個與第一個的合並關系。直線版的矩陣連乘對角線式的最優子結構不見了。f(i, j)表示i-j合並的最優值似乎並不可行,因為我們可以得到的最優值第一步就是第一個與最後一個合並,那麽f(i, j)並不能表示這種關系。
  修改一下,f(i, j)表示從第i個開始,合並後面j個得到的最優值。sum(i, j)表示從第i個開始直到i+j個的數量和。那麽這個問題就得到解決了。註意要把其看成環形,即在有限域內的合並。

  破圓化直:將圓形的石子歸並化為直線型石子歸並。
  方法是:將原來的石子長度增加一倍,加在原來的後面,a[1]~a[n],a[1]~a[n],
      求從1,2,3,~n開始的n個合並的最小值,最其中一個最小值即可。

  狀態轉移方程為:   技術分享圖片   其中有:   技術分享圖片   上面第二類與第三類的代碼復雜度都是O(n^3),n為石子堆數目,那麽還有沒有復雜度更低的方法呢?   答案是:有。也是使用動態規劃,由於過程滿足平行四邊形法則,優化後可以將復雜度降為O(n^2)。

石子合並