java递归实现俄罗斯套娃,leetCode进阶算法题+解析(七十一)

今天周三,这周的笔记才开始。讲真这周十点之前没回过家。哎,闲话也不说了,直接开始刷题吧。

重构字符串

题目:给定一个字符串S,检查是否能重新排布其中的字母,使得两相邻的字符不同。若可行,输出任意可行的结果。若不可行,返回空字符串。

示例 1:

输入: S = "aab"

输出: "aba"

示例 2:

输入: S = "aaab"

输出: ""

注意:

S 只包含小写字母并且长度在[1, 500]区间内。

思路:这道题似曾相识的感觉。好像做过类似的。首先要判断是不是有一个元素超过全长的一半。如果超了肯定是false。因为怎么插入都有挨着的。如果不超过,那么从最多的字母开始插入。因为都是小写字母,我这里打算用数组下标代替字母。值代替个数。大概思路就这样,我去实现下试试。

直接贴代码:

public static String reorganizeString(String S) {

if (S.length() < 2) {

return S;

}

int[] counts = new int[26];

int maxCount = 0;

int length = S.length();

for (char c:S.toCharArray()) counts[c - 'a']++;

for(int i:counts) maxCount = Math.max(maxCount,i);

if (maxCount > (length + 1) / 2) {

return "";

}

char[] reorganizeArray = new char[length];

int evenIndex = 0, oddIndex = 1;

int halfLength = length / 2;

for (int i = 0; i < 26; i++) {

char c = (char) ('a' + i);

while (counts[i] > 0 && counts[i] <= halfLength && oddIndex < length) {

reorganizeArray[oddIndex] = c;

counts[i]--;

oddIndex += 2;

}

while (counts[i] > 0) {

reorganizeArray[evenIndex] = c;

counts[i]--;

evenIndex += 2;

}

}

return new String(reorganizeArray);

}

这个思路和我上面说的差不太多,就是计数后的处理有点不同,两个指针往上插入字符,所以每次是跳两个单元格的。一个1,3,5插入,一个2,4,6插入。这样防止一样的挨着。而且这里有一个知识点:如果一个元素自己就超过整个数组的一半:比如aaabb这种,那么一定要从第0个开始插入这个元素。当然了如果不足一半则从第一个开始插入就行。这也是上面代码中先从id为1的开始判断的原因。

具体的做法也没啥好说的,而且我这个题的性能已经不错了,所以就不多说了,直接下一题。反正这个题不算难,就这样吧,下一题。

俄罗斯套娃信封问题

题目:给定一些标记了宽度和高度的信封,宽度和高度以整数对形式 (w, h) 出现。当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。请计算最多能有多少个信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。说明:不允许旋转信封。

示例:

输入: envelopes = [[5,4],[6,4],[6,7],[2,3]]

输出: 3

解释: 最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。

思路:这个题是2021/3/4的每日一题。大体思路是以w排序,然后获取h的最长子序列。这个子序列的长度就是可以套娃的最长长度。感觉思路没啥问题。我去是实现下试试。

