当前位置: 首页 > news >正文

每日算法-250330

记录今天的算法学习与复习。


1234. 替换子串得到平衡字符串

题目

题目截图

思路

核心思路: 滑动窗口

解题过程

根据题意,一个字符串 s 被认为是平衡的,当且仅当 ‘Q’, ‘W’, ‘E’, ‘R’ 四种字符在 s 中出现的次数都相等(即 n/4)。我们需要找到一个最短的子串,替换它之后能让整个字符串变平衡。

反向思考:如果我们要替换子串 s[l..r],那么这个子串 之外 的字符 s[0...l-1]s[r+1...n-1] 是保持不变的。为了使 整个 字符串最终平衡,这些 不变 的字符中,每种字符 (‘Q’, ‘W’, ‘E’, ‘R’) 的数量都不能超过 n/4。如果某个字符在子串外部的数量已经超过 n/4,那么无论我们如何修改子串 s[l..r],都无法使该字符的总数降到 n/4,也就无法使整个字符串平衡。

因此,我们的目标是找到一个最短的窗口 [left, right](代表要替换的子串),使得窗口 之外 的所有字符计数 (total_count[char] - window_count[char]) 均 <= n/4

我们可以使用滑动窗口来解决这个问题:

  1. 先统计整个字符串 s 中各字符的出现次数 hash[c]
  2. 如果初始状态已经是平衡的,直接返回 0。
  3. 初始化 left = 0, right = 0。扩展右边界 right,并将 s[right] 的计数从 hash 中减去(表示这个字符现在在窗口内,不属于 “窗口外” 的部分了)。
  4. 检查当前窗口是否满足条件:即窗口外的所有字符计数 (hash['Q'], hash['W'], hash['E'], hash['R']) 是否都 <= n/4
  5. 如果满足条件,说明当前窗口 [left, right] 是一个潜在的可替换子串。我们尝试缩小窗口以找到最短长度:记录当前窗口长度 right - left + 1 并更新最小值 ret;然后将 s[left] 的计数加回 hash(表示 s[left] 移出窗口,回到 “窗口外” 的部分),并将 left 右移。重复此步骤直到条件不再满足。
  6. 继续扩展右边界 right,重复步骤 3-5,直到 right 到达字符串末尾。
  7. 最终 ret 中存储的就是最短的可替换子串长度。

复杂度

  • 时间复杂度: O ( n ) O(n) O(n),其中 n 是字符串的长度。每个字符最多被 leftright 指针访问两次。
  • 空间复杂度: O ( C ) O(C) O(C) O ( 1 ) O(1) O(1),其中 C 是字符集的大小 (QWER,为常数)。我们使用了一个固定大小的哈希数组来存储字符计数。

Code

import java.util.Arrays;

class Solution {
    public int balancedString(String ss) {
        char[] s = ss.toCharArray();
        int n = s.length;
        // m 是每个字符的目标数量
        int m = n / 4;
        // ret 初始化为一个不可能的最大值
        int ret = n;
        // 统计整个字符串的字符计数
        int[] hash = new int[128];
        for (char c : s) {
            hash[c]++;
        }

        // 检查初始字符串是否已经平衡
        if (hash['Q'] == m && hash['W'] == m && hash['E'] == m && hash['R'] == m) {
            return 0;
        }

        // 滑动窗口
        for (int left = 0, right = 0; right < n; right++) {
            // 字符 s[right] 进入窗口,将其从 "窗口外计数" 中移除
            hash[s[right]]--;

            // 检查窗口外的字符计数是否都满足 <= m
            // 如果满足,尝试缩小窗口
            while (left <= right && hash['Q'] <= m && hash['W'] <= m && hash['E'] <= m && hash['R'] <= m) {
                // 更新最短窗口长度
                ret = Math.min(ret, right - left + 1);
                // 字符 s[left] 离开窗口,将其加回 "窗口外计数"
                hash[s[left]]++;
                // 缩小窗口左边界
                left++;
            }
        }

        // 最终结果
        return ret;
    }
}

682. 棒球比赛

题目

题目截图

思路

核心思路: 模拟 / 栈 (可以用数组模拟)

解题过程

题目描述了一个计分系统,包含四种操作。我们可以使用一个列表或数组来模拟记录有效得分的过程,其行为类似于栈:

  1. 遍历操作数组 ops
  2. 维护一个记录有效分数的列表(或数组和指针)。
  3. 根据当前操作 ops[i] 执行相应逻辑:
    • 如果是整数 x:将其转换为数字,添加到分数列表末尾。
    • 如果是 "+":取列表末尾的两个分数,求和,并将和添加到列表末尾。
    • 如果是 "D":取列表末尾的分数,乘以 2,并将结果添加到列表末尾。
    • 如果是 "C":移除列表末尾的分数(使其无效)。
  4. 遍历结束后,计算分数列表中所有分数的总和。

