LeetCode 第 198场周赛题解报告

竟然排名21,终于拿到一次奖品了奥利给(这次题好复杂,老铁可以多看看注释~)

5464. 换酒问题

知识点:模拟
初始时有numBottles个瓶酒,喝完之后每 numExchange 个瓶子可以兑换一瓶新酒,然后接着喝,喝完接着兑,直到手里的瓶子少于 numExchange 个。兑换过程中累计喝了多少瓶酒就完事了。

class Solution {
public:
    int numWaterBottles(int numBottles, int numExchange) {
        int sum = numBottles; //一开始喝了 numBottles 瓶酒
        while(numBottles >= numExchange) {
            int tmp = numBottles / numExchange; //兑换
            numBottles -= numExchange*tmp; 
            sum += tmp; //把兑换的酒喝了
            numBottles += tmp; // 得到了 tmp 个新瓶子,继续尝试兑换
        }
        return sum;
    }
};

5465. 子树中标签相同的节点数

知识点:深度优先搜索
对于节点 x,如果我们知道以 x 为根节点的子树中各个字符的出现的次数(假设这些数据存储在一个map中,key 为字符,value 为出现次数),那么我们可以轻易的知道该子树中与 x 节点有相同字符的节点数(拿着x节点的字符去 map 查一下就ok了)。
如果对于每个节点我们都有这样的数据,那就太棒了,直接遍历每个节点,然后累加一下就O了。

那么如何拿到这些 map 呢?
如果 x 节点是叶子节点,那么这个map就太好构造了(毕竟只有一个节点)。
如果 x 节点不是叶子节点,那么可以通过其子节点的map来构造——累加各个子节点的map,然后再加上 x 节点自己的(子节点的map如何获得呢?递归啊老铁!)。

class Solution {
public:
    vector<int> next[100000]; // 邻接表
    int anw[100000]; // 用来放答案的数组
    
    //dfs 函数的目的:递归的计算每个几点的map,并维护答案数组。
    // --- 
    //root: 当前正在被处理的节点;
    //pre: root 的父节点,如果root为根节点,则 pre = -1
    //cnt: pre 的 map
    void dfs(int root, int pre, const std::string &labels, int *cnt) {
        int newcnt[26] = {0}; // newcnt 是 root 节点的 map
        int pos = labels[root] - 'a';
        newcnt[pos] += 1;
        for(auto nex : next[root]) { // 遍历子节点
            if(nex == pre) {
                continue;
            }
            dfs(nex, root, labels, newcnt); // 计算 newcnt
        }
        if (cnt != nullptr) {
            for(int i = 0; i < 26; i++) { // 用 newcnt 更新下父节点的 map
                cnt[i] += newcnt[i];
            }   
        }
        anw[root] = newcnt[pos]; // 计算答案
    }
    
    vector<int> countSubTrees(int n, vector<vector<int>>& edges, string labels) {
        // 先把边表转成邻接表
        for(const auto &edge : edges) {
            next[edge[0]].push_back(edge[1]);
            next[edge[1]].push_back(edge[0]);
        }
        // 初始化一下
        memset(anw, 0, sizeof(int)*n);
        
        //开始递归
        dfs(0, -1, labels, nullptr);
        return vector<int>(anw, anw+n);
    }
};

5466. 最多的不重叠子字符串

知识点:BFS;动态规划

个人感觉这个题比第四道题难啊,都快给我写吐了。

