LeetCode第229场周赛

周赛地址:https://leetcode-cn.com/contest/weekly-contest-229/

第一题:交替合并字符串

这个题目不难,就是两个字符串归并。

class Solution {
    public String mergeAlternately(String word1, String word2) {
        int l1 = word1.length(), l2 = word2.length(), i = 0, j = 0;
        StringBuilder stringBuilder = new StringBuilder(l1 + l2);
        while (i < l1 && j < l2) {
            stringBuilder.append(word1.charAt(i++));
            stringBuilder.append(word2.charAt(j++));
        }
        while (i < l1) {
            stringBuilder.append(word1.charAt(i++));
        }
        while (j < l2) {
            stringBuilder.append(word2.charAt(j++));
        }
        return stringBuilder.toString();
    }
}

第二题:移动所有球到每个盒子所需的最小操作数

自己的解法(暴力):

最简单的思路,就暴力模拟,时间复杂度高,对每一个盒子,从左到右统计有球盒子到当前盒子的举例。

class Solution {
    public int[] minOperations(String boxes) {
        int length = boxes.length();
        int[] result = new int[length];
        for (int i = 0; i < length; i++) {
            for (int j = 0; j < length; j++) {
                if (boxes.charAt(j) == '1') {
                    result[i] += Math.abs(j - i);
                }
            }
        }
        return result;
    }
}

在看其他大佬的题解的时候,看到两种解法,整理下来。

解法一:

以boxes="001011"为例分析。

从左向右遍历,定义一个left数组,表示当前盒子左侧球移动到当前盒子需要的操作数。left[0]、left[1]、left[2]都是0,left[3] = left[2] + boxes[3]左面的球数 = 0 + 1 = 1,left[4] = left[3] + boxes[4]左面的球数 = 1 + 1 = 2,left[5] = left[4] + boxes[5]左面的球数 = 2 + 2 = 4。

于是left[i] = left[i - 1] + boxes[i]左面的球数。于是就要有一个变量统计boxes[i]左面的球数,这个球数是不断累加的。同理,right[i] = right[i + 1] + boxes[i]右面的球数,最后,把left和right按下标求和,就是最终结果。

class Solution {
    public int[] minOperations(String boxes) {
        int length = boxes.length();
        int[] result = new int[length], left = new int[length], right = new int[length];
        int count = 0;// 用于累加球数
        if (boxes.charAt(0) == '1') {// 对0位特判
            count = 1;
        }
        for (int i = 1; i < length; i++) {
            left[i] = left[i - 1] + count;
            if (boxes.charAt(i) == '1') {
                count++;
            }
        }
        count = 0;// 计数器归零
        if (boxes.charAt(length - 1) == '1') {// 对length - 1特判
            count = 1;
        }
        for (int i = length - 2; i >= 0; i--) {
            right[i] = right[i + 1] + count;
            if (boxes.charAt(i) == '1') {
                count++;
            }
        }
        for (int i = 0; i < length; i++) {
            result[i] = left[i] + right[i];
        }
        return result;
    }
}

 解法二:

假设一个盒子i的操作数是total,观察这个位置,它的左侧(包含自身)有a个球,右侧有b个球。

由此条件,可以知道,这个盒子右侧的盒子i + 1的操作数是total + a - b,i + 1盒子和i盒子对比,距离左侧的a个球,距离都加了1,所以加了a,距离右侧的b个球,距离都减了1,所以减了b。

也就是说,如果我知道了一个盒子数据total、a、b,那么其他的盒子都可以用total + a - b来算出来,在遍历的过程中,total、a、b的值随着碰到1的情况,会有增减。total是临近的盒子,并非第一个盒子。

class Solution {
    public int[] minOperations(String boxes) {
        int length = boxes.length();
        int[] result = new int[length];
        int left = 0, right = 0, total = 0;
        if (boxes.charAt(0) == '1') {
            left++;
        }
        // 计算result[0]的操作次数
        for (int i = 1; i < length; i++) {
            if (boxes.charAt(i) == '1') {
                total += i;
                right++;
            }
        }
        result[0] = total;
        // 根据total、left、right计算剩余元素的操作次数
        // total其实是临近的一个盒子,并不是第一个盒子
        for (int i = 1; i < length; i++) {
            total += left - right;
            if (boxes.charAt(i) == '1') {
                left++;
                right--;
            }
            result[i] = total;
        }
        return result;
    }
}

第三题:执行乘法运算的最大分数

一开始以为是贪心,按照贪心写了一下,发现连测试用例都过不了,很明显方法找错了。正确的方法是动态规划。那就先找状态转移方程。