在提供的代码中,使用了数组 nums 和指针 index 来模拟这个过程,index 指向下一个可以插入元素的位置,同时也表示当前有效分数的数量。

复杂度

  • 时间复杂度: O ( n ) O(n) O(n),其中 n 是操作数组 ops 的长度。需要遍历一次数组。
  • 空间复杂度: O ( n ) O(n) O(n),在最坏的情况下(例如所有操作都是数字),需要存储 n 个分数。

Code

class Solution {
    public int calPoints(String[] ops) {
        int n = ops.length;
        // 使用数组模拟栈来存储有效分数
        int[] nums = new int[n];
        // index 指向下一个可插入位置,也代表当前有效分数数量
        int index = 0;

        for (int i = 0; i < n; i++) {
            switch (ops[i]) {
                case "C":
                    // 使上一个分数无效,index 后退
                    // 注意:这里直接将值设为 0 也可以,但 index-- 更符合栈的移除操作
                    // 后面求和时,无效位置的值不应被计算,但因为 index 减少了,循环求和时自然会跳过
                    if (index > 0) { // 防止对空栈执行 C 操作
                       index--;
                       // 可选:nums[index] = 0; 清零非必需,因为求和只到 index
                    }
                    break;
                case "D":
                    // 将上一个分数翻倍并压入
                    if (index > 0) { // 确保有上一个分数
                       nums[index] = nums[index - 1] * 2;
                       index++;
                    }
                    break;
                case "+":
                    // 将最后两个分数的和压入
                    if (index >= 2) { // 确保有最后两个分数
                       nums[index] = nums[index - 1] + nums[index - 2];
                       index++;
                    }
                    break;
                default:
                    // 是数字,直接解析并压入
                    int val = Integer.parseInt(ops[i]);
                    nums[index++] = val;
                    break;
            }
        }

        // 计算所有有效分数的总和
        int sum = 0;
        for (int i = 0; i < index; i++) { // 只对 index 之前的有效分数求和
            sum += nums[i];
        }
        return sum;
    }
}

53. 最大子数组和(复习)

题目

题目截图

复习笔记

这次重写依然采用动态规划(或者说是在线处理)的思路。核心思想是:对于数组中的每一个元素 nums[i],以它结尾的最大子数组和要么是 nums[i] 本身(即从 nums[i] 开始一个新的子数组),要么是 nums[i] 加上以 nums[i-1] 结尾的最大子数组和(即将 nums[i] 加入到前面的子数组中)。

我们用 curSum 记录以当前元素结尾的最大子数组和。状态转移方程为:
curSum = Math.max(nums[i], curSum + nums[i])

同时,我们需要一个变量 maxSum 来记录遍历过程中遇到的所有 curSum 的最大值,这就是全局的最大子数组和。

这次复习过程中,对这个状态转移的理解和应用还算顺利,但需要确保完全内化,做到不假思索地写出。

具体解题思路回顾请看 3月23号的算法题。

复杂度

  • 时间复杂度: O ( n ) O(n) O(n),只需要一次遍历。
  • 空间复杂度: O ( 1 ) O(1) O(1),只使用了常数个额外变量。

Code

class Solution {
    public int maxSubArray(int[] nums) {
        // 初始化 curSum 和 maxSum 为第一个元素的值
        int curSum = nums[0];
        int maxSum = nums[0];
        // 从第二个元素开始遍历
        for (int i = 1; i < nums.length; i++) {
            // 决定是开启新子数组还是加入原子数组
            curSum = Math.max(nums[i], curSum + nums[i]);
            // 更新全局最大和
            maxSum = Math.max(curSum, maxSum);
        }
        return maxSum;
    }
}

1652. 拆炸弹(复习)

题目

题目截图

复习笔记

解题方法依然是滑动窗口。关键在于处理 k 的三种情况:

  1. k == 0: 结果数组所有元素都是 0。
  2. k > 0: 对于 code[i],需要求和 code[i+1]code[i+k](注意数组循环)。
  3. k < 0: 对于 code[i],需要求和 code[i-1]code[i+k](即 code[i-|k|]code[i-1], 注意数组循环)。

