【华为OD】完美走位
文章目录
- 【华为OD】完美走位
- 题目描述
- 输入
- 输出
- 备注
- 示例
- 示例一
- 示例二
- 示例三
- 示例四
- 解题思路
- 解法一:滑动窗口
- 思路
- Java实现
- Python实现
- C++实现
- 解法二:前缀和 + 哈希表
- 思路
- Java实现
- Python实现
- C++实现
- 复杂度分析
- 解法一(滑动窗口)
- 解法二(前缀和 + 哈希表)
- 总结
【华为OD】完美走位
题目描述
在第一人称射击游戏中,玩家通过键盘的 A、S、D、W 四个按键控制游戏人物分别向左、向后、向右、向前进行移动,从而完成走位。
假设玩家每按动一次键盘,游戏人物会向某个方向移动一步,如果玩家在操作一定次数的键盘并且各个方向的步数相同时,此时游戏人物必定会回到原点,则称此次走位为完美走位。
现给定玩家的走位(例如:ASDA),请通过更换其中一段连续走位的方式使得原走位能够变成一个完美走位。其中待更换的连续走位可以是相同长度的任何走位。
请返回待更换的连续走位的最小可能长度。如果原走位本身是一个完美走位,则返回 0。
输入
输入为由键盘字母表示的走位s,例如:ASDA
输出
输出为待更换的连续走位的最小可能长度
备注
- 走位长度 1 ≤ s.length ≤ 10^5
- s.length 是 4 的倍数
- s 中只含有 A、S、D、W 四种字符
示例
示例一
输入:
ASDW
输出:
0
说明: 已经是完美走位了。
示例二
输入:
AASW
输出:
1
说明: 需要把一个 A 更换成 D,这样可以得到 ADSW 或者 DASW。
示例三
输入:
AAAA
输出:
3
说明: 可以替换后 3 个 A,得到 ASDW。
示例四
输入:
AAAAADDD
输出:
4
解题思路
本题的关键在于将原问题转化为覆盖子串的问题。
题目有两个重要条件:
- 完美走位字符串是指字符串中 A、S、D、W 四种字符出现次数相等的字符串
- s.length 是 4 的倍数
对于长度为 len(s) 的原字符串 s 来说,为了使其转变为一个完美走位字符串,其中 A、S、D、W 四种字符出现次数应该均为 num = len(s) // 4
。
原字符串 s 中各个字符出现的次数可以用哈希表进行统计,对于出现次数多于 num = len(s) // 4
的字符 ch,应该修改 cnt_s[ch] - len(s) // 4
个字符为其他出现次数少于 num = len(s) // 4
的字符,才能够使得 s 变为一个完美走位字符串。
以示例四为例,s = “AAAAADDD”,字符"A"出现的次数为5,字符"D"出现的次数为3,而 num = len(s) // 4 = 2
,需要修改3个"A"和1个"D"为剩余两种字符,才能使得s变为完美走位字符串。故我们需要找到包含3个"A"和1个"D"的最小子串。
因此这个问题就转变为了,找到覆盖 cnt_s[ch] - len(s) // 4
个字符 ch(ch 满足条件 cnt_s[ch] > len(s) // 4
)的最短子串。
解法一:滑动窗口
思路
使用滑动窗口来找到包含所有需要替换字符的最短子串。
Java实现
import java.util.*;public class Solution {public int minReplaceLength(String s) {int n = s.length();if (n % 4 != 0) return -1;int target = n / 4;Map<Character, Integer> count = new HashMap<>();// 统计每个字符的出现次数for (char c : s.toCharArray()) {count.put(c, count.getOrDefault(c, 0) + 1);}// 找出需要减少的字符及其数量Map<Character, Integer> need = new HashMap<>();for (Map.Entry<Character, Integer> entry : count.entrySet()) {if (entry.getValue() > target) {need.put(entry.getKey(), entry.getValue() - target);}}// 如果已经是完美走位if (need.isEmpty()) return 0;// 滑动窗口int left = 0, right = 0;int minLen = n;Map<Character, Integer> window = new HashMap<>();while (right < n) {char c = s.charAt(right);window.put(c, window.getOrDefault(c, 0) + 1);right++;// 检查是否满足条件while (isValid(window, need)) {minLen = Math.min(minLen, right - left);char leftChar = s.charAt(left);window.put(leftChar, window.get(leftChar) - 1);if (window.get(leftChar) == 0) {window.remove(leftChar);}left++;}}return minLen;}private boolean isValid(Map<Character, Integer> window, Map<Character, Integer> need) {for (Map.Entry<Character, Integer> entry : need.entrySet()) {if (window.getOrDefault(entry.getKey(), 0) < entry.getValue()) {return false;}}return true;}
}
Python实现
from collections import Counter, defaultdictdef min_replace_length(s):n = len(s)if n % 4 != 0:return -1target = n // 4count = Counter(s)# 找出需要减少的字符及其数量need = {}for char, cnt in count.items():if cnt > target:need[char] = cnt - target# 如果已经是完美走位if not need:return 0# 滑动窗口left = 0min_len = nwindow = defaultdict(int)for right in range(n):window[s[right]] += 1# 检查是否满足条件while all(window[char] >= cnt for char, cnt in need.items()):min_len = min(min_len, right - left + 1)window[s[left]] -= 1if window[s[left]] == 0:del window[s[left]]left += 1return min_len
C++实现
#include <string>
#include <unordered_map>
#include <algorithm>
using namespace std;class Solution {
public:int minReplaceLength(string s) {int n = s.length();if (n % 4 != 0) return -1;int target = n / 4;unordered_map<char, int> count;// 统计每个字符的出现次数for (char c : s) {count[c]++;}// 找出需要减少的字符及其数量unordered_map<char, int> need;for (auto& p : count) {if (p.second > target) {need[p.first] = p.second - target;}}// 如果已经是完美走位if (need.empty()) return 0;// 滑动窗口int left = 0, right = 0;int minLen = n;unordered_map<char, int> window;while (right < n) {window[s[right]]++;right++;// 检查是否满足条件while (isValid(window, need)) {minLen = min(minLen, right - left);window[s[left]]--;if (window[s[left]] == 0) {window.erase(s[left]);}left++;}}return minLen;}private:bool isValid(const unordered_map<char, int>& window, const unordered_map<char, int>& need) {for (auto& p : need) {if (window.count(p.first) == 0 || window.at(p.first) < p.second) {return false;}}return true;}
};
解法二:前缀和 + 哈希表
思路
通过前缀和记录每个位置各字符的累计出现次数,然后枚举所有可能的子串长度,使用哈希表快速判断是否存在满足条件的子串。
Java实现
import java.util.*;public class Solution {public int minReplaceLength(String s) {int n = s.length();if (n % 4 != 0) return -1;int target = n / 4;Map<Character, Integer> count = new HashMap<>();// 统计每个字符的出现次数for (char c : s.toCharArray()) {count.put(c, count.getOrDefault(c, 0) + 1);}// 找出需要减少的字符及其数量Map<Character, Integer> need = new HashMap<>();for (Map.Entry<Character, Integer> entry : count.entrySet()) {if (entry.getValue() > target) {need.put(entry.getKey(), entry.getValue() - target);}}// 如果已经是完美走位if (need.isEmpty()) return 0;// 前缀和Map<Character, int[]> prefix = new HashMap<>();for (char c : "ASDW".toCharArray()) {prefix.put(c, new int[n + 1]);}for (int i = 0; i < n; i++) {char c = s.charAt(i);for (char ch : "ASDW".toCharArray()) {prefix.get(ch)[i + 1] = prefix.get(ch)[i] + (ch == c ? 1 : 0);}}// 枚举子串长度for (int len = 1; len <= n; len++) {for (int i = 0; i <= n - len; i++) {boolean valid = true;for (Map.Entry<Character, Integer> entry : need.entrySet()) {char c = entry.getKey();int needCount = entry.getValue();int actualCount = prefix.get(c)[i + len] - prefix.get(c)[i];if (actualCount < needCount) {valid = false;break;}}if (valid) return len;}}return n;}
}
Python实现
from collections import Counterdef min_replace_length(s):n = len(s)if n % 4 != 0:return -1target = n // 4count = Counter(s)# 找出需要减少的字符及其数量need = {}for char, cnt in count.items():if cnt > target:need[char] = cnt - target# 如果已经是完美走位if not need:return 0# 前缀和prefix = {c: [0] * (n + 1) for c in 'ASDW'}for i in range(n):c = s[i]for ch in 'ASDW':prefix[ch][i + 1] = prefix[ch][i] + (1 if ch == c else 0)# 枚举子串长度for length in range(1, n + 1):for i in range(n - length + 1):valid = Truefor char, need_count in need.items():actual_count = prefix[char][i + length] - prefix[char][i]if actual_count < need_count:valid = Falsebreakif valid:return lengthreturn n
C++实现
#include <string>
#include <unordered_map>
#include <vector>
using namespace std;class Solution {
public:int minReplaceLength(string s) {int n = s.length();if (n % 4 != 0) return -1;int target = n / 4;unordered_map<char, int> count;// 统计每个字符的出现次数for (char c : s) {count[c]++;}// 找出需要减少的字符及其数量unordered_map<char, int> need;for (auto& p : count) {if (p.second > target) {need[p.first] = p.second - target;}}// 如果已经是完美走位if (need.empty()) return 0;// 前缀和unordered_map<char, vector<int>> prefix;string chars = "ASDW";for (char c : chars) {prefix[c] = vector<int>(n + 1, 0);}for (int i = 0; i < n; i++) {char c = s[i];for (char ch : chars) {prefix[ch][i + 1] = prefix[ch][i] + (ch == c ? 1 : 0);}}// 枚举子串长度for (int len = 1; len <= n; len++) {for (int i = 0; i <= n - len; i++) {bool valid = true;for (auto& p : need) {char c = p.first;int needCount = p.second;int actualCount = prefix[c][i + len] - prefix[c][i];if (actualCount < needCount) {valid = false;break;}}if (valid) return len;}}return n;}
};
复杂度分析
解法一(滑动窗口)
- 时间复杂度: O(n),其中 n 是字符串长度
- 空间复杂度: O(1),只使用了常数个哈希表
解法二(前缀和 + 哈希表)
- 时间复杂度: O(n²),需要枚举所有可能的子串
- 空间复杂度: O(n),需要存储前缀和数组
总结
这道题的核心思想是将"完美走位"问题转化为"最短覆盖子串"问题。滑动窗口解法更加高效,时间复杂度为O(n);而前缀和解法思路更直观,但时间复杂度较高。在实际应用中,推荐使用滑动窗口解法。