LeetCode 题解(1000-*, 持续更新,part3)

第一部分,编号500以内的部分
第二部分,编号501-1000部分
本博客不再更新,后面的部分见我写的另一个博客 myblog

这是编号1000以上的部分(加锁部分暂时买不起,后面有机会开个会员)

  1. Grid Illumination

题意:给定一个N,表示有N*N的网格(下标0到N-1),网格中有一些点放置有灯。每个灯能够照亮所在的行列以及两条对角线。
给定一些查询,每个查询是一个点,首先先确定位置是否被照亮,如果是,那么周围3*3方格内的灯都灭掉。接着查询下一个。
需要返回每个查询位置是否被照亮,保存为一个数组

题解:首先用一个set保存灯所在的点,接着我们可以保存被照亮的行,列和两个对角线,行和列直接用坐标就可以表示。而对角线我们取它和x轴的交点,分别是x - y 和x + y。接着就是在上面做查询和删除操作了。

class Solution {
    static const int MOD = 100007;
    struct pair_hash {
        inline size_t operator()(const pair<int,int> & p) const {
            return (p.first % MOD * 1000 + p.second) % MOD;
        }
    };
    
public:
    vector<int> gridIllumination(int N, vector<vector<int>>& lamps, vector<vector<int>>& queries) {
        unordered_set<pair<int, int>, pair_hash> lamps_pos;
        unordered_map<int, int> ill_pos[4];
        for(int i = 0;i < lamps.size(); ++i){
            int x = lamps[i][0], y = lamps[i][1];
            lamps_pos.insert(make_pair(x, y));
            ++ill_pos[0][x];
            ++ill_pos[1][y];
            ++ill_pos[2][x - y];
            ++ill_pos[3][x + y];
        }
        vector<int> ans;
        
        for(int i = 0;i < queries.size(); ++i){
            int x = queries[i][0], y = queries[i][1];
            
            if(ill_pos[0][x] || ill_pos[1][y] || ill_pos[2][x - y] || ill_pos[3][x + y])
                ans.push_back(1);
            else 
                ans.push_back(0);
            
            for(int j = -1; j <= 1; ++j){
                for(int k = -1; k <= 1; ++k){
                    int cur_x = x + j;
                    int cur_y = y + k;
                    if(cur_x < 0 || cur_y < 0 || cur_x >= N || cur_y >= N) continue;
                    if(lamps_pos.count(make_pair(cur_x, cur_y))){
                        lamps_pos.erase(make_pair(cur_x, cur_y));
                        --ill_pos[0][cur_x];
                        --ill_pos[1][cur_y];
                        --ill_pos[2][cur_x - cur_y];
                        --ill_pos[3][cur_x + cur_y];
                    }
                }
            }
        }
        return ans;
    }
};
  1. Numbers With Repeated Digits

题意:给定一个数字N,问不大于N的正整数有多少个里面具有重复数字的

题解:重复的难求,我们求不重复的,然后总数N + 1个减去不重复的
不重复的我们可以通过逐位计算。不过有另外的解法,稍慢一点,就是动态规划,见代码注释。

int dp[10][1024];

class Solution {
public:
    int numDupDigitsAtMostN(int N) {
        if (N < 10) return 0;
        string str = to_string(N);
        int numDigits = str.length();        
        memset(dp, -1, sizeof dp);
        
        return N - helper(0, 0, 1, str) + 1;
    }
    
    //dp[index][d]表示index位的数,位标记为d的不重复数字数目
    //helper计算的是不超过N的有多少
    //tight表示前缀固定,和N的前缀一样
    int helper(int index, int d, int tight, string& str)
    {
        if (index == str.length()) return 1;        
        if (tight != 1 && dp[index][d]  != -1) return dp[index][d];
        int res  = 0;
        int range = tight == 1 ? str[index] - '0' : 9;
        for (int i = 0 ; i <= range; i++)
        {
            if (d & (1 << i)) continue;
            int newTight = 0;
            
            if (tight == 1 && (str[index] - '0') == i) newTight = 1;
            //前导0不能做标记,因为可以重复出现
            if (d == 0 && i == 0) res += helper(index + 1, d, newTight, str);
            else res += helper(index + 1, d | (1 << i), newTight, str);
        } 
		if (!tight) dp[index][d] = res;
        return res;
    }
};
  1. Camelcase Matching

搞错了,这是个中等题,难怪这么简单。
题意:给定一些query串和一个pattern。一个query串能和pattern匹配如果pattern能够通过插入一些小写字母得到query串。
返回一个bool数组,表示每个query是否和pattern匹配

题解:贪心匹配。每个query如果能匹配完pattern,且其他字符都是小写字母就算匹配成功。

class Solution {
public:
    vector<bool> camelMatch(vector<string>& queries, string pattern) {
        vector<bool> ans;
        
        for(int i = 0;i < queries.size(); ++i){
            
            string &cur_q = queries[i];
            int pos = 0;
            int match = true;
            
            for(int j = 0; j < cur_q.length(); ++j){
                if(pos < pattern.length() && cur_q[j] == pattern[pos]){
                    ++pos;
                    continue;
                }
                if('a' <= cur_q[j] && cur_q[j] <= 'z') continue;
                match = false;
                break;
            }
            ans.push_back(match && pos == pattern.size());
        }
        return ans;
    }
};
  1. Stream of Characters

题意:给定一些words(字符串),构造一个数据结构可以支持一下查询。每次查询一个字符。如果倒数k(k>=1)个查询的字符组成的字符串在words中出现,则返回true,否则返回false

题解:几乎是AC自动机的模板题。构建AC自动机。记录到当前字符为止,匹配的最长位置。然后如果当前位置是一个单词结尾返回true,否则如果last指针指向的位置不是0,则说明有后缀可以匹配到字符串,返回true。
当然,用逆序字符串构建Trie也可以过。

class AAM{
    static const int N = 300000;
    int ch[N][26];
    int fail[N],last[N],vis[N];
    bool val[N];
    int cnt,root;
    
    int p; //当前到达的节点(最长匹配)
public:

    void init(){
       cnt = 0, p = root = cnt++;
       memset(vis,0,sizeof(vis));
       memset(ch[0],0,sizeof(ch[0]));
       memset(val,0,sizeof(val));
        
    }
    
    void insert(string s){
            int len = s.length();
            int p = root;
            for(int i = 0; i < len; ++i){
                    int index = s[i] - 'a';
                    if(!ch[p][index]){
                            memset(ch[cnt],0,sizeof(ch[cnt]));
                            ch[p][index] = cnt++;
                    }
                    p = ch[p][index];
            }
            val[p] = true;
    }
    
    void getfail(){
            int p = root;
            queue<int>Q;
            fail[p] = last[p] = p;
            for(int i = 0;i < 26; ++i){
                int u = ch[p][i];
                if(u) {last[u] = fail[u] = p;Q.push(u);}
                else ch[p][i] = p;
            }
            while(!Q.empty()){
                int p = Q.front(); Q.pop();
                for(int i = 0; i < 26; ++i){
                        int &u = ch[p][i];
                        if(u) {
                                fail[u] = ch[fail[p]][i];
                                last[u] = val[fail[u]]?fail[u]:last[fail[u]];
                                Q.push(u);
                        }else u = ch[fail[p]][i];
                }
            }
    }
   
    bool find(char c){
        p = ch[p][c - 'a'];
        return val[p] || last[p];
    }
   
};
class StreamChecker {
    AAM ac_m;
public:
    StreamChecker(vector<string>& words) {
        ac_m.init();
        for(int i = 0;i < words.size(); ++i)
            ac_m.insert(words[i]);
        
        ac_m.getfail();
    }
    
    bool query(char letter) {
        return ac_m.find(letter);
    }
};

/**
 * Your StreamChecker object will be instantiated and called as such:
 * StreamChecker* obj = new StreamChecker(words);
 * bool param_1 = obj->query(letter)
*/
 
 
  1. Recover a Tree From Preorder Traversal

题意:给定一个字符串,表示一颗二叉树的前序遍历顺序,如下例,其中数字是节点值,横杠表示深度。而且如果一个节点只有一个儿子,那么必定是左儿子。

例子
Input: “1-2–3--4-5–6--7”
Output: [1,2,5,3,4,6,7]

题解:用一个栈恢复遍历过程即可。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */




class Solution {
    
    int get_val(const string &s, int &pos){
        int val = 0;
        while(pos < s.size() && isdigit(s[pos])) {
            val = val * 10 + s[pos] - '0';
            ++pos;
        }
        return val;
    }
    
    int get_depth(const string &s, int &pos){
        int depth = 0;
        while(pos < s.size() && s[pos] == '-') {
            ++depth;
            ++pos;
        }
        return depth;
    }
    
    
public:
    TreeNode* recoverFromPreorder(string s) {
        stack<pair<TreeNode*, int> >  st;
        TreeNode *root = nullptr;
        int pos = 0;
        root = new TreeNode(get_val(s, pos));
        st.push(make_pair(root, 0));
        
        while(pos < s.size()){
            int depth = get_depth(s, pos);
            int val = get_val(s, pos);
            TreeNode* cur = new TreeNode(val);
            
            
            TreeNode* top = st.top().first;
            int pre_depth = st.top().second;
            if(pre_depth + 1 == depth){
                top->left = cur;
            }else{
                while(pre_depth + 1 != depth){
                    st.pop();
                    top = st.top().first;
                    pre_depth = st.top().second;
                }   
                top->right = cur;
                st.pop();
            }
            st.push(make_pair(cur, depth));
        }
        
        return root;
    }
};
  1. Escape a Large Maze

题意:在一个1 0 6 × 1 0 6 10^6\times10^6106×106的棋盘上(索引从0到1 0 6 − 1 10^6 - 11061),有一些障碍点(blocked最多200个),给定一个source和target两个点,问source能不能每次走上下左右四个方向到达target。

题解:如果不是棋盘这么大,直接BFS就能求解。稍微想一想就知道,这么少的障碍点,实际上大部分都是能通过的,我们可以将障碍点网里缩,而不改变source和target的连通性。具体做法就是将x坐标和y坐标重新布局索引。
直观的想象一下,如果不同的块之间有大于1的缝存在,我们只需要让这个缝等于1就可以了。
我们可以分别考虑x和y。考虑x的情况,先排序,如果有0,那么保持0,不然的话下标从1开始(把下方压缩为一行的空间)。
如果当前的x和前一个一样,那么保持相同的索引,如果比前面多一,那么保持多一。如果多二及以上,那么我们把它们的距离缩小到2,也就是索引加2。y同理。这样缩小得到的问题棋盘不会多于400 * 400。

题的数据有缺陷,标程也是:输入数据
[[99999,99998],[99998,99999]]
[0,0]
[99999,99999]
答案应该是false而标准程序给出的是true,并且数据中没有这样的例子。
这个和样例
[[0,1],[1,0]]
[0,0]
[1,1]
是一样的,只不过一个右上角,一个左下角。

class Solution {
public:
    
    int re_idx(vector<int> &xy_loc, map<int,int> &xy_map){
        int idx = 1;
        //边界触及0
        if(xy_loc[0] == 0) idx = 0;
        xy_map[xy_loc[0]] = idx;
        
        for(int i = 1;i < xy_loc.size(); ++i){
            if(xy_loc[i] == xy_loc[i - 1]) continue;
            if(xy_loc[i] == xy_loc[i - 1] + 1) xy_map[xy_loc[i]] = ++idx;
            else xy_map[xy_loc[i]] = (idx += 2);
        }
        //边界触及99999
        if(*xy_loc.rbegin() == 99999) return idx;
        return idx + 1;
    }
    
    bool isEscapePossible(vector<vector<int>>& blocked, vector<int>& source, vector<int>& target) {
        
        map<int,int> x_map, y_map;
        vector<int> x_loc, y_loc;
        x_loc.push_back(source[0]);
        x_loc.push_back(target[0]);
        y_loc.push_back(source[1]);
        y_loc.push_back(target[1]);
        
        
        for(int i = 0;i < blocked.size(); ++i){
            x_loc.push_back(blocked[i][0]);
            y_loc.push_back(blocked[i][1]);
        }
        
        int idx_x = 1, idx_y = 1;
        
        sort(x_loc.begin(), x_loc.end());
        sort(y_loc.begin(), y_loc.end());
        
        
        int x_max = re_idx(x_loc, x_map);
        int y_max = re_idx(y_loc, y_map);
        
        bool vis[x_max + 1][y_max + 1];
        memset(vis, 0, sizeof(vis));
        
        source[0] = x_map[source[0]];
        target[0] = x_map[target[0]];
        
        source[1] = y_map[source[1]];
        target[1] = y_map[target[1]];
        
        for(int i = 0;i < blocked.size(); ++i){
            int x = x_map[blocked[i][0]];
            int y = y_map[blocked[i][1]];
            vis[x][y] = true;
        }
        
        int x_dir[] = {1, 0, -1, 0};
        int y_dir[] = {0, 1, 0, -1};
        
        cout<<source[0]<<source[1]<<endl;
        cout<<target[0]<<target[1]<<endl;
        cout<<x_max<<y_max<<endl;
        
        queue<pair<int,int>> Q;
        Q.push(make_pair(source[0], source[1]));
        vis[source[0]][source[1]] = true;
        while(!Q.empty()){
            pair<int,int> u = Q.front(); Q.pop();
            int x = u.first, y = u.second;
            
            if(x == target[0] && y == target[1]) return true;
            
            for(int i = 0;i < 4; ++i){
                int next_x = x + x_dir[i];
                int next_y = y + y_dir[i];
                if(next_x < 0 || next_y < 0 || next_x > x_max || next_y > y_max || vis[next_x][next_y]) continue;
                vis[next_x][next_y] = true;
                Q.push(make_pair(next_x, next_y));
            }
        }
        return false;
        
    }
};
  1. Longest Duplicate Substring

题意;给定一个字符串,求里面最长的重复子串(出现2次或以上)。

题解:
解法1,二分答案+Rabin-Carp就是二分长度,然后滚动hash判断是否重复。
解法2,问题实际上就是求所有后缀的最长公共前缀,可以用后缀数组做法
下面是后缀数组代码

class Solution {
    static const int maxn = 1e5 + 2;

    int wa[maxn], wb[maxn], wv[maxn], ws[maxn], sa[maxn], height[maxn], *rank;
    int cmp(int *r, int a, int b, int l, int n){
        if(r[a] != r[b]) return false;
        if(a + l >= n && b + l >= n) return true;
        if(a + l >= n || b + l >= n) return false;
        return r[a + l] == r[b + l];
    }

    //待排序的字符串放在r数组中,从r[0]到r[n-1],长度为n,且最大值小于m
    //sa是我们要求的后缀数组
    //这里x数组保存的值相当于是rank值。下面的操作只是用x数组来比较字符的大小,所以没有必要求出当前真实的rank值。所以这里x保存位置i的字符就好
    //后面的x才是rank值,x[i]=后缀i的名次
    int* cal_suffix_arr(int *r, int *sa, int n, int m){
        int *x = wa,*y = wb;
        memset(ws, 0, m * sizeof(int));
        //对第一个字符排序
        for(int i = 0;i < n; ++i) ++ws[x[i] = r[i]];
        for(int i = 1;i < m; ++i) ws[i] += ws[i - 1];
        for(int i = n - 1; i >=0; --i) sa[--ws[x[i]]] = i;
    
        
        //对长度为2^j的后缀排序
        for(int j = 1, p = 0; p < n; j*=2, m = p){
            p = 0;
            //接下来进行若干次基数排序,在实现的时候,这里有一个小优化。基数排序要分两次,第一次是对第二关键字排序,
            //第二次是对第一关键字排序。对第二关键字排序的结果实际上可以利用上一次求得的sa直接算出,没有必要再算一次。
            //数组y保存的是对第二关键字排序的结果,y[p]= 排名为p的为哪个后缀的第二关键字
            //因为从n - j开始后面的第二部分都是空串,所以排在前面
            for(int i = n - j; i < n; ++i) y[p++] = i;
            for(int i = 0; i < n; ++i) if(sa[i] >= j) y[p++] = sa[i] - j;
            //wv[i] = x[y[i]]是第二关键字排名为i的后缀的第一关键字排名
            //基数排序。按第二关键字排序后,第一关键字的原排名。用第一关键字分桶就得到了一二关键字的排序
            //ws是m个桶
            for(int i = 0; i < n; ++i) wv[i] = x[y[i]];
            for(int i = 0; i < m; ++i) ws[i] = 0;
            //分桶,排序
            for(int i = 0; i < n; ++i) ++ws[wv[i]]; //++ws[x[i]]也一样
            for(int i = 1; i < m; ++i) ws[i] += ws[i - 1];
            for(int i = n - 1; i >= 0; --i) sa[--ws[wv[i]]] = y[i];
            //求解新的rank数组x
            //根据sa求rank。sa[i]是排名为i的后缀,rank[sa[i]]后缀sa[i]的排名
            //因为这里的sa数组对于后缀相同时,排名按位置前后排,所以这里rank还需要判重
            //对于两个后缀sa[i - 1]和sa[i],他们第一关键字和第二关键字是否都一样,这个可以通过判断本来的rank在对应位置是否相同
            swap(x, y); x[sa[0]]=0; p = 1;
            for(int i = 1; i < n; ++i){
                x[sa[i]] = cmp(y, sa[i - 1], sa[i], j, n) ? p - 1: p++;
                
            }
        }
        return x;
    }


