8月刷题笔记

刷题笔记—8月

LCP40.心算挑战(贪心、排序)

class Solution {
public:
    int maxmiumScore(vector<int>& cards, int cnt) {
        //24.8.1
        ranges::sort(cards, greater()); //从大到小排序
        int s = reduce(cards.begin(), cards.begin()+cnt, 0);
        if(s%2 == 0) return s;

        auto replace_sum = [&](int x) -> int {
            for(int i = cnt; i < cards.size(); i++) {
                if(cards[i]%2 != x%2) {
                    return s-x+cards[i];
                }
            }
            return 0;
        };

        int x = cards[cnt-1];
        int ans = replace_sum(x);
        cout << ans;
        for(int i = cnt-2; i >= 0; i--) {
            if(cards[i]%2 != x%2) {
                ans = max(ans, replace_sum(cards[i]));
                break;
            }
        }
        return ans;
    }
};

我感觉这压根不是简单题啊!想过很多办法:双指针、前缀和、dp,dp会超时。结果是个贪心题,话说贪心思路的确可以想到,但是要实现出来好像真的很麻烦!

946.验证栈序列(栈、模拟)

class Solution {
public:
    bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
        stack<int> st;
        int j = 0;
        for(int pu : pushed) {
            st.push(pu);
            while(!st.empty() && st.top() == popped[j]) {
                st.pop();
                j++;
            }
        }
        return st.empty();
    }
};
func validateStackSequences(pushed []int, popped []int) bool {
    st := []int{}
    j := 0
    for _, val := range pushed {
        st = append(st, val)
        for len(st) > 0 && st[len(st)-1] == popped[j] {
            st = st[:len(st)-1]
            j++
        }
    }
    if len(st) > 0 {
        return false
    } else {
        return true
    }
}

简单的栈的使用,主要是模拟题目的过程。

3128.直角三角形(数学)

class Solution {
public:
    long long numberOfRightTriangles(vector<vector<int>>& grid) {
        int n = grid[0].size();
        vector<int> col_sum(n, -1); //提前减一
        for(auto row : grid) {
            for(int i = 0; i < n; i++) {
                col_sum[i] += row[i];
            }
        }

        long long ans = 0;
        for(auto row : grid) {
            int rowsum = reduce(row.begin(), row.end(), 0)-1;   //提前减一
            for(int j = 0; j < n; j++) {
                if(row[j] == 1) ans += rowsum*col_sum[j];
            }
        }
        return ans;
    }
};
func numberOfRightTriangles(grid [][]int) int64 {
    n := len(grid[0])
    colSum := make([]int, n)
    for _, row := range grid {
        for j, x := range row {
            colSum[j] += x
        }
    }

    ans := 0
    for _, row := range grid {
        rowSum := -1   //提前减一
        for _, x := range row {
            rowSum += x
        }
        for j, x := range row {
            if x == 1 {
                ans += rowSum*(colSum[j]-1)
            }
        }
    }

    return int64(ans)
}

1021.删除最外层的括号(栈、计数法)

class Solution {
public:
    string removeOuterParentheses(string s) {
        int level = 0;
        string ans;
        for(auto c : s) {
            if(c == ')') level--;
            if(level) ans.push_back(c);
            if(c == '(') level++;
        }
        return ans;
    }
};
func removeOuterParentheses(s string) string {
    level := 0
    ans := []byte{}
    for _, c := range s {
        if c == ')' {
            level--
        }
        if level > 0 {
            ans = append(ans, byte(c))
        }
        if c == '(' {
            level++
        }
    }
    return string(ans)
}

涨见识的一道题,关键是要想到这个方法,已经是知道要用栈来写这一题,但是还是没办法解决,这种感觉太难受了,就是无论怎么硬想都想不出来。。。这里的level是用来计数的,第一次和为0就代表第一个原语,还有这里的顺序很重要!感觉就把这个当作模板来记忆吧!

1190.反转每对括号间的子串(栈)

class Solution {
public:
    string reverseParentheses(string s) {
        stack<string> st;
        string str = "";
        for(char c : s) {
            if(c == '(') {
                st.push(str);
                str = "";
            } else if(c == ')') {
                reverse(str.begin(), str.end());
                str = st.top()+str;
                st.pop();
            } else {
                str.push_back(c);
            }
        }
        return str;
    }
};
func reverseParentheses(s string) string {
    st := [][]byte{}
    str := []byte{}
    for _, c := range s {
        if c == '(' {
            st = append(st, str)
            str = []byte{}
        } else if c == ')' {
            for j, n := 0, len(str); j < n-1-j; j++ {
                str[j], str[n-1-j] = str[n-1-j], str[j]
            }
            str = append(st[len(st)-1], str...)
            st = st[:len(st)-1]
        } else {
            str = append(str, byte(c))
        }
    }
    return string(str)
}

看来还是大意了,以为栈的题目都是很简单的,现在来看好像其实并不如此,还是有很多题型和方法是需要总结的。对于这一题,和之前都有所不同,这里是将栈定义成了stack<string>类型保存的每一层的str,当遇到’(‘就将str入栈并str更新为空,当遇到’)'就将这一层的str反转并返回给上一层,这里也是复习到了reverse(nums.begin(), nums.end())反转函数,然后就是对于go,是没有反转函数的,这样也复习到了对对称字符串的操作了,最后特别说明一行str = append(st[len(st)-1], str...),由于str是一个字符串,所以append中的str要写成str…

1003.检查替换后的词是否有效(栈)

自己写的版本

