Bootstrap

算法学习day11

一、用最少数量的箭引爆气球()

一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstartxend, 且满足  xstart ≤ x ≤ xend,则该气球会被 引爆 。

输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
-在x = 6处射出箭,击破气球[2,8]和[1,6]。
-在x = 11处发射箭,击破气球[10,16]和[7,12]。

思路:按照左边界从小到大排列。

出现的情况:

1.points[i][0]>points[i-1][1]:第二个气球的左边界大于第一个气球的右边界(没有交集),此时射箭的数量就要++

2.points[i][0]<=points[i-1][1];此时这两个区域一定有交集,然后更新points[i][1]=Math.min(points[i][1],points[i-1][1]); 因为要看下一个区域和上两个区域有没有交集,就要取最小的那个边界,使都能满足

代码:

class Solution {
    public int findMinArrowShots(int[][] points) {
        /**
        对左边界或者右边界进行排序
        然后分情况 如果左边界大于上一个的右边界 直接result++
        直接 左边界小于等于上一个的右边界 那么更新右边界。
        然后 循环下一个
         */
         int result=1;
         Arrays.sort(points,(a,b)->Integer.compare(a[0],b[0]));//使用Integer的内置方法不会溢出
         for(int i=1;i<points.length;i++){
            //如果左边界大于上一个的右边界
            if(points[i][0]>points[i-1][1]){
                result++;
            }else{
                points[i][1]=Math.min(points[i][1],points[i-1][1]);
            }
         }
         return result;
    }
}

二、无重叠区间(和气球类似)

给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 

输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。

思路:对集合进行左边界排序,如果intervals[i][0]>=intervals[i][1],说明没有重叠

;重叠之后处理的逻辑:将intervals[i][1]=Math.min(intervals[i][1],intervals[i-1][1]);result++

为了能使移除区间的数量是最小的,每次更新右边界为重叠的最小右边界,才能使删除的最小。

代码:

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        /**
        对区间进行 左边界排序
        然后挨个判断xxx
         */
        Arrays.sort(intervals,(a,b)->Integer.compare(a[0],b[0]));
        int result=0;
        for(int i=1;i<intervals.length;i++){
            if(intervals[i][0]>=intervals[i-1][1])continue;
            else{
                result++;
                intervals[i][1]=Math.min(intervals[i-1][1],intervals[i][1]);
            }
        }
        return result;
    }
}

三、合并区间


以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。

思路:

先往集合里面放一个,如果第二个不重叠,那就放进去;

如果第二个重叠的话,就把更新后的数组放进去,然后把之前的删除掉;

代码:

class Solution {
    public int[][] merge(int[][] intervals) {
        /**
         * 继续对intervals 左边界排序
         * 如果出现重叠部分 就将左右边界更新 然后放到一个新的int[][]里面
         */
        Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
        
        ArrayList<int[]> list = new ArrayList<>();
        list.add(intervals[0]);// 先加一个
        for (int i = 1; i < intervals.length; i++) {
            if (intervals[i][0] > intervals[i - 1][1]) {
                list.add(intervals[i]);// 因为i-1已经添加了 如果i和i-1没有重叠的话 也加进去
            } else {
                intervals[i][0] = intervals[i - 1][0];// 更新一下最小边界
                intervals[i][1] = Math.max(intervals[i][1], intervals[i - 1][1]);
                list.remove(list.size() - 1);
                list.add(intervals[i]);
            }
        }
        int[][] newIntervals = new int[list.size()][2];
        int i = 0;
        for (int[] arr : list) {
            newIntervals[i++] = arr;
        }
        return newIntervals;
    }
}

如果遇到两个重叠的区间,那么左区间是intervals[i-1][0],因为是按照左区间的大小排序的

然后右边界为:Math.max(intervals[i-1][1],intervals[i-1][1])

四、划分字母区间

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段同一字母最多出现在一个片段中。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。

返回一个表示每个字符串片段的长度的列表

输入:s = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。