    void cal_height(int n, int *sa, int *rank, int *r, int *height) {
        // vector<int> rank(n);
        // for(int i = 0; i < n; ++i)rank[sa[i]] = i;

        int k = 0;
        for(int i = 0; i < n; ++i){
            int cur = rank[i];
            if(cur == 0) {
                height[cur] = 0;
                k = 0;
            } else {
                if(k > 0) --k;
                int j = sa[cur-1];
                while(i + k < n && j + k < n && r[i + k] == r[j + k]) ++k;
                height[cur] = k;
            }
        }
    }
    
public:
    string longestDupSubstring(string S) {
        int n = S.size(), m = 26;
        int *s = new int[n];
        for(int i = 0;i < n; ++i){
            s[i] = S[i] - 'a';
        }
        
        rank = cal_suffix_arr(s, sa, n, m);
        cal_height(n, sa, rank, s, height);
        int max_len = 0, pos = 0;
        
        
        for(int i = 1;i < n; ++i){
            if(height[i] > max_len){
                max_len = height[i];
                pos = sa[i];
            }
        }
        delete []s;
        return S.substr(pos, max_len);
        
        return "";
    }
};
  1. Number of Submatrices That Sum to Target

题意:给定一个至多300*300的整数矩阵,和一个target,问有多少子矩阵的和为target

题解:做过最大矩阵和的应该很容易想到,这个用前缀和以及HashMap可以使得复杂度为O(N^3)。
具体的就是先求矩阵前缀和。然后枚举上下边,变成一维问题。
也就是给定一个数组,求和为target的子段有多少?
有了前缀和,枚举每个位置i,前缀和为pre_sum[i],就是找前面有多少个j使得pre_sum[i] - pre_sum[j] = target
转化一下,就是求i前面的前缀和有多少个等于pre_sum[i] - target。这个可以用一个map来保存计数。

class Solution {
public:
    int numSubmatrixSumTarget(vector<vector<int>>& matrix, int target) {
        int n = matrix.size(), m = matrix[0].size();
        
        vector<vector<int>> pre_sum(n + 1, vector<int>(m + 1, 0));
        
        
        for(int i = 1;i <= n; ++i)
            for(int j = 1; j <= m; ++j)
                pre_sum[i][j] = pre_sum[i - 1][j] + pre_sum[i][j - 1] + matrix[i - 1][j - 1] - pre_sum[i - 1][j - 1];
         unordered_map<int,int> pre_sum_catch;
        
        int ans = 0;
        for(int i = 1; i <= n; ++i)
            for(int j = i; j <= n; ++j){
                pre_sum_catch.clear();
                pre_sum_catch[0] = 1;
                for(int k = 1; k <= m; ++k){
                    int p = pre_sum[j][k] - pre_sum[i - 1][k];
                    ans += pre_sum_catch[p - target];
                    ++pre_sum_catch[p];
                }
            }
        return ans;
    }
};
  1. Shortest Common Supersequence

题意:给定两个字符串,求一个最短的字符串能将这两个串作为子序列

题解:动态规划。dp[i][j]表示将str1的前缀i和str2的前缀j作为子序列的串的最短长度。
则当前串的最短长度的串,考虑最后一个字符,首先它必须等于str1[i]或者str2[j]
如果str1[i] == str2[j] 那么最后一个字符和他们都相等,则同时考虑它们两个不会产生更差的结果(就是贪心),转化为子问题,dp[i][j] = dp[i - 1][j - 1] + 1
如果不相等,那么考虑最后一个字符的两种情况取最优。

class Solution {
public:
    string shortestCommonSupersequence(string str1, string str2) {
        int n = str1.length(), m = str2.length();
        int dp[n + 1][m + 1];
        
        //init
        for(int i = 0;i <= n; ++i) dp[i][0] = i;
        for(int j = 0;j <= m; ++j) dp[0][j] = j;
        
        
        for(int i = 1;i <= n; ++i){
            for(int j = 1;j <= m; ++j){
                if(str1[i - 1] == str2[j - 1])
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                else
                    dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1;
            }
        }
        
        vector<char> s;
        int cur_i = n, cur_j = m;

        //回溯答案        
        for(int k = 0; k < dp[n][m]; ++k){
            if(cur_i > 0 && cur_j > 0 && str1[cur_i - 1] == str2[cur_j - 1]){
                s.push_back(str1[cur_i - 1]);
                --cur_i; --cur_j;
            }else{
                if(cur_i > 0 && dp[cur_i - 1][cur_j] + 1 == dp[cur_i][cur_j]){
                    s.push_back(str1[cur_i - 1]);
                    --cur_i;
                }else{
                    s.push_back(str2[cur_j - 1]);
                    --cur_j;
                }
            }
        }
         
        return string(s.rbegin(), s.rend());
        
    }
};
  1. Find in Mountain Array

题意:交互题。一个数组称为MountainArray,如果它的长度至少位3,且存在一个0<i<len - 1使得0到i递增,i到len - 1递减

题解:不能直接操作数组,要通过一个类获取第k个值。先找出最大值所在的下标。通过二分搜索得到,然后先搜索前半段,再搜索后半段即可。

/**
 * // This is the MountainArray's API interface.
 * // You should not implement it, or speculate about its implementation
 * class MountainArray {
 *   public:
 *     int get(int index);
 *     int length();
 * };
 */
class Solution {
public:
    int findInMountainArray(int target, MountainArray &mountainArr) {
        int n = mountainArr.length();
        
        int left = 0, right = n - 1, peak_idx;
        
        while(left <= right){
            int mid = (left + right) / 2;
            int mid_num = mountainArr.get(mid);
            
            if(mountainArr.get(mid + 1) < mid_num){
                right = mid - 1;
            }else
                left = mid + 1;
        }
        
        peak_idx = right + 1;
        left = 0; right = peak_idx;

        while(left <= right){
            int mid = (left + right) / 2;

            int mid_num = mountainArr.get(mid);
            if(mid_num <= target){
                left = mid + 1;
            }else
                right = mid - 1;
        }
        
        if(left > 0 && mountainArr.get(--left) == target) return left;
        
        left = peak_idx + 1; right = n - 1;
        
        while(left <= right){
            int mid = (left + right) / 2;
            int mid_num = mountainArr.get(mid);
            if(mid_num >= target){
                left = mid + 1;
            }else
                right = mid - 1;
        }
        
        if(mountainArr.get(--left) == target) return left;
        return -1;
    }
};
  1. Brace Expansion II

题意:给定一个表达式,形式如 “{{a,z},a{b,c},{ab,z}}”,其中大括号表示可选内容。{a,z}表示a或者z,a{b,c}表示ab或者ac。所以前面的串能表示的有[“a”,“ab”,“ac”,“z”]

给定一个大括号表达式, 问这个串能表示哪些串,按字典序返回。

题解:采用递归,分解成子问题解决。比较难搞的一个模拟题。写的比较挫。
首先如果整个串被一个大括号包含,那么答案就是每个逗号分隔部分答案的并
如果不被括号包围,那么就是各部分答案的笛卡尔积

class Solution {
    //判断是否被一个大括号包围整个串
    bool check_split(string expression){
        int brace_num = 0;
        if(*expression.rbegin() != '}' or *expression.begin() != '{') return false;
        
        for(int i = 0;i < expression.length() - 1; ++i){
            if(expression[i] == '{') ++brace_num;
            else if(expression[i] == '}') --brace_num;

            if(brace_num == 0) return false;
        }
        return true;
    }
    
    //将串按照逗号分隔为子串
    vector<string> split(string expression){
        vector<string> vs;
        
        for(int i = 1; i < expression.size() - 1; ++i){
            int j = i + 1;
            int brace_num = (expression[i] == '{');
            
            while(j < expression.size() - 1 and (expression[j] != ',' or brace_num != 0)){
                switch(expression[j++]){
                    case '}': --brace_num; break;
                    case '{': ++brace_num; break;
                }
            }
            vs.push_back(expression.substr(i, j - i));
            
            i = j;
        }
        return vs;
    }
    //计算笛卡尔积
    vector<string> merge_strs(vector<vector<string>> &vs){
        vector<string> v(1,"");
        
        for(int i = 0;i < vs.size(); ++i){
            vector<string> t;
            for(int j = 0;j < v.size(); ++j){
                for(int k = 0;k < vs[i].size(); ++k){
                    t.push_back(v[j] + vs[i][k]);
                }
            }
            v.swap(t);
        }
        sort_and_remove_duplicate(v);
        return v;
    }
    //排序,去重
    void sort_and_remove_duplicate(vector<string> &v){
        sort(v.begin(), v.end());
        int pos = 0;
        for(int i = 0;i < v.size(); ++i){
            if(i < v.size() - 1 and v[i] == v[i + 1] ) continue;
            v[pos++] = v[i];
        }
        v.resize(pos);
    }
    
    
public:
    vector<string> braceExpansionII(string expression) {
        
        vector<string> vs;
        string substr = "";
        int brace_count = 0;
       //分两种情况
        if(check_split(expression)){
            vector<string> strs = split(expression);
            
            for(int i = 0;i < strs.size(); ++i){
                vector<string> t = braceExpansionII(strs[i]);
                vs.insert(vs.end(), t.begin(), t.end());
            }
            
            sort_and_remove_duplicate(vs);
            return vs;
        }
        
        vector<vector<string> >substrs;
        for(int i = 0;i < expression.size(); ++i){
            int j = i + 1;
            if(expression[i] == '{'){
                int brace_num = 0;
                
                while(expression[j] != '}' or brace_num != 0){
                    switch(expression[j++]){
                        case '}': --brace_num; break;
                        case '{': ++brace_num; break;
                    }
                }
                substrs.push_back(braceExpansionII(expression.substr(i, j - i + 1)));
                
            //字母
            }else{
                j = i;
                while(j + 1 < expression.size() and expression[j + 1] != '{') ++j;
                substrs.push_back(vector<string>(1, expression.substr(i, j - i + 1)));

            }
            i = j;
        }
        
        return merge_strs(substrs);
        
    }
};
  1. Parsing A Boolean Expression

题意:给定一个真值表达式,包函数字符{’(’, ‘)’, ‘&’, ‘|’, ‘!’, ‘t’, ‘f’, ‘,’},求表达式的真值。
例如
Input: expression = “|(&(t,f,t),!(t))”
Output: false
题解:采用栈的方式进行计算,遇到有括号时,计算栈顶部分的表达式,所有表达式都计算完。因为没有空格,且都是合法的表达式,所以比较简单。

class Solution {
    
    
    bool is_opt(char e){
        return e == '&' || e == '|' || e == '!' || e == ' ';
    }
    
public:
    bool parseBoolExpr(string expression) {
        stack<char> s;
        stack<bool> val;
        char opt;
        bool cur_val;
        
        for(int i = 0;i < expression.size(); ++i){
            //碰到运算符,因为不存在空格,所以直接可以continue了
            //而且表达式都有括号,所以在遇到左括号时push,不然的话在遇到运算符就可push了
            if(is_opt(expression[i]) || expression[i] == ',') continue;
            //非运算符
            
            //左括号
            if(expression[i] == '('){
                if(i > 0 && is_opt(expression[i - 1])) s.push(expression[i - 1]);
                else s.push(' ');
            //右括号
            }else if(expression[i] == ')'){
                while(!is_opt(s.top())){
                    val.push(s.top() == 't');
                    s.pop();
                }
                opt = s.top(); s.pop();
                cur_val = (opt == '&');
                    
                while(!val.empty()){
                    switch(opt){
                        case ' ' : cur_val = val.top(); break;
                        case '|': cur_val |= val.top(); break;
                        case '&': cur_val &= val.top(); break;
                        case '!': cur_val = !val.top(); break;
                    }
                    val.pop();
                }
                if(cur_val) s.push('t');
                else s.push('f');
            // expression[i] == 't'或者'f'
            }else{
                s.push(expression[i]);
            }   
        }   
        return s.top() == 't';
        
    }
};
  1. Longest Chunked Palindrome Decomposition

题意:给定一个字符串text。问最多能把它分成多少个子串,text=(a_1, a_2,…,a_k)使得a_i = a_{k + 1 - i}

题解:贪心。贪心做分割。假设前缀a能和后缀a匹配,如果存在一个前缀包含a,假设为a b,能和后缀匹配。那么在a这里分割比在b处分割更好。
证明: 设前缀[a, c, b] 和后缀[a, a, c] 或者[a,c,a]匹配(分别对应的情况是[a c] 和末尾的[a c]或者[a]和末尾的[a]匹配,其中len(a) == len(b)),(中括号表示拼接,c可以为空串)。

  • 情形1:[ a , c , b ] [a,c,b][a,c,b][ a , a , c ] [a,a,c][a,a,c]匹配。则有[ c , b ] = = [ a , c ] [c,b] == [a,c][c,b]==[a,c]
    将c表示成[ c 1 , c 2 , . . . , c 0 ] [c_1,c_2,...,c_0][c1,c2,...,c0]其中l e n ( c i ) = = l e n ( a ) , l e n ( c 0 ) < l e n ( a ) len(c_i) == len(a), len(c_0) < len(a)len(ci)==len(a),len(c0)<len(a)
    那么由[ c , b ] = [ a , c ] [c,b] = [a,c][c,b]=[a,c] 得到 [ c 1 , . . . , c n , c 0 , b ] = [ a , c 1 , . . . , c n , c 0 ] [c_1,...,c_n,c_0,b] = [a,c_1,...,c_n,c_0][c1,...,cn,c0,b]=[a,c1,...,cn,c0]
    即有c 1 = c 2 = . . . = c n = a , 且 [ c 0 , b ] = [ a , c 0 ] c_1 = c_2 =...=c_n = a,且[c_0, b] = [a, c_0]c1=c2=...=cn=a[c0,b]=[a,c0]
    a aa表示成a = [ d , e ] , l e n ( d ) = l e n ( c 0 ) a = [d, e],len(d) = len(c_0)a=[d,e]len(d)=len(c0),则上面等式表示[ c 0 , b ] = [ d , e , c 0 ] [c_0, b] = [d, e, c_0][c0,b]=[d,e,c0]
    我们得到d = c 0 , a = [ c 0 , e ] , b = [ e , c 0 ] d = c_0,a = [c_0, e], b = [e, c_0]d=c0,a=[c0,e],b=[e,c0]
    代回原串[ a , c , b ] [a,c,b][a,c,b]得到[ a , c , b ] = [ ( c 0 , e ) n + 1 , c 0 ] [a,c,b] = [(c_0,e)^{n + 1}, c_0][a,c,b]=[(c0,e)n+1,c0]
    显然这个串可以进一步分割得到更优的分割方案
  • 情形2:前缀[ a , c , b ] [a, c, b][a,c,b]和后缀 [ a , c , a ] [a,c,a][a,c,a]匹配。得到a = = b a == ba==b。[a,c,a]可以分割成三段[a] [c] [a] 比一段好。
class Solution {
public:
    int longestDecomposition(string text) {
        deque<char> left;
        deque<char> right;

        int maxSplits = 0;
        const int textSize = text.size();
        int i = 0;
        int j = textSize - 1;

        while (i < j) { 
            left.push_back(text[i++]);
            right.push_front(text[j--]);
            //这里可以用hash比较是否相同,会更快一点
            if (left == right) {
                maxSplits += 2;
                left.clear();
                right.clear();
            }
        }
        //剩下多于一个字符不能成对或者剩下一个字符
        if (!left.empty() || i == j) {
            ++maxSplits;
        }
        return maxSplits;
    }
};
  1. Smallest Sufficient Team

题意:给定一个req_skills字符串数组表示团队的必备技能。people是一个数组,people[i]表示第i个人的技能,是个字符串数组。问最少是哪些人可以组成一个团队(每个技能都有人会)

题解:先把技能用位表示成状态对应位为1表示技能已具备。设dp[i][j]表示选第i个人,前i个人达到状态为j时的最少人数。就可以用递推解决。最小的人数就是所有k中dp[k][(1 <<n ) - 1]最小值。然后回溯得到答案。
递推方程为d p [ i ] [ j ∣ s ] = min ⁡ k < i ( d p [ i ] [ j ∣ s ] , d p [ k ] [ j ] + 1 ) dp[i][j|s] = \min_{k < i}(dp[i][j|s], dp[k][j] + 1)dp[i][js]=k<imin(dp[i][js],dp[k][j]+1)
s为第i个人的技能状态。
观察到我们计算dp[i][j|s]时,只需要最小的dp[k][j]即可,并不需要所有的k。
所以用一个state_catch[j]表示状态为j时的最少人数。递推方程变成
d p [ i ] [ j ∣ s ] = min ⁡ ( d p [ i ] [ j ∣ s ] , s t a t e c a t c h [ j ] + 1 ) dp[i][j|s] = \min(dp[i][j|s], state_catch[j] + 1)dp[i][js]=min(dp[i][js],statecatch[j]+1)
接着注意到,如果用一个数据people_idx表示达到状态时最少人数的最后一个人的id,那么我们就不需要保存dp值了。回溯的时候根据state_catch 和people_idx就能得到答案。
这样就对空间和时间都进行了一定的优化。

