阅读视图

发现新文章,点击刷新页面。

每日一题-替换数组中的非互质数🔴

给你一个整数数组 nums 。请你对数组执行下述操作:

  1. nums 中找出 任意 两个 相邻非互质 数。
  2. 如果不存在这样的数,终止 这一过程。
  3. 否则,删除这两个数,并 替换 为它们的 最小公倍数(Least Common Multiple,LCM)。
  4. 只要还能找出两个相邻的非互质数就继续 重复 这一过程。

返回修改后得到的 最终 数组。可以证明的是,以 任意 顺序替换相邻的非互质数都可以得到相同的结果。

生成的测试用例可以保证最终数组中的值 小于或者等于 108

两个数字 xy 满足 非互质数 的条件是:GCD(x, y) > 1 ,其中 GCD(x, y)xy最大公约数

 

示例 1 :

输入:nums = [6,4,3,2,7,6,2]
输出:[12,7,6]
解释:
- (6, 4) 是一组非互质数,且 LCM(6, 4) = 12 。得到 nums = [12,3,2,7,6,2] 。
- (12, 3) 是一组非互质数,且 LCM(12, 3) = 12 。得到 nums = [12,2,7,6,2] 。
- (12, 2) 是一组非互质数,且 LCM(12, 2) = 12 。得到 nums = [12,7,6,2] 。
- (6, 2) 是一组非互质数,且 LCM(6, 2) = 6 。得到 nums = [12,7,6] 。
现在,nums 中不存在相邻的非互质数。
因此,修改后得到的最终数组是 [12,7,6] 。
注意,存在其他方法可以获得相同的最终数组。

示例 2 :

输入:nums = [2,2,1,1,3,3,3]
输出:[2,1,1,3]
解释:
- (3, 3) 是一组非互质数,且 LCM(3, 3) = 3 。得到 nums = [2,2,1,1,3,3] 。
- (3, 3) 是一组非互质数,且 LCM(3, 3) = 3 。得到 nums = [2,2,1,1,3] 。
- (2, 2) 是一组非互质数,且 LCM(2, 2) = 2 。得到 nums = [2,1,1,3] 。
现在,nums 中不存在相邻的非互质数。 
因此,修改后得到的最终数组是 [2,1,1,3] 。 
注意,存在其他方法可以获得相同的最终数组。

 

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 105
  • 生成的测试用例可以保证最终数组中的值 小于或者等于 108

替换数组中的非互质数

方法一:栈

思路与算法

由于题目不加证明地给出了「任意顺序替换相邻的非互质数都可以得到相同的结果」,因此我们可以从前往后进行替换。

我们可以使用一个栈来进行替换操作。具体地,我们对数组 $\textit{nums}$ 进行一次遍历。当遍历到 $\textit{nums}[i]$ 时。在其被放入栈顶前,我们重复进行替换操作,直到 $\textit{nums}[i]$ 和栈顶的元素互质,或者栈为空为止。此时,我们再将替换完成的 $\textit{nums}[i]$ 放入栈顶。

最终栈底到栈顶的元素序列即为答案。

代码

###C++

class Solution {
public:
    vector<int> replaceNonCoprimes(vector<int>& nums) {
        vector<int> ans;
        for (int num: nums) {
            while (!ans.empty()) {
                int g = gcd(ans.back(), num);
                if (g > 1) {
                    num = ans.back() / g * num;
                    ans.pop_back();
                }
                else {
                    break;
                }
            }
            ans.push_back(num);
        }
        return ans;
    }
};

###Python

class Solution:
    def replaceNonCoprimes(self, nums: List[int]) -> List[int]:
        ans = list()
        for num in nums:
            while ans:
                g = math.gcd(ans[-1], num)
                if g > 1:
                    num = ans[-1] // g * num
                    ans.pop()
                else:
                    break
            ans.append(num)
        
        return ans

###Java

class Solution {
    public List<Integer> replaceNonCoprimes(int[] nums) {
        List<Integer> ans = new ArrayList<>();
        for (int num : nums) {
            while (!ans.isEmpty()) {
                int last = ans.get(ans.size() - 1);
                int g = gcd(last, num);
                if (g > 1) {
                    num = last / g * num;
                    ans.remove(ans.size() - 1);
                } else {
                    break;
                }
            }
            ans.add(num);
        }
        return ans;
    }

    private int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }
}

###C#

public class Solution {
    public IList<int> ReplaceNonCoprimes(int[] nums) {
        List<int> ans = new List<int>();
        for(int i = 0; i < nums.Length; i++) {
            int num = nums[i];
            while (ans.Count > 0) {
                int last = ans[ans.Count - 1];
                int g = GCD(last, num);
                if (g > 1) {
                    num = last / g * num;
                    ans.RemoveAt(ans.Count - 1);
                } else {
                    break;
                }
            }
            ans.Add(num);
        }
        return ans;
    }

    private int GCD(int a, int b) {
        return b == 0 ? a : GCD(b, a % b);
    }
}

###Go

func replaceNonCoprimes(nums []int) []int {
    ans := []int{}
    for _, num := range nums {
        for len(ans) > 0 {
            last := ans[len(ans) - 1]
            g := gcd(last, num)
            if g > 1 {
                num = last / g * num
                ans = ans[:len(ans) - 1]
            } else {
                break
            }
        }
        ans = append(ans, num)
    }
    return ans
}

func gcd(a, b int) int {
    for b != 0 {
        a, b = b, a % b
    }
    return a
}

###C

int gcd(int a, int b) {
    return b == 0 ? a : gcd(b, a % b);
}

int* replaceNonCoprimes(int* nums, int numsSize, int* returnSize) {
    int* ans = (int*)malloc(numsSize * sizeof(int));
    int pos = 0;
    for (int i = 0; i < numsSize; i++) {
        int num = nums[i];
        while (pos > 0) {
            int last = ans[pos - 1];
            int g = gcd(last, num);
            if (g > 1) {
                num = last / g * num;
                pos--;
            } else {
                break;
            }
        }
        ans[pos++] = num;
    }
    *returnSize = pos;
    return ans;
}

###JavaScript

var replaceNonCoprimes = function(nums) {
    const ans = [];
    for (let num of nums) {
        while (ans.length > 0) {
            const last = ans[ans.length - 1];
            const g = gcd(last, num);
            if (g > 1) {
                num = Math.floor(last / g) * num;
                ans.pop();
            } else {
                break;
            }
        }
        ans.push(num);
    }
    return ans;
};

const gcd = (a, b) => {
    return b === 0 ? a : gcd(b, a % b);
}

###TypeScript

function replaceNonCoprimes(nums: number[]): number[] {
    const ans: number[] = [];
    for (let num of nums) {
        while (ans.length > 0) {
            const last = ans[ans.length - 1];
            const g = gcd(last, num);
            if (g > 1) {
                num = Math.floor(last / g) * num;
                ans.pop();
            } else {
                break;
            }
        }
        ans.push(num);
    }
    return ans;
};

function gcd(a: number, b: number): number {
    return b === 0 ? a : gcd(b, a % b);
}

