算法学习重启

算法学习笔记


分治:

LeetCode #169 求众数

问题特征:
1、分解出的子问题的解可合并为当前解
2、子问题没有公共子问题,若有,可考虑贪心或者动态规划
3、分解出的子问题可解决,且更容易解决!


滑动窗口:

leetCode #209 长度最小子数组

tips:
1、确定前后指针 哪个是基准(在第一层循环里,一步一步加1)
2、确定某个状态前后,前后指针该怎么动,以进入到下一个查找状态

leetCode #1004 最长1子数组

误区:
窗口大小可以变化,并非一直固定大小窗口,往前移动。

tips:
窗口的滑动:
1、重点:题意转换。把「最多可以把 K 个 0 变成 1,求仅包含 1 的最长子数组的长度」转换为 「找出一个最长的子数组,该子数组内最多允许有 K 个 0 」;
2、右指针往右滑动一个,尝试更优解;左指针尝试调整,使得窗口内0的个数小于等于k。

代码如下:

class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        int left = 0;
        int len = k;
        int zeroNum = 0;
        // 此处直接以right为新的右边界,并判断当前状态进行处理,循环将比较简单
        // 如果写while循环,且每次用right+1 判断是否应该向前一步,将使得情况复杂,且需要考虑初始状态。
        for (int right = 0; right < nums.size(); right++) {
            if (nums[right] == 0) {
                zeroNum++;
                while (zeroNum > k) {
                    if (nums[left] == 0) {
                        zeroNum--;
                    } 
                    left++;
                }
            }
            len = max(right - left + 1, len);
        }
        return len;
    }
};

滑动窗口模板:
思路:
1、以右指针作为驱动,拖着左指针向前走。右指针每次只移动一步,而左指针在内部 while 循环中每次可能移动多步。右指针是主动前移,探索未知的新区域;左指针是被迫移动,负责寻找满足题意的区间。
2、定义何为满意的区间。

拓扑排序:

leetCode #210 课程表问题

tips:
1、有向图检查是否有环,拓扑排序;无向图检查是否有环,并查集。
2、有BFS 和 DFS两种方式,完成拓扑排序,其中,BFS最为经典,需要记住,且记住需要辅助数据结构,邻接表和队列,每次访问入度为0的点。
3、在拓扑排序过程中,需要使用相同key的map,可使用unordered_multimap,并记住其使用迭代器遍历相同key的方式。
4、BFS检查是否有环,全部访问完成后,查看其拓扑排序的点的个数等于全部即可;DFS判断是否有环,在访问点过程中,因此需要辅助数据结构,以(0,1,2)标记当前点所处的访问状态。

        pair<unordered_multimap<int, int>::iterator, unordered_multimap<int, int>::iterator> pos = neb.equal_range(cur);
        while (pos.first != pos.second) {
           
            pos.first++;
        }

BFS代码如下:

class Solution {
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        vector<int> res;
        // 邻接表
        unordered_multimap<int, int> pre;
        // 入度数组
        vector<int> indegree(numCourses, 0);
        // BFS 辅助数据结构,队列
        queue<int> traQueue;
        for (int i = 0; i < prerequisites.size(); i++) {
            indegree[prerequisites[i][0]]++;
            pre.insert(make_pair(prerequisites[i][1], prerequisites[i][0]));
        }
        for (int i = 0; i < numCourses; i++) {
            if (indegree[i] == 0) {
                traQueue.push(i);
            }
        }
        while (!traQueue.empty()) {
            // 遍历相同key,迭代器的使用
            pair<unordered_multimap<int, int>::iterator, unordered_multimap<int, int>::iterator> pos =
                    pre.equal_range(traQueue.front());
            // 队列从尾进,从头部出
            res.push_back(traQueue.front());
            while (pos.first != pos.second) {
                indegree[pos.first->second]--;
                if (indegree[pos.first->second] == 0) {
                    // 拓扑排序,BFS方式,必须保证访问的点入度为0
                    traQueue.push(pos.first->second);
                }
                pos.first++;
            }
            traQueue.pop();
        }
        // 通过判断入度为0的,进入res队列的,是不是等于总的课程数,来确定有没有环
        // 如果没有,则入队列的顺序即为一种topo排序
        if (res.size() == numCourses) {
            return res;
        }
        return {};
    }
};