class Solution {
    
public:
    vector<int> smallestSufficientTeam(vector<string>& req_skills, vector<vector<string>>& people) {
        unordered_map<string, int> skill2bit;
        int state_catch[1<<17], people_idx[1<<17];
        
        int n = req_skills.size();
        
        vector<int> people_skill(people.size());
        
        for(int i = 0;i < n; ++i){
            skill2bit[req_skills[i]] = 1 << i;
        }
        
        memset(state_catch, -1, sizeof(state_catch));
        
        for(int i = 0;i < people.size(); ++i){
            int cur_skill = 0;
            for(int j = 0;j < people[i].size(); ++j)
                cur_skill |= skill2bit[people[i][j]];
            people_skill[i] = cur_skill;
        }
        
        state_catch[0] = 0;
        
        for(int i = 1;i <= people_skill.size(); ++i){
            int cur_skill = people_skill[i - 1];
            for(int j = (1 << n) - 1; j >= 0; --j){
                if(state_catch[j] == -1) continue;
                int next_skill = j | cur_skill;
                int &sk = state_catch[next_skill];
                
                int dp = sk == -1? state_catch[j] + 1 : min(sk, state_catch[j] + 1);
                
                if(sk == -1 || dp < sk){
                    sk = dp;
                    people_idx[next_skill] = i - 1;
                }
            }
        }
        
        int min_num = state_catch[(1<<n) - 1];
        int cur_skill = (1 << n) - 1;
        vector<int> ans;
        
        while(min_num){
        	//枚举所有包含cur_skill的状态,
            for(int j = cur_skill; j < (1 << n); ++j){
                if((j & cur_skill) != cur_skill) continue;
                if(min_num == state_catch[j]){
                    ans.push_back(people_idx[j]);
                    //计算剩下还需要的技能
                    cur_skill &= ((1 << n) - 1) ^ people_skill[people_idx[j]];
                    break;
                }
            }
            --min_num;
        }
        return ans;
    }
};
  1. Online Majority Element In Subarray

题意:对一个数组(1到20000)进行查询,每次查询一个子数组区间[left, right],给定区间和一个threshold,返回区间中出现次数超过threshold的整数。其中threshold * 2 > right - left + 1。即超过半数。

题解:比较容易想到的是线段树的做法。每次查询是O(logn) 构建一颗线段树,节点保存的是区间超过半数的值。
某个值在某个区间出现的次数,我们可以用一个numbers来存每个数的下标,然后二分就得到了。
查询的时候,左右子树中各返回一个候选值。两个值取出现次数大于threshold的就可以了。

不过还有更多更好的方法:猫树1猫树2RMQ快速查询算法

class MajorityChecker {
    static const int MAXN = 20001;
    vector<int> a;
    vector<int> numbers[MAXN];
    int n;
    int segment_tree[4 * MAXN];
    
    int get_num(int val, int left, int right){
        vector<int> &val_vec = numbers[val];
        return upper_bound(val_vec.begin(), val_vec.end(), right) - lower_bound(val_vec.begin(), val_vec.end(), left);
    }
    int build_tree(int node, int left, int right){
        if(left == right){
            segment_tree[node] = a[left];
        }else{
            int mid = (left + right) >> 1;
            
            int l_val = build_tree(node << 1, left, mid);
            int r_val = build_tree((node << 1) | 1, mid + 1, right);
            
            int interval_size = right - left + 1;
            if(l_val > 0 and 2 * get_num(l_val, left, right) > interval_size)
                segment_tree[node] = l_val;
            else if(r_val > 0 and 2 * get_num(r_val, left, right) > interval_size)
                segment_tree[node] = r_val;
        }
        return segment_tree[node];
    }
    
    int mode_query(int node, int int_left, int int_right, int q_left, int q_right, const int &threshold){
        if(int_right < q_left or int_left > q_right) return 0;
        if(q_left <= int_left and int_right <= q_right) return segment_tree[node];
        
        int mid = (int_left + int_right) >> 1;
        int l_val = mode_query(node << 1, int_left, mid, q_left, q_right, threshold);
        int r_val = mode_query((node << 1) | 1, mid + 1, int_right, q_left, q_right, threshold);
        
        if(l_val > 0 and get_num(l_val, q_left, q_right) >= threshold)
            return l_val;
        else if(r_val > 0 and get_num(r_val, q_left, q_right) >= threshold)
            return r_val;
        return 0;
    }
    
public:
    MajorityChecker(vector<int>& arr) {
        this->a.swap(arr);
        
        n = a.size();
        for(int i = 0;i < n; ++i){
            numbers[a[i]].push_back(i);
        }
        
        build_tree(1, 0, n - 1);
        
    }
    
    int query(int left, int right, int threshold) {
        int mode = mode_query(1, 0, n - 1, left, right, threshold);
        if(mode > 0 and get_num(mode, left, right) >= threshold)
            return mode;
        else
            return -1;
    }
};

/**
 * Your MajorityChecker object will be instantiated and called as such:
 * MajorityChecker* obj = new MajorityChecker(arr);
 * int param_1 = obj->query(left,right,threshold);
 */
  1. Last Substring in Lexicographical Order

题意:给定一个字符串,输出最大字典序的子串

题解:首先字典序最大的一定是某个后缀。可以用后缀数组,但是显得有点杀鸡用牛刀。有更好的办法。我们可以从前往后扫一遍,避免一些冗余计算,可以让时间复杂度为O(n)。

我们设i位置之前的最大字典序后缀的下标是maxidx。
如果s[i] > s[maxidx],则到i这里,最大字典序是后缀i
如果s[i] < s[maxidx],则到i这里,最大字典序依然是后缀maxidx
如果s[i] == s[maxidx],则i后缀有可能比后缀maxidx大。这时候往后继续比较
也就是先比较子串s[maxidx,i] 和s[i, 2i - maxidx] (不包含后面那个字符)。

  • 如果不相等,那么后缀就可以确定哪个大。并且i + 1 到2i - maxidx 的后缀都不可能比它大(子串已经比较过了,如果有更大的,那么maxidx应该往后移,不可能是当前位置)
    假设从max_idx开始到i的串为[s c] ,i到 2i-maxidx的串同样为[s c] 而2i-maxidx后面的为d,即max_idx开始的后缀表示为 [s c][s c][d]
    如果i到2i - maxidx中有某个位置的后缀更大,也就是上面从c开始的后缀。那么有 [c d] > [c s c d],即有[c d] > [c s] 推出[c > s]
    这和maxidx的定义矛盾。i前面比s开始后缀大的还有以c开始的后缀
  • 如果相等,和上面一样。将串表示成[s][s][d],则要么[s s d]是最大的,要么[d]更大,不可能是[s d]最大。因为如果[s d] > [s s d],则有[d] > [s]
class Solution {
public:
    string lastSubstring(string s) {
        int maxidx = 0 , i = 1 , n = s.size();
        while(i < s.size())
        {
            if(s[i] > s[maxidx])
            {
                maxidx = i;
            }
            else if(s[i] == s[maxidx]){
                int curr = i , j = maxidx;
                while(i < n && j < curr && s[i] == s[j]){
                    ++i; ++j
                }
                if(i >= n || j >= curr ||  s[j] > s[i])
                    continue;
                else
                    maxidx = curr;
                continue;
            }
            i++;
        }
        return s.substr(maxidx);
    }
};
  1. Dinner Plate Stacks

题意:设计一个结构它由无穷个栈组成,每个的容量为capacity。 支持三种操作。

  • push:找到最前面未满的栈,往栈顶添加元素
  • pop:找到最末非空的栈,pop出栈顶
  • popAtStack,指定下标的栈进行pop操作

题解:可以使用双指针找对应位置。下面是用两个堆维护插入和删除栈下标。一个大顶堆,一个小顶堆。虽然渐进时间复杂度比较低,但是常数比较大,所以还是比较耗时的,比不上双指针。

class DinnerPlates {
    int cap;
    int tot_num;
    //可以初始化为一个较大的容量,但是这里采用动态增长。存所有栈
    vector<stack<int>> stacks;
    //小顶堆,存那些没有满的stack下标。堆顶是下标最小的
    priority_queue <int, vector<int>, greater<int>> unfull_Q;
    //大顶堆,存那些非空的stack下标。堆顶是最大非空栈下标
    set<int, greater<int>> nonempty_Q;
    
    
public:
    DinnerPlates(int capacity) {
        cap = capacity;
        tot_num = 0;
    }
    
    void push(int val) {
        if(stacks.size() * cap <= tot_num){
            stacks.push_back(stack<int>());
            unfull_Q.push(stacks.size() - 1);
        }
        int stack_idx = unfull_Q.top(); 
        stacks[stack_idx].push(val);
        if(stacks[stack_idx].size() == 1) nonempty_Q.insert(stack_idx);
        if(stacks[unfull_Q.top()].size() == this -> cap) unfull_Q.pop();
        
        ++tot_num;
    }
    
    int pop() {
        if(tot_num == 0) return -1;
        int stack_idx = *nonempty_Q.begin();
        //while(stacks[stack_idx].empty()){
        //     nonempty_Q.erase(nonempty_Q.begin());
        //     stack_idx = *nonempty_Q.begin();
        // }
        int r = stacks[stack_idx].top();
        if(stacks[stack_idx].size() == this->cap) unfull_Q.push(stack_idx);
        stacks[stack_idx].pop();
        if(stacks[stack_idx].size() == 0) nonempty_Q.erase(nonempty_Q.begin()); 
        
        --tot_num;
        
        return r;
    }
    
    int popAtStack(int index) {
        if(index >= stacks.size() ||  stacks[index].empty()) return -1;
        int r = stacks[index].top(); stacks[index].pop();
        if(stacks[index].size() == this->cap - 1) unfull_Q.push(index);
        if(stacks[index].size() == 0) nonempty_Q.erase(index);
        
        --tot_num;
        return r;
    }
};

/**
 * Your DinnerPlates object will be instantiated and called as such:
 * DinnerPlates* obj = new DinnerPlates(capacity);
 * obj->push(val);
 * int param_2 = obj->pop();
 * int param_3 = obj->popAtStack(index);
 */
  1. Number of Valid Words for Each Puzzle

题意:给定两个字符串数组words,和puzzles。puzzles是一些谜面,words是一些谜底。一个谜面的谜底必须满足

  • 包含谜面的第一个字符
  • 所有字符都在谜面中出现

对每个谜面puzzles[i],我们要计算words中有多少个谜底可以和它对应。

题解:用位表示表示每个字符串,然后words用一个map存起来就可以进行计数了。对于每个puzzels[i],枚举可能的字符子集然后在map获取对应word的数目。

class Solution {
public:
	//字符串转换为位表示
    int bit_rep(string s){
        int r = 0;
        for(int i = 0;i < s.size(); ++i){
            r |= 1 << (s[i] - 'a');
        }
        return r;
    }
    //计算每个puzzles[i]对应的word数目
    int num_count(int r, int first_char, map<int,int> &word_num){
        if(!r) return 0;
        vector<int> bit_pos;
        int pos = 0;
        while((1 << pos) <= r){
            if(r & (1 << pos)) bit_pos.push_back(pos);
            ++pos;
        }
        
        int num = 0;
        for(int i = 0;i < (1 << bit_pos.size()); ++i){
            
            r = 0;
            for(int j = 0;j < bit_pos.size(); ++j){
                if(((1 << j) & i) == 0) continue;
                r |= 1 << bit_pos[j];
            }
            
            if((r & (1 << first_char)) == 0) continue;
            num += word_num[r];
        }
        
        return num;
    }

    vector<int> findNumOfValidWords(vector<string>& words, vector<string>& puzzles) {
    	//用一个map保存二进制表示的word的数目
        map<int,int> word_num;
        vector<int> ans;
        for(int i = 0;i < words.size(); ++i){
            int r = bit_rep(words[i]);
            ++word_num[r];
        }
        
        for(int i = 0;i < puzzles.size(); ++i){
            int puzzle_r = bit_rep(puzzles[i]);
            int first_char = puzzles[i][0] - 'a';
            ans.push_back(num_count(puzzle_r, first_char, word_num));
            
        }
        return ans;
    }
};
  1. Make Array Strictly Increasing

题意:给定两个数组arr1和arr2。每次可以进行一个操作,任选arr1的下标i和arr2的下标j,令arr1[i] = arr2[j]。问最少多少次操作后,arr1为递增序列。如果不能使得arr1为递增序列,返回-1

题解:容易想到的两种动态规划
dp[i][j]表示前i个数,进行j次赋值,第i个数的最小值是多少。转移方程由当前i赋值或者不赋值两种情况取最小。
如果当前不赋值,则最小值是arr1[i]
如果当前赋值,则最小值是比dp[i - 1][j - 1]大的最小值(这可以通过预处理或者保存下标解决)。
下面采用另一种动态规划,dp[i][j]表示,数组1的位置i赋值为数组2的j位置时,arr1[i] = arr2[j],前i个(0到i)数保持递增的最小赋值次数。dp[i][m]表示不赋值

class Solution {
public:
    int makeArrayIncreasing(vector<int>& arr1, vector<int>& arr2) {
        int n = arr1.size();
        int m = arr2.size();
        //dp[i][j]表示arr1第i个数赋值为arr2第j个数时,前i个数为递增列的最少赋值次数
        int dp[n + 1][m + 1];
        //保存arr2中比arr1[i]小的最大数的下标。arr2先去重
        int idx[n];
        memset(dp, 0x3f, sizeof(dp));
        dp[0][m] = 0;
        for(int i = 0;i < m; ++i) dp[0][i] = 1;
        
        sort(arr2.begin(), arr2.end());
        
        //去重
        m = 0;
        for(int i = 0;i < arr2.size(); ++i){
            if(i > 0 && arr2[i] == arr2[i - 1]) continue;
            arr2[m++] = arr2[i];
        }
        arr2.resize(m);

       	//计算比每个arr1[i]小的最大的arr2[j]的下标j
        for(int i = 0;i < n; ++i){
            idx[i] = lower_bound(arr2.begin(), arr2.end(), arr1[i]) - arr2.begin() - 1;
        }
        
        for(int i = 1;i < n; ++i){
            //当前值不变
            if(arr1[i] > arr1[i - 1])
                dp[i][m] = dp[i - 1][m];
            
            if(idx[i] != -1){
                dp[i][m] = min(dp[i][m], dp[i - 1][idx[i]]);
            }
            //当前值变为arr2[j]
            for(int j = 0; j < m; ++j){
                if(arr2[j] > arr1[i - 1]) dp[i][j] = dp[i - 1][m] + 1;
                if(j > 0) dp[i][j] = min(dp[i][j], dp[i - 1][j - 1] + 1);
            }
        }
        
        int ans = m + 1;
        for(int i = 0;i <= m; ++i) ans = min(ans, dp[n - 1][i]);
        
        return ans > m ? -1 : ans;
    }
};
  1. Critical Connections in a Network
    题意:给定一个图,connects保存的是边。问哪些边是重要的。即删除该边后,图不连通。
    题解:双联通分量模板题。边u->v是critical的,当且仅当,u到v只有一条简单路径。双联通分量就是,一个连通分量中的任意两点都存在大于等于2条简单路径。求双联通分量的办法和求连通分量基本一致。将无向边当做有向边处理(按dfs遍历顺序确定方向即可),且一条边只能走一次(加一个father防止多走一次)。然后用求强联通分量的方法Tarjan算法即可。
class Solution {
    static const int MAXN = 1e5+5;
    int n,m;
    int dfn[MAXN],low[MAXN],vis[MAXN],color[MAXN];
    int color_num, tot;
    stack<int> s;
    vector<int> graph[MAXN];
    
    void init(int n){
        tot = 0;
        color_num = 0;
        memset(vis, 0, sizeof(vis));
        memset(dfn, 0, sizeof(dfn));
        memset(low, 0, sizeof(low));
        memset(color, 0, sizeof(color));
        stack<int>().swap(s);
        for(int i = 0;i <= n; ++i) graph[i].clear();
    }
    
    void Tarjan(int now,int fa) {
        dfn[now]=low[now] = ++tot;
        vis[now] = true;
        s.push(now);
        
        for (int i = 0;i < graph[now].size(); ++i) {
            int u = graph[now][i];
            //点还没有遍历过
            if(dfn[u] == 0)  
                Tarjan(u, now),low[now] = min(low[now], low[u]);
            //点遍历过的,并且还在栈中
            else if(vis[u] && u != fa) 
                low[now] = min(low[now], dfn[u]);
        }
        //找到一个双联通分量
        if(dfn[now] == low[now]) {
            int top;
            ++color_num;
            do{
                top=s.top();    
                color[top] = color_num;
                vis[top]=0; 
                s.pop();
            }while(top != now);
        }
    }
    
    
public:
    //有重边的下,要处理一下重边
    vector<vector<int>> criticalConnections(int n, vector<vector<int>>& connections) {
        init(n);
        //建图
        for(size_t i = 0;i < connections.size(); ++i){
            graph[connections[i][0]].push_back(connections[i][1]);
            graph[connections[i][1]].push_back(connections[i][0]);
        }
        
        for(int i = 0; i< n; ++i){
            if(dfn[i]) continue;
            Tarjan(i, -1);
        }
        vector<vector<int> > ans;
        for(size_t i = 0;i < connections.size(); ++i){
            if(color[connections[i][0]] != color[connections[i][1]]) ans.push_back(connections[i]);
        }
        
        return ans;       
        
    }
};
  1. Design Skiplist