首先要读明白一句话:如果一个子字符串包含字符 c ,那么 s 中所有 c 字符都应该在这个子字符串中
这句话里包含了一个递归的潜规则——如果一个子串包含 c,那么该子串必须包含所有的 c,又因为这些 c 中间可能插入了其他的字符 h,所以该子串也应该包含所有的 h,进而 h 中间还可能插入了 a,所以该子串中也应该包含所有的 a,依次类推,直到不会引入新的字符类别
想明白了这个潜规则就能得出子串个数不会超过26的结论。26字母有序排列,不互相掺杂,这个时候可以划分出26个子串。只要字符互相掺杂,就必然会使子串数量减少。比如,aabbcc 最多可以划分出三个子串(aa,bb,cc),abbacc 最多只能划分出两个(abba,cc 或者 bb,cc)。
接下来,我们需要处理出每个子串的起始位置。我们可以从子串必须包含某个特定字符入手,把这个特定字符作为递归的潜规则的入口,进而得到整个子串的起点和终点。
比如,现在有字符串 abccbadd:

  • 以 a 为入口,可以发现还要包含 b,进而发现还必须包含 c。最终得到 abccba
  • 以 b 为入口,只需要递归一次,即将 c 包含进来。最终得到 bccb
  • 以 c 为入口,不需要递归。最终得到 cc
  • 以 d 为入口,也不需要递归。最终得到 dd。

有了每个子串的起点和终点,我们就要开始最后一步处理了——动态规划 (已吐)
设 dp[i] 为前i个字符中的最优解。(因为最优结果是二维的,包含长度 len 和个数 cnt ,所以dp[i]应该是个结构体)
首先来考虑下初始状态,在前 0 个字符,即空串中的最优解为 dp[0] = {cnt : 0, len: 0}。
接下来处理第一个字符,如果该字符串是某个子串的结尾,那么:
dp[1] = {cnt : 1, len : 1}
如果该字符不是某个子串的结尾,那么 dp[1] = dp[0],因为以该字符为结尾无法构成一个合法的子串。
推广到第 i 个子串,如果第 i 个字符是某个子串的结尾,设该子串为 str,其长度为 len,那么 dp[i] = better(dp[i-1], dp[i-len] + str) 。
否则,dp[i] = dp[i-1]。

现在我们有了最优解的长度和子串个数,然后我们可以通过状态转移的过程倒推出各个子串。

class Solution {
public:
    struct Node {
        int cnt; // 子串个数
        int len; // 子串总长度
        int pre; // 当前状态中最后一个子串的其实位置
        Node() : cnt(0), len(0), pre(0) {}
    };
    Node dp[100001];
    
    int left[26]; // 第 i 种字符最左边的位置
    int right[26]; // 第 i 种字符最右边的位置
    unordered_set<int> inc[26]; // left[i] 和 right[i] 之间包含的字符种类
    
    int mleft[26]; // 以第 i 种字符为入口的子串的起始位置
    int mright[26]; // 以第 i 种字符为入口的子串的终点位置
    
    // 以 goal 字符为入口,计算mleft[goal] 和 mright[goal]
    // 本质上是个 bfs
    void update(int goal) {
        int &L = mleft[goal];
        int &R = mright[goal];
        L = 1000000000;
        R = -1;
        
        queue<int> q;
        q.push(goal);
        bool mark[26] = {false};
        mark[goal] = true;
        while(q.empty() == false) {
            int f = q.front();
            q.pop();
            L = min(left[f], L);
            R = max(right[f], R);
            
            for(auto it = inc[f].begin(); it != inc[f].end(); ++it) {
                if(mark[*it] == false) {
                    mark[*it] = true;
                    q.push(*it);
                }
            }
        }
    }
    
