动态规划组
1. 数塔问题(动态规划 + 滚动数组)
经典dp了,学动态规划的第一道例题,给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。
[2],
[3,4],
[6,5,7],
[4,1,8,3]
来源:力扣(LeetCode)链接:https://leetcode-cn.com/problems/triangle
思路1
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
+
1
]
)
+
c
[
i
]
[
j
]
dp[i][j] = min(dp[i-1][j], dp[i-1][j+1]) + c[i][j]
dp[i][j]=min(dp[i−1][j],dp[i−1][j+1])+c[i][j]
关键是滚动数组在空间上的优化:通过转移方程可以看出,每一行的最小值,只与下一行有关,因此我们只需要使用2行的dp数组 d p [ 2 ] [ n ] dp[2][n] dp[2][n]即可,由于是枚举行号,因此我们利用行号的奇偶性来判断行号。
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
int leng = triangle.size();
if (leng == 1) return triangle[0][0];
int dp[2][1007] = {0};
int minValue = 99999;
dp[0][0] = triangle[0][0];
for(int i=1;i<leng;++i) {
// 特殊处理最左列
dp[i & 1][0] = triangle[i][0] + dp[i-1 & 1][0];
for (int j=1;j<i;++j) {
dp[i & 1][j] = triangle[i][j] + min(dp[i-1 & 1][j-1], dp[i-1 & 1][j]);
}
// 特殊处理最右列
dp[i & 1][i] = triangle[i][i] + dp[i-1 & 1][i-1];
}
for(int i=0;i<leng;++i) {
minValue = min(dp[leng-1 & 1][i], minValue);
}
return minValue;
}
};
思路2
自下而上,略
2. 不同的二叉搜索树1(动态规划 & 卡特兰数)
给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?
示例:
输入: 3
输出: 5
解释:
给定 n = 3, 一共有 5 种不同结构的二叉搜索树:
1 3 3 2 1
\ / / / \ \
3 2 1 1 3 2
/ / \ \
2 1 2 3
来源:力扣(LeetCode)链接:https://leetcode-cn.com/problems/unique-binary-search-trees
思路:动态规划
在[1…n]中枚举i作为树根,然后递归的枚举[1…i-1],和[i+1…n]。
定义如下信息:
- G ( n ) G(n) G(n):长度为 n 的序列能构成的不同二叉搜索树的个数
- F ( i , n ) F(i, n) F(i,n)以i为根,序列长度为n的不同二叉搜索树个数
推出
- G ( n ) = Σ F ( i , n ) , 1 < = i < = n G(n) = \Sigma F(i,n),1<=i<=n G(n)=ΣF(i,n),1<=i<=n
- F ( i , n ) = G ( i − 1 ) ∗ G ( n − i ) F(i,n) = G(i-1) * G(n-i) F(i,n)=G(i−1)∗G(n−i)
把4式代入3式
G ( n ) = Σ G ( i − 1 ) ∗ G ( n − i ) G(n) = \Sigma{ G(i-1) * G(n-i) } G(n)=ΣG(i−1)∗G(n−i)
class Solution {
public:
int numTrees(int n) {
int g[20] = {1, 1};
for (int i = 2; i <= n; ++ i) {
for (int j = 1; j <= i; ++ j) {
g[i] += g[j-1] * g[i-j];
}
}
return g[n];
}
};
3. 整数拆分(DP & 数学)
https://blog.csdn.net/swallowblank/article/details/107685100
4. 不同的二叉搜索树2(递归 & 记忆化搜索)
给定一个整数 n,生成所有由 1 … n 为节点所组成的 二叉搜索树 。
示例:
输入:3
输出:
[
[1,null,3,2],
[3,2,null,1],
[3,1,null,null,2],
[2,1,3],
[1,null,2,null,3]
]
解释:
以上的输出对应以下 5 种不同结构的二叉搜索树:
1 3 3 2 1
\ / / / \ \
3 2 1 1 3 2
/ / \ \
2 1 2 3
思路:和上一题相似
我们枚举树根,并递归的生成左右子树
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
vector<vector<vector<TreeNode*>>> globalTree;
public:
vector<TreeNode*> generateTrees(int n) {
if (n == 0) return {};
globalTree.resize(n+1, vector<vector<TreeNode*>>(n+1));
return back(1, n);
}
vector<TreeNode*> back(int start, int end) {
if (start > end) return { nullptr };
vector<TreeNode*> allTrees;
if (!globalTree[start][end].empty()) {
return globalTree[start][end];
}
for (int i=start; i<=end; ++i) {
vector<TreeNode*> leftTree = back(start, i-1);
vector<TreeNode*> rightTree = back(i+1, end);
for (auto& leftpt : leftTree) {
for (auto& rightpt : rightTree) {
allTrees.emplace_back(new TreeNode(i, leftpt, rightpt));
}
}
}
globalTree[start][end] = allTrees;
return allTrees;
}
};
5. 寻宝(状压DP)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/xun-bao
这道题根本没想到状压DP,但是用状压BFS做出来了,但是超时了。
class Solution {
public:
bool mark[100][100][1<<11][2];
int M_id[103][103];
int dir[4][2] = {
{-1,0},
{0,1},
{1,0},
{0,-1}
};
int sx,sy,ex,ey,M_num,target;
struct node {
int x, y;
int state;
int hasRock;
int step;
node(int a, int b, int c, int d, int e): x(a), y(b), state(c), hasRock(d), step(e) {}
};
int bfs (vector<string>& maze, int len) {
node start = node(sx, sy, 0, 0, 0);
queue<node> q;
q.push(start);
mark[start.x][start.y][start.state][start.hasRock] = true;
while(!q.empty()) {
node now = q.front(); q.pop();
if (maze[now.x][now.y] == 'T' && mark[now.x][now.y][target][0] == true)
return now.step;
else {
for (int i=0;i<4;++i) {
int new_x = now.x + dir[i][0];
int new_y = now.y + dir[i][1];
if (new_x < 0 || new_x >= len || new_y < 0 || new_y >= len || maze[new_x][new_y] == '#') {
continue;
}
int new_state = now.state, new_hasRock = now.hasRock;
if (maze[new_x][new_y] == 'O' && now.hasRock == 1) {
continue;
}
else if (maze[new_x][new_y] == 'O' && now.hasRock == 0 && now.state != target) {
new_hasRock = 1;
}
if (maze[new_x][new_y] == 'M' && now.hasRock == 1 && (now.state & (1 << M_id[new_x][new_y])) == 0) {
new_state = now.state | (1 << M_id[new_x][new_y]);
new_hasRock = 0;
}
if (mark[new_x][new_y][new_state][new_hasRock] == true) {
continue;
}
q.push(node(new_x,new_y,new_state,new_hasRock,now.step+1));
mark[new_x][new_y][new_state][new_hasRock] = true;
}
}
}
return -1;
}
int minimalSteps(vector<string>& maze) {
memset(mark, false, sizeof(mark));
memset(M_id, 0, sizeof(M_id));
int len = maze.size();
M_num = 0;
for (int i=0; i<len; ++i) {
for (int j=0;j<len;++j) {
if (maze[i][j] == 'S') {
sx = i;
sy = j;
}
else if (maze[i][j] == 'T') {
ex = i;
ey = j;
}
else if (maze[i][j] == 'M') {
M_id[i][j] = M_num;
M_num += 1;
}
}
}
target = (1 << M_num) - 1;
return bfs(maze, len);
}
};
6. 打家劫舍3(DP思路 & 记忆化搜索)
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
示例 1:
输入: [3,2,3,null,3,null,1]
3
/ \
2 3
\ \
3 1
输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
来源:力扣(LeetCode)链接:https://leetcode-cn.com/problems/house-robber-iii
思路:记忆化搜索
经典的拿和不拿问题,如果不拿当前的,则从四个孙子节点选最大的;如果拿当前的,则从两个子节点选最大的再加上自身。
这里我用了一个小技巧,很多题解都没有提到,就是在记忆化搜索中有一个很重要的点就是——在递归的时候判断当前节点是否已经被计算过,如果被计算过,那么直接返回,以避免重复计算。在一般的题目中,我们会用一个额外的数组visited
来判断是否已经被完全计算,但此题由于是数值类型,我用负数来代表完全被计算过,在参与其他节点的运算时,只需取反恢复即可。
class Solution {
public:
int get(TreeNode* root) {
if (root == NULL) return 0;
if (root->val < 0) return -(root->val);
int b = 0;
if (root->left != NULL) {
root->val += get(root->left->left) + get(root->left->right);
}
if (root->right != NULL) {
root->val += get(root->right->left) + get(root->right->right);
}
b += get(root->left) + get(root->right);
root->val = -max(root->val, b);
return -(root->val);
}
int rob(TreeNode* root) {
return get(root);
}
};
图论搜索组
1. 相同的树(DFS、BFS)【简单】
给定两个二叉树,编写一个函数来检验它们是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
示例 1:
输入: 1 1
/ \ / \
2 3 2 3
[1,2,3], [1,2,3]
输出: true
示例 2:
输入: 1 1
/ \
2 2
[1,2], [1,null,2]
输出: false
来源:力扣(LeetCode)链接:https://leetcode-cn.com/problems/same-tree
思路:
没什么好说的, 两个指针同时指向两个数的根节点,同时dfs或bfs判断是否相同。
bool isSameTree(TreeNode* p, TreeNode* q) {
if (p == nullptr && q == nullptr) {
return true;
} else if (p == nullptr || q == nullptr) {
return false;
} else if (p->val != q->val) {
return false;
} else {
return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}
}
2. 判断二分图(BFS & 染色)
给定一个无向图graph,当这个图为二分图时返回true。
如果我们能将一个图的节点集合分割成两个独立的子集A和B,并使图中的每一条边的两个节点一个来自A集合,一个来自B集合,我们就将这个图称为二分图。
graph将会以邻接表方式给出,graph[i]表示图中与节点i相连的所有节点。每个节点都是一个在0到graph.length-1之间的整数。这图中没有自环和平行边: graph[i] 中不存在i,并且graph[i]中没有重复的值。
示例 1:
输入: [[1,3], [0,2], [1,3], [0,2]]
输出: true
解释:
无向图如下:
0----1
| |
| |
3----2
我们可以将节点分成两组: {0, 2} 和 {1, 3}。
来源:力扣(LeetCode)链接:https://leetcode-cn.com/problems/is-graph-bipartite
思路:BFS
图的染色问题,对于图中的任意两个节点 uu 和 vv,如果它们之间有一条边直接相连,那么 uu 和 vv 必须属于不同的集合。
如果给定的无向图连通,那么我们就可以任选一个节点开始,给它染成红色。随后我们对整个图进行遍历,将该节点直接相连的所有节点染成绿色,表示这些节点不能与起始节点属于同一个集合。我们再将这些绿色节点直接相连的所有节点染成红色,以此类推,直到无向图中的每个节点均被染色。
如果我们能够成功染色,那么红色和绿色的节点各属于一个集合,这个无向图就是一个二分图;如果我们未能成功染色,即在染色的过程中,某一时刻访问到了一个已经染色的节点,并且它的颜色与我们将要给它染上的颜色不相同,也就说明这个无向图不是一个二分图。
class Solution {
public:
int belong[107];
bool BFS(int start, vector<vector<int>>& g) {
queue<int> q;
q.push(start);
belong[start] = 0;
while(!q.empty()) {
int now = q.front();q.pop();
for (auto i : g[now]) {
if (belong[i] == -1) {
belong[i] = (belong[now]+1) & 1;
q.push(i);
} else if (belong[i] != ((belong[now]+1) & 1)) {
return false;
}
}
}
return true;
}
bool isBipartite(vector<vector<int>>& graph) {
int length = graph.size();
memset(belong, -1, sizeof(belong));
for ( int i = 0; i < length; ++ i) {
if (graph[i].size() == 0) continue;
else if (belong[i] == -1 && !BFS(i, graph)) return false;
}
return true;
}
};
3. 课程表(判环 & DFS & BFS & 拓扑排序)
你这个学期必须选修 numCourse 门课程,记为 0 到 numCourse-1 。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1]
给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?
示例 1:
输入: 2, [[1,0]]
输出: true
解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。
示例 2:
输入: 2, [[1,0],[0,1]]
输出: false
解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。
来源:力扣(LeetCode)链接:https://leetcode-cn.com/problems/course-schedule
**思路:**判环 & DFS & BFS & 拓扑排序,我只写了深搜判环,广度后续可以再看看
这里主要要对节点做3类标记
- 未搜索
- 正在搜索,即还在遍历子节点
- 完成搜索,该节点的全部子节点遍历完毕
只有做三种标记才能判环,只有visited or !visited是不够的。
class Solution {
public:
vector<int> visited;
vector<vector<int>> g;
bool dfs(int u) {
visited[u] = 1;
for (int v : g[u]) {
if (visited[v] == 1) {
return false;
}
else if (visited[v] == 2) {
if(!dfs(v)) return false;
}
}
visited[u] = 2;
return true;
}
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
if (numCourses == 1) return true;
g.resize(numCourses);
visited.resize(numCourses);
for (auto &i: prerequisites) {
g[i[1]].push_back(i[0]);
}
for(int i=0; i<numCourses; ++i) {
if (visited[i] == 0) {
if (dfs(i)) continue;
else return false;
}
}
return true;
}
};
分治
1. 搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
示例 1:
输入: [1,3,5,6], 5
输出: 2
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int n=nums.size();
if(n==0) return 0;
int left=0,right=n-1;
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]<target) left=mid+1;
else if(nums[mid]==target) return mid;
else right=mid;
}
if(nums[left]<target) return n;
return left;
}
};
智商、数学题
1. 两数之和 II - 输入有序数组(双指针)
给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。
函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。
说明:
- 返回的下标值(index1 和 index2)不是从零开始的。
- 你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
示例:
输入: numbers = [2, 7, 11, 15], target = 9
输出: [1,2]
解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。
来源:力扣(LeetCode)链接:https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted
思路:双指针
使用双指针的实质是缩小查找范围。那么会不会把可能的解过滤掉?答案是不会。假设numbers[i]+numbers[j]=target 是唯一解,其中0≤i<j≤numbers.length−1。初始时两个指针分别指向下标 0和下标numbers.length−1,左指针指向的下标小于或等于 i,右指针指向的下标大于或等于 j。除非初始时左指针和右指针已经位于下标 i 和 j,否则一定是左指针先到达下标 i 的位置或者右指针先到达下标 j 的位置。
如果左指针先到达下标 i 的位置,此时右指针还在下标 j 的右侧,sum>target,因此一定是右指针左移,左指针不可能移到 ii 的右侧。
如果右指针先到达下标 j 的位置,此时左指针还在下标 i 的左侧,sum<target,因此一定是左指针右移,右指针不可能移到 j 的左侧。
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int length = numbers.size();
vector<int> ans;
int i = 0, j = length - 1;
while(i < j) {
if (numbers[i] + numbers[j] < target) {
++i;
} else if (numbers[i] + numbers[j] > target) {
--j;
} else {
ans.push_back(++i);
ans.push_back(++j);
return ans;
}
}
return ans;
}
};
2. 回文对(哈希/字典树 + 回文判断)【困难】
给定一组 互不相同 的单词, 找出所有不同 的索引对(i, j),使得列表中的两个单词, words[i] + words[j] ,可拼接成回文串。
示例 1:
输入:["abcd","dcba","lls","s","sssll"]
输出:[[0,1],[1,0],[3,2],[2,4]]
解释:可拼接成的回文串为 ["dcbaabcd","abcddcba","slls","llssssll"]
来源:力扣(LeetCode)链接:https://leetcode-cn.com/problems/palindrome-pairs
思路:这个题思路很重要
枚举每一个字符串s,再枚举s的每一个下标i,分别拆成前后缀t1,t2两个子串,再分别判断t1,t2是不是回文串,若是回文,判断另外一半子串是否在原数组中存在翻转。
算法实现用到的小技巧:
- 其中判断是否存在翻转串,用到了hash结构
unordered_map
。 - 利用
string_view
代替strng
进行大量的字符串操作,避免大量的耗时
class Solution {
private:
vector<string> wordsRev;
unordered_map<string_view, int> indices;
public:
int findWord(const string_view& s, int left, int right) {
auto iter = indices.find(s.substr(left, right - left + 1));
return iter == indices.end() ? -1 : iter->second;
}
bool isPalindrome(const string_view& s, int left, int right) {
int len = right - left + 1;
for (int i = 0; i < len / 2; i++) {
if (s[left + i] != s[right - i]) {
return false;
}
}
return true;
}
vector<vector<int>> palindromePairs(vector<string>& words) {
int n = words.size();
for (const string& word: words) {
wordsRev.push_back(word);
reverse(wordsRev.back().begin(), wordsRev.back().end());
}
for (int i = 0; i < n; ++i) {
indices.emplace(wordsRev[i], i);
}
vector<vector<int>> ret;
for (int i = 0; i < n; i++) {
int m = words[i].size();
if (!m) {
continue;
}
string_view wordView(words[i]);
for (int j = 0; j <= m; j++) {
if (isPalindrome(wordView, j, m - 1)) {
int left_id = findWord(wordView, 0, j - 1);
if (left_id != -1 && left_id != i) {
ret.push_back({i, left_id});
}
}
if (j && isPalindrome(wordView, 0, j - 1)) {
int right_id = findWord(wordView, j, m - 1);
if (right_id != -1 && right_id != i) {
ret.push_back({right_id, i});
}
}
}
}
return ret;
}
};
3. 整数拆分
https://blog.csdn.net/swallowblank/article/details/107685100
4. 计数二进制子串(脑筋急转弯)
给定一个字符串 s,计算具有相同数量0和1的非空(连续)子字符串的数量,并且这些子字符串中的所有0和所有1都是组合在一起的。重复出现的子串要累计它们出现的次数。
示例 1 :
输入: "00110011"
输出: 6
解释: 有6个子串具有相同数量的连续1和0:“0011”,“01”,“1100”,“10”,“0011” 和 “01”。
请注意,一些重复出现的子串要累计它们出现的次数。
另外,“00110011”不是有效的子串,因为所有的0(和1)没有组合在一起。
来源:力扣(LeetCode)链接:https://leetcode-cn.com/problems/count-binary-substrings
思路:
我们可以将字符串 s 按照 0 和 1 的连续段分组,存在counts 数组中,例如 s = 00111011,可以得到这样的counts 数组:counts={2,3,1,2}。
它们能组成的满足条件的子串数目为 min{u,v},即一对相邻的数字对答案的贡献。
class Solution {
public:
int countBinarySubstrings(string s) {
vector<int> counts;
int ptr = 0, n = s.size();
while (ptr < n) {
char c = s[ptr];
int count = 0;
while (ptr < n && s[ptr] == c) {
++ptr;
++count;
}
counts.push_back(count);
}
int ans = 0;
for (int i = 1; i < counts.size(); ++i) {
ans += min(counts[i], counts[i - 1]);
}
return ans;
}
};
模拟
1. 大数加法(如何优雅的书写,是一个好题)
给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和。
提示:
num1 和num2 的长度都小于 5100
num1 和num2 都只包含数字 0-9
num1 和num2 都不包含任何前导零
你不能使用任何內建 BigInteger 库, 也不能直接将输入的字符串转换为整数形式
来源:力扣(LeetCode)链接:https://leetcode-cn.com/problems/add-strings
我写的和标准答案是有很大差距
#include<cstring>
using namespace std;
class Solution {
public:
string addStrings(string num1, string num2) {
string ans;
int end_1 = num1.size();
int end_2 = num2.size();
int i = end_1 - 1;
int j = end_2 - 1;
int k = 0;
for (;i>=-1 && j >=-1;) {
int now;
if (i<0 && j >= 0) {
now = num2[j] - '0';
}
else if (i>=0 && j<0) {
now = num1[i] - '0';
} else if (i >= 0 && j >=0)
now = num1[i] - '0' + num2[j] - '0';
if (i<0 && j<0 && k == 0) break;
if (i<0 && j<0 && k != 0)
now = 0;
now += k;
if (now >= 10) {
ans.push_back('0'+(now-10));
k = 1;
} else {
ans.push_back('0'+now);
k = 0;
}
if (i > -1)
i --;
if (j>-1)
j --;
}
string aans;
for (int i=ans.size()-1;i>=0;i--) {
aans.push_back(ans[i]);
}
return aans;
}
};
标答
class Solution {
public:
string addStrings(string num1, string num2) {
int i = num1.length() - 1, j = num2.length() - 1, add = 0;
string ans = "";
while (i >= 0 || j >= 0 || add != 0) {
int x = i >= 0 ? num1[i] - '0' : 0;
int y = j >= 0 ? num2[j] - '0' : 0;
int result = x + y + add;
ans.push_back('0' + result % 10);
add = result / 10;
i -= 1;
j -= 1;
}
// 计算完以后的答案需要翻转过来
reverse(ans.begin(), ans.end());
return ans;
}
};