1. 程式人生 > >[區間DP]石子合併極其變種問題(環形,40000堆型)P1880 [NOI1995]石子合併+[Sdoi2008]石子合併/poj1738An old Stone Game

[區間DP]石子合併極其變種問題(環形,40000堆型)P1880 [NOI1995]石子合併+[Sdoi2008]石子合併/poj1738An old Stone Game

 有N堆石子,現要將石子有序的合併成一堆,規則如下:

(1)每次只能移動任意相鄰的2堆石子合併,合併花費為新合成的一堆石子的數量。求將這N堆石子合併成一堆總花費,要求N<=300。

變形一:(2)每次只能移動相鄰的2堆石子合併,合併花費為新合成的一堆石子的數量。求將這N堆石子合併成一堆的總花費最小(或最大)要求N<=40000。

變形二:(3)問題(2)的是在石子排列是直線情況下的解法,石子改為環形排列

題目:

題目限制

時間限制 記憶體限制 評測方式 題目來源
1000ms 256000KiB 遠端評測 CodeVS

題目描述 Description

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

  試設計一個演算法,計算出將N堆石子合併成一堆的最小得分。

輸入描述 Input Description

  第一行是一個數N。

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

#include <iostream>
#include <string.h>
#include <stdio.h>
 
using namespace std;
const int N = 50005;
 
int stone[N];
int n,t,ans;
 
void combine(int k)
{
    int tmp = stone[k] + stone[k-1];
    ans += tmp;
    for(int i=k;i<t-1;i++)
        stone[i] = stone[i+1];
    t--;
    int j = 0;
    for(j=k-1;j>0 && stone[j-1] < tmp;j--)
        stone[j] = stone[j-1];
    stone[j] = tmp;
    while(j >= 2 && stone[j] >= stone[j-2])
    {
        int d = t - j;
        combine(j-1);
        j = t - d;
    }
}
 
int main()
{
    while(scanf("%d",&n)!=EOF)
    {
        if(n == 0) break;
        for(int i=0;i<n;i++)
            scanf("%d",stone+i);
        t = 1;
        ans = 0;
        for(int i=1;i<n;i++)
        {
            stone[t++] = stone[i];
            while(t >= 3 && stone[t-3] <= stone[t-1])
                combine(t-2);
        }
        while(t > 1) combine(t-1);
        printf("%d\n",ans);
    }
    return 0;
}

輸出描述 Output Description

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

樣例輸入 Sample Input

4

1

1

1

1

樣例輸出 Sample Output

8

資料範圍及提示 Data Size & Hint

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

對於 60% 的資料,1≤N≤1000

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

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

題目描述

在一個圓形操場的四周擺放N堆石子,現要將石子有次序地合併成一堆.規定每次只能選相鄰的2堆合併成新的一堆,並將新的一堆的石子數,記為該次合併的得分。

試設計出1個演算法,計算出將N堆石子合併成1堆的最小得分和最大得分.

區間DP 做法(要求N<=300)演算法複雜度O(N^3):

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <queue>
#include <map>

#define frein freopen("D:\\cprogram\\acmprogram\\input", "r", stdin)
#define freout freopen("D:\\cprogram\\acmprogram\\ouput", "w", stdout)
#define MAXN 1005
#define MAXM 500005
#define INF 1000000000
using namespace std;
int f[309][309];
int f2[309][309];
int a[309],sum[309];

int main(int argc, char const *argv[])
{
	int n;
	scanf("%d",&n);
	for (int i = 1; i <= n; ++i)
	{
		scanf("%d",&a[i]);
		a[i+n] = a[i];
	}
	memset(f,0x3f,sizeof(f));
	memset(f2,0,sizeof(f2));
	
	for (int i = 1; i <= n; ++i)
	{
		f2[i][i] = 0;
		f[i][i] = 0;
		sum[i]=sum[i-1]+a[i];
	}

	for (int len = 2; len <= n; ++len)
	{
		for (int l = 1; l <= n-len+1; ++l)
		{
			int r = l+len-1;
			for (int k = l; k < r ; ++k)
			{
//				printf("===f[%d][%d]%d  f[%d][%d]%d\n",l,k,f[l][k],k+1,r,f[k+1][r]);
				f[l][r] = min(f[l][r],f[l][k]+f[k+1][r]);
				f2[l][r] = max(f2[l][r],f2[l][k]+f2[k+1][r]);
			}	
				f[l][r]+=sum[r]-sum[l-1];
				f2[l][r]+=sum[r]-sum[l-1];
		}
	}
	printf("%d\n",f[1][n]);
	return 0;
}

