子数组和的最大值问题
在使用 MarsCode AI 的刷题功能时,我遇到了一道颇具挑战性的题目:给定一个长度为 n 的数组,删除其中一个元素后,从新构成的数组中选取长度为 k 的子数组,求该子数组和的最大值。这道题目看似简单,但深入分析后发现,需要综合运用滑动窗口和双端队列等多种技巧。
初始尝试:
起初,我认为直接删除数组中的最小值,然后利用滑动窗口计算长度为 k 的子数组和即可解决问题。然而,运行结果却不尽如人意,答案完全错误。这让我意识到,删除最小值并不一定能保证形成最大子数组和。
第一次优化:
我尝试直接删除数组中的最小值,然后使用滑动窗口计算长度为 k 的子数组和。代码如下:
public static int solution(int n, int k, int[] nums) {
List<Integer> a = new ArrayList<>();
int minn = Integer.MAX_VALUE;
int minIndex = -1;
for (int i = 0; i < nums.length; i++) {
a.add(nums[i]);
if (nums[i] < minn) {
minn = nums[i];
minIndex = i; // 记录最小元素的索引
}
}
// 删除数组中最小的元素
a.remove(minIndex); // 使用索引删除元素
int result = Integer.MIN_VALUE;
// 使用滑动窗口计算长度为 k 的子数组和
for (int i = 0; i <= a.size() - k; i++) {
int sum = 0;
for (int j = i; j < i + k; j++) {
sum += a.get(j);
}
result = Math.max(result, sum);
}
return result;
}
一发直接入土,答案错误!!!
然而,结果仍然错误。显然,这种思路并不正确。
题目要求的是求最小子数组和。
第二次优化:
在滑动窗口过程中,尝试去除最小值,即将 k 变为 k+1,然后删除其中的最小值。代码如下:
public static int solution(int n, int k, int[] nums) {
List<Integer> a = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
a.add(nums[i]);
}
List<Integer> b = new ArrayList<>();
int result = Integer.MIN_VALUE;
// 使用滑动窗口计算长度为 k+1 的子数组和,并去除其中的最小值
for (int i = 0; i <= a.size() - k - 1; i++) {
int sum = 0;
int minn = Integer.MAX_VALUE;
b.clear();
for (int j = i; j <= i + k && j < a.size(); j++) {
sum += a.get(j);
minn = Math.min(minn, a.get(j));
b.add(a.get(j));
}
result = Math.max(result, sum - minn);
}
return result;
}
然而,当 n == k 时,循环不会执行,因此需要单独计算整个数组的总和。
AC代码:
public static int solution(int n, int k, int[] nums) {
// 补充当 n == k 时
if(k==n){
int sum = 0;
for(int i = 0;i<nums.length;i++){
sum+=nums[i];
}
return sum;
}
List<Integer> a = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
a.add(nums[i]);
}
List<Integer> b = new ArrayList<>();
int result = Integer.MIN_VALUE;
// 使用滑动窗口计算长度为 k+1 的子数组和,并去除其中的最小值
for (int i = 0; i <= a.size() - k - 1; i++) {
int sum = 0;
int minn = Integer.MAX_VALUE;
b.clear();
for (int j = i; j <= i + k && j < a.size(); j++) {
sum += a.get(j);
minn = Math.min(minn, a.get(j));
b.add(a.get(j));
}
result = Math.max(result, sum - minn);
}
return result;
}
引入双端队列的优化:
关键在于如何处理删除一个元素后的情况。MarsCode AI 建议利用双端队列高效维护当前窗口的最小值索引,从而在每次滑动窗口时快速计算删除最小值后的结果。初次接触双端队列的我感到有些吃力,但在 AI 的解释和同学们的讨论下,我逐渐掌握了其原理。
要找到长度为 k 的子数组和的最大值,可能有两种情况:
- 不删除元素,直接在原数组中寻找长度为 k 的子数组的最大和。
- 删除一个元素,然后在剩下的数组中寻找长度为 k 的子数组的最大和。
由于删除一个元素相当于从长度为 k+1 的子数组中删除一个元素得到长度为 k 的子数组,因此我们可以考虑所有长度为 k+1 的子数组,删除其中的一个元素(最佳情况下是删除最小的那个元素),来计算可能的最大和。
具体实现如下:
Deque<Integer> minQueue = new LinkedList<>();
int sumK1 = 0;
int maxSumK1MinusMin = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
sumK1 += nums[i];
// 维护最小值的单调队列
while (!minQueue.isEmpty() && nums[i] <= nums[minQueue.peekLast()]) {
minQueue.pollLast();
}
minQueue.offerLast(i);
if (i >= k + 1) {
sumK1 -= nums[i - (k + 1)];
if (minQueue.peekFirst() <= i - (k + 1)) {
minQueue.pollFirst();
}
}
if (i >= k) {
int currentSum = sumK1 - nums[minQueue.peekFirst()];
maxSumK1MinusMin = Math.max(maxSumK1MinusMin, currentSum);
}
}
这个优化不仅高效,还优雅地解决了删除元素后计算最大子数组和的问题。
收获与感悟:
通过这次刷题的经历,我不仅学会了如何解决“子数组和的最大值”这一具体问题,更掌握了一些重要的算法技巧,比如:
- 滑动窗口:降低计算复杂度的利器。
- 双端队列:动态维护窗口内的最小值或最大值。
更重要的是,这次刷题让我深刻体会到团队合作的力量。同学们一起讨论、竞争、优化的过程,不仅让我快速找到问题的突破口,也让我感受到学习算法的乐趣。
感谢 MarsCode AI 的强大功能和智能点拨,它在关键时刻总能给出清晰的建议,让我在刷题的道路上越走越顺畅!
最少字符串操作次数
问题描述
小U得到一个只包含小写字母的字符串 S。她可以执行如下操作:每次选择字符串中两个相同的字符删除,然后在字符串末尾添加一个任意的小写字母。小U想知道,最少需要多少次操作才能使得字符串中的所有字母都不相同?
测试样例
样例1:
输入:S = "abab"
输出:2
样例2:
输入:S = "aaaa"
输出:2
样例3:
输入:S = "abcabc"
输出:3
题目理解
小 U 拿到一个仅由小写字母组成的字符串 ( S )。她可以进行以下操作任意次:
- 选择字符串中任意两个相同的字符,删除它们。
- 在字符串的末尾添加一个任意的小写字母。
目标是通过最少的操作次数,使得字符串中所有的字母都不相同,即字符串中不再有重复的字母。
题目分析
要解决这个问题,我们需要找到使字符串中所有字母都唯一的最小操作次数。为了达到这个目标,我们需要消除所有的重复字符。
首先,我们需要统计每个字符在字符串中出现的次数。对于每个出现次数大于 1 的字符,我们需要消除多余的重复,使得每个字符只剩下一个。
操作策略
- 配对删除:对于每个重复的字符,两个一组进行删除操作。这意味着,对于每个字符,其出现次数除以 2 的整数部分就是需要执行的删除操作次数。
- 添加字符:每次删除操作后,我们需要在字符串末尾添加一个字符。为了避免再次引入重复,我们应尽可能添加未在字符串中出现过的字符。
注意事项
- 字母表限制:因为小写字母只有 26 个,所以最终字符串的长度最多为 26。如果删除操作后,字符串的长度超过了 26,我们需要额外的操作来删除多余的字符对,确保最终的字符数不超过 26。
算法步骤
- 统计字符出现次数:遍历字符串 ( S ),使用哈希表记录每个字符出现的次数。
- 计算删除操作次数:
-
- 对于每个字符,计算其需要执行的删除操作次数,即出现次数除以 2 的整数部分。
- 累加所有字符的删除操作次数,得到总的删除操作次数 sum1。
- 计算剩余未成对的字符数:
-
- 对于每个字符,计算其未成对的数量,即出现次数对 2 取模的结果。
- 累加所有字符的未成对数量,得到总的未成对字符数 sum2。
- 计算总字符数:
-
- 总字符数为未成对字符数加上删除操作次数(因为每次删除操作都会在末尾添加一个字符),即 ( totalChars=sum2+ sum1。
- 判断是否超过字母表限制:
-
- 如果 totalChars <= 26 ,则最小操作次数为 sum1。
- 如果 totalChars > 26 ,则需要额外的删除操作,最小操作次数为 sum1+ totalChars- 26。
举例说明
样例 1:
- 输入:( S = "abab" )
- 字符统计:'a' 出现 2 次,'b' 出现 2 次。
- 删除操作次数:'a' 需要 1 次,'b' 需要 1 次,总计 2 次。
- 未成对字符数:'a' 0 个,'b' 0 个。
- 总字符数:( 0 + 2 = 2 < 26 ),故最小操作次数为 2。
样例 2:
- 输入:( S = "aaaa" )
- 字符统计:'a' 出现 4 次。
- 删除操作次数:'a' 需要 2 次,总计 2 次。
- 未成对字符数:'a' 0 个。
- 总字符数:( 0 + 2 = 2 < 26 ),故最小操作次数为 2。
样例 3:
- 输入:( S = "abcabc" )
- 字符统计:'a'、'b'、'c' 各出现 2 次。
- 删除操作次数:每个字符需要 1 次,总计 3 次。
- 未成对字符数:每个字符 0 个。
- 总字符数:( 0 + 3 = 3 < 26 ),故最小操作次数为 3。
完整代码解析
import java.util.HashMap;
import java.util.Map;
public class Main {
public static int solution(String S) {
// 创建一个哈希表来统计每个字符出现的次数
Map<Character, Integer> cishu = new HashMap<>();
for (char c : S.toCharArray()) {
cishu.put(c, cishu.getOrDefault(c, 0) + 1);
}
int sum1 = 0; // 统计删除操作次数(每次删除两个相同的字符)
int sum2 = 0; // 统计未成对的字符数量(剩余的单个字符)
for (int count : cishu.values()) {
sum1 += count / 2; // 计算需要的删除操作次数
sum2 += count % 2; // 计算未成对的字符数量
}
// 计算删除操作后字符串的总字符数
int totalChars = sum2 + sum1;
if (totalChars <= 26) {
// 如果总字符数不超过 26,返回删除操作次数
return sum1;
} else {
// 如果总字符数超过 26,需要额外的删除操作
return sum1 + (totalChars - 26);
}
}
public static void main(String[] args) {
System.out.println(solution("aaabbbccceeedfgihjklmnopqrstuvw")); // 输出操作次数
System.out.println(solution("aaabcdefghijklmnopqrstuvwxyz"));
System.out.println(solution("abcabc"));
}
}
复杂度分析
- 时间复杂度:( O(n) ),其中 ( n ) 是字符串的长度。需要遍历字符串统计字符出现次数。
- 空间复杂度:( O(1) ),因为哈希表最多存储 26 个字母的统计信息。
结论
通过上述算法,我们可以高效地计算出使字符串中所有字母都唯一所需的最小操作次数。关键在于正确地统计需要删除的字符对数和处理字母表的限制。