Bootstrap

算法学习笔记(九):网格图DFS、图论算法DFS、动态规划DP、贪心

一.网格图DFS

适用于需要计算连通块个数、大小的题目

1.岛屿数量

        给你一个由 1(陆地) 和 0(水)组成的二维网格,请你计算网格中岛屿的数量

        岛屿总是被水包围,并且每座岛屿只能由水平方向和\或竖直方向上相邻的陆地连接形成

        此外,你可以假设该网格的四条边均被水包围

        

     

class Solution {
    public int numIslands(char[][] grid) {
        /**
            插旗法:
            真他妈牛逼,看了下题解真不是人想出来的
            也就是说从起点开始,从四个方向开始遍历,直到发现边界就停止
            边界:为0,或者到头了,或者已经被插旗
            这样只要是起点能连通的位置都会被插旗,剩下的就是隔着0的区域访问不到
            那么就相当于当前这一大块就是一个岛屿
            然后继续寻找下一个岛屿
         */
        int count = 0;
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[i].length; j++) {
                //从[0,0]开始作为起点进行插旗
                if (grid[i][j] == '1') {
                    dfs(i, j, grid);
                    count++;
                }
            }
        }
        return count;
    }

    private void dfs(int i, int j, char[][] grid) {
        //边界问题
        if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length 
                || grid[i][j] != '1') {
            return;
        }
        //重点:将当前节点插旗,变为不为1的数
        grid[i][j] = '0';
        //开始四个方向进行dfs
        //上
        dfs(i - 1, j, grid);
        //下
        dfs(i + 1, j, grid);
        //左
        dfs(i, j - 1, grid);
        //右
        dfs(i, j + 1, grid);
    }    
}

2.岛屿的最大面积

        给你一个大小为 m x n 的二进制矩阵 grid

        岛屿是由一些相邻的 1(土地)构成的组合,这里的相邻要求两个1必须在水平或者竖直的四个

        方向上相邻,你可以假设grid的四个边缘都被0(水)包围

        岛屿的面积是岛上值为1的单元格的数目

        请计算grid的最大岛屿面积,如果没有岛屿,请返回0

     

class Solution {
    public int maxAreaOfIsland(int[][] grid) {
        //插旗法 比上一题难一点 要算面积
        int sum = 0;
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[i].length; j++) {
                if (grid[i][j] == 1) {
                    sum = Math.max(sum, dfs(i, j, grid));
                }
            }
        }
        return sum;
    }

    private int dfs(int i, int j, int[][] grid) {
        //边界问题一订要定义好 不然会越界
        if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] != 1) {
            return 0;
        }
        int sum = 0;
        grid[i][j] = 2;
        sum++;  //自身
        sum += dfs(i - 1, j, grid); //上
        sum += dfs(i + 1, j, grid); //下
        sum += dfs(i, j - 1, grid); //左
        sum += dfs(i, j + 1, grid); //右
        return sum;
    }
}

  3.水域大小

        你有一个用于表示一片土地的整数矩阵land,该矩阵中每个点的值代表对应地点的海拔

        高度,若值为0则表示水域,由垂直、水平或者对角连接的水域为池塘,池塘的大小是指

        相连的水域的个数,编写一个算法来计算矩阵中所有池塘的大小,返回值需要从小到大

        排序