###Rust

impl Solution {
    pub fn replace_non_coprimes(nums: Vec<i32>) -> Vec<i32> {
        let mut ans = Vec::new();
        for mut num in nums {
            while let Some(&last) = ans.last() {
                let g = Self::gcd(last, num);
                if g > 1 {
                    num = last / g * num;
                    ans.pop();
                } else {
                    break;
                }
            }
            ans.push(num);
        }
        ans
    }

    fn gcd(a: i32, b: i32) -> i32 {
        if b == 0 { a } else { Self::gcd(b, a % b) }
    }
}

复杂度分析

  • 时间复杂度:$O(n \log C)$,其中 $n$ 是数组 $\textit{nums}$ 的长度,$C$ 是数组 $\textit{nums}$ 中的数据范围,$O(\log C)$ 即为单次计算最大公约数需要的时间。

  • 空间复杂度:$O(1)$。这里不计入返回值需要使用的空间。

2197.替换数组中的非互质数 用栈模拟

思想
周赛T4 使用一个栈来模拟过程:每次数组当前元素进栈后,维持该循环:取出栈顶两个元素,只要两数非互质就按最小公倍数合并后入栈;假如栈中元素不够或两数互质,则弹出的元素再入栈并停止循环,继续遍历下一个数组元素。最后返回栈中元素(倒序)
代码(C++)

###cpp

class Solution {
public:
    vector<int> replaceNonCoprimes(vector<int>& nums) {
        stack<long long> st;
        st.push( nums[0] );
        int n = nums.size();
        for (int i = 1; i < n; ++i) {
            st.push(nums[i]);
            while (1) {
                long long first = st.top();   st.pop();
                long long second;
                if ( !st.empty() ) second = st.top();
                else {                                      // 栈中元素不够,退出循环
                    st.push(first);
                    break;
                }
                if ( gcd(first, second) > 1 ) {
                    st.pop();
                    st.push( lcm(first, second) );
                } else {                                    // 两数互质,退出循环
                    st.push( first );
                    break;
                }
            }
            
        }
        
        vector<int> ans;
        while ( !st.empty() ) {
            ans.emplace_back(st.top());
            st.pop();
        }
        return vector<int>(ans.rbegin(), ans.rend());   // 倒序
    }
    
    long long gcd(long long a, long long b) {   // 最大公约数
        return b == 0 ? a : gcd(b, a % b);
    }
    
    long long lcm(long long a, long long b) {   // 最小共倍数
        return a * b / gcd(a, b);
    }
};

时间复杂度:O(nlogC)
空间复杂度:O(n)

邻项消除问题的通用做法(Python/Java/C++/C/Go/JS/Rust)

如果有三个相邻且可以合并的数 $x,y,z$,那么先合并 $x$ 和 $y$ 再合并 $z$,还是先合并 $y$ 和 $z$ 再合并 $x$,结果是一样的吗?

是一样的,由于 $\text{LCM}$ 本质是质因数分解中质数的指数取最大值(见初等数论教材),对于 $\max(a,b,c)$ 来说,我们有

$$
\max(a,b,c) = \max(\max(a,b),c) = \max(a,\max(b,c))
$$

所以 $x,y,z$ 先合并 $x,y$ 还是先合并 $y,z$ 都可以。得到的结果均为 $\text{LCM}(x,y,z)$。

该结论可以推广到更多元素的情况。

据此,对于任意一种合并顺序,我们总是可以将该顺序重排成:

  • 优先选择最左边的能合并的相邻元素,将其合并。

这可以用栈模拟:

  1. 创建一个空栈。
  2. 从左到右遍历 $\textit{nums}$。
  3. 设 $x = \textit{nums}[i]$。如果栈不为空且 $x$ 与栈顶不互质,那么弹出栈顶 $y$,更新 $x$ 为 $\text{LCM}(x,y)$。循环直到栈为空或者 $x$ 与栈顶互质。
  4. 把 $x$ 入栈。
  5. 遍历结束,栈即为答案。
class Solution:
    def replaceNonCoprimes(self, nums: List[int]) -> List[int]:
        st = []
        for x in nums:
            while st and gcd(x, st[-1]) > 1:
                x = lcm(x, st.pop())
            st.append(x)
        return st
class Solution {
    public List<Integer> replaceNonCoprimes(int[] nums) {
        List<Integer> st = new ArrayList<>();
        for (int x : nums) {
            while (!st.isEmpty() && gcd(x, st.getLast()) > 1) {
                x = lcm(x, st.removeLast());
            }
            st.add(x);
        }
        return st;
    }

    private int gcd(int a, int b) {
        while (a != 0) {
            int tmp = a;
            a = b % a;
            b = tmp;
        }
        return b;
    }

    // 先除后乘,避免溢出
    private int lcm(int a, int b) {
        return a / gcd(a, b) * b;
    }
}
class Solution {
public:
    vector<int> replaceNonCoprimes(vector<int>& nums) {
        vector<int> st;
        for (int x : nums) {
            while (!st.empty() && gcd(x, st.back()) > 1) {
                x = lcm(x, st.back());
                st.pop_back();
            }
            st.push_back(x);
        }
        return st;
    }
};
int gcd(int a, int b) {
    while (a != 0) {
        int tmp = a;
        a = b % a;
        b = tmp;
    }
    return b;
}

// 先除后乘,避免溢出
int lcm(int a, int b) {
    return a / gcd(a, b) * b;
}

int* replaceNonCoprimes(int* nums, int numsSize, int* returnSize) {
    int* stack = nums; // 把 nums 当作栈用
    int top = -1; // 栈顶下标
    for (int i = 0; i < numsSize; i++) {
        int x = nums[i];
        while (top >= 0 && gcd(x, stack[top]) > 1) {
            x = lcm(x, stack[top--]); // 出栈
        }
        stack[++top] = x; // 入栈
    }
    *returnSize = top + 1;
    return stack;
}
func replaceNonCoprimes(nums []int) []int {
st := nums[:0] // 把 nums 当作栈用
for _, x := range nums {
for len(st) > 0 && gcd(x, st[len(st)-1]) > 1 {
x = lcm(x, st[len(st)-1])
st = st[:len(st)-1]
}
st = append(st, x)
}
return st
}

func gcd(a, b int) int {
for a != 0 {
a, b = b%a, a
}
return b
}

func lcm(a, b int) int {
return a / gcd(a, b) * b
}
var replaceNonCoprimes = function(nums) {
    const st = [];
    for (let x of nums) {
        while (st.length > 0 && gcd(x, st[st.length - 1]) > 1) {
            x = lcm(x, st.pop());
        }
        st.push(x);
    }
    return st;
};

function gcd(a, b) {
    while (a !== 0) {
        const tmp = a;
        a = b % a;
        b = tmp;
    }
    return b;
};