DFS代码如下:

class Solution {
public:
    stack<int> topoSt;
    unordered_multimap<int, int> neb;

    bool visit(int cur, vector<int> &isVisited) {
        // isVisited 标记非常重要,其三种状态,可直接判断未访问,访问中,已访问完,帮助完成拓扑遍历和判断是否成环
        // 一个点只要其自身和子节点访问完毕,即可判定访问完,无需考虑其前驱
        if (isVisited[cur] == 2) {
            return false;
        }
        if (isVisited[cur] == 1) {
            return true;
        }
        isVisited[cur] = 1;
        // 使用equal_range返回迭代器,来遍历相同key
        pair<unordered_multimap<int, int>::iterator, unordered_multimap<int, int>::iterator> pos = neb.equal_range(cur);
        while (pos.first != pos.second) {
        	// 继续递归,深度遍历下去
            bool isLoop = visit(pos.first->second, isVisited);
            if (isLoop) {
                return true;
            }
            pos.first++;
        }
        isVisited[cur] = 2;
        // 未检测到环,则将当前点推入栈中
        topoSt.push(cur);
        return false;
    }

    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        vector<int> res;
        if (prerequisites.size() == 0) {
            // 对于没有依赖关系,可直接返回一种顺序
            for (int i = 0; i < numCourses; i++) {
                res.push_back(i);
            }
            return res;
        }
        // 对于有重复key情况,使用unordered_multimap
        for (int i = 0; i < prerequisites.size(); i++) {
            neb.insert(make_pair(prerequisites[i][1], prerequisites[i][0]));
        }
        bool isLoop;
        vector<int> isVisited(numCourses, 0);
        // 拓扑排序,从任一点开始即可,因此直接循环开启BFS
        for (int i = 0; i < numCourses; i++) {
            isLoop = false;
            // 深度遍历过程中,直接判断有无成环
            isLoop = visit(i, isVisited);
            if (isLoop) {
                break;
            }
        }
        if (!isLoop) {
        	// 此时,将栈依次pop出,即为一种topo排序
            while (!topoSt.empty()) {
                res.push_back(topoSt.top());
                topoSt.pop();
            }
        }
        return res;
    }
};

差分:

LeetCode #1094

差分思想:

1、源于前缀和;
2、从前往后的一种累加,计算出每一个点上的状态。
3、差分思想的优点在于,每次只用在区间的起点和终点记录下变化,对于区间中的点的状态不用每次都加上。而最后从前往后的累加,即可得出每个点的状态,省去很多区间中的点上的重复计算。

参考博客
https://www.codeleading.com/article/27593349573/

LeetCode #122

心得:

1、差分思想比较明显,有区间,有区间的变化,重点在于捕捉变化;
2、暗含贪心思想,把每一次的增长 纳入囊中;
3、也可以使用动态规划,状态转移为 一天到上一天,而不是一个 持股区间 到上一个 持股区间!!

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        vector<int> change(prices.size(), 0);
        for (int i = 1; i < change.size(); i++) {
            change[i] = prices[i] - prices[i - 1];
        }
        int profit = 0;
        for (int i = 1; i < change.size(); i++) {
            if (change[i] > 0) {
                profit += change[i];
            }
        }
        return profit;
    }
};

并查集:

LeetCode # 547 求省份数量

问题特征:
1、获取 连通图的个数

tips :
1、find的时候同时进行路径压缩
2、合并的时候注意是 祖先跟祖先挂上
3、三个关键函数,find(), union(), returnNum()

此题典型考察并查集思想,解题主要注意以下几点:
1、压缩路径与建立父子关系同步进行,也就是并非先建立所有联系,再进行压缩路径,而是在建联期间 压缩路径。
2、建联的突破口 从循环处理矩阵中为1的(i,j) 位置开始。
3、建联的形式,是先寻找两个点的祖先节点,如果相等就挂上,如果不等,就在(i,j)中选一个当祖先

LeetCode #684

心得:
1、并查集解法不难看出
2、理解,返回最后一条使其不成树的边,就是第一条使其成环的边
3、简便处理,每个点的祖先初始化成自己,而不是-1;
4、切入点:每加入一条边,就对两个点进行分析,归入各自集团。

