牛客小白月赛22 - 仓库选址(二维前缀和)
链接:https://ac.nowcoder.com/acm/contest/4462/G
来源:牛客网
空间限制:C/C++ 262144K,其他语言524288K
64bit IO Format: %lld
题目描述
输入描述:
输出描述:
每组输入在一行中输出答案。
输入
3 2 2 1 1 1 0 4 4 0 8 2 0 1 4 5 0 0 1 0 1 3 9 2 0 6 7 0 0 0 0 0 0 0 1 0 3 0 1 2 9 1 2 1 2 8 7 1 3 4 3 1 0 2 2 7 7 0 1 0 0 1 0 0 0 0 0 0 0
输出
2 55 162
备注:
题意是找到一个位置,使得其它所有位置上的数乘以两个位置之间的距离的总和最小。
由于时间给4s,所以直接暴力枚举每一个位置然后取一个最小值即可,最后耗时约2000ms。
1 #include <bits/stdc++.h> 2 typedef long long LL; 3 const int INF=0x3f3f3f3f; 4 const double eps =1e-8; 5 const int mod=1e9+7; 6 const int maxn=1e5+10; 7 using namespace std; 8 9 10 int G[105][105]; 11 12 int main() 13 { 14 #ifdef DEBUG 15 freopen("sample.txt","r",stdin); 16 #endif 17 18 int T; 19 scanf("%d",&T); 20 while(T--) 21 { 22 int n,m; 23 scanf("%d %d",&m,&n); 24 for(int i=1;i<=n;i++) 25 { 26 for(int j=1;j<=m;j++) 27 scanf("%d",&G[i][j]); 28 } 29 int ans=INF; 30 for(int x=1;x<=n;x++) 31 { 32 for(int y=1;y<=m;y++) 33 { 34 int sum=0; 35 for(int i=1;i<=n;i++) 36 { 37 for(int j=1;j<=m;j++) 38 { 39 sum+=G[i][j]*(abs(x-i)+abs(y-j)); 40 } 41 } 42 ans=min(ans,sum); 43 } 44 } 45 printf("%d\n",ans); 46 } 47 48 return 0; 49 }
但是,暴力太对不起这题了,故我们用二维前缀和来重写这题,最后耗时约20ms。
我们首先暴力处理出仓库在(1, 1)花费sum,再利用二维前缀和处理运输次数ci[i][j],然后枚举每一位置的花费。
假设仓库当前在(x, y),当仓库移动到(x, y+1)时,那么从(1, 1)到(n, y)的子矩阵移动距离+1,从(1, y+1)到(n, m)的子矩阵移动距离-1。所以从(1, 1)到(n, y)的矩阵区域内一共要运输多少次,总花费就加多少,因为每次运输的距离都要+1,而从(1, y+1)到(n, m)的矩阵区域内一共要运输多少次,总花费就减多少,因此可以由(x, y)的花费求出(x, y+1)的花费
同样考虑(x, y)移动到(x+1, y)的情况,从(1, 1)到(x, m)的子矩阵移动距离+1,从(x+1, 1)到(n, m)的子矩阵移动距离-1。
根据代码和注释理解吧:
1 #include <bits/stdc++.h> 2 typedef long long LL; 3 const int INF=0x3f3f3f3f; 4 const double eps =1e-8; 5 const int mod=1e9+7; 6 const int maxn=1e5+10; 7 using namespace std; 8 9 int ci[105][105];//二维前缀和求出(1,1)到(i,j)的运输次数和 10 11 int cal(int x1,int y1,int x2,int y2)//计算从左上角(x1, y1)到右下角(x2, y2)矩阵区域的运输次数和 12 { 13 return ci[x2][y2]-ci[x1-1][y2]-ci[x2][y1-1]+ci[x1-1][y1-1]; 14 } 15 16 int main() 17 { 18 #ifdef DEBUG 19 freopen("sample.txt","r",stdin); 20 #endif 21 22 int T; 23 scanf("%d",&T); 24 while(T--) 25 { 26 int n,m; 27 scanf("%d %d",&m,&n); 28 int sum=0; 29 for(int i=1;i<=n;i++) 30 { 31 for(int j=1;j<=m;j++) 32 { 33 int x; 34 scanf("%d",&x); 35 sum+=x*(i-1+j-1);//把该处的花费累加到仓库在(1,1)处的总花费上 36 ci[i][j]=ci[i-1][j]+ci[i][j-1]-ci[i-1][j-1]+x;//二维前缀和求出(1,1)到(i,j)的运输次数和 37 } 38 } 39 int ans=INF; 40 for(int i=1;i<=n;i++) 41 { 42 int t=sum; 43 for(int j=1;j<=m;j++)//根据该行首(i,1)的花费,遍历这一行每一列的花费 44 { 45 ans=min(ans,t); 46 t+=cal(1,1,n,j)-cal(1,j+1,n,m); 47 } 48 sum+=cal(1,1,i,m)-cal(i+1,1,n,m);//sum每次更新相当于求出了仓库在(1,1),(2,1)...(n,1)的花费 49 } 50 printf("%d\n",ans); 51 } 52 53 return 0; 54 }
最后给出我看到的有意思的:
该方法来自参考于牛客用户Randolph、的博客
数学方法(O(NM))
如果数据范围较大,方法一恐怕是过不了的。做过的同学可能会想到《算法竞赛进阶指南》0x05中的货仓选址,这一题是给出每个地点的坐标,每个地方只需去一次,求将货仓建在哪个坐标上到各点距离和最小,答案中货仓的坐标就是所有坐标中的中位数(所有数与中位数的绝对差之和最小。具体证明请看其题解,我也写了一篇哦qwq!,这里就不赘述了)
那么,我们可以用货仓选址中的中位数来解决这题吗?货仓选址与这一题很相似,但也有不同,有什么不同呢?
- 每个地方可能去多次:
我们想到,其实没必要按照题目中的一个地方去多次,可以改成多个地方,每个地方只去一次,这样就可做了 - 二维坐标:
想想能不能由一维的情况拓展到二维呢?其实也是可以的,我们先把每一行都看做一个整体,变成一维,再用中位数的方法找到最优行(即仓库选在这一行到其他行之间的距离和最小,其实就是选在这一行的仓库到其他行中地点的竖直距离和最小);然后再把每一列都看做一个整体做一遍,得到最优列(即仓库选在这一列到其他列之间的距离和最小,也就是选在这一列的仓库到其他列中地点的水平距离和最小),那么最终仓库在哪最优呢?当然是最优行与最优列的交点洛!
当然数学方法可能不太好理解,可以看看上面的方法二
1 #include<cstdio> 2 #include<cstring> 3 #include<cmath> 4 using namespace std; 5 int s[110][2],x[110],y[110],ans[2],sum; 6 inline void get_pos(int num,int lim) {//求中位数到底位于哪一行或那一列,当然你不用二分直接扫一遍s数组判断也可以 7 int l=1,r=lim; 8 while(l<=r) { 9 int mid=(l+r)/2; 10 if (s[mid][num]<sum/2) l=mid+1;//sum/2,中位数,也就是最优的位置 11 else r=mid-1,ans[num]=mid;//返回最优行最优列的位置 12 } 13 } 14 int main() { 15 int T,n,m,_ans; 16 scanf("%d",&T); 17 while(T--) { 18 scanf("%d%d",&m,&n),sum=_ans=0; 19 memset(x,0,sizeof x); 20 memset(y,0,sizeof y); 21 for (int i=1,a; i<=n; i++) 22 for (int j=1; j<=m; j++) { 23 scanf("%d",&a),sum+=a;//sum统计一共有多少个地方,实际上把一个地方去a次,改成了a个地方,每个地方只去一次 24 x[i]+=a,y[j]+=a;//x,y分别统计每行、每列的数字和,即把每行、每列看为一个整体 25 } 26 for (int i=1; i<=n; i++) s[i][0]=s[i-1][0]+x[i]; 27 for (int i=1; i<=m; i++) s[i][1]=s[i-1][1]+y[i];//前缀和 28 get_pos(0,n),get_pos(1,m);//找最优行与最优列 29 for (int i=1; i<=n; i++) _ans+=abs(ans[0]-i)*x[i];//计算所有竖直距离 30 for (int i=1; i<=m; i++) _ans+=abs(ans[1]-i)*y[i];//计算所有水平距离 31 //当然这个计算答案用二维的(abs(x - i) + abs(y - j))也可以 32 printf("%d\n",_ans); 33 } 34 }
-