function lcm(a, b) {
    return a / gcd(a, b) * b;
};
impl Solution {
    pub fn replace_non_coprimes(nums: Vec<i32>) -> Vec<i32> {
        fn gcd(mut a: i32, mut b: i32) -> i32 {
            while a != 0 {
                let tmp = a;
                a = b % a;
                b = tmp;
            }
            b
        }
        fn lcm(a: i32, b: i32) -> i32 {
            a / gcd(a, b) * b
        }

        let mut st = vec![];
        for mut x in nums {
            while !st.is_empty() && gcd(x, *st.last().unwrap()) > 1 {
                x = lcm(x, st.pop().unwrap());
            }
            st.push(x);
        }
        st
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log U)$。其中 $n$ 是 $\textit{nums}$ 的长度,题目保证 $U\le 10^8$。由于每个元素至多入栈出栈各一次,所以二重循环的总循环次数是 $\mathcal{O}(n)$ 的。
  • 空间复杂度:$\mathcal{O}(n)$ 或 $\mathcal{O}(1)$。如果把 $\textit{nums}$ 当作栈用,可以做到 $\mathcal{O}(1)$ 空间。

专题训练

见下面数据结构题单的「§3.3 邻项消除」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

每日一题-可以输入的最大单词数🟢

键盘出现了一些故障,有些字母键无法正常工作。而键盘上所有其他键都能够正常工作。

给你一个由若干单词组成的字符串 text ,单词间由单个空格组成(不含前导和尾随空格);另有一个字符串 brokenLetters ,由所有已损坏的不同字母键组成,返回你可以使用此键盘完全输入的 text 中单词的数目。

 

示例 1:

输入:text = "hello world", brokenLetters = "ad"
输出:1
解释:无法输入 "world" ,因为字母键 'd' 已损坏。

示例 2:

输入:text = "leet code", brokenLetters = "lt"
输出:1
解释:无法输入 "leet" ,因为字母键 'l' 和 't' 已损坏。

示例 3:

输入:text = "leet code", brokenLetters = "e"
输出:0
解释:无法输入任何单词,因为字母键 'e' 已损坏。

 

提示:

  • 1 <= text.length <= 104
  • 0 <= brokenLetters.length <= 26
  • text 由若干用单个空格分隔的单词组成,且不含任何前导和尾随空格
  • 每个单词仅由小写英文字母组成
  • brokenLetters互不相同 的小写英文字母组成

【track & traning】一行代码,思路简单,性能高效接近90

方便快速学习算法与理解~

🌇 点赞 👍 收藏 ⭐留言 📝 一键三连 ~关注Jam,从你我做起!

兄弟会背叛你,女人会离开你,金钱会诱惑你,生活会刁难你,只有数学不会,不会就是不会
天才与否,取决于最终达到的高度。真正的天才是那些脚踏实地的人
静下心来好好做自己,走稳脚下每一步,就是最好的路,强者都是孤独的

推荐 python 算法的书籍,体系化学习算法与数据结构,用正确的方式成为offer收割机
leetcode —— 系统化快速学习算法,这不是内卷,这只是悄悄地努力,然后惊艳所有的人
image.png


求解思路

暴力破解

代码

###python3

class Solution:
    def canBeTypedWords(self, text: str, brokenLetters: str) -> int:
        return sum(len(set(word) & set(brokenLetters)) == 0  for word in text.split())

ʚ自在飞花ɞ | 使用哈希数组空间换时间 O(n*m) -> O(n+m)

思路: 枚举,哈希

时间复杂度: $O(n+m)$,$n$ 为 text 的长度,$m$ 为故障键位的数量。

考虑text仅有一个单词的情况:

  • 初始化标记变量 flag = true
  • 遍历 text 的每个字符 c
  • c 为故障则设置 flag = false

考虑text包含多个单词的情况。单词由单个空格切分,且单词不含任何前导和尾随空格。

因此在遍历 text 的过程中,若 c 为空格或 \0,则标记一个单词的结束。此时可根据 flag 的值更新答案,并重置 flag

复杂度分析

考虑判断c是否故障的两种方法。

第一种,暴力枚举

每次判断需遍历一遍 brokenLetters,时间复杂度:$O(m)$。一共需要判断 $n$ 次,因此整体的时间复杂度为 $O(n*m)$。没有使用额外的空间,空间复杂度故为 O(1)。

第二种,哈希数组

预先遍历一遍 brokenLetters,初始化哈希数组,时间复杂度为 $O(m)$,空间复杂度为 $O(m)$。

借助哈希数组判断 c 为是否为故障的,可将单词判断的时间复杂度降至$O(1)$。

因此整体时间复杂度为$O(n+m)$:

  • $O(m)$:遍历 brokenLetters,初始化哈希数组。
  • $O(n)$:遍历 text 计算答案。

###cpp

class Solution {
public:
    int canBeTypedWords(string text, string bl) {
        bool mark[26] = {0};
        for (auto c : bl) {
            mark[c-'a'] = true;
        }
        bool flag = true;
        int cnt = 0;
        for (int i = 0; i <= text.size(); i++) {
            // 遇到了空格或 \0,表明一个单词遍历完了,
            if (text[i] == ' ' || text[i] == '\0') {
                if (flag) cnt++; // 根据 flag 更新答案
                flag = true; // 重置 flag
            } else if (mark[text[i]-'a']) {
                flag = false; // 键位坏掉了
            }
        }
        return cnt;
    }
};

两种方法:按空格分割 / 位运算优化(Python/Java/C++/Go)

把 $\textit{text}$ 按照空格分割成单词。对于每个单词,如果单词中的所有字母都不在 $\textit{brokenLetters}$ 中,那么把答案加一。

优化前

class Solution:
    def canBeTypedWords(self, text: str, brokenLetters: str) -> int:
        ans = 0
        for word in text.split():
            if all(c not in brokenLetters for c in word):
                ans += 1
        return ans
class Solution {
    public int canBeTypedWords(String text, String brokenLetters) {
        int ans = 0;
        for (String word : text.split(" ")) {
            if (!containsAny(word, brokenLetters)) {
                ans++;
            }
        }
        return ans;
    }