class Solution {
    public int[] pondSizes(int[][] land) {
        //我草了 还有对角线 想想对角线怎么处理?
        //对角线 有4个呢 那就是8个方向去遍历咯
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < land.length; i++) {
            for (int j = 0; j < land[i].length; j++) {
                if (land[i][j] == 0) {
                    list.add(dfs(i, j, land));
                }
            }
        }
        int[] arr = new int[list.size()];
        list.sort((a, b) -> a - b);
        for (int i = 0; i < list.size(); i++) {
            arr[i] = list.get(i);
        }
        return arr;
    }

    private int dfs(int i, int j, int[][] land) {
        //边界问题
        if (i < 0 || i >= land.length || j < 0 || j >= land[0].length || land[i][j] != 0) {
            return 0;
        }
        int sum = 0;
        land[i][j] = 1;
        sum++;
        //上下左右,左上,左下,右上,右下
        sum += dfs(i - 1, j, land);
        sum += dfs(i + 1, j, land);
        sum += dfs(i, j - 1, land);
        sum += dfs(i, j + 1, land);
        sum += dfs(i - 1, j - 1, land);
        sum += dfs(i + 1, j - 1, land);
        sum += dfs(i - 1, j + 1, land);
        sum += dfs(i + 1, j + 1, land);
        return sum;
    }
}

二.动态规划DP

动态规划解题思路:

        1.寻找子问题

                寻找定义子问题,如果可以把原问题变成一个和原问题相似、规模更小

                的子问题,这种情况就可以用递归来解决

        2.递归怎么写

                状态定义与状态转移方程:

        3.递归+记录返回值=记忆化搜索

                因为在递归中,有着大量的重复递归调用(递归入参相同),由于递归函数都是幂等的,

                同样的入参无论计算多少次结果都是一样的,所以我们可以用记忆化搜索来优化,

                就是将同样入参的计算结果放到缓存中,优先从缓存中拿结果,没有在递归

        4.1:1翻译成递推

                既我们可以去掉递归中的递,只保留归的部分,即自底向上计算

        5.空间优化

                一般dp问题,都是只会用到前面几个状态,之前已经计算过的状态在后面是永远不会

                再用了的,所以我们只需要知道比如上一个状态和上上一个状态即可               

1.爬楼梯

        假设你正在爬楼梯,需要 n 阶你才能到达楼顶

        每次你都可以爬 1 或者2个台阶,你有多少种不同的方式可以爬到楼顶呢?

        第一种解法:这个解法如果数据量大的话往往会超时    

class Solution {
    public int climbStairs(int n) {
        /**
            如果最后一步是爬了1个台阶 那么首先要爬到n-1台阶上
            如果最后一步是爬了2个台阶,那么首先要爬到n-2台阶上
            所以 而爬到n-1和n-2是两种不同的方法 所以
            dfs(n) = dfs(n - 1) + dfs(n - 2)
         */

        return dfs(n);
    }

    private int dfs(int n) {
        if (n <= 1) return 1;   //边界问题 0 和1
        return dfs(n - 1) + dfs(n - 2);
    }
}

        第二种解法:+缓存

class Solution {
    public int climbStairs(int n) {
        /**
            如果最后一步是爬了1个台阶 那么首先要爬到n-1台阶上
            如果最后一步是爬了2个台阶,那么首先要爬到n-2台阶上
            所以 而爬到n-1和n-2是两种不同的方法 所以
            dfs(n) = dfs(n - 1) + dfs(n - 2)
            为了边界问题,0和1因为爬到0和1个台阶都是只有1种方式
            所以初始化dfs(0) dfs(1) = 1 然后公式演变成
            dfs(n + 2) = dfs(1) + dfs(0)
         */
        Map<Integer, Integer> map = new HashMap<>();
        return dfs(n, map);
    }

    private int dfs(int n, Map<Integer, Integer> map) {
        if (n <= 1) return 1;   //边界问题 0 和1
        if (map.get(n) != null) return map.get(n);
        int num = dfs(n - 1, map) + dfs(n - 2, map);
        map.put(n, num);
        return num;
        //这个方式会超时 因为每次都是递归了2遍 变成了 n^2次了 增加一个缓存
        // return dfs(n - 1) + dfs(n - 2);
    }
}

        第三种解法:继续优化,1:1翻译成递推

        dfs(i) = dfs(i - 1) + dfs(i - 2),那么我们可以定义成这样

        f[i] = f[i - 1] + f[i - 2],之前是递归计算每个状态,现在是枚举计算每个状态

        初始值f[0] = f[1] = 1,翻译自递归边界dfs(0) = 1,dfs(1) = 1

        f[n] 翻译自dfs(n)       

