1. 程式人生 > >ACM DP 石子合併問題

ACM DP 石子合併問題

滴,集訓第二十一天打卡。

可能是對組隊不太滿意,都不大高興做新的訓練...

所以最近一直在磨DP,翻一下部落格,發現最近都是DP啊...

這個石子合併的,我做的訓練資料n是40000直線型的,一開始沒注意,先做了環形,再換直線,然後發現樸素DP不能用了...

再看的GarsiaWachs演算法,一上午啊,都在這了..

部分參考來自http://blog.csdn.net/acdreamers/article/details/18039073

石子合併有三種題型。

(1)有N堆石子,現要將石子有序的合併成一堆,規定如下:每次只能移動任意的2堆石子合併,合併花費為新合成的一堆石子的數量。求將這N堆石子合併成一堆的總花費最小(或最大)。

分析:當然這種情況是最簡單的情況,合併的是任意兩堆,直接貪心即可,每次選擇最小的兩堆合併。本問題實際上就是哈夫曼的變形。

(2)有N堆石子,現要將石子有序的合併成一堆,規定如下:每次只能移動相鄰的2堆石子合併,合併花費為新合成的一堆石子的數量。求將這N堆石子合併成一堆的總花費最小(或最大)。

分析:我們熟悉矩陣連乘,知道矩陣連乘也是每次合併相鄰的兩個矩陣,那麼石子合併可以用矩陣連乘的方式來解決。 設dp[i][j]表示第i到第j堆石子合併的最優值,sum[i][j]表示第i到第j堆石子的總數量。那麼就有狀態轉移公式: 最普通的演算法O(n^3):
其中,dp[i][j]代表i到j堆的最優值,sum[i]代表第1堆到第i堆的數目總和。


#include <stdio.h>
#include <algorithm>
using namespace std;
int dp[205][205],sum[205],a[205];
int main()
{
    int n,i,j,k,l,s;
    while(scanf("%d",&n)!=EOF)
    {
    	s=0;
       	for(i=1;i<=n;i++)
		{
			scanf("%d",&a[i]);
			s+=a[i];
			sum[i]=s;
		}
   		//memset(dp,0,sizeof(dp));
   		for(i=1;i<=n;i++)
		dp[i][i]=0;
   		for(l=1;l<n;l++)
   		{
		 	for(i=1;i<=n-l;i++)
	 		{
				j=i+l;
				dp[i][j]=1000000000;
				for(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];
 			}  	
  		}
		printf("%d\n",dp[1][n]);
    }
}

考慮四邊形不等式優化,接近O(n^2):
原狀態轉移方程中的k的列舉範圍便可以從原來的(i~j-1)變為(s[i,j-1]~s[i+1,j])。
#include <stdio.h>
#include <algorithm>
using namespace std;
int dp[105][105],sum[105],a[105],ss[105][105];
int main()
{
    int n,i,j,k,l,s;
    while(scanf("%d",&n)!=EOF)
    {
   		s=0;
	   	for(i=1;i<=n;i++)
		{
			scanf("%d",&a[i]);
			s+=a[i];
			sum[i]=s;
		}
		//memset(dp,0,sizeof(dp));
		for(i=1;i<=n;i++)
		dp[i][i]=0,ss[i][i]=i;
		for(l=1;l<n;l++)
		{
		 	for(i=1;i<=n-l;i++)
	 		{
				j=i+l;
				dp[i][j]=1000000000;
				for(k=ss[i][j-1];k<=ss[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];
	                    ss[i][j]=k;
	                }
			 	}
	            dp[i][j]+=sum[j]-sum[i-1];
			}  	
		}
		printf("%d\n",dp[1][n]);
    }	
}

HYSBZ-3229 石子合併(GarsiaWachs演算法)

在一個操場上擺放著一排N堆石子。現要將石子有次序地合併成一堆。規定每次只能選相鄰的2堆石子合併成新的一堆,並將新的一堆石子數記為該次合併的得分。 試設計一個演算法,計算出將N堆石子合併成一堆的最小得分。

Input

  第一行是一個數N。 
  以下N行每行一個數A,表示石子數目。

Output

  共一個數,即N堆石子合併成一堆的最小得分。

Sample Input

4

1

1

1

1

Sample Output

8

HINT

對於 100% 的資料,1≤N≤40000

對於 100% 的資料,1≤A≤200

思路:區間DP不能做了,範圍40000開二維太大了。
#include <stdio.h>
int stone[40005],ans,t;
void combine(int k)
{
	int tmp,i,j,d;
	tmp=stone[k-1]+stone[k];
	ans+=tmp;
	t--;
	for(i=k;i<t;i++) 
	stone[i]=stone[i+1];
	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])
	{
		d=t-j;
		combine(j-1);
		j=t-d;
	}
}
int main()
{
	int n,i,j;
	scanf("%d",&n);
	stone[0]=0x3f3f3f3f; 
	stone[n+1]=0x3f3f3f3f-1;
	for(i=1;i<=n;i++)
	scanf("%d",&stone[i]);
	ans=0;t=3;
	for(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;
}


(3)問題(2)的是在石子排列是直線情況下的解法,如果把石子改為環形排列,又怎麼做呢? 分析:狀態轉移方程為:
其中有:
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
int dp[1005][1005];
int main()
{
	int n,i,j,a[1001],sum[1005],s=0,k,l;
	scanf("%d",&n);
	sum[0]=0;
	for(i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		s+=a[i];
		sum[i]=s;
	}
	memset(dp,0,sizeof(dp));
	for(l=2;l<=n;l++)//歸併的石子長度
	{
		for(i=1;i<=n-l+1;i++)//i為起點,j為終點
		{
			j=i+l-1;
			dp[i][j]=1000000000;
			for(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]);
}