2007年NOIP提高组第三题 难度还是比较大的
首先来看一下题意:
题目描述
帅帅经常跟同学玩一个矩阵取数游戏:对于一个给定的 n \times mn×m 的矩阵,矩阵中的每个元素 a_{i,j}ai,j 均为非负整数。游戏规则如下:
- 每次取数时须从每行各取走一个元素,共 nn 个。经过 mm 次后取完矩阵内所有元素;
- 每次取走的各个元素只能是该元素所在行的行首或行尾;
- 每次取数都有一个得分值,为每行取数的得分之和,每行取数的得分 = 被取走的元素值 \times 2^i×2i ,其中 ii表示第 ii 次取数(从 11 开始编号);
- 游戏结束总得分为 mm 次取数得分之和。
帅帅想请你帮忙写一个程序,对于任意矩阵,可以求出取数后的最大得分。
输入输出格式
输入格式:
输入文件包括 n+1n+1 行:
第 11 行为两个用空格隔开的整数 nn 和 mm 。
第 2~n+12 n+1 行为 n \times mn×m 矩阵,其中每行有 mm 个用单个空格隔开的非负整数。
输出格式:
输出文件仅包含1行,为一个整数,即输入矩阵取数后的最大得分。
输入输出样例
输入样例:
输出样例:
2 3 82
1 2 3
3 4 2
数据范围:
60%的数据满足: 1\le n, m \le 301≤n,m≤30 ,答案不超过 10^{16}1016
100%的数据满足: 1\le n, m \le 801≤n,m≤80 , 0 \le a_{i,j} \le 10000≤ai,j≤1000
题解:
对于这个问题,我首先进行猜想。因为每一行要从首尾取数,而取出来的数字要乘以一个系数2的i次方,因此我一开始想比较首尾取出更小的那个数来先乘,然后把每一行加起来,得到答案。这是贪心的做法,当然不是正解,不过也得到了20分。
(下代码为20分贪心做法)
#include<cstdio>
#include<iostream>
#include<cmath>
#include<cstdlib>
#include<algorithm>
#define ll long long
using namespace std;
const int maxn=31;
int a[maxn][maxn];
int n,m;
ll ans=0,k;
ll power(ll x, ll y) {
ll z = 1;
while (y) {
if (y & 1) (z *= x) ;
(x *= x) ; y >>= 1;
}
return z;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&a[i][j]);
for(int i=1;i<=n;i++)
{
int head=1,tail=m;
int z=1;
for(int j=1;j<=m;j++)
{
if(a[i][head]>a[i][tail])
{
ans+=a[i][tail]*power(2,z);
tail--;
z++;
}
else if(a[i][head]<a[i][tail])
{
ans+=a[i][head]*power(2,z);
head++;
z++;
}
else if(a[i][head]==a[i][tail])
{
ans+=a[i][head]*power(2,z);
if(a[i][head+1]>a[i][tail-1]) tail--;
else head++;
z++;
}
}
}
cout<<ans<<endl;
return 0;
}
既然在洛谷里这道题目的标签是动态规划,那么我们不妨努力往动归的方向思考。而这个问题可以转化为一道队列dp题目。
我们首先设f[i][j]为区间[i,j]的最大值,那么f[i][j]可以等于2*f[i+1][j]+2*a[i]或者2*f[i][j-1]+2*a[j]。因为题目要求系数为2的i次方,因此我们每一次都对范围×2就可以解决这个问题。根据以上推论,我们可以得到状态转移方程:
f[i][j]=max(2f[i+1][j]+2a[i],2f[i][j-1]+2a[j])
f[i][j]=max(f[i+1][j]+a[i],f[i][j−1]+a[j]);
f[i][j]=f[i][j]*2;
以上方程的代码实现如下:
其中len枚举0-m,i+len为区间末尾
for(int len=0;len<=m;len++)
{
for(int i=1;i+len<=m;i++)
{
f[i][i+len]=max(2*f[i+1][i+len]+2*a[i],2*f[i][i+len-1]+2*a[i+len]);
}
}
return f[1][m];
由于题目的数据较大,所以我决定开long long。然而只得了60分,显然long long不是最优的。
这个时候我们可以开__int128来解决这个问题,然而不知什么原因我写挂了。。。
所以我们决定用高精度解决这个问题。
定义一个结构体gj,存下它的tot(位数)与num[i](每一位的数),将结果ans,区间f,以及读入的g都定义为gj类型,再手写operator 两个gj类型相加、一个gj一个int相乘与两个gj比较大小。写完之后本以为能顺利通过,然而编译错误,原因是输入时读入的是整型数组,不能直接用于gj。无奈,只好手写change函数,把int类型转化为gj类型。而后再写一个output函数来输出即可。结果在这里大概卡了一个小时,因为答案一直不正确,百思不得其解。最后明白了局部变量的初始值需要赋0,否则其空余部分的位不一定是0,可能会输出错误的答案。将change函数里的k.num memset后,还剩下一个点没有通过。给出一组反例1 3 0 0 0时没有输出,而答案应该是0。手动特判后通过了这道题目。
//高精度+动归
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstdlib>
#include<cstring>
using namespace std;
const int maxn=85;
int n,m;
struct gj
{
int tot;
int num[32];
};
gj ans;
gj f[maxn][maxn];
gj g[maxn][maxn];
gj operator + (const gj A,const gj B)
{
gj C;
memset(C.num,0,sizeof(C.num));
C.tot=max(A.tot,B.tot);
for(int i=1;i<=C.tot;i++)
{
C.num[i]+=A.num[i]+B.num[i];
if(C.num[i]>9)
{
C.num[i+1]+=C.num[i]/10;
C.num[i]%=10;
}
}
if(C.num[C.tot+1]!=0) C.tot++;
return C;
}
gj operator * (const gj A,const int x)
{
gj C;
memset(C.num,0,sizeof(C.num));
C.tot=A.tot;
for(int i=1;i<=C.tot;i++)
{
C.num[i]+=A.num[i]*x;
}
for(int i=1;i<=C.tot;i++)
{
if(C.num[i]>9)
{
C.num[i+1]+=C.num[i]/10;
C.num[i]%=10;
C.tot=max(C.tot,i+1);
}
}
return C;
}
gj max(const gj A, const gj B)
{
if(A.tot>B.tot) return A;
if(A.tot<B.tot) return B;
for(int i=A.tot;i>=1;i--)
{
if(A.num[i]>B.num[i]) return A;
else if(A.num[i]<B.num[i]) return B;
}
return A;
}
gj solve(gj a[]) //区间dp
{
memset(f,0,sizeof(f));
for(int len=0;len<=m;len++)
{
for(int i=1;i+len<=m;i++) //i+len 相当于区间的末尾
{
f[i][i+len]=max(f[i+1][i+len]+a[i],f[i][i+len-1]+a[i+len]);
f[i][i+len]=f[i][i+len]*2;
}
}
//cout<<f[0][0].tot<<endl;
return f[1][m];
}
gj change(int x)
{
int l=0;
gj k;
memset(k.num,0,sizeof(k.num));
while(x!=0)
{
l++;
k.num[l]=x%10;
x/=10;
}
k.tot=l;
return k;
}
void output(gj ans)
{
for(int i=ans.tot;i>=1;i--)
{
printf("%d",ans.num[i]);
}
printf("\n");
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
int temp;
scanf("%d",&temp);
g[i][j]=change(temp);
}
}
for(int i=1;i<=n;i++)
{
ans=ans+solve(g[i]);
// output(solve(g[i]));
}
if(ans.tot==0)
{
cout<<'0';
return 0;
}
output(ans);
return 0;
}
总体来说,这道题目比较困难,对动归的思考与认识需要到一定程度才能有所启发,还要随机应变熟练掌握高精度的写法。以后需要多练习类似的题目,从中获得启发。