1. 程式人生 > >石子歸併(區間dp & 四邊形不等式優化)

石子歸併(區間dp & 四邊形不等式優化)

基準時間限制:1 秒 空間限制:131072 KB 分值: 20 難度:3級演算法題  收藏  取消關注 N堆石子擺成一條線。現要將石子有次序地合併成一堆。規定每次只能選相鄰的2堆石子合併成新的一堆,並將新的一堆石子數記為該次合併的代價。計算將N堆石子合併成一堆的最小代價。 例如: 1 2 3 4,有不少合併方法 1 2 3 4 => 3 3 4(3) => 6 4(9) => 10(19) 1 2 3 4 => 1 5 4(5) => 1 9(14) => 10(24) 1 2 3 4 => 1 2 7(7) => 3 7(10) => 10(20) 括號裡面為總代價可以看出,第一種方法的代價最低,現在給出n堆石子的數量,計算最小合併代價。 Input
第1行:N(2 <= N <= 100)
第2 - N + 1:N堆石子的數量(1 <= A[i] <= 10000)
Output
輸出最小合併代價
Input示例
4
1
2
3
4
Output示例
19

好經典的一個區間dp的題啊!不過這次在紙上畫了畫,自己都想的差不多了,但是自己只寫了個O(n^3)的演算法,後來看了看,發現還有一個用四邊形不等式優化的方法,也學習了學習,時間優化到了O(n^2)。

好了不廢話了,第一次寫一個型別題的時候,都會仔細的寫一篇講解的:

要求是把n堆石子合併成一堆,但是隻能合併相鄰的,所以不能用貪心的方法了。

然後我們考慮簡化問題的方法,即:把n-1堆石子合併成一堆求最少耗費,然後再和剩下的一堆合併。

然後再次簡化:把n-2堆石子合併成一堆,再和剩下的2堆合併。

……

……

……

最後問題就成了,把2堆石子合併。

然後我們再開始正序往後想,兩堆石子的合併想必都會,就是從1 ~ n-1 依次列舉。

那麼三堆呢?

比如 1 2 3 三堆,我們可以分開,考慮:dp[1][3] = min(dp[1][2] + dp[3][3] , dp[1][1] + dp[2][3]) + sum[1,3]

那麼四堆呢?

我們再分割槽間 [1,1]+[2,3] 和 [1,2] + [3,4] 和 [1,3] + [4,4] 最小值加上一個sum[1,4]就是移動這四堆所需要的最小耗費。

現在再看是不是清晰了點,那麼我我們先看n^3的方法:

三個for:

①列舉長度l:2 ~ n

②列舉起始端點:1 ~ n - l + 1

③列舉斷點位置(就是上面的把區間拆開的點的位置):i ~ endd(endd為末端點,等於i + l - 1)

程式碼如下:(O(n^3))(下面還有優化的演算法

#include <stdio.h>
#include <cstring>
#include <algorithm>
using namespace std;
#define CLR(a,b) memset(a,b,sizeof(a))
#define INF 0x3f3f3f3f
#define LL long long
int main()
{
	int n;
	int t;
	int dp[111][111];
	int sum[111];
	sum[0] = 0;
	scanf ("%d",&n);
	for (int i = 1 ; i <= n ; i++)
		scanf ("%d",&t) , sum[i] = sum[i-1] + t;
	for (int i = 1 ; i <= n ; i++)
		dp[i][i] = 0;
	for (int l = 2 ; l <= n ; l++)		//選取區間長度
	{
		for (int i = 1 ; i <= n-l+1 ; i++)
		{
			int endd = i + l - 1;		//區間末端點
			dp[i][endd] = INF;
			for (int k = i+1 ; k <= endd ; k++)
				dp[i][endd] = min(dp[i][endd] , dp[i][k-1] + dp[k][endd] + sum[endd] - sum[i-1]);
		}
	}
	printf ("%d\n",dp[1][n]);
	return 0;
}

知道了基本的,然後我們來談優化,優化其實很好想,每次我們找斷點k的時候,都是從i ~ endd一個個列舉。我們是否可以用一個方法快速找到呢?答案是可以的,這裡有一個四邊形不等式:點選開啟連結

也就是說,我們用一個數組s[][]記錄從 i 到 j 斷點的位置,那麼下一輪,我們就找這兩個斷點中間的點:

s[i][j-1] <= k <= s[i+1][j]    (注意,這兩個s的數值在算 l - 1 的時候已經算出來了)

這個時候這個k就基本確定在那一兩個數中了。時間複雜度成功優化,當然多開了一個s陣列佔用了空間複雜度,但是是沒辦法的事,本來就是矛盾嘛。

程式碼如下:

#include <stdio.h>
#include <cstring>
#include <algorithm>
using namespace std;
#define CLR(a,b) memset(a,b,sizeof(a))
#define INF 0x3f3f3f3f
#define LL long long
int main()
{
	int n;
	int t;
	int dp[111][111];
	int s[111][111];
	int sum[111];
	sum[0] = 0;
	scanf ("%d",&n);
	for (int i = 1 ; i <= n ; i++)
	{
		scanf ("%d",&t);
		sum[i] = sum[i-1] + t;
		s[i][i] = i;
	}
	CLR(dp,0);
	for (int l = 2 ; l <= n ; l++)		//選取區間長度
	{
		for (int i = 1 ; i <= n-l+1 ; i++)
		{
			int endd = i + l - 1;		//區間末端點
			dp[i][endd] = INF;
			for (int k = s[i][endd-1] ; k <= s[i+1][endd] ; k++)		//利用四邊形不等式優化範圍 
			{
				if (dp[i][endd] > dp[i][k] + dp[k+1][endd] + sum[endd] - sum[i-1])		//就算這裡k+1超了也不怕
				{
					dp[i][endd] = dp[i][k] + dp[k+1][endd] + sum[endd] - sum[i-1];
					s[i][endd] = k;
				}
			}
		}
	}
	printf ("%d\n",dp[1][n]);
	return 0;
}