题意:设一一个跳表

题解:见代码,易懂。每个节点保存右方和下方的地址。增加一个虚拟表头,使得实现更方便。它还有可以再优化的地方,就是我们可以提前计算出当前插入的节点网上插入多少层,这样查找的时候就可以顺便插入,不用用一个vector保存所有可能插入点。

代码来自讨论区,有个bug,没有删除节点,会有内存泄漏

struct Node {
    Node *right, *down;
    int val;
    Node(Node *right, Node *down, int val): right(right), down(down), val(val) {}
};

class Skiplist {
    Node* head;
    
public:
    Skiplist() { head = new Node(NULL, NULL, 0); }
    
    bool search(int num) {
        Node *p = head;
        while(p) {
            //先往右
            while(p -> right and p -> right -> val < num) p = p -> right;
            //不能往右走就往下
            if(!p -> right or p -> right -> val > num) p = p -> down;
            else return true;
        }
        return false;
    }
    
    void add(int num) {
        insertPoints.clear();
        Node *p = head;
        while(p) {
            while(p -> right and p -> right -> val < num) p = p -> right;
            insertPoints.push_back(p);
            p = p -> down;
        }
        
        Node* downNode = NULL;
        bool insertUp = true;
        while(insertUp and insertPoints.size()) {
            Node *ins = insertPoints.back();
            insertPoints.pop_back();
            
            ins -> right = new Node(ins -> right, downNode, num);
            downNode = ins -> right;
            
            insertUp = (rand() & 1) == 0;
        }
        //增加一层
        if(insertUp) {
            return NULL;
            head = new Node(new Node(NULL, downNode, num), head, 0);
        }
    }
    vector<Node*> insertPoints;hb
    
    bool erase(int num) {
        Node *p = head;
        bool seen = false;
        
        while(p) {
            while(p -> right and p -> right -> val < num) p = p -> right;
            if(!p -> right or p -> right -> val > num) p = p -> down;
            else {
                //找到一个p->right->val == num,则
                seen = true;
                Node *tmp = p -> right;
                p -> right = p -> right -> right;
                p = p -> down;
                delete tmp;
            }
        }
        return seen;
    }
};

/**
 * Your Skiplist object will be instantiated and called as such:
 * Skiplist* obj = new Skiplist();
 * bool param_1 = obj->search(target);
 * obj->add(num);
 * bool param_3 = obj->erase(num);
 */
  1. Minimum Moves to Reach Target with Rotations

题意:有一个n*n的网格。有一条蛇,占用两个格,从位置(0,0)和(0,1)位置,往右下方走,问走到右下角(n - 1,n - 2), (n - 1, n - 1)至少多少步,不能通过则返回-1?
每一步,蛇能按如下规则移动:

  • 不能移动到格子为1的地方,不能超出方格范围
  • 向右移动,整体向右平移一格(蛇可以是横向的,或者纵向的)
  • 向下移动,整体向下平移一格
  • 顺时针旋转,如果当前蛇在位置(i,j)和(i,j + 1),且以(i,j)为左上角的2*2方格中的数为0。则蛇可以旋转为(i,j)和(i + 1,j)位置
  • 逆时针旋转,蛇是纵向的,4个格子为0,旋转为横向的。
    如下图所示
    在这里插入图片描述

题解:动态规划。设dp[i][j][k]表示到达蛇尾在位置i,j,面向为k的(k为0表示水平,1是竖直)。代码中,为了

class Solution {
public:
    int minimumMoves(vector<vector<int>>& grid) {
        int n = grid.size();
        if(grid[0][0] != 0 || grid[0][1] != 0 || grid[n - 1][n - 2] != 0 || grid[n - 1][n - 1] != 0) return -1;
        
        int dp[n + 1][n + 1][2];
        //初始化成一个大的值
        memset(dp, 0x3f ,sizeof(dp));

        dp[0][0][0] = 0;
        
        for(int i = 0;i < n ; ++i){
            for(int j = 0;j < n - 1; ++j){
                if(grid[i][j] != 0) continue;
                
                //两种平移
                if(grid[i][j + 1] == 0)
                    dp[i][j][0] = min(dp[i][max(j - 1, 0)][0], dp[max(i - 1, 0)][j][0]) + 1;
                
                if(i < n - 1 && grid[i + 1][j] == 0)
                    dp[i][j][1] = min(dp[max(i - 1, 0)][j][1], dp[i][max(j - 1, 0)][1]) + 1;
                
                //左下角不为0,则不能旋转
                if(i >= n - 1 || grid[i + 1][j + 1] != 0) continue;
                
                //两种旋转
                if(i < n - 1 && grid[i + 1][j] == 0)
                    dp[i][j][1] = min(dp[i][j][1], dp[i][j][0] + 1);
                
                if(grid[i][j + 1] == 0)
                    dp[i][j][0] = min(dp[i][j][0], dp[i][j][1] + 1);
            } 
        }
        
        return dp[n - 1][n - 2][0] >= 0x3f3f3f3f? -1 : dp[n - 1][n - 2][0] - 1;
    }
};
  1. Sort Items by Groups Respecting Dependencies

题意:有n个项目和m个小组(都是从0开始)。每个项目要么没有归属group[i] = -1,要么归属为group[i]。
你需要安排这些项目的次序,返回排序后的项目,满足以下条件

  • 同一个组的项目,彼此相邻
  • 项目之间有一个依赖关系。要执行第i个项目必须先完成的项目为beforeItems[i](一个列表)
    返回任意一个符合的结果,如果没有,返回空列表

题解:拓扑排序。使用两层的拓扑排序。建立一个group间的依赖关系图(-1的组每个元素单独成一个组即可),以及一个组内项目的依赖关系图。然后进行拓扑排序。先按group排,对每个group,对组内的项目进行排序。没有可行方案当且仅当两个图中有环,拓扑排序不能排完所有元素。

class Solution {
public:
    vector<int> sortItems(int n, int m, vector<int>& group, vector<vector<int> >& beforeItems) {
        vector<vector<int> > group_graph(n + 1, vector<int>()), item_graph(n + 1, vector<int>());
        vector<int> item_degree(n + 1, 0);

        int group_idx = m;

        for(int i = 0;i < group.size(); ++i)
            if(group[i] == -1) group[i] = group_idx++;
		//保存每个group的元素,后面遍历使用
        vector<vector<int> > group2items(group_idx, vector<int>());
        vector<int> group_degree(group_idx, 0);

        //建图
        for(int i = 0;i < beforeItems.size(); ++i){
            int i_group = group[i];

            for(int j = 0;j < beforeItems[i].size(); ++j){
                int u = beforeItems[i][j];
                int j_group = group[u];

                if(i_group == j_group){//item graph
                        item_graph[u].push_back(i);
                        ++item_degree[i];
                }else{// group graph
                    group_graph[j_group].push_back(i_group);
                    ++group_degree[i_group];
                }
            }
            group2items[i_group].push_back(i);
        }

        vector<int> ans;
        queue<int> group_Q, item_Q;
        
        //group的拓扑排序
        for(int i = 0;i < group_idx; ++i){
            if(group_degree[i] == 0) group_Q.push(i);
        }
        while(!group_Q.empty()){
            int g = group_Q.front(); group_Q.pop();
           

            //一个group的item的拓扑排序
            for(int i = 0;i < group2items[g].size(); ++i){
                int u = group2items[g][i];
                if(item_degree[u] == 0) item_Q.push(u);
            }
            while(!item_Q.empty()){
                int u =  item_Q.front(); item_Q.pop();
                ans.push_back(u);
                for(int i = 0;i < item_graph[u].size(); ++i){
                    int v = item_graph[u][i];
                    if(--item_degree[v] == 0) item_Q.push(v);
                }
            }
            for(int i = 0;i < group_graph[g].size(); ++i){
                int g2 = group_graph[g][i];
                if(--group_degree[g2] == 0) group_Q.push(g2);
            }
            
        }
        if(ans.size() != n) return {};
        else  return ans;
    }
};
  1. Count Vowels Permutation

题意:求长度为n且满足如下规则的字符串数目。结果mod 1e9+7
只包含a,e,i,o,u五个字符
a后面只能接e
e后面只能接a或者i
i后面不能接i
o后面只能接i,u
u后面只能接a

题解:动态规划。dp[i][j]表示长度为i,结束为字母为i的字符串数目。然后用滚动数组

class Solution {
public:
    long long dp[2][5];
    long long MOD = 1e9 + 7;
    int countVowelPermutation(int n) {
        bool legal_transfer_state[5][5];
        memset(dp, 0, sizeof(dp));
        memset(legal_transfer_state, 0, sizeof(legal_transfer_state));
        
        legal_transfer_state[0][1] = 
        legal_transfer_state[1][0] = 
        legal_transfer_state[1][2] = 
        legal_transfer_state[2][0] = 
        legal_transfer_state[2][1] = 
        legal_transfer_state[2][3] =   
        legal_transfer_state[2][4] = 
        legal_transfer_state[3][2] = 
        legal_transfer_state[3][4] = 
        legal_transfer_state[4][0] = true;
        
        int cur = 0;
        for(int i = 0;i < 5; ++i) dp[0][i] = 1;
        
        for(int i = 1;i < n; ++i){
            cur ^= 1; 
            memset(dp[cur], 0, sizeof(dp[cur]));
            
            for(int j = 0; j < 5; ++j){
                if(dp[cur^1][j] == 0) continue;
                dp[cur^1][j] %= MOD;
                for(int k = 0; k < 5; ++k){
                    
                    if(!legal_transfer_state[j][k]) continue;
                    dp[cur][k] += dp[cur^1][j];
                
                }
            }
        }
        long long ans = 0;
        for(int i = 0;i < 5; ++i)
            ans += dp[cur][i];
        return ans % MOD;
        
    }
};
  1. Maximum Equal Frequency

题意:给定一个正整数数字串nums,问其中最长的前缀满足删除一个元素后所有元素出现的次数都相等

题解:对于每个前缀,求各种频次出现的次数。如果都是频次为1,显然删除任意一个都可以。另外满足条件的前缀,要么是一个频次为1,其他的频次都相同,要么一个频次比其他的高一,其他的都相同。而求前缀i,各种频次的次数,可以由i - 1的结果得到,因为只有当前数字的频次+1,其他不变。代码中freq_counter保存的就是频次的数量。num_counter保存的是各个数字出现的次数。

class Solution {
    static const int N = 1e5 + 1;
    int num_counter[N];
    int freq_counter[N];
    
public:
    int maxEqualFreq(vector<int>& nums) {
        memset(num_counter, 0 , sizeof(num_counter));
        int max_freq = 0;
        int ans = 1;
        
        for(int i = 0;i < nums.size(); ++i){
            ++num_counter[nums[i]];
            --freq_counter[num_counter[nums[i]] - 1];
            ++freq_counter[num_counter[nums[i]]];
            max_freq = max(max_freq, num_counter[nums[i]]);
            if(max_freq == 1 || max_freq * freq_counter[max_freq] == i || (max_freq - 1) * (freq_counter[max_freq - 1] + 1) == i) ans = i + 1;
        }
        return ans;
    }
}; 
  1. Divide Chocolate

题意:给定一个正整数数组。把它分为K+1段。问和最小的段的最大值是多少。

题解:比较简单。二分答案即可。

class Solution {
public:
    bool check(vector<int>& sweetness, int min_val, int K){
        int sum = 0;
        for(int i = 0;i < sweetness.size(); ++i){
            sum += sweetness[i];
            if(sum >= min_val){
                sum = 0;
                --K;
            }
        }
        return K <= 0;
    }
    int maximizeSweetness(vector<int>& sweetness, int K) {
        int min_sweetness = INT_MAX, sum = 0;
        
        for(int i = 0;i < sweetness.size(); ++i){
            sum += sweetness[i];
            min_sweetness = min(min_sweetness, sweetness[i]);
        }
        //最小不会小过最小值,最大不会超过平均
        int max_sweetness = sum / ++K;
        
        
        int left = min_sweetness, right = max_sweetness;
        while(left <= right){
            int mid = (right - left) / 2 + left;
            if(check(sweetness, mid, K)){
                left = mid + 1;
            }else{
                right = mid - 1;
            }
        }
        return left - 1;
    }
};
  1. Maximum Profit in Job Scheduling

题意:最大收益任务安排。每个任务有一个开始时间和结束时间以及一个收益。时间不重叠的任务可以一起执行。问最大的收益

题解:动态规划。先把任务按结束时间前后排序,结束时间早的排在前面。dp[i][j](j = 0或者1)表示前i个任务在第i个任务不选或者选择的情况下的最大收益。答案就是dp[n - 1][0], dp[n - 1][1]的较大值
当前第i个如果不选,那么它的最大收益就是前面一个选和不选的最大收益即max(dp[i - 1][0], dp[i - 1][1])
如果当前任务i,被选了,那么它的最大收益取决于前一个和它没有时间重叠的任务选择状态的最大收益。
没有时间重叠就是 endTime 比startTime[i]小的。而前一个时间没有重叠的就是其中的最大值,可以通过二分搜索得到。所以时间复杂度就是O(nlogn)

class Solution {
public:
    
    static bool cmp(const pair<int,int> &a, const pair<int,int> &b){
            return a.second < b.second;
    }
    
    int binary_search(int st, vector<pair<int,int>>&idx){
        int left = 0, right = idx.size() - 1;
        while(left <= right){
            int mid = (left + right) >> 1;
            if(idx[mid].second <= st)
                left = mid + 1;
            else right = mid - 1;
        }
        return left - 1;
    }
    
    int jobScheduling(vector<int>& startTime, vector<int>& endTime, vector<int>& profit) {
        int n = startTime.size();
        int dp[n][2];
        memset(dp, 0, sizeof(dp));
        
        vector<pair<int,int>> idx(n);
        for(int i = 0;i < n; ++i) idx[i] = make_pair(i, endTime[i]);
        sort(idx.begin(), idx.end(), cmp);
        
        for(int i = 0; i < n; ++i){
            int cur_idx = idx[i].first;
            int left_idx = binary_search(startTime[cur_idx], idx);
            if(i > 0) dp[i][0] = max(dp[i - 1][0], dp[i - 1][1]);
            dp[i][1] = profit[cur_idx];
            if(left_idx >= 0)
                dp[i][1] = max(dp[left_idx][0], dp[left_idx][1]) + profit[cur_idx];
        }
        return max(dp[n - 1][0], dp[n - 1][1]);
    }
};
  1. Tiling a Rectangle with the Fewest

题意:如图,给n*m的格子铺上正方形。问最少需要多少个正方形
在这里插入图片描述
题解:深度搜索,每次从左边最低点开始填,见代码。dp计算的是一个上界,就是每次将整个矩形分割成两部分的铺法

class Solution {
public:
	int tilingRectangle(int n, int m) {
		if (n == m) return 1;
        //n < m
		if (n > m) swap(n, m);
		int heights[n] = {};

		int dp[n + 1][m + 1] = {};

		for (int i = 1; i <= n; ++i)
			for (int j = 1; j <= m; ++j){
				if (i == j) {
					dp[i][j] = 1;
					continue;
				}
				dp[i][j] = i * j;
				for (int k = 1; k <= i / 2; ++k)
					dp[i][j] = min(dp[i][j], dp[i - k][j] + dp[k][j]);
				for (int k = 1; k <= j / 2; ++k)
					dp[i][j] = min(dp[i][j], dp[i][j - k] + dp[i][k]);
			}

		int res(dp[n][m]);
		dfs(n, m, heights, 0, res);
		return res;
	}
	
    //height记录每行已经被铺上方块的长度
    //cnt是搜索深度,也就是方块数目
    //每次从最短的地方开始补,然后深搜
	
	void dfs(const int& n, const int& m, int heights[], int cnt, int& res) {
		if (cnt >= res) return;

		int left(0), min_height(m);
		for (int i = 0; i < n; i++)
			if (heights[i] < min_height)
				min_height = heights[left = i];

		if (min_height == m) {
			res = min(cnt, res);
			return;
		}

		int right(left);
        //找最低的位置最大方块left,right为最上和最下的位置
		while (right < n && heights[left] == heights[right] && right - left + min_height < m) ++right;
        
       
		for (int i = right; i > left; --i) {
            //开始补
			for (int j = left; j < i; ++j)
				heights[j] += i - left;
			dfs(n, m, heights, cnt + 1, res);
            //回退
			for (int j = left; j < i; ++j)
				heights[j] -= i - left;
		}

	}
};
  1. Palindrome Removal
    题意:给定一个整数串,每次操作可以移除一个回文子串。问最少操作多少次整个串为空。
    题解:动态规划。dp[i][j]表示子串i到j需要操作多少次全部移除。
    枚举i最后和谁匹配。如果和中间某个k匹配,则次数为dp[i+1][k-1] + dp[k+1][j]。如果和j匹配则为dp[i + 1][j]。取最小的为dp[i][j]即可。
    这题的标程有误,[1,2,3,4,3,2,1,1,7,8,7,1]应该是2,标程输出3,因为第一个1和中间的1匹配而不是末尾。样例太弱,导致有一些错误的解法
class Solution {
    
    int dp[101][101];
    