我们定义dp[i][j]表示,左侧选了i个数字,右侧选了j个数字对应的结果,当i + j == m时,查看这些dp[i][j],选出最大值。

dp[0][0] = 0。

i == 0的时候,总是从右侧取,dp[0][j] = dp[0][j - 1] + multipliers[i + j - 1] * nums[n - j]。

j == 0的时候,总是从左侧取,dp[i][0] = dp[i - 1][0] + multipliers[i + j - 1] * nums[i - 1]。

dp[i][j]来自两个方向:dp[i - 1][j] + multipliers[i + j - 1] * nums[i - 1]或dp[i][j - 1] + multipliers[i + j - 1] * nums[n - j],分别代表取左侧第i个或取右侧第j个。

查找所有i + j == m的情况,找到最大值。

class Solution {
    public int maximumScore(int[] nums, int[] multipliers) {
        int n = nums.length, m = multipliers.length, max = Integer.MIN_VALUE;
        int[][] dp = new int[m + 1][m + 1];
        // 总是从左侧取
        for (int i = 1; i <= m; i++) {
            dp[i][0] = dp[i - 1][0] + multipliers[i - 1] * nums[i - 1];
        }
        // 总是从右侧取
        for (int j = 1; j <= m; j++) {
            dp[0][j] = dp[0][j - 1] + multipliers[j - 1] * nums[n - j];
        }
        // 其他情况
        for (int i = 1; i < m; i++) {
            for (int j = 1; i + j <= m; j++) {
                dp[i][j] = Math.max(dp[i - 1][j] + multipliers[i + j - 1] * nums[i - 1], dp[i][j - 1] + multipliers[i + j - 1] * nums[n - j]);
            }
        }
        for (int i = 0; i <= m; i++) {
            max = Math.max(dp[i][m - i], max);
        }
        return max;
    }
}

第四题:由子序列构造的最长回文串的长度

要做这个题,先了解下另两道题:最长回文子串最长回文子序列。这两道题如果明白了,就更容易过渡到这个题上了。

因为最长回文子串最长回文子序列这我也是现学现做的,所以把这道题的解法也写在这里了。

最长回文子串

暴力解法

依次判断每个区间[i, j]是不是回文串,维护一个起始位置start和一个最大长度maxLength,用于最后的substring(start, start + maxLength)。

class Solution {
    public String longestPalindrome(String s) {
        int length = s.length(), start = 0, maxLength = 1;
        if (length < 2) {
            return s;
        }
        for (int i = 0; i < length - 1; i++) {
            for (int j = i + 1; j < length; j++) {
                if (j - i + 1 > maxLength && judge(s, i, j)) {
                    start = i;
                    maxLength = j - i + 1;
                }
            }
        }
        return s.substring(start, start + maxLength);
    }

    /**
     * 判断s[start,end]是不是回文串
     *
     * @param s
     * @param start
     * @param end
     * @return
     */
    private boolean judge(String s, int start, int end) {
        while (start < end) {
            if (s.charAt(start++) != s.charAt(end--)) {
                return false;
            }
        }
        return true;
    }
}

中心扩散

以一个字符(奇扩展)或两个字符(偶扩展)作为中心,向两侧进行扩展,并判断两边界值是否相同,记录最大长度,如果最大长度是奇数,那么就是奇扩展,否则就是偶扩展。

根据返回的最大长度,用中心位置 - (最大长度 - 1) / 2,获取start位置,遍历过程中,不断维护最大长度,最后做截取。

class Solution {
    public String longestPalindrome(String s) {
        int length = s.length(), start = 0, maxLength = 1;
        if (length < 2) {
            return s;
        }
        for (int i = 0; i < length; i++) {
            // 分别讨论奇扩展和偶扩展
            int odd = expand(s, length, i, i);
            int even = expand(s, length, i, i + 1);
            if (odd > maxLength) {
                start = i - (odd - 1) / 2;
                maxLength = odd;
            }
            if (even > maxLength) {
                start = i - (even - 1) / 2;
                maxLength = even;
            }
        }
        return s.substring(start, start + maxLength);
    }

    /**
     * @param s
     * @param length
     * @param start
     * @param end
     * @return 返回最大可以expand的长度(双侧总长度)
     * 如果长度是奇数,那么就是奇扩展
     * 如果长度是偶数,那么就是偶扩展
     */
    private int expand(String s, int length, int start, int end) {
        while (start >= 0 && end < length && s.charAt(start) == s.charAt(end)) {
            start--;
            end++;
        }
        return end - start - 1;
    }
}

动态规划1

用dp[i][j]表示字符串[i, j]是不是回文串。首先dp[i][i]一定是回文串,回文串总长度是1。

