返回

驾驭算法题的通用武器:实用思想剖析

见解分享

刷题第三弹:算法通用思想

这篇文章是关于如何刷题的第三篇文章,或许也是最终章了。这一章,我们将探讨一些几乎所有算法题都能用到的超实用思想。

我们先来看一下前两篇文章的地址:

如果你还没有看过前两篇文章,我建议你先去看看。

今天,我将兑现当初吹下的牛皮。话不多说,直接上干货。如果你觉得这篇文章有用,请三连支持我,让我能够坚持下去,给大家带来更多的干货。

动态规划

动态规划是一种自顶向下的算法,它将问题分解成子问题,并存储子问题的解,以避免重复计算。动态规划特别适合解决具有重叠子问题和最优子结构性质的问题。

  • 步骤:

    1. 将问题分解成子问题。
    2. 确定子问题的重叠性。
    3. 使用存储或备忘录记录子问题的解。
    4. 自底向上地解决子问题。
  • 示例: 斐波那契数列

public int fib(int n) {
    int[] dp = new int[n + 1];
    dp[0] = 0;
    dp[1] = 1;
    for (int i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}

贪心

贪心算法是一种自底向上的算法,它在每一步都做出局部最优选择,期望最后得到全局最优解。贪心算法不保证总是能得到全局最优解,但对于某些问题,它可以提供一个近似最优解。

  • 步骤:

    1. 定义局部最优准则。
    2. 在每一步中,选择满足局部最优准则的选项。
    3. 重复步骤 2,直到问题解决。
  • 示例:活动选择问题

public List<Activity> selectActivities(List<Activity> activities) {
    Collections.sort(activities, (a, b) -> a.endTime - b.endTime);
    List<Activity> selectedActivities = new ArrayList<>();
    selectedActivities.add(activities.get(0));
    int lastActivityEndTime = activities.get(0).endTime;
    for (int i = 1; i < activities.size(); i++) {
        Activity activity = activities.get(i);
        if (activity.startTime >= lastActivityEndTime) {
            selectedActivities.add(activity);
            lastActivityEndTime = activity.endTime;
        }
    }
    return selectedActivities;
}

回溯

回溯算法是一种深度优先搜索算法,它通过尝试所有可能的解,并回溯到上一步尝试其他解,来解决问题。回溯算法特别适合解决组合优化问题。

  • 步骤:

    1. 定义搜索空间。
    2. 逐层搜索搜索空间,在每个节点处做出选择。
    3. 如果选择导致无效解,则回溯到上一步并尝试其他选择。
    4. 找到满足约束条件的解。
  • 示例:八皇后问题

public boolean solveNQueens(int n) {
    char[][] board = new char[n][n];
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            board[i][j] = '.';
        }
    }
    return solveNQueens(board, 0);
}

private boolean solveNQueens(char[][] board, int row) {
    if (row == board.length) {
        return true;
    }
    for (int col = 0; col < board.length; col++) {
        if (isSafe(board, row, col)) {
            board[row][col] = 'Q';
            if (solveNQueens(board, row + 1)) {
                return true;
            }
            board[row][col] = '.';
        }
    }
    return false;
}

private boolean isSafe(char[][] board, int row, int col) {
    for (int i = 0; i < row; i++) {
        if (board[i][col] == 'Q') {
            return false;
        }
    }
    for (int i = row, j = col; i >= 0 && j >= 0; i--, j--) {
        if (board[i][j] == 'Q') {
            return false;
        }
    }
    for (int i = row, j = col; i >= 0 && j < board.length; i--, j++) {
        if (board[i][j] == 'Q') {
            return false;
        }
    }
    return true;
}

分治

分治算法是一种自顶向下的算法,它将问题分解成较小的子问题,递归地解决子问题,并合并子问题的解来得到最终解。分治算法特别适合解决具有递归结构的问题。

  • 步骤:

    1. 将问题分解成较小的子问题。
    2. 递归地解决子问题。
    3. 合并子问题的解。
  • 示例:归并排序

public int[] mergeSort(int[] arr) {
    if (arr.length <= 1) {
        return arr;
    }
    int mid = arr.length / 2;
    int[] leftHalf = mergeSort(Arrays.copyOfRange(arr, 0, mid));
    int[] rightHalf = mergeSort(Arrays.copyOfRange(arr, mid, arr.length));
    return merge(leftHalf, rightHalf);
}

private int[] merge(int[] leftHalf, int[] rightHalf) {
    int[] mergedArr = new int[leftHalf.length + rightHalf.length];
    int leftIdx = 0;
    int rightIdx = 0;
    int mergedIdx = 0;
    while (leftIdx < leftHalf.length && rightIdx < rightHalf.length) {
        if (leftHalf[leftIdx] < rightHalf[rightIdx]) {
            mergedArr[mergedIdx++] = leftHalf[leftIdx++];
        } else {
            mergedArr[mergedIdx++] = rightHalf[rightIdx++];
        }
    }
    while (leftIdx < leftHalf.length) {
        mergedArr[mergedIdx++] = leftHalf[leftIdx++];
    }
    while (rightIdx < rightHalf.length) {
        mergedArr[mergedIdx++] = rightHalf[rightIdx++];
    }
    return mergedArr;
}

并查集

并查集是一种数据结构,它用于维护一组元素的集合。并查集支持三种主要操作:查找、合并和检查连通性。并查集特别适合解决连通性问题。

  • 步骤:

    1. 创建一个数组,其中每个元素指向其父节点。
    2. 使用 find() 操作查找元素的根节点。
    3. 使用 union() 操作合并两个集合。
    4. 使用 connected() 操作检查两个元素是否属于同一集合。
  • 示例:朋友圈问题

public class UnionFind {
    private int[] parent;  // 存储每个元素的父节点
    private int[] size;  // 存储每个集合的大小

    public UnionFind(int n) {
        parent = new int[n];
        size = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;  // 初始化每个元素的父节点为自己
            size[i] = 1;  // 初始化每个集合的大小为 1
        }
    }

    public int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);  // 递归查找根节点
        }
        return parent[x];
    }

    public