class Solution {
    public int climbStairs(int n) {
        /**
            1.定义子问题
                假设最后一次是爬1个台阶,那么就是爬到n-1需要多少种方式
                假设最后一次是爬2个台阶,那么就是爬到n-2需要多少种方式
                最后爬到n的总方式是 上面两种方式之和 
                即 fn = fn-1 + fn-2
                但是在这过程种很多都是会重复计算 那么用一个缓存来记录算过的值
         */
        // int[] cache = new int[n + 1];
        // return dp(n, cache);

        //用逆推 0和1的时候方法是确定的
        int[] dp = new int[n + 1];
        dp[0] = 1; 
        dp[1] = 1;
        for (int i = 2; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }

        return dp[n];
    }

    // private int dp(int n, int[] cache) {
    //     if (n <= 1) return 1;   //0和1个台阶的时候就只有一种方法
    //     if (cache[n] != 0) return cache[n];
    //     int num = dp(n - 1, cache) + dp(n - 2, cache);
    //     cache[n] = num;
    //     return num;
    // }
    
}

第四种解法:继续空间优化

因为计算当前值只需要知道上一个状态和上上一个状态,所以每次只保留这两个变量即可

class Solution {
    public int climbStairs(int n) {
        /**
            分析子问题,假设最后一步是1各台阶,那么爬到n-1个台阶的方法就是dfs(n-1)
            最后一步是2个台阶,爬到n-2个台阶的方法就是n-2
            最后总共有 fds(n-1)+dfs(n-2)种方法
            那么运用缓存 因为在dfs过程中肯定会有重复计算的
            那么就是缓存中有的就不用计算了
            再优化,递推: 因为1个台阶和2个台阶的方法都是1种 
            再优化 n-1是上一个 n-2是上上一个 因为每次计算完上一个上上一个的值再接下来就不会用到
            所以每次就只使用这俩变量就可以了 
         */

        // int[] dp = new int[n + 1];
        // dp[0] = 1;
        // dp[1] = 1;
        // for (int i = 2; i <= n; i++) {
        //     dp[i] = dp[i - 1] + dp[i - 2];
        // }
        // return dp[n];
        //优化一下空间
        int dp0 = 1, dp1 = 1;
        for (int i = 1; i < n; i++) {
            int dpi = dp1 + dp0;
            dp0 = dp1;
            dp1 = dpi;
        }
        return dp1;
    }
}

