2843.统计对称整数的数目

目标

给你两个正整数 low 和 high 。

对于一个由 2 * n 位数字组成的整数 x ,如果其前 n 位数字之和与后 n 位数字之和相等,则认为这个数字是一个对称整数。

返回在 [low, high] 范围内的 对称整数的数目 。

示例 1:

输入:low = 1, high = 100
输出:9
解释:在 1 到 100 范围内共有 9 个对称整数:11、22、33、44、55、66、77、88 和 99 。

示例 2:

输入:low = 1200, high = 1230
输出:4
解释:在 1200 到 1230 范围内共有 4 个对称整数:1203、1212、1221 和 1230 。

说明:

  • 1 <= low <= high <= 10^4

思路

计算给定区间内的对称整数数目,对称整数的长度为偶数,且左边数字之和等于右边数字之和。

数据范围小可以直接暴力枚举。

代码

class Solution {
    public int countSymmetricIntegers(int low, int high) {
        int res = 0;
        for (int i = low; i <= high; i++) {
            String num = String.valueOf(i);
            int r = num.length();
            if (r % 2 == 1) {
                continue;
            }
            r--;
            int l = 0;
            int diff = 0;
            while (l < r) {
                diff += num.charAt(l++) - num.charAt(r--);
            }
            res += diff != 0 ? 0 : 1;
        }
        return res;
    }
}

性能

2999.统计强大整数的数目

目标

给你三个整数 start ,finish 和 limit 。同时给你一个下标从 0 开始的字符串 s ,表示一个 正 整数。

如果一个 正 整数 x 末尾部分是 s (换句话说,s 是 x 的 后缀),且 x 中的每个数位至多是 limit ,那么我们称 x 是 强大的 。

请你返回区间 [start..finish] 内强大整数的 总数目 。

如果一个字符串 x 是 y 中某个下标开始(包括 0 ),到下标为 y.length - 1 结束的子字符串,那么我们称 x 是 y 的一个后缀。比方说,25 是 5125 的一个后缀,但不是 512 的后缀。

示例 1:

输入:start = 1, finish = 6000, limit = 4, s = "124"
输出:5
解释:区间 [1..6000] 内的强大数字为 124 ,1124 ,2124 ,3124 和 4124 。这些整数的各个数位都 <= 4 且 "124" 是它们的后缀。注意 5124 不是强大整数,因为第一个数位 5 大于 4 。
这个区间内总共只有这 5 个强大整数。

示例 2:

输入:start = 15, finish = 215, limit = 6, s = "10"
输出:2
解释:区间 [15..215] 内的强大整数为 110 和 210 。这些整数的各个数位都 <= 6 且 "10" 是它们的后缀。
这个区间总共只有这 2 个强大整数。

示例 3:

输入:start = 1000, finish = 2000, limit = 4, s = "3000"
输出:0
解释:区间 [1000..2000] 内的整数都小于 3000 ,所以 "3000" 不可能是这个区间内任何整数的后缀。

说明:

  • 1 <= start <= finish <= 10^15
  • 1 <= limit <= 9
  • 1 <= s.length <= floor(log10(finish)) + 1
  • s 数位中每个数字都小于等于 limit 。
  • s 不包含任何前导 0 。

思路

返回指定区间 [start, finish] 内,后缀为 s 且每个数字不超过 limit 的数字个数。

数位dp,需要特殊处理后缀,比如 s = 10,start = 101, finish = 521 还剩两位时,01 < 10, 21 > 10 都不能计数。

代码


/**
 * @date 2025-04-10 20:19
 */
public class NumberOfPowerfulInt2999 {

    public long numberOfPowerfulInt(long start, long finish, int limit, String s) {
        long suffix = Long.parseLong(s);
        if (finish < suffix) {
            return 0L;
        }
        int[] high = Long.toString(finish).chars().map(x -> x - '0').toArray();
        int hl = high.length;
        long[] mem = new long[hl];
        int[] low = new int[hl--];
        long tmp = start;
        while (tmp > 0) {
            low[hl--] = (int) (tmp % 10);
            tmp /= 10;
        }
        Arrays.fill(mem, -1L);
        return dfs(0, low, high, true, true, mem, limit, s);
    }