思路: 使用哈希表记录每一个字母出现的最远位置。然后遍历字符串,每次更新范围中的最远下标位置(这里是使在start end这个区间里面,出现的字母只在这个区间里面出现),当i==maxDistance的时候,就将count加到list中,并且更新一下maxDistance

代码:

class Solution {
    public List<Integer> partitionLabels(String s) {
         Map<Character,Integer> map=new HashMap<>();
         for(int i=0;i<s.length();i++){
            map.put(s.charAt(i),i);//这里的位置是根据下标算的
         }
         List<Integer> result=new ArrayList<>();
         Integer maxDistance=map.get(s.charAt(0));
         int count=0;
         for(int i=0;i<s.length();i++){
            count++;
            if(map.get(s.charAt(i))>maxDistance){
                maxDistance=map.get(s.charAt(i));
            }
            if(i==maxDistance){
                result.add(count);
                count=0;
                if(i!=s.length()-1){
                    maxDistance=map.get(s.charAt(i+1));
                }
            }
         }
         return result;
    }
}

五、单调递增的数字(贪心)

当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。

给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。

输入: n = 332
输出: 299

如何能找到一个数字<=n,并且每一位都是单调递增的。思路:首先从后往前遍历,当遇到i和i-1上的数字不是单调递增的就将i-1上的数字-1,然后flag=i,for循环结束之后,从flag开始,将后面的数字都改为9。

eg;332  2比3小,将 3--,然后flag记作3;然后往前一位遍历,2还是比3小,3--,flag=2;遍历完之后,从flag出发到最后一位,将值都置为9

代码:

class Solution {
    public int monotoneIncreasingDigits(int n) {
        String str=Integer.toString(n);
        char[] chars=str.toCharArray();
        int flag=str.length();
        for(int i=str.length()-1;i>0;i--){
            int n1=chars[i]-'0';
            int n2=chars[i-1]-'0';
            if(n1<n2){
                flag=i;
                chars[i-1]--;
            }
        }
        for(int i=flag;i<str.length();i++){
            chars[i]='9';
        }
        return Integer.parseInt(String.valueOf(chars));
    }
}

六、两数相加(链表)

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

思路:使用一个while循环 将两个节点的值加起来(考虑进位) 然后创建一个val为它的节点 等到遍历结束,还要考虑是否有进位。

代码:

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        //虚拟头结点和临时指针
        ListNode pre=new ListNode(0);
        ListNode cur=pre;
        int carry=0;//进位
        //循环判断
        while(l1!=null||l2!=null){
            int num1=l1==null?0:l1.val;
            int num2=l2==null?0:l2.val;
            int num3=num1+num2+carry;

            carry=num3/10;
            int val=num3%10;
            cur.next=new ListNode(val);
            cur=cur.next;
            if(l1!=null)l1=l1.next;
            if(l2!=null)l2=l2.next;
        }
        //最后判断进位
        if(carry!=0)cur.next=new ListNode(carry);
        return pre.next;
    }
}

动态规划

动态规划题型:

基础题:斐波那契数字/爬楼梯

背包问题/打家劫舍/股票问题/子序列问题

解决动态规划需要掌握的几个点:

1.明白dp数组以及下标的含义
2.掌握递推公式
3.dp数组如何初始化
4.遍历顺序
5.打印数组

一、爬楼梯问题(入门/简单)

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

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

思路:要求有多少种方法可以爬到楼顶?那就是在有多少种方法爬到楼顶下一层+爬到楼顶下下一层。eg:假设楼顶为6楼,那我爬向6楼只能从4楼或者5楼开始。因此

1.dp[i]含义:爬到i楼的有多少种方法

2.递推公式:dp[i]=dp[i-1]+dp[i-2];

3.如何初始化:dp[1]=1,dp[2]=2;

4.遍历顺序:从3开始遍历

二、使用最小花费爬楼梯(普通)

可以在下标为0/1的楼梯上选择是否要向上爬,一次可以爬一个楼梯或者两个楼梯,每一个下标对应一个花费,求爬到楼顶的最小花费。

