欢迎访问我的博客首页。
牛客最近考过的题
1. 三角形
2021年11月6号美图。
题目:给出一个三角形,计算从三角形顶部到底部的最小路径和,每一步都可以移动到下面一行相邻的数字。例如,给出的三角形如下:[[20],[30,40],[60,50,70],[40,10,80,30]],最小的从顶部到底部的路径和是20 + 30 + 50 + 10 = 110。链接。
分析:假如用二维数组 triangle 存储三角形,三角形的每行 i 都从 triangle[i][0] 开始存储。则 triangle[i][j] 下面一行相邻的数字是 triangle[i+1][j] 和 triangle[i+1][j+1]。
1. 分治算法
使用分治算法提交,没有超时。
class Solution {
public:
int minimumTotal(vector<vector<int>> &triangle) {
int h = triangle.size();
int res = INT_MAX;
fac(triangle, res);
return res;
}
private:
void fac(vector<vector<int>> &triangle, int& res, int x = 0, int y = 0, int sum = 0) {
if (y == triangle.size()) {
res = min(res, sum);
return;
}
sum += triangle[y][x];
fac(triangle, res, x, y + 1, sum);
if (x + 1 <= y + 1)
fac(triangle, res, x + 1, y + 1, sum);
}
};
2. 由上而下的动态规划算法
题目中说,如果你能只用O(N)的额外的空间来完成这项工作的话,就可以得到附加分,其中N是三角形中的行总数。和 01 背包一样,本题中第 i 层仅与第 i-1 层有关,所以我们可以使用一维的 dp 数组。
class Solution {
public:
int minimumTotal(vector<vector<int>> &triangle) {
if (triangle.size() == 0)
return 0;
if (triangle.size() == 1)
return triangle[0][0];
vector<int> dp(triangle.size());
// 1.最小问题的解。
dp[0] = triangle[0][0];
// 2.为求最小值做准备。
for (int i = 1; i < triangle.size(); i++)
dp[i] = INT_MAX;
// 3.递推。
for (int i = 1; i < triangle.size(); i++) {
for (int j = triangle[i].size() - 1; j >= 0; j--) {
if (j == 0)
dp[j] = dp[j] + triangle[i][j];
else if (j == triangle[i].size() - 1)
dp[j] = dp[j - 1] + triangle[i][j];
else
dp[j] = min(dp[j - 1], dp[j]) + triangle[i][j];
}
}
// 4.在dp中找最小值。
int res = dp[0];
for (int i = 1; i < dp.size(); i++)
res = min(res, dp[i]);
return res;
}
};
我们从第 2 行开始往下推导,所以第 6 行提前处理三角形只有一行的情况,这样我们就可以把 dp[0] 初始化为 triangle[0][0]。当然也可以从第 1 行开始往下推导,这时的 dp[0] 应该初始化为何值,就要根据递推公式的需要而决定:
class Solution {
public:
int minimumTotal(vector<vector<int>> &triangle) {
if (triangle.size() == 0)
return 0;
vector<int> dp(triangle.size());
// 1.递推的基础(根据递推公式的需要而决定)。
dp[0] = 0;
// 2.为求最小值做准备。
for (int i = 1; i < triangle.size(); i++)
dp[i] = INT_MAX;
// 3.递推。
for (int i = 0; i < triangle.size(); i++) {
for (int j = triangle[i].size() - 1; j >= 0; j--) {
if (j == 0)
dp[j] = dp[j] + triangle[i][j];
else if (j == triangle[i].size() - 1)
dp[j] = dp[j - 1] + triangle[i][j];
else
dp[j] = min(dp[j - 1], dp[j]) + triangle[i][j];
}
}
// 4.在dp中找最小值。
int res = dp[0];
for (int i = 1; i < dp.size(); i++)
res = min(res, dp[i]);
return res;
}
};
3. 由下而上的动态规划算法
因为三角形最上一层只有一个元素,最下一层有多个元素。所以从上向下推导,需要在 dp 数组中找最小值。而从下向上推导,dp[0] 就是结果。
class Solution {
public:
int minimumTotal(vector<vector<int>> &triangle) {
if (triangle.size() == 0)
return 0;
int H = triangle.size();
vector<int> dp(H);
// 1.最小问题的解。
for (int i = 0; i < triangle[H - 1].size(); i++)
dp[i] = triangle[H - 1][i];
// 2.递推。
for (int i = H - 2; i >= 0; i--) {
for (int j = 0; j < triangle[i].size(); j++) {
dp[j] = min(dp[j], dp[j + 1]) + triangle[i][j];
}
}
return dp[0];
}
};
由上而下的动态规划算法中,dp[j] = min(dp[j - 1], dp[j]) + triangle[i][j],即 dp[j] 与更小的 dp[j-1] 有关。所以为了避免覆盖,第二层循环中 j 是从大到小的。由下而上的动态规划算法中,dp[j] = min(dp[j], dp[j + 1]) + triangle[i][j],即 dp[j] 与更大的 dp[j+1] 有关。所以为了避免覆盖,第二层循环中 j 是从小到大的。
2. 设计LRU缓存结构
题目。
分析:我们可以使用一个数据结构,里面的缓存按新旧程度顺序存放。插入和更新操作在一端执行,删除操作在另一端执行。队列、双端队列、双向链表在一端插入、在另一端删除的时间复杂度都是 O(1)。
但更新时,我们需要找到待更新元素在数据结构中的位置,先删除它,然后重新插入。怎么在 O(1) 的时间复杂度内找到这个待更新的元素且删除它,这是本题的关键。在 O(1) 的时间复杂度内删除元素,只能使用链表;在 O(1) 的时间复杂度内查找元素,只能使用哈希表。所以方法是:使用哈希表,哈希表的键与缓存结点的键相同。哈希表的值是迭代器,它指向链表中与之键相同的结点。这样我们就可以根据键从哈希表中直接定位链表的结点。
因为链表的 erase() 函数的参数只能是迭代器,所以哈希表的值也只能是迭代器。如果我们自己实现链表,哈希表的值当然可以是指针或引用类型。
如果哈希表的值是正向迭代器,最新的缓存结点应该放在双向链表头部;如果哈希表的值是反向迭代器,最新的缓存结点应该放在双向链表尾部。因为链表的迭代器不支持自加自减运算,使用正向迭代器无法直接得到指向链表最后一个元素的迭代器,使用反向迭代器无法直接得到指向链表第一个元素的迭代器。
class Solution {
public:
vector<int> LRU(vector<vector<int>>& operators, int k) {
vector<int> res;
for (auto op : operators) {
if (op.size() == 3)
set(op[1], op[2], k);
else
res.push_back(get(op[1]));
}
return res;
}
private:
void set(int key, int value, int k) {
// 1.如果存在,先删除。
if (ump.find(key) != ump.end()) {
cache.erase(ump[key]);
}
// 2.插入链表头部并记入哈希表。
cache.push_front(Node(key, value));
ump[key] = cache.begin();
// 3.如果缓存过大,就删除链表尾部较久未使用的元素。
if (cache.size() > k) {
ump.erase(cache.back().key);
cache.pop_back();
}
}
int get(int key) {
// 1.找不到。
if (ump.find(key) == ump.end())
return -1;
// 2.为了更新,先记录下来。
Node update(key, ump[key]->value);
// 3.更新:删除。
cache.erase(ump[key]);
// 3.更新:插入链表头部且更新哈希表中的记录。
cache.push_front(update);
ump[key] = cache.begin();
return ump[key]->value;
}
private:
struct Node {
Node(int k = 0, int v = 0) :key(k), value(v) {}
int key, value;
};
list<Node> cache;
unordered_map<int, list<Node>::iterator> ump;
};
3. 最长回文子串
2021年11月8号嘀嘀。详见《LeetCode 5.最长回文子串》。
4. 翻译字符串
2021年11月9号商汤。不知道这道题叫什么名字,面试官给个例子,让写代码。例子如下:
input = "2[ab3[c]]2[de]fg"
output = "abcccabcccdedefg"
嵌套,n>10
首先给几个测试样例供参考:
string str = "2[ab2[c4[de]]]"; //完全的嵌套结构。
string str = "2[2[bc]]"; //完全的嵌套结构,第二个2前面没有字母。
string str = "2[ab3[c]]2[de]"; //一个嵌套结构和一个基本结构。
string str = "2[a2[c]2[d]]"; // 一个嵌套结构包含两个基本结构。
string str = "12[a]"; //数字可能不只1位。
这道题和表达式求值的思路是一样的,甚至比表达式求值更简单,因为表达式求值要分一级运算和二级运算。本题使用两个栈很容易解决。
class Solution {
public:
string fun(string& str) {
if (str.size() == 0)
return "";
stack<char> st1, st2;
// 1.把字符串全部放入st1。
for (auto ch : str)
st1.push(ch);
// 2.把st1的元素向st2转移,每遇到左括号就处理第3步。
while (st1.empty() != true) {
char ch = st1.top();
st2.push(ch);
st1.pop();
if (ch == '[') {
// 3.继续从st1读取数字部分。
string n_string;
while (st1.empty() != true) {
char ch = st1.top();
if (ch < '0' || ch > '9')
break;
n_string = ch + n_string;
st1.pop();
}
int n_digit = atoi(n_string.c_str());
// 4.处理st2中顶部一个括号内的字符串,放入group,group是正序的。
string group;
st2.pop();
while (true) {
char ch = st2.top();
group.push_back(ch);
st2.pop();
if (ch == ']')
break;
}
group.pop_back();
// 5.把n_digit个group放入st2。
while (n_digit--) {
for (int i = group.size() - 1; i >= 0; i--)
st2.push(group[i]);
}
}
}
// 6.把st2中的字符放入字符串。
string res;
while (st2.empty() != true) {
res.push_back(st2.top());
st2.pop();
}
return res;
}
};