class Solution {
public:
	int find(vector<int> &parent, int index) {
	int cur = index;
	while (parent[cur] != cur) {
		cur = parent[cur];
	}
	return cur;
}
	vector<int> findRedundantConnection(vector<vector<int>>& edges) {
	vector<int> parent(1001);
	for (int i = 0; i < parent.size(); i++) {
		parent[i] = i; // 祖先或者代表节点,初始化成自己即可
	}
	for (int i = 0; i < edges.size(); i++) {
	int leftP = find(parent, edges[i][0]); // 每加入一条边,就分析两个节点的祖先,判断关系,并归	类。就是此题切入点
	int rightP = find(parent, edges[i][1]);
	if (leftP != rightP) {
		parent[rightP] = leftP;
	} else {
		return edges[i]; // 第一条使其成环的边,就是要求的最后一条边
	}
		}
	return {0, 0};
	}
};

前缀和:

LeetCode #560

心得:
1、此题需看出子数组之和为前后两个前缀和之差,不然容易想到双指针方法,当然,双指针也可以做,但是基于双指针做优化比较难;
2、为了使得边界情况运算成立,需要将坐标0之前的前缀和(也是0),添加进去
3、不需要把所有前缀和算出来之后,再去遍历出结果(这样就又蜕化成了两层循环),而是每算一个都拿去跟前面的前缀和作比较,因此,需要记录之前出现的前缀和及其次数,因此需要用到hash。

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        unordered_map<int, int> preArr;
        int count = 0;
        preArr.insert(make_pair(0, 1));
        int sum = 0;
        for (int i = 0; i < nums.size(); i++) {
            sum += nums[i];
            if (preArr.find(sum - k) != preArr.end()) {
                count += preArr[sum - k];
            }
            if (preArr.find(sum) != preArr.end()) {
                preArr[sum]++;
            } else {
                preArr[sum] = 1;
            }
        }
        return count;
    }
};

LeetCode #974

心得:
1、与560题相同,为前缀和问题
2、不同的是,560题中,两个前缀和相减即可,此题 中,欲得区间是否能被k整除,要对前后两个前缀和进行模运算,并以hash表存之;
3、当模为负数时,要将其“纠正”;

class Solution {
public:
    int subarraysDivByK(vector<int>& nums, int k) {
        unordered_map<int, int> preArr;
        preArr.insert(make_pair(0, 1));
        int sum = 0;
        int count = 0;
        for (int i = 0; i < nums.size(); i++) {
            sum += nums[i];
            int mod = (sum % k + k) % k; // 当模为负数时,应变为正数
            if (preArr.find(mod) != preArr.end()) {
                count += preArr[mod]; // 同模的前缀和之差 即可被k整除
                preArr[mod]++;
            } else {
                preArr[mod] = 1;
            }
        }
        return count;

    }
};

单调栈:

LeetCode #84求最大矩形面积

问题特征:

1、空间换时间

2、“在一维数组空间中,找第一个符合某条件的元素”,比如在LeetCode #84 题(求最大矩形面积) 中,要想求出当前柱形高能圈出的最大矩形面积,一个非常关键的点就是要找到数组右边第一个比它低的柱形。

3、在计算结果时,出现了后遍历到,但是先计算出所需结果的情况(先进后出)的情况。


LeetCode #739

1、寻找下一个较大的数,想到单调栈思想
2、循环数组的遍历空值,循环2 * n -1边;
3、while循环中, index.empty()的判断放在前面,避免另一句访问越界
4、pop完,赋值完之后,记得将当前较大数 push进去。

class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
        vector<int> res(temperatures.size(), 0);
        stack<int> index;
        index.push(0);
        for (int i = 1; i < temperatures.size(); i++) {
            if (temperatures[i] <= temperatures[index.top()]) {
                index.push(i);
            } else {
                while (!index.empty() && temperatures[index.top()] < temperatures[i]) {
                    res[index.top()] = i - index.top();
                    index.pop();
                }
                index.push(i);
            }
        }
        return res;
    }
};

贪心:

leetcode#452 最少射箭次数

弄清为甚按右端排序:
1、循环是从左往右寻找;
2、从场景分析,将箭放在当前气球区间的最右端,既能保证扎破当前气球,又可以尽最大可能扎破更多的气球;

