JS算法之机器人的运动范围及剪绳子

338 阅读5分钟

这是我参与8月更文挑战的第20天,活动详情查看:8月更文挑战

机器人的运动范围

剑指Offer 13. 机器人的运动范围

地上有一个m行m列的方格,从坐标 [0, 0] 到坐标 [m-1, n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35,37],因为 3+5+3+7=18。但它不能进入方格 [35,38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

示例1:

 输入:m = 2, n = 3, k = 1
 输出:3

示例2:

 输入:m = 3, n = 1, k = 0
 输出:1

提示:

  • 1 <= n,m <= 100
  • 0 <= k <= 20

题解

法一 BFS

看到题目中有“向x移动”之类的字眼就会想到BFS。而且必有除方格临界值外的限制条件:“行坐标和列坐标的数位之和大于k”。

根据题意大致可以划分以下几个关键点:

  • 位数和的计算
  • 四周方向的遍历
  • 限制条件
  • 格子数的统计

逻辑梳理

位数和的计算

  1. 利用字符串

    将数值划分成由位数组成的数组,然后累加每个位数上的值。

 function getSum(num){
   let stringAry = num.toString().split('');
   return stringAry.reduce((a,b)=>Number(a)+Number(b),0);
 }
  1. 数学公式

    • 对数值进行基于10的取余,得到该位数上的值
    • 更新数值(更新后的数值 = 更新前的数值/10),就移除了之前的位数
    • 对更新后的数值进行相同的操作(取余,更新)
    • 当数值为0时,得出计算结果
 function getSum(num){
   let answer = 0;
   while(num){
     answer += num % 10;
     // 向下取整,因为可能出现小数
     num = Math.floor(num/10);
   }
   return answer;
 }

四周方向的遍历

可以使用 方向数组 来进行遍历辅助。

 // 方向数组
 const directionAry = [
     [-1, 0], // 上
     [0, 1], // 右
     [1, 0], // 下
     [0, -1] // 左
 ];

本题可以简化成“向右”和“向下”的遍历操作

限制条件

单元格坐标 [ i, j ]不能超过其方格的边界

  • i >= 0
  • j >= 0
  • i < m
  • j < m
  • getSum(offsetX) + getSum(offsetY) > k(题目规定)

此外,已经到达过的单元格,是不需要再纳入到到达个数统计到范畴内的,最便捷的做法就是使用Set,Set这一数据结构满足了 项的唯一性

格子数的统计

利用Set的优点即可。

代码实现

 /**
  * @param {number} m
  * @param {number} n
  * @param {number} k
  * @return {number}
  */
 var movingCount = function(m, n, k) {
     function getSum(num){
       let answer = 0;
       while(num){
         answer += num % 10;
         num = Math.floor(num / 10);
       }
       return answer;
     }
   // 方向数组
   const directionAry = [
     [-1,0], // 上
     [0,1],  // 右
     [1,0],  // 下
     [0,-1], // 左
   ];
   
   // 已经走过的坐标
   let set = new Set(['0,0']);
   
   // 遍历的坐标队列,题目要求从[0,0]开始走
   let queue = [[0,0]];
   
   // 遍历队列中的坐标
   while(queue.length){
     // 移除队首坐标
     let [x,y] = queue.shift();
     
     // 遍历方向
     for(let i=0;i<4;i++){
       let offsetX = x + directionAry[i][0];
       let offsetY = y + directionAry[i][1];
       
       // 临界值判断
       if(offsetX < 0 || offsetX >= m || offsetY < 0 || offsetY >= n || getSum(offsetX) + getSum(offsetY) > k || set.has(`${offsetX},${offsetY}`)){
         continue;
       }
       
       // 走过的格子就不再纳入统计
       set.add(`${offsetX},${offsetY}`);
       
       // 将该坐标加入队列(因为这个坐标的四周没有走过,需要纳入下次的遍历)
       queue.push([offsetX,offsetY]);
     }
   }
   // 走过坐标的个数就是可以到达的格子数
   return set.size;
 };

法二 DFS

 /**
  * @param {number} m
  * @param {number} n
  * @param {number} k
  * @return {number}
  */
 var movingCount = function(m,n,k){
   function getSum(num){
       let answer = 0;
       while(num){
         answer += num % 10;
         num = Math.floor(num / 10);
       }
       return answer;
     }
   
   const directedArr = [
     [1,0], // down
     [0,1]  // right
   ];
   
   var set = new Set(['0,0']);
   dfs(0,0,k);
   
   function dfs(x,y,k){
     for(let i = 0;i < 2;i++){
       let offsetX = x + directedArr[i][0];
       let offsetY = y + directedArr[i][1];
       if(offsetX<0 || offsetX>m-1 || offsetY<0 || offsetY>n-1 || getSum(offsetX) + getSum(offsetY) > k || set.has(`${offsetX},${offsetY}`)){
         continue;
       }
       set.add(`${offsetX},${offsetY}`);
       dfs(offsetX,offsetY,k);
     }
   }
   return set.size;
 }

剪绳子

剑指Offer 14 - I. 剪绳子

给你一根长度为n的绳子,请把绳子剪成整数长度的m段(m、n都是整数,n > 1并且m > 1),每段绳子的长度记为k[0],k[1]...k[m-1]。请问k[0] * k[1] * ... * k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

示例1:

 输入: 2
 输出: 1
 解释: 2 = 1 + 1, 1 × 1 = 1

示例2:

 输入: 10
 输出: 36
 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36

提示:2 <= n <= 58

题解

对7来说,可以拆成 3+4,最大乘积是12

对8来说,可以拆成 3+3+2,最大乘积是18

法一 动态规划

状态数组dp[i]表示:数字i拆分为至少两个正整数之和的最大乘积。为了方便计算,dp的长度是n+1,值初始化为1。

显然dp[2]等于1,外层循环应该从3开始遍历,一直到n停止。内存循环j从1开始遍历,一直到i之前停止,它代表数字 i 可以拆分成 j+(i-j)。但j*(i-j)不一定是最大乘积,因为i-j不一定大于dp[i-j](数字 i - j 拆分成整数之和的最大乘积),这里要选择最大的值作为 dp[i] 的结果。空间复杂度是O(N),时间复杂度O(N^2) 。代码实现如下:

 /**
  * @param {number} n
  * @return {number}
  */
 var cuttingRope = function(n){
   const dp = new Array(n+1).fill(1);
   for(let i = 3;i <= n;i++){
     for(let j = 1;j < i;++j){
       dp[i] = Math.max(dp[i],j * (i - j),j * dp[i - j]);
     }
   }
   return dp[n];
 }

法二 贪心法

找规律的思路,拆成多个2和3的和,保证乘积最大。因为2和3可以合成任何数字;根据贪心算法,尽量将原数拆成更多的3,然后再拆成更多的2,保证拆出来的整数的乘积结果最大。

但是有不足,如果整数n的形式是3k+1,如7时,照上面规则会被拆成“3+3+1“。应该被拆成”3+4“才对。

综上,算法的整体思路是:

  • n除3的结果为a,余数是b
  • 当b为0,直接将a个3相乘
  • 当b为1,直接将(a-1)个3相乘,再乘以4
  • 当b为2,将a个3相乘,再乘以2

空间复杂度为O(1),时间复杂度为O(1)。代码实现如下:

 /**
  * @param {number} n
  * @return {number}
  */
 var cuttingRope = function(n){
   if(n===2) return 1;
   if(n===3) return 2;
   // a的含义:n能拆成的3的个数
   const a = Math.floor(n/3);
   const b = n % 3;
   
   // n是3的倍数
   if(b===0) return Math.pow(3,a);
   // n是3k+1
   if(b===1) return Math.pow(3,a-1) * 4;
   return Math.pow(3,a) * 2;
 }

坚持每日一练!前端小萌新一枚,希望能点个哇~