ACM DP 石子合併問題
阿新 • • 發佈:2019-02-09
滴,集訓第二十一天打卡。
可能是對組隊不太滿意,都不大高興做新的訓練...
所以最近一直在磨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]);
}