3、注意,自定义cmp函数如果放在solution类中,应申明为static,因为我们普通的成员函数都有一个隐含的this指针,表面上看我们的谓词函数cmp()只有两个参数,但实际上它有三个参数,而我们调用sort()排序函数的时候只需要用到两个参数进行比较,所以就出现了形参与实参不匹配的情况(函数有三个形参,但是只输入了两个实参)。所以,解决办法就是把谓词函数com()定义为static成员函数。

代码如下:

    bool cmp(const vector<int> &a, const vector<int> &b) {
        return a[1] < b[1];
    }

class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        sort(points.begin(), points.end(), cmp);
        int rightIdx = points[0][1];
        int shots = 1;
        for (int i = 1; i < points.size(); i++) {
            if (points[i][0] > rightIdx) {
                shots++;
                rightIdx = points[i][1];
            }
        }
        return shots;
    }
};

前缀树(字典树):

leetcode#208 实现Trie

tips:
1、根据指针是否为空来判断节点内容,也就是说子节点的内容由父节点的指针数组对应元素是否为空决定;
2、无需重复创建节点;
3、查找和 查找前缀可以抽函数;

class Trie {
private:
    bool isEnd;
    Trie *next[26];
public:
    Trie() {
        isEnd = false;
        memset(next, 0x00, sizeof(next));
    }
    // 关键抽函数,先找到对应node再说
    Trie* searchPrefix(string prefix) {
        Trie* node = this;
        for (char ch : prefix) {
            ch -= 'a';
            if (node->next[ch] == nullptr) {
                return nullptr;
            }
            node = node->next[ch];
        }
        return node;
    }
    
    void insert(string word) {
        if (word.empty()) {
            return;
        }
        Trie *Cur = this;
        for (int i = 0; i < word.size(); i++) {
            // 如果已经有此节点,无需重复创建
            if (Cur->next[word[i] - 'a'] == nullptr) {
                Cur->next[word[i] - 'a'] = new Trie();
            }
            if (i == word.size() - 1) {
                Cur->next[word[i] - 'a']->isEnd = true;
                return;
            }
            Cur = Cur->next[word[i] - 'a'];
        }
        return;
    }
    
    bool search(string word) {
        Trie *node = this->searchPrefix(word);
        return node != nullptr && node->isEnd == true;
    }
    
    bool startsWith(string prefix) {
        return this->searchPrefix(prefix) != nullptr;
    }
};

BFS :

leetcode#130 被包围的区域

1、连通问题,思考能否用并查集解决
2、解答之初,曾尝试 使用BFS + DFS ,导致思路混乱,以后慎用
3、并查集做法 要重点记住 union的方法,find中的路径压缩方法;

BFS方法

class Solution {
public:
    // 注意此处 二维数组申明初始化方式
    int dir[4][2] = {{1, 0}, {0, -1}, {-1, 0}, {0, 1}};

    void BFSFunc(vector<vector<char>>& board, int xIdx, int yIdx) {
        int rowSize = board.size();
        int column = board[0].size();
        queue<pair<int, int>> traQue;
        traQue.push(make_pair(xIdx, yIdx));
        while (!traQue.empty()) {
            pair<int, int> traNode = traQue.front();
            int traX = traNode.first;
            int traY = traNode.second;
            for (int i = 0; i < 4; i++) {
                // 注意此处,坐标转移方法
                int nextX = traX + dir[i][0];
                int nextY = traY + dir[i][1];
                if (nextX >= 0 && nextX < rowSize && nextY >= 0 && nextY < column && board[nextX][nextY] == 'O') {
                    board[nextX][nextY] = '#';
                    traQue.push(make_pair(nextX, nextY));
                }
            }
            traQue.pop();
        }
    }

    void solve(vector<vector<char>>& board) {
        int rowSize = board.size();
        int column = board[0].size();
        for (int i = 0; i < rowSize; i++) {
            for (int j = 0; j < column; j++) {
                if ((i == 0 || i == rowSize - 1 || j == 0 || j == column - 1) && board[i][j] == 'O') {
                    board[i][j] = '#';
                    BFSFunc(board, i, j);
                }
            }
        }
        for (int i = 0; i < rowSize; i++) {
            for (int j = 0; j < column; j++) {
                if (board[i][j] == 'O') {
                    board[i][j] = 'X';
                }
                if (board[i][j] == '#') {
                    board[i][j] = 'O';
                }
            }
        }
    }
};