因为i≤j,所以在完成dp[i][j]的填充后,dp[i][j]的上三角有值,下三角都是空的。

chars[i] != chars[j]:dp[i][j] = false;

chars[i] == chars[j]:dp[i][j] = j - i < 3 || dp[i + 1][j - 1];

当dp[i][j] = dp[i + 1][j - 1]的时候,dp[i][j]的值来自它左下角位置的值,在填充dp[i][j]的时候,需要按列填充。如果按照行填充,dp[1][3]的值来自dp[2][2],此时dp[2][2]还没有运算呢,所以不能按行填充。

class Solution {
    public String longestPalindrome(String s) {
        int length = s.length(), start = 0, maxLength = 1;
        if (length < 2) {
            return s;
        }
        // dp[i][j]用来标记字符串s[i, j]是不是回文串
        boolean[][] dp = new boolean[length][length];
        // 字符串只有一个字符,是回文串
        for (int i = 0; i < length; i++) {
            dp[i][i] = true;
        }
        // 如果dp[i + 1][j - 1] == true && s[i] == s[j],即s[i + 1, j - 1]是回文串 并且 s[i] 和 s[j]相等
        // 那么dp[i][j] == true,即s[i, j]也是回文串
        // 状态转移方程:dp[i][j] = dp[i + 1][j - 1] && s[i] == s[j]
        // 只需要考虑i ≤ j,那么在填充dp的时候,只会填充主对角线以上的元素
        // 在填充的时候,要采用列优先,否则会在取dp[i + 1][j - 1]值的时候,dp[i + 1][j - 1]的值可能还没算出来
        // 但是这么写是不正确的,比如dp[0][1] = dp[1][0] && s.charAt(0) == s.charAt(1)
        // 当s = "aa"的时候,dp[1][0]按照数组默认值是false的,但是实际上dp[1][0]是无意义的
        // 所以,并不是所有的情况,都符合状态转移方程的
        // 重新分析一下
        // 当s[i] != s[j]的时候,dp[i][j]一定是false的
        // 当s[i] == s[j]的时候,分成两种情况:
        // 1.j - i < 3:如果j - i == 0,那么dp[i][j] = true;如果j - i == 1,那么dp[i][j] = true;如果j - i == 2,那么dp[i][j] = true;
        // 2.其他:dp[i][j] = dp[i + 1][j - 1]

        // 遍历[1,length)列
        for (int j = 1; j < length; j++) {
            // 第j列填充j次,第一个填充的是dp[0][1],所以这里i是[0,j)
            for (int i = 0; i < j; i++) {
                dp[i][j] = (s.charAt(i) == s.charAt(j)) && (j - i < 3 || dp[i + 1][j - 1]);
                // 只要有dp[i][j] == true的,就要检查是否更新maxLength
                if (dp[i][j] && j - i + 1 > maxLength) {
                    start = i;
                    maxLength = j - i + 1;
                }
            }
        }
        return s.substring(start, start + maxLength);
    }
}

动态规划2

class Solution {
    public String longestPalindrome(String s) {
        int length = s.length(), start = 0, maxLength = 1;
        if (length < 2) {
            return s;
        }
        // dp[i][j]用来标记字符串s[i, j]是不是回文串
        boolean[][] dp = new boolean[length][length];
        // l为当前分析的回文串的长度,分析[1, length]所有情况
        for (int l = 1; l <= length; l++) {
            // 长度是l的时候,字符串左边界下标范围[0, length - l]
            for (int i = 0; i <= length - l; i++) {
                int j = l + i - 1;// j:长度是l的时候,右边界的下标
                if (l == 1) {// l == 1,那么i == j,必然是true
                    dp[i][j] = true;
                } else if (l == 2) {// l == 2,那么dp[i][j]取决于s.charAt(i) == s.charAt(j)
                    dp[i][j] = s.charAt(i) == s.charAt(j);
                } else {// 其他情况
                    dp[i][j] = s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1];
                }
                if (dp[i][j] && l > maxLength) {
                    start = i;
                    maxLength = l;
                }
            }
        }
        return s.substring(start, start + maxLength);
    }
}

Manacher算法

暂时没看懂,看懂了再来补上。

最长回文子序列

二维数组解法

用dp[i][j]记录s[i, j]的最长回文子序列的长度。

当长度是1的时候,dp[i][i]肯定是1。

当扫描到的边界字符是相同的,即s[i] == s[j],那么s[i, j]字符串,回文长度要在原来基础上(dp[i + 1, j - 1])增加2,即dp[i][j] = dp[i + 1][j - 1] + 2。

当扫描到的字符不相同时,添加这个字符对回文串是没有贡献值的,这里取dp[i + 1][j]和dp[i][j - 1]的较大者。