    private boolean containsAny(String word, String brokenLetters) {
        for (char c : word.toCharArray()) {
            if (brokenLetters.indexOf(c) != -1) {
                return true;
            }
        }
        return false;
    }
}
class Solution {
public:
    int canBeTypedWords(string text, string brokenLetters) {
        istringstream iss(text);
        string word;
        int ans = 0;
        while (iss >> word) {
            if (ranges::all_of(word, [&](char c){ return brokenLetters.find(c) == string::npos; })) {
                ans++;
            }
        }
        return ans;
    }
};
func canBeTypedWords(text, brokenLetters string) (ans int) {
for _, word := range strings.Split(text, " ") {
if !strings.ContainsAny(word, brokenLetters) {
ans++
}
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(nm)$,其中 $n$ 是 $\textit{text}$ 的长度,$m$ 是 $\textit{brokenLetters}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$ 或 $\mathcal{O}(1)$。

优化

把 $\textit{brokenLetters}$ 视作一个字母集合,我们需要判断单词中的字母是否在这个集合中。

根据 从集合论到位运算,常见位运算技巧分类总结,集合可以用二进制数表示,集合的运算可以用位运算快速计算。

class Solution:
    def canBeTypedWords(self, text: str, brokenLetters: str) -> int:
        broken_mask = 0
        for c in brokenLetters:
            broken_mask |= 1 << (ord(c) - ord('a'))  # 把 c 加到集合中

        ans = 0
        ok = True
        for c in text:
            if c == ' ':  # 上一个单词遍历完毕
                ans += ok
                ok = True
            elif broken_mask >> (ord(c) - ord('a')) & 1:  # c 在 brokenLetters 中
                ok = False
        ans += ok  # 最后一个单词
        return ans
class Solution {
    public int canBeTypedWords(String text, String brokenLetters) {
        int brokenMask = 0;
        for (char c : brokenLetters.toCharArray()) {
            brokenMask |= 1 << (c - 'a'); // 把 c 加到集合中
        }

        int ans = 0;
        int ok = 1;
        for (char c : text.toCharArray()) {
            if (c == ' ') { // 上一个单词遍历完毕
                ans += ok;
                ok = 1;
            } else if ((brokenMask >> (c - 'a') & 1) > 0) { // c 在 brokenLetters 中
                ok = 0;
            }
        }
        ans += ok; // 最后一个单词
        return ans;
    }
}
class Solution {
public:
    int canBeTypedWords(string text, string brokenLetters) {
        int broken_mask = 0;
        for (char c : brokenLetters) {
            broken_mask |= 1 << (c - 'a'); // 把 c 加到集合中
        }

        int ans = 0;
        bool ok = true;
        for (char c : text) {
            if (c == ' ') { // 上一个单词遍历完毕
                ans += ok;
                ok = true;
            } else if (broken_mask >> (c - 'a') & 1) { // c 在 brokenLetters 中
                ok = false;
            }
        }
        ans += ok; // 最后一个单词
        return ans;
    }
};
func canBeTypedWords(text, brokenLetters string) (ans int) {
brokenMask := 0
for _, c := range brokenLetters {
brokenMask |= 1 << (c - 'a') // 把 c 加到集合中
}

ok := 1
for _, c := range text {
if c == ' ' { // 上一个单词遍历完毕
ans += ok
ok = 1
} else if brokenMask>>(c-'a')&1 > 0 { // c 在 brokenLetters 中
ok = 0
}
}
ans += ok // 最后一个单词
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n + m)$,其中 $n$ 是 $\textit{text}$ 的长度,$m$ 是 $\textit{brokenLetters}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

每日一题-元音拼写检查器🟡

在给定单词列表 wordlist 的情况下,我们希望实现一个拼写检查器,将查询单词转换为正确的单词。

对于给定的查询单词 query,拼写检查器将会处理两类拼写错误:

  • 大小写:如果查询匹配单词列表中的某个单词(不区分大小写),则返回的正确单词与单词列表中的大小写相同。
    • 例如:wordlist = ["yellow"], query = "YellOw": correct = "yellow"
    • 例如:wordlist = ["Yellow"], query = "yellow": correct = "Yellow"
    • 例如:wordlist = ["yellow"], query = "yellow": correct = "yellow"
  • 元音错误:如果在将查询单词中的元音 ('a', 'e', 'i', 'o', 'u')  分别替换为任何元音后,能与单词列表中的单词匹配(不区分大小写),则返回的正确单词与单词列表中的匹配项大小写相同。
    • 例如:wordlist = ["YellOw"], query = "yollow": correct = "YellOw"
    • 例如:wordlist = ["YellOw"], query = "yeellow": correct = "" (无匹配项)
    • 例如:wordlist = ["YellOw"], query = "yllw": correct = "" (无匹配项)

此外,拼写检查器还按照以下优先级规则操作:

  • 当查询完全匹配单词列表中的某个单词(区分大小写)时,应返回相同的单词。
  • 当查询匹配到大小写问题的单词时,您应该返回单词列表中的第一个这样的匹配项。
  • 当查询匹配到元音错误的单词时,您应该返回单词列表中的第一个这样的匹配项。
  • 如果该查询在单词列表中没有匹配项,则应返回空字符串。

给出一些查询 queries,返回一个单词列表 answer,其中 answer[i] 是由查询 query = queries[i] 得到的正确单词。

 

示例 1:

输入:wordlist = ["KiTe","kite","hare","Hare"], queries = ["kite","Kite","KiTe","Hare","HARE","Hear","hear","keti","keet","keto"]
输出:["kite","KiTe","KiTe","Hare","hare","","","KiTe","","KiTe"]

示例 2:

输入:wordlist = ["yellow"], queries = ["YellOw"]
输出:["yellow"]

 

提示:

  • 1 <= wordlist.length, queries.length <= 5000
  • 1 <= wordlist[i].length, queries[i].length <= 7
  • wordlist[i] 和 queries[i] 只包含英文字母

三个哈希表(Python/Java/C++/Go/JS/Rust)

设 $s$ 是 $\textit{wordlist}$ 中的一个字符串,$q$ 是 $\textit{queries}$ 中的一个字符串。

实现完全匹配,可以把 $\textit{wordlist}$ 中的字符串保存到一个哈希集合中,直接查询 $q$ 是否在哈希集合中。

实现不区分大小写的匹配,可以创建一个哈希表 $\textit{lowerToOrigin}$,哈希表的 key 是 $s$ 的每个字母都变成小写字母后的字符串,value 是 $s$。把 $q$ 的每个字母都变成小写字母,查询是否在 $\textit{lowerToOrigin}$ 中,如果在,则为不区分大小写的匹配,匹配结果为 value 值。

实现不区分大小写+元音模糊匹配,可以创建一个哈希表 $\textit{vowelToOrigin}$,哈希表的 key 是 $s$ 的每个字母都变成小写字母,且每个元音都替换成 $\texttt{?}$ 的字符串,value 是 $s$。把 $q$ 的每个字母都变成小写字母,每个元音都替换成 $\texttt{?}$ 后,查询是否在 $\textit{vowelToOrigin}$ 中,如果在,则为不区分大小写+元音模糊匹配,匹配结果为 value 值。

注意:题目要求返回单词列表中的第一个匹配项,即最靠左的匹配项。我们可以倒着遍历 $\textit{wordlist}$,处理每个字符串。这样对于相同的 key,哈希表中的 value 就是 $\textit{wordlist}$ 中的最靠左的匹配项了。

###py

class Solution:
    def spellchecker(self, wordlist: List[str], queries: List[str]) -> List[str]:
        origin = set(wordlist)
        lower_to_origin = {}
        vowel_to_origin = {}
        trans = str.maketrans("aeiou", "?????")  # 替换元音为 '?'

        for s in reversed(wordlist):
            lower = s.lower()
            lower_to_origin[lower] = s  # 例如 kite -> KiTe
            vowel_to_origin[lower.translate(trans)] = s  # 例如 k?t? -> KiTe

        for i, q in enumerate(queries):
            if q in origin:  # 完全匹配
                continue
            lower = q.lower()
            if lower in lower_to_origin:  # 不区分大小写的匹配
                queries[i] = lower_to_origin[lower]
            else:  # 不区分大小写+元音模糊匹配
                queries[i] = vowel_to_origin.get(lower.translate(trans), "")
        return queries

###java

class Solution {
    public String[] spellchecker(String[] wordlist, String[] queries) {
        int n = wordlist.length;
        Set<String> origin = new HashSet<>(Arrays.asList(wordlist));
        Map<String, String> lowerToOrigin = new HashMap<>(n); // 预分配空间
        Map<String, String> vowelToOrigin = new HashMap<>(n);

        for (int i = n - 1; i >= 0; i--) {
            String s = wordlist[i];
            String lower = s.toLowerCase();
            lowerToOrigin.put(lower, s); // 例如 kite -> KiTe
            vowelToOrigin.put(replaceVowels(lower), s); // 例如 k?t? -> KiTe
        }

        for (int i = 0; i < queries.length; i++) {
            String q = queries[i];
            if (origin.contains(q)) { // 完全匹配
                continue;
            }
            String lower = q.toLowerCase();
            if (lowerToOrigin.containsKey(lower)) { // 不区分大小写的匹配
                queries[i] = lowerToOrigin.get(lower);
            } else { // 不区分大小写+元音模糊匹配
                queries[i] = vowelToOrigin.getOrDefault(replaceVowels(lower), "");
            }
        }
        return queries;
    }

    private String replaceVowels(String str) {
        char[] s = str.toCharArray();
        for (int i = 0; i < s.length; ++i) {
            char c = s[i];
            if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
                s[i] = '?';
            }
        }
        return new String(s);
    }
}

###cpp

class Solution {
    string tolower_string(string s) {
        for (char& c : s) {
            c = tolower(c);
        }
        return s;
    }

    string replace_vowels(string s) {
        for (char& c : s) {
            if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
                c = '?';
            }
        }
        return s;
    }

public:
    vector<string> spellchecker(vector<string>& wordlist, vector<string>& queries) {
        int n = wordlist.size();
        unordered_set<string> origin(wordlist.begin(), wordlist.end());
        unordered_map<string, string> lower_to_origin;
        unordered_map<string, string> vowel_to_origin;

        for (int i = n - 1; i >= 0; i--) {
            string& s = wordlist[i];
            string lower = tolower_string(s);
            lower_to_origin[lower] = s; // 例如 kite -> KiTe
            vowel_to_origin[replace_vowels(lower)] = s; // 例如 k?t? -> KiTe
        }

        for (string& q : queries) {
            if (origin.contains(q)) { // 完全匹配
                continue;
            }
            string lower = tolower_string(q);
            auto it = lower_to_origin.find(lower);
            if (it != lower_to_origin.end()) { // 不区分大小写的匹配
                q = it->second;
            } else { // 不区分大小写+元音模糊匹配
                auto it = vowel_to_origin.find(replace_vowels(lower));
                q = it != vowel_to_origin.end() ? it->second : "";
            }
        }
        return queries;
    }
};

###go

func spellchecker(wordlist, queries []string) []string {
n := len(wordlist)
origin := make(map[string]bool, n) // 预分配空间
lowerToOrigin := make(map[string]string, n)
vowelToOrigin := make(map[string]string, n)
// 把元音都替换成 '?'
vowelReplacer := strings.NewReplacer("a", "?", "e", "?", "i", "?", "o", "?", "u", "?")

for _, s := range slices.Backward(wordlist) {
origin[s] = true
lower := strings.ToLower(s)
lowerToOrigin[lower] = s // 例如 kite -> KiTe
vowelToOrigin[vowelReplacer.Replace(lower)] = s // 例如 k?t? -> KiTe
}

for i, q := range queries {
if origin[q] { // 完全匹配
continue
}
lower := strings.ToLower(q)
if s, ok := lowerToOrigin[lower]; ok { // 不区分大小写的匹配
queries[i] = s
} else { // 不区分大小写+元音模糊匹配
queries[i] = vowelToOrigin[vowelReplacer.Replace(lower)]
}
}

return queries
}

###js

var spellchecker = function(wordlist, queries) {
    const origin = new Set(wordlist);
    const lowerToOrigin = new Map();
    const vowelToOrigin = new Map();

    for (let i = wordlist.length - 1; i >= 0; i--) {
        const s = wordlist[i];
        const lower = s.toLowerCase();
        lowerToOrigin.set(lower, s); // 例如 kite -> KiTe
        vowelToOrigin.set(lower.replace(/[aeiou]/g, '?'), s); // 例如 k?t? -> KiTe
    }

    for (let i = 0; i < queries.length; i++) {
        const q = queries[i];
        if (origin.has(q)) { // 完全匹配
            continue;
        }
        const lower = q.toLowerCase();
        // 先看能否不区分大小写匹配,再看能否不区分大小写+元音模糊匹配
        queries[i] = lowerToOrigin.get(lower) ?? vowelToOrigin.get(lower.replace(/[aeiou]/g, '?')) ?? "";
    }
    return queries;
};

###rust

use std::collections::{HashSet, HashMap};

impl Solution {
    pub fn spellchecker(wordlist: Vec<String>, queries: Vec<String>) -> Vec<String> {
        // 替换元音为 '?'
        fn replace_vowels(s: String) -> String {
            let s = s.bytes()
                .map(|c| match c {
                    b'a' | b'e' | b'i' | b'o' | b'u' => b'?',
                    _ => c,
                })
                .collect();
            unsafe { String::from_utf8_unchecked(s) }
        }

        let n = wordlist.len();
        let mut origin = HashSet::with_capacity(n); // 预分配空间
        let mut lower_to_origin = HashMap::with_capacity(n);
        let mut vowel_to_origin = HashMap::with_capacity(n);

        for s in wordlist.into_iter().rev() {
            origin.insert(s.clone());
            let lower = s.to_lowercase();
            lower_to_origin.insert(lower.clone(), s.clone()); // 例如 kite -> KiTe
            vowel_to_origin.insert(replace_vowels(lower), s); // 例如 k?t? -> KiTe
        }

        queries.into_iter()
            .map(|q| {
                if origin.contains(&q) {
                    return q; // 完全匹配
                }
                let lower = q.to_lowercase();
                if let Some(s) = lower_to_origin.get(&lower) {
                    s.clone() // 不区分大小写的匹配
                } else if let Some(s) = vowel_to_origin.get(&replace_vowels(lower)) {
                    s.clone() // 不区分大小写+元音模糊匹配
                } else {
                    "".to_string() // 匹配失败
                }
            })
            .collect()
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}((n+m)L)$,其中 $n$ 是 $\textit{wordlist}$ 的长度,$m$ 是 $\textit{queries}$ 的长度,$L\le 7$ 是字符串的最大长度。
  • 空间复杂度:$\mathcal{O}(nL)$。返回值不计入。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

C++ 字典树+位运算+回溯

查找单词有3种匹配方式
1、完全匹配,包括拼写与大小写
2、不完全匹配,找到相同拼写中第一个出现的单词
3、替换元音字母匹配

对于字母的匹配,可以将字典树以不区分字母大小写来建立

单词最长只有7位,可以用二进制记录单词的大小写状态,再在字典树中用哈希表记录相同拼写的不同大小写状态

替换元音字母匹配则用回溯来穷举实现

经测试,一棵字典树用回溯查找元音字母匹配,时间上和空间上都优于两棵字典树

时间复杂度理论上是O(L(M + N)), M是wordlist长度,N是queries长度,L是单词最长长度

执行用时:44 ms
image.png

###cpp

class Solution {
    struct node{
        int first;
        node* next[26];
        unordered_map<int, int> pos;  //记录大小写状态
        node() {
            first = -1;
            for(int i = 0; i < 26; i++) next[i] = NULL;
        }
    }*head;

