• 【好书推荐】《剑指Offer》之硬技能(编程题12~16)


    本文例子完整源码地址:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/sword

    《【好书推荐】《剑指Offer》之软技能》

    《【好书推荐】《剑指Offer》之硬技能(编程题1~6)》

    《【好书推荐】《剑指Offer》之硬技能(编程题7~11)》

    持续更新,敬请关注公众号:coderbuff,回复关键字“sword”获取相关电子书。

    12.矩阵中的路径

    题目:请设计一个函数,用来判断一个矩阵中是否存在一条包含其字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。

    *回溯法:适合由多个步骤组成的问题,并且每个步骤有多个选项。

     1 /**
     2 * 矩阵中是否存在给定路径
     3 * @author OKevin
     4 * @date 2019/6/4
     5 **/
     6 public class Solution {
     7 
     8    /**
     9     *
    10     * @param matrix 一位数组表示矩阵
    11     * @param rows 行数
    12     * @param cols 列数
    13     * @param path 路径
    14     * @return true-存在;false-不存在
    15     */
    16    public boolean findPath(char[] matrix, Integer rows, Integer cols, char[] path) {
    17        if (matrix == null || rows <= 0 || cols <= 0 || path == null) {
    18            return false;
    19        }
    20        boolean[] visited = new boolean[rows * cols];
    21        int pathLength = 0;
    22        for (int row = 0; row < rows; row++) {
    23            for (int col = 0; col < cols; col++) {
    24                if (findPathCore(matrix, rows, cols, row, col, path, pathLength, visited)) {
    25                    return true;
    26                }
    27            }
    28        }
    29        return false;
    30    }
    31 
    32    private boolean findPathCore(char[] matrix, Integer rows, Integer cols, int row, int col, char[] path, int pathLength, boolean[] visited) {
    33        if (pathLength == path.length) {
    34            return true;
    35        }
    36        if (row >= 0 && row < rows && col >= 0 && col < cols && matrix[row * cols + col] == path[pathLength] && !visited[row * cols + col]) {
    37            visited[row * cols + col] = true;
    38            pathLength++;
    39            if (findPathCore(matrix, rows, cols, row, col - 1, path, pathLength, visited)
    40                    || findPathCore(matrix, rows, cols, row - 1, col, path, pathLength, visited)
    41                    || findPathCore(matrix, rows, cols, row, col + 1, path, pathLength, visited)
    42                    || findPathCore(matrix, rows, cols, row + 1, col, path, pathLength, visited)) {
    43                return true;
    44            }
    45            visited[row * cols + col] = false;
    46 
    47        }
    48        return false;
    49    }
    50 }

    13.机器人的运动范围

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

    此题有一个小的点需要靠平时的积累,数位和的计算。

     1 /**
     2 * 计算数位和
     3 * 例如:85的数位和为8+5=13
     4 * 计算过程:
     5 * 85 % 10 = 5(个位)
     6 * 85 / 10 = 8(移除个位)
     7 * 8 % 10 = 8(十位)
     8 * 5 + 8 = 13
     9 * @param number 数字
    10 * @return 数位和
    11 */
    12 private int getDigitSum(int number) {
    13    int sum = 0;
    14    while (number > 0) {
    15        sum += number % 10;
    16        number /= 10;
    17    }
    18    return sum;
    19 }

    另外还需要注意几个临界条件:

    1. 访问的行和列一定是大于等于0;

    2. 访问的行和列一定是小于总行数和总列数(并不是小于等于,因为是从第0行开始)

    3. 行和列的数位和小于阈值

    4. 没有被访问过

    row >= 0 && row < rows && col >= 0 && col < cols && (getDigitSum(row) + getDigitSum(col) < threshold) && !visited[row * cols + col]

    题目中看似提到了m行n列,立马想到了用二维数字来表示。实际上如果用二维数组是增加了复杂性,用一维数组同样能表示出二维数组。例如:m行n列就一共又m*n个元素,visited[m*n]。访问第1行第1列,在一维数组中则为visited[1*m+1],访问第1行第2列则为visited[1*m+2],也就是在一位数组中,数据是按照一列一列存放的。如果要访问第2行是2*cols+第几列。

    另外既然需要求出达到多少个格子,则是需要访问格子周围即:(i - 1, j)、(i, j - 1)、(i + 1, j)、(i, j + 1)。

     1 /**
     2 * Description:
     3 * 机器人的运动范围
     4 * 2019-06-18
     5 * Created with OKevin.
     6 */
     7 public class Solution {
     8    public int movingCount(int threshold, int rows, int cols) {
     9        if (threshold < 0 || rows <= 0 || cols <= 0) {
    10            return 0;
    11        }
    12        boolean[] visited = new boolean[rows * cols];
    13        int count = movingCountCore(threshold, rows, cols, 0, 0, visited);
    14        return count;
    15    }
    16 
    17    private int movingCountCore(int threshold, int rows, int cols, int row, int col, boolean[] visited) {
    18        int count = 0;
    19        if (check(threshold, rows, cols, row, col, visited)) {
    20            visited[row * cols + col] = true;
    21            /**
    22             * 当前访问到了(i, j)坐标,此时则继续访问(i - 1, j)、(i, j - 1)、(i + 1, j)、(i, j + 1)
    23             */
    24            count = 1 + movingCountCore(threshold, rows, cols, row - 1, col, visited) + movingCountCore(threshold, rows, cols, row, col-1, visited) + movingCountCore(threshold, rows, cols, row + 1, col, visited) + movingCountCore(threshold, rows, cols, row + 1, col, visited);
    25        }
    26        return count;
    27    }
    28 
    29    private boolean check(int threshold, int rows, int cols, int row, int col, boolean[] visited) {
    30        //横坐标与纵坐标的数位和相加小于阈值,且没有访问过
    31        if (row >= 0 && row < rows && col >= 0 && col < cols && (getDigitSum(row) + getDigitSum(col) <= threshold) && !visited[row * cols + col]) {
    32            return true;
    33        }
    34        return false;
    35    }
    36 
    37    /**
    38     * 计算数位和
    39     * 例如:85的数位和为8+5=13
    40     * 计算过程:
    41     * 85 % 10 = 5(个位)
    42     * 85 / 10 = 8(移除个位)
    43     * 8 % 10 = 8(十位)
    44     * 5 + 8 = 13
    45     * @param number 数字
    46     * @return 数位和
    47     */
    48    private int getDigitSum(int number) {
    49        int sum = 0;
    50        while (number > 0) {
    51            sum += number % 10;
    52            number /= 10;
    53        }
    54 
    55        return sum;
    56    }
    57 }

    14.剪绳子

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

    这道题是求解最优化问题。理论上讲,在题目中出现最大、最小、一共有多少种解法都可以用动态规划求解。

    解法一:动态规划

    拿到这道题,习惯性的可能会先从由上往下的解题思路去想,比如:长度为9,可以分为几段:1,1,7;1,2,6等等。会去思考这个长度会分成几个段,再将每个段的乘积求出来,取最大的那个段。

    但实际上,对于求解最优化问题,可以转换为一系列子问题。对于本题一段绳子来讲,它无论如何都至少被切为2段。例如长度为8时,可能被切为:1,7;2,6;3,5;4,4。当然还有5,3,这实际上又和前面重复了,所以一段绳子如果被切为2段,就只有n/2种可能性。

    切为2段并不是最终的最大乘积长度,例如8切为了以上4种可能性的两段,并不意味着8的切成m段的最大乘积长度为15(3*5)。它当然还能切为2*3*3=18。那为什么说只需要切为2段呢?

    这是因为我们需要把这个问题不断地划分为小的问题。

    例如8被切为了1和7,这两段不能再继续切分,它就是最小的问题;同理,8被切为了2和6,但是6仍然可以继续被切为1和5,2和4,3和3,所以2和6并不是最小的问题,以此类推,最终推出长度为6的绳子切成m段的最大乘积是9(3*3),那么8被切为2和6时,2*9就等于18。同理继续推3和5,4和4。

    上面的分析得出了什么样的结论呢?结论就是,只需要想象成2段,再各自继续切2段。也就是说假设长度为n的绳子,f(n)是它的各段最大乘积长度,它在被切第一刀时,第一段长度为(1,2,...n-1),第二段的长度为(n-1,n-2,...,1)。推出f(n)=max(f(i)*f(n-1))的关联关系。这里一定需要好好理解,切成2段后,并不是直接将两段相乘,而是再继续将各段切分直至不能再切且取最大乘积长度

    在《算法笔记》(刁瑞 谢妍著)一书中对动态规划做了求解步骤的总结:

    1. 定义子问题

    2. 定义状态转换规则,即递推关系

    3. 定义初始状态

    套用到这套题上,我认为就是需要明确以下3点:

    1. 该问题的核心在于求出每段的最大乘积长度,这是子问题,也就是上文所述,再被切为两段时,需要明确是否能继续切直至不能再切且取最大乘积长度。

    2. 递推关系,也已明确(n)=max(f(i)*f(n-1))

    3. 初始状态,长度为1不能切,长度为2最长为1,长度为3最长为2。

     1 /**
     2 * Description:
     3 * 剪绳子——动态规划
     4 * 2019-06-19
     5 * Created with OKevin.
     6 */
     7 public class Solution1 {
     8 
     9    public int maxProductAfterCutting(int length) {
    10        if (length < 2) {
    11            return 0;
    12        }
    13        if (length == 2) {
    14            return 1;
    15        }
    16        if (length == 3) {
    17            return 2;
    18        }
    19        int[] products = new int[length + 1];   //数组中存储的是每段的最优解
    20        //大于长度3的绳子,当然可以划分出1,2,3长度的绳子
    21        products[0] = 0;
    22        products[1] = 1;
    23        products[2] = 2;
    24        products[3] = 3;
    25        int max = 0;
    26        for (int i = 4; i <= length; i++) {
    27            max = 0;
    28            for (int j = 1; j <= i / 2; j++) {  //除以2的原因在上文中也以提到,将一段绳子划分为2段时,实际上中间后的切分和前面是重复的
    29                int product = products[j] * products[i - j];    //递推关系f(i)*f(n-1)
    30                if (max < product) {
    31                    max = product;
    32                }
    33                products[i] = max;
    34            }
    35        }
    36        max = products[length];
    37        return max;
    38    }
    39 }

    优点:动态规划类似于分治算法,将大的问题逐步划分为小的问题求解。

    缺点:此题采用动态规划的时间复杂度为O(n^2),且空间复杂度为O(n)

    解法二:贪婪算法

    贪婪算法的核心是,先挑最大的,再挑比较大的,再挑小的(贪婪嘛)。

    本题对于长度为n(n>=5)的绳子应尽量多划分为长度3的段。对于长度为4的段,应划分为长度为2的段。

    也即是,如果长度为10,那么10/3=3个长度为3的段,划分结果为3*3*3*1,最后一个段为1,划分为3*3*4。

     1 /**
     2 * Description:
     3 * 剪绳子——贪婪算法
     4 * 2019-06-20
     5 * Created with OKevin.
     6 */
     7 public class Solution2 {
     8    public int maxProductAfterCutting(int length) {
     9        if (length < 2) {
    10            return 0;
    11        }
    12        if (length == 2) {
    13            return 1;
    14        }
    15        if (length == 3) {
    16            return 2;
    17        }
    18        int timesOf3 = length / 3;
    19        if (length - timesOf3 * 3 == 1) {
    20            timesOf3 -= 1;
    21        }
    22        int timesOf2 = (length - timesOf3*3) / 2;
    23        return (int) (Math.pow(3, timesOf3) * Math.pow(2, timesOf2));
    24    }
    25 }

    15.二进制中1的个数

    题目:请实现一个函数,输入一个整数,输出该数二进制表示中1的个数。例如,把9表示成二进制是1001,有2位是1。因此,如果输入9,则该函数输出2。

    此题可采用移位运算+与运算求解

     1 /**
     2 * Description:
     3 * 移位运算+与运算
     4 * 2019-06-20
     5 * Created with OKevin.
     6 */
     7 public class Solution {
     8    public int NumberOf1(int num) {
     9        int count = 0;
    10        while (num != 0) {
    11            if ((num & 1) == 1) {
    12                count++;
    13            }
    14            num = num >>> 1;    //因为运算>>>表示无符号右移,意味着如果是负数,仍然会向右移,同时用0补齐。如果使用>>有符号右移,那么符号位1永远会存在,也就是会产生死循环
    15        }
    16        return count;
    17    }
    18 }

    16.数值的整数次方

    题目:实现函数Math.pow,求m的n次方。

    循环暴力法

     1 /**
     2 * Description:
     3 * 循环暴力法
     4 * 2019-06-20
     5 * Created with OKevin.
     6 */
     7 public class Solution1 {
     8    public int pow(int m, int n) {
     9        int result = 1;
    10        for (int i = 0; i < n; i++) {
    11            result *= m;
    12        }
    13        return result;
    14    }
    15 }

    很遗憾,这种解法连校招级都算不上,顶多算是刚学习编程时的水平。

    其实这道题,并没有考查过多的算法,更多的是考查对细节的把握。一个数的整数次方,不光是整数,还有可能是负数,也有可能是0。如果数值为0,则0的幂是没有意义的。

     1 /**
     2 * Description:
     3 * 考虑指数为0,负数,整数;数值为0的情况;0^0在数学上没有意义
     4 * 2019-06-21
     5 * Created with OKevin.
     6 */
     7 public class Solution2 {
     8 
     9    public double pow(int m, int n) {
    10        double result = 0;
    11        if (m == 0 && n < 0) {
    12            return -1;
    13        }
    14        int absN = Math.abs(n);    //取绝对值
    15        result = calc(m, absN);
    16        if (n < 0) {
    17            result = 1 / result;
    18        }
    19        return result;
    20    }
    21 
    22    private int calc(int m, int n) {
    23        int result = 1;
    24        for (int i = 0; i < n; i++) {
    25            result *= m;
    26        }
    27        return result;
    28    }
    29 }

    改进后的代码考虑到了指数是负数的情况。但实际上这仍然有优化的空间。如果指数是32,意味着calc方法需要循环31次。然而实际上循环到一半的时候就可以求它本身。也就是说a^n/2 * a^n/2,n为偶数;a^(n-1)/2 * a^(n-1)/2 * a,n为奇数。

    改进后的calc方法:

     1 private int calc(int m, int n) {
     2    if (n == 0) {
     3        return 1;
     4    }
     5    if (n == 1) {
     6        return m;
     7    }
     8    int result = calc(m, n >> 1);    //右移1位表示除以2
     9    result *= result;
    10    if ((m & 1) == 1) {     //位运算判断是会否为奇数,奇数的二进制第一位一定是1与1做与运算即可判断是否为奇数,代替m%2是否等于0
    11        result *= m;
    12    }
    13    return result;
    14 }

    本文例子完整源码地址:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/sword

    《【好书推荐】《剑指Offer》之软技能》

    《【好书推荐】《剑指Offer》之硬技能(编程题1~6)》

    《【好书推荐】《剑指Offer》之硬技能(编程题7~11)》

    持续更新,敬请关注公众号:coderbuff,回复关键字“sword”获取相关电子书。

     

     

    这是一个能给程序员加buff的公众号 (CoderBuff)

  • 相关阅读:
    【英语天天读】Places and People
    【OpenCV学习】错误处理机制
    【英语天天读】Heart of a stranger 陌生的心灵
    【英语天天读】第一场雪
    【OpenCV学习】角点检测
    【英语天天读】Life is What We Make It
    【英语天天读】培养自信
    【英语天天读】Perseverance
    【OpenCV学习】cvseqpartition序列分类
    【英语天天读】自然
  • 原文地址:https://www.cnblogs.com/yulinfeng/p/11062335.html
Copyright © 2020-2023  润新知