题目描述
题目难度:中等
通过率:24.02%
知识点:模拟
题目分析
这是一个典型的代码模拟现实的题目,通过数据结构抽象模拟出棋盘,并根据规则计算出答案;
读完题干提炼出以下内容:
- 题目围绕一个n*n的正方形棋盘展开;
- 棋盘的构成是按照数字螺旋递增构成,数字从1开始;
- 牛牛每次投掷骰子,结果作为步长,即会前进1-6步;
- 假设现在的数字是i, 走j步后数字的计算公式为
(i+j-1)%(n*n) +1,说直接点就是数字走到n*n之后就回到1开始继续走,例如3*3的棋盘,走到9的下一步就是1; - 题目中给出的计算公式转成文字描述就是以当前节点为中心,横竖一个步长的范围内的数据总和。
针对第五点举个例子:
假设是一个4*4 的矩阵,当前走到了14
若本次投掷为步长为1,则该公式需要累加的范围如下图所示:
若投掷步长为2,则对应累加范围是:
假设当前坐标为 i,j 步长为k,则累加范围是:
i-k+1 到 i+k-1 行的所有数字(边界上限制是n-1列,下线为0)
加上
j-k+1 到 j+k-1 列的所有数字(边界上限制是n-1列,下线为0)
但是同一个数字只会加一次,不会重复加。
拿示例的例子来说
输入:
4,[1,2,5,6,2]
复制
返回值:
[48,138,274,410,510]
棋盘数字如下图所示:
第一步1,走到了数字2,坐标为(0,1),由于步长为1 ,所以计算范围是第0行和第1列的所有数字之和,如图所示:
所以累计分数为:0(题干说初始在1的分数为0)+1+2+3+4+13+16+9 = 48第二步,步长为2,即从2走到了4,此时计算范围是
所以本次累计和为: 48(上次结果)+ 1+2+3+4+12+13+14+5+15+6+8+7 = 138;
- 第三步,步长为5,从4走到9,计算范围是全部数字(因为计算后的区域覆盖了全棋盘)
所以本次累计和为: 138(上次结果)+ 1+2+3+..+16 = 274;
第四步,步长为6,从9走到了15,计算范围仍然覆盖了全部数字,
所以本次累计和为: 274(上次结果)+ 1+2+3+..+16 = 410;第五步,步长为2,从15 走一步到16 再走一步到 1,计算范围是:
所以本次累计和为: 410(上次结果)+ 1+2+3+4+12+13+14+5+11+16+10+9 = 510;
所以返回结果是[48,138,274,410,510]。
解题步骤:
我们把解题步骤大致分两步,
- 画出棋盘
- 计算范围并求和
画出棋盘
直接上代码了,这个比较简单
(index是我为了快速获取某个数字对应的下标所建的一个索引结构。这里可以先忽略)
public static int[][] drawChessboard(int n){
int[][] chessboard = new int[n][n];
// 当前未填充的棋盘边界,
int max_j = n-1, max_i = n-1, min_i = 0, min_j = 0;
int count = 1;// 从1开始填充数字
// 当还有空间可以填充,则继续填充
while ((min_i<=max_i)||(min_j<=max_j)){
for(int j=min_j;j<=max_j;j++){
chessboard[min_i][j] = count;
index[count] = new int[]{min_i,j};//记录对应count的坐标
count++;
}
min_i++;// min_i行已经填充完
for(int i=min_i;i<=max_i;i++){
chessboard[i][max_j] = count;
index[count] = new int[]{i,max_j};
count++;
}
max_j--;// max_j列已经填充完
for(int j=max_j;j>=min_j;j--){
chessboard[max_i][j] = count;
index[count] = new int[]{max_i,j};
count++;
}
max_i--;// max_i行已经填充完
for(int i=max_i;i>=min_i;i--){
chessboard[i][min_j] = count;
index[count] = new int[]{i,min_j};
count++;
}
min_j++;// min_j行已经填充完
}
return chessboard;
}计算范围并求和的方法:
暴力法
按照常规的思路去遍历累加棋盘的数字,相关计算的关键代码如:
// chessboard表示已填充好的棋盘, fi,fj表示当前所在坐标,scope代表步长
private int calculateScope(int[][] chessboard, int fi,int fj, int scope) {
int sum = 0;
int n = chessboard.length;
// 每个数字只能累加一次,所以这里记录下是否加过了
int[][] visited = new int[n][n];
for(int i=Math.max(0,fi-scope+1);i<=Math.min(n-1,fi+scope-1);i++){
//累加范围内行的所有数字
for(int j=0;j<n;j++){
sum+=chessboard[i][j];
visited[i][j] = 1;
}
}
for(int j=Math.max(0,fj-scope+1);j<=Math.min(n-1,fj+scope-1);j++){
//累加范围内列的所有数字
for(int i=0;i<n;i++){
// 确保每个数只加一次
if(visited[i][j]!=1){
sum+=chessboard[i][j];
}
}
}
return sum;
}但是这个在时间耗时较多,提交会出现计算超时的问题。
预计算法
我们发现,计算的范围都是正行整列,暴力法中涉及很多重复计算,其实棋盘n并不大,我们完全可以提前将每一列和行的和计算出来,但是在具体计算时会涉及减去交叉的部分,但是由于步长1-6分布,所以交叉的部分最大也就是6*6的规模,相对来说就可以忽略了。
所以需要一个预计算的函数,这里需要特别注意取模,防止越界(暴力法中忽略了这个点)
public Map<String,Integer> preCalculate2(int[][] chessboard){
int n = chessboard.length;
Map<String,Integer> preSums = new HashMap<>(n*2);
// 计算每一行的和
for(int i=0;i<n;i++){
long lineSum = 0;
for(int j=0;j<n;j++){
lineSum+=chessboard[i][j];
preSums.put("line:"+i,(int)(lineSum%1000000007));
}
}
// 计算每一列的和
for(int j=0;j<n;j++){
long colomnSum = 0;
for(int i=0;i<n;i++){
colomnSum+=chessboard[i][j];
preSums.put("colomn:"+j,(int)(colomnSum%1000000007));
}
}
return preSums;
}计算好之后我们再看下计算范围的函数应该怎么写:
private int calculateScope2(int[][] chessboard, int fi,int fj, int scope, Map<String, Integer> preSums) {
long sum = 0;
int n = chessboard.length;
// 直接加上范围行的和
for(int i=Math.max(0,fi-scope+1);i<=Math.min(n-1,fi+scope-1);i++){
sum+=preSums.get("line:"+i);
}
// 直接加上范围列的和
for(int j=Math.max(0,fj-scope+1);j<=Math.min(n-1,fj+scope-1);j++){
sum+=preSums.get("colomn:"+j);
}
// 减去交叉部分的数字
for(int i=Math.max(0,fi-scope+1);i<=Math.min(n-1,fi+scope-1);i++){
for(int j=Math.max(0,fj-scope+1);j<=Math.min(n-1,fj+scope-1);j++){
sum-=chessboard[i][j];
}
}
// 同样要注意和的范围别越界
return (int)(sum%1000000007);
}完整代码:
这里包含了一些无用代码,但可以体现出整个题目的优化过程,所以保留下来了并做了注释。
import java.util.*;
public class Solution {
/**
* 求每一步后的总分数,所有分数都对1,000,000,007取模
* @param n int整型 棋盘大小
* @param query int整型一维数组 每次掷出的点数的列表
* @return int整型一维数组
*/
// 因为一开始是用map结果保存坐标信息的,但是这样会遇到内存超出范围的问题,所以后续优化成了int[][]来记录下标
//public static Map<Integer,String> index = new HashMap<>();
public static int[][] index;
public int[] getScores (int n, int[] nums) {
int[] re =new int[nums.length];
int n2 = n*n;
index = new int[n * n + 1][2];
// 画棋盘
int[][] chessboard = drawChessboard(n);
// 预计算,计算出整列和整行的和
Map<String,Integer> preSums2 = preCalculate2(chessboard);
// 当前走到的数字
int cur_num = 1;
// 当前的累计和,为了防止越界先用long记录,后面取模
long cur_sum = 0;
for(int i=0;i<nums.length;i++){
int adder = nums[i];// 步长
cur_num = cur_num+adder;// 当前所在数字
cur_num = cur_num % n2==0 ? n2:cur_num%n2;// 走到上限从1开始
cur_sum += calculateScope(chessboard,cur_num,adder,preSums2);//累积求和
cur_sum = cur_sum%1000000007;//计算当前值
re[i] = (int)cur_sum;//保存该步累积求和数据
}
return re;
}
// 暴力法遍历求和
private int calculateScope(int[][] chessboard, int fi,int fj, int scope) {
int sum = 0;
int n = chessboard.length;
int[][] visited = new int[n][n];
for(int i=Math.max(0,fi-scope+1);i<=Math.min(n-1,fi+scope-1);i++){
for(int j=0;j<n;j++){
sum+=chessboard[i][j];
visited[i][j] = 1;
}
}
for(int j=Math.max(0,fj-scope+1);j<=Math.min(n-1,fj+scope-1);j++){
for(int i=0;i<n;i++){
if(visited[i][j]!=1){
sum+=chessboard[i][j];
}
}
}
return sum;
}
// 预计算法求和
private int calculateScope2(int[][] chessboard, int fi,int fj, int scope, Map<String, Integer> preSums) {
long sum = 0;
int n = chessboard.length;
for(int i=Math.max(0,fi-scope+1);i<=Math.min(n-1,fi+scope-1);i++){
sum+=preSums.get("line:"+i);
}
for(int j=Math.max(0,fj-scope+1);j<=Math.min(n-1,fj+scope-1);j++){
sum+=preSums.get("colomn:"+j);
}
for(int i=Math.max(0,fi-scope+1);i<=Math.min(n-1,fi+scope-1);i++){
for(int j=Math.max(0,fj-scope+1);j<=Math.min(n-1,fj+scope-1);j++){
sum-=chessboard[i][j];
}
}
return (int)(sum%1000000007);
}
private int calculateScope(int[][] chessboard, int cur_num, int scope, Map<String, Integer> preSums) {
// 按照map记录下标对应的处理代码
// String[] ijs = index.get(cur_num+"").split(",");
// int fi = Integer.parseInt(ijs[0]);
// int fj = Integer.parseInt(ijs[1]);
int fi = index[cur_num][0];
int fj = index[cur_num][1];
//pre1 return preSums.get(fi+","+fj+","+scope);
return calculateScope2(chessboard, fi, fj,scope, preSums);
}
// 画棋盘
public static int[][] drawChessboard(int n){
int[][] chessboard = new int[n][n];
int max_j = n-1, max_i = n-1, min_i = 0, min_j = 0;
int count = 1;
while ((min_i<=max_i)||(min_j<=max_j)){
for(int j=min_j;j<=max_j;j++){
chessboard[min_i][j] = count;
index[count] = new int[]{min_i,j};
//index.put(count+"",min_i+","+j);
count++;
}
min_i++;
for(int i=min_i;i<=max_i;i++){
chessboard[i][max_j] = count;
index[count] = new int[]{i,max_j};
//index.put(count+"",i+","+max_j);
count++;
}
max_j--;
for(int j=max_j;j>=min_j;j--){
chessboard[max_i][j] = count;
index[count] = new int[]{max_i,j};
//index.put(count+"",max_i+","+j);
count++;
}
max_i--;
for(int i=max_i;i>=min_i;i--){
chessboard[i][min_j] = count;
index[count] = new int[]{i,min_j};
//index.put(count+"",i+","+min_j);
count++;
}
min_j++;
}
return chessboard;
}
//预计算之前还想了一个办法,就是对每个节点从步长1-6都预计算出来和,这样都不用按行按列加了,直接取值就行,但是这样时间会超时,于是后面优化成上面提到的那种办法
public Map<String,Integer> preCalculate(int[][] chessboard){
int n = chessboard.length;
Map<String,Integer> preSums = new HashMap<>();
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
for(int k=1;k<=6;k++){
String key = i+","+j+","+k;
preSums.put(key,calculateScope(chessboard,i,j,k));
}
}
}
return preSums;
}
// 预计算出各行列的和
public Map<String,Integer> preCalculate2(int[][] chessboard){
int n = chessboard.length;
Map<String,Integer> preSums = new HashMap<>(n*2);
for(int i=0;i<n;i++){
long lineSum = 0;
for(int j=0;j<n;j++){
lineSum+=chessboard[i][j];
preSums.put("line:"+i,(int)(lineSum%1000000007));
}
}
for(int j=0;j<n;j++){
long colomnSum = 0;
for(int i=0;i<n;i++){
colomnSum+=chessboard[i][j];
preSums.put("colomn:"+j,(int)(colomnSum%1000000007));
}
}
return preSums;
}
}时间复杂度:,n表示棋盘大小,m为查询长度。
空间复杂度:,棋盘空间

京公网安备 11010502036488号