class Solution {
public:
    bool isValid(string s) {
        stack<char> st;
        for(char ch : s) {
            if(!st.empty() && ch == 'c') {
                char t = st.top();
                st.pop();
                if(!st.empty() && t == 'b' && st.top() == 'a') st.pop();
                else st.push(t), st.push(ch);
            } else {
                st.push(ch);
            }
        }
        return st.empty();
    }
};

思路就是将abc看成整体,所以只需要遇到c的时候再出栈就行,然后就是栈操作的注意事项,这一题wa了两次,主要就是错在了两次的栈判空操作上,由于需要取出c的前两位,所以就需要有两次的st判空操作,这样就能ac这一题了

func isValid(s string) bool {
    st := []byte{}
    for _, ch := range s {
        if len(st) > 0 && ch == 'c' {
            t := st[len(st)-1]
            st = st[:len(st)-1]
            if len(st) > 0 && t == 'b' && st[len(st)-1] == 'a' {
                st = st[:len(st)-1]
            } else {
                st = append(st, byte(ch))
            }
        } else {
            st = append(st, byte(ch))
        }
    }

    if len(st) > 0 {
        return false
    } else {
        return true
    }
}

灵神版本

class Solution {
public:
    bool isValid(string s) { // s 同时作为栈
        int i = 0; // i-1 表示栈顶下标,i=0 表示栈为空
        for (char c: s) {
            if (c > 'a' && (i == 0 || c - s[--i] != 1))
                return false;
            if (c < 'c')
                s[i++] = c; // 入栈
        }
        return i == 0;
    }
};

真的太过于简洁了,很多东西都合并在一起了,不看题解文章真的有点难理解。。。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2216.美化数组的最少删除数

class Solution {
public:
    int minDeletion(vector<int>& nums) {
        int cnt = 0, n = nums.size();
        for(int i = 0; i < n; i++) {
            if((i-cnt)%2 == 0 && i+1 < n && nums[i] == nums[i+1]) cnt++;
        }
        return (n-cnt)%2 == 0 ? cnt : cnt+1;
    }
};
func minDeletion(nums []int) int {
    cnt, n := 0, len(nums)
    for i := 0; i < n; i++ {
        if (i-cnt)%2 == 0 && i+1 < n && nums[i] == nums[i+1] {
            cnt++
        }
    }

    if (n-cnt)%2 == 0 {
        return cnt
    } else {
        return cnt+1
    }
}

这是栈的题单中的一道题,但是我看不懂栈的写法,从评论区看到一个很牛逼的写法,关键就是找到这个下标i-cnt,这一点我感觉很妙,也非常切合题目意思,删除一个元素之后其它元素向前移动,这个就是充分利用了下标的和变量之间的关系

1006.笨阶乘(栈)

class Solution {
public:
    int clumsy(int n) {
        //24.8.3
        stack<int> st;
        st.push(n);
        n--;
        int index = 0;
        while(n > 0) {
            cout << n;
            if(index%4 == 0) st.top() *= n;
            else if(index%4 == 1) st.top() /= n;
            else if(index%4 == 2) st.push(n);
            else if(index%4 == 3) st.push(-n);
            index++;
            n--;
        }
        
        int sum = 0;
        while(!st.empty()) {
            sum += st.top();
            st.pop();
        }

        return sum;
    }
};
func clumsy(n int) int {
    st := []int{}
    st = append(st, n)
    n--
    index := 0
    for n > 0 {
        if index%4 == 0 {
            st[len(st)-1] *= n
        } else if index%4 == 1 {
            st[len(st)-1] /= n
        } else if index%4 == 2 {
            st = append(st, n)
        } else if index%4 == 3 {
            st = append(st, -n)
        }
        n--
        index++
    }

    sum := 0
    for len(st) > 0 {
        sum += st[len(st)-1]
        st = st[:len(st)-1]
    }
    return sum
}

栈的灵活运用,思考的时候想到了利用取余的方式判断乘除加减,就是对栈操作上还是有很多漏洞,这一题的思路就是遇到乘除就将栈顶元素进行运算,遇到加减就将n或-n入栈就行,唉,主要是没想到这一些栈操作。

224.基本计算器(栈)

class Solution {
public:
    int calculate(string s) {
        //24.8.3
        stack<int> st;  //保存每一层的符号位
        int sign = 1;
        st.push(sign);
        int n = s.size(), i = 0, ans = 0;
        while(i < n) {
            if(s[i] == ' ') i++;
            else if(s[i] == '+') sign = st.top(), i++;
            else if(s[i] == '-') sign = -st.top(), i++;
            else if(s[i] == '(') st.push(sign), i++;
            else if(s[i] == ')') st.pop(), i++;
            else {
                long long num = 0;
                while(i < n && s[i] >= '0' && s[i] <= '9') {
                    num = num*10+s[i]-'0';
                    i++;
                }
                ans += sign*num;
            }
        }
        return ans;
    }
};

func calculate(s string) int {
    st := []int{}
    st = append(st, 1)
    i, n, ans, sign := 0, len(s), 0, 1
    for i < n {
        switch s[i] {
        case ' ':
            i++
        case '+':
            sign = st[len(st)-1]
            i++
        case '-':
            sign = -st[len(st)-1]
            i++
        case '(':
            st = append(st, sign)
            i++
        case ')':
            st = st[:len(st)-1]
            i++
        default:
            num := 0
            for i < n && s[i] >= '0' && s[i] <= '9' {
                num = num*10+int(s[i]-'0')  //一定要有int()
                i++
            }
            ans += sign*num
        }
    }
    return ans
}