    int findNear(string &q, int x, node* &cur) {
        if(cur == NULL) return -1;

        if(x == q.length()) {
            return cur->first;
        }else {
            int k = q[x] - (q[x] >= 'a' ? 'a' : 'A'), ret = -1, temp;
            if(k == 0 || k == 4 || k == 8 || k == 14 || k == 20) {  //0 == 'a', 4 == 'e', 8 == 'i', 14 == 'o', 20 == 'u'
                ret = findNear(q, x + 1, cur->next[0]);

                temp = findNear(q, x + 1, cur->next[4]);
                if(ret == -1 || (temp > -1 && temp < ret)) ret = temp;

                temp = findNear(q, x + 1, cur->next[8]);
                if(ret == -1 || (temp > -1 && temp < ret)) ret = temp;

                temp = findNear(q, x + 1, cur->next[14]);
                if(ret == -1 || (temp > -1 && temp < ret)) ret = temp;

                temp = findNear(q, x + 1, cur->next[20]);
                if(ret == -1 || (temp > -1 && temp < ret)) ret = temp;

                return ret;
            }else {
                return findNear(q, x + 1, cur->next[k]);
            }
        }
    }
public:
    vector<string> spellchecker(vector<string>& wordlist, vector<string>& queries) {
        vector<string> res;
        int i, j, k, cap, pos;
        head = new node();

        for(i = wordlist.size() - 1; i >= 0; i--) { //建立字典树
            node* cur = head;
            cap = 0;    //cap记录单词的大小写状态
            for(j = 0; j < wordlist[i].length(); j++) {
                if(wordlist[i][j] >= 'a') {
                    k = wordlist[i][j] - 'a';   //k是字母的序号
                    cap = cap * 2;  //小写状态+0
                }else {
                    k = wordlist[i][j] - 'A';   //k是字母的序号
                    cap = cap * 2 + 1;  //大写状态+1
                }
                if(cur->next[k] == NULL) cur->next[k] = new node();
                cur = cur->next[k];
            }
            cur->pos[cap] = i;  //记录大小写状态对应的wordlist中的出现位置
            cur->first = i; //wordlist是从后往前遍历,所以每次都可以更新first
        }

        for(auto &q : queries) {    //查找符合要求的单词
            node* cur = head;
            cap = 0;    //cap记录要查找的单词的大小写状态
            pos = -1;
            for(j = 0; cur != NULL && j < q.length(); j++) {    //对每个字母进行匹配
                if(q[j] >= 'a') {
                    k = q[j] - 'a'; //k是字母的序号
                    cap = cap * 2;  //小写状态+0
                }else {
                    k = q[j] - 'A'; //k是字母的序号
                    cap = cap * 2 + 1;  //大写状态+1
                }
                cur = cur->next[k];
            }
            if(cur == NULL || cur->first == -1) {   //匹配失败
                pos = findNear(q, 0, head); //替换元音字母查找
                if(pos == -1) res.push_back("");    //替换元音字母匹配失败
                else res.push_back(wordlist[pos]);
            }else { //单词字母匹配成功
                pos = cur->first;   //wordlist中最早出现的单词
                if(cur->pos.find(cap) != cur->pos.end()) pos = cur->pos[cap];   //大小写状态匹配成功,则找到对应的单词出现的位置
                res.push_back(wordlist[pos]);
            }
        }

        return res;
    }
};

元音拼写检查器

方法:哈希映射(HashMap)

思路与算法

我们分析了算法需要考虑的 3 种情况: 当查询完全匹配时,当查询存在大小写不同的单词匹配时,当查询与出现元音错误的单词匹配时。

在所有 3 种情况下,我们都可以使用哈希表来查询答案。

