标签 算法题解 下的文章

题目描述

给你⼀根⻓度为n 的绳⼦,请把绳⼦剪成整数⻓的m 段( m 、n 都是整数, n>1 并 且m>1 , m<=n ),每段绳⼦的⻓度记为k[1],...,k[m]。请问k[1]x...xk[m] 可能的最⼤乘积是多少?例如,当绳⼦的⻓度是8 时,我们把它剪成⻓度分别为2 、3 、3 的三段,此时得到的最⼤乘积是18`。

输⼊描述:输⼊⼀个数n,意义⻅题⾯。(2 <= n <= 60)

返回值描述:输出答案。

示例1
输⼊:8
返回值:18

思路及解答

备忘录

本题的解答思路就是每个⻓度的绳⼦,要么最⻓的情况是不剪开(⻓度是本身),要么⻓度是剪开两段的乘积。因此每个⻓度 length 都需要遍历两个相加之后等于 length 的乘积,取最⼤值。

初始化值⻓度为 1 的值为 1 ,从⻓度为 2 开始,每⼀种⻓度都需要遍历两个⼦⻓度的乘积。

显然,为了避免多次重复计算,可以写个备忘录

public class Solution {
    public int cutRope(int target) {
        if (target <= 1) {
            return target;
        }
        int[] nums = new int[target + 1];
        nums[1] = 1;
        nums[0] = 1;
        for (int i = 2; i <= target; i++) {
            int max = i;
            for(int j=0;j<=i/2;j++){
                int temp = nums[j] * nums[i-j];
                if(temp > max){
                    max = temp;
                }
            }
            nums[i]=max;
        }
        return nums[target];
    }
}

动态规划

⽤动态规划的思维来做,假设绳⼦⻓度为 n 的 最⼤的⻓度为 f(n) ,那你说 f(n) 怎么计算得来呢?

  1. f(n) 可能是 n(不切分)
  2. 也可能是 f(n-1) 和 f(1) 的乘积
  3. 也可能是 f(n-2) 和 f(2) 的乘积
  4. ......

那么也就是想要求 f( n ) 我们必须先把 f(n-1) , f(n-2) ...之类的前⾯的值先求出来, f(1)=1 这是初始化值。

public class Solution {
    public int cutRope(int target) {
        int[] dp = new int[target + 1];
        dp[1] = 1;
        for (int i = 2; i <= target; i++) {
            for (int j = 1; j < i; j++) {
                dp[i] = Math.max(dp[i], (Math.max(j, dp[j])) * (Math.max(i - j, dp[i - j])));
            }
        }
        return dp[target];
    }
}
  • 时间复杂度:O(n²),外层循环n-3次,内层循环i/2次
  • 空间复杂度:O(n),需要dp数组存储中间结果

贪心算法(最优解)

基于数学推导的贪心策略,优先剪出长度为3的段。当n≥5时,优先剪出长度为3的段;剩余4时剪成2×2

为什么选择3?

  1. 数学证明:当n ≥ 5时,3(n-3) ≥ 2(n-2) > n
  2. 接近自然底数e:最优分段长度应接近e ≈ 2.718,3是最接近的整数
  3. 4的特殊处理:2×2 > 3×1,所以剩余4时剪成2×2而不是3×1
public class Solution {
    public int cutRope(int n) {
        // 特殊情况处理
        if (n <= 3) return n - 1;
        
        // 计算可以剪出多少段长度为3的绳子
        int countOf3 = n / 3;
        
        // 处理剩余部分:当剩余长度为1时,调整策略
        if (n - countOf3 * 3 == 1) {
            countOf3--; // 减少一段3,与剩余的1组成4
        }
        
        // 计算剩余部分能剪出多少段长度为2的绳子
        int countOf2 = (n - countOf3 * 3) / 2;
        
        // 计算结果:3的countOf3次方 × 2的countOf2次方
        return (int)(Math.pow(3, countOf3)) * (int)(Math.pow(2, countOf2));
    }
}
  • 时间复杂度:O(1),只有常数次操作
  • 空间复杂度:O(1),只使用固定变量

数学公式法(理论最优)

根据n除以3的余数直接套用公式

public class Solution {
    public int cutRope(int n) {
        if (n <= 3) return n - 1;
        
        int countOf3 = n / 3;
        int remainder = n % 3;
        
        // 根据余数直接返回结果
        if (remainder == 0) {
            return (int) Math.pow(3, countOf3);
        } else if (remainder == 1) {
            return (int) Math.pow(3, countOf3 - 1) * 4;
        } else { // remainder == 2
            return (int) Math.pow(3, countOf3) * 2;
        }
    }
}
  • 时间复杂度:O(1)
  • 空间复杂度:O(1)

题目描述

请设计⼀个函数,⽤来判断在⼀个矩阵中是否存在⼀条包含某字符串所有字符的路径。路径可以从矩阵中的任意⼀个格⼦开始,每⼀步可以在矩阵中向左,向右,向上,向下移动⼀个格⼦。如果⼀条路径经过了矩阵中的某⼀个格⼦,则该路径不能再进⼊该格⼦。 例如矩阵:

中包含⼀条字符串 " bcced " 的路径,但是矩阵中不包含 " abcb " 路径,因为字符串的第⼀个字符 b占据了矩阵中的第⼀⾏第⼆个格⼦之后,路径不能再次进⼊该格⼦。

示例1

输⼊:[[a,b,c,e],[s,f,c,s],[a,d,e,e]],"abcced"
返回值:true

思路及解答

DFS回溯

主要的思路是对于每⼀个字符为起点,递归向四周拓展,然后遇到不匹配返回 false ,匹配则接着匹配直到完成,⾥⾯包含了 回溯 的思想。步骤如下:

针对每⼀个字符为起点,初始化⼀个和矩阵⼀样⼤⼩的标识数组,标识该位置是否被访问过,⼀开始默认是false 。

  1. 如果当前的字符索引已经超过了字符串⻓度,说明前⾯已经完全匹配成功,直接返回 true
  2. 如果⾏索引和列索引,不在有效的范围内,或者改位置已经标识被访问,直接返回 false
  3. 否则将当前标识置为已经访问过
  4. 如果矩阵当前位置的字符和字符串相等,那么就字符串的索引加⼀,递归判断周边的四个,只要⼀个的结果为 true ,就返回 true ,否则将该位置置为没有访问过(相当于回溯,退回上⼀步),返回 false 。矩阵当前位置的字符和字符串不相等,否则同样也是将该位置置为没有访问过(相当于回溯,退回上⼀步),返回 false 。

⽐如查找 bcced :

public class Solution {
    public boolean hasPath(char[][] matrix, String word) {
        // write code here
        if (matrix == null || word == null || word.length() == 0) {
            return false;
        }
        
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix[0].length; j++) {
                boolean[][] flags = new boolean[matrix.length][matrix[0].length];
                
                boolean result = judge(i, j, matrix, flags, word, 0);
                if (result) {
                    return true;
                }
            }
        }
        return false;
   }
    
   public boolean judge(int i, int j, char[][] matrix, boolean[][] flags, String words, int index) {
        if (index >= words.length()) {
            return true;
        }
       
        if (i < 0 || j < 0 || i >= matrix.length || j >= matrix[0].length || flags[i][j]) {
            return false;
        }
        flags[i][j] = true;
        
        if (matrix[i][j] == words.charAt(index)) {
            if (judge(i - 1, j, matrix, flags, words, index + 1)
            || judge(i + 1, j, matrix, flags, words, index + 1)
            || judge(i, j + 1, matrix, flags, words, index + 1)
            || judge(i, j - 1, matrix, flags, words, index + 1)) {
                return true;
            } else {
                flags[i][j] = false;
                return false;
            }
        } else {
            flags[i][j] = false;
            return false;
        }
    }
}
  • 时间复杂度:O(3^k × m × n),其中k为单词长度,m、n为矩阵尺寸。每个点有3个方向可选(不能回退)
  • 空间复杂度:O(k),递归栈深度和visited数组空间

方向数组优化

使用额外的访问标记数组来记录路径状态。

public class Solution {
    public boolean exist(char[][] board, String word) {
        if (board == null || board.length == 0 || board[0].length == 0 || word == null) {
            return false;
        }
        
        int m = board.length, n = board[0].length;
        boolean[][] visited = new boolean[m][n];
        char[] words = word.toCharArray();
        
        // 遍历矩阵中的每个单元格作为起始点
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (dfs(board, visited, words, i, j, 0)) {
                    return true;
                }
            }
        }
        return false;
    }
    
    private boolean dfs(char[][] board, boolean[][] visited, char[] word, int i, int j, int index) {
        // 终止条件1:找到完整路径
        if (index == word.length) {
            return true;
        }
        
        // 终止条件2:越界或已访问或字符不匹配
        if (i < 0 || i >= board.length || j < 0 || j >= board[0].length || 
            visited[i][j] || board[i][j] != word[index]) {
            return false;
        }
        
        // 标记当前单元格为已访问
        visited[i][j] = true;
        
        // 向四个方向进行DFS搜索
        boolean found = dfs(board, visited, word, i + 1, j, index + 1) ||  // 向下
                       dfs(board, visited, word, i - 1, j, index + 1) ||  // 向上
                       dfs(board, visited, word, i, j + 1, index + 1) ||  // 向右
                       dfs(board, visited, word, i, j - 1, index + 1);    // 向左
        
        // 回溯:恢复访问状态
        visited[i][j] = false;
        
        return found;
    }
}
  • 时间复杂度:O(3^k × m × n),其中k为单词长度,m、n为矩阵尺寸。每个点有3个方向可选(不能回退)
  • 空间复杂度:O(k),递归栈深度和visited数组空间

时间空间复杂度与方法一相同,但代码更易扩展(如需要八方向移动时只需修改DIRECTIONS数组)

原地标记优化(最优)

通过修改原矩阵来标记访问状态,节省空间。

public class Solution {
    public boolean exist(char[][] board, String word) {
        if (board == null || board.length == 0 || word == null || word.length() == 0) {
            return false;
        }
        
        char[] words = word.toCharArray();
        
        for (int i = 0; i < board.length; i++) {
            for (int j = 0; j < board[0].length; j++) {
                if (dfsOptimized(board, words, i, j, 0)) {
                    return true;
                }
            }
        }
        return false;
    }
    
    private boolean dfsOptimized(char[][] board, char[] word, int i, int j, int index) {
        // 边界检查和字符匹配检查
        if (i < 0 || i >= board.length || j < 0 || j >= board[0].length || 
            board[i][j] != word[index]) {
            return false;
        }
        
        // 找到完整路径
        if (index == word.length - 1) {
            return true;
        }
        
        // 原地标记:将当前字符临时替换为特殊标记(不能出现的字符)
        char temp = board[i][j];
        board[i][j] = '#';  // 标记为已访问
        
        // 四个方向搜索
        boolean res = dfsOptimized(board, word, i + 1, j, index + 1) ||
                     dfsOptimized(board, word, i - 1, j, index + 1) ||
                     dfsOptimized(board, word, i, j + 1, index + 1) ||
                     dfsOptimized(board, word, i, j - 1, index + 1);
        
        // 回溯:恢复原始字符
        board[i][j] = temp;
        
        return res;
    }
}

关键技巧:

  1. 临时修改:将访问过的board[i][j]改为'#'(或其他不在字母表中的字符)
  2. 自动避障:后续搜索遇到'#'会因字符不匹配而自动跳过
  3. 状态恢复:回溯时恢复原始字符,确保不影响其他路径搜索

算法分析:

  • 时间复杂度:O(3^k × m × n),与前述方法相同
  • 空间复杂度:O(1),显著优化!仅使用常数空间(递归栈空间不可避免)