進行四邊形優化,原狀態轉移方程中的k的列舉範圍便可以從原來的(i~j-1)變為(s[i,j-1]~s[i+1,j])。演算法複雜度進一步接近O(n^2)(逃~~~ ε=ε=ε=┏(゜ロ゜;)┛;

可以優化為O(N^2)的時間複雜度的情況,

From 黑書

凸四邊形不等式:w[a][c]+w[b][d]<=w[b][c]+w[a][d](a<b<c<d)

區間包含關係單調: w[b][c]<=w[a][d](a<b<c<d)

定理1:  如果w同時滿足四邊形不等式和決策單調性 ,則f也滿足四邊形不等式

定理2:  若f滿足四邊形不等式,則決策s滿足 s[i][j-1]<=s[i][j]<=s[i+1][j]

定理3: w為凸當且僅當w[i][j]+w[i+1][j+1]<=w[i+1][j]+w[i][j+1]   

簡要證明:

若w[a][c]+w[b][d]<=w[b][c]+w[a][d],歸納證明f[a][c]+f[b][d]<=f[b][c]+f[a][d]

  設f[a][d]最優決策是在s取到,f[b][c]最優決策在t取到,設s<t,反之同理

  可知a<s<t<c<d

    f[a][c]+f[b][d]<=f[a][s]+f[s+1][c]+w[a][c] + f[b][t]+f[t+1][d]+w[b][d]

            =f[a][s]+f[s+1][c]+w[a][d] + f[b][t]+f[t+1][d]+w[b][c]

           <=f[a][s]+w[a][d]+f[s+1][d] + f[b][t]+w[b][c]+f[t+1][c]         歸納得到 sc+td<sd+tc  起始條件即定理3

           =f[a][d]+f[b][c]

得證.

若f[a][c]+f[b][d]<=f[b][c]+f[a][d],則s[i][j-1]<=s[i][j]<=s[i+1][j]

  僅證s[i][j-1]<=s[i][j],右邊同理

  記f_k[i][j]=f[i][k]+f[k+1][j]+w[i][j]

  記s點為[i,j]最優點,t點為[i,j+1]最優點,

  則只需證明 在[i,j+1]決策時, 取s點能夠比取在k∈[i,s-1]的點更優即可

    即證明 f_s[i,j+1]<=f_k[i,j+1]

  又因為f_s[i,j]<=f_k[i,j]

     只需證明 0 <= f_k[i,j] - f_s[i,j] <= f_k[i,j+1] - f_s[i,j+1]

      可發現右邊即 f_k[i,j] + f_s[i,j+1] <= f_k[i,j+1] + f_s[i,j]  

      展開後即: f[k][j] + f[s][j+1] <= f[k][j+1] + f[s][j]

      正是 k<s<j<j+1 的四邊形不等式

得證.

一般利用定理3證明凸函式,然後利用定理2的結論 s[i][j-1]<=s[i][j]<=s[i+1][j]

  就能夠使得複雜度由O(n^3)降低為O(n^2)

詳細證明參見《動態規劃演算法的優化技巧》--毛子青(會因為論文用i,j,i',j'搞得霧水,但是慢慢推一下就能夠出來)

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <queue>
#include <map>

#define frein freopen("D:\\cprogram\\acmprogram\\input", "r", stdin)
#define freout freopen("D:\\cprogram\\acmprogram\\ouput", "w", stdout)
#define MAXN 1005
#define MAXM 500005
#define INF 1000000000
using namespace std;
int f[309][309];
int ss[309][309];
int a[309],sum[309];

int main(int argc, char const *argv[])
{
	int n;
	scanf("%d",&n);
	for (int i = 1; i <= n; ++i)
	{
		scanf("%d",&a[i]);
		a[i+n] = a[i];
	}
	memset(f,0x3f,sizeof(f));
	
	for (int i = 1; i <= n; ++i)
	{
		ss[i][i]=i;
		f[i][i] = 0;
		sum[i]=sum[i-1]+a[i];
	}

	for (int len = 2; len <= n; ++len)
	{
		for (int l = 1; l <= n-len+1; ++l)
		{
			int r = l+len-1;
			for (int k = ss[l][r-1]; k <=ss[l+1][r] ; ++k)
			{
				if(f[l][r]>f[l][k]+f[k+1][r])
				{
					f[l][r] = f[l][k]+f[k+1][r];
					ss[l][r]=k;	
				}
			}	
				f[l][r]+=sum[r]-sum[l-1];
		}
	}
	printf("%d\n",f[1][n]);
	return 0;
}

但是當題目 N<=40000時,樸素DP不能用了。需要GarsiaWachs演算法,不會證明,直接看說步驟:以下步驟和例項為抄襲

設序列是stone[],從左往右,找一個滿足stone[k-1] <= stone[k+1]的k,找到後合併stone[k]和stone[k-1],再從當前位置開始向左找最大的j,使其滿足stone[j] > stone[k]+stone[k-1],插到j的後面就行。一直重複,直到只剩下一堆石子就可以了。在這個過程中,可以假設stone[-1]和stone[n]是正無窮的。

舉個例子:

186 64 35 32 103

因為35<103,所以最小的k是3,我們先把35和32刪除,得到他們的和67,並向前尋找一個第一個超過67的數,把67插入到他後面,得到:186 67 64 103,現在由5個數變為4個數了,繼續:186 131 103,現在k=2(別忘了,設A[-1]和A[n]等於正無窮大)234 186,最後得到420。最後的答案呢?就是各次合併的重量之和,即420+234+131+67=852。

基本思想是通過樹的最優性得到一個節點間深度的約束,之後證明操作一次之後的解可以和原來的解一一對應,並保證節點移動之後他所在的深度不會改變。具體實現這個演算法需要一點技巧,精髓在於不停快速尋找最小的k,即維護一個“2-遞減序列”樸素的實現的時間複雜度是O(n*n),但可以用一個平衡樹來優化,使得最終複雜度為O(nlogn)。

輸入輸出格式

輸入格式:

資料的第1行試正整數N,1≤N≤100,表示有N堆石子.第2行有N個數,分別表示每堆石子的個數.

輸出格式:

輸出共2行,第1行為最小得分,第2行為最大得分.

輸入輸出樣例

輸入樣例#1: 複製

4
4 5 9 4

輸出樣例#1: 複製

43
54

環形的石子合併,想象一排有2N堆的石子,在這個2N的鏈條上,使其[i~i+N-1]合併成一個頂點就可以了;轉移方程如下

 

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <queue>
#include <map>

#define frein freopen("D:\\cprogram\\acmprogram\\input", "r", stdin)
#define freout freopen("D:\\cprogram\\acmprogram\\ouput", "w", stdout)
#define MAXN 1005
#define MAXM 500005
#define INF 1000000000
using namespace std;
int f[309][309];
int f2[309][309];
int a[309],sum[309];

int main(int argc, char const *argv[])
{
	int n;
	scanf("%d",&n);
	for (int i = 1; i <= n; ++i)
	{
		scanf("%d",&a[i]);
		a[i+n] = a[i];
	}
	memset(f,0x3f,sizeof(f));
	memset(f2,0,sizeof(f2));
	
	for (int i = 1; i <= 2*n; ++i)
	{
		f2[i][i] = 0;
		f[i][i] = 0;
		sum[i]=sum[i-1]+a[i];
	}

	for (int len = 2; len <= n; ++len)
	{
		for (int l = 1; l <= 2*n-len+1; ++l)
		{
			int r = l+len-1;
			for (int k = l; k < r ; ++k)
			{
				f[l][r] = min(f[l][r],f[l][k]+f[k+1][r]);
				f2[l][r] = max(f2[l][r],f2[l][k]+f2[k+1][r]);
			}	
				f[l][r]+=sum[r]-sum[l-1];
				f2[l][r]+=sum[r]-sum[l-1];
		}
	}
	int ansmin = 0x3f3f3f3f,ansmax = 0;
	for (int i = 1; i <= n; ++i)
	{
		ansmax = max(f2[i][i+n-1],ansmax);
		ansmin = min( f[i][i+n-1],ansmin);
	}
	printf("%d\n%d\n",ansmin,ansmax);
	return 0;
}