并查集方法:

class Solution {
public:
    // 注意此处 二维数组申明初始化方式
    int dir[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};

    vector<int> parent;
    int virtualNode;

    int toIdx(vector<vector<char>>& board, int x, int y) {
        return board[0].size() * x + y;
    }

    void unionElement(int left, int right)
    {
    	// 按条件进行union
        int leftRoot = find(left);
        int rightRoot = find(right);
        if (leftRoot == virtualNode) {
            parent[rightRoot] = leftRoot;
            return;
        }
        if (rightRoot == virtualNode) {
            parent[leftRoot] = rightRoot;
            return;
        }
        parent[leftRoot] = rightRoot;
    }

    int find(int node)
    {
        int tmpRoot = parent[node];
        while (parent[tmpRoot] != tmpRoot)
        {
            tmpRoot = parent[tmpRoot];
        }
        //路径压缩,重点记住
        int next;
        while(node != parent[node]){
            next = parent[node];
            parent[node] = tmpRoot;
            node = next;
        }
        return tmpRoot;
    }

    bool isConnected(int left, int right)
    {
        int leftRoot = find(left);
        int rightRoot = find(right);
        if (leftRoot == rightRoot) {
            return true;
        }
        return false;
    }

    void solve(vector<vector<char>>& board) {
        for (int i = 0; i < board.size() * board[0].size() + 1; i++) {
            parent.push_back(i);
        }
        virtualNode = board.size() * board[0].size();
        for (int i = 0; i < board.size(); i++) {
            for (int j = 0; j < board[0].size(); ++j) {
                if (board[i][j] == 'O') {
                    int idx = toIdx(board, i, j);
                    if (i == 0 || i == board.size() - 1 || j == 0 || j == board[0].size() - 1) {
                        // 边界上的'O'直接跟虚拟点 连通
                        unionElement(idx, virtualNode);
                    } else {
                        for (int p = 0; p < 4; p++) {
                            int nextX = i + dir[p][0];
                            int nextY = j + dir[p][1];
                            if (nextX >= board.size() || nextX < 0 || nextY >= board[0].size() || nextY < 0) {
                                continue;
                            }
                            if (board[nextX][nextY] == 'O') {
                                int nextIdx = toIdx(board, nextX, nextY);
                                unionElement(idx, nextIdx);
                            }
                        }
                    }
                }
            }
        }
        for (int i = 0; i < board.size(); i++) {
            for (int j = 0; j < board[0].size(); j++) {
                int node = toIdx(board, i, j);
                if (board[i][j] == 'O' && !isConnected(node, virtualNode)) {
                    board[i][j] = 'X';
                }
            }
        }
    }
};

BFS & DFS:

leetcode#934 最短的桥

1、矩阵问题,如果允许,可以修改元素的值,以达到标记和计算的作用;
2、岛屿问题,可观察题意,此处也可以用并查集,但是比较麻烦;
3、此题先找到第一个岛屿每个元素,再“扩散”至第二岛屿,思路比较简单。