  • 对于第一种情况(完全匹配),我们使用集合存放单词以有效地测试查询单词是否在该组中。
  • 对于第二种情况(大小写不同),我们使用一个哈希表,该哈希表将单词从其小写形式转换为原始单词(大小写正确的形式)。
  • 对于第三种情况(元音错误),我们使用一个哈希表,将单词从其小写形式(忽略元音的情况下)转换为原始单词。

该算法仅剩的要求是认真规划和仔细阅读问题。

###java

class Solution {
    Set<String> words_perfect;
    Map<String, String> words_cap;
    Map<String, String> words_vow;

    public String[] spellchecker(String[] wordlist, String[] queries) {
        words_perfect = new HashSet();
        words_cap = new HashMap();
        words_vow = new HashMap();

        for (String word: wordlist) {
            words_perfect.add(word);

            String wordlow = word.toLowerCase();
            words_cap.putIfAbsent(wordlow, word);

            String wordlowDV = devowel(wordlow);
            words_vow.putIfAbsent(wordlowDV, word);
        }

        String[] ans = new String[queries.length];
        int t = 0;
        for (String query: queries)
            ans[t++] = solve(query);
        return ans;
    }

    public String solve(String query) {
        if (words_perfect.contains(query))
            return query;

        String queryL = query.toLowerCase();
        if (words_cap.containsKey(queryL))
            return words_cap.get(queryL);

        String queryLV = devowel(queryL);
        if (words_vow.containsKey(queryLV))
            return words_vow.get(queryLV);

        return "";
    }

    public String devowel(String word) {
        StringBuilder ans = new StringBuilder();
        for (char c: word.toCharArray())
            ans.append(isVowel(c) ? '*' : c);
        return ans.toString();
    }

    public boolean isVowel(char c) {
        return (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u');
    }
}

###python

class Solution(object):
    def spellchecker(self, wordlist, queries):
        def devowel(word):
            return "".join('*' if c in 'aeiou' else c
                           for c in word)

        words_perfect = set(wordlist)
        words_cap = {}
        words_vow = {}

        for word in wordlist:
            wordlow = word.lower()
            words_cap.setdefault(wordlow, word)
            words_vow.setdefault(devowel(wordlow), word)

        def solve(query):
            if query in words_perfect:
                return query

            queryL = query.lower()
            if queryL in words_cap:
                return words_cap[queryL]

            queryLV = devowel(queryL)
            if queryLV in words_vow:
                return words_vow[queryLV]
            return ""

        return map(solve, queries)

复杂度分析

  • 时间复杂度:$O(\mathcal{C})$,其中 $\mathcal{C}$ 是 wordlistqueries 中内容的总数。