和上一题相似,都属于表达式解析类型的题目,当然这里仅限的符号加减和括号,还没有涉及到乘除运算。两题解法其实很相似,栈中都是保存的每一层的数据,这里保存的是当前层的符号位,上一题保存的是值,感觉对栈的应用层次还是挺低的,就是明确这一题是用栈来实现,但是就是写不出

1475.商品折扣后的最终价格(单调栈)

class Solution {
public:
    vector<int> finalPrices(vector<int>& prices) {
        int n = prices.size();
        stack<int> st;
        vector<int> ans(n);
        for(int i = n-1; i >= 0; i--) {
            while(!st.empty() && st.top() > prices[i]) st.pop();
            ans[i] = st.empty() ? prices[i] : prices[i]-st.top();
            st.push(prices[i]);
        }
        return ans;
    }
};

func finalPrices(prices []int) []int {
    st := []int{}
    n := len(prices)
    ans := make([]int, n)
    for i := n-1; i >= 0; i-- {
        for len(st) > 0 && st[len(st)-1] > prices[i] {
            st = st[:len(st)-1]
        }
        if len(st) == 0 {
            ans[i] = prices[i]
        } else {
            ans[i] = prices[i]-st[len(st)-1]
        }
        st = append(st, prices[i])
    }
    return ans
}

栈中维护的是右侧第一个小于prices[i]的值,自底向上是递增的一个栈,当栈空时表明当前prices[i]右侧没有小于它的值,每次循环结束后需要将当前prices[i]入栈

496.下一个更大元素I(单调栈、哈希表)

从右往左

class Solution {
public:
    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
        int n1 = nums1.size(), n2 = nums2.size();
        vector<int> ans(n1, -1);    //找不到直接初始化为-1
        unordered_map<int, int> hash;    //nums1[i] -- i
        for(int i = 0; i < n1; i++) {
            hash[nums1[i]] = i;
        }

        stack<int> st;
        for(int i = nums2.size()-1; i >= 0; i--) {
            int x = nums2[i];
            while(!st.empty() && x >= st.top()) st.pop();
            if(!st.empty() && hash.contains(x)) ans[hash[x]] = st.top();
            st.push(x);
        }
        return ans;
    }
};

func nextGreaterElement(nums1 []int, nums2 []int) []int {
    n1, n2 := len(nums1), len(nums2)
    hash := make(map[int]int)
    for i, x := range nums1 {
        hash[x] = i
    }

    st := []int{}
    ans := make([]int, n1)
    //得把ans数组初始化为-1 否则要在if len(st) > 0处加个else判断
    // for i := range ans {
    //     ans[i] = -1
    // }

    for i := n2-1; i >= 0; i-- {
        x := nums2[i]
        for len(st) > 0 && x >= st[len(st)-1] {
            st = st[:len(st)-1]
        }
        if len(st) > 0 {
            if j, ok := hash[x]; ok {
                ans[j] = st[len(st)-1]
            }
        } else {
            ans[hash[x]] = -1
        }
        st = append(st, x)
    }
    return ans
}

过了一天后感觉对单调栈有了新认识,之前做过单调栈的题,但是昨天做的时候忘掉的差不多了,怎么都想不清楚这个单调栈维护的过程。

从左往右

class Solution {
public:
    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
        int n1 = nums1.size(), n2 = nums2.size();
        vector<int> ans(n1, -1);    //找不到直接初始化为-1
        unordered_map<int, int> hash;    //nums1[i] -- i
        for(int i = 0; i < n1; i++) {
            hash[nums1[i]] = i;
        }

        stack<int> st;  //记录还没有找到下一个更大元素的栈
        for(int x : nums2) {
            while(!st.empty() && x > st.top()) {
                ans[hash[st.top()]] = x;
                st.pop();
            }
            if(hash.contains(x)) {
                st.push(x);
            }
        }
        return ans;
    }
};

572.另一棵树的子树(二叉树)—100

class Solution {
public:

    bool isSameTree(TreeNode* p, TreeNode* q) {
        if(p == nullptr || q == nullptr) return p == q;
        return p->val == q->val && isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
    }

    bool isSubtree(TreeNode* root, TreeNode* subRoot) {
        if(root == nullptr) return false;
        return isSameTree(root, subRoot) || isSubtree(root->left, subRoot) || isSubtree(root->right, subRoot);
    }
};

func isSameTree(p, q *TreeNode) bool {
    if p == nil || q == nil {
        return p == q
    }
    return p.Val == q.Val && isSameTree(p.Left, q.Left) && isSameTree(p.Right, q.Right)
}

func isSubtree(root *TreeNode, subRoot *TreeNode) bool {
    if root == nil {
        return false
    }
    return isSameTree(root, subRoot) || isSubtree(root.Left, subRoot) || isSubtree(root.Right, subRoot)
}

这一题和100关联了起来,调用了100的函数isSameTree用来判断p,q是否为相同的树,关键就是最后return的或逻辑,我感觉不是很好理解,我的理解是可能只要是root的一个子树满足于subtree相同就行了,所以是或的逻辑

1019.链表中的下一个更大节点

从右往左遍历

class Solution {
public:
    vector<int> nextLargerNodes(ListNode* head) {
        vector<int> t;
        ListNode* p = head;
        while(p) {
            t.push_back(p->val);
            p = p->next;
        }
        int n = t.size();

        stack<int> st;
        vector<int> ans(n);
        for(int i = 0; i < n; i++) {
            int x = t[i];
            while(!st.empty() && x > t[st.top()]) {
                ans[st.top()] = x;
                st.pop();
            }
            //if(st.empty()) ans[i] = 0;
            st.push(i);
        }
        return ans;
    }
};

从左往右遍历