    //采用记忆化搜索
    int dfs(int left, int right, const vector<int> &arr){
        if(left >= right) return 1;
        int &ans = dp[left][right];
        if(~ans) return ans;
        ans = right - left + 1;
        if(arr[left] == arr[right]) ans = dfs(left + 1, right - 1, arr);
        for(int i = left; i < right; ++i){
            if(arr[left] != arr[i]) continue;
            ans = min(ans, dfs(left + 1, i - 1, arr) + dfs(i + 1, right, arr));
        }
        return ans;
    }
    
    
public:
    int minimumMoves(vector<int>& arr) {
        memset(dp, -1, sizeof(dp));
        return dfs(0, int(arr.size() - 1), arr);
    }
};
  1. Check If It Is a Good Array

题意:一个整数数组如果存在一个子集,每个数乘以随意一个整数然后相加等于1,则称数组为GoodArray。给定一个数组问是不是GoodArray
题解:数论基础知识。就是求所有数的最大公因子是不是1。

class Solution {
    int gcd(int a,int b){
        return b ? gcd(b, a % b) : a;
        
    }
public:
    bool isGoodArray(vector<int>& nums) {
        int prev = nums[0];
        for(int i = 1;i < int(nums.size()); ++i){
            prev = gcd(prev, nums[i]);
        }
        return prev == 1;
    }
    
    
};

  1. Maximum Score Words Formed by Letters
    题意:给定一个字符串数组words,还有一些字符letters,以及各个字符的得分score。你需要用letters中的字符去组成words中的字符串,每个字符只能用一次,每组成一个words中的字符串,会得到字符串得分(串中字符得分和),每个串只能计分一次。问最大的得分是多少?
    题解:背包问题。动态规划,状态压缩,dp[s]表示取s中的字符串的得分,-1表示不可取到。
class Solution {
public:
    int maxScoreWords(vector<string>& words, vector<char>& letters, vector<int>& score) {
        int dp[1<<14];
        memset(dp, -1, sizeof(dp));
        dp[0] = 0;
        int n = words.size();
        int letter_count[26] = {0};
        for(int i = 0;i < letters.size(); ++i){
            ++letter_count[letters[i] - 'a'];
        }

        int ans = 0;
        for(int i = 0; i < (1 << n); ++i){
            if(dp[i] == -1) continue;
            vector<int> count(26, 0);

            for(int k = 0; k < n; ++k){
                if(i & (1 << k)){
                    for(int c = 0; c < words[k].size(); ++c)
                        ++count[words[k][c] - 'a'];
                }
            }
            
            for(int j = 0; j < n; ++j){
                if(i & (1 << j)) continue;
                int cur_state = i | (1 << j);
                int valid = true;
                
                for(int c = 0; c < words[j].size(); ++c)
                        ++count[words[j][c] - 'a'];

                for(int k = 0; k < 26; ++k){
                    if(letter_count[k] < count[k]){
                        valid = false;
                        break;
                    }
                }

                if(valid) {
                    dp[cur_state] = 0;
                    for(int k = 0; k < 26; ++k)
                        dp[cur_state] += count[k] * score[k];
                    ans = max(ans, dp[cur_state]);
                }

                for(int c = 0; c < words[j].size(); ++c)
                    --count[words[j][c] - 'a'];
            }

        }

        return ans;
    }
};
  1. Number of Ways to Stay in the Same Place After Some Steps
    题意:有一个长度为 arrLen 的数组,开始有一个指针在索引 0 处。
    每一步操作中,你可以将指针向左或向右移动 1 步,或者停在原地(指针不能被移动到数组范围外)。给你两个整数 steps 和 arrLen ,请你计算并返回:在恰好执行 steps 次操作以后,指针仍然指向索引 0 处的方案数。
    题解:动态规划。首先,当arrLen超过steps / 2 + 1时,后面的位置都不可达,因为向左走和向右走的步数要一样多,最多为step/2步。dp[i][j]表示step为i时,到达j位置的方案数。则dp[i][j]柯一友左边向右走,或者原地不动,或者右边向左走三种情况得到。再用滚动数组减少内存
class Solution {
private:
    static const int mod = 1e9 + 7;
    
public:
    int numWays(int steps, int arrLen) {
        arrLen = min(arrLen, steps / 2 + 1);
        vector<vector<long long>> f(2, vector<long long>(steps + 1));
        f[0][0] = 1;
        for (int i = 1; i <= steps; ++i) {
            fill(f[i & 1].begin(), f[i & 1].end(), 0);
            for (int j = 0; j < arrLen; ++j) {
                for (int k = -1; k <= 1; ++k) {
                    if (j - k >= 0 && j - k < arrLen) {
                        f[i & 1][j] += f[(i & 1) ^ 1][j - k];
                    }
                }
                f[i & 1][j] %= mod;
            }
        }
        return f[steps & 1][0];
    }
};
// class Solution {
//     int dp[501][252];
//     static const int MOD = 1e9 + 7;
//     // int dfs(int step, int len){
//     //     if(step <= 1 or len == 1) return 1;
//     //     if(dp[step][len] != -1) return dp[step][len];
        
//     //     long long ans = dfs(step - 1, len);
//     //     for(int k = 2; k <= step; ++k){
//     //         ans += (long long) dfs(k - 2, len - 1) * dfs(step - k, len);
//     //         ans %= MOD;
//     //     }
//     //     return dp[step][len] = ans % MOD;
//     // }

// public:
//     int numWays(int steps, int arrLen) {
//         if(arrLen >= steps / 2 + 1) arrLen = steps / 2 + 1;
//         memset(dp, -1, sizeof(dp));

//         for(int i = 0; i <= arrLen;  ++i) dp[0][i] = dp[1][i] = 1;
//         for(int i = 0; i <= steps; ++i) dp[i][1] = 1;

//         for(int i = 2;i <= steps; ++i){
//             for(int len = 2; len <= arrLen; ++len){
//                 long long res = dp[i - 1][len];
//                 for(int k = 2; k <= i; ++k){
//                     res += (long long) dp[k - 2][len - 1] * dp[i - k][len];
//                     res %= MOD;
//                 }
//                 dp[i][len] = res;
//             }
//         }
        
//         return dp[steps][arrLen];

//     }
// };
  1. Number of Ships in a Rectangle

题意:这是一道交互题,一片海域(每个整点可以停船),给定topRight, bottomLeft代表一个区域的右上角和左下角,求这里面有多少只船。现给定一个对象sea,Sea.hasShips(topRight, bottomLeft)查询区域是否包含船只。
题解:二分区域进行查找,dfs

/**
 * // This is Sea's API interface.
 * // You should not implement it, or speculate about its implementation
 * class Sea {
 *   public:
 *     bool hasShips(vector<int> topRight, vector<int> bottomLeft);
 * };
 */

class Solution {
    int count(Sea &sea, vector<int> topRight, vector<int> bottomLeft){
        if(!sea.hasShips(topRight, bottomLeft)) return 0;
        if(topRight == bottomLeft) return 1;
        int x_len = topRight[0] - bottomLeft[0];
        int y_len = topRight[1] - bottomLeft[1];
        if(x_len >= y_len){
            return count(sea, {bottomLeft[0] + x_len / 2, topRight[1]}, bottomLeft) + 
                    count(sea, topRight, {bottomLeft[0] + x_len / 2 + 1, bottomLeft[1]});

        }else{
            return count(sea, {topRight[0], bottomLeft[1] + y_len / 2}, bottomLeft) +
                    count(sea, topRight, {bottomLeft[0], bottomLeft[1] + y_len / 2 + 1});
        }
    }

public:
    int countShips(Sea sea, vector<int> topRight, vector<int> bottomLeft) {
        return count(sea, topRight, bottomLeft);
    }
};
  1. Palindrome Partitioning III

题意:给定一个字符串和一个数k。每次操作可以改变任意一个字符。问至少多少次修改能够将串变为k个回文串拼接而成。
题解:动态规划。dp[i][j]表示子串0~i分成j个回文串至少要多少次操作。则我们只要枚举i为结尾的最后一个回文串的起始位置p,dp[i][j] = min(dp[p][j - 1] + cost[p + 1][i])。其中cost[i][j]表示子串i到j修改为回文串需要的操作,这个通过预处理得到(也是一个动态规划)

class Solution {
public:
    int palindromePartition(string s, int k) {
        int n = s.size();
        if (n == k) return 0;
        vector<vector<int>> cost(n, vector<int>(n, 0));
        
        for(int i = 0;i < n - 1; ++i)
            cost[i][i + 1] = (s[i] != s[i + 1]);

        for(int len = 2; len < n; ++len){
            for (int i = 0; i + len < n; ++i) {
                cost[i][i + len] = cost[i + 1][i + len - 1] + (s[i] != s[i + len]);
            }
        }

        vector<vector<int>> dp(n + 1, vector<int>(k + 1, 1000));
        dp[0][1] = 0;
        for (int i = 1; i < n; ++i) {
            dp[i][1] = cost[0][i];
            for (int j = 2; j <= k; ++j) {
                for (int p = 0; p < i; ++p) {
                    dp[i][j] = min(dp[i][j], dp[p][j - 1] + cost[p + 1][i]);
                }
            }
        }
        return dp[n - 1][k];
    }
};
  1. Minimum Moves to Move a Box to Their Target Location

题意:推箱子,给定一个网格,一个箱子起始位置,一个人的起始位置,一个目标位置。问多少步可以把箱子推导目标位置(箱子的移动步数)。
题解:这里LeetCode题解有个比较好的BFS解答。我稍微加了点注释,修改了一点点。还可以用Autohome.com.cn算法

typedef pair<int, int> ii;
const int d[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
const int N = 20;
int flag[N][N][N][N];

//人的位置和箱子的位置
struct Node {
    int px, py, bx, by;
};

class Solution {
public:
    int minPushBox(vector<vector<char>>& a) {
        int n = a.size(), m = a[0].size();
        int px, py, bx, by, tx, ty;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < m; ++j) {
                if (a[i][j] == 'S') {
                    px = i;
                    py = j;
                    a[i][j] = '.';
                } else if (a[i][j] == 'T') {
                    tx = i;
                    ty = j;
                    a[i][j] = '.';
                } else if (a[i][j] == 'B') {
                    bx = i;
                    by = j;
                    a[i][j] = '.';
                }
            }
        }
        memset(flag, -1, sizeof(flag));
        //用双端队列,避免用优先队列或者两个队列。因为只是人走的话,不改变箱子移动次数,所以优先遍历,放在队列投
        deque<Node> Q;
        Q.push_back({px, py, bx, by});
        flag[px][py][bx][by] = 0;
        while (!Q.empty()) {
            auto [px, py, bx, by] = Q.front();
            if (bx == tx && by == ty) return flag[px][py][bx][by];
            Q.pop_front();
            //移动人
            for (int k = 0; k < 4; ++k) {
                int nx = px + d[k][0];
                int ny = py + d[k][1];
                //人走下一步
                if (nx < 0 || nx >= n || ny < 0 || ny >= m || a[nx][ny] != '.') continue;
                //不能穿越箱子
                if (nx == bx && ny == by) continue;
                //状态已经访问过
                if (flag[nx][ny][bx][by] >= 0) continue;
                //箱子没动,所以没变
                flag[nx][ny][bx][by] = flag[px][py][bx][by];
                Q.push_front({nx, ny, bx, by});
            }
            //移动箱子
            if (abs(px - bx) + abs(py - by) == 1) {
                
                //人和箱子都移动,箱子往人和箱子那一侧移动,人移动到箱子位置
                int nbx = 2 * bx - px;
                int nby = 2 * by - py;
                if (!(nbx < 0 || nbx >= n || nby < 0 || nby >= m || a[nbx][nby] != '.') && flag[bx][by][nbx][nby] < 0) {
                    flag[bx][by][nbx][nby] = flag[px][py][bx][by] + 1;
                    Q.push_back({bx, by, nbx, nby});
                }
            }
        }
        return -1;
    }
};

  1. Minimum Number of Flips to Convert Binary Matrix to Zero Matrix
    题意:给定一个0,1矩阵,每次可以执行一个操作可以对一个位置及其邻居(上下左右)进行翻转(0变成1,1边0)。问至少多少次操作可以变成0矩阵,不能的话输出-1
    题解:由于矩阵最大只有3*3大小。注意到一个地方要么翻转一次要么不翻转,因为翻转的次序是可以交换的,两次翻转相当于不翻转。所以枚举翻转的地方,遍历所有可能即可。按道理是个中等题而已
class Solution {
    int n, m;
    vector<int> dir_x, dir_y;
    bool check_zero_mat(vector<vector<int>>& mat){
        for(int i = 0;i < n; ++i){
            for(int j = 0; j < m; ++j)
                if(mat[i][j]) return false;
        }
        return true;
    }

    void do_flip(vector<vector<int>>& mat, int op){
        for(int i = 0;i < n * m; ++i){
            if(!(op & (1 << i))) continue;
            int x = i / m;
            int y = i % m;

            mat[x][y] ^= 1;
            for(int d = 0; d < 4; ++d){
                int nx = x + dir_x[d];
                int ny = y + dir_y[d];
                if(nx < 0 or ny < 0 or nx >= n or ny >= m) continue;

                mat[nx][ny] ^= 1;
            }
        }

    }

public:
    int minFlips(vector<vector<int>>& mat) {
        n = mat.size();
        m = mat[0].size();
        dir_x = {0, 0, 1, -1};
        dir_y = {1, -1, 0, 0};

        if(check_zero_mat(mat)) return 0;

        int ans = numeric_limits<int>::max();
        for(int i = 1; i < (1 << (n * m)); ++i){
            int op_num = __builtin_popcount(i);
            if(op_num >= ans) continue;
            do_flip(mat, i);
            if(check_zero_mat(mat)) ans = min(ans, op_num);
            do_flip(mat, i);
        }

        return ans == numeric_limits<int>::max() ? -1 : ans;
    }
};
  1. Minimum Falling Path Sum II

题意:给定一个整数方阵,每一行选一个数,相邻的行 不能选择同一列,问最小的和是多少?

题解:容易想到动态规划,一行行处理。然而注意到对于每一行,选择最小的任意两个数才可能是最小的,因为如果选择另一个位置,使得和最小,上行的位置肯定和最小两个数中的一个不同,将当前这个数替换那个数不会变大。而且上一行一定也是这么选择的。所以设dp[i][0]为当前i行取最小数时的最小和,dp[i][1]为当前行取次小数的最小和。根据上一行最小和次小和进行转移即可。然后用滚动数组,节省空间。

class Solution {
public:
    int minFallingPathSum(vector<vector<int>>& arr) {
        int n = arr.size();
        int dp[2][2] = {0};

        int cur = 1;
        int pre_min = 0, pre_smin = 1;
        for(int i = 0; i < n; ++i){
            cur ^= 1;
            //找出最小和次小数所在位置
            int cur_min = 0, cur_smin = 1;
            if(arr[i][0] > arr[i][1]) swap(cur_min, cur_smin);
            for(int j = 2;j < n; ++j){
                if(arr[i][j] <= arr[i][cur_min]){
                    cur_smin = cur_min; 
                    cur_min = j;
                }else if(arr[i][j] < arr[i][cur_smin]){
                    cur_smin = j;
                }
            }
            
            if(cur_min != pre_min) {
                dp[cur][0] = dp[cur^1][0] + arr[i][cur_min];
                if(cur_min != pre_smin){
                    dp[cur][0] = min(dp[cur][0], dp[cur^1][1] + arr[i][cur_min]);
                }
            }else{
                dp[cur][0] = dp[cur^1][1] + arr[i][cur_min];
            }


            if(cur_smin != pre_min){
                dp[cur][1] = dp[cur^1][0] + arr[i][cur_smin];
                if(cur_smin != pre_smin){
                    dp[cur][1] = min(dp[cur][1], dp[cur^1][1] + arr[i][cur_smin]);
                }

            }else{
                dp[cur][1] = dp[cur^1][1] + arr[i][cur_smin];
            }

            pre_min = cur_min;
            pre_smin = cur_smin;
        }
        return min(dp[cur][0], dp[cur][1]);
    }
};
  1. Shortest Path in a Grid with Obstacles Elimination

题意:给定一个n*m的网格,0表示可通过,1表示障碍物。问从左上角走到右下角,最多通过k个障碍物的最短路。
题解:BFS加一个状态表示通过的障碍数,然后记录访问过的状态。广度优先遍历的过程中,注意到如果在某个点之前通过的障碍数不比现在多的话,那当前的走法就不用考虑了,因为前一种走法比当前好。所以用一个vis数组表示当前到达某位置最小通过的障碍数。