为了统一处理 k > 0k < 0 的情况,代码中使用了一个技巧:

  • k < 0 时,先将原数组 code 反转,然后令 a = |k|。这样问题就转换成了求解反转后数组的 “向后” a 个元素的和。
  • 计算完结果数组 ret 后,如果 k < 0,再将 ret 反转回来,得到最终答案。

这样,主要的计算逻辑就只需要处理 k > 0(或等价的 a > 0)的情况,即计算每个位置之后 a 个元素的和。这可以通过滑动窗口实现:

  • 先计算出 ret[0] 的值,即 code[1]code[a] 的和 (注意处理循环下标)。
  • 然后窗口向右滑动,对于 ret[i] (当 i > 0),其和可以通过 ret[i-1] 的和减去 code[i](移出窗口的元素)并加上 code[i+a](移入窗口的元素)得到 (注意处理循环下标)。

本次复习遇到的问题:

  • 忘记在处理 k < 0 之前,无论 k 正负,都应该先用 a 保存 k 的绝对值或原始值(根据后续逻辑决定)。代码中是在 a < 0 时才赋值,应在开始就处理 a = Math.abs(k) 或类似逻辑。(修正后的代码在判断 k<0 后设置 a = -k,并在反转数组后使用 a 作为正数窗口大小,是正确的处理方式)
  • 滑动窗口更新 sumret[left] 的逻辑细节:sum 的更新应该是 sum = sum - code[left] + code[right],并且 ret[left] 应该在 sum 更新 之后 赋值(或者在更新前赋值 ret[left-1] 的结果)。代码中的 sum += (code[right] - code[left])ret[left] = sum; left++; 逻辑是正确的,它计算的是下一个位置 left 的结果。

具体解法回顾请看 3月23号的算法题。

复杂度

  • 时间复杂度: O ( n ) O(n) O(n),滑动窗口遍历一次。如果 k < 0,两次数组反转也是 O ( n ) O(n) O(n)
  • 空间复杂度: O ( n ) O(n) O(n),需要一个额外的数组 ret 来存储结果。如果允许原地修改(题目通常不允许),可以做到 O ( 1 ) O(1) O(1)

Code

class Solution {
    public int[] decrypt(int[] code, int k) {
        int n = code.length;
        int[] ret = new int[n];

        // k == 0 的情况
        if (k == 0) {
            // 结果数组默认为 0,无需操作
            return ret;
        }

        // 为了统一处理 k > 0 和 k < 0,我们用 a 代表窗口大小 (绝对值)
        int a = Math.abs(k);
        // 如果 k < 0,先反转数组,问题变为向后求和
        if (k < 0) {
            reverse(code);
        }

        // 计算初始窗口 [1, a] 的和 (注意下标循环)
        int sum = 0;
        for (int i = 1; i <= a; i++) {
            // 使用模运算处理循环下标
            sum += code[i % n];
        }
        // 计算 ret[0]
        ret[0] = sum;

        for (int i = 1; i < n; i++) {
            sum = sum - code[i % n] + code[(i + a) % n];
            ret[i] = sum;
        }

        if (k < 0) {
            reverse(ret);

        }

        return ret;
    }

    // 辅助函数:反转数组
    private void reverse(int[] arr) {
        int left = 0, right = arr.length - 1;
        while (left < right) {
            int tmp = arr[left];
            arr[left] = arr[right];
            arr[right] = tmp;
            left++;
            right--;
        }
    }
}

相关文章:

  • 两阶段提交2PC原理
  • HCIP--5实验:综合实验
  • Unity中给Animator扩展一个异步等待播放指定clip的方法
  • QFlightInstruments飞行仪表控件库
  • 快速幂算法
  • 【Qt】文件与音视频
  • 【C++】map
  • 华为开源自研AI框架昇思MindSpore应用案例:基于MindSpore框架实现PWCNet光流估计
  • 梓航建站CMS独立版最新v1.9.4全插件PC+H5
  • 表格结构数据
  • 3.29:数据结构-绪论线性表-上
  • 类与对象(下)
  • 智能指针/内存泄露/类型转换
  • BeanDefinition和Beanfactory实现一个简单的bean容器
  • Prompt Flow 与 DSPy:大型语言模型开发的未来
  • 【论文阅读】LongDiff:Training-Free Long Video Generation in One Go
  • 全流程AI论文辅助系统开发实战:从构思到文献增值的智能进化
  • 测试开发-定制化测试数据生成(Python+jmeter+Faker)
  • 动态规划(DP)
  • 聚类(Clustering)基础知识2