class Solution {
public:    
    int dir[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
    queue<pair<int, int>> firstBridge;
    
    void DFSBridge(vector<vector<int>>& grid, int x, int y) {
        firstBridge.push(make_pair(x, y));
        // 将第一岛屿的元素都改为2,以示标记
        grid[x][y] = 2;
        for (int i = 0; i < 4; i++) {
            int nextX = x + dir[i][0];
            int nextY = y + dir[i][1];
            if (nextX < 0 || nextX >= grid.size() || nextY >= grid[0].size() || nextY < 0) {
                continue;
            }
            if (grid[nextX][nextY] == 1) {
                DFSBridge(grid, nextX, nextY);
            }
        } 
    }
    
    int shortestBridge(vector<vector<int>>& grid) {
        bool out = false;
        // 要跳出两层循环,需如此break
        for (int i = 0; i < grid.size(); i++) {
            if (out) {
                break;
            }
            for (int j = 0; j < grid[0].size(); j++) {
                if (grid[i][j] == 1) {
                    // 先找到第一岛屿,并深度遍历完
                    DFSBridge(grid, i, j);
                    out = true;
                    break;
                }
            }
        }
        // 扩散到第二岛屿,因此,使用宽度优先遍历
        while (!firstBridge.empty()) {
            int x = firstBridge.front().first;
            int y = firstBridge.front().second;
            firstBridge.pop();
            for (int i = 0; i < 4; i++) {
                int nextX = x + dir[i][0];
                int nextY = y + dir[i][1];
                if (nextX < 0 || nextX >= grid.size() || nextY >= grid[0].size() || nextY < 0 || grid[nextX][nextY] >= 2) {
                    continue;
                }
                if (grid[nextX][nextY] == 1) {
                    if (grid[x][y] == 2) {
                        return 0;
                    } else {
                        return grid[x][y] - 2;
                    }
                }
                // 将非岛屿改为递增数字,一则标识,二则记录距离
                if (grid[nextX][nextY] == 0) {
                    grid[nextX][nextY] = grid[x][y] + 1;
                    firstBridge.push(make_pair(nextX, nextY));
                }
            }
        }
        return 0;
    }
};

动态规划:

leetcode#322 零钱兑换

1、判断为典型组合问题,思考能否走动态规划;

2、首先尝试将问题用递归解决,amount 在选择一个coin[ i ]之后,转化为 求amount - coin[ i ]的子问题,最开始曽想第一步就把 算出用多少个 coin[ i ],根本原因是找错了递归的方式。

3、找到递归之后,寻找重复子解,然后自下而上构建状态转移公式;

4、具体而言,此题中需要 注意边界情况:amount = 0 时,返回0,而不是-1,因为,-1代表此步无解,而amount到最后必然后减至0,因此,amount = 0不能为-1;

class Solution {
public:
    int *memo;
    int coinChange(vector<int>& coins, int amount) {
        // 注意此处动态数组的new的方式
        // 如果初始化则,memo = new int[amount + 1]{i, i, i, ......};
        // 但是这里不是vector, 不能(n, i)的方式初始化
        memo = new int[amount + 1]();
        for (int i = 0; i < amount + 1; i++) {
            memo[i] = -1;
        }
        memo[0] = 0;
        for (int i = 1; i < amount + 1; i++) {
            int res = INT32_MAX;
            for (int j = 0; j < coins.size(); j++) {
                if (i - coins[j] < 0) {
                    continue;
                }
                if (memo[i - coins[j]] != -1) {
                    res = min(res, memo[i - coins[j]]);
                }
            }
            if (res != INT32_MAX) {
                memo[i] = res + 1;
            }
        }
        return memo[amount];
    }
};

leetcode#213 打家劫舍

总结主要有两点:

1、状态转移的步长不要太大,比如此题中,仅仅考虑当前 这家“需不需要进去打劫”,即可递归至下一状态(在打劫下一家时,能获得的最大收益);

2、此题有#198 问题变型题,主要难点在于,将头尾特殊情况,分两种情况进行讨论,即可拆解,再取最大值。

3、套路不变,依然是递归->记忆化搜索->动态规划;

class Solution {
public:
    int rob(vector<int>& nums) {
        vector<int> memo(nums.size(), 0);
        memo[0] = nums[0];
        if (nums.size() == 1) {
            return memo[0];
        }
        memo[1] = max(nums[0], nums[1]);
        if (nums.size() == 2) {
            return memo[1];
        }
        int res1;
        // case1:抢了第一家,因此最后一家不抢
        for (int i = 2; i < nums.size() - 1; i++) {
            memo[i] = max(nums[i] + memo[i - 2], memo[i - 1]);
        }
        res1 = memo[nums.size() - 2];
        // case2:不抢第一家,因此最后一家算进去
        memo[1] = nums[1];
        memo[2] = max(nums[1], nums[2]);
        for (int i = 3; i < nums.size(); i++) {
            memo[i] = max(nums[i] + memo[i - 2], memo[i - 1]);
        }
        int res2 = memo[nums.size() - 1];
        return max(res1, res2);
    }
};

leetCode#5最长回文子串

1、寻找状态转移公式,与其他简单DP问题不同,此题dp[i][j] 需转移至dp[i + 1][j - 1] 上;
2、编码过程中,需注意for循环中i 和 j 的位置问题,因为 要转移至 dp[i + 1][j - 1]上,因此第j - 1 列的内容必须依据被算出,因此最外层循环应该是j ,即以列为单位来计算。

3、回文天然具有「状态转移」性质:一个回文去掉头尾字符以后,剩下的部分依然是回文。反之,如果一个字符串头尾两个字符都不相等,那么这个字符串一定不是回文。
4、「动态规划」的方法先找递归,而递归从题目的性质着手。

代码如下:

class Solution {
public:
	int length = 1;
	int leftIdx = 0;
    string longestPalindrome(string s) {
        int len = s.length();
        vector<vector<bool> > isSub(s.length(), vector<bool>(len)); // 注意动态二维数组创建方式
        for (int i = 0; i < s.length(); i++) {
            isSub[i][i] = true;
        }
        // 注意for循环顺序,先循环j
        for (int j = 1; j < s.length(); j++) {
            for (int i = 0; i < j; i++) {
				if (s[i] == s[j]) {
					if (i + 2 >= j) {
						isSub[i][j] = true;
					} else {
						isSub[i][j] = isSub[i + 1][j - 1];
					}
				} else {
					isSub[i][j] = false;
				}
				if (isSub[i][j] == true && j - i + 1 > length) {
					length = j - i + 1;
					leftIdx = i;
				}
            }
        }
	    return s.substr(leftIdx, length); // 注意用法,起点 + 长度
    }
};

leetCode#376 摆动序列

1、 理解题目波动场景的特殊性,从 status【i】 到 status【i - 1】,仅记一个辅助遍历还不够,新增 down【】 和 up【】,并推导出公式;

2、贪心算法的关键在于,为了使得波动最长,应砍掉所有 单调区间的中间元素,继而仅记录波峰和波谷元素,拼接即最长序列;

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if (nums.size() == 1) {
            return 1;
        }
        vector<int> down(nums.size(), 1);
        vector<int> up(nums.size(), 1);
        for (int i = 1; i < nums.size(); i++) {
            if (nums[i] > nums[i - 1]) {
                up[i] = max(up[i - 1], down[i - 1] + 1);
                down[i] = down[i - 1];
            }
            if (nums[i] < nums[i - 1]) {
                up[i] = up[i - 1];
                down[i] = max(up[i - 1] + 1, down[i - 1]);
            }
            if (nums[i] == nums[i - 1]) {
                up[i] = up[i - 1];
                down[i] = down[i - 1];
            }
        }
        return max(down[nums.size() - 1], up[nums.size() - 1]);
    }
};