    public long dfs(int index, int[] low, int[] high, boolean isLowLimit, boolean isHighLimit, long[] mem, int limit, String s) {
        if (index == high.length - s.length()) {
            boolean unaviable = false;
            if (isHighLimit) {
                StringBuilder hr = new StringBuilder();
                int tmp = index;
                while (tmp < high.length) {
                    hr.append(high[tmp++]);
                }
                unaviable = Long.parseLong(hr.toString()) < Long.parseLong(s);
            }
            if (isLowLimit) {
                StringBuilder lr = new StringBuilder();
                while (index < high.length) {
                    lr.append(low[index++]);
                }
                unaviable = unaviable || Long.parseLong(lr.toString()) > Long.parseLong(s);
            }
            return unaviable ? 0 : 1;
        }
        if (!isLowLimit && !isHighLimit && mem[index] != -1) {
            return mem[index];
        }
        long res = 0;
        int up = isHighLimit ? Math.min(high[index], limit) : limit;
        int down = isLowLimit ? low[index] : 0;

        for (int i = down; i <= up; i++) {
            res += dfs(index + 1, low, high, isLowLimit && i == low[index], isHighLimit && i == high[index], mem, limit, s);
        }

        if (!isHighLimit && !isLowLimit) {
            mem[index] = res;
        }
        return res;
    }

}

性能

416.分割等和子集

目标

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。

示例 2:

输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。

说明:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

思路

给定非空数组 nums,判断能否将数组划分成两个子序列,使得子序列的元素和相等。

可以求出所有元素和,然后记忆化搜索子序列,使用所有元素和减去子序列和可得剩余子序列的和。

代码


/**
 * @date 2025-04-07 8:47
 */
public class CanPartition416 {

    /**
     * 定义 dp[i][j] 表示 i ~ n - 1 是否存在和为 j 的子序列,初始化 dp[n][0] = true
     * 状态转移方程为 dp[i][j] = dp[i + 1][j] || dp[i + 1][j - nums[i]]
     */
    public boolean canPartition_v1(int[] nums) {
        int t = Arrays.stream(nums).sum();
        if (t % 2 != 0) {
            return false;
        }
        int n = nums.length;
        boolean[][] dp = new boolean[n + 1][t / 2 + 1];
        dp[n][0] = true;
        for (int i = n - 1; i >= 0; i--) {
            for (int j = 0; j <= t / 2; j++) {
                dp[i][j] = j >= nums[i] && dp[i + 1][j - nums[i]] || dp[i + 1][j];
            }
        }
        return dp[0][t / 2];
    }

    int total;

    public boolean canPartition(int[] nums) {
        for (int num : nums) {
            total += num;
        }
        if (total % 2 != 0) {
            return false;
        }
        int[][] mem = new int[nums.length][total + 1];
        for (int[] m : mem) {
            Arrays.fill(m, -1);
        }
        return dfs(0, nums, 0, mem);
    }

    public boolean dfs(int index, int[] nums, int sum, int[][] mem) {
        if (index == nums.length) {
            return total == sum << 1;
        }
        if (mem[index][sum] != -1) {
            return mem[index][sum] == 1;
        }
        boolean res;
        res = dfs(index + 1, nums, sum, mem);
        if (!res) {
            res = dfs(index + 1, nums, sum + nums[index], mem);
        }
        mem[index][sum] = res ? 1 : 0;
        return res;
    }

}

性能

368.最大整除子集

目标

给你一个由 无重复 正整数组成的集合 nums ,请你找出并返回其中最大的整除子集 answer ,子集中每一元素对 (answer[i], answer[j]) 都应当满足:

  • answer[i] % answer[j] == 0 ,或
  • answer[j] % answer[i] == 0

如果存在多个有效解子集,返回其中任何一个均可。

示例 1:

输入:nums = [1,2,3]
输出:[1,2]
解释:[1,3] 也会被视为正确答案。

示例 2:

输入:nums = [1,2,4,8]
输出:[1,2,4,8]

说明:

  • 1 <= nums.length <= 1000
  • 1 <= nums[i] <= 2 * 10^9
  • nums 中的所有整数 互不相同

思路

// todo

代码

性能

2140.解决智力问题

目标

给你一个下标从 0 开始的二维整数数组 questions ,其中 questions[i] = [pointsi, brainpoweri] 。

这个数组表示一场考试里的一系列题目,你需要 按顺序 (也就是从问题 0 开始依次解决),针对每个问题选择 解决 或者 跳过 操作。解决问题 i 将让你 获得 pointsi 的分数,但是你将 无法 解决接下来的 brainpoweri 个问题(即只能跳过接下来的 brainpoweri 个问题)。如果你跳过问题 i ,你可以对下一个问题决定使用哪种操作。

比方说,给你 questions = [[3, 2], [4, 3], [4, 4], [2, 5]] :

  • 如果问题 0 被解决了, 那么你可以获得 3 分,但你不能解决问题 1 和 2 。
  • 如果你跳过问题 0 ,且解决问题 1 ,你将获得 4 分但是不能解决问题 2 和 3 。

请你返回这场考试里你能获得的 最高 分数。

示例 1:

输入:questions = [[3,2],[4,3],[4,4],[2,5]]
输出:5
解释:解决问题 0 和 3 得到最高分。
- 解决问题 0 :获得 3 分,但接下来 2 个问题都不能解决。
- 不能解决问题 1 和 2
- 解决问题 3 :获得 2 分
总得分为:3 + 2 = 5 。没有别的办法获得 5 分或者多于 5 分。

示例 2:

输入:questions = [[1,1],[2,2],[3,3],[4,4],[5,5]]
输出:7
解释:解决问题 1 和 4 得到最高分。
- 跳过问题 0
- 解决问题 1 :获得 2 分,但接下来 2 个问题都不能解决。
- 不能解决问题 2 和 3
- 解决问题 4 :获得 5 分
总得分为:2 + 5 = 7 。没有别的办法获得 7 分或者多于 7 分。

说明:

  • 1 <= questions.length <= 10^5
  • questions[i].length == 2
  • 1 <= pointsi, brainpoweri <= 10^5

思路

有一个二维数组 questions 表示一场考试里的一系列题目,questions[i][0] 表示解决第 i 题能获得的分数,questions[i][1] 表示解决该题需要消耗的脑力,即解决了第 i 题后,i 后面的 questions[i][1] 个题目都无法解决。返回在该场考试所能获得的最高分。

这个题有许多值得思考的地方,有空整理一下。//todo

代码


/**
 * @date 2025-04-01 8:47
 */
public class MostPoints2140 {

    public long mostPoints(int[][] questions) {
        int n = questions.length;
        long[] dp = new long[n + 1];
        for (int i = n - 1; i >= 0; i--) {
            int j = Math.min(i + questions[i][1] + 1, n);
            dp[i] = Math.max(dp[i + 1], dp[j] + questions[i][0]);
        }
        return dp[0];
    }

}

性能

1745.分割回文串IV

目标

给你一个字符串 s ,如果可以将它分割成三个 非空 回文子字符串,那么返回 true ,否则返回 false 。

当一个字符串正着读和反着读是一模一样的,就称其为 回文字符串 。

示例 1:

输入:s = "abcbdd"
输出:true
解释:"abcbdd" = "a" + "bcb" + "dd",三个子字符串都是回文的。

示例 2:

输入:s = "bcbddxy"
输出:false
解释:s 没办法被分割成 3 个回文子字符串。

说明:

  • 3 <= s.length <= 2000
  • s​​​​​​ 只包含小写英文字母。

思路

判断能否将给定字符串分割成三个非空回文子串。

核心逻辑:

  • 计算所有子串是否是回文。
  • 从起点 i = 0 开始,枚举所有终点 j,如果是 s[i~j] 是回文,k-- 接着以 j + 1 为起点继续递归搜索。
  • 结束条件是 i == s.length() || k < 0,如果 k == 0 返回 true。