// 斜着遍历
class Solution {
    public int longestPalindromeSubseq(String s) {
        int length = s.length();
        int[][] dp = new int[length][length];
        // l为当前分析的回文串的长度,分析[1, length]所有情况
        for (int l = 1; l <= length; l++) {
            // 长度是l的时候,字符串左边界下标范围[0, length - l]
            for (int i = 0; i <= length - l; i++) {
                int j = i + l - 1;// j:长度是l的时候,右边界的下标
                if (l == 1) {
                    dp[i][j] = 1;
                } else {
                    if (s.charAt(i) == s.charAt(j)) {
                        dp[i][j] = dp[i + 1][j - 1] + 2;
                    } else {
                        dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
                    }
                }
            }
        }
        return dp[0][length - 1];
    }
}
// i递减,j递增
class Solution {
    public int longestPalindromeSubseq(String s) {
        int length = s.length();
        int[][] dp = new int[length][length];
        for (int i = 0; i < length; i++) {
            dp[i][i] = 1;
        }
        // dp[i][j]的值需要用到dp[i + 1][j - 1]、dp[i + 1][j]、dp[i][j - 1]
        // 因此采用i递减、j递增的遍历方式遍历
        for (int i = length - 2; i >= 0; i--) {
            for (int j = i + 1; j < length; j++) {
                if (s.charAt(i) == s.charAt(j)) {
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                } else {
                    dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[0][length - 1];
    }
}

一维数组解法

在更新dp[i][j]值的时候,我们会用到dp[i + 1][j - 1],dp[i + 1][j],dp[i][j - 1]。

状态压缩可以看作是二维数组向一维数组的一个投影,通常是把i坐标删掉,也就是向x轴方向投影,那么,就会存在覆盖的问题,我们需要在覆盖前,用一个临时变量存储一下,后面再用到这个值,就用临时变量即可。

class Solution {
    public int longestPalindromeSubseq(String s) {
        int length = s.length();
        // 状态压缩,仅用一维数组和临时变量存储中间值
        int[] dp = new int[length];
        for (int i = 0; i < length; i++) {
            dp[i] = 1;
        }
        for (int i = length - 2; i >= 0; i--) {
            int pre = 0;// pre是用来存储之前二维数组中的dp[i + 1][j - 1]的
            for (int j = i + 1; j < length; j++) {
                int temp = dp[j];
                // 在更新之前,dp[j]对应的是dp[i + 1][j],dp[j - 1]对应的是dp[i][j - 1]
                if (s.charAt(i) == s.charAt(j)) {
                    dp[j] = pre + 2;
                } else {
                    dp[j] = Math.max(dp[j], dp[j - 1]);
                }
                // j = i + 1的时候,把dp[j](即dp[i + 1][j])保存到pre里
                // j = i + 2的时候,pre就是dp[i + 1][j - 1]了,j增加1了,不过,这里用pre把上一轮的值保存下来了,pre就相当于这一轮循环的dp[i + 1][j - 1]了
                pre = temp;
            }
        }
        return dp[length - 1];
    }
}

回到原题,我们考虑将word1和word2拼接起来,这里的限制条件是:选出的回文串,word1至少取一个,word2至少取一个,即回文串必须来自两个子序列,不能只来自其中一个。我们需要在最长回文子序列的基础上,添加一个限制条件即可解决。

class Solution {
    public int longestPalindrome(String word1, String word2) {
        int l1 = word1.length(), l2 = word2.length(), l = l1 + l2, maxLength = 0;
        String word = word1 + word2;
        // 用dp[i][j]表示word[i,j]中最长回文子序列的长度,
        int[][] dp = new int[l][l];
        // 虽然dp[i][i]不符合题意,但是dp[i][i]需要作为后面更新数据的基础
        for (int i = 0; i < l; i++) {
            dp[i][i] = 1;
        }
        for (int i = l - 2; i >= 0; i--) {
            for (int j = i + 1; j < l; j++) {
                if (word.charAt(i) == word.charAt(j)) {
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                    // 注意这里多出的条件:i < l1 && l1 <= j
                    // 为什么要把这个条件放在if里面,不能放在else的外面
                    // 对于这个例子:"aa","bb",在计算dp[1][3]的时候,dp[2][3]=2,dp[1][2]=0,此时更新dp[1][3]=2
                    // dp[1][3]中的i,j满足i < l1 && l1 <= j条件,但是实际的回文串是bb,也就是dp[2][3],word1里没有选择元素,这是不符合题意的
                    if (dp[i][j] > maxLength && i < l1 && l1 <= j) {
                        maxLength = dp[i][j];
                    }
                } else {
                    dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }
        return maxLength;
    }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值