石子合并

石子合并是一类动态规划问题,其中石子合并代价的解释为两堆石子的数量和。分为如下典型的三类:

不加限制的合并

即为每次任意合并两堆,输出为最小花费。
贪心策略每次找出最小的两个元素即可(n-1轮):

1
2
3
4
5
6
7
n=int(input())
w=list(map(int, input().split()))
for i in range(n-1):
label_min=min(w)
w.remove(label_min)
w[w.index(min(w))]+=label_min
print(w[0])

线性(相邻)合并

即为每次合并相邻两堆,输出为最小花费。
动态规划,具体思路为:

  • 确定状态:设dp[i][j]表示合并第i到j个石子的最小代价。
  • 确定状态转移方程:对于第i到第j个石子的合并,可以选择在任意一个位置k断开,将问题分成合并i到k之间的石子和合并k+1到j之间的石子两个子问题。因此,可以得到状态转移方程:
    dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + sum[j]-sum[i-1]) (i<=k<j)
    其中sum[i]表示第i个石子前(包括i)的重量和,即需要合并的代价。
  • 确定边界:当只有一个石子时,代价为0,因此dp[i][i] = 0。
  • 最终结果:最终的结果为dp[1][n],表示合并全部石子的最小代价。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

using namespace std;

int f[310][310]; //在这里创建数组,则初始化均为0
int main() {
int n;
cin >> n;
int w[n+1], sum[n+1];
sum[0]=0;
for(int i=1;i<=n;i++){
cin>>w[i];
sum[i]=sum[i-1]+w[i];
}
for(int l=2;l<=n;l++){
for(int i=1;i+l-1<=n;i++){
int j=i+l-1;
f[i][j]=1e8;
for(int k=i;k<j;k++)
f[i][j]=min(f[i][j], f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);
}
}
cout<<f[1][n]<<endl;
return 0;
}

环形合并

将环形转换为直线:通过将石子数量变为2n来转换成直线问题。 即将两个数组w[1,…,n]串联即可,这样存在1~n ; 2~n~1 ; n~1~n-1共n个数组,对于n个数组,采用线性合并的方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include<iostream>

using namespace std;
const int N=410;
int f[N][N];
int g[N][N];

int main(){
int n;
cin>>n;
int w[n+1];
int sum[2*n+1]={0};
for(int i=1;i<=n;i++){
cin>>w[i];
sum[i]=sum[i-1]+w[i];
}
for(int i=1;i<=n;i++){
sum[i+n]=sum[i-1+n]+w[i];
}
for(int l=2;l<=2*n;l++){
for(int i=1;i+l-1<=2*n;i++){
int j=i+l-1;
f[i][j]=1e8;
g[i][j]=0;
for(int k=i;k<j;k++){
f[i][j]=min(f[i][j], f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);
g[i][j]=max(g[i][j], g[i][k]+g[k+1][j]+sum[j]-sum[i-1]);
}
}
}
int label1=f[1][n];
int label2=g[1][n];
for(int i=2;i<=n;i++){
if(f[i][i+n-1]<label1)label1=f[i][i+n-1];
if(g[i][i+n-1]>label2)label2=g[i][i+n-1];
}
cout<<label1<<endl;
cout<<label2<<endl;
return 0;
}