代码


/**
 * @date 2025-03-04 15:58
 */
public class CheckPartitioning1745 {

    public boolean checkPartitioning(String s) {
        int n = s.length();
        boolean[][] isPalindrome = new boolean[n][n];
        for (boolean[] row : isPalindrome) {
            Arrays.fill(row, true);
        }
        for (int i = n - 2; i >= 0; i--) {
            for (int j = i + 1; j < n; j++) {
                isPalindrome[i][j] = s.charAt(i) == s.charAt(j) && isPalindrome[i + 1][j - 1];
            }
        }
        char[][] mem = new char[n][4];
        return dfs(0, 3, isPalindrome, s, mem);
    }

    public boolean dfs(int i, int k, boolean[][] isPalindrome, String s, char[][] mem) {
        int n = s.length();
        if (i == n || k < 0) {
            return k == 0;
        }
        if (mem[i][k] != '\u0000') {
            return mem[i][k] == 'T';
        }
        boolean res = false;
        for (int j = i; j < n; j++) {
            if (isPalindrome[i][j]) {
                res = res || dfs(j + 1, k - 1, isPalindrome, s, mem);
            }
        }
        mem[i][k] = res ? 'T' : 'F';
        return res;
    }

}

性能

1278.分割回文串III

目标

给你一个由小写字母组成的字符串 s,和一个整数 k。

请你按下面的要求分割字符串:

  • 首先,你可以将 s 中的部分字符修改为其他的小写英文字母。
  • 接着,你需要把 s 分割成 k 个非空且不相交的子串,并且每个子串都是回文串。

请返回以这种方式分割字符串所需修改的最少字符数。

示例 1:

输入:s = "abc", k = 2
输出:1
解释:你可以把字符串分割成 "ab" 和 "c",并修改 "ab" 中的 1 个字符,将它变成回文串。

示例 2:

输入:s = "aabbc", k = 3
输出:0
解释:你可以把字符串分割成 "aa"、"bb" 和 "c",它们都是回文串。

示例 3:

输入:s = "leetcode", k = 8
输出:0

说明:

  • 1 <= k <= s.length <= 100
  • s 中只含有小写英文字母。

思路

将字符串 s 分割成 k 个非空回文子串,允许修改字符中的任意字符,求修改字符的最小次数。

需要将字符串分割成 k 份并且每一份都是回文。我们需要暴力枚举所有可能的分法,并求得每种分法的修改次数,取其最小值。

核心逻辑:

  • 计算所有子串变为回文的修改次数,ops[i][j] = s.charAt(i) == s.charAt(j) ? ops[i + 1][j - 1] : ops[i + 1][j - 1] + 1;。由于需要从 i + 1 转移过来,所以外层倒序。初始化数组所有值为 0,外层从倒数第二个开始,内层从 i + 1 开始。
  • 从起点 i = 0 开始,枚举所有终点 j,无论 s[i~j] 是否是回文,我们直接加上 ops[i][j]k-- 接着以 j + 1 为起点继续递归搜索。
  • 结束条件:
    • i 未到结尾 k 先减为 0,不符合要求,返回 INF,可以取 0x3f3f3f3f
    • i == s.length(),如果 k == 0 返回 0,否则说明分割的子串没有 k 个,不符合题意返回 INF

代码


/**
 * @date 2025-03-03 8:52
 */
public class PalindromePartition1278 {

    public int palindromePartition(String s, int k) {
        int n = s.length();
        int[][] ops = new int[n][n];
        for (int i = n - 2; i >= 0; i--) {
            for (int j = i + 1; j < n; j++) {
                ops[i][j] = s.charAt(i) == s.charAt(j) ? ops[i + 1][j - 1] : ops[i + 1][j - 1] + 1;
            }
        }
        int[][] mem = new int[n][k + 1];
        for (int[] row : mem) {
            Arrays.fill(row, -1);
        }
        return dfs(0, k, s, ops, mem);
    }

