Leetcode 每日一题和每日一题的下一题刷题笔记 12/30
写在前面
这是我参与更文挑战的第12天,活动详情查看:更文挑战
快要毕业了,才发现自己被面试里的算法题吊起来锤。没办法只能以零基础的身份和同窗们共同加入了力扣刷题大军。我的同学们都非常厉害,他们平时只是谦虚,口头上说着自己不会,而我是真的不会。。。乘掘金鼓励新人每天写博客,我也凑个热闹,记录一下每天刷的前两道题,这两道题我精做。我打算每天刷五道题,其他的题目嘛,也只能强行背套路了,就不发在博客里了。
本人真的只是一个菜鸡,解题思路什么的就不要从我这里参考了,编码习惯也需要改进,各位如果想找刷题高手请教问题我觉得去找 宫水三叶的刷题日记 这位大佬比较好。我在把题目做出来之前尽量不去看题解,以免和大佬的内容撞车。
另外我也希望有得闲的大佬提供一些更高明的解题思路给我,欢迎讨论哈!
好了废话不多说开始第十二天的前两道题吧!
2021.6.12 每日一题
这道题看不懂题面,但是看到第一个例子就大致明白了。。。这应该也是背包问题,而且没那么单纯。各位可以列一下状态转移方程,会发现一下子连定义都定义不出来。列出来了?很好,这道题用完全背包和多重背包的方法来做。没列出来,嗯,这道题确实不是一次背包问题就完了,要用两次背包问题,或者一次背包问题一次单调栈,或者一次背包问题,再来一个记忆化。这么提示不知道能不能反应过来,这个问题有两个子问题,首先要把能获取到的最大位数找出来,然后把成本数位排列组合找数位排列组成的最大的数。第一个问题肯定是背包问题,后面这个随意,看个人爱好,不要超时就好。
完全背包做法
完全背包就是物品无限,这道题的状态转移方程比较难搞,我不打算按照前几天那种优化空间复杂度的形式来讲了,少一个维度这种题太难讲太难理解了。
dp[i][j] 表示现在看到前 i 个元素种类,现在的总成本是 j,这种情况下构成的最大整数(整数是字符串的形式,需要定一个无效状态,比如这里用 '#')。有没有发现我没有定义一个“i 位元素选了多少个”类似这种的数组,定义这种维度的话状态转移就太麻烦了,自己可以试一试。所以一般的完全背包问题状态转移只会从两种状态转移过来,第一种是“看到了但不取,就是玩儿”,第二种是“看到了取一下,咱看后续”。我在第十天也提到了这个思路,各位可以去看看第十天的笔记,放到结尾参考链接里了。这样就能写状态转移方程了,这里 i 是从 1 开始。
这里前面这个 dp[i - 1][j] 是“看到了但不取,就是玩儿”,后面的 dp[i][j - cost[i]] + value[i] 是“看到了取一下,咱看后续”,这里可没说到底取了多少,反正至少取了一个,也就是说,cost[i] 和 value[i] 都是第 i 个元素种类上的总成本和总价值,这么说太虚了,放到代码里再看看效果。
对了,初始状态这里有很多的无效状态,老生常谈了,只有 dp[0][0] 是有意义的,其他的 dp[0][j] 是没有意义的,这些状态的值赋 '#'。然后我们这里遍历的 i 是元素种类的编号,编号肯定是越来越大的,所以后面新加的数位一定要加到之前获得的最大值的开头,这里用字符串就方便了,正好数字很有可能非常大,这里算是题目在给我们降低难度了,题面上不说大整数用字符串来存也是可以的,假如题面上不说,估计有一批人会用数组模拟的队列来做,一部分人直接拼接字符串。同时,因为数字很大,数字比大小,要自己实现一下了,字符串,或者数组,或者队列,比较的思路大同小异。先比位数,位数多的肯定大,如果位数一样多,按位比较,某一位上出现大小关系了,整个数的大小关系也就确定了。
代码如下:
class Solution {
public:
int cost[9 + 5];
string dp[9 + 5][5000 + 5];
// 返回两者较大的一个
string string_max(const string &lhs, const string &rhs) {
if (lhs.size() > rhs.size()) return lhs;
if (rhs.size() > lhs.size()) return rhs;
// 当两字符串长度相等时
if (lhs > rhs) return lhs;
else return rhs;
}
string largestNumber(vector<int>& c, int target) {
int len = c.size();
for (int i = 0; i < len; ++i) {
cost[i + 1] = c[i];
}
// dp[i][j]表示前i个元素, 恰好构成成本为j时, 构成的最大的整数(整数用字符串表示)
// 无效状态用'#'表示
for (int j = 0; j <= target; ++j) {
dp[0][j] = '#';
}
dp[0][0] = "";
for (int i = 1; i <= 9; ++i) {
for (int j = 0; j <= target; ++j) {
string a, b;
// 不选第i个
a = dp[i - 1][j];
// 加选一个
if (j - cost[i] >= 0 && dp[i][j - cost[i]] != "#")
b = std::to_string(i) + dp[i][j - cost[i]];
dp[i][j] = string_max(a, b);
}
}
if (dp[9][target] == "#") return "0";
else return dp[9][target];
}
};
作者:ofshare
链接:https://leetcode-cn.com/problems/form-largest-integer-with-digits-that-add-up-to-target/solution/xiang-xi-jiang-jie-wan-quan-bei-bao-zhuang-tai-de-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
我对完全背包掌握的不好,所以这里先用 OFShare 大佬的题解代码顶上了。
他的评论区里有另一位大佬 sholmes9091 提供的思路,假如几个数位上的成本一样,只会去取最大数位上的相同成本。
class Solution {
private:
string MyStrCmp(const string& lhs, const string& rhs)
{
if (lhs.size() > rhs.size())
return lhs;
else if (rhs.size() > lhs.size())
return rhs;
else
{
return lhs > rhs ? lhs : rhs;
}
}
public:
string largestNumber(vector<int>& cost, int target) {
// only keep the bigger digit with same cost
map<int, int> mp;
for (int i = cost.size() - 1; i >= 0; --i)
{
if (find_if(mp.begin(), mp.end(),
[&cost, i](auto& x)
{
return x.second == cost[i];
}
) == mp.end())
{
mp[i+1] = cost[i];
}
}
string dp[mp.size()+1][target + 1];
dp[0][0] = "";
for (int j = 1; j <= target; ++j)
{
dp[0][j] = "#";
}
for (auto it = mp.begin(); it != mp.end(); ++it)
{
int i = distance(mp.begin(), it) + 1;
int digit = it->first;
int c = it->second;
for (int j = 0; j <= target; ++j)
{
string a, b;
// do not pick ith digit
a = dp[i - 1][j];
// pick one more ith digit
if (j - c >= 0 && dp[i][j - c] != "#")
{
b = to_string(digit) + dp[i][j - c];
}
dp[i][j] = MyStrCmp(a, b);
}
}
if (dp[mp.size()][target] == "#")
return "0";
else
return dp[mp.size()][target];
}
};
这个思路也非常好,过段时间我准备按照这种思路拿哈希表把有最大数位的相同成本摘出来做一下这道题,检验一下这个月刷题的成果。
背包问题分步
好了,不用分步考虑问题的解法已经给出来了,接下来是分步考虑问题的解法。
计算最终答案的位数
那么,先来处理第一个子问题。现在状态转移方程这么写,这里 i 从 0 开始。
这个状态转移方程只有在 的时候才能用,不满足这个条件的话,其实就相当于“看到了但不取,就是玩儿”这种转移。懂的都懂,我不写了。现在的状态方程和之前的区别就是价值变成了最终答案的位数。另外,一开始那些没有意义的地方也不能随便塞一个没意义的东西了,现在价值是答案的位数,要让没意义的东西一直没意义,那让这里是一个非常小的负数就可以了,+1 的操作使得它还是一个非常小的负数。
写一下这部分的代码:
class Solution {
public:
string largestNumber(vector<int> &cost, int target) {
vector<vector<int>> dp(10, vector<int>(target + 1, INT_MIN));
dp[0][0] = 0;
for (int i = 0; i < 9; i++) {
int c = cost[i];
for (int j = 0; j <= target; j++) {
if (j < c) {
dp[i + 1][j] = dp[i][j];
} else {
if (dp[i][j] > dp[i + 1][j - c] + 1) {
dp[i + 1][j] = dp[i][j];
} else {
dp[i + 1][j] = dp[i + 1][j - c] + 1;
}
}
}
}
}
};
有些地方还空着对吧,然后也没有返回值,代码还没写完,空着的地方填就是下一个子问题需要的东西。
计算最终答案
计算最终答案,这一步用另一个二维数组 from[i + 1][j] 来存现在这个状态从哪里来,这又要写状态转移方程了。
这里转移来源的确定就是看 from[i + 1][j] == j,如果是,那就是从 i 转移过来的,不用管是不是看到了,不等于就肯定是从 i + 1 转移过来的。
其实 from[i + 1][j] 这个二维数组存在的意义就是这个数组是倒着来用的,而前面是正着循环,分开的话好区分,不容易混。
倒着来,肯定要先多用靠前的编号,前面的编号更大。所以当 dp[i][j] == dp[i + 1][j - cost[i]] 时肯定选择后面这种往现在的状态上转移。
之前我还说要找个排序,现在这种倒着来的思路直接按顺序加入到字符串末尾就是最大的顺序。
class Solution {
public:
string largestNumber(vector<int> &cost, int target) {
vector<vector<int>> dp(10, vector<int>(target + 1, INT_MIN));
vector<vector<int>> from(10, vector<int>(target + 1));
dp[0][0] = 0;
for (int i = 0; i < 9; i++) {
int c = cost[i];
for (int j = 0; j <= target; j++) {
if (j < c) {
dp[i + 1][j] = dp[i][j];
from[i + 1][j] = j;
} else {
if (dp[i][j] > dp[i + 1][j - c] + 1) {
dp[i + 1][j] = dp[i][j];
from[i + 1][j] = j;
} else {
dp[i + 1][j] = dp[i + 1][j - c] + 1;
from[i + 1][j] = j - c;
}
}
}
}
if (dp[9][target] < 0) {
return "0";
}
string ans;
int i = 9, j = target;
while (i > 0) {
if (j == from[i][j]) {
i--;
} else {
ans += '0' + i;
j = from[i][j];
}
}
return ans;
}
};
这种方法很好理解,把正着走和倒着来分开存,讲起来也不拧巴。
但是,根据我前几天的笔记,肯定是有降低空间复杂度的做法的,降低空间复杂度那就省掉当前这个 i 编号数位。每一步都要找到自己的来源,出现 dp[j] == dp[j - cost[i]] 为真的情况直接从 dp[j - cost[i]] 这里转过来。
class Solution {
public:
string largestNumber(vector<int> &cost, int target) {
vector<int> dp(target + 1, INT_MIN);
dp[0] = 0;
for (int c : cost) {
for (int j = c; j <= target; j++) {
dp[j] = max(dp[j], dp[j - c] + 1);
}
}
if (dp[target] < 0) {
return "0";
}
string ans;
for (int i = 8, j = target; i >= 0; i--) {
for (int c = cost[i]; j >= c && dp[j] == dp[j - c] + 1; j -= c) {
ans += '1' + i;
}
}
return ans;
}
};
同样是留个坑给自己填,省空间但不分步的解法我之后会补充在前面的完全背包那里。
2021.6.12 每日一题下面的题
前面这个题感觉自己一下做了三四道题。。。现在脑子有点抽抽。
力扣给我胡乱塞了一道 MYSQL 题,哎没办法,假如面试的时候也这么搞不知道我面试是不是就没戏了,感觉我在语言之间切换的前摇有点长啊。
我还是参考大佬的思路来做这道题吧,官方题解这个不够通用,看完评论区发现了这个思路:
with t1 as (
select
id,
visit_date,
people,
# 求出差值,因为id一定不会相同,所以使用最熟悉的rank就好
id-rank() over(order by id) rk
from stadium
where people >= 100
)
select
id,
visit_date,
people
from t1
# where条件过滤出条数大于3的
where rk in (
select rk from t1 group by rk having count(1) >= 3);
# 作者:bryce-28
# 链接:https://leetcode-cn.com/problems/human-traffic-of-stadium/solution/da-shu-ju-fang-xiang-xia-ci-ti-jie-ti-si-lu-by-bry/
# 来源:力扣(LeetCode)
# 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
在大数据方向下,遇到这种求连续的问题第一时间就要想到开窗求差值。首先过滤出 people > 100 的字段,开窗,用 id 减去 rank 排名,并根据 id 进行排序;若是连续的那么,差值一定是相同的。再过滤出条数大于等于 3 的,完成解题。
大佬的代码运行效率恐怖如斯。
小结
完全背包问题或者分步考虑解背包问题,开窗之后求差值
参考链接
我确实应该像人家这样归纳一下,奈何题做得太少。