class Solution {
public:
    vector<int> nextLargerNodes(ListNode* head) {
        vector<int> t;
        ListNode* p = head;
        while(p) {
            t.push_back(p->val);
            p = p->next;
        }
        int n = t.size();

        stack<int> st;
        vector<int> ans(n);
        for(int i = 0; i < n; i++) {
            int x = t[i];
            while(!st.empty() && x > t[st.top()]) {
                ans[st.top()] = x;
                st.pop();
            }
            //if(st.empty()) ans[i] = 0;
            st.push(i);
        }
        return ans;
    }
};

把问题化简之后就好写很多了!就是先遍历一遍链表,同时将链表的值存入数组t中,这样问题就变成了求数组中当前元素的下一个更大的值了

227.基本计算器II(栈)

class Solution {
public:
    int calculate(string s) {
        vector<int> st;
        char preSign = '+';
        long long num = 0, n = s.size();
        for(int i = 0; i < n; i++) {
            if(isdigit(s[i])) num = num*10+s[i]-'0';
            if(!isdigit(s[i]) && s[i] != ' ' || i == n-1) {
                switch(preSign) {
                    case '+':
                        st.push_back(num);
                        break;
                    case '-':
                        st.push_back(-num);
                        break;
                    case '*':
                        st.back() *= num;
                        break;
                    default:
                        st.back() /= num;
                }
                preSign = s[i];
                num = 0;
            }
        }
        return reduce(st.begin(), st.end(), 0);
    }
};

这种求表达式计算的值主要难点就在于分类讨论加上栈的操作,把这两点处理好就能很好的写出这一类题了

856.括号的分数(栈)

class Solution {
public:
    int scoreOfParentheses(string s) {
        stack<int> st;
        st.push(0);
        for(char c : s) {
            if(c == '(') st.push(0);
            else {
                int t = st.top();
                st.pop();
                st.top() += max(2*t, 1);
            }
        }
        return st.top();
    }
};

func scoreOfParentheses(s string) int {
    st := []int{}
    st = append(st, 0)
    for _, c := range s {
        if c == '(' {
            st = append(st, 0)
        } else {
            t := st[len(st)-1]
            st = st[:len(st)-1]
            st[len(st)-1] += max(2*t, 1)
        }
    }
    return st[len(st)-1]
}

真的模拟不出来啊!!!有一种有力使不出的感觉,一个劲的想模拟这个过程,但是就是无论怎么想都想不出来,题解使用一个max函数就将这个括号得分的情况分类讨论出来了,这真的很妙!

1209.删除字符串中的所有相邻重复项II(栈)

class Solution {
public:
    string removeDuplicates(string s, int k) {
        stack<int> cnt;
        for(int i = 0; i < s.size(); i++) {
            if(i == 0 || s[i] != s[i-1]) cnt.push(1);
            else if(++cnt.top() == k) {
                s.erase(i-k+1, k);
                cnt.pop();
                i -= k;
            }
        }
        return s;
    }
};

用栈来维护上一个字符出现的次数,回顾了一下字符串的操作,删除字符串中的子串。erase(a, b),参数a:下标的起始位置,参数b:删除子串的长度。

2211.统计道路上的碰撞次数(脑经急转弯)

class Solution {
public:
    int countCollisions(string directions) {
        int cntL = 0, cntR = 0, cntS = 0, n = directions.size();
        int i = 0, j = n-1;
        if(n == 1) return 0;
        while(i < n && directions[i++] == 'L') cntL++;
        while(j > 0 && directions[j--] == 'R') cntR++;
        //cout << cntL << " " << cntR << " " << count(directions.begin(), directions.end(), 'S');
        cntS = count(directions.begin(), directions.end(), 'S');
        return n-cntL-cntR-cntS;
    }
};

func countCollisions(directions string) int {
    directions = strings.TrimLeft(directions, "L")    //前缀向左的车不会发生碰撞---移除开头L
    directions = strings.TrimRight(directions, "R")   //后缀向右的车不会发生碰撞---移除末尾R
    return len(directions)-strings.Count(directions, "S") //剩下非停止的车必然会碰撞
}

本来说是一道栈的题目,直接给题解区秒杀了,我的天呐,厉害实在是厉害

1046.最后一块石头的重量(堆)

class Solution {
public:
    int lastStoneWeight(vector<int>& stones) {
        priority_queue<int> pq;
        for(int s : stones) pq.push(s);

        while(pq.size() > 1) {
            int a = pq.top();
            pq.pop();
            int b = pq.top();
            pq.pop();
            if(a > b) pq.push(a-b);
        }
        return pq.empty() ? 0 : pq.top();
    }
};

这个就体现出了选择好的数据结构解题的重要性了。记住记住:优先队列定义默认是大根堆,也就是堆顶保存的是最大的元素,如果要定义小根堆那么需要加上greater<>默顶大反g

2558.从数量最多的堆取走礼物(堆、模拟)

class Solution {
public:
    long long pickGifts(vector<int>& gifts, int k) {
        priority_queue<int> pq(gifts.begin(), gifts.end());
        while(k--) {
            int t = pq.top();
            pq.pop();
            pq.push(sqrt(t));
        }
        long long ans = 0;
        while(!pq.empty()) {
            ans += pq.top();
            pq.pop();
        }
        return ans;
    }
};

首先是要想到这一题要用堆来写,然后就模拟堆的过程就行了,堆和栈队列一样,在遍历的时候不能用迭代的方法来遍历,只能弹出判空来遍历这一类型的数据结构

636.函数的独占时间(栈)

typedef pair<int, int> PII;