    public int dfs(int i, int k, String s, int[][] ops, int[][] mem) {
        int n = s.length();
        if (i == n) {
            return k > 0 ? 0x3f3f3f3f : 0;
        }
        if (k == 0) {
            return 0x3f3f3f3f;
        }
        if (mem[i][k] != -1) {
            return mem[i][k];
        }
        int res = Integer.MAX_VALUE;
        for (int j = i; j < n; j++) {
            res = Math.min(res, ops[i][j] + dfs(j + 1, k - 1, s, ops, mem));
        }
        mem[i][k] = res;
        return res;
    }

}

性能

132.分割回文串II

目标

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串。

返回符合要求的 最少分割次数 。

示例 1:

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

示例 2:

输入:s = "a"
输出:0

示例 3:

输入:s = "ab"
输出:1

说明:

  • 1 <= s.length <= 2000
  • s 仅由小写英文字母组成

思路

计算将字符串分割成回文子串的最小分割次数。

定义 dp[i] 表示前 i + 1 个字符的最小分割次数,如果 [0, i] 个字符已经是回文,无需切割,切割次数为 0。否则,需要枚举起点 j,如果 [j, i] 是回文,dp[i] = Math.min(dp[j - 1] + 1)

预处理 [i, j] 是否是回文,isPalindrome[i][j] = s.charAt(i) == s.charAt(j) && isPalindrome[i + 1][j - 1],由于状态由 i + 1 转换而来,外层要倒序,内层正序或倒序都可以。

代码


/**
 * @date 2025-03-02 0:10
 */
public class MinCut132 {

    public int minCut(String s) {
        int n = s.length();
        boolean[][] isPalindrome = new boolean[n][n];
        for (boolean[] row : isPalindrome) {
            Arrays.fill(row, true);
        }
        for (int i = n - 2; i >= 0; i--) {
            for (int j = i + 1; j < n; j++) {
                isPalindrome[i][j] = s.charAt(i) == s.charAt(j) && isPalindrome[i + 1][j - 1];
            }
        }
        int[] dp = new int[n];
        for (int i = 0; i < n; i++) {
            if (isPalindrome[0][i]) {
                continue;
            }
            int res = Integer.MAX_VALUE;
            for (int j = 1; j <= i; j++) {
                if (isPalindrome[j][i]) {
                    res = Math.min(res, dp[j - 1] + 1);
                }
            }
            dp[i] = res;
        }
        return dp[n - 1];
    }

}

性能

131.分割回文串

目标

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

示例 1:

输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

示例 2:

输入:s = "a"
输出:[["a"]]

说明:

  • 1 <= s.length <= 16
  • s 仅由小写英文字母组成

思路

返回将字符串划分为回文子串的所有可能方案。

定义 dp[i] 表示将前 i + 1 个字符划分为回文子串的所有可能方案。dp[i] 可以取 dp[i - 1] 每种方案的最后一或两个回文,判断能否与当前字符合并成新的回文。

代码


/**
 * @date 2025-03-01 20:16
 */
public class Partition131 {

    public List<List<String>> partition(String s) {
        int n = s.length();
        List<List<String>>[] dp = new List[n];
        for (int i = 0; i < n; i++) {
            dp[i] = new ArrayList<>();
        }
        char[] chars = s.toCharArray();
        dp[0].add(new ArrayList<>());
        dp[0].get(0).add((String.valueOf(chars[0])));
        for (int i = 1; i < n; i++) {
            dp[i] = new ArrayList<>();
            for (List<String> list : dp[i - 1]) {
                // 首先将单个字符加入
                List<String> tmp = new ArrayList<>(list);
                tmp.add(String.valueOf(chars[i]));
                dp[i].add(tmp);
                String cur = String.valueOf(chars[i]);
                // 判断能与最后一个回文合并成新的回文
                if (list.get(list.size() - 1).equals(cur)) {
                    tmp = new ArrayList<>(list.subList(0, list.size() - 1));
                    tmp.add(list.get(list.size() - 1) + cur);
                    dp[i].add(tmp);
                }
                // 判断能否与后两个回文合并成新的回文
                if (list.size() > 1 && list.get(list.size() - 2).equals(cur)) {
                    tmp = new ArrayList<>(list.subList(0, list.size() - 2));
                    tmp.add(list.get(list.size() - 2) + list.get(list.size() - 1) + cur);
                    dp[i].add(tmp);
                }
            }
        }
        return dp[n - 1];
    }

}

