小奇的矩阵 题解

版权声明:八月炊火的博客如需转载请注明出处 https://blog.csdn.net/qq_34990731/article/details/83188702

小奇的矩阵
题目:
题目背景:
小奇总是在数学课上思考奇怪的问题。
问题描述:
给定一个n*m的矩阵,矩阵中的每个元素aij为正整数。
接下来规定
1.合法的路径初始从矩阵左上角出发,每次只能向右或向下走,终点为右下角。
2.路径经过的n+m-1个格子中的元素为A1,A2…A(n+m-1),Aavg为Ai的平均数,路径的V值为(n+m-1)*∑(Ai-Aavg) ^2 (1<=i<=n+m-1) 求V值最小的合法路径,输出V值即可,有多组测试数据。
输入格式:
第一行包含一个正整数T,表示数据组数。
对于每组数据:
第一行包含两个正整数n和m,表示矩阵的行数和列数。
接下来n行,每行m个正整数aij,描述这个矩阵。
输出格式:
对于每次询问,输出一行一个整数表示要求的结果
样例输入:
1
2 2
1 2
3 4
样例输出:
14
数据范围:
对于30%的数据 n<=10,m<=10
有另外40%的数据 n<=15 m<=15,矩阵中的元素不大于5
对于100%的数据 T<=5,n<=30,m<=30,矩阵中的元素不大于30

知识点: 数学、DP

讲解:
这一题太坑了,刚开始完全没看懂(这就是你打暴力的理由),后面看了题解才清楚,原来这么奇妙。暴力就直接枚举m+n-1个的排列顺序,期望的分30,现在讲一下正解,我们可以发现我们唯一不确定的就是总长度,而这又是最重要的所以我们考虑暴力枚举所有的总长,然后我们就可以根据总长去DP出每一个点对应的∑(Ai-Aavg) ^2 (1<=i<=n+m-1)(对于n+m-1我们最后乘上去就好了),然后取最小值,不过现在的问题是枚举的长度要相同才可以转移,如果还要判断相同的话时间复杂度又会往上提,就TLE了,不过我们可以证明不管是不是相同我们都转移对答案无影响,我们设实际的长度为len,与枚举长度相差x,现在这个点的值是A,然后我们计算的是∑(A-len±x)^2 , 而实际的值是∑(A-len)^2 , 如果我们可以证明∑(A-len)^2 <= ∑(A-len±x)^2 的话因为我们取最小值,那么对我们答案也不会有影响,我们把两遍化简发现只剩下n*x^2>=0,这样这个等式是恒成立的所以我们就可以这样做,首先枚举所有可能的总长,然后去DP计算。对于这一题还有一种做法会在下面讲到。
代码:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int map[40][40],n,m;
double ans=56100,f[40][40];
double sqr(double x)
{
	return x*x;
}
void dp(int x)
{
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			f[i][j]=56100;//先初始化为一个极大值 
	double avg=(double)x/(n+m-1);//计算我们的平均值 
	f[1][1]=sqr(map[1][1]-avg);//第一个位置初始化 
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			if(f[i][j]!=56100)//如果这个格子可以到,那么我们就用它来更新别人 
			{
				f[i+1][j]=min(f[i+1][j],f[i][j]+sqr(map[i+1][j]-avg));//更新选最小 
				f[i][j+1]=min(f[i][j+1],f[i][j]+sqr(map[i][j+1]-avg));
			}
	ans=min(ans,f[n][m]);//选最小的ans 
}
int main()
{
	int T;
	scanf("%d",&T);//多组数据 
	while(T--)
	{
		ans=56100;//初始化 
		scanf("%d %d",&n,&m);
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++)
				scanf("%d",&map[i][j]);//读入 
		for(int i=1;i<=1800;i++)//枚举所有可能的总长,因为格子最多59个,每个最多30,所以我们可以计算出这个值 
			dp(i);
		printf("%d\n",(int)(ans*(n+m-1)+1e-5));//1e-5也就是0.000001,是一个精度差,因为小数运算会掉精度所以要加 
	}
	return 0;
}

方法2:
我们把那个公式拆开可以得到N*∑(A ^ 2)-len^2,这样我们可以去三维DP,大体思路和上一种方法相同,因为对于len我们已经提出来了,所以我们只要多加一维计算在每个len情况下∑(A ^ 2)的最小值就好了,我们定义DP[k][i][j]表示在长度为k是第i行第j列的最小值,这样我们去DP一下然后统计最小的答案就好了。

代码:

#include<iostream>
#include<cstdio> 
using namespace std;
int map[40][40],n,m;
double ans=1000000000,f[1810][40][40];
void dp()
{
	f[map[1][1]][1][1]=map[1][1]*map[1][1];//边界 
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			for(int k=0;k<=1800;k++)//暴力DP 
				if(f[k][i][j]!=1000000)//如果这个点走不到的话我们就不可以拿它来 
				{
					f[k+map[i+1][j]][i+1][j]=min(f[k+map[i+1][j]][i+1][j],f[k][i][j]+map[i+1][j]*map[i+1][j]);//更新一下 
					f[k+map[i][j+1]][i][j+1]=min(f[k+map[i][j+1]][i][j+1],f[k][i][j]+map[i][j+1]*map[i][j+1]);
				}
	for(int i=1;i<=1800;i++)//选择所有答案中最小的 
		if(f[i][n][m]!=1000000)
			ans=min(ans,(m+n-1)*f[i][n][m]-i*i);//计算一下 
}
int main()
{
	int T;
	scanf("%d",&T);//多组数据 
	while(T--)
	{
		ans=1000000000;//初始化 
		scanf("%d %d",&n,&m);
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++)
				scanf("%d",&map[i][j]);//读入数据 
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++)
				for(int k=0;k<=1800;k++)
					f[k][i][j]=1000000;//初始化 
		dp();//DP一波 
		printf("%d\n",(int)ans);//int类型注意转化 
	}
	return 0;
}

希望大家可以AC。

猜你喜欢

转载自blog.csdn.net/qq_34990731/article/details/83188702