leetCode#300 最长增长子序列

1、一开始考虑题目问什么,就把什么定义成状态。题目问最长上升子序列的长度,其实可以把「子序列的长度」定义成状态,但是发现「状态转移」不好做。

2、基于「动态规划」的状态设计需要满足「无后效性」的设计思想,同时nums[i + 1] 能否接在 nums[ i ]构成子序列,取决于nums[ i ]在这个序列中,所以最后将状态定义为「以 nums[i] 结尾 的「上升子序列」的长度」。

「无后效性」的设计思想:让不确定的因素确定下来,以保证求解的过程形成一个逻辑上的有向无环图。这题不确定的因素是某个元素是否被选中,而我们设计状态的时候,让 nums[i] 必需被选中,这一点是「让不确定的因素确定下来」,也是这样设计状态的原因。

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        vector<int> length(nums.size(), 1);
        int maxLen = 0;
        for (int i = 0; i < nums.size(); i++) {
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    length[i] = max(length[j] + 1, length[i]);
                }
            }
            maxLen = max(maxLen, length[i]);
        }
        return maxLen;
    }
};

二分查找:

leetcode#81 搜索旋转排序数组 II

1、循环条件:left <= right , 注意 left == right 成立!
2、维护 循环区间不变量 ,保持迭代的正确性,即 left ,right 移动之后,绝对不能把可能 的 target 漏到区间外面去了;
3、防止 while 死循环, 这里 left 和 right 至少要有一个 赋值为 mid + 1 或者 mid -1, 区间长度每次至少减少1;

最好写左闭右开的二分算法(维护的 是[left, right) 区间,因此没找到最终返回的也应该是left):

        while (left < right) {
            mid = left + ((right - left) >> 1);
            if(target < nums.at(mid))   //如果小于num[mid],那么target在区间[left,mid)之间,mid不可能但依然为边界,为开区间
                right = mid;
            else if (target > nums.at(mid))  //如果大于num[mid],那么target在区间[mid+1,right)之间
                left = mid + 1;
            else  //如果等于num[mid],那么直接返回
                return mid;
        }
        return left;
    

