3003.执行操作后的最大分割数量

目标

给你一个下标从 0 开始的字符串 s 和一个整数 k。

你需要执行以下分割操作,直到字符串 s 变为 空:

  • 选择 s 的最长 前缀,该前缀最多包含 k 个 不同 字符。
  • 删除 这个前缀,并将分割数量加一。如果有剩余字符,它们在 s 中保持原来的顺序。

执行操作之 前 ,你可以将 s 中 至多一处 下标的对应字符更改为另一个小写英文字母。

在最优选择情形下改变至多一处下标对应字符后,用整数表示并返回操作结束时得到的 最大 分割数量。

示例 1:

输入:s = "accca", k = 2
输出:3
解释:
最好的方式是把 s[2] 变为除了 a 和 c 之外的东西,比如 b。然后它变成了 "acbca"。
然后我们执行以下操作:
1. 最多包含 2 个不同字符的最长前缀是 "ac",我们删除它然后 s 变为 "bca"。
2. 现在最多包含 2 个不同字符的最长前缀是 "bc",所以我们删除它然后 s 变为 "a"。
3. 最后,我们删除 "a" 并且 s 变成空串,所以该过程结束。
进行操作时,字符串被分成 3 个部分,所以答案是 3。

示例 2:

输入:s = "aabaab", k = 3
输出:1
解释:
一开始 s 包含 2 个不同的字符,所以无论我们改变哪个, 它最多包含 3 个不同字符,因此最多包含 3 个不同字符的最长前缀始终是所有字符,因此答案是 1。

示例 3:

输入:s = "xxyz", k = 1
输出:4
解释:
最好的方式是将 s[0] 或 s[1] 变为 s 中字符以外的东西,例如将 s[0] 变为 w。
然后 s 变为 "wxyz",包含 4 个不同的字符,所以当 k 为 1,它将分为 4 个部分。

说明:

  • 1 <= s.length <= 10^4
  • s 只包含小写英文字母。
  • 1 <= k <= 26

思路

有一个字符串 s 和一个整数 k,允许至多将 s 中的任意一个字符替换为其它小写英文字母,然后循环执行以下操作:删除字符串 s 的 最长 前缀,要求前缀中 最多 包含 k 个不同字符。求最大的删除次数。

// todo

代码

性能

3186.施咒的最大总伤害

目标

一个魔法师有许多不同的咒语。

给你一个数组 power ,其中每个元素表示一个咒语的伤害值,可能会有多个咒语有相同的伤害值。

已知魔法师使用伤害值为 power[i] 的咒语时,他们就 不能 使用伤害为 power[i] - 2 ,power[i] - 1 ,power[i] + 1 或者 power[i] + 2 的咒语。

每个咒语最多只能被使用 一次 。

请你返回这个魔法师可以达到的伤害值之和的 最大值 。

示例 1:

输入:power = [1,1,3,4]
输出:6
解释:
可以使用咒语 0,1,3,伤害值分别为 1,1,4,总伤害值为 6 。

示例 2:

输入:power = [7,1,6,6]
输出:13
解释:
可以使用咒语 1,2,3,伤害值分别为 1,6,6,总伤害值为 13 。

说明:

  • 1 <= power.length <= 10^5
  • 1 <= power[i] <= 10^9

思路

有一个数组 power 表示咒语的伤害值,魔法师使用其中任意咒语后,就无法再使用伤害值 +1 +2 -1 -2 的咒语,每个咒语只能使用一次,求伤害值的最大值。

使用哈希表记录伤害值相同的和,创建新数组(不包含重复伤害值),根据伤害值排序。如果不选当前伤害值,状态可以从后面转移而来,如果选则从后面大于当前伤害值 + 2 的转移而来。

代码


/**
 * @date 2025-10-11 8:57
 */
public class MaximumTotalDamage3186 {

    public long maximumTotalDamage(int[] power) {
        Map<Integer, Long> sum = new HashMap<>();
        for (int i : power) {
            sum.merge(i, (long) i, Long::sum);
        }
        int n = sum.size();
        int[] powers = new int[n];
        int i = 0;
        for (Integer damage : sum.keySet()) {
            powers[i++] = damage;
        }
        Arrays.sort(powers);
        long[] mem = new long[n + 1];
        mem[n - 1] = sum.get(powers[n - 1]);
        for (int j = n - 2; j >= 0; j--) {
            int k = j;
            while (k < n && powers[k] - powers[j] <= 2) {
                k++;
            }
            mem[j] = Math.max(mem[j + 1], mem[k] + sum.get(powers[j]));
        }
        return mem[0];
    }

}

性能

3459.最长V形对角线段的长度

目标

给你一个大小为 n x m 的二维整数矩阵 grid,其中每个元素的值为 0、1 或 2。

V 形对角线段 定义如下:

  • 线段从 1 开始。
  • 后续元素按照以下无限序列的模式排列:2, 0, 2, 0, ...。
  • 该线段:
    • 起始于某个对角方向(左上到右下、右下到左上、右上到左下或左下到右上)。
    • 沿着相同的对角方向继续,保持 序列模式 。
    • 在保持 序列模式 的前提下,最多允许 一次顺时针 90 度转向 另一个对角方向。

返回最长的 V 形对角线段 的 长度 。如果不存在有效的线段,则返回 0。

示例 1:

输入: grid = [[2,2,1,2,2],[2,0,2,2,0],[2,0,1,1,0],[1,0,2,2,2],[2,0,0,2,2]]
输出: 5
解释:
最长的 V 形对角线段长度为 5,路径如下:(0,2) → (1,3) → (2,4),在 (2,4) 处进行 顺时针 90 度转向 ,继续路径为 (3,3) → (4,2)。

示例 2:

输入: grid = [[2,2,2,2,2],[2,0,2,2,0],[2,0,1,1,0],[1,0,2,2,2],[2,0,0,2,2]]
输出: 4
解释:
最长的 V 形对角线段长度为 4,路径如下:(2,3) → (3,2),在 (3,2) 处进行 顺时针 90 度转向 ,继续路径为 (2,1) → (1,0)。

示例 3:

输入: grid = [[1,2,2,2,2],[2,2,2,2,0],[2,0,0,0,0],[0,0,2,2,2],[2,0,0,2,0]]
输出: 5
解释:
最长的 V 形对角线段长度为 5,路径如下:(0,0) → (1,1) → (2,2) → (3,3) → (4,4)。

示例 4:

输入: grid = [[1]]
输出: 1
解释:
最长的 V 形对角线段长度为 1,路径如下:(0,0)。

说明:

  • n == grid.length
  • m == grid[i].length
  • 1 <= n, m <= 500
  • grid[i][j] 的值为 0、1 或 2。

思路

代码

性能

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;
    }

}

性能