题解 | #下棋#

下棋

http://www.nowcoder.com/practice/ddb95ccacada4024905e5a4f740c56a1

题目描述

图片说明
图片说明

题目难度:中等
通过率:24.02%
知识点:模拟

题目分析

这是一个典型的代码模拟现实的题目,通过数据结构抽象模拟出棋盘,并根据规则计算出答案;
读完题干提炼出以下内容:

  1. 题目围绕一个n*n的正方形棋盘展开;
  2. 棋盘的构成是按照数字螺旋递增构成,数字从1开始;
  3. 牛牛每次投掷骰子,结果作为步长,即会前进1-6步;
  4. 假设现在的数字是i, 走j步后数字的计算公式为(i+j-1)%(n*n) +1 ,说直接点就是数字走到n*n之后就回到1开始继续走,例如3*3的棋盘,走到9的下一步就是1;
  5. 题目中给出的计算公式转成文字描述就是以当前节点为中心,横竖一个步长的范围内的数据总和。
    针对第五点举个例子:
    假设是一个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]。

解题步骤:

我们把解题步骤大致分两步,

  1. 画出棋盘
  2. 计算范围并求和

画出棋盘

直接上代码了,这个比较简单
(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为查询长度。
空间复杂度:,棋盘空间

全部评论

相关推荐

我即大橘:耐泡王
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务