  • 空间复杂度:$O(\mathcal{C})$。

每日一题-找到频率最高的元音和辅音🟢

给你一个由小写英文字母('a''z')组成的字符串 s。你的任务是找出出现频率 最高 的元音('a''e''i''o''u' 中的一个)和出现频率最高的辅音(除元音以外的所有字母),并返回这两个频率之和。

注意:如果有多个元音或辅音具有相同的最高频率,可以任选其中一个。如果字符串中没有元音或没有辅音,则其频率视为 0。

一个字母 x 的 频率 是它在字符串中出现的次数。

 

示例 1:

输入: s = "successes"

输出: 6

解释:

  • 元音有:'u' 出现 1 次,'e' 出现 2 次。最大元音频率 = 2。
  • 辅音有:'s' 出现 4 次,'c' 出现 2 次。最大辅音频率 = 4。
  • 输出为 2 + 4 = 6

示例 2:

输入: s = "aeiaeia"

输出: 3

解释:

  • 元音有:'a' 出现 3 次,'e' 出现 2 次,'i' 出现 2 次。最大元音频率 = 3。
  • s 中没有辅音。因此,最大辅音频率 = 0。
  • 输出为 3 + 0 = 3

 

提示:

  • 1 <= s.length <= 100
  • s 只包含小写英文字母

统计字母出现次数+位运算优化(Python/Java/C++/C/Go/JS/Rust)

在遍历 $s$ 的过程中,用哈希表(或者数组)统计每个字母的出现次数 $\textit{cnt}$。

  • 如果字母是元音,更新 $\textit{maxVowelCnt}$ 的最大值。
  • 如果字母是辅音,更新 $\textit{maxConsonantCnt}$ 的最大值。

最终答案是 $\textit{maxVowelCnt} + \textit{maxConsonantCnt}$。

写法一

###py

class Solution:
    def maxFreqSum(self, s: str) -> int:
        cnt = [0] * 26
        max_vowel_cnt = max_consonant_cnt = 0
        for ch in s:
            idx = ord(ch) - ord('a')
            cnt[idx] += 1
            if ch in "aeiou":
                max_vowel_cnt = max(max_vowel_cnt, cnt[idx])
            else:
                max_consonant_cnt = max(max_consonant_cnt, cnt[idx])
        return max_vowel_cnt + max_consonant_cnt

###py

class Solution:
    def maxFreqSum(self, s: str) -> int:
        cnt = Counter(s)

        max_vowel_cnt = 0
        for ch in "aeiou":
            max_vowel_cnt = max(max_vowel_cnt, cnt[ch])
            del cnt[ch]  # 这样下面计算的一定是辅音出现次数的最大值