class Solution {
    struct state{
        int n;
        int m;
        int k;
        int step;
        state(int a, int b, int c, int s): n(a), m(b), k(c), step(s){}
    };

public:
    int shortestPath(vector<vector<int>>& grid, int k) {
        int n = grid.size();
        int m = grid[0].size();
        int vis[n][m];
        memset(vis, 0x3f, sizeof(vis));

        int dir_x[] = {0, 0, 1, -1};
        int dir_y[]  = {1, -1, 0, 0};
        queue<state> Q;
        Q.push({0, 0, 0, 0});
        vis[0][0] = 0;

        while(!Q.empty()){
            state s = Q.front(); Q.pop();
            if(s.n == n - 1 && s.m == m - 1) return s.step;

            for(int d = 0; d < 4; ++d){
                int nx = s.n + dir_x[d];
                int ny = s.m + dir_y[d];
                if(nx < 0 or ny < 0 or nx >= n or ny >= m or vis[nx][ny] <= s.k + grid[nx][ny] or s.k + grid[nx][ny] > k) continue;
                vis[nx][ny] = s.k + grid[nx][ny];
                Q.push({nx, ny, s.k + grid[nx][ny], s.step + 1});
            }
        }
        return -1;
    }
};
  1. Maximum Candies You Can Get from Boxes
    题意:有一些箱子。每个箱子的信息[status, candies, keys, containedBoxes]表示箱子开闭状态,糖果数目,其他箱子的钥匙,里面包含的其他箱子。。给定初始的一些箱子。每次你可以打开已经开着的箱子,获取其中的糖果,钥匙和其他箱子。拿到的钥匙可以开对应的箱子(如果箱子已经拿到)。问最后能得到的箱子糖果数最多为多少?
    题解:类似拓扑排序。从已经开着的且在初始列表中的箱子开始。包含关系看成一个有向图,而前后关系决定于箱子什么时候才能开。可以用一个优先队列,每次选一个开着的箱子,将里面的东西拿出来,将钥匙先将可以打开的箱子的状态改为开,而里面箱子放到队列中,直到没有开着的箱子即结束。
class Solution {
    
    struct Node{
        int id, st;
        Node(int idx, int stat):id(idx), st(stat){}
        Node(){}
        bool operator < (const Node &b) const {
            return st < b.st;
        }
    };
    
    
public:
    int maxCandies(vector<int>& status, vector<int>& candies, vector<vector<int>>& keys, vector<vector<int>>& containedBoxes, vector<int>& initialBoxes) {

        priority_queue<Node> Q;
        int n = int(status.size());
        vector<bool> vis(n, false);
        
        for(size_t i = 0;i < initialBoxes.size(); ++i){
            Q.push(Node(initialBoxes[i], status[initialBoxes[i]]));
        }
        
        int ans = 0;
        while(!Q.empty()){
            int u = Q.top().id; Q.pop();
            if(vis[u]) continue;
            if(!status[u]) break;
            ans += candies[u];
            vis[u] = true;
            for(size_t i = 0;i < keys[u].size(); ++i){
                status[keys[u][i]] = 1;
            }
            
            for(size_t i = 0;i < containedBoxes[u].size(); ++i){
                if(!vis[containedBoxes[u][i]])
                    Q.push(Node(containedBoxes[u][i], status[containedBoxes[u][i]]));
            }
        }
        return ans;
    }
};
  1. Number of Paths with Max Score
    题意:有一个矩阵,问从右下角到左上角的最大和路径的和和路径数。除了左上角和右下角,每个位置要么是X,要么是0到9,X表示不可通过,0-9表示数值。每次可以向左移动一格,向上或者向左上角移动一格。
    题解:改为从左上角到右下角,先用动态规划求到每个位置的最大和。然后回溯路径数。
class Solution {
    const int MOD = 1e9 + 7;
public:
    vector<int> pathsWithMaxScore(vector<string>& board) {
        int n = board.size();
        int m = board[0].length();
        vector<vector<int>> score(n, vector<int>(m, 0)),
                            path(n, vector<int>(m, 0));
        board[0][0] = '0';
        board[n - 1][m - 1] = '0';
            
        for(int i = 0;i < n; ++i){
            for(int j = 0;j < m; ++j){
                if(board[i][j] == 'X')
                    continue;
                int num = board[i][j] - '0';
                if(i > 0 && board[i - 1][j] != 'X'){
                    score[i][j] = max(score[i][j], score[i - 1][j] + num);
                }
                
                if(j > 0 && board[i][j - 1] != 'X'){
                    score[i][j] = max(score[i][j], score[i][j - 1] + num);
                }
                
                if(i > 0 && j > 0 && board[i - 1][j - 1] != 'X'){
                    score[i][j] = max(score[i][j], score[i - 1][j - 1] + num);
                }
                if(score[i][j] == 0 && !(i == 0 && j == 0 || i == n - 1 && j == m - 1))
                     board[i][j] = 'X';
            }
        }
        
        path[0][0] = 1;
        for(int i = 0;i < n; ++i){
            for(int j = 0;j < m; ++j){
                if(board[i][j] == 'X')
                    continue;
                
                int num = board[i][j] - '0';
                if(i > 0 && path[i - 1][j] != 0 && score[i - 1][j] + num == score[i][j]){
                    path[i][j] = (path[i][j] + path[i - 1][j]) % MOD;
                }
                
                if(j > 0 && path[i][j - 1] != 0 && score[i][j - 1] + num == score[i][j]){
                    
                    path[i][j] = (path[i][j] + path[i][j - 1]) % MOD;
                }
                
                if(i > 0 && j > 0 && path[i - 1][j - 1] != 0 && score[i - 1][j - 1] + num == score[i][j]){
                    path[i][j] = (path[i][j] + path[i - 1][j - 1]) % MOD;
                }
            }
        }

        return {score[n - 1][m - 1], path[n - 1][m - 1]};
        
    }
};
  1. Verbal Arithmetic Puzzle

题意:给定一个字符串数组words和一个字符串result。问能不能把每个字母解码成一个0到9的数字,把words每个字符串表示的数字加起来得到result表示的数字。解码不能重复,即不同字母对应不同数字,另外不能有前导0。全部不同的字母最多不超过10个。如果能返回true,不能返回false。
题解:dfs。问题是怎么dfs,如果直接按字母的不同取值进行搜索会超时。而按words进行逐个解码,然后看看result有没有解则需要耗费大量时间。如果逐位开始搜索,我们从所有字符串的最低位开始到最高位这样的顺序搜索,逐步求和,如果某一位不对应,我们可以提前剪枝,不用等到全部解码完才知道,进而减少大量不必要的枚举。

class Solution {
    vector<string> words;
    bool head[91] = {0};
    bool used[10] = {0};
    int char2num[91];
    int base[9];
    int max_len;
    
    bool dfs(int idx, int w, int sum){
        if(w == words.size()){
            w = 0;
            //剪枝
            if(sum % base[++idx])
                return false;
        }
       
        while(idx >= words[w].size() && idx < max_len){
            if(++w == words.size()){
                return dfs(idx, w, sum);
            }
        }
        //终止条件
         if(idx >= max_len){
            return sum == 0;
        }
        
        if(char2num[words[w][idx]] != -1){
            if(w == words.size() - 1)
                return dfs(idx, w + 1, sum - char2num[words[w][idx]] * base[idx]);
            return dfs(idx, w + 1, sum + char2num[words[w][idx]] * base[idx]);
            
        }else{
            for(int i = 0;i < 10; ++i){
                if(used[i]) continue;
                if(i == 0 && head[words[w][idx]])
                    continue;
                used[i] = true;
                char2num[words[w][idx]] = i;
                
                if(w == words.size() - 1){
                    if(dfs(idx, w + 1, sum - i * base[idx]))
                        return true;
                }else{
                     if(dfs(idx, w + 1, sum + i * base[idx] ))
                        return true;
                }
                
                char2num[words[w][idx]] = -1;
                used[i] = false;
            }
            return false;
        }
        
    }
    
public:
    bool isSolvable(vector<string>& words, string result) {
        words.push_back(result);
       
        max_len = 0;
        for(size_t i = 0;i < words.size(); ++i){
            if(words[i].size() > 1)
                head[words[i][0]] = true;
            if(words[i].size() > words.back().size())
                return false;
            max_len = max(max_len, (int)words[i].length());
            reverse(words[i].begin(), words[i].end());
        }
        
        this->words.swap(words);
        
        base[0] = 1;
        for(int i = 1;i < 9; ++i) base[i] = base[i - 1] * 10;
        memset(char2num, -1, sizeof(char2num));
        
        return dfs(0, 0, 0);
        
    }
};
  1. Minimum Insertion Steps to Make a String Palindrome

题意:给定一个字符串,问至少插入多少个字符可以使得字符串成为回文串。;
题解:动态规划。dp[i][j]为子串i到j至少插入多少个字符变成回文串。如果s[i] == s[j],那么s[i]肯定和s[j]匹配,dp[i][j] = dp[i + 1][j - 1]。如果s[i] != s[j],那么要么插入一个和s[i]匹配,要么插入一个和s[j]匹配,故dp[i][j] = min(dp[i + 1][j], dp[i][j - 1]) + 1

class Solution {
    int dp[500][500];
    string s;
    int dfs(int left, int right){
        if(left >= right) return 0;
        
        int &ans = dp[left][right];
        if(ans != -1)
            return ans;
        
        if(s[left] == s[right]){
            return ans = dfs(left + 1, right - 1);
        }else{
            return ans = min(dfs(left + 1, right), dfs(left, right - 1)) + 1;
        }
    }
public:
    int minInsertions(string s) {
        memset(dp, -1, sizeof(dp));
        this->s.swap(s);
        return dfs(0, this->s.length() - 1);
    }
};
  1. Distinct Echo Substrings

题意:给定一个字符串,问有多少不同子串,它是由两个相同字符串拼接起来的。
题解:滚动Hash。

class Solution {
public:
    int distinctEchoSubstrings(string text) {
        typedef unsigned long long ull;
        int n = text.size();
        int base = 27;
        vector<vector<ull>> h(n, vector<ull>(n));
        for (int i = 0; i < n; i++) {
            h[i][i] = text[i] - 'a' + 1;
            for (int j = i + 1; j < n; j++) {
                h[i][j] = h[i][j - 1] * base + (text[j] - 'a' + 1);
            }
        }
        set<ull> st;
        for (int i = 0; i < n; i++) {
            for (int j = 1; i + j * 2 <= n; j++) {
                if (h[i][i + j - 1] == h[i + j][i + j * 2 - 1]) {
                    st.insert(h[i][i + j - 1]);
                }
            }
        }
        return st.size();
    }
};

  1. Minimum Distance to Type a Word Using Two Fingers

题意:给定一个如下图布局的键盘和一个字符串。问用两根手指在键盘上打出这个字符串最少需要移动多少距离。移动距离按曼哈顿距离计算。初始可以放任何位置。
在这里插入图片描述
题解:动态规划。设dp[i][j]为已经输完前i个字符,另一个手指位置在j上的最少移动距离。j = 0~25表示26个字母,j = 26表示另一个手指还没用。那么dp[i][j]可以由以下得到,前一个字符(即i- 1)不是j,那么dp[i][j] = dp[i - 1][j]。当前一个字符是j时,那么就要枚举到i - 1时的另一个手指位置,取最小值。

class Solution {
    int dp[300][27];
    int dis(char a, char b){
        return abs((a - 'A') / 6 - (b - 'A') / 6) + abs((a - 'A')% 6 - (b - 'A') % 6);
    }
    
public:
    int minimumDistance(string word) {
        
        memset(dp, 0x3f, sizeof(dp));
        dp[0][26] = 0;
        //pair<int,int> prev_pos = {(word[0] - 'A') / 6, (word[0] - 'A') % 6}, cur_pos;
        for(int i = 1;i < word.length(); ++i){
            //cur_pos = {(word[i] - 'A') / 6, (word[i] - 'A') % 6};
            dp[i][26] = dp[i - 1][26] + dis(word[i - 1], word[i]);
            
            for(int j = 0;j < 26; ++j){
                dp[i][j] = dp[i - 1][j] + dis(word[i - 1], word[i]);
                
                if(j == word[i - 1] - 'A'){
                    for(int k = 0;k < 27; ++k)
                       dp[i][j] = min(dp[i][j], dp[i - 1][k] + (k == 26 ? 0 : dis('A' + k, word[i])));
                }
            }
        }
        int ans = dp[word.size() - 1][0];
        for(int i = 0;i < 27; ++i)
            ans = min(ans, dp[word.size() - 1][i]);
        return ans;
    }
};
  1. Minimum Number of Taps to Open to Water a Garden

题意:给定一一维线段,位置0到n,每一个位置有一个线段沿着这个点左右延伸长度ranges[i]。问最少需要多少个线段覆盖区间[0,n]。如果没有覆盖方案,返回-1
题解:实际上就是区间覆盖的问题。把ranges处理成线段的左右端点(小于0的变成0,大于n的变成n)。这些线段按左端点从小到大排序。接着从左边开始覆盖,每次取能覆盖最左边未覆盖点的线段中右端点最大的,贪心即可。

class Solution {
    struct cmp{
        
        bool operator()(const pair<int,int> &a, const pair<int,int> &b) const{
            if(a.first == b.first) return a.second > b.second;
            return a.first < b.first;
        }
    };
    
public:
    int minTaps(int n, vector<int>& ranges) {
    
        priority_queue<int> Q;
        vector<pair<int,int>> segs(n + 1);
        
        for(int i = 0;i <= n; ++i){
            segs[i] = {max(0, i - ranges[i]), min(n, i + ranges[i])};
        }
        sort(segs.begin(), segs.end(), cmp());

        int idx = 1;
        Q.push(segs[0].second);
        
        int right = -1;
        int ans = 0;
        
        int u, max_val = -1;
        while(!Q.empty()){
            u = Q.top(); Q.pop();
            if(right >= u)  break;
            right = u;
            ++ans;
            
            while(idx <= n && segs[idx].first <= right){
                max_val = max(max_val, segs[idx++].second);
            }
            Q.push(max_val);
        }
        
        return right < n ? -1 : ans;
        
    }
};
  1. Reverse Subarray To Maximize Array Value

题意:给定一个数组,数组的值等于相邻位置差绝对值之和。可以任意翻转一个子数组。问数组最大的值为多少
题解:当子数组起始位置或者终止位置在边界的时候特殊处理,对于一般的情况。设我们翻转l到r位置的子数组,则原数组
a [ l − 1 ] , a [ l ] , a [ l + 1 ] . . . , a [ r − 1 ] , a [ r ] , a [ r + 1 ] a[l - 1] ,a[l],a[l+1]...,a[r-1],a[r],a[r+1]a[l1],a[l],a[l+1]...,a[r1],a[r],a[r+1]
变为a [ l − 1 ] , a [ r ] , a [ r − 1 ] , . . . , r [ l + 1 ] , a [ l ] , a [ r + 1 ] a[l-1],a[r],a[r-1],...,r[l+1],a[l],a[r+1]a[l1],a[r],a[r1],...,r[l+1],a[l],a[r+1]
数组的值变化为
( ∣ a [ r ] − a [ l − 1 ] ∣ + ∣ a [ r + 1 ] − a [ l ] ∣ ) − ( ∣ a [ r + 1 ] − a [ r ] ∣ + ∣ a [ l ] − a [ l − 1 ] ∣ ) (|a[r]-a[l-1]|+|a[r+1]-a[l]|) - (|a[r+1]-a[r]| + |a[l]-a[l-1]|)(a[r]a[l1]+a[r+1]a[l])(a[r+1]a[r]+a[l]a[l1])

这个变化值左边括号中绝对值中和l和r的值相关,直接最大化肯定是不行的。按照经验肯定要拆成只和单点有关的一个值。注意到绝对值可以用max进行拆解,∣ a − b ∣ = m a x ( a − b , b − a ) |a-b| = max(a - b,b-a)ab=max(ab,ba)
两个绝对值就变成四种情况
∣ a [ r ] − a [ l − 1 ] + ∣ a [ r + 1 ] − a [ l ] ∣ = max ⁡ ( a [ r + 1 ] + a [ r ] − ( a [ l − 1 ] + a [ l ] ) , a [ r + 1 ] − a [ r ] − ( a [ l ] − a [ l − 1 ] ) , − a [ r + 1 ] + a [ r ] − ( − a [ l ] + a [ l − 1 ] ) , − a [ r + a ] − a [ r ] − ( − a [ l ] − a [ l − 1 ] ) ) \begin{aligned}|a[r] - a[l-1] &+ |a[r+1]-a[l]|\\=\max(&a[r+1] + a[r] - (a[l-1] + a[l]),\\ &a[r+1] - a[r] - (a[l] - a[l-1]),\\ &-a[r+1]+a[r]-(-a[l]+a[l-1]),\\ &-a[r+a]-a[r]-(-a[l]-a[l-1])\\)\end{aligned}a[r]a[l1]=max()+a[r+1]a[l]a[r+1]+a[r](a[l1]+a[l]),a[r+1]a[r](a[l]a[l1]),a[r+1]+a[r](a[l]+a[l1]),a[r+a]a[r](a[l]a[l1])
再减去(∣a[r+1]−a[r]∣+∣a[l]−a[l−1]∣)