性能

时间复杂度 O(n * 2^n),假设每个字符之间都有一个逗号,考虑选或不选共有 2^(n - 1) 种划分。前 i + 1 个 字符有 2^i 种划分, 等比数列求和得到 2^n - 1

2944.购买水果需要的最少金币数

目标

给你一个 下标从 1 开始的 整数数组 prices ,其中 prices[i] 表示你购买第 i 个水果需要花费的金币数目。

水果超市有如下促销活动:

  • 如果你花费 prices[i] 购买了下标为 i 的水果,那么你可以免费获得下标范围在 [i + 1, i + i + 1] 的水果。

注意 ,即使你 可以 免费获得水果 j ,你仍然可以花费 prices[j] 个金币去购买它以获得它的奖励。

请你返回获得所有水果所需要的 最少 金币数。

示例 1:

输入:prices = [3,1,2]
输出:4
解释:
用 prices[0] = 3 个金币购买第 1 个水果,你可以免费获得第 2 个水果。
用 prices[1] = 1 个金币购买第 2 个水果,你可以免费获得第 3 个水果。
免费获得第 3 个水果。
请注意,即使您可以免费获得第 2 个水果作为购买第 1 个水果的奖励,但您购买它是为了获得其奖励,这是更优化的。

示例 2:

输入:prices = [1,10,1,1]
输出:2
解释:
用 prices[0] = 1 个金币购买第 1 个水果,你可以免费获得第 2 个水果。
免费获得第 2 个水果。
用 prices[2] = 1 个金币购买第 3 个水果,你可以免费获得第 4 个水果。
免费获得第 4 个水果。

示例 3:

输入:prices = [26,18,6,12,49,7,45,45]
输出:39
解释:
用 prices[0] = 26 个金币购买第 1 个水果,你可以免费获得第 2 个水果。
免费获得第 2 个水果。
用 prices[2] = 6 个金币购买第 3 个水果,你可以免费获得第 4,5,6(接下来的三个)水果。
免费获得第 4 个水果。
免费获得第 5 个水果。
用 prices[5] = 7 个金币购买第 6 个水果,你可以免费获得第 7 和 第 8 个水果。
免费获得第 7 个水果。
免费获得第 8 个水果。
请注意,即使您可以免费获得第 6 个水果作为购买第 3 个水果的奖励,但您购买它是为了获得其奖励,这是更优化的。

说明:

1 <= prices.length <= 1000
1 <= prices[i] <= 10^5

思路

有 n 个水果,其价格由 prices 表示,当我们以 prices[i] 枚金币购买了第 i + 1 个苹果时,我们可以免费获得下标 [i + 1, i + i + 1]所有 个苹果(当然也可以购买以获得后面的奖励),求获得全部苹果所需的最少硬币。

最直接的想法是记忆化搜索。

代码


/**
 * @date 2025-01-24 9:07
 */
public class MinimumCoins2944 {

    public int minimumCoins(int[] prices) {
        int[] mem = new int[2 * prices.length + 3];
        Arrays.fill(mem, Integer.MAX_VALUE);
        return dfs(0, prices, mem, 0);
    }

    public int dfs(int index, int[] prices, int[] mem, int cost) {
        int n = prices.length;
        if (index >= n) {
            return cost;
        }
        int res = cost + prices[index];
        int next = index * 2 + 2;
        if (mem[next] == Integer.MAX_VALUE) {
            mem[next] = dfs(next, prices, mem, 0);
        }
        int remainder = mem[next];
        if (remainder == 0) {
            return res;
        }
        for (int i = index + 1; i < n && i <= index * 2 + 1; i++) {
            if (mem[i] == Integer.MAX_VALUE) {
                mem[i] = dfs(i, prices, mem, 0);
            }
            remainder = Math.min(mem[i], remainder);
        }
        return res + remainder;
    }

}

性能