一般是由长度小的子问题推到长度大的子问题,解法一般比较固定,先枚举长度再枚举左端点 最后枚举中间的分割点
通过小区间的情况扩展到大区间
1、合并石子1
线性的,合并相邻的,通过计算前缀和减少了计算量,但也是O(N^3)
cin>>n; for(int i=1;i<=n;i++) { cin>>a[i]; s[i]=s[i-1]+a[i]; } memset(f,127/3,sizeof(f)); for(int i=1;i<=n;i++) f[i][i]=0; //赋一个极大的值但是相加不会溢出 for(int i=n-1;i>=1;i--){ for(int j=i+1;j<=n;j++){ for(int k=i;k<=j-1;k++){ if(f[i][j]>f[i][k]+f[k+1][j]+s[j]-s[i-1]) //把i--k和k+1--j这两堆合并起来,原本的得分加上现在的新增的得分 f[i][j]=f[i][k]+f[k+1][j]+s[j]-s[i-1]; } } } cout<<f[1][n]<<endl;
2、合并石子2
题解:https://www.luogu.com.cn/problem/P1880
在一个圆形操场的四周摆放 NN 堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
试设计出一个算法,计算出将 NN 堆石子合并成 11 堆的最小得分和最大得分。
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1010; const int INF=0x3fffffff; //区间DP //把环形化成线性 //合并石子这道题:很典型的区间dp,通过求解子区间,然后按照合并为大区间 int w[221],dp1[220][220]; //存储区间i--j int a[220]; int n; int dp2[220][220]; int main(){ cin>>n; for(int i=1;i<=n;i++){ cin>>a[i]; a[i+n]=a[i]; w[i]=w[i-1]+a[i]; //前缀和减少计算 } for(int i=n+1;i<=2*n;i++) w[i]=w[i-1]+a[i]; for(int len=1;len<n;len++){ for(int i=1,j=i+len;i<(n*2)&&j<(2*n);i++,j=i+len){ dp1[i][j]=INF; dp2[i][j]=-1; for(int k=i;k<j;k++){ //中间元素 dp1[i][j]=min(dp1[i][j],dp1[i][k]+dp1[k+1][j]+w[j]-w[i]+a[i]); dp2[i][j]=max(dp2[i][j],dp2[i][k]+dp2[k+1][j]+w[j]-w[i]+a[i]); } } } int maxx=-1,minn=INF; for(int i=1;i<=n;i++){ maxx=max(maxx,dp2[i][i+n-1]); minn=min(minn,dp1[i][i+n-1]); } cout<<minn<<endl<<maxx<<endl; return 0; }
通过四边形不等式进行优化,时间复杂度下降至O(N^2),因为保存上一次的最佳分割点,证明自己找资料再看看:
for(int k=rel[i][j-1];k<=rel[i+1][j];k++)
以合并石子2为例
//经过四边形不等式优化的合并石子 int rel[maxn][maxn]; int dp[maxn][maxn]; int summ[maxn]; int a[maxn]; int n; int main(){ cin>>n; memset(summ,0,sizeof(summ)); memset(dp,0x3f,sizeof(dp)); //别忘了初始化 for(int i=1;i<=n;i++){ cin>>a[i]; dp[i][i]=0; rel[i][i]=i; summ[i]=summ[i-1]+a[i]; } for(int i=1;i<=n;i++){ summ[i+n]=summ[i+n-1]+a[i]; dp[i+n][i+n]=0; rel[i+n][i+n]=i+n; } for(int len=1;len<=n;len++){ for(int i=1;i<=2*n-len;i++){ int j=i+len-1; for(int k=rel[i][j-1];k<=rel[i+1][j];k++){ if(dp[i][j]>dp[i][k]+dp[k+1][j]+summ[j]-summ[i-1]) { dp[i][j]=dp[i][k]+dp[k+1][j]+summ[j]-summ[i-1]; rel[i][j]=k; } } } } int ans= 0xfffffff; for(int i=1;i<=n;i++){ ans=min(ans,dp[i][i+n-1]); } cout<<ans<<endl; return 0; }
3、凸包的路径
这道题将环形的信息转化为了长度为2倍的链,就变成了线性的问题
而且这道题。。。我觉得我不是很懂
CF838E Convex Countour
按顺时针给出一个凸多边形,任意两点间有一条直线边,求每个点恰好一次的最长不自交(和之前线段相交)路径
题解:https://www.luogu.com.cn/problemnew/solution/CF838E
//https://www.luogu.com.cn/problemnew/solution/CF838E #include <cstdio> #include <cmath> #include <algorithm> using namespace std; const int N=2510; struct Point { double x, y; Point(int x=0, int y=0):x(x), y(y){} } p[N]; double dis(Point a, Point b) { return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)); } double f[N][N][2]; int n; int main() { scanf("%d", &n); for (int i=0; i<n; i++) { int x, y; scanf("%d%d", &x, &y); p[i]=Point(x, y); } for (int len=2; len<=n; len++) for (int l=0; l<n; l++) { int r=(l+len-1)%n; f[l][r][0]=max(f[(l+1)%n][r][0]+dis(p[l], p[(l+1)%n]), f[(l+1)%n][r][1]+dis(p[l], p[r])); f[l][r][1]=max(f[l][(r-1+n)%n][0]+dis(p[r], p[l]), f[l][(r-1+n)%n][1]+dis(p[r], p[(r-1+n)%n])); } double ans=0; for (int i=0; i<n; i++) ans=max(ans, max(f[i][(i+n-1)%n][0], f[i][(i+n-1)%n][1])); printf("%.10lf", ans); return 0; }
4、矩阵相乘提高版,或者题目也可以说成是抽卡片,抽卡片更好理解
给出n个数字,不能删除两边,删除i的代价是a[i-1]*a[i]*a[i+1]
简单的区间DP,设f[i][j]为[i,j][i,j]为一个矩阵的最小代价
f[i][j]=min(f[i][k]+f[k][j]+a[i]∗a[k]∗a[j],f[i][j])
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1010; const int INF=0x3fffffff; long long a[110],dp[110][110]; //区间dp //dp[i][j]=min(dp[i][k]+dp[k+1][j]+a[i-1][k][k+1] int n; int main(){ cin>>n; int x; for(int i=0;i<=n;i++){ cin>>a[i]; dp[i][i]=0; if(i!=0&&i!=n) cin>>x; //根据转移表达式,有一个不用输入 } for(int len=2;len<=n;len++){ for(int i=1;i<=n-len+1;i++){ int j=i+len-1; long long temp=dp[i+1][j]+a[i-1]*a[i]*a[j]; for(int k=i+1;k<j;k++){ long long t=dp[i][k]+dp[k+1][j]+a[i-1]*a[k]*a[j]; temp=min(temp,t); } dp[i][j]=temp; } } cout<<dp[1][n]<<endl; return 0; }
5、添加括号
【题目背景】
给定一个正整数序列a1,a2,...,an,(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个中间和,求出使中间和之和最小的添括号方法。
const int maxn=1010; const int INF=0x3fffffff; //区间dp:但是涉及三个问题 //第一个:找到最优值,这个就是转台转移表达式 //首先是dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]) dp[i][j]+=sum(i,j) //然后是输出括号:记录每个数字前面有多少个左括号,后面有多少个右括号 //最后是输出中间值 //后面两个问题都是通过DFS来解决,当然是在第一个问题通过DP解决了,并且记录了最优值得基础上 int a[21],summ[21][21]; //注意理解这个summ数组,是一个斜三角形,summ[i][j]就代表了从i到j得和,因为第i行就是从第i-1开始的 int dp[21][21]; //最小值 int be[21],af[21]; // 记录每个数字前面有多少个左括号,后面有多少个右括号 int n; int res[21][21]; void dfs(int l,int r){ if(l==r) return ; dfs(l,l+res[l][r]); //递归 dfs(l+res[l][r]+1,r); be[l]++; af[r]++ ; } void dfs2(int l,int r){ if(l==r) return; dfs2(l,l+res[l][r]); dfs2(l+res[l][r]+1,r); cout<<summ[l][r]<<" "; //最后输出 } int main(){ cin>>n; memset(dp,0x3f,sizeof(dp)); for(int i=1;i<=n;i++){ cin>>a[i]; dp[i][i]=0; } for(int i=1;i<=n;i++){ for(int j=i;j<=n;j++) summ[i][j]=summ[i][j-1]+a[j]; //初始summ数组 } for(int len=1;len<n;len++){ for(int i=1;i<=n-len;i++){ int j=i+len; for(int step=0;step<len;step++){ //记录 if(dp[i][j]>dp[i][i+step]+dp[i+step+1][j]){ dp[i][j]=dp[i][i+step]+dp[i+step+1][j]; res[i][j]=step; //i和j之间的括号在那个位置 } } dp[i][j]+=summ[i][j] ;// 还要加上和 } } dfs(1,n); //记录左右括号 for(int i=1;i<=n;i++){ for(int j=1;j<=be[i];j++) cout<<"("; //前面有多少个左括号 cout<<a[i]; for(int j=1;j<=af[i];j++) cout<<")"; if(i!=n) cout<<"+"; } cout<<endl<<dp[1][n]<<endl; dfs2(1,n); return 0; }
6、最长括号匹配 Poj2955 括号匹配(一)
dp[i][j]表示i~j个字符间的最长匹配
if(a[i]==a[j]) dp[i][j]=dp[i+1][j-1]+2
dp[i][j]=max{dp[i][j],dp[i][k]+dp[k+1][j]}
int dp[105][105]; int main() { char s[105]; while(scanf("%s",s+1)!=EOF) { memset(dp,0,sizeof(dp));//dp初始化为0,因为一方面是找最大之,一方面初始匹配数为0 int len = strlen(s+1);//dp[i][i]不用处理,因为自己和自己不匹配就是0 if(s[1]=='e')break; for(int l = 1;l<=len;l++){ for(int i = 1;i+l<=len+1;i++){ int j= i+l-1; if((s[i]=='('&&s[j]==')')||(s[i]=='['&&s[j]==']')){//如果匹配,先更新 dp[i][j] = dp[i+1][j-1]+2; } for(int k = i;k<j;k++){//k<j dp[i][j] = max(dp[i][j],dp[i][k]+dp[k+1][j]); } } } cout<<dp[1][len]<<endl; } return 0; }
7、hdu 4632 Palindrome subsequence
最多回文子串,要求不连续,位置不同就可以了
先全部都处理:dp[i][j]=dp[i+1][j]+dp[i][j-1]-dp[i+1][j-1] 这个操作是并上,有容斥原理可以得到,而且这个是一个固定的状态,注意要取模,所以要先加模,再取模
如果s[i]==s[j]的话,那么就可以分为dp[i][j]=dp[i][j]+dp[i+1][j-1]+1,
//最多回文子串 //先全部都处理:dp[i][j]=dp[i+1][j]+dp[i][j-1]-dp[i+1][j-1] //如果s[i]==s[j]的话,那么就可以分为dp[i][j]=dp[i][j]+dp[i+1][j-1]+1 char a[maxn]; int n; int dp[maxn][maxn]; int main(){ scanf("%d",&n); int index=1; while(n--){ scanf("%s",a+1); int nn=strlen(a+1); memset(dp,0,sizeof(dp)); for(int i=1;i<=nn;i++) dp[i][i]=0; for(int len=1;len<=nn;len++){ for(int i=1;i<=nn-len+1;i++){ int j=i+len-1; dp[i][j]=(dp[i+1][j]+dp[i][j-1]-dp[i+1][j-1]+10007)%10007; if(a[i]==a[j]) dp[i][j]=(dp[i][j]+dp[i+1][j-1]+1)%10007; } } printf("Case %d: %d ",index++,dp[1][nn]); } return 0; }
8、整数划分
给出两个整数 n , m ,要求在 n 中加入m - 1 个乘号,将n分成m段,求出这m段的最大乘积
这里给的乘号是有限个,所以状态方程里必须包含使用乘号的个数,此外还要包含区间长度。所以怎么用二维dp实现包含m和n,我们可以用dp[i][j]表示在第1~i个字符里插入j个乘号的最大值。
状态转移方程 dp[i][j]表示在第1i个字符里插入j个乘号的最大值;用num[i][j]表示第ij个字符表示的数字;
dp[i][j] = max(dp[i][j],dp[k][j-1]*num[k+1][i])
1570:【例 2】能量项链
这个其实就是矩阵相乘,一样的转移表达式
注意要拆分乘2*N,而且这个的循环一二维循环的是区间左右下标,注意转移表达式
dp[j][i]=max(dp[j][i],dp[j][k]+dp[k+1][i]+a[j]*a[k+1]*a[i+1]);
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1010; const int INF=0x3fffffff; typedef long long LL; //能量项链 //矩阵相乘提高版,或者题目也可以说成是抽卡片,抽卡片更好理解 //f[i][j]=min(f[i][k]+f[k][j]+a[i]?a[k]?a[j],f[i][j]) int n; LL a[220]; LL dp[220][220]; int main(){ scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%lld",&a[i]); dp[i][i]=0; a[i+n]=a[i]; } LL ans=-1; for(int i=2;i<n*2;i++){ //整个区间都需要更新 for(int j=i-1;i-j<n&&j>=1;j--){ //从后往前推 for(int k=j;k<i;k++){ dp[j][i]=max(dp[j][i],dp[j][k]+dp[k+1][i]+a[j]*a[k+1]*a[i+1]); // dp[j][i]=max(dp[j][i],dp[j][k]+dp[k+1][i]+a[j-1]*a[k]*a[i]); } ans=max(ans,dp[j][i]); } } printf("%lld",ans); return 0; }
1571:【例 3】凸多边形的划分
和上一道题的转移式比较像,但是这道题数据大, 要写高精度
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]+a[i]*a[j]*a[k]);
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1010; const int INF=0x3fffffff; typedef long long LL; // dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]+a[i]*a[j]*a[k]); //但是需要写高精度 /* int n; LL a[51]; LL dp[51][51]; int main(){ scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%lld",&a[i]); } LL ans=INF; for(int len=1;len<=n;len++){ for(int i=1;i<=n-len+1;i++){ dp[i][i+len]=INF; dp[i][i+1]=0; //不能形成 int j=i+len-1; for(int k=i;k<=i+len-1;k++){ dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]+a[i]*a[j]*a[k]); } if(len==n) ans=min(ans,dp[i][i+n-1]); } } printf("%lld",ans); return 0; } */ inline void qread(int &x){ x=0; int ch=getchar(); while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9') x=x*10+ch-'0',ch=getchar(); } struct bignum{ int num[48]; void sset(int x){ memset(num,0,sizeof(num)); while(x){ num[++num[0]]=x%10; x/=10; } } void clea(){ memset(num,0,sizeof(num)); } void INf(){ num[0]=30; for(int i=1;i<=num[0];i++) num[i]=9; } }; bignum minn(const bignum &a,const bignum &b){ if(a.num[0]>b.num[0]) return b; if(a.num[0]<b.num[0]) return a; for(int i=a.num[0];i>=1;i--) { if(a.num[i]>b.num[i]) return b; if(a.num[i]<b.num[i]) return a; } return a; } bignum add(const bignum &a,const bignum &b){ bignum c; c.clea(); c.num[0]=max(a.num[0],b.num[0]); int jw=0; for(int i=1;i<=c.num[0];i++){ c.num[i]=a.num[i]+b.num[i]+jw; jw=c.num[i]/10; c.num[i]%=10; } if(jw) c.num[++c.num[0]]=jw; return c; } bignum mul(bignum a,bignum b){ bignum c; c.clea(); c.num[0]=a.num[0]+b.num[0]; //相乘 for(int i=1;i<=a.num[0];i++){ int jw=0; for(int j=1;j<=b.num[0];j++){ c.num[i+j-1]+=a.num[i]*b.num[j]+jw; jw=c.num[i+j-1]/10; c.num[i+j-1]%=10; } c.num[i+b.num[0]]=jw; } while(c.num[c.num[0]]==0) --c.num[0]; if(!c.num[0]) c.num[0]=1; return c; } void show(const bignum &a){ for(int i=a.num[0];i>=1;i--) putchar(a.num[i]+48); } int n; bignum data[120]; bignum dp[120][120]; int main(){ qread(n); int x; for(int i=1;i<=n;i++){ qread(x); data[i].sset(x); data[i+n]=data[i]; } data[n<<1|1]=data[1]; for(int len=2;len<n;len++){ //格式 for(int i=1;i<=(n<<1)-len;i++){ int j=i+len; dp[i][j].INf(); for(int k=i+1;k<j;k++) dp[i][j]=minn(dp[i][j],add(dp[i][k],add(dp[k][j],mul(data[i],mul(data[k],data[j]))))); } } bignum ans; ans.INf(); for(int i=1;i<=n;i++) ans=minn(ans,dp[i][i+n-1]); //最后取值 show(ans); cout<<endl; return 0; }
1573:分离与合体
表达转移式变化了,因为题目的要求,但是做法还是一样,而且这道题还需要输出对策,
合并时获得的价值就是 (1 号金钥匙价值 +3 号金钥匙价值)×( 2 号金钥匙价值))。
LYD 请你编程求出最终可以获得的最大总价值,并按照分离阶段从前到后,区域从左到右的顺序,输出发生分离区域编号。若有多种方案,选择分离区域尽量靠左的方案(也可以理解为输出字典序最小的)。
例如先打印一分为二的区域,然后从左到右打印二分为四的分离区域,然后是四分为八的……
/*
大意就是选取区间一点区间划分为左右部分,合并时得分为划分前(左端点+右端点) * 选取点的权值。
我们还是按照老办法,用跨度来 DP 就可以得到最大得分。
关于路径输出,因为本题中区间划分是同时进行的,就是说如果存在多段区间都可划分,那么它们是可以同时划分的,而题意又要求按照划分的先后和区域的左右输出,
也就是说要先判断该节点是属于第几次划分的,由于我们 path 数组存放了区间的划分点,那么我们不断递归,递归树第 k 层就对应着第 k 次划分,而左右顺序取决于我们的递归顺序;
因此我们可以通过传入变量 step 代表当前递归树的层数,num 代表当前要输出的是第几步划分出的区域,当 step = num 就说明满足条件。
*/
所以这个DFS(pint函数比较重要)
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=303; const int INF=0x3fffffff; typedef long long LL; //重点是理解题意 /* 大意就是选取区间一点区间划分为左右部分,合并时得分为划分前(左端点+右端点) * 选取点的权值。 我们还是按照老办法,用跨度来 DP 就可以得到最大得分。 关于路径输出,因为本题中区间划分是同时进行的,就是说如果存在多段区间都可划分,那么它们是可以同时划分的,而题意又要求按照划分的先后和区域的左右输出, 也就是说要先判断该节点是属于第几次划分的,由于我们 path 数组存放了区间的划分点,那么我们不断递归,递归树第 k 层就对应着第 k 次划分,而左右顺序取决于我们的递归顺序; 因此我们可以通过传入变量 step 代表当前递归树的层数,num 代表当前要输出的是第几步划分出的区域,当 step = num 就说明满足条件。 */ int n; LL a[maxn],dp[maxn][maxn]; int path[maxn][maxn]; void prin(int i,int j,int step,int num){ //step 代表当前递归树的层数,num 代表当前要输出的是第几步划分出的区域 //num从1增加到n表示,从一分为二,二分为四这个顺序 //step就是一直都是1,(调用时) if(i>=j) return; if(step==num) cout<<path[i][j]<<" "; prin(i,path[i][j],step+1,num); prin(path[i][j]+1,j,step+1,num); } void solve(){ for(int len=1;len<n;len++){ //这里还是模板 for(int i=1;i<=n-len;i++){ int j=i+len; for(int k=i;k<j;k++){ if(dp[i][j]<dp[i][k]+dp[k+1][j]+(a[i]+a[j])*a[k]){ dp[i][j]=dp[i][k]+dp[k+1][j]+(a[i]+a[j])*a[k]; //k是分割点 path[i][j]=k; } } } } LL mx=dp[1][n]; cout<<mx<<endl; for(int i=1;i<=n;i++) prin(1,n,1,i); } int main(){ cin>>n; for(int i=1;i<=n;i++){ cin>>a[i]; } solve(); return 0; }
1574:矩阵取数游戏
每一行是不会互相影响的
所以只需要对每一行算出最优,然后相加就可以了
记得要用高精,
用M表示输入的矩阵的一行,M[i]表示该行第i个值
dp[i][j]表示区间变为[i,j]时的最优解
状态转移方程为:dp[i][j]=max(dp[i-1][j]+M[i-1]* 2^{m-j+i-1} ,dp[i][j+1]+M[j+1]*2^{m-j+i-1})
从两边向中间靠拢
终态是区间为空,dp[i][i]+M[i]*2^m(从1~n遍历,取最大)
!!!这道题也需要写高精度,这个真的需要练习
//怎么连最简单的一点都没想到呢,每一行是不会互相影响的 //所以只需要对每一行算出最优,然后相加就可以了 //记得要用高精, //用M表示输入的矩阵的一行,M[i]表示该行第i个值 //dp[i][j]表示区间变为[i,j]时的最优解 //状态转移方程为:dp[i][j]=max(dp[i-1][j]+M[i-1]* 2^{m-j+i-1} ,dp[i][j+1]+M[j+1]*2^{m-j+i-1}) //从两边向中间靠拢 //终态是区间为空,dp[i][i]+M[i]*2^m /* int n,m,a[maxn]; LL f[maxn][maxn]; //f[i][j]之间的最优值 void prin(LL x){ if(x>9) prin(x/10); putchar(x%10+'0'); } LL solve(){ memset(f,0,sizeof(f)); for(int i=1;i<=m;i++){ //从两边开始 for(int j=m;j>=i;j--){ LL b=(LL)1<<(m-j+i-1); f[i][j]=max(f[i-1][j]+(LL)a[i-1]*b,f[i][j+1]+(LL)a[j+1]*b); } } LL mx=-1; //最后一步 for(int i=1;i<=m;i++) mx=max(mx,f[i][i]+(LL)a[i]*((LL)1<<m)); //取最大 return mx; } int main(){ scanf("%d %d",&n,&m); LL ans=0; while(n--){ for(int i=1;i<=m;i++) scanf("%d",&a[i]); ans+=solve(); } prin(ans); return 0; } */ //老老实实写高精度TAT 4个点过不了 #include <bits/stdc++.h> using namespace std; const int N=85; const int L=105,Power=4,Base=10000; int n,m,a[N]; struct Bignum { int a[L]; Bignum(){memset(a,0,sizeof a);} Bignum(int x) { memset(a,0,sizeof a); while(x) {a[++*a]=x%10; x/=10;} return; } inline void Print() { int i; printf("%d",a[*a]); for(i=*a-1;i>=1;i--) { if(a[i]<1000) putchar('0'); if(a[i]<100) putchar('0'); if(a[i]<10) putchar('0'); printf("%d",a[i]); } puts(""); return; } inline void Init() { memset(a,0,sizeof a); } }Bin[N],dp[N][N]; inline bool operator<(const Bignum &p,const Bignum &q) { if(p.a[0]!=q.a[0]) return (p.a[0]<q.a[0])?1:0; int i; for(i=p.a[0];i>=1;i--) if(p.a[i]!=q.a[i]) { return (p.a[i]<q.a[i])?1:0; } return 0; } inline Bignum max(Bignum p,Bignum q) { return (p<q)?(q):(p); } inline Bignum operator+(const Bignum &p,const Bignum &q) { int i; Bignum ans=p; for(i=1;i<=q.a[0];i++) { ans.a[i]+=q.a[i]; if(ans.a[i]>=Base){ans.a[i+1]+=ans.a[i]/Base; ans.a[i]%=Base;} } while(ans.a[ans.a[0]+1]) ans.a[0]++; return ans; } inline Bignum operator*(const Bignum &p,const Bignum &q) { int i,j; Bignum ans; ans.a[0]=p.a[0]+q.a[0]; for(i=1;i<=p.a[0];i++) { for(j=1;j<=q.a[0];j++) { ans.a[i+j-1]+=p.a[i]*q.a[j]; if(ans.a[i+j-1]>Base) { ans.a[i+j]+=ans.a[i+j-1]/Base; ans.a[i+j-1]%=Base; } } } while(!ans.a[ans.a[0]]) ans.a[0]--; return ans; } inline Bignum operator*(const Bignum &p,const int &q) { int i; Bignum ans; ans.a[0]=p.a[0]+5; for(i=1;i<=p.a[0];i++) { ans.a[i]+=p.a[i]*q; if(ans.a[i]>Base) { ans.a[i+1]+=ans.a[i]/Base; ans.a[i]%=Base; } } while(!ans.a[ans.a[0]]) ans.a[0]--; return ans; } inline Bignum Solve() { int i,j; for(i=1;i<=m;i++) { for(j=1;j+i-1<=m;j++) { int l=j,r=j+i-1; dp[l][r]=max(dp[l][r-1]+Bin[m-i+1]*a[r],dp[l+1][r]+Bin[m-i+1]*a[l]); } } // dp[1][m].Print(); return dp[1][m]; } int main() { int i,j; Bignum ans; scanf("%d%d",&n,&m); Bin[0]=Bignum(1); for(i=1;i<=m;i++) Bin[i]=Bin[i-1]*2; // for(i=1;i<=m;i++) // { // Bin[i].Print(); // } // puts(""); for(i=1;i<=n;i++) { for(j=1;j<=m;j++) scanf("%d",&a[j]); ans=ans+Solve(); } ans.Print(); return 0; }