一、用最少数量的箭引爆气球()
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x
处射出一支箭,若有一个气球的直径的开始和结束坐标为 x
start
,x
end
, 且满足 xstart ≤ x ≤ x
end
,则该气球会被 引爆 。
输入: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]+" ");
}
}
}