【题解】洛谷P1005 矩阵取数(dp 高精)

2007年NOIP提高组第三题 难度还是比较大的

首先来看一下题意:

题目描述

帅帅经常跟同学玩一个矩阵取数游戏:对于一个给定的 n \times mn×m 的矩阵,矩阵中的每个元素 a_{i,j}ai,j 均为非负整数。游戏规则如下:

  1. 每次取数时须从每行各取走一个元素,共 nn 个。经过 mm 次后取完矩阵内所有元素;
  2. 每次取走的各个元素只能是该元素所在行的行首或行尾;
  3. 每次取数都有一个得分值,为每行取数的得分之和,每行取数的得分 = 被取走的元素值 \times 2^i×2i ,其中 ii表示第 ii 次取数(从 11 开始编号);
  4. 游戏结束总得分为 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 301n,m30 ,答案不超过 10^{16}1016
100%的数据满足: 1\le n, m \le 801n,m80 , 0 \le a_{i,j} \le 10000ai,j1000


题解:

对于这个问题,我首先进行猜想。因为每一行要从首尾取数,而取出来的数字要乘以一个系数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][j1]+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;
}
总体来说,这道题目比较困难,对动归的思考与认识需要到一定程度才能有所启发,还要随机应变熟练掌握高精度的写法。以后需要多练习类似的题目,从中获得启发。

猜你喜欢

转载自blog.csdn.net/rem_inory/article/details/81007815