本题解法:

class Solution {
public:
    bool search(vector<int>& nums, int target) {
        int gap = 0;
        int left = 0;
        int right = nums.size() - 1;
        // “回退” 右坐标,使得后部分严格小于前部分
        while (nums[right] == nums[left] && right > left) {
            right--;
        }
        // 若元素均相等,可直接判断
        if (left == right) {
            if (nums[left] == target) {
                return true;
            } else {
                return false;
            }
        }
        int midIdx = 0;
        // 找到旋转点所在,注意left == right 仍然满足循环条件
        while (left <= right) {
            // 若发现当前区间已满足非降序排列,则直接以最后坐标为旋转点
            if (nums[right] >= nums[0]) {
                gap = right;
                break;
            }
            midIdx = left + (right - left) / 2;
            if (midIdx + 1 < nums.size() && nums[midIdx] > nums[midIdx + 1]) {
                gap = midIdx;
                break;
            }
            if (midIdx - 1 >= 0 && nums[midIdx - 1] > nums[midIdx]) {
                gap = midIdx - 1;
                break;
            }
            // 注意此处left,right 赋值方式,此处维持了左闭右闭区间,要至少保证left,right有一个赋值为midIdx + 1 或者 midIdx - 1
            if (nums[midIdx] <= nums[right]) {
                right = midIdx - 1;
            }
            if (nums[midIdx] >= nums[left]) {
                left = midIdx;
            }
        }
        // 大于最大值,直接返回
        if (target > nums[gap]) {
            return false;
        }
        if (target < nums[0]) {
            left = gap + 1;
            right = nums.size() - 1;
        } else {
            left = 0;
            right = gap;
        }
        while (left <= right) {
            midIdx = left + (right - left) / 2;
            if (nums[midIdx] == target) {
                return true;
            }
            // 此处维护的是左闭右闭区间
            if (nums[midIdx] > target) {
                right = midIdx - 1;
            } else {
                left = midIdx + 1;
            }
        }
        return false;
    }
};

快排:

leetCode#215 数组中的第K个最大元素

1、C++ 随机数写法
2、快排的写法

class Solution {
public:
    int findKthLargestInner(vector<int>& nums, int startIdx, int endIdx, int k) {
        // tips:C++ 随机数写法
        int baseIdx = rand() % (endIdx - startIdx + 1) + startIdx;
        swap(nums[startIdx], nums[baseIdx]);
        
        int left = startIdx;
        int right = endIdx;
        int target = nums[startIdx];
        while (left < right) {
            // tips: 此处是 <= 号, =基准的数字在任一边都行,不影响基准的位置
            // 以左边的元素为基准,因此,必须right先开始动
            while (nums[right] <= target && left < right) {
                right--;
            }
            while (nums[left] >= target && left < right) {
                left++;
            }
            // tips: 如果left == right, 无须swap
            if (left < right) {
                swap(nums[left], nums[right]);
                // swap之后,left,right 不必++ --,因为下一次循环自会处理
            }
        }
        // right先动的,left必在左区间内,可swap
        swap(nums[left], nums[startIdx]);
        if (left + 1 == k) {
            return nums[left];
        } else if (left + 1 < k) {
            return findKthLargestInner(nums, left + 1, endIdx, k);
        } else {
            return findKthLargestInner(nums, startIdx, left - 1, k);
        }
    }

    int findKthLargest(vector<int>& nums, int k) {
        return findKthLargestInner(nums, 0, nums.size() - 1, k);
    }
};


小顶堆:

leetCode#215 数组中的第K个最大元素

1、优先队列的声明
2、小顶堆的声明方式,greater

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        // 升序队列,小顶堆
        // greater<int> 是升序排列方式
        priority_queue<int, vector<int>, greater<int>> myqueue;

        for (int i = 0; i < nums.size(); ++i) {
            if (myqueue.size() < k) {
                myqueue.emplace(nums[i]);
            } else {
                if (myqueue.top() < nums[i]) {
                    myqueue.pop();
                    myqueue.push(nums[i]);
                }
            }
        }
        return myqueue.top();
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值