2.使用最小花费爬楼梯

        给你一个整数数组 cost,其中cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你

        支付此费用,即可旋转向上爬一个或者两个台阶

        你可以选择从下标为 0 或者 1 的台阶开始爬楼梯

        请你计算并返回到达楼顶的最低花费

        

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        /**
            分析子问题
            当最后是爬一个楼梯时,那么就是爬到n-1的费用+当前n-1的费用
            同理,当最后是爬2个楼梯,那么就是n-2的费用+当前n-2的费用
            然后取两者最小值
            int min = Math.min(dfs(n-1) + cost[n-1], dfs(n-2) + cost[n-2])
            剩下的就是开始优化
                优化:加缓存
                优化:递推 因为dfs(n-1)其实就是fn-1 同时 f0和f1是已知的都是=0 因为可以从0或者1开始 
                int[] dp = new int[n+1]
                min = Math.min(dp[n-1] + cost[n-1], dp[n-2]+cost[n-2]) 
        
         */
        // int[] cache = new int[cost.length + 1];
        // Arrays.fill(cache, -1);
        // return dfs(cost, cache, cost.length);

        //进行递推 因为 f0 f1都是已知的
        // int n = cost.length;
        // int[] dp = new int[n + 1];
        // dp[0] = 0;
        // dp[1] = 0;
        // for (int i = 2; i <= n; i++) {  //为什么从2开始呢 因为按照题目来0和1都是已知的 那么肯定从未知的第一个台阶2开始
        //     dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
        // }
        // return dp[n];

        //继续优化 dpi-1可以认为是上一个 dpi-2是上上一个 那么总是只需要这两个变量即可 
        //上一次的上一个和上上一个算完以后就不会再用了
        int dp0 = 0, dp1 = 0; //为什么是0 按照上面推理 因为可以从0或者1开始爬 所以到达0或者1不需要成本
        for (int i = 1; i < cost.length; i++) {
            int min = Math.min(dp1 + cost[i], dp0 + cost[i - 1]);
            dp0 = dp1;
            dp1 = min;
        }
        return dp1;

        // int f0 = 0, f1 = 0;
        // for (int i = 1; i < cost.length; i++) {
        //     int fn = Math.min(f1 + cost[i], f0 + cost[i - 1]);
        //     f0 = f1;
        //     f1 = fn;
        // }
        // return f1;
    }

    // private int dfs(int[] cost, int[] cache, int n) {
    //     if (n <= 1) return 0;
    //     if (cache[n] != -1) return cache[n];
    //     cache[n] = Math.min(dfs(cost, cache, n - 1) + cost[n - 1], dfs(cost, cache, n - 2) + cost[n - 2]);
    //     return cache[n];
    // }
}

   3.打家劫舍

        你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃

        的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两件相邻的房屋在同一

        晚上被小偷闯入,系统会自动报警

        给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报的情况下,一夜

        之内能够偷窃到的最高金额

class Solution {
    public int rob(int[] nums) {
        /**
            最后一个房子偷不偷 下标从0开始
            偷 那么就是fn-2 +hn-1 前n-2个房子+最后一间房子的金额 因为索引从0开始 n-1就是最后一间房
            不偷 那就是fn-1  
            然后就是计算这俩最高值 max = Math.max(f(n-2) + h(n-1), f(n-1));
            然后换算
        
         */

        int n = nums.length;
        // int[] dp = new int[n + 1];  //从0开始
        // dp[0] = 0;
        // dp[1] = nums[0];
        // for (int i = 2; i <= n; i++) {
        //     dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i - 1]);
        // }
        // return dp[n];
        //空间优化 
        // int dp0 = 0, dp1 = nums[0];
        // for (int i = 2; i <= n; i++) {
        //     int max = Math.max(dp1, dp0 + nums[i - 1]);
        //     dp0 = dp1;
        //     dp1 = max;
        // }
        // return dp1;

        //再优化
        int dp0 = 0, dp1 = 0;
        for (int x : nums) {
            int dp = Math.max(dp1, dp0 + x);
            dp0 = dp1;
            dp1 = dp;
        }
        return dp1;
    }
}

三.贪心

1.重新分装苹果

        给你一个长度为n的数组 apple 和另一个长度为 m 的数组 capacity

        一共有 n 个包裹,其中第 i 个包裹中装着 apple[i]个苹果,同时,还有 m 个箱子,第 i 个

        箱子的容量为 capacity[i] 个苹果,请你选择一些箱子来将这 n 个包裹中的苹果重新分装

        到箱子中,返回你需要选择的最小箱子数

        注意:同一个包裹中的苹果可以分装到不同的箱子中

class Solution {
    public int minimumBoxes(int[] apple, int[] capacity) {
        //妙啊 贪心 这题目没有说输出箱子的index 只是计算个数
        //而且1个包裹里的苹果可以放到不同箱子里 所以从大到小排序箱子
        //直到装完苹果为止
        int appleSum = 0;
        for (int num : apple) {
            appleSum += num;
        }
        Arrays.sort(capacity);
        int m = capacity.length;
        int i = m - 1;
        while (appleSum > 0) {
            appleSum -= capacity[i--];
        }
        return m - i - 1;
    }
}

        

;