( ∣ a [ r ] − a [ l − 1 ] ∣ + ∣ a [ r + 1 ] − a [ l ] ∣ ) − ( ∣ a [ r + 1 ] − a [ r ] ∣ + ∣ a [ l ] − a [ l − 1 ] ∣ ) (|a[r]-a[l-1]|+|a[r+1]-a[l]|) - (|a[r+1]-a[r]| + |a[l]-a[l-1]|)(a[r]a[l1]+a[r+1]a[l])(a[r+1]a[r]+a[l]a[l1])
就变为
∣ a [ r ] − a [ l − 1 ] + ∣ a [ r + 1 ] − a [ l ] ∣ = max ⁡ ( ( a [ r + 1 ] + a [ r ] − ∣ a [ r + 1 ] − a [ r ] ∣ ) − ( a [ l − 1 ] + a [ l ] + ∣ a [ l ] − a [ l − 1 ] ∣ ) , ( a [ r + 1 ] − a [ r ] − ∣ a [ r + 1 ] − a [ r ] ∣ ) − ( a [ l ] − a [ l − 1 ] + ∣ a [ l ] − a [ l − 1 ] ∣ ) , ( − a [ r + 1 ] + a [ r ] − ∣ a [ r + 1 ] − a [ r ] ∣ ) − ( − a [ l ] + a [ l − 1 ] + ∣ a [ l ] − a [ l − 1 ] ∣ ) , ( − a [ r + a ] − a [ r ] − ∣ a [ r + 1 ] − a [ r ] ∣ ) − ( − a [ l ] − a [ l − 1 ] + ∣ a [ l ] − a [ l − 1 ] ∣ ) ) \begin{aligned}|a[r] - a[l-1] &+ |a[r+1]-a[l]|\\=\max((&a[r+1] + a[r] -∣a[r+1]−a[r]∣) - (a[l-1] + a[l] + ∣a[l]−a[l−1]∣),\\ &(a[r+1] - a[r] -∣a[r+1]−a[r]∣) - (a[l] - a[l-1] + ∣a[l]−a[l−1]∣),\\ &(-a[r+1]+a[r] -∣a[r+1]−a[r]∣)-(-a[l]+a[l-1]+∣a[l]−a[l−1]∣),\\ &(-a[r+a]-a[r] -∣a[r+1]−a[r]∣)-(-a[l]-a[l-1]+∣a[l]−a[l−1]∣)\\)\end{aligned}a[r]a[l1]=max(()+a[r+1]a[l]a[r+1]+a[r]a[r+1]a[r])(a[l1]+a[l]+a[l]a[l1]),(a[r+1]a[r]a[r+1]a[r])(a[l]a[l1]+a[l]a[l1]),(a[r+1]+a[r]a[r+1]a[r])(a[l]+a[l1]+a[l]a[l1]),(a[r+a]a[r]a[r+1]a[r])(a[l]a[l1]+a[l]a[l1])
∣ a [ r ] − a [ l − 1 ] + ∣ a [ r + 1 ] − a [ l ] ∣ |a[r] - a[l-1] + |a[r+1]-a[l]|a[r]a[l1]+a[r+1]a[l]取最大值就等于每一项分别最大化,再在4个值中求最大值
注意到max每一项都分为形式f ( r ) − g ( l ) f(r) - g(l)f(r)g(l)的形式,max ⁡ ( f ( r ) − g ( l ) ) = max ⁡ ( f ( r ) ) − min ⁡ ( g ( l ) ) \max(f(r) - g(l))= \max(f(r)) - \min(g(l))max(f(r)g(l))=max(f(r))min(g(l)),这样就变成单点值最大化了。
不过这里有一个约束,就是必须有r > l r>lr>l。通过观察知道max中第一项和第四项,第二和第三项其实是r rrl ll的位置互换,所以如果我们取消掉r > l r>lr>l这个限制,第一项和第四项就可以一起计算,第三项和第二项一起计算,就合并为两项了。

class Solution {
public:
    
    int maxValueAfterReverse(vector<int>& nums) {
        int arr_val = 0;
        int n = nums.size();
        
        for(int i = 1; i < n; ++i)
            arr_val += abs(nums[i] - nums[i - 1]);
        
        int max_val = 0;

        for(int i = 1; i < n; ++i){
            
            max_val = max(max_val, abs(nums[0] - nums[i]) - 
                                   abs(nums[i] - nums[i - 1])); // 左端点为0右端点为i
            
            max_val = max(max_val, abs(nums[n - 1] - nums[i - 1]) -
                                   abs(nums[i] - nums[i - 1])); // 右端点为n-1,左端点为i
        }
        
        int max_tmp, min_tmp, left, right;
        
        for(int i = -1; i < 2; i += 2) {
            max_tmp = INT_MIN;
            min_tmp = INT_MAX;
            
            for(int j = 1; j < n; ++j) {
                left = i * (nums[j - 1] + nums[j]);
                right = abs(nums[j] - nums[j - 1]);
                max_tmp = max(max_tmp, left - right);
                min_tmp = min(min_tmp, left + right);
            }
            
            max_val = max(max_val, max_tmp - min_tmp);
        }

        return arr_val + max_val;
    }
};
  1. Jump Game V

题意:给定一个整数数组,可以从任一位置i开始,每次能沿着当前位置d范围内跳到j,其中要满足i和j中间(包括j)的任何数小于i位置的数。问最多可以跳跃几次。
题解:动态规划(+线段树)。注意到,我们是从高处往低处跳,也就是说如果i能跳到j,那么j不能跳到i。设dp[i]为从位置i开始跳的最大跳跃数。则dp[i] = max_j (dp[j] ) + 1,其中j要满足i能跳到j。因为i怎么跳和比i处值大的位置的选择无关,也就是跳到i这个位置以后怎么跳和前面怎么跳没有关系。
可以用记忆化搜索,也可以从值小的开始递推进行。时间复杂度是O(nlogn + nd) 。如果规模加大,d很大,可以用线段树对求最大值部分进行加速时间复杂度降为O(nlogn)。

class Solution {
public:
    int maxJumps(vector<int>& arr, int d) {
        
        vector<pair<int,int>> point;
        int n = arr.size();
        vector<int> dp(n, 0);
        for(int i = 0;i < arr.size(); ++i){
            point.push_back({arr[i], i});
        }
        
        sort(point.begin(), point.end());
        
        int ans = 1;
        for(int i = 0;i < n; ++i){
            pair<int,int> val_idx = point[i];
            int max_val = 0;
            
            for(int dir = -1; dir < 2; dir += 2){
                for(int s = 1; s <= d; ++s){
                    int next_pos = val_idx.second + s * dir;
                    if(next_pos < 0 || next_pos >= n || arr[next_pos] >= val_idx.first) break;
                    max_val = max( max_val, dp[next_pos]);
                }
            }
            dp[val_idx.second] = max_val + 1;
            ans = max(ans, max_val + 1);
        }
        return ans;
    }
};
  1. Jump Game IV

题意:给定一个整数数组,问从位置0到位置n - 1至少需要多少次跳跃。每次跳跃可以往左移动一格往右移动一格或者跳到任意一个和当前位置的值相同的位置。

题解:把位置看成图上的节点,而两点可跳跃是一条无向边。问题就是节点0到节点n - 1的最短路。所以可以用BFS来做。用vis来保存当前整数值是否通过相同值跳跃访问过。dis保存BFS过程中的最短距离,也用于判断该节点是否已经进过队列,避免重复进队。val2indices保存每个相同整数值的下标。

class Solution {
public:
    int minJumps(vector<int>& arr) {
        
        int n = arr.size();
        unordered_set<int> vis;
        vector<int> dis(n, 0);
        queue<int> Q;
        
        unordered_map<int,vector<int>> val2indices;
        
        for(int i = 0;i < n; ++i)
            val2indices[arr[i]].push_back(i);
      
        Q.push(0);
        dis[0] = 1;
        
        while(true){
            int u = Q.front(); Q.pop();
            
            if(u > 0 && !dis[u - 1]){
                dis[u - 1] = dis[u] + 1;
                Q.push(u - 1);
            }
            if(u < n - 1 && !dis[u + 1]){
                dis[u + 1] = dis[u] + 1;
                Q.push(u + 1);
            }
            
            if(vis.find(arr[u]) == vis.end()){
                vis.insert(arr[u]);
                vector<int> &vec = val2indices[arr[u]];
                for(int i = 0;i < vec.size(); ++i){
                    if(!dis[vec[i]]){
                        dis[vec[i]] = dis[u] + 1;
                        Q.push(vec[i]);
                    }
                    
                }
            }
            
            if(dis[n - 1]){
                return dis[n - 1] - 1;
            }
        }
        return -1;
    }
};
  1. Construct Target Array With Multiple Sums

题意:一个全是一的数组,每次操作,可以把整个数组和替换掉其中的一个位置。给定一个数组,问能不能从一个全是一的数组通过若干步操作得到。
题解:我们可以从最大的数开始逆推。设当前的数组总和为sum,最大的数为x。则x变化了sum - x,即x变化之前的值为x - (sum - x)。每次选一个最大值出来往前推就能判断可不可行。所以用一个优先队列,每次取一个最大的,变回原来的值。但是这样做有可能有很多冗余,因为最大的它变回去后仍可能最大。假设y yy是第二大的数,则回到k kk步前的值刚好小于y yy(当y == 1时,取等号,因为x最小为1),有
x − k ( s u m − x ) < y x - k(sum - x) < yxk(sumx)<y

( x − y ) / ( s u m − x ) < k (x-y)/(sum-x)< k(xy)/(sumx)<k
所以最小的k取值为f l o o r ( ( x − y ) / ( s u m − x ) + 1 ) ( 当 y = = 1 时 为 c e i l ( ( x − y ) / ( s u m − x ) ) = f l o o r ( ( s u m − y − 1 ) / ( s u m − x ) ) ) floor((x-y)/(sum-x) + 1)(当y == 1时为ceil((x-y)/(sum-x)) = floor((sum-y - 1) / (sum-x)))floor((xy)/(sumx)+1)y==1ceil((xy)/(sumx))=floor((sumy1)/(sumx))

class Solution {
public:
    bool isPossible(vector<int>& target) {
    
        int n = target.size();
        if(n == 1){
            return target[0] == 1;
        }
        
        
        long long sum = 0;
        
        priority_queue<int> Q(target.begin(), target.end());
        
        for(int i = 0;i < n; ++i)
            sum += target[i];
        
        long long x;
        while(!Q.empty()){
            x = Q.top(); Q.pop();
            if(x == 1) return true;
            if((x << 1) <= sum) return false;
            
            sum -= x;
            
            if(Q.top() == 1)
                x -=  (x - Q.top() + sum - 1) / sum * sum;
            else
                x -=  (x - Q.top() + sum) / sum * sum;
            
            
            if(x <= 0) return false;
            sum += x;
            
            Q.push(x);
        }
        
        return false;
    }
};
  1. Count All Valid Pickup and Delivery Options
    题意:给你 n 笔订单,每笔订单都需要快递服务。请你统计所有有效的 收件/配送 序列的数目,确保第 i 个物品的配送服务 delivery(i) 总是在其收件服务 pickup(i) 之后。由于答案可能很大,请返回答案对 10^9 + 7 取余的结果。
    题解:组合计数。抽象出来就是有n对数,问组合数为多少。答案为(2n)! / (2^n)
class Solution {

    const long long MOD = 1e9 + 7;    
public:
    int countOrders(int n) {
        long long res = 1;
        for(int i = 2;i <= n; ++i){
            res = (res * i * ((i << 1) - 1)) % MOD;   
        }
        return res;
    }
};
  1. Largest Multiple of Three

题意:给定一个数组,每一个是一个0到9的数字。可以从中选择一些拼成一个数,问最大的3的倍数是什么?
题解:3的倍数等价于各位数字之和是3的倍数,首先将所有数字的和加起来,记为tot。如果tot%3 == 0,那么全部数字从大到小拼接即可。如果tot %3!=0,说明要去掉一些数字。如果tot %3 == 1,那么要去掉一个的模3为1的数或者去掉两个模3为2的数。显然优先去掉最小的。tot%3==2的情况类似.

class Solution {
public:
    string largestMultipleOfThree(vector<int>& digits) {
        int count[10] = {0}, mod[3] = {0};
        int tot = 0;
        
        for(auto d:digits){
            ++count[d];
            ++mod[d % 3];
            tot += d;
        }
        vector<char> res;
        
        tot %= 3;
        int rm = tot > 0, rm_md = tot;
        
        
        if(tot && mod[tot] == 0){
            rm = 2;
            rm_md = tot == 1? 2 : 1;
        }
            
        
        for(int i = 1;i < 9; ++i){
            if(i % 3 == rm_md && count[i]){
                if(rm <= count[i]){
                    count[i] -= rm;
                    break;
                }
                --count[i];
                --rm;
            }
        }
        
        for(int i = 9;i >= 0; --i){
            for(int j = 0;j < count[i]; ++j)
                res.push_back('0' + i);
        }
        
        
        if(res.size() > 0 && res[0] == '0') return "0";
        
        return string(res.begin(), res.end());
    }
};
  1. Minimum Cost to Make at Least One Valid Path in a Grid

题意:给定一个grid,每个位置有一个数字(1到4),分别代表下图的四个箭头,右左下上。从左上角开始沿着箭头走,问走到右下角最少要改变多少处箭头
在这里插入图片描述
题解:贪心,BFS(队列或优先队列,优先队列较慢)。注意到不可能走环,故从第一个位置开始,尽可能沿着箭头走,遇到走不通的才改变箭头即可。

#define VALID(u) (u.first >= 0 && u.second >= 0 && u.first < n && u.second < m)
#define GETDIST(u) (dist[u.first][u.second])
#define SETDIST(u,d) (dist[u.first][u.second] = d)
#define GETNEXT(u) (u + dir[grid[u.first][u.second] - 1])

template<typename T1, typename T2> 
inline const pair<T1, T2> operator+(const pair<T1, T2> &p1, const pair<T1, T2> &p2)
{
    pair<T1, T2> ret;
    ret.first = p1.first + p2.first;
    ret.second = p1.second + p2.second;
    return ret;
}
class Solution {
public:
    int minCost(vector<vector<int>>& grid) {
        int n = grid.size(), m = grid[0].size();
        vector<vector<int>> dist(n, vector<int>(m, -1));
        pair<int,int> dir[] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
        
        queue<pair<int,int>> Q;
        pair<int,int> begin(0, 0), end(n - 1, m - 1);
        while(VALID(begin) && GETDIST(begin) == -1){
            Q.push(begin);
            SETDIST(begin, 0) = 0;
            begin = GETNEXT(begin);
        }
        
        while(!Q.empty()){
            pair<int,int> u = Q.front(); Q.pop();
            if(u == end)
                return dist[n - 1][m - 1];
            
            for(int d = 0; d < 4; ++d){
                pair<int,int> next = u + dir[d];
                
                while(VALID(next) && GETDIST(next) == -1){
                    Q.push(next);
                    SETDIST(next, GETDIST(u) + 1);
                    next = GETNEXT(next);
                }
            }
        }
        return -1;
    }
};
  1. Maximum Sum BST in Binary Tree

题意:给定一棵二叉树,问其中和最大的一棵二叉搜索树的和
题解:树dp。对每个节点,计算两个值,第一个是左子树和右子树中BST最大和,第一个以这个值为根的BST和值(前提是这个子树是BST)。用递归计算,返回这两个值dp[node][0]和dp[node][1]。
当该子树可以构成BST时dp[node][1] = dp[leftChild][1] + dp[rightChild][1]
构不成时,设为INT_MIN,这个值用于父亲辅助判断是否能构成BST
dp[node][0] = max{dp[node][1], dp[leftChild][0], dp[rightChild][0]}

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    
    pair<int,int> getAns(TreeNode* node, int &minVal, int &maxVal){

        if(node == nullptr){
            minVal = INT_MAX;
            maxVal = INT_MIN;
            return {0, 0};
        }
        
        if(node->left == nullptr && node->right == nullptr){
            minVal = maxVal = node->val;
            return {max(0, node->val), node->val};
        }
        
        int  leftMax, rightMin;
        pair<int,int> leftVal = getAns(node->left, minVal, leftMax);
        pair<int,int> rightVal = getAns(node->right, rightMin, maxVal);
        
        minVal = min(minVal, node->val);
        
        maxVal = max(maxVal, node->val);
        
        
        int maxSum = max(leftVal.first, rightVal.first);
        int sum = INT_MIN;
        
        if(leftVal.second != INT_MIN && rightVal.second != INT_MIN && leftMax < node->val && node->val < rightMin){
            sum = leftVal.second + rightVal.second + node->val;
            maxSum = max(maxSum, sum);
        }
        return {maxSum, sum};  
        
    }
    
    int maxSumBST(TreeNode* root) {
        int minVal, maxVal;
        return getAns(root, minVal, maxVal).first;
        
    }
};
  1. Frog Position After T Seconds

题意:给定一棵树,从1位置开始,每一次可以等概率沿着边走到一个未访问过的节点,如果无边可走就停在原地。给定一个target和一个t,问t步后停留在target上的概率
题解:由于马尔科夫性,所以每次只要计算有多少个分支,把概率做相乘即可

class Solution {
    int n;
    double dfs(vector<int> e[], int fa, int node, int t, int target){
        if(t < 0) return 0;
        int numBranch = e[node].size();
        if(node != 1)  --numBranch;
        
        if(node == target){
            return (t == 0) || numBranch == 0;
        }
        
        double res = 0;
        for(int i = 0; i < e[node].size(); ++i){
            if(e[node][i] == fa) continue;
            res  = 1.0 / numBranch * dfs(e, node, e[node][i], t - 1, target);
            if(res > 0) break;
            
        }
        
        return res;
    }
public:
    double frogPosition(int n, vector<vector<int>>& edges, int t, int target) {
        
        this->n = n;
        vector<int> e[n + 1];
        for(int i = 0;i < edges.size(); ++i){
            int from = edges[i][0];
            int to = edges[i][1];
            e[from].push_back(to);
            e[to].push_back(from);
        }
        return dfs(e, 0, 1, t, target);
        
    }
};
  1. Pizza With 3n Slices