    vector<string> maxNumOfSubstrings(string s) {
        memset(left, 0x7f7f, sizeof(left));
        memset(right, 0, sizeof(right));
        
        // 计算 left 和 right
        for(int i = 0; i < s.size(); i++) {
            int pos = s[i]-'a';
            left[pos] = min(left[pos], i+1);
            right[pos] = max(right[pos], i+1);
        }
        // 计算 inc
        for(int i = 0; i < 26; i++) {
            for(int l = left[i]-1; l < right[i]; l++) {
                if(inc[i].find(s[l]-'a') == inc[i].end()) {
                    inc[i].insert(s[l]-'a');
                }
            }
        }
        // 计算 mleft mright,即每个子串的起始位置和终点位置。
        for(int i = 0; i < 26; i++) {
            update(i);
        }
        
        //动态规划求解最优解
        for(int i = 1; i <= s.size(); i++) {
            int pos = s[i-1]-'a';
            if(mright[pos] == i) {
                Node tmp;
                tmp.cnt = dp[mleft[pos]-1].cnt + 1;
                tmp.len = dp[mleft[pos]-1].len + mright[pos] - mleft[pos]+1;
                tmp.pre = mleft[pos]-1; //记录一下当前子串的起始位置
                
                // 选择较优的一种策略,子串数量更多或者长度更短
                if(tmp.cnt > dp[i-1].cnt
                    || (tmp.cnt == dp[i-1].cnt && tmp.len < dp[i-1].len)) {
                    dp[i] = tmp;
                } else {
                    dp[i] = dp[i-1];
                }
            } else {
                dp[i] = dp[i-1];
            }
        }
        
        //通过 pre 构造答案
        int pos = dp[s.size()].pre;
        int sum = dp[s.size()].len;
        vector<string> anw;
        do {
            int L = pos+1;
            int R = mright[s[L-1]-'a'];
            string str = s.substr(L-1, R-L+1);
            sum -= str.size();
            anw.push_back(std::move(str));
            pos = dp[pos].pre;
        }while(sum > 0);
        
        return anw;
    }
};

5467. 找到最接近目标值的函数值

知识点:模拟退火;三分
首先来看看 func(arr, l, r) 到底在搞啥子。原来就是返回 arr[l…r] 子区间的与值啊。
这里还是有个潜规则:固定 l,然后随着 r 的增加,func(arr, l, r) 是单调非递增的!
一个单调非递增函数和一个常量做减法还是一个单调非递增函数
那一个单调非递增函数取绝对值呢,要么还是单调非递增的,要么就变成了一个凹函数
那问题的答案就已经呼之欲出了!枚举 l,然后模拟退火求出一个最优 r。这样就得到了 n 个局部最优的区间 [l,r],在这些区间中最优的那个就是答案了
这玩意比上个题简洁多了啊

class Solution {
public:
    
    //通过预处理,快速求解arr[L..R]的与值
    int pre[100001][20] = {0};
    
    int get(int L, int R, int target) {
        int val = 0;
        for(int i = 0, bit = 1; i < 20; i++, bit <<= 1) {
            // 如果第 i 个bit 在 [L,R] 中全为 1,那么与值的该bit也必然为 1。
            if(pre[R][i] - pre[L-1][i] == R-L+1) {
                val |= bit;   
            }
        }
        return abs(val-target);
    }
    
    // 用模拟退火求解关于 L 的局部最优解
    int query(int L, int n, int target) {
        int dir[2] = {-1, 1};  // 两个方向
        int step = 1000; // 初始步长
        int now = L; // R 的起始位置
        int best = 100000000; // 局部最优解
        
        while(step > 0) {
            int Lpos = now + step*dir[0];
            if(Lpos < L) {
                Lpos = L;
            }
            int Rpos = now + step*dir[1];
            if(Rpos > n) {
                Rpos = n;
            }
            // 向左右两个方向各走一步,求值
            int ldis = get(L, Lpos, target);
            int rdis = get(L, Rpos, target);
            int pbest = best;
            
            //更新位置及最优解
            if(ldis < best) {
                now = Lpos;
                best = ldis;
            }
            if(rdis < best) {
                now = Rpos;
                best = rdis;
            }
            
            //如果没有找到更优解,那就缩小步长
            if(pbest == best) {
                step /= 2;
            }
        }
        return best;
    }
    
    int closestToTarget(vector<int>& arr, int target) {
        int anw = 100000000;
        
        //统计前 i 个数字中,第 j 个bit 为 1 的数量。
        for(int i = 0; i < arr.size(); i++) {
            for(int j = 0, bit = 1; j < 20; j++, bit <<= 1) {
                pre[i+1][j] = pre[i][j] + ((bit&arr[i]) ? 1 : 0);
            }
        }
        
        for(int i = 1; i <= arr.size(); i++) {
            anw = min(anw, query(i, arr.size(), target));
        }
        
        return anw;
    }
};

LeetCode 第 198场周赛题解报告_第1张图片

你可能感兴趣的:(题解给力)