class Solution {

public int maxEnvelopes(int[][] envelopes) {

if(envelopes.length == 0) return 0;

Arrays.sort(envelopes, new Comparator() {

@Override

public int compare(int[] o1, int[] o2) {

if(o1[0]>o2[0]) return 1;

if(o1[0]

//这里是重点,因为装下必须长宽都高。所以这种长一样的防止以后计算出问题,所以一定要宽长的在前面

return o2[1]-o1[1];

}

});

int[] d = new int[envelopes.length];

for(int i = 0;i

//d是最长递增子序列就是可以套娃的最大数

int[] dp = new int[envelopes.length];

for(int i = 1;i

for(int j = 0;j

//说明当前元素比之前的元素大。可以继续往上续.找一个最长的续上

if(d[i]>d[j]) dp[i] = Math.max(dp[i],dp[j]+1);

}

}

int ans = 0;

for(int i : dp){

ans = Math.max(ans,i);

}

return ans+1;

}

}

又一次凭自己做出困难题目了,贼开心,嘿嘿。简单来说这个题的思路就是我上面说的那个,然后要注意一点的是排序的时候长一样宽度大的要在前面。

因为一个测试案例: [[4,5],[4,6],[6,7],[2,3],[1,1]]

这个测试案例中:4.5 4.6是不可以套娃的,但是如果是长一样宽顺序的话,得出来的解决是5<6,这样结果就会出问题,所以一定是倒叙排序,也就是6,5这样的。

剩下没啥了,就是一个最长递增子序列的问题我也不在这里多说了,下一题。

最多能完成排序的块

题目:数组arr是[0, 1, ..., arr.length - 1]的一种排列,我们将这个数组分割成几个“块”,并将这些块分别进行排序。之后再连接起来,使得连接的结果和按升序排序后的原数组相同。我们最多能将数组分成多少块?

示例 1:

输入: arr = [4,3,2,1,0]

输出: 1

解释:

将数组分成2块或者更多块,都无法得到所需的结果。

例如,分成 [4, 3], [2, 1, 0] 的结果是 [3, 4, 0, 1, 2],这不是有序的数组。

示例 2:

输入: arr = [1,0,2,3,4]

输出: 4

解释:

我们可以把它分成两块,例如 [1, 0], [2, 3, 4]。

然而,分成 [1, 0], [2], [3], [4] 可以得到最多的块数。

注意:

arr 的长度在 [1, 10] 之间。

arr[i]是 [0, 1, ..., arr.length - 1]的一种排列。

思路:这个题首先,1是保底答案。其次就是看能分多少块了.我有个模糊的想法,那就是先排序,看这个元素原本应该在的位置和现在的位置一样不一样。有一个模糊的思路,下面我去代码试试。

其实这个题有个重点,是元素的值会分别是0到n-1,而且还不会重复,所以这个题就简单多了,从前往后遍历,如果说遍历到了i,而当前最大值也是i。则说明前面这些元素已经可以单独提出来作为一个块了,也就是块+1.当然这个也不影响保底的1.因为最坏的结果最大的在早早就出来了,所以要遍历到最后一个元素才能进入到这个结果+1的分支。反正情况就是这样,想明白就挺简单了,下面是代码:

class Solution {

public int maxChunksToSorted(int[] arr) {

int res = 0, max = 0;

for (int i = 0; i < arr.length; i++){

max = Math.max(max, arr[i]);//统计前i个位置的最大元素

if (max == i) res++;

}

return res;

}

}

因为这个代码比较简单,思路上面也都说了,所以我就不多说什么了。直接接下一题了。

分割回文串

题目:给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文。返回符合要求的 最少分割次数 。

示例 1:

输入:s = "aab"

输出:1

解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。

示例 2:

输入:s = "a"

输出:0

示例 3:

输入:s = "ab"

输出:1

提示:

1 <= s.length <= 2000

s 仅由小写英文字母组成

思路:这个题我看了标签,是dp。我觉得主要还是每次都获取最大的回文串切分吧。其中如果是dp,那么肯定是有个递推公式,也就是说应该是有个填充规律。所以我目前四路是从前往后。其次是回文串分奇偶。所以每一个元素都要作为中心点来遍历一遍。大概思路是有了,具体的我去代码试试

最终版代码:

class Solution {

public int minCut(String s) {

if(s.length()<=1) return 0;

int len = s.length();

int[] dp = new int[len];

Arrays.fill(dp,Integer.MAX_VALUE);

for(int i = 0;i

dp(i,i,s,dp);

dp(i,i+1,s,dp);

}

return dp[len-1];

}

//因为回文串分单双。所以奇数个中心是temp,偶数个中心是temp和temp1

public void dp(int temp,int temp1,String s,int[] dp){

while(temp>=0 && temp1

//注意这个三元运算:如果当前元素是第一个,那么是不需要切割的,所以是0.

//注意咱们因为是从前开始便利的,所以得出的结果这个回文串之后的最小结果

dp[temp1] = Math.min(dp[temp1],temp == 0?0:dp[temp-1]+1);

temp--;

temp1++;

}

}

}

这个思路其实也不算很难,但是我自己怎么都绕不过来,所以最终看了题解。发现我离正确答案就差那么一层纱。简而言之其思路也就是递推公式如下:从最左边开始遍历,每一个字符都看作是回文串的中心点来判断。如果是最左边是第一个字符则当前不用切割,如果最左边不是第一个字符,那么切割的方式是左边再往左的那个切割次数+1(因为当前和左边的左边没办法组成一个回文串,一定要割一刀,所以+1)。

以此类推,最后一个字符的割的次数就是需要割的最大次数。

因为dp这种东西本来思路就比写法重要,这个要慢慢理解。我其实当时自己想到了这个递推公式,但是我没有把中间过程的每一步都重新判断赋值,而是一次判断出整个回文串,反正就是爬山100步,我只走了50步,剩下最重要的反而没想明白。

然后这个题就这样吧,懂得自然懂。不懂的建议去debug一步一步跑几次就明白了。下一题。

全局倒置与局部倒置

题目:数组 A 是 [0, 1, ..., N - 1] 的一种排列,N 是数组 A 的长度。全局倒置指的是 i,j 满足 0 <= i < j < N 并且 A[i] > A[j] ,局部倒置指的是 i 满足 0 <= i < N 并且 A[i] > A[i+1] 。当数组 A 中全局倒置的数量等于局部倒置的数量时,返回 true 。

示例 1:

输入: A = [1,0,2]

输出: true

解释: 有 1 个全局倒置,和 1 个局部倒置。

示例 2:

输入: A = [1,2,0]

输出: false

解释: 有 2 个全局倒置,和 1 个局部倒置。

注意:

A 是 [0, 1, ..., A.length - 1] 的一种排列

A 的长度在 [1, 5000]之间

这个问题的时间限制已经减少了。

思路:我审题三四遍才看明白,不知道是我语文没救了,还是leetcode越来越不说人话了。总而言之,所谓的全局倒置就是前面的比后面的大(非连续)。比如 1,2,0。这里1和0,2和0是两对全局倒置,而局部倒置是一定要连续的递减。比如1,2,0.只有2,0是局部倒置。其实这样我们可以直接想象得到:局部倒置本身就是全局倒置之一,所以哪怕不等一定是全局倒置大于局部倒置,我们只要判断是不是有不是连续的全局倒置就行了,这样只要看有没有不连续的递减就行,我去试试代码。

直接上最终版代码:

class Solution {

public boolean isIdealPermutation(int[] A) {

//只要判断是不是有非连续全局倒置就可以了。

int max = A[0];

for(int i = 2;i

if(max>A[i]){

return false;

}

max = Math.max(max,A[i-1]);

}

return true;

}

}

一次ac,而且性能超过百分百,所以说这个题就不多说啥了,最近做题最顺的一道,开心。思路啥的都没问题。代码也很简单,直接下一题了。

在KR字符串中交换相邻字符

题目:在一个由 'L' , 'R' 和 'X' 三个字符组成的字符串(例如"RXXLRXRXL")中进行移动操作。一次移动操作指用一个"LX"替换一个"XL",或者用一个"XR"替换一个"RX"。现给定起始字符串start和结束字符串end,请编写代码,当且仅当存在一系列移动操作使得start可以转换成end时, 返回True。

示例 :

输入: start = "RXXLRXRXL", end = "XRLXXRRLX"

输出: True

解释:

我们可以通过以下几步将start转换成end:

RXXLRXRXL ->

XRXLRXRXL ->

XRLXRXRXL ->

XRLXXRRXL ->

XRLXXRRLX

提示:

1 <= len(start) = len(end) <= 10000。

start和end中的字符串仅限于'L', 'R'和'X'。

思路:看题目所说:XL可以换成LX,RX可以换成XR。大概题目要求就是这样。重点是这个题目的标签是脑筋急转弯。仔细审题:首先因为只是字母交换,所以如果起始字符串的字母个数不一样肯定不行。其次连起来:因为只能这三种元素组成,而且这里XL替换成LX,RX替换成XR注意一下,这里只有X与L,R的位置变化了,而且都是X往后换了,L和R之间是不能换位的,所以我们可以理解为起始串除了X以后应该相等。不然怎么也换不了的。其次因为是只能从start开始转换,也就是本质上是单向的。

而相对于L,X只能往后移,也就是说L只能往X前面移动。而R则相反,可以往X的后面移动。也就是说LLLRXLRRR.我们可以把X往R前面移动,或者把X往L后面移动,但是X前面的LLL最少三个,不能变少,同样X后面的R最少三个,也不能变少。

- 所以如果start的X前面的L比end中X前面的L多,是怎么也少不了的,也转化不成功。

- 同理,如果start的X前面的R小于end的X前面的R,因为R只能往后,是多不了了,所以也转化不成功。

以上三个条件就是决定转化能否成功的全部因素,下面我去写代码:

class Solution {

public boolean canTransform(String start, String end) {

String s1 = start.replace("X","");

if(!s1.equals(end.replace("X",""))) return false;

int[][] d = new int[start.length()-s1.length()][2];

int l = 0;

int r = 0;

int temp = 0;

for(int i = 0;i

if(start.charAt(i) == 'X'){

d[temp] = new int[]{l,r};

temp++;

}else if(start.charAt(i) == 'L'){

l++;

}else {

r++;

}

}

l = 0;

r = 0;

temp = 0;

for(int i = 0;i

if(end.charAt(i) == 'X'){

if(d[temp][0]>l || d[temp][1]

temp++;

}else if(end.charAt(i) == 'L'){

l++;

}else {

r++;

}

}

return true;

}

}

思路没啥问题,但是代码的性能不是很好。我直接去看看性能第一的代码吧,get不到优化点了:

class Solution {

public boolean canTransform(String start, String end) {

int n=start.length();

char[] src=start.toCharArray();

char[] des=end.toCharArray();

int i=0;

int j=0;

while(i

while(i

while(j

if((i==n&&j

if(i

if(src[i]!=des[j]||(src[i]=='L'&&ij)) {

return false;

}

i++;

j++;

}

}

return true;

}

}

我竟然看了半天才看懂,大概的思路和我之前的是一样的:

第一个判断(因为遇到X会继续往下,所以可以看作跳过X),跳过X后如果两个字符串不全等肯定是不行的。这里是一个一个元素比较的。

第二个判断:当前是L的话,如果i小于j。也就是说start的L多余end的L。又因为L只能往前不能往后,所以一定不行。

第三个判断:当前是R的话,如果i大于j。也就是说start的R小余end的R。因为R只能往后不能往前,所以前面的补不了,所以也无法变换。

这三种情况都是直接false,否则就是true。

其实这个思路是差不多的,但是具体的实现我没有那么透彻,还是理解不深,哎,下一题。

第K个语法符号

题目:在第一行我们写上一个 0。接下来的每一行,将前一行中的0替换为01,1替换为10。给定行数 N 和序数 K,返回第 N 行中第 K个字符。(K从1开始)

例子:

输入: N = 1, K = 1

输出: 0

输入: N = 2, K = 1

输出: 0

输入: N = 2, K = 2

输出: 1

输入: N = 4, K = 5

输出: 1

解释:

第一行: 0

第二行: 01

第三行: 0110

第四行: 01101001

注意:

N 的范围 [1, 30].

K 的范围 [1, 2^(N-1)].

思路:这个题的话,审完题我的第一个想法居然是暴力获取每一行的每一个字符。。。但是看看这个N和K的范围,还是算了吧。。简单来说这个题我觉额其实应该是有规律的。比如每一行都是上一行翻倍的元素数目。其次 0-> 0 1 1->1 0 也就是说偶数位是上一层的元素。奇数位是偶数位%1的结果。这个题应该是个找规律的题目,我去仔细看看怎么写代码。

ps:找出个规律,下一行就是上一行+上一行翻转(0变1,1变0).感觉可以用到。

这个题就是一个标准的数学题,所以我屈服了,思路是有,但是写出来怎么写怎么不对:

class Solution {

public int kthGrammar(int N, int K) {

return Integer.bitCount(K - 1) % 2;

}

}

于是看了题解,一行代码搞定。这块其实是关于位的运算。每一行的后半部分都是前半部分的翻转。把下标 K 写成二进制的形式,如果 K 在第二部分,那么二进制形式的第一个比特位一定是 1。据此可以将方法三继续简化,实际上翻转的次数等于 K-1 二进制表示形式中 1 出现的个数。(这个是题解的说法)

这篇笔记就记到这里,如果稍微帮到你了记得点个喜欢点个关注,也祝大家工作顺顺利利!生活愉快!

你可能感兴趣的:(java递归实现俄罗斯套娃)