class Solution {
public:
    vector<int> exclusiveTime(int n, vector<string>& logs) {
        vector<int> ans(n, 0);
        stack<PII> st;
        for(int i = 0; i < logs.size(); i++) {
            vector<string> temp = split(logs[i], ':');
            int id = stoi(temp[0]), t1 = stoi(temp[2]);
            if(temp[1] == "start") st.push({id, t1});
            else {
                auto top = st.top();
                st.pop();
                int t2 = top.second;
                ans[top.first] += t1-t2+1;/ans[1]=5-2+1=4
                if(!st.empty()) ans[st.top().first] -= t1-t2+1;//ans[0]=6-0-(5-2+1)
            }
        }
        return ans;
    }

    //手撕split函数
    vector<string> split(string& s, char delimiter) {
        vector<string> ans;
        int left = 0, right = 0;
        while(right < s.size()) {
            if(s[right] == delimiter) {
                ans.push_back(s.substr(left, right-left));
                left = right+1;
            }
            right++;
        }
        ans.push_back(s.substr(left, right-left));
        return ans;
    }
};

首先要看懂这一题要有一点操作系统的知识,这样才好理解这一题的意思。这一个题解也是我从评论区中看到的我想学的一个题解,pair的应用和手撕split函数。感觉C++没有split函数就复杂了好多,像python和java都可以直接调用split函数可以简化很多步骤。这一题还有个难点就在于这个时间的计算上要对应这个ans的下标需要巧妙的搭配上。以示例一为计算结果的过程放在了代码旁边的注释上了

func exclusiveTime(n int, logs []string) []int {
    ans := make([]int, n)
    type pair struct{ idx, timestamp int }
    st := []pair{}
    for _, log := range logs {
        sp := strings.Split(log, ":")
        idx, _ := strconv.Atoi(sp[0])
        timestamp, _ := strconv.Atoi(sp[2])
        if sp[1][0] == 's' {
            if len(st) > 0 {
                ans[st[len(st)-1].idx] += timestamp - st[len(st)-1].timestamp
                st[len(st)-1].timestamp = timestamp
            }
            st = append(st, pair{idx, timestamp})
        } else {
            p := st[len(st)-1]
            st = st[:len(st)-1]
            ans[p.idx] += timestamp - p.timestamp + 1
            if len(st) > 0 {
                st[len(st)-1].timestamp = timestamp + 1
            }
        }
    }
    return ans
}

go的计算方法和cpp的计算方法是不一样的,go的计算方法是改变了栈顶的time值,从而来计算ans的,而cpp是直接通过计算来算出ans的,相比来说go的写法更容易发现一些,cpp的计算方法需要发掘出数字之间的关系,想到需要一点时间。

2434.使用机器人打印字典序最小的字符串(栈、哈希表、贪心)

class Solution {
public:
    string robotWithString(string s) {
        string ans;
        stack<char> st;
        int cnt[26]{};
        int mn = 0;
        for(char c : s) cnt[c-'a']++;
        for(char c : s) {
            cnt[c-'a']--;
            while(mn <= 25 && cnt[mn] == 0) mn++;
            st.push(c);
            while(!st.empty() && st.top()-'a' <= mn) {
                ans += st.top();
                st.pop();
            }
        }
        return ans;
    }
};

func robotWithString(s string) string {
    ans := make([]byte, 0, len(s))
    st := []byte{}
    cnt := [26]int{}
    for _, c := range s {
        cnt[c-'a']++
    }
    mn := byte(0)
    for _, c := range s {
        cnt[c-'a']--
        for mn < 26 && cnt[mn] == 0 {
            mn++
        }
        st = append(st, byte(c))
        for len(st) > 0 && st[len(st)-1]-'a' <= mn {
            ans = append(ans, st[len(st)-1])
            st = st[:len(st)-1]
        }
    }
    return string(ans)
}

感觉这个题还是挺难的,如果纯粹的模拟这一题肯定是过不了的,主要是要想到这是一个栈的过程,然后在解题过程中如何展现出"维护剩余字母的最小字母",难点就在于此了。

735.小行星碰撞(栈、邻项消除)

class Solution {
public:
    vector<int> asteroidCollision(vector<int>& asteroids) {
        vector<int> st;
        for(auto aster : asteroids) {
            bool alive = true;
            while(alive && aster < 0 && !st.empty() && st.back() > 0) {
                alive = aster < -st.back();
                if(st.back() <= -aster) st.pop_back();
            }
            if(alive) st.push_back(aster);
        }
        return st;
    }
    
};

func asteroidCollision(asteroids []int) []int {
    st := []int{}
    for _, aster := range asteroids {
        alive := true
        for alive == true && aster < 0 && len(st) > 0 && st[len(st)-1] > 0 {
            alive = aster < -st[len(st)-1];
            if st[len(st)-1] <= -aster {
                st = st[:len(st)-1]
            }
        }
        if alive == true {
            st = append(st, aster)
        }
    }
    return st
}

自己写的屎山代码然后还超时,唉,这次官解都写的特别简单,其实感觉自己能明白这种意思,但是写出来就是没那意思。。。

690.员工的重要性(dfs、哈希表)

class Solution {
public:
    int getImportance(vector<Employee*> employees, int id) {
        unordered_map<int, Employee*> mp;
        for(auto e : employees) mp[e->id] = e;
        auto dfs = [&](auto&& dfs, int id) -> int {
            auto e = mp[id];
            int res = e->importance;
            for(auto s : e->subordinates) {
                res += dfs(dfs, s);
            }
            return res;
        };
        return dfs(dfs, id);
    }
};