题意:给定一个圆盘,如下图所示,共3n块,每块有个值。轮流做以下操作,你选择一块,然后Alice取逆时针方向的一块,Bob取顺时针的下一块。
问你最多得到的块的值和为多少?
在这里插入图片描述
题解:动态规划或者贪心。注意到这个问题等价于你在其中选择不相邻的n/3块。因为首先,如果存在两个相邻,那么肯定不存在合法的方案取得这两个。另外需要证明,如果两两不相邻,则存在一种合法方案。
证明:
我们只需要证明如果当前这些要取的块互不相邻,则存在一个块,取完后,剩下的将要取的块依旧不相邻,即存在一个块,它和逆时针方向的下一块间隔2个或以上其他不取的块。设总数为3n块,我们要取n块,已经取了k块,则剩余的有3n - 3k块,我们要取的剩下 n - k>0块互不相邻。如果不存在一个块它和逆时针方向的下一块间隔2个或以上其他块,则两两间隔为1(每一选择的块,有一块逆时针方向的邻接非选块和它对应),即剩下的总块数应该为2 * (n - k) = 2n - 2k != 3n - 3k产生矛盾。

所以,问题变成再n个数中取n/3个互不相邻的数(包括首尾相邻)。
第一种方法:动态规划,dp[0][i][j]表示前i个数取j个且不取第一个数的最大和,dp[1][i][j]表示前i个数取j个且取第一个数的最大和,可以合并一起,并且还可以用滚动数组。
第二种方法:直接从大到小贪心是不行的,比如下面[8,9,8,6,1,1],最好的选择应该是选两个8,而不是先选9。
在这里插入图片描述
但是,可以用一种替换和反悔的方法来做到从大到小取。我们先取9,然后删除898这三个,然后在9的位置插入8 + 8 - 9 = 7。如果后面选择7,则相当于放弃选择9而选择两个8(要么选两个8,要么选一个9,不存在选择一边这种情况,因为这样不如选择中间那个)。设原问题为从n个数中取k个不相邻的数,我们优先取最大的数,按如上操作,如果这个数不包含在最优选择中,问题变成在剩下n - 2个数中取不包含替换后的数的k - 1个数。如果这个数包含在最优选择中,问题变成在剩下n - 2个数中取包含替换后的数的k - 1个数。综合两种情况,问题变成在剩下n - 2个数中取包含替换后的数的k - 1个数,和问题一样的结构,所以贪心是正确的。
实现:因为我们不需要插入,所以用一个vector保存一个双端链表,每个元素,保存左右邻接点的下标。用优先队列做如上的操作,由于优先队列不能删除指定的数,所以加一个can_take表示是否删除了。

//动态规划class Solution {
public:
    int maxSizeSlices(vector<int>& slices) {
        int n = static_cast<int>(slices.size());
        
        vector<vector<int>> dp(n << 1, vector<int>(n << 1, 0));

        dp[0][1] = slices[0];
        dp[1][1] = slices[0];
        
        dp[1][n / 3 + 2] = slices[1];
        
        for(int i = 2;i < n; ++i){
            for(int j = 1; j <= n / 3; ++j){
                dp[i][j] = max(dp[i - 1][j], dp[i - 2][j - 1] + slices[i]);
                dp[i][j + n / 3 + 1] = max(dp[i - 1][j + n / 3 + 1], dp[i - 2][j + n / 3] + slices[i]);
            }
        }
        
        return max(dp[n - 2][n / 3], dp[n - 1][n / 3 * 2 + 1]);
    }
};

作者:lucifer1004

//贪心

struct Node {
    int value, l, r;
};

vector<Node> a; // 基于vector实现双向链表

struct Id {
    int id;

    bool operator<(const Id &that) const {
        return a[id].value < a[that.id].value;
    }
};

void del(int i) {
    // 这里不需要更新i的左右指针,因为i已经不会再被使用了
    a[a[i].l].r = a[i].r;
    a[a[i].r].l = a[i].l;
}

class Solution {
public:
    int maxSizeSlices(vector<int> &slices) {
        int n = slices.size();
        int k = n / 3;

        // 初始化双向链表
        a.clear();
        for (int i = 0; i < n; ++i)
            a.emplace_back(Node{slices[i], (i - 1 + n) % n, (i + 1) % n});
        priority_queue<Id> pq;
        vector<bool> can_take(n, true); // 标记某一序号是否能够选取
        int idx = 0;
        for (int i = 0; i < n; ++i)
            pq.push(Id{i}); // 优先队列初始化
        int cnt = 0, ans = 0;
        while (cnt < k) {
            int id = pq.top().id;
            pq.pop();
            if (can_take[id]) { // 当前序号可用
                cnt++;
                ans += a[id].value;
                // 标记前后序号
                int pre = a[id].l;
                int nxt = a[id].r;
                can_take[pre] = false;
                can_take[nxt] = false;
                // 更新当前序号的值为反悔值
                a[id].value = a[pre].value + a[nxt].value - a[id].value;
                // 当前序号重新入队
                pq.push(Id{id});
                // 删除前后序号(更新双向链表)
                del(pre);
                del(nxt);
            }
        }
        return ans;
    }
};

  1. Longest Happy Prefix

题意:给定一个串,求和后缀匹配最长的前缀
题解:一个简单的做法就是动态规划(其实就是KMP求next加了一个状态,避免了回溯,其实没有必要),注意到以i为结尾的某个后缀如果和前缀匹配,那么以i - 1为结尾的这部分后缀一样和前缀匹配,所以以i为结尾后缀和前缀匹配的部分不会比i - 1匹配的部分长1以上。只有两种情况产生,第一种,以i为结尾的那部分后缀包含了以i - 1为结尾的那部分后缀。第二种,i - 1匹配的那部分比较长。设dp[i][ch]为以i为结尾的后缀,下一个字符为ch时,匹配最长的前缀的位置。
则idx = dp[i - 1][s[i] - ‘a’] + 1,为以i结尾后缀匹配的最长前缀的位置。dp[i][c]有两种递推方式,第一种,s[idx+1] == c,此时dp[i][c] = idx + 1
另一种dp[i]c] = dp[idx][c](因为这时候以i结尾的最长匹配后缀也是以idx结尾的最长匹配后缀)。然后处理好边界条件即可,

解法2:滚动哈希(略)

解法3:KMP的next数组。next数组的含义就是i为结尾的后缀和前缀的匹配长度。

class Solution {
public:
    string longestPrefix(string s) {
        int n = static_cast<int>(s.length());
        vector<vector<int>> dp(n, vector<int>(26, -1));
        
        if(n == 1) return "";
        
        for(int i = 1; i < n; ++i){
            
            int idx = dp[i - 1][s[i] - 'a'];
            if(++idx != 0){
                
                for(int c = 0; c < 26; ++c){
                    dp[i][c] = dp[idx][c];
                }
                
                dp[i][s[idx + 1] - 'a'] = idx;
                
            }else if(s[i] == s[0]){
                dp[i][s[1] - 'a'] = 0;
            }
        }
        
        int res = -1;
        
        for(int i = 0; i < 26; ++i){
            res = max(res, dp[n - 1][i]);
        }
        
        return s.substr(0, res + 1);
    }
};
//KMP的next数组
class Solution {
public:
    string longestPrefix(string s) {
        int n = static_cast<int>(s.length());
        vector<int> next(n + 1, -1);
        
        for(int i = 0, j = -1; i < n; ){
            if(j == -1 || s[i] == s[j]){
                next[++i] = ++j;
            }else{
                j = next[j];
            }
        }
        return s.substr(0, next[n]);
    }
};
  1. Find All Good Strings

题意:给定两个长度为n的字符串s1,s2和一个长度至多为50的字符串evil,问字典序位于s1,s2中间的好串有多少个。其中好串定义为不包含evil子串的字符串。
题解:数位dp,kmp。设g(s)为字典序小于等于s且长度相等的好串数目。则答案为g(s2) - g(s1’),其中s1’和s1的前n - 1位相同,最后一位小1。
再设f[i][j][k]表示长度为i,最长匹配(为什么要最长,因为要避免重复计算,和多个前缀匹配只算最长的)到evil的前j个字符(以evil前j个字符为后缀),是(k=1)否(k=0)卡到上界(和s的前缀匹配)的好串数目
g ( s ) = ∑ i = 0 e v i l . l e n g t h − 1 ( f [ n ] [ i ] [ 0 ] + f [ n ] [ i ] [ 1 ] ) g(s) = \sum_{i=0}^{evil.length - 1} \left( f[n][i][0] + f[n][i][1] \right)g(s)=i=0evil.length1(f[n][i][0]+f[n][i][1])

问题就在于f的计算了,利用递推f[i][j][k]可由f[i - 1][j-1][k] +
但是数据限制上有很大的差别。

解题思路和题解来自:作者:zqy1018

const int M = 1000000007;
class Solution {
    int nxt[51];
    int f[501][51][2];
    int fd[51][26];
    int subp(int n, string& s, string& e){
        memset(f, 0, sizeof(f));
        int l = e.length();
        //f[i][j][0]表示长度为i其小于s的i前缀,末尾最长匹配到evil的前j个字符的good string的个数
        //f[i][j][1]则是长度为i且为s的i前缀时......的个数
        //fd[t][j] evil的前t个字符,增加一个字符j,能匹配到的最大的长度
        f[0][0][1] = 1;
        for (int i = 1; i <= n; ++i){
            for (int j = 0; j < 26; ++j){
                for (int t = 0; t < l; ++t){
                    size_t epos = fd[t][j];
                    f[i][epos][0] = (f[i][epos][0] + f[i - 1][t][0]) % M;
                    if (j < s[i - 1] - 'a')
                        f[i][epos][0] = (f[i][epos][0] + f[i - 1][t][1]) % M;
                    else if (j == s[i - 1] - 'a')
                        f[i][epos][1] = (f[i][epos][1] + f[i - 1][t][1]) % M;
                    
                }
            }
        }
        long long res = 0;
        for (int j = 0; j < l; ++j){
            res += f[n][j][0] + f[n][j][1];
        }
        return static_cast<int>(res % M);
    }
public:
    int findGoodStrings(int n, string s1, string s2, string evil) {
        nxt[0] = -1;
        int l = evil.size();
        // 构造 next 数组
        for (int j = -1, i = 1; i < l; ++i){
            while (j > -1 && evil[i] != evil[j + 1])
                j = nxt[j];
            if (evil[i] == evil[j + 1])
                nxt[i] = ++j;
            else nxt[i] = -1;
        }
        // 计算失配转移
        for (int i = -1; i < l - 1; ++i){
            for (int j = 0; j < 26; ++j){
                if (evil[i + 1] - 'a' == j)
                    fd[i + 1][j] = i + 2;
                else {
                    if (i == -1) fd[0][j] = 0;
                    else fd[i + 1][j] = fd[nxt[i] + 1][j];
                }
            }
        }
        s1[n - 1] -= 1;
        return (subp(n, s2, evil) - subp(n, s1, evil) + M) % M;
    }
};
  1. Reducing Dishes

题意:给定一个数组satisfaction,从中选择一些数字,第一个得分为1 * satisfaction[i] (i为该数字下标),第二个得分为2 * satisfaction[j] (j为第二个下标),依此类推,最大得分总和为多少?

题解:贪心。先将数字从小到大排序,注意到我们取的一定是数组的后半截(当然也可能全取或不取)。因为假设不是后半截,那么有某个数字,它没取,但是取比它更小的,我们可以用这个数替换掉那个更小的得到更优。所以从后往前,枚举即可

class Solution {
public:
    int maxSatisfaction(vector<int>& satisfaction) {
        sort(satisfaction.begin(), satisfaction.end());
        int ans = 0;
        int cur = 0;
        int sum = 0;
        
        for(int i = satisfaction.size() - 1; i >= 0;  --i){
            sum += satisfaction[i];
            cur += sum;
            ans = max(ans, cur);
        }
        
        return ans;
    }
};
  1. Stone Game III

题意:给定一个数组(有正有负),轮流进行游戏,轮到的人可以取剩下的数前面1~3个数字。Alice先取。Bob后取。拿到的数字之和最大获胜。它们都按最优策略取数。问最后谁赢? 平手输出"Tie"

题解:因为每次都取最前的数。可以看到这个可以用动态规划来解,因为每次最优取数可以看做重新开始的一盘游戏。设dp[i]为从i位置开始,最多获取总和数目(为什么用和,因为总和是固定的,先手的拿多后手的拿的就少,和最多就是最优的解)。
枚举取的数目,1到3,因为总和不变,所以sum[i + j] - dp[i + j]就是除去当前轮后面能取到的和
d p [ i ] = max ⁡ j = 0 , 1 , 2 ( ( ∑ k = 0 j s t o n e V a l u e [ i + k ] ) + s u m [ i + j ] − d p [ i + j ] ) dp[i] = \max_{j=0,1,2} \left(\left(\sum_{k=0}^j stoneValue[i + k]\right)+ sum[i + j] - dp[i + j]\right)dp[i]=j=0,1,2max((k=0jstoneValue[i+k])+sum[i+j]dp[i+j])

class Solution {
public:
    string stoneGameIII(vector<int>& stoneValue) {
        
        int n = stoneValue.size();
        vector<int> dp(n, INT_MIN), sum(n ,0);
        
        for(int i = 0;i < 3; ++i){
            dp.push_back(0);
            sum.push_back(0);
            stoneValue.push_back(0);
        }
        
        for(int i = n - 1; i >= 0; --i){
            sum[i] = sum[i + 1] + stoneValue[i];
            int pick = stoneValue[i];
            for(int j = 1; j <= 3; ++j){
                dp[i] = max(dp[i], pick + sum[i + j] - dp[i + j]);
                pick += stoneValue[i + j];
            }
        }
        
        sum[0] -= dp[0];
        if(dp[0] == sum[0]) return "Tie";
        if(dp[0] < sum[0]) return "Bob";
        return "Alice";
        
        
    }
};
  1. Number of Ways to Paint N × 3 Grid

题意:给一个N*3的网格涂色,有三种颜色可选,相邻的不能同色,问有多少种涂色方案?
题解:
方法1:状态压缩动态规划,dp[i][state]表示n*3的最后一行涂色为state的方案数目
方法2:递推(动态规划),见题解
方法3:方法2+矩阵快速幂

//方法 1
class Solution {
    struct color{
      int a, b, c;  
    };
    
    
public:
    int numOfWays(int n) {
        const int kMod = 1e9 + 7;
        
        vector<color> state;
        bool conflict[12][12] = {0};
         int dp[2][12] = {0};
        
        for(int i = 0; i < 3; ++i){
            for(int j = 0; j < 3; ++j){
                for(int k = 0; k < 3; ++k){
                    if(i == j || j == k)
                        continue;
                    dp[0][state.size()] = 1;
                    state.push_back({i, j, k});
                   
                }   
            }
        }
        assert(state.size() == 12);
        
        for(int i = 0; i < state.size(); ++i){
            conflict[i][i] = true;
            for(int j = i + 1; j < state.size(); ++j){
                if(state[i].a == state[j].a || 
                   state[i].b == state[j].b || 
                   state[i].c == state[j].c)
                    conflict[i][j] = conflict[j][i] = true;
            }
        }
        
       
        int cur = 0;
        long long tmp;
        
        for(int i = 1; i < n; ++i){
            cur ^= 1;
            for(int j = 0; j < state.size(); ++j){
                tmp = 0;
                for(int k = 0; k < state.size(); ++k){
                    if(!conflict[j][k])
                        tmp += dp[cur^1][k];
                }
                dp[cur][j] = tmp % kMod;
                
            }
        }
        
        long long ans = 0;
        for(int i = 0;i < state.size(); ++i){
            ans += dp[cur][i];
        }
        
        return ans % kMod;
    }
};
//https://leetcode-cn.com/problems/number-of-ways-to-paint-n-x-3-grid/solution/shu-xue-jie-jue-fei-chang-kuai-le-by-lindsaywong/
public class Solution {
    public int NumOfWays(int n) {
            if (n == 0)
                return 0;
            else if (n == 1)
                return 12;
            var temp = 1000000007;
            long  repeat = 6;
            long  unrepeat = 6;
            for(int i = 2; i <=n; i++)
            {
                long  newrep = (repeat * 3) % temp + unrepeat * 2 % temp;
                long  newunrep = repeat * 2 % temp + unrepeat * 2 % temp;
                repeat = newrep;
                unrepeat = newunrep;
            }
            return (int)((repeat + unrepeat)%temp);
    }
}
//方法3
class Solution {
    static const int kMod = 1e9 + 7;
    vector<long long> matMul(vector<long long> &a,  vector<long long> &b){
        vector<long long> res(a.size());
        res[0] = (a[0] * b[0] + a[1] * b[2]) % kMod;
        res[1] = (a[0] * b[1] + a[1] * b[3]) % kMod;
        res[2] = (a[2] * b[0] + a[3] * b[2]) % kMod;
        res[3] = (a[2] * b[1] + a[3] * b[3]) % kMod;
        return res;
    }
    
public:
    int numOfWays(int n) {
        vector<long long> mat {1, 0, 0, 1}, tmp = {3, 2, 2, 2};
        
        while(n){
            if(n & 1){
                mat = matMul(mat, tmp);   
            }
            tmp = matMul(tmp, tmp);
            n >>= 1;
        }
        
        return ((mat[1] + mat[3]) * 3) % kMod;
    }
};

版权声明:本文为u010510549原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。