背景
给定一个正整数序列a(1),a(2),...,a(n),(1<=n<=20)
不改变序列中每个元素在序列中的位置,把它们相加,并用括号记每次加法所得的和,称为中间和。例如:
给出序列是4,1,2,3。第一种添括号方法:
((4+1)+(2+3))=((5)+(5))=(10)
有三个中间和是5,5,10,它们之和为:5+5+10=20
第二种添括号方法
(4+((1+2)+3))=(4+((3)+3))=(4+(6))=(10)
中间和是3,6,10,它们之和为19。描述
现在要添上n-1对括号,加法运算依括号顺序进行,得到n-1个中间和,求出使中间和之和最小的添括号方法。如果有多组最优解,考虑将括号尽可能往前放。
样例输入
4 4 1 2 3
样例输出
(4+((1+2)+3)) 19 3 6 10
这道题和合并石子那一道DP很像,不同的是这一次要打印路径。
因为要记录路径,所以选择使用记忆化搜索,比DP的代码更清晰易读。
用root[l,r]表示这个区间的断点位置,dp[i][j]表示[i,j]区间的最优答案
显然有:
0、对于一段区间合并产生的价值,就是这个区间的值的和。因此我们可以采用前缀和进行优化
1、对于任意 i∈[1,n] 有 dp[i][i] = 0 (不用合并)
2、对于任意 i∈[1,n) 有 dp[i][i+1]=a[i]+a[i+1] (只有两个,一定合并)
3、对于任意一组 (l,r),r>l且r-l>1,那么就有dp[l,r]=min(dp[l][i]+dp[i+1][r]+sum[r]-sum[l-1];)其中i∈[l,r) (枚举断点 区间DP)
取到最优值的时候,记录断点即可。后续递归输出。
#include<cstdio> #include<cstring> #include<cmath> #include<algorithm> using namespace std; template<class T> inline void read(T &_a){ bool f=0;int _ch=getchar();_a=0; while(_ch<'0' || _ch>'9'){if(_ch=='-')f=1;_ch=getchar();} while(_ch>='0' && _ch<='9'){_a=(_a<<1)+(_a<<3)+_ch-'0';_ch=getchar();} if(f)_a=-_a; } int n,a[21],dp[21][21],root[21][21],sum[21],zuo[21],you[21]; bool vis[21][21]; void dfs(int l,int r) { vis[l][r]=true; if(l==r) {dp[l][r]=0; return ;} for (register int i=1;i<=n;++i) for (register int v=l;v+i-1<=r;++v) if(!vis[v][v+i-1]) dfs(v,v+i-1); if(r==l+1) { root[l][r]=l; dp[l][r]=a[l]+a[r]; return ; } for (register int i=l;i<r;++i) { if(dp[l][i]+dp[i+1][r]+sum[r]-sum[l-1]<=dp[l][r]) { dp[l][r]=dp[l][i]+dp[i+1][r]+sum[r]-sum[l-1]; root[l][r]=i; } } } void print(int l,int r) { if(l==r) printf("%d",a[l]); if(l>=r) return ; printf("("); print(l,root[l][r]); printf("+"); print(root[l][r]+1,r); printf(")"); } void prints(int l,int r) { if(!root[l][r]) return ; prints(l,root[l][r]); prints(root[l][r]+1,r); printf("%d ",sum[r]-sum[l-1]); } int main() { read(n); for (register int i=1;i<=n;++i) read(a[i]),sum[i]=sum[i-1]+a[i]; memset(dp,0x7f,sizeof(dp)); dfs(1,n); print(1,n); printf(" %d ",dp[1][n]); prints(1,n); return 0; }