func getImportance(employees []*Employee, id int) int {
    mp := make(map[int]*Employee, len(employees))
    for _, e := range employees {
        mp[e.Id] = e
    }

    var dfs func(int) int
    dfs = func(id int) int {
        e := mp[id]
        res := e.Importance
        for _, subid := range  e.Subordinates {
            res += dfs(subid)
        }
        return res
    }
    return dfs(id)
}

思考这一题的时候点还是到位了,但是写不到位,主要感觉还是dfs不熟练,很明显的一个树结构,加上数据范围不是很大,所以这一题用dfs很合理

2336.无限集中的最小数字(优先队列、有序集合、哈希表)

有序集合写法

class SmallestInfiniteSet {
public:

    int thres = 1;
    set<int> s;

    SmallestInfiniteSet() {}
    
    int popSmallest() {
        if(s.empty()) {
            int ans = thres++;
            return ans;
        }
        auto ans = *s.begin();
        s.erase(s.begin());
        return ans;
    }
    
    void addBack(int num) {
        if(num < thres) {
            s.insert(num);
        }
    }
};


小根堆+哈希表

class SmallestInfiniteSet {
public:

    vector<bool> vis;
    priority_queue<int, vector<int>, greater<>> pq;
    int idx;

    SmallestInfiniteSet() : idx(1) {
        vis.resize(1010, false);
    }
    
    int popSmallest() {
        if(pq.empty()) {
            int ans = idx++;
            return ans;
        }
        int ans = pq.top();
        pq.pop();
        vis[ans] = false;
        return ans;
    }
    
    void addBack(int num) {
        if(num >= idx || vis[num]) return;
        if(num == idx-1) idx--;
        else {
            pq.push(num);
            vis[num] = true;
        }
    }
};

写这一题的时候有两个困扰我的点:1、如何表示这个无限长的正整数集合;2、对堆进行操作的时候如何进行判重的操作。看了题解之后其实就是这两点是答案的所在。这里用idx边界来表示这个正整数集合,使用小根堆+哈希表来达到去重的操作,在添加元素的时候就进行去重,这样就不用担心集合里出现重复的元素了。

2530.执行k次操作后的最大分数(堆、向上取整)

class Solution {
public:
    long long maxKelements(vector<int>& nums, int k) {
        long long ans = 0;
        priority_queue<int> pq;
        for(auto n : nums) pq.push(n);
        while(k--) {
            int t = pq.top();
            //cout << t << endl;
            ans += t;
            pq.pop();
            t = ceil((double)t/3);
            pq.push(t);
        }
        return ans;
    }
};

func maxKelements(nums []int, k int) int64 {
    var ans int64
    h := hp{nums}
    heap.Init(&h)   //原地堆化
    for ; k > 0; k-- {
        ans += int64(h.IntSlice[0]) //堆顶
        h.IntSlice[0] = (h.IntSlice[0]+2)/3	//对堆顶进行操作
        heap.Fix(&h, 0) //调整堆
    }
    return ans
}

type hp struct{ sort.IntSlice }
func (h hp) Less(i, j int) bool { return h.IntSlice[i] > h.IntSlice[j] } // 最大堆
func (hp) Push(any)             {}
func (hp) Pop() (_ any)         { return }

熟悉堆操作,就是简单的对堆的插入删除,调整堆的操作

3066.超过阈值的最少操作数II(堆、模拟)—1962、2208、2233

class Solution {
public:
    int minOperations(vector<int>& nums, int k) {
        int ans = 0;
        priority_queue<long long, vector<long long>, greater<>> pq;
        for(auto n :nums) pq.push((long long)n);
        while(pq.top() < k) {
            long long x = pq.top(); pq.pop();
            long long y = pq.top(); pq.pop();
            pq.push(2*x+y);
            ans++;
        }
        return ans;
    }
};

自己能顺利ac了,但是看了题解发现写的就是屎山代码,看到题解的简化版本就能顿悟的感觉怎么能不佩服!

3144.分割字符频率相等的最少子字符串(动态规划、记忆化搜索)

class Solution {
public:
    int minimumSubstringsInPartition(string s) {
        int n = s.size();
        vector<int> memo(n, -1);
        auto dfs = [&](auto&& dfs, int i) -> int {
            if(i < 0) return 0;
            int& res = memo[i];
            if(res > 0) return res;
            res = INT_MAX;
            int cnt[26]{}, maxcnt = 0, k = 0;
            for(int j = i; j >= 0; j--) {
                k += cnt[s[j]-'a']++ == 0;
                maxcnt = max(maxcnt, cnt[s[j]-'a']);
                if(i-j+1 == k*maxcnt) {
                    res = min(res, dfs(dfs, j-1)+1);
                }
            }
            return res;
        };
        return dfs(dfs, n-1);
    }
};

转化成递推

class Solution {
public:
    int minimumSubstringsInPartition(string s) {
        int n = s.size();
        vector<int> dp(n+1, INT_MAX);
        dp[0] = 0;
        for(int i = 0; i < n; i++) {
            int cnt[26]{}, maxcnt = 0, k = 0;
            for(int j = i; j >= 0; j--) {
                k += cnt[s[j]-'a']++ == 0;
                maxcnt = max(maxcnt, cnt[s[j]-'a']);
                if(i-j+1 == k*maxcnt) {
                    dp[i+1] = min(dp[i+1], dp[j]+1);
                }
            }
        }
        return dp[n];
    }
};

如何理解外层循环是从0开始的呢?可以这样理解,由于我求的是dp[n],所以我最后的结果是dp[n],如果我从后往前遍历的话会导致dp[n]早已赋值完毕了,这样我们求的dp[n]就不是答案了,所以正确的外层遍历顺序是从前往后遍历。