        max_consonant_cnt = max(cnt.values(), default=0)
        return max_vowel_cnt + max_consonant_cnt

###java

class Solution {
    public int maxFreqSum(String s) {
        int[] cnt = new int[26];
        int maxVowelCnt = 0;
        int maxConsonantCnt = 0;
        for (char ch : s.toCharArray()) {
            cnt[ch - 'a']++;
            if (ch == 'a' || ch == 'e' || ch == 'i' || ch == 'o' || ch == 'u') {
                maxVowelCnt = Math.max(maxVowelCnt, cnt[ch - 'a']);
            } else {
                maxConsonantCnt = Math.max(maxConsonantCnt, cnt[ch - 'a']);
            }
        }
        return maxVowelCnt + maxConsonantCnt;
    }
}

###cpp

class Solution {
public:
    int maxFreqSum(string s) {
        int cnt[26]{};
        int max_vowel_cnt = 0;
        int max_consonant_cnt = 0;
        for (char ch : s) {
            cnt[ch - 'a']++;
            if (ch == 'a' || ch == 'e' || ch == 'i' || ch == 'o' || ch == 'u') {
                max_vowel_cnt = max(max_vowel_cnt, cnt[ch - 'a']);
            } else {
                max_consonant_cnt = max(max_consonant_cnt, cnt[ch - 'a']);
            }
        }
        return max_vowel_cnt + max_consonant_cnt;
    }
};

###c

#define MAX(a, b) ((b) > (a) ? (b) : (a))

int maxFreqSum(char* s) {
    int cnt[26] = {};
    int max_vowel_cnt = 0;
    int max_consonant_cnt = 0;
    for (int i = 0; s[i]; i++) {
        char ch = s[i];
        cnt[ch - 'a']++;
        if (ch == 'a' || ch == 'e' || ch == 'i' || ch == 'o' || ch == 'u') {
            max_vowel_cnt = MAX(max_vowel_cnt, cnt[ch - 'a']);
        } else {
            max_consonant_cnt = MAX(max_consonant_cnt, cnt[ch - 'a']);
        }
    }
    return max_vowel_cnt + max_consonant_cnt;
}

###go

func maxFreqSum(s string) int {
cnt := [26]int{}
maxVowelCnt := 0
maxConsonantCnt := 0
for _, ch := range s {
cnt[ch-'a']++
if strings.ContainsRune("aeiou", ch) {
maxVowelCnt = max(maxVowelCnt, cnt[ch-'a'])
} else {
maxConsonantCnt = max(maxConsonantCnt, cnt[ch-'a'])
}
}
return maxVowelCnt + maxConsonantCnt
}

###js

var maxFreqSum = function(s) {
    const cnt = Array(26).fill(0);
    let maxVowelCnt = 0;
    let maxConsonantCnt = 0;
    for (const ch of s) {
        const idx = ch.charCodeAt(0) - 'a'.charCodeAt(0);
        cnt[idx]++;
        if ("aeiou".includes(ch)) {
            maxVowelCnt = Math.max(maxVowelCnt, cnt[idx]);
        } else {
            maxConsonantCnt = Math.max(maxConsonantCnt, cnt[idx]);
        }
    }
    return maxVowelCnt + maxConsonantCnt;
};

###rust

impl Solution {
    pub fn max_freq_sum(s: String) -> i32 {
        let mut cnt = [0; 26];
        let mut max_vowel_cnt = 0;
        let mut max_consonant_cnt = 0;
        for ch in s.bytes() {
            let idx = (ch as u8 - b'a') as usize;
            cnt[idx] += 1;
            if "aeiou".contains(ch as char) {
                max_vowel_cnt = max_vowel_cnt.max(cnt[idx]);
            } else {
                max_consonant_cnt = max_consonant_cnt.max(cnt[idx]);
            }
        }
        max_vowel_cnt + max_consonant_cnt
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$ 或 $\mathcal{O}(n + |\Sigma|)$,其中 $n$ 是 $s$ 的长度,$|\Sigma|=26$ 是字符集合的大小。
  • 空间复杂度:$\mathcal{O}(|\Sigma|)$。

写法二

根据 从集合论到位运算,我们可以把元音集合

$$
{\texttt{a},\texttt{e},\texttt{i},\texttt{o},\texttt{u}}
$$

视作数字

$$
2^0 + 2^4 + 2^8 + 2^{14} + 2^{20} = 1065233
$$

即十六进制的 $\texttt{0x104111}$。

可以用位运算快速判断字母是否在元音集合中。

###py

class Solution:
    def maxFreqSum(self, s: str) -> int:
        VOWEL_MASK = 0x104111
        cnt = [0] * 26
        max_cnt = [0] * 2
        for ch in s:
            ch = ord(ch) - ord('a')
            bit = VOWEL_MASK >> ch & 1
            cnt[ch] += 1
            max_cnt[bit] = max(max_cnt[bit], cnt[ch])
        return sum(max_cnt)

###java

class Solution {
    public int maxFreqSum(String s) {
        final int VOWEL_MASK = 0x104111;
        int[] cnt = new int[26];
        int[] maxCnt = new int[2];
        for (char ch : s.toCharArray()) {
            ch -= 'a';
            int bit = VOWEL_MASK >> ch & 1;
            cnt[ch]++;
            maxCnt[bit] = Math.max(maxCnt[bit], cnt[ch]);
        }
        return maxCnt[0] + maxCnt[1];
    }
}

###cpp

class Solution {
public:
    int maxFreqSum(string s) {
        const int VOWEL_MASK = 0x104111;
        int cnt[26]{};
        int max_cnt[2]{};
        for (char ch : s) {
            ch -= 'a';
            int bit = VOWEL_MASK >> ch & 1;
            cnt[ch]++;
            max_cnt[bit] = max(max_cnt[bit], cnt[ch]);
        }
        return max_cnt[0] + max_cnt[1];
    }
};

###c

#define VOWEL_MASK 0x104111
#define MAX(a, b) ((b) > (a) ? (b) : (a))

int maxFreqSum(char* s) {
    int cnt[26] = {};
    int max_cnt[2] = {};
    for (int i = 0; s[i]; i++) {
        int ch = s[i] - 'a';
        int bit = VOWEL_MASK >> ch & 1;
        cnt[ch]++;
        max_cnt[bit] = MAX(max_cnt[bit], cnt[ch]);
    }
    return max_cnt[0] + max_cnt[1];
}

###go

func maxFreqSum(s string) int {
const vowelMask = 0x104111
cnt := [26]int{}
maxCnt := [2]int{}
for _, ch := range s {
ch -= 'a'
bit := vowelMask >> ch & 1
cnt[ch]++
maxCnt[bit] = max(maxCnt[bit], cnt[ch])
}
return maxCnt[0] + maxCnt[1]
}

###js

var maxFreqSum = function(s) {
    const VOWEL_MASK = 0x104111;
    const cnt = Array(26).fill(0);
    const maxCnt = [0, 0];
    for (const ch of s) {
        const idx = ch.charCodeAt(0) - 'a'.charCodeAt(0);
        const bit = VOWEL_MASK >> idx & 1;
        cnt[idx]++;
        maxCnt[bit] = Math.max(maxCnt[bit], cnt[idx]);
    }
    return maxCnt[0] + maxCnt[1];
};

###rust

impl Solution {
    pub fn max_freq_sum(s: String) -> i32 {
        const VOWEL_MASK: usize = 0x104111;
        let mut cnt = [0; 26];
        let mut max_cnt = [0; 2];
        for ch in s.bytes() {
            let idx = (ch as u8 - b'a') as usize;
            let bit = VOWEL_MASK >> idx & 1;
            cnt[idx] += 1;
            max_cnt[bit] = max_cnt[bit].max(cnt[idx]);
        }
        max_cnt[0] + max_cnt[1]
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $s$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

[Python3/Java/C++/Go/TypeScript] 一题一解:脑筋急转弯(清晰题解)

方法一:脑筋急转弯

我们不妨记字符串中元音字母的个数为 $k$。

如果 $k = 0$,即字符串中没有元音字母,那么小红无法移除任何子字符串,小明直接获胜。

如果 $k$ 为奇数,那么小红可以移除整个字符串,小红直接获胜。

如果 $k$ 为偶数,那么小红可以移除 $k - 1$ 个元音字母,此时剩下一个元音字母,小明无法移除任何子字符串,小红直接获胜。

综上所述,如果字符串中包含元音字母,那么小红获胜,否则小明获胜。

###python

class Solution:
    def doesAliceWin(self, s: str) -> bool:
        vowels = set("aeiou")
        return any(c in vowels for c in s)

###java

class Solution {
    public boolean doesAliceWin(String s) {
        for (int i = 0; i < s.length(); ++i) {
            if ("aeiou".indexOf(s.charAt(i)) != -1) {
                return true;
            }
        }
        return false;
    }
}

###cpp

class Solution {
public:
    bool doesAliceWin(string s) {
        string vowels = "aeiou";
        for (char c : s) {
            if (vowels.find(c) != string::npos) {
                return true;
            }
        }
        return false;
    }
};

###go

func doesAliceWin(s string) bool {
vowels := "aeiou"
for _, c := range s {
if strings.ContainsRune(vowels, c) {
return true
}
}
return false
}

###ts

function doesAliceWin(s: string): boolean {
    const vowels = 'aeiou';
    for (const c of s) {
        if (vowels.includes(c)) {
            return true;
        }
    }
    return false;
}

###rust

impl Solution {
    pub fn does_alice_win(s: String) -> bool {
        let vowels = "aeiou";
        for c in s.chars() {
            if vowels.contains(c) {
                return true;
            }
        }
        false
    }
}

###cs

public class Solution {
    public bool DoesAliceWin(string s) {
        string vowels = "aeiou";
        foreach (char c in s) {
            if (vowels.Contains(c)) {
                return true;
            }
        }
        return false;
    }
}

时间复杂度 $O(n)$,其中 $n$ 为字符串 $s$ 的长度。空间复杂度 $O(1)$。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~

每日一题-字符串元音游戏🟡

小红和小明在玩一个字符串元音游戏。

给你一个字符串 s,小红和小明将轮流参与游戏,小红开始:

  • 在小红的回合,她必须移除 s 中包含 奇数 个元音的任意 非空 子字符串
  • 在小明的回合,他必须移除 s 中包含 偶数 个元音的任意 非空 子字符串

第一个无法在其回合内进行移除操作的玩家输掉游戏。假设小红和小明都采取 最优策略

如果小红赢得游戏,返回 true,否则返回 false

英文元音字母包括:a, e, i, o, 和 u

 

示例 1:

输入: s = "leetcoder"

输出: true

解释:
小红可以执行如下移除操作来赢得游戏:

  • 小红先手,她可以移除加下划线的子字符串 s = "leetcoder",其中包含 3 个元音。结果字符串为 s = "der"
  • 小明接着,他可以移除加下划线的子字符串 s = "der",其中包含 0 个元音。结果字符串为 s = "er"
  • 小红再次操作,她可以移除整个字符串 s = "er",其中包含 1 个元音。
  • 又轮到小明,由于字符串为空,无法执行移除操作,因此小红赢得游戏。

示例 2:

输入: s = "bbcd"

输出: false

解释:
小红在她的第一回合无法执行移除操作,因此小红输掉了游戏。

 

提示:

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