输入:cost = [10, 15, 20]
输出:15
解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。

注意:这里的楼顶是下标为3的地方

1.dp[i]:爬到第i层需要的价值

2.递推公式:dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);

3.初始化:dp[0],dp[1]=0;因为跳到台阶0,1是不需要花费的。

4.遍历顺序,从i=2开始。

代码:

    public int minCostClimbingStairs(int[] cost) {
        int[] dp=new int[cost.length+1];//dp数组的含义就是到达下标为i的最小花费
        dp[0]=0;
        dp[1]=0;
        for(int i=2;i<dp.length;i++){
            dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[dp.length-1];
    }

三、不同路径

dp[][]:dp数组是一个二维数组,表示的是到(x,y)这个点一共有多少种路径

递推公式:dp[x][y]=dp[x-1][y]+dp[x][y-1];

初始化:最上面一行dp[0][i] 和最左边一行dp[i][0] 初始化都应该为1,因为只能往右和往下走,所以这些点都为1
遍历顺序:从(1,1)开始

    public int uniquePaths(int m, int n) {
        int[][] dp=new int[m][n];
        for(int i=0;i<m;i++){
            dp[i][0]=1;
        }
        for(int i=0;i<n;i++){
            dp[0][i]=1;
        }
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                dp[i][j]=dp[i-1][j]+dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }

四、不同路径II

在上一道题的基础上,有障碍物挡着。

dp五部曲和上一道题其实类似。不同的地方在于初始化\遍历时候的逻辑

初始化的时候先判断有没有障碍,如果有的话,后面就都走过不去了,直接break

遍历的时候,如果遇到有障碍的,直接将dp[i][j]置为0,

代码:

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int x=obstacleGrid.length;
        int y=obstacleGrid[0].length;
        int[][] dp=new int[x][y];
        //初始化
        for(int i=0;i<x;i++){
            if(obstacleGrid[i][0]==1)break;
            dp[i][0]=1;
        }
        for(int j=0;j<y;j++){   
            if(obstacleGrid[0][j]==1)break;
            dp[0][j]=1; 
        }

        for(int i=1;i<x;i++){
            for(int j=1;j<y;j++){
                if(obstacleGrid[i][j]==1)dp[i][j]=0;
                else{
                    dp[i][j]=dp[i-1][j]+dp[i][j-1];
                }
            }
        }
        return dp[x-1][y-1];
    }   
}

五、整数拆分(不好理解)

1.dp[i]的含义:数值为i拆分成不同整数的积的最大值。

2.递推公式:dp[i]=Math.max(dp[i],Math.max(j*(i-j),j*dp[i-j]));

对递推公式的解释:

一个数可以直接分成i,j(两个数)。这样乘积为j*(i-j);

还可以分成多个数,乘积为:j*dp[i-j];

那么在取最大值的时候,为什么还要比较dp[i]呢?:因为在递推公式推导的过程中,每次计算dp[i],取最大的而已。

3.dp的初始化:dp[0]=0;dp[1]=0;

4.遍历顺序

        for(int i=2;i<=n;i++){
            for(int j=1;j<i;j++){
                dp[i]=Math.max(dp[i],Math.max(j*(i-j),j*dp[i-j]));
            }
        }

i从2->n(这里i也可以直接从3开始,因为dp[2]=2),然后将i依次分成 1 i-1|| 2,i-2。每次多更新dp[i]

代码:

class Solution {
    public int integerBreak(int n) {
        int[] dp=new int[n+1];
        dp[0]=dp[1]=0;
        for(int i=2;i<=n;i++){
            for(int j=1;j<i;j++){
                dp[i]=Math.max(dp[i],Math.max(j*(i-j),j*dp[i-j]));
            }
        }
        printfDp(dp);
        return dp[n];
    }
    public void printfDp(int[] dp){
        for(int i=0;i<dp.length;i++){
            System.out.print(i+":"+dp[i]+" ");
        }
    }
}

;