208.实现Trie(前缀树)

class Trie {
public:

    bool isEnd;
    Trie* next[26];

    Trie() {
        isEnd = false;
        memset(next, 0, sizeof(next));
    }
    
    void insert(string word) {
        Trie* node = this;
        for(char c : word) {
            if(node->next[c-'a'] == NULL) {
                node->next[c-'a'] = new Trie();
            }
            node = node->next[c-'a'];
        }
        node->isEnd = true;
    }
    
    bool search(string word) {
        Trie* node = this;
        for(char c : word) {
            node = node->next[c-'a'];
            if(node == NULL) return false;
            //node = node->next[c-'a'];
        }
        return node->isEnd;
    }
    
    bool startsWith(string prefix) {
        Trie* node = this;
        for(char c : prefix) {
            node = node->next[c-'a'];
            if(node == NULL) return false;
            //node = node->next[c-'a'];
        }
        return true;
    }
};

这个东西实现起来感觉有点像多叉树,准确来说应该是26叉树,在写的过程中insert函数我有不同的见解,就是和search一样,将node=node->next[c-‘a’]放在前面,但是这样是不对的,看样子是将node开辟了一个新空间,但是这里的node是个临时变量,其实就是用来遍历这个树的一个指针,所以在insert函数只能是node = node->next[c-'a'];放在循环体末尾。

3153.所有数对中数位差之和(枚举右维护左、拆位)

class Solution {
public:
    long long sumDigitDifferences(vector<int>& nums) {
        vector<array<int, 10>> cnt(to_string(nums[0]).size());  //内层用vector或者array都行 行表示当前是哪一位  列表示当前这一位是哪个数字
        long long ans = 0;
        for(int i = 0; i < nums.size(); i++) {
            for(int j = 0, x = nums[i]; x > 0; j++, x /= 10) {
                ans += i-cnt[j][x%10]++;    //找到数量关系i-cnt[j][x%10]很重要 下标i表示前面有i个数
            }
        }
        return ans;
    }
};

func sumDigitDifferences(nums []int) (ans int64) {
    cnt := make([][10]int, len(strconv.Itoa(nums[0])))
    for i, x := range nums {
        for j := 0; x > 0; x /=10 {
            ans += int64(i-cnt[j][x%10])
            cnt[j][x%10]++
            j++
        }
    }
    return ans
}

做这一题的思路仅限于想到了要竖着看这些数,但是难在不知道如何实现上,关键是这个数组的定义要搞明白,主要是打消了固定的思维方式,这两层for循环并不是对应的cnt数组下标。当然发掘每个数位差之和这个等量关系也很重要,i-cnt[j] [x%10]

14.最长公共前缀(字符串)

class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        string s = strs[0];
        for(int i = 0; i < s.size(); i++) {
            for(string str : strs) {
                if(i == s.size() || s[i] != str[i]) {
                    return s.substr(0, i);  //当下标超过str的长度 或者遇到了不同的字符
                }
            }
        }
        return s;   //遍历完整个s了 s就是最大的那个公共前缀
    }
};

func longestCommonPrefix(strs []string) string {
    s := strs[0]
    for i := range s {
        for _, str := range strs {
            if i == len(str) || str[i] != s[i] {
                return s[:i]
            }
        }
    }
    return s
}

从灵神字典树题单中找的,结果是个简单题,我连暴力的写法都想不出,感觉大抵是费了。。。其实感觉和刚刚做过的拆位题很像,也都是要一个一个竖着看,只不过这里需要左对齐竖着看,从这两道题目来看啊,感觉就是熟悉了习惯的遍历方法,突然换一个遍历的方式就感觉很陌生,看来还是需要多适应适应。

3127.构造相同颜色的正方形(举证、枚举)

class Solution {
public:
    bool canMakeSquare(vector<vector<char>>& grid) {
        auto check = [&] (int i, int j) -> bool {
            int cnt[2]{};
            cnt[grid[i][j]&1]++;
            cnt[grid[i+1][j]&1]++;
            cnt[grid[i][j+1]&1]++;
            cnt[grid[i+1][j+1]&1]++;
            return cnt[0] != 2;
        };
        return check(0, 0) || check(0, 1) || check(1, 0) || check(1, 1);
    }
};

func canMakeSquare(grid [][]byte) bool {
    check := func(i, j int) bool {
        cnt := make([]int, 2)
        cnt[grid[i][j]&1]++
        cnt[grid[i][j+1]&1]++
        cnt[grid[i+1][j]&1]++
        cnt[grid[i+1][j+1]&1]++
        return cnt[0] != 2
    }
    return check(0, 0) || check(0, 1) || check(1, 0) || check(1, 1)
}

简单题简单做,这一题做出来后看题解也是相同的思路,只不过没有注意到的是这是一个3*3的一个举证,所以只需要枚举四个左上角就行了,写两个for循环确实显得比较臃肿

8月总结

求三角形个数

以3128为例,枚举三角形的中间,然后通过乘法原理的数学等式来累加直角三角形的个数ans+=(rowsum-1)*(colsum-1),主要是发现这个乘法原理吧,再就是对二维矩阵的操作

计数法和栈的应用

这一用法出现在1021,题目意思是要去除最外层的括号,这里就需要用到计数法,当计数第一次到0就代表第一层原语结束,循环体内的if条件的顺序十分重要!

对称字符串的操作

//C++
for(int i = 0, n = nums.size(); i < n-1-i; i++) {
    swap(nums[i], nums[n-1-i]);
}

//Go
for j, n := 0, len(str); j < n-1-j; j++ {
    str[j], str[n-1-j] = str[n-1-j], str[j]
}

发掘变量之间的关系

对于某些题中,当你发现了某些变量之间的等价关系之后解题就会变得非常简单,以2216为例,下标i-cnt就非常符合题中“删除一个数,其它元素向前移动”,这样就动态的表示出来了数组的下标

字符串和数字之间的转化

//字符串转数字	如果涉及到取模运算,那么在每位累加的时候取模就行
long long num = 0;
while(i < n && s[i] >= '0' && s[i] <= '9') {
    num = num*10+s[i]-'0';
    i++;
}
ans += sign*num;

//方法二	只适用于小数字,如果字符串非常长,就不适用
stoi("111111")
    
//数字转字符串
to_string(111111)

单调栈

有两种遍历方式:从左到右和从右到左,对于我自己来说,感觉从右到左遍历好像更好理解一点。总结一下这两天做的单调栈的题,有了全新的认识,这类题型感觉可以算是一种模板。两种遍历方式则栈的定义就会不同,下面是模板代码,以1475为例

//从右往左遍历
int n = prices.size();
stack<int> st;	//栈定义为下一个更...的元素值(或下标)的候选项	需要从后往前遍历
vector<int> ans(n);
for(int i = n-1; i >= 0; i--) {
    while(!st.empty() && ...) st.pop();
    if(...) ans[i] ...
    else ans[i] ...
    st.push(...);
}

//从左往右遍历

int n = prices.size();
stack<int> st;	//栈定义为还未被使用的元素值(或下标)	需要从前往后遍历
vector<int> ans(n);
for(int i = 0; i < n; i++) {
    while(!st.empty() && ...) {
        ans[i]...
        st.pop();
    }

    if(...) ...
    st.push(...);
}

一般来说从右往左遍历栈定义成元素值,从左往右遍历栈定义成下标

省略号的逻辑就是因题而异了,大致模板就是这样,只要将省略号的逻辑写对了这个题就写对了

手撕split函数

vector<string> split(string& s, char delimiter) {
	vector<string> ans;
    int left = 0, right = 0;
    while(right < s.size()) {
        if(s[right] == delimiter) {
            ans.push_back(s.substr(left, right-left));
            left = right+1;
        }
        right++;
    }
    ans.push_back(s.substr(left, right-left));//首尾操作
    return ans;
}

去重的思路

哈希表(记忆化搜索,数组去重、堆去重)、set容器

向上取整的应用(ceil函数、+k-1)

cout << ceil(2.3);	//3
cout << ceil(11/3);	//3
cout << ceil()(double)11/3);	//4

在调用这个函数的时候只需要注意一个点,那就是括号里面的数一定要是浮点数,以第二个式子为例,先计算10/3=3,然后再将3调入到ceil函数,那么就变成了ceil(3),自然就会输出3了而不是4,改正的方法是ceil((double)10/3)

还有一种写法就是在除k之前将val先加上k-1,这样也能达到向上取整的效果

cout << (11+2)/3;	//4

前缀树、字典树(trie)模板—可以说成26叉树

class Trie {
public:

    bool isEnd;
    Trie* next[26];//或者vector<Trie*> node;这样定义
    

    //Trie() : isEnd(false), node(26, null) {}或者这样初始化
    Trie() {
        isEnd = false;
        memset(next, 0, sizeof(next));
    }
    
    void insert(string word) {
        Trie* node = this;
        for(char c : word) {
            if(node->next[c-'a'] == NULL) {
                node->next[c-'a'] = new Trie();
            }
            node = node->next[c-'a'];
        }
        node->isEnd = true;
    }
    
    bool search(string word) {
        Trie* node = this;
        for(char c : word) {
            node = node->next[c-'a'];
            if(node == NULL) return false;
            //node = node->next[c-'a'];
        }
        return node->isEnd;
    }
    
    bool startsWith(string prefix) {
        Trie* node = this;
        for(char c : prefix) {
            node = node->next[c-'a'];
            if(node == NULL) return false;
            //node = node->next[c-'a'];
        }
        return true;
    }
};

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
"Labuladong"是一个著名的算法题解博主,他的刷题笔记非常受欢迎。他的笔记具有以下几个特点: 1. 思路清晰:Labuladong的刷题笔记总是能够很清晰地阐述解题思路。他善于将复杂的问题简化为易于理解的小问题,并通过逐步引入关键概念和方法,帮助读者理解并掌握解题思路。 2. 逻辑严谨:Labuladong的刷题笔记经过深思熟虑,逻辑严谨。他会从问题的定义开始,逐步引入相关的概念和解题思路,循序渐进地解决问题。这种严谨的逻辑结构有助于读者理解和消化算法的核心思想。 3. 举例详细:Labuladong的刷题笔记通常会通过具体的例子来说明解题思路。这种举例的方式不仅能够帮助读者更好地理解解题方法,还可以帮助读者更好地应用这些方法解决其他类似的问题。 4. 知识点整合:Labuladong的刷题笔记不仅仅是一个题解,而是将相关的算法知识点整合起来,构建出一个完整的学习体系。他会引入一些底层的算法原理,将不同的解题方法进行比较和总结。这种整合的方式能够帮助读者更好地理解和掌握算法的本质。 总之,Labuladong的刷题笔记以其思路清晰、逻辑严谨、举例详细和知识点整合等特点,为广大读者提供了一种深入学习和理解算法的有效途径。通过阅读他的刷题笔记并进行实践,读者能够提高解题能力,并在面对各种算法问题时能够找到正确、高效的解决方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值