1-01 将一维数组转变成二维数组
今天的每日一题:2022. 将一维数组转变成二维数组 - 力扣(LeetCode) (leetcode-cn.com)
class Solution {
public:
vector<vector<int>> construct2DArray(vector<int> &original, int m, int n) {
vector<vector<int>> ans(m,vector<int>(n));
if (original.size() != m * n) {
return {};
}
int index =0;
for(int i = 0; i < m; ++i)
{
for(int j = 0; j < n; ++j)
{
ans[i][j]=original[index++];
}
}
return ans;
}
};
1-02 消除游戏
今天的每日一题是:390. 消除游戏 - 力扣(LeetCode) (leetcode-cn.com)
class Solution {
public:
int lastRemaining(int n) {
int a1 = 1;
int k = 0, cnt = n, step = 1;
while (cnt > 1) {
if (k % 2 == 0) { // 正向
a1 = a1 + step;
} else { // 反向
a1 = (cnt % 2 == 0) ? a1 : a1 + step;
}
k++;
cnt = cnt >> 1;
step = step << 1;
}
return a1;
}
};
1-03 一周中的第几天(x)
今天的每日一题是:一周中的第几天
给定day, month, year来输出week
**方法一:**使用模拟
即通过规律来推导出当天距1970年一共有多少天,然后+3(1970年最后一天是周四,数组下标为3)%7(weekday)得出是一周的第几天。
然而实际编写代码的过程中出现了问题,实际得出天数差值少了2;
**原因是:**当最后一年为世纪闰年时(如2000),最后一个月之前的所有月份都被当成了29天来算。
f(m==2&&(year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
可以看到 || year % 4000 == 0;导致当最后一年为世纪闰年时前面的m==2不起作用。
#include<iostream>
#include<string.h>
#include<vector>
using namespace std;
int dayOfTheWeek(int day, int month, int year) {
int dif = 0; //1970年最后一天是周四,下标为3
vector<string> week={ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday","Sunday"};
vector<int> monthDay={31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
//初始化对应数组
//判断闰年闰年有366天,平年有365天
for(int y=1971;y<year;++y)
{
bool isleap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
dif += isleap?366:365;
}
//cout<<dif<<endl;//10592
for(int m=1;m<month;++m)//这里少了两天【BUG】
{
if(m==2&&((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
{
dif += 29;
}
else
{
dif += monthDay[m-1];
}
if(m==1) cout<<dif<<endl;
}
cout<<dif<<endl;
dif=dif+day+3;
int w=dif % 7;
return dif;
}
int main()
{
cout<<dayOfTheWeek(29,2,2000)<<endl;//10653 X 10655(29有7是闰) 最后是10652+3
system("pause");
}
**方法二:**根据菜勒公式来推出时间:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-70O6XoWl-1649921051332)(https://bkimg.cdn.bcebos.com/formula/65717a6a02c703f5ce22f59d077da3ff.svg)]
1-04 猫和老鼠(困难)
今天的每日一题是:913. 猫和老鼠
using VI = vector<int>;
using VVI = vector<VI>;
using VVVI = vector<VVI>;
class Solution {
public:
int n;
int helper(VVI& graph, int t, int x, int y, VVVI& dp){
if (t == 2 * n)
return 0;
if (x == y)
return dp[t][x][y] = 2;
if (x == 0)
return dp[t][x][y] = 1;
if (dp[t][x][y] != -1)
return dp[t][x][y]; // “不要搜了,爷👴已经搜好了”,老爷爷对小伙子说
if (t % 2 == 0){ // 老鼠走🐀
bool catWin = true;
for (int i = 0; i < graph[x].size(); ++ i){
int nx = graph[x][i];
int next = helper(graph, t + 1, nx, y, dp);
if (next == 1)
return dp[t][x][y] = 1; // 直接回家
else if (next != 2)
catWin = false; // 假如出现平地且没有回家,就说明下一步🐱不可能赢
}
if (catWin)
return dp[t][x][y] = 2;
else
return dp[t][x][y] = 0;
}else{ // 猫走,和上面差不多
bool mouseWin = true;
for (int i = 0; i < graph[y].size(); ++ i){
int ny = graph[y][i];
if (ny == 0) continue;
int next = helper(graph, t + 1, x, ny, dp);
if (next == 2)
return dp[t][x][y] = 2;
else if (next != 1)
mouseWin = false;
}
if (mouseWin)
return dp[t][x][y] = 1;
else
return dp[t][x][y] = 0;
}
}
//初始化
int catMouseGame(vector<vector<int>>& graph) {
n = graph.size();
VVVI dp(2 * n, VVI(n, VI(n, -1)));
return helper(graph, 0, 1, 2, dp);
}
};
1-05 替换所有的问号
今天的每日一题是:1576. 替换所有的问号 - 力扣(LeetCode) (leetcode-cn.com)
class Solution {
public:
//void helper(string &str ,const int index);
string modifyString(string s) {
//从头开始检测字符串,当发现?时传递给辅助函数
for(int i=0;i<s.size();++i)
{
if(s[i]=='?'){helper(s,i);}
}
return s;
}
void helper(string &str,const int index)
{
char temp='a';
//因为只有前后需要字符检测,且只需要注意首尾两种特殊情况
if(index == 0)
{
while(str[1]==temp){temp++;}
}else if(index == str.size()-1)
{
while(str[index-1]==temp){temp++;}
}else
{
while(str[index-1]==temp||str[index+1]==temp){temp++;}
}
str[index]=temp;
return;
}
};
报错:Line 12: Char 10: error: class member cannot be redeclared
void helper(string &str,const int index)
^
Line 3: Char 10: note: previous declaration is here
void helper(string &str ,const int index);
^
1 error generated.
原因是类中不需要最前面的函数原型,这被视为重定义。
同样的,若是将string字符串加头加尾则不需要考虑麻烦的边界值问题。
1-06 简化路径(x)
今天的每日一题是:71. 简化路径 - 力扣(LeetCode) (leetcode-cn.com)
1-07 括号的最大嵌套深度
今天的每日一题是:1614. 括号的最大嵌套深度 - 力扣(LeetCode) (leetcode-cn.com)
首先看到字符串,会想到使用栈来解决。遇到’('压入栈,遇到‘)’弹出栈。计算栈内部元素最大数量。
实际上不需要使用栈,仅需要使用该思想。
class Solution {
public:
int maxDepth(string s) {
int temp=0,maxdp=0;
for(char ch:s)
{
if(ch=='(')
{
temp++;
}
else if(ch==')') {
maxdp=max(temp,maxdp);
temp-=1;
}
}
return maxdp;
}
};
使用强制for循环来代替数组索引遍历
1-08 格雷编码(x)
今天的每日一题是:89. 格雷编码 - 力扣(LeetCode) (leetcode-cn.com)
首先,需要先了解什么是格雷编码:上一个编码与下一个编码只有一位不同,且第一个编码和最后一个编码也只有一位不同。
例如:00,01,11,10;需要注意的是第一个编码必须是0,且每个码只能出现一次,范围为[0,2^(n-1)]
解决本题的方式可用找规律解决,n位格雷编码,与n+1位格雷编码中。用2与3位代替。
2位:00,01,11,10;
3位:000,001,011,010,110,111,101,100
发现如下规律,3位格雷编码前四位与2位格雷编码相同,后四位为2为格雷编码逆序在前添1;(【00->100】3位格雷编码最后一个)
class Solution {
public:
vector<int> grayCode(int n) {
vector<int>res;
res.push_back(0); //第一个一定是0
if(n==0) return res;
int first=1;
for(int i=0;i<n;++i)
{
for(int j=res.size()-1;j>=0;--j)
{
res.push_back(first + res[j]); //根据规律,前面不变,后面逆序首部添1
}
first<<=1; //第一次添加时为一位格雷码,直接加1。第二次就是直接加10(2),第三次加100(4);
}
return res;
}
};
1-09 按键持续时间最长的键
今天的每日一题是:1629. 按键持续时间最长的键 - 力扣(LeetCode) (leetcode-cn.com)
class Solution {
public:
char slowestKey(vector<int>& releaseTimes, string keysPressed) {
if(releaseTimes.size()==0) return keysPressed[0];
int m_time=releaseTimes[0];
char res=keysPressed[0];
for(int i=1;i<releaseTimes.size();++i)
{
int n_time=releaseTimes[i]-releaseTimes[i-1];
if(n_time>m_time)
{
m_time=n_time;
res=keysPressed[i];
}
else if (n_time==m_time && keysPressed[i]>res){
res=keysPressed[i];
}
}
return res;
}
};
1-10 累加数(x)
今天的每日一题是:306. 累加数 - 力扣(LeetCode) (leetcode-cn.com)
使用的方法是无可奈何的暴力枚举
一个累加序列,当它的第一个数字和第二个数字以及总长度确定后,这整个累加序列也就确定了。根据这个性质,我们可以穷举累加序列的第一个数字和第二个数字的所有可能性,对每个可能性,进行一次合法性的判断。当出现一次合法的累加序列后,即可返回 true。当所有可能性都遍历完仍无法找到一个合法的累加序列时,返回false。
记字符串num 的长度为 nn,序列最新确定的两个数中,位于前面的数字为first,first 的最高位在num 中的下标为 firstStart,first 的最低位在 num 中的下标为 firstEnd。记序列最新确定的两个数中,位于后面的数字为 second,second 的最高位在 num 中的下标为 secondStart,second 的最低位在num 中的下标为 secondEnd。在穷举第一个数字和第二个数字的过程中,容易得到以下两个结论:firstStart=0,firstEnd+1=secondStart。因此,我们只需要用两个循环来遍历secondStart 和 secondEnd 所有可能性的组合即可。
在判断累加序列的合法性时,用字符串的加法来算出 first 与 second 之和third。将 third 与num 接下来紧邻的相同长度的字符串进行比较。当 third 过长或者与接下来的字符串不相同时,则说明这不是一个合法的累加序列。当相同时,则我们为这个序列新确定了一个数字。如果 third 刚好抵达num 的末尾时,则说明这是一个合法的序列。当 num 还有多余的字符时,则需要更新 firstStart,firstEnd,secondStart,secondEnd, 继续进行合法性的判断。
当输入规模较小时,这题可以直接使用整形或者长整型的数字的相加。而我们这里使用了字符串的加法,因此也能处理溢出的过大的整数输入。
仍需要注意的是,当某个数字长度大于等于 2 时,这个数字不能以 0 开头,这部分的判断可以在两层循环体的开头完成。
class Solution {
public:
bool isAdditiveNumber(string num) {
int n = num.size();
for (int secondStart = 1; secondStart < n - 1; ++secondStart) {
if (num[0] == '0' && secondStart != 1) { //代表第一个数是0开头
break;
}
for (int secondEnd = secondStart; secondEnd < n - 1; ++secondEnd) {
if (num[secondStart] == '0' && secondStart != secondEnd) { //第二个数是0开头
break;
}
if (valid(secondStart, secondEnd, num)) { //当valid()返回true代表字符串正确
return true;
}
}
}
return false;
}
bool valid(int secondStart, int secondEnd, string num) {
int n = num.size();
int firstStart = 0, firstEnd = secondStart - 1;
while (secondEnd <= n - 1) {
string third = stringAdd(num, firstStart, firstEnd, secondStart, secondEnd);
int thirdStart = secondEnd + 1;
int thirdEnd = secondEnd + third.size();
if (thirdEnd >= n || !(num.substr(thirdStart, thirdEnd - thirdStart + 1) == third)) {
break;
}
if (thirdEnd == n - 1) { //只有当最后一个数被算到才算成功
return true;
}
firstStart = secondStart;
firstEnd = secondEnd;
secondStart = thirdStart;
secondEnd = thirdEnd;
}
return false;
}
string stringAdd(string s, int firstStart, int firstEnd, int secondStart, int secondEnd) {
string third;
int carry = 0, cur = 0;
while (firstEnd >= firstStart || secondEnd >= secondStart || carry != 0) {
cur = carry; //carry用来保存对应位前的数(每次/10,如当前是个位其保存个位前数据)
if (firstEnd >= firstStart) {
cur += s[firstEnd] - '0'; //从个位数向上加
--firstEnd;
}
if (secondEnd >= secondStart) {
cur += s[secondEnd] - '0';
--secondEnd;
}
carry = cur / 10;
cur %= 10;
third.push_back(cur + '0'); //将cur转化为字符
}
reverse(third.begin(), third.end()); //逆转后为返回值
return third;
}
};
1-11 逃离大迷宫(困难)
今天的每日一题是:1036. 逃离大迷宫 - 力扣(LeetCode) (leetcode-cn.com)
需要注意的是等差数列:1+2+3+…+n-1
其总和为:Sn=n*a1+n(n-1)d/2或Sn=n(a1+an)/2
故非障碍网格总和为:n(n-1)/2
官方题解
class Solution {
private:
// 在包围圈中
static constexpr int BLOCKED = -1;
// 不在包围圈中
static constexpr int VALID = 0;
// 无论在不在包围圈中,但在 n(n-1)/2 步搜索的过程中经过了 target
static constexpr int FOUND = 1;
static constexpr int dirs[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
static constexpr int BOUNDARY = 1000000;
public:
bool isEscapePossible(vector<vector<int>>& blocked, vector<int>& source, vector<int>& target) {
if (blocked.size() < 2) {
return true;
}
auto hash_fn = [fn = hash<long long>()](const pair<int, int>& o) -> size_t {
auto& [x, y] = o;
return fn((long long)x << 20 | y);
};
unordered_set<pair<int, int>, decltype(hash_fn)> hash_blocked(0, hash_fn);
for (const auto& pos: blocked) {
hash_blocked.emplace(pos[0], pos[1]);
}
auto check = [&](vector<int>& start, vector<int>& finish) -> int {
int sx = start[0], sy = start[1];
int fx = finish[0], fy = finish[1];
int countdown = blocked.size() * (blocked.size() - 1) / 2;
queue<pair<int, int>> q;
q.emplace(sx, sy);
unordered_set<pair<int, int>, decltype(hash_fn)> visited(0, hash_fn);
visited.emplace(sx, sy);
while (!q.empty() && countdown > 0) {
auto [x, y] = q.front();
q.pop();
for (int d = 0; d < 4; ++d) {
int nx = x + dirs[d][0], ny = y + dirs[d][1];
if (nx >= 0 && nx < BOUNDARY && ny >= 0 && ny < BOUNDARY && !hash_blocked.count({nx, ny}) && !visited.count({nx, ny})) {
if (nx == fx && ny == fy) {
return FOUND;
}
--countdown;
q.emplace(nx, ny);
visited.emplace(nx, ny);
}
}
}
if (countdown > 0) {
return BLOCKED;
}
return VALID;
};
if (int result = check(source, target); result == FOUND) {
return true;
}
else if (result == BLOCKED) {
return false;
}
else {
result = check(target, source);
if (result == BLOCKED) {
return false;
}
return true;
}
}
};
【动态规划】
----1、最佳观光组合(x)
刷题计划:1014. 最佳观光组合 - 力扣(LeetCode) (leetcode-cn.com)
最初使用双层for循环进行遍历,遍历i对应的每一个j,用来寻找最大值res。时间复杂度为O(n^2),成功时间超时。
解决方法:
可以知道一对观光景点i,j的得分为:goal = valuse[i]+values[j]+i-j;
因为values[j] - j固定的,则将该公式拆分:mx=values[i] + i; 其值在遍历j时进行更新维护。
这样就可以讲时间复杂度缩减到O(n)。
class Solution {
public:
int maxScoreSightseeingPair(vector<int>& values) {
int res=0,mx=0;
for(int j=1;j<values.size();++j)
{
mx = max(mx,values[j-1]+j-1);
res = max(res, mx+values[j]-j);
}
return res;
}
};
----2、单词拆分(x)
刷题计划:139. 单词拆分 - 力扣(LeetCode) (leetcode-cn.com)
我们定义 dp[i] 表示字符串 s 前 i 个字符组成的字符串 s[0…i−1] 是否能被空格拆分成若干个字典中出现的单词。
从前往后计算考虑转移方程,每次转移的时候我们需要枚举包含位置 i−1 的最后一个单词,看它是否出现在字典中以及除去这部分的字符串是否合法即可。
公式化来说,我们需要枚举 s[0…i−1] 中的分割点 j ,看 s[0…j−1] 组成的字符串 s1 (默认 j=0 时 s 为空串)和 s[j…i−1] 组成的字符串 s2 是否都合法,如果两个字符串均合法,那么按照定义 s1 和 s2拼接成的字符串也同样合法。由于计算到 dp[i] 时我们已经计算出了dp[0…i−1] 的值,因此字符串 s1是否合法可以直接由dp[j] 得知,剩下的我们只需要看s2 是否合法即可,因此我们可以得出如下转移方程:
dp[i]=dp[j] && check(s[j…i−1])
其中check(s[j…i−1]) 表示子串 s[j…i−1] 是否出现在字典中。
对于检查一个字符串是否出现在给定的字符串列表里一般可以考虑哈希表来快速判断,同时也可以做一些简单的剪枝,枚举分割点的时候倒着枚举,如果分割点 j到 i 的长度已经大于字典列表里最长的单词的长度,那么就结束枚举,但是需要注意的是下面的代码给出的是不带剪枝的写法。
对于边界条件,我们定义dp[0]=true 表示空串且合法。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
auto wordDictSet = unordered_set <string> (); //创建用于核对的哈希表
for (auto word: wordDict) { //插入全部数据
wordDictSet.insert(word);
}
auto dp = vector <bool> (s.size() + 1); //初始化DP数组,代表前n个字符组成的字符串是否合法
dp[0] = true; //空字符串默认合法
for (int i = 1; i <= s.size(); ++i) { //开始遍历判断
for (int j = 0; j < i; ++j) {
if (dp[j] && wordDictSet.find(s.substr(j, i - j)) != wordDictSet.end()) { //s1+s2合法
dp[i] = true; //代表前i个字符组成的字符串合法
break;
}
}
}
return dp[s.size()];
}
};
----3、最佳买卖股票时机含冷冻期(x)
刷题计划:309. 最佳买卖股票时机含冷冻期 - 力扣(LeetCode) (leetcode-cn.com)
题目解析:最佳买卖股票时机含冷冻期 - 最佳买卖股票时机含冷冻期 - 力扣(LeetCode) (leetcode-cn.com)
由于购买股票会产生三种不同状态,因此需要确定三个状态转移方程:
如下代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n=prices.size();
if(n<=1) return 0;
vector<vector<int>>dp(n,vector<int>(3)); //初始化dp数组
dp[0][0]=-prices[0];
for(int i=1;i<n;++i)
{
dp[i][0]=max(dp[i-1][0],dp[i-1][2]-prices[i]); //代表第i天结束后手里有股票
dp[i][1]=dp[i-1][0]+prices[i]; //代表第i天结束后手里没有股票且处于冷冻期
dp[i][2]=max(dp[i-1][1],dp[i-1][2]); //代表第i天结束后手里没有股票且不处于冷冻期
}
return max(dp[n-1][1],dp[n-1][2]);
}
};
----4、买卖股票的最佳时机含手续费
刷题计划:714. 买卖股票的最佳时机含手续费 - 力扣(LeetCode) (leetcode-cn.com)
该题解决思路与上一题相同。
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int n=prices.size();
if(n<=1) return 0;
vector<vector<int>> dp(n,vector<int>(2));
dp[0][0]=-prices[0];
for(int i=1;i<n;++i)
{
dp[i][0]=max(dp[i-1][0],dp[i-1][1]-prices[i]); //代表手里有股票
dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i]-fee); //代表手里没有股票
}
return dp[n-1][1];
}
};
----5、解码方法
刷题计划:91. 解码方法 - 力扣(LeetCode) (leetcode-cn.com)
这道题就是根据判断条件来确定状态转移方程。重点在于处理边界条件:如230、2002、011这类返回0
个人代码:
/*
1123:
1: 1; --1个
11:1 1; 11; --2个
112: 1 1 2; 11 2; 1 12; --3个
1123:选择在一起或不在一起:不在:3,在:2即dp[i]=dp[i-1]+dp[i-2];
若当前数为0则必须选择在一起,若前一个数为0则必须不在一起
*/
class Solution {
public:
int numDecodings(string s) {
if(s[0]=='0') return 0;
int n=s.size();
if(n==1) return 1;
vector<int> dp(n);
dp[0]=1;
if(s[1]=='0'&&sum(s,1)>26) return 0;
if(s[1]=='0' || sum(s,1)>26){
dp[1]=dp[0];
}else{
dp[1]=2;
}
for(int i=2;i<n;++i)
{
if(s[i]=='0'&&s[i-1]=='0' || s[i]=='0'&&sum(s,i)>26)
{
return 0;
}
if(s[i] == '0')
{
dp[i]=dp[i-2];
}
else if(s[i-1]=='0' || sum(s,i)>26)
{
dp[i]=dp[i-1];
}
else{
dp[i]=dp[i-1]+dp[i-2];
}
}
return dp[n-1];
}
int sum(const string &s,int i)
{
return (s[i-1]-'0')*10+s[i]-'0';
}
};
官方题解:(简洁)
f[0]即空字符串可以有 1 种解码方法,解码出一个空字符串。
f数组即前n个字符能够组合成合法序列的个数。
需要注意的是:在for循环中s[i-1]相当于当前值,因为数组元素多了一个空字符串
class Solution {
public:
int numDecodings(string s) {
int n = s.size();
vector<int> f(n + 1);
f[0] = 1;
for (int i = 1; i <= n; ++i) {
if (s[i - 1] != '0') { //当前字符不为'0'
f[i] += f[i - 1]; //不与前面的组合
}
if (i > 1 && s[i - 2] != '0' && ((s[i - 2] - '0') * 10 + (s[i - 1] - '0') <= 26)) {//前面字符不为0
f[i] += f[i - 2]; //与前面的组合
}
}
return f[n];
}
};
可以使用变量来替换数组
----*6、不同的二叉搜索树(x)
刷题计划:96. 不同的二叉搜索树 - 力扣(LeetCode) (leetcode-cn.com)
G(n)= i=1∑n F(i,n) (1)
F(i,n)= G(i−1)⋅G(n−i) (2)
G(n)= i=1∑n G(i−1)⋅G(n−i) (3)由(1)(2)得到
其中G(n)代表长度为n的序列能构成二叉搜索树的个数
F(i,n)代表以i为根长度为n的序列能构成的二叉搜索树的个数
当n=0或1是G(n)=0。分别代表空树和只有根节点
class Solution {
public:
int numTrees(int n) {
vector<int>dp(n+1,0);
dp[0]=dp[1]=1;
for(int i=2;i<=n;++i)
{
for(int j=1;j<=i;++j)
{
dp[i]+=dp[j-1]*dp[i-j];
}
}
return dp[n];
}
};
1-12 递增的三元子序列
今天的每日一题是:334. 递增的三元子序列 - 力扣(LeetCode) (leetcode-cn.com)
首先最容易想到的是三层for循环,但是这种情况下时间复杂度为O(n^3),会发生超时。
我们使用贪心法来解决这个问题:
首先维护一个a,它是三元组中最小的元素,每当遇到比a更小的元组时其都需要被更新。
其次维护一个b,他是三元组中第二小的元素,只有当遇到比a大但比b小的元素才将其更新。
当遇到比b还要大的元素时,证明遇到三元组中的最后一个元素,此时返回true;
其中重点是初始化a和b时为最大值。
class Solution {
public:
bool increasingTriplet(vector<int>& nums) {
int a=INT_MAX,b=INT_MAX;
for(int & num:nums)
{
if(num <= a){ //首先a找最小的
a = num;
}else if(num <= b){ //b在保持a的情况下尽量小,此时num>a
b=num;
}else { //此时num>b,返回true
return true;
}
}
return false;
}
};
【动态规划】
----1、最小路径和 (x)
今天的刷题计划是:64. 最小路径和 - 力扣(LeetCode) (leetcode-cn.com)
使用动态规划的方法,初始化第一行和第一列(只能由左边或上边得到)
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int row=grid.size(),col=grid[0].size();
vector<vector<int>>dp(row,vector<int>(col));
dp[0][0]=grid[0][0];
for(int i=1;i<row;++i)
{
dp[i][0]=dp[i-1][0]+grid[i][0];
}
for(int j=1;j<col;++j)
{
dp[0][j]=dp[0][j-1]+grid[0][j];
}
for(int i=1;i<row;++i)
{
for(int j=1;j<col;++j)
{
dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i][j];
}
}
return dp[row-1][col-1];
}
};
----2、最大正方形(x)
刷题计划:221. 最大正方形 - 力扣(LeetCode) (leetcode-cn.com)
dp(i,j)代表以坐标(i,j)为右下角的最大正方形边长。
装态转移方程:
(1)边界:i0或j0 时dp[i][j]=0;
(2)dp[i] = min(min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1])+1;
即由其上方、左方、左上角最小值+1得到。这是因为其只受到最小值影响。
原因参考:1277. 统计全为 1 的正方形子矩阵 - 力扣(LeetCode) (leetcode-cn.com)
class Solution {
public:
int maximalSquare(vector<vector<char>>& matrix) {
int row=matrix.size(),col=matrix[0].size();
vector<vector<int>> dp(row,vector<int>(col));
int m_edge=0;
for(int i=0;i<row;++i)
{
for(int j=0;j<col;++j)
{
if(matrix[i][j]=='1')
{
if(i==0 || j==0)
{
dp[i][j]=1;
}
else
{
dp[i][j] = min(min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1])+1;
}
m_edge = max(m_edge,dp[i][j]);
}
}
}
return pow(m_edge,2);
}
};
----3、等差数列划分(x)
刷题计划:413. 等差数列划分 - 力扣(LeetCode) (leetcode-cn.com)
实际一个等差数列个数遵循一个等差数列:如当4个元素组成,共有:1+2=3个等差数列。当5个元素:1+2+3 =6个等差数列。
故设定t,若后面元素添加后仍能够组成等差数列则t++;res+=t(res即为等差数列总个数)
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums) {
if(nums.size()<3) return 0;
int d=nums[0]-nums[1], t=0,res=0; //t代表当前等差列表减去前面的个数
for(int i=2;i<nums.size();i++)//等差数列长度至少为3,故直接从2开始枚举
{
if(nums[i-1]-nums[i] == d)
{
t++;
}else{
d = nums[i-1] - nums[i];
t=0;
}
res+=t;
}
return res;
}
};
而我使用的方法是找到每个不同的等差数列的长度,将长度根据公式(n-2)(n-1)/2来得到当前等差数列的全部情况下的总个数。
实际测试时出现问题:输入 [1,2,3,4]返回0;
-----------------------------------------------BUG----------------------------------------------------------
class Solution {
public:
int ntor(int n)
{
if(n<3) return 0;
else if(n==3) return 1;
else return (n-1)*(n-2);
}
int numberOfArithmeticSlices(vector<int>& nums) {
int len=nums.size();
vector<int>dp(len);
int res;
for(int i=1;i<len-1;++i)
{
if((nums[i]-nums[i-1])==(nums[i+1]-nums[i]))
{
dp[i+1]=dp[i]+1;
if(i==len-2) return res+ntor(dp[i+1]) ;
}else
{
res+=ntor(dp[i+1]);
}
}
return res;
}
};
1-13 至少是其他数字两倍的最大数
今天的每日一题是:747. 至少是其他数字两倍的最大数 - 力扣(LeetCode) (leetcode-cn.com)
很容易想到两种解题思路:1、两遍遍历 2、排序。
而我们选择我们使用之前学到的类似贪心的方法进行一次遍历来解决问题。
可以将first、second初始化为-1,来避免判断。
class Solution {
public:
int dominantIndex(vector<int>& nums) {
if(nums.size()==1) return 0;
int res,first=INT_MIN,second=INT_MIN;
for(int i=0;i<nums.size();++i)
{
if(nums[i]>=first)
{
second=first;
first=nums[i];
res=i;
}else if(nums[i]>=second)
{
second=nums[i];
}
}
return 2*second<=first?res:-1;
}
};
【动态规划】
----1、丑数 II(x)
刷题计划:264. 丑数 II - 力扣(LeetCode) (leetcode-cn.com)
(1)首先理解丑数:只能被2、3、5整除的数,第一个丑数为1;(前十个丑数:{1, 2, 3, 4, 5, 6, 8, 9, 10, 12})
(2)**dp数组:**对应着第i个丑数
(3)状态转移方程:
维护三个指针,最开始都指向dp[1]然后逐个进行乘法,这三个指针分别代表对dp[pointer]选择×2、×3或×5;
dp[i](从2开始,dp[1]被初始化为1)被赋值为对应操作最小的那个(因为数组必须顺序递增)
然后其乘积为dp[i]的所有指针向后移动一位。(为保证数组元素不重复)
class Solution {
public:
int nthUglyNumber(int n) {
if(n<7) return n;//按照规律,前6个丑数为1~6.可以舍去改行代码
vector<int> dp(n+1);
dp[1]=1;
int pointer1=1,pointer2=1,pointer3=1; //首先将三个指针都指向第一个丑数的位置
for(int i=2;i<=n;++i)
{
int num1=dp[pointer1]*2,num2=dp[pointer2]*3,num3=dp[pointer3]*5;//进行对应操作
dp[i]=min(num1,min(num2,num3));//选取其中最小的一个进行赋值
if(dp[i]==num1)
{
pointer1++;
}
if(dp[i]==num2)
{
pointer2++;
}
if(dp[i]==num3)
{
pointer3++;
}
}
return dp[n];
}
};
这道题一开始死活不明白三指针到底是怎么用的。后来突然就想明白了,我尝试解释一下:
例如 n = 10, primes = [2, 3, 5]。 打印出丑数列表:1, 2, 3, 4, 5, 6, 8, 9, 10, 12
首先一定要知道,后面的丑数一定由前面的丑数乘以2,或者乘以3,或者乘以5得来。例如,8,9,10,12一定是1, 2, 3, 4, 5, 6乘以2,3,5三个质数中的某一个得到。
这样的话我们的解题思路就是:从第一个丑数开始,一个个数丑数,并确保数出来的丑数是递增的,直到数到第n个丑数,得到答案。那么问题就是如何递增地数丑数?
观察上面的例子,假如我们用1, 2, 3, 4, 5, 6去形成后面的丑数,我们可以将1, 2, 3, 4, 5, 6分别乘以2, 3, 5,这样得到一共6*3=18个新丑数。也就是说1, 2, 3, 4, 5, 6中的每一个丑数都有一次机会与2相乘,一次机会与3相乘,一次机会与5相乘(一共有18次机会形成18个新丑数),来得到更大的一个丑数。
这样就可以用三个指针,
pointer2, 指向1, 2, 3, 4, 5, 6中,还没使用乘2机会的丑数的位置。该指针的前一位已经使用完了乘以2的机会。
pointer3, 指向1, 2, 3, 4, 5, 6中,还没使用乘3机会的丑数的位置。该指针的前一位已经使用完了乘以3的机会。
pointer5, 指向1, 2, 3, 4, 5, 6中,还没使用乘5机会的丑数的位置。该指针的前一位已经使用完了乘以5的机会。
下一次寻找丑数时,则对这三个位置分别尝试使用一次乘2机会,乘3机会,乘5机会,看看哪个最小,最小的那个就是下一个丑数。最后,得到下一个丑数的指针位置加一,因为它对应的那次乘法使用完了。
这里需要注意下去重的问题,如果某次寻找丑数,找到了下一个丑数10,则pointer2和pointer5都需要加一,因为5乘2等于10, 2乘5也等于10,这样可以确保10只被数一次。
----2、下降路径最小和(x)
刷题计划:931. 下降路径最小和 - 力扣(LeetCode) (leetcode-cn.com)
最初这道题被我写成了创建dp[n]数组,用于保存对应和。但是实际加时却不会变化到对应位置,仍保持原位。
如: [[2,1,3],[6,5,4],[7,8,9]],当1+4后仍处于dp[1],可以继续加7,最后返回12。
显然这是不正确的,dp需要变化位置,可以使用当前行加上一行来解决该问题。
不要忽略该矩阵为正方形,即:rows=colums=n
正确方法:
原地修改,从第二行i=1开始,每个元素由其上个最优方案得到。最后遍历数组最后一行,返回最小值。
(也可以从下到上,最后遍历原数组第一行)
class Solution {
public:
int minFallingPathSum(vector<vector<int>>& matrix) {
int n=matrix.size();
if(n==1) return matrix[0][0]; //因为这是正方形矩阵
int res = INT_MAX;
for(int i=1;i<n;i++)
{
matrix[i][0]+=min(matrix[i-1][0],matrix[i-1][1]); //处理边界值
matrix[i][n-1]+=min(matrix[i-1][n-1],matrix[i-1][n-2]);
for(int j=1;j<n-1;j++)
{
matrix[i][j]+=min(matrix[i-1][j-1],min(matrix[i-1][j],matrix[i-1][j+1]));//正常计算
}
}
for(int &num:matrix[n-1])//遍历取得最大值
{
res=min(res,num);
}
return res;
}
};
----*3、最长回文子串(x)
刷题计划:5. 最长回文子串 - 力扣(LeetCode) (leetcode-cn.com)
1、首先明确回文字符串规则:
(1)正反读相同
(2)一个字符为回文
(3)2个相同字符组成字符串为回文
(4)两头相同的3个字符的字符串回文
2、动态规划dp数组含义:
n×n的二维数组,dp[i][j]代表i到j的字符串,1代表回文,0代表不回文
3、dp数组初始化:
dp[i][i]=true; 代表单个字符,单个字符必定回文
4、状态转移方程:
if(s[i] == s[j] && dp[i+1][j-1] == true) dp[i][j]=true; //即当字符串两头字符相同时若除去两头字符仍回文,则现在的字符串回文
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
if(n<=1) return s;//单个字符必定回文
vector<vector<int>> dp(n,vector<int>(n));//dp[i][j]代表从i到j的字符串为回文字符串
for(int m=0;m<n;m++) //初始化,单个字符必定回文
{
dp[m][m]=true;
}
int begin=0,m_len=1;
for(int L=1;L<n;L++) //代表字符串i到j的长度
{
for(int i=0;i<n;i++) //逐个遍历寻找回文字符串
{
int j=i+L; //得到字符串尾部位置
if(j>=n) break; //剪枝,防止越界
if(s[i]!=s[j]) //字符串两头不等必定不回文
{
dp[i][j]=false;
}else if(L<=2) //由两个相同字符组成的字符串或两头相同的3个字符组成的字符串必定回文
{
dp[i][j]=true;
}else
{
dp[i][j]=dp[i+1][j-1]; //需要注意的是这里,之前被写成了==
}
if(dp[i][j] && L+1>m_len){ //仅continue第一个判断是错误的,这是没有考虑到else,故仍需判断dp[i][j]
begin=i;
m_len=L+1; //更新
}
}
}
return s.substr(begin,m_len); //返回字符串截取
}
};
----4、最长回文子序列
刷题列表:516. 最长回文子序列 - 力扣(LeetCode) (leetcode-cn.com)
其中一种解题方法是将字符串反转,该字符串与原字符串的最长公共子序列的长度即为答案。
我们的解法则是使用动归,创建二维数组进行遍历更新。
(1)DP含义:
i到j的最大回文序列长度。
(2)DP数组初始化:
与上一题相同,dp[i][i]=1;即当个字符为回文,长度为1;
(3)状态转移方程
两头相等:dp[i][j]=dp[i+1][j-1]+2;
两头不等:dp[i][j]=max( dp[i+1][j] , dp[i][j-1] );
因为可以删除任意字符,所以不需要在意其内是否含有非回文部分
(4)遍历方式
该题重点在遍历方式上,需要一块一块地进行推进。
这种遍历方式总是先一步判断内部的大小,即在为dp[i][j]赋值前,已经先一步将dp[i-1][j-1]、dp[i+1][j]、dp[i][j-1]赋值。
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n=s.size();
vector<vector<int>> dp(n,vector<int>(n));
for(int i=n-1;i>=0;i--)
{
dp[i][i]=1; //dp初始化
for(int j=i+1;j<n;j++)
{
if(s[i]==s[j]){ //两头字符相等
dp[i][j]=dp[i+1][j-1]+2;
}else{
dp[i][j]=max(dp[i+1][j],dp[i][j-1]);
}
}
}
return dp[0][n-1]; //第一个字符到最后一个字符组成字符串中最长回文序列的长度
}
};
1-14 *查找和最小的 K 对数字(x)
今天的每日一题是:373. 查找和最小的 K 对数字 题解 - 力扣(LeetCode) (leetcode-cn.com)
使用自定义比较的优先级队列,将全部组合放入到队列中,弹出的前k个组合即为答案。
**具体:**由于数组有序,和最小的组合一定是(nums[0],nums2[0])和最大的组合一定是(nums1[size()-1],nums2[size()-1]);
而第二小的则是(a,b+1)或(a+1,b);
故将数据压入队列的方式为:先放入(nums1[0n-1],nums2[0])弹出首元素,在压入(nums1[0n-1],nums2[1]),循环遍历nums2压入弹出,最后得出答案。
参考代码
class Solution {
public:
vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k) {
auto cmp = [&nums1, &nums2](const pair<int, int> & a, const pair<int, int> & b) {
return nums1[a.first] + nums2[a.second] > nums1[b.first] + nums2[b.second];
};
int m = nums1.size();
int n = nums2.size();
vector<vector<int>> ans;
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> pq(cmp);
for (int i = 0; i < min(k, m); i++) {
pq.emplace(i, 0); //注意压入的是索引
}
while (k-- > 0 && !pq.empty()) { //k为返回最小个数
auto [x, y] = pq.top();
pq.pop();
ans.emplace_back(initializer_list<int>{nums1[x], nums2[y]});
if (y + 1 < n) {
pq.emplace(x, y + 1);
}
}
return ans;
}
};
pq(cmp);`因为lambda这种特殊的class没有默认构造函数,pq内部排序比较的时候要使用的是一个实例化的lambda对象,只能通过lambda的 copy构造进行实例化,从哪里copy呢,就需要pq构造函数的时候传入这个lambda对象。pq的自定义比较也可以使用struct或者class,因为struct和class都有默认构造函数,此时就不需要`pq(Type)` 而是直接`pq`即可。同理,自定义比较也可以使用函数,同样此时也需要提供函数对象进行实例化`pq(CmpFunc)`,假设比较函数为`bool CmpFunc(int x, int y) {return x < y;}
因为C++priority_queue模板里的三个对象都是类,所以传入的参数必须是类型,因为我们自己定义的cmp函数不存在在queue库中(不像less、great等),所以该priority_queue函数的初始化需要使用有参构造,所以传递了cmp函数这个对象进去进行此priority_queue函数的初始化。而因为great、less等函数本身就是queue库中包含的类,所以使用该方法时,priority_queue变量名后不加(),即通过无参(默认)构造函数来对priority_queue进行初始化。
decltype为类型声明符,当使用decltype(var)
的形式时,decltype会直接返回变量的类型(包括顶层const和引用),不会返回变量作为表达式的类型。使用decltype(expr)
的形式时,decltype会返回表达式结果对应的类型。(86条消息) 【C++深陷】之“decltype”_不能自拔-CSDN博客_decltype
(86条消息) c++11特性之initializer_list_一蓑烟雨任平生 也无风雨也无晴-CSDN博客_initializer_list
emplace操作是C++11新特性,新引入的的三个成员emplace_front、emplace 和 emplace_back。这些操作构造而不是拷贝元素到容器中,这些操作分别对应push_front、insert 和push_back,允许我们将元素放在容器头部、一个指定的位置和容器尾部。
两者的区别
当调用insert时,是将对象传递给insert,对象被拷贝到容器中,而当我们使用emplace时,是将参数传递给构造函数,emplace使用这些参数在容器管理的内存空间中直接构造元素。(86条消息) C++11新特性之十一:emplace_土戈的博客-CSDN博客_emplace
1-15 计算力扣银行的钱
今天的每日一题是:1716. 计算力扣银行的钱 - 力扣(LeetCode) (leetcode-cn.com)
很简单的一道题,可以使用遍历与数学方法解决。
class Solution {
public:
int totalMoney(int n) {
int sum=1,pre=1,preWeek=1;
for(int i=1;i<n;++i)
{
if(i%7==0){
pre=preWeek+1;
preWeek=pre;
}else
{
pre++;
}
sum+=pre;
}
return sum;
}
};
----1、*元素和小于等于阈值的正方形的最大边长(x)
前置题目:1292. 元素和小于等于阈值的正方形的最大边长 - 力扣(LeetCode) (leetcode-cn.com)
p是mat的二位前缀和,通过容斥原理得到。
class Solution {
public:
int getRect(const vector<vector<int>>& P, int x1, int y1, int x2, int y2) {//两个坐标分别代表矩形左上角和右下角
return P[x2][y2] - P[x1 - 1][y2] - P[x2][y1 - 1] + P[x1 - 1][y1 - 1]; //返回指定矩形和
}
int maxSideLength(vector<vector<int>>& mat, int threshold) {
int m = mat.size(), n = mat[0].size();
vector<vector<int>> P(m + 1, vector<int>(n + 1));
//初始化一个二维前缀和数组
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
P[i][j] = P[i - 1][j] + P[i][j - 1] - P[i - 1][j - 1] + mat[i - 1][j - 1];
}
}
int l = 1, r = min(m, n), ans = 0; //边长不会超过两者中的较小值
while (l <= r) { //二分查找
int mid = (l + r) / 2;
bool find = false;
for (int i = 1; i <= m - mid + 1; ++i) { //mid+1是因为p数组行和列从1开始才有效
for (int j = 1; j <= n - mid + 1; ++j) {
if (getRect(P, i, j, i + mid - 1, j + mid - 1) <= threshold) {
find = true;
}
}
}
if (find) {
ans = mid;
l = mid + 1;
}
else {
r = mid - 1;
}
}
return ans;
}
};
----2、矩阵区域和(x)
刷题计划:1314. 矩阵区域和 - 力扣(LeetCode) (leetcode-cn.com)
使用了与上一题相同的二维前缀和来解决问题。
class Solution {
public:
int get(vector<vector<int>> &P,int row,int col,int x,int y)
{
x=max(min(row,x),0);
y=max(min(col,y),0);
return P[x][y];
}//避免越界,返回正确的和
vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) {
int row=mat.size(),col=mat[0].size();
vector<vector<int>> P(row+1,vector<int>(col+1));
for(int i=1;i<=row;++i)
{
for(int j=1;j<=col;++j)
{
P[i][j]=P[i][j-1]+P[i-1][j]-P[i-1][j-1]+mat[i-1][j-1];
}
}
//以上完成二维前缀和的更新
vector<vector<int>> ans(row,vector<int>(col));
for(int i=0;i<row;++i)
{
for(int j=0;j<col;++j)
{
ans[i][j]=get(P,row,col,i+k+1,j+k+1)-get(P,row,col,i-k,j+k+1)-get(P,row,col,i+k+1,j-k)+get(P,row,col,i-k,j-k);
}
}
return ans;
}
};
----3、 二维区域和检索
刷题列表:304. 二维区域和检索 - 矩阵不可变 - 力扣(LeetCode) (leetcode-cn.com)
使用二维前缀和解决问题,构造函数用来初始化二维前缀和数组,sum函数根据前缀和数组得到和。
class NumMatrix {
public:
vector<vector<int>> P;
NumMatrix(vector<vector<int>>& matrix) {
int row=matrix.size(),col=matrix[0].size();
if(row<1) return ;
P.resize(row+1,vector<int>(col+1));
for(int i=1;i<=row;++i)
{
for(int j=1;j<=col;++j)
{
P[i][j]=P[i-1][j]+P[i][j-1]-P[i-1][j-1]+matrix[i-1][j-1];
}
}
}
int sumRegion(int row1, int col1, int row2, int col2) {
int res=P[row2+1][col2+1]-P[row2+1][col1]-P[row1][col2+1]+P[row1][col1];
return res;
}
};
----4、最长递增子序列
刷题列表:300. 最长递增子序列 - 力扣(LeetCode) (leetcode-cn.com)
很简单的一道dp题。
我的解法 :
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size();
vector<int>dp(n,1);
int res=1;
for(int i=1;i<n;++i)
{
int m_pre =0;
for(int j=i;j>=0;--j)
{
if(nums[i]>nums[j] && dp[j]>m_pre)
{
m_pre=dp[j];
}
}
dp[i]+=m_pre;
res=max(res,dp[i]);
}
return res;
}
};
官方解法:
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = (int)nums.size();
if (n == 0) {
return 0;
}
vector<int> dp(n, 0);
for (int i = 0; i < n; ++i) {
dp[i] = 1;
for (int j = 0; j < i; ++j) {
if (nums[j] < nums[i]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
return *max_element(dp.begin(), dp.end());
}
};
----5、 *摆动序列(x)
刷题列表:376. 摆动序列 - 力扣(LeetCode) (leetcode-cn.com)
1、dp含义
up为上升序列,即up[n-1]到up[n]为上升
down为下降序列,即up[n-1]到up[n]为下降
up[i]代表到达索引i时上升序列的最大长度,同理down[i]
2、dp数组初始化
up[0]=down[0]=1; 即只有一个元素的情况下既是上升也是下降序列
3、dp状态转移公式
(1)nums[i]>nums[i-1]
- up[i]=max(up[i-1],down[i-1]+1);
up[i]:首先up[i]只能从up[i-1]或down[i-1]得到,若是从down[i-1]得到则需要证明down中的最后一个元素是必定小于nums[i],首先down[i-1]中的倒数第二个元素必定比最后一个元素要大(down是由up变化来的),而若dow[i-1]序列的最后一个元素比nums[i]大则可以将nums[i-1]替换掉,因为down[i-2]最后一个元素大于down[i-1]最后一个元素,则down[i-1]被替换后的序列最后一个元素必定小于nus[i],在down[i-1]序列后添加nums[i]即为up[i];
(L(down[i-2])>L(down[i-1])>nums[i], nums[i]>nums[i-1] L(down[i-1])=nums[i-1] L(down[i])=nums[i] )
需要考虑up[i-1]>down[i-1],
- down[i]=down[i-1];
down[i]:只能从up[i-1]或直接等于down[i-1]得到,down想要从up[i-1]变来需要nums[i]小于L(up[i-1]),而nums[i-1]<nums[i]则就算是小于L(up[i-1])也是nums[i-1]而此时的L(up[i-1])不可能大于nums[i],即该条件下down[i]无法由up[i-1]得到。
(2)nums[i]<nums[i-1]
以下同理
-
down[i]=max(down[i-1],up[i-1]+1);
-
up[i]=up[i-1];
(3)nums[i]==nums[i-1]
相等不变
-
up[i]=up[i-1];
-
down[i]=down[i-1];
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int n = nums.size();
if(n<2) return n; //根据规则,当n=2时若重复则为1
vector<int> up(n),down(n); //定义上升和下降序列
up[0]=down[0]=1; //初始化
for(int i=1;i<n;++i)
{
if(nums[i]>nums[i-1])
{
up[i]=max(up[i-1],down[i-1]+1);
down[i]=down[i-1];
}else if(nums[i]<nums[i-1])
{
down[i]=max(down[i-1],up[i-1]+1);
up[i]=up[i-1];
}else{
up[i]=up[i-1];
down[i]=down[i-1];
}
}
return max(up[n-1],down[n-1]);
}
};
----6、判断子序列(x)
刷题列表:392. 判断子序列 - 力扣(LeetCode) (leetcode-cn.com)
1、dp含义:
/dp数组dp[i][j]表示字符串t以i位置开始第一次出现字符j的位置
2、dp数组初始化
初始化边界条件,dp[i][j] = m表示t中不存在字符j
3、递推公式
若i位置不是则跳到i+1
if(t[i] == j+‘a’) dp[i][j]=i;
else dp[i][j]=dp[i+1][j];
class Solution {
public:
bool isSubsequence(string s, string t) {
int n = s.size(),m = t.size();
//dp数组dp[i][j]表示字符串t以i位置开始第一次出现字符j的位置
vector<vector<int>> dp(m+1,vector<int>(26,0));
//初始化边界条件,dp[i][j] = m表示t中不存在字符j
for(int i=0;i<26;++i)
{
dp[m][i]=m;
}
//从后往前递推初始化dp数组
for(int i=m-1;i>=0;--i)
{
for(int j=0;j<26;++j)
{
if(t[i] == j+'a')
{
dp[i][j]=i;
}else{
dp[i][j]=dp[i+1][j];
}
}
}
int add = 0;
for(int i=0;i<n;i++)
{
//t中没有s[i] 返回false
if(dp[add][s[i]-'a'] == m)
{
return false;
}
//否则直接跳到t中s[i]第一次出现的位置之后一位
add = dp[add][s[i]-'a']+1;
}
return true;
}
};
----7、*最长公共子序列(x)
刷题列表:1143. 最长公共子序列 - 力扣(LeetCode) (leetcode-cn.com)
1、dp含义
text1[ 0 ~ i]与text2[ 0 ~ j]的最大公共子序列长度
2、dp数组初始化
创建m+1行、n+1列。左边界与上边界初始化为0;(空字符与任何字符串都没有公共子序列)
3、状态转移方程
if(text1[i-1]==text2[j-1]) 因为边界设置+1,故遍历普通数组-1。
dp[i][j]=dp[i-1][j-1]+1; 公共字符直接按顺序放在对应后面
else: dp[i][j]=max(dp[i-1][j],dp[i][j-1]); 继承时,选择一个较大的作为当前子序列长度
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m=text1.size(),n=text2.size();
vector<vector<int>>dp(m+1,vector<int>(n+1));
for(int i=1;i<=m;++i)
{
for(int j=1;j<=n;++j)
{
if(text1[i-1]==text2[j-1])
{
dp[i][j]=dp[i-1][j-1]+1;
}else{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
}
return dp[m][n];
}
};
----8、*编辑距离(困难)
1、dp含义
dp[i][j]代表word1中前i个字符组成字符串到达word2中前j个字符组成字符串 所需编辑距离
2、dp数组初始化
0到n的编辑需要n部,初始化dp[i][0]=i,dp[0][j]=j;
3、状态转移方程
dp[i][j]只可能由以下三种方式到达,因为六种操作抛除效果重合后实际上操作只能分为三种:
(1)插入word1(2)插入world2(3)替换world1
dp[i][j]=dp[i][j-1]+1; //代表插入world1
dp[i][j]=dp[i-1][j]+1; //代表插入world2
dp[i][j]=dp[i-1][j-1];或dp[i][j]=dp[i-1][j-1] //当i和j对应字符相等时无需操作,否则需要替换
dp取这三个中的最优解即最小值。
class Solution {
public:
int minDistance(string word1, string word2) {
int m=word1.size(),n=word2.size();
//判断空串
if(m*n==0) return m+n;
vector<vector<int>> dp(m+1,vector<int>(n+1));
//dp数组初始化
for(int i=0;i<=m;++i)
{
dp[i][0]=i;
}
for(int j=0;j<=n;++j)
{
dp[0][j]=j;
}
//遍历递推
for(int i=1;i<=m;++i)
{
for(int j=1;j<=n;++j)
{
int left=dp[i][j-1]+1;
int down=dp[i-1][j]+1;
int leftdown=dp[i-1][j-1];//若最后一个相同则不需替换操作
if(word1[i-1]!=word2[j-1]){
leftdown++;
}
dp[i][j]=min(left,min(down,leftdown));
}
}
return dp[m][n];
}
};
01-16 链表随机节点
今天的每日一题是:382. 链表随机节点 - 力扣(LeetCode) (leetcode-cn.com)
需要复习链表相应的知识点。
方法一是使用数组保存节点值并随机输出
class Solution {
vector<int>vec;
public:
Solution(ListNode* head) {
while(head){
vec.emplace_back(head->val);
head=head->next;
}
}
int getRandom() {
return vec[random() % vec.size()];
}
};
方法二:水塘抽样法
输出时进行[ 0 ~ i]的随机读取,若读到的数是0则更新输出为i,否则输出保存的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JjlI7Fq8-1649921051333)(C:\Users\FOX\AppData\Roaming\Typora\typora-user-images\image-20220116231630871.png)]
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
ListNode *head;
public:
Solution(ListNode* head) {
this->head = head;
}
int getRandom() {
int i=1,res=0;
for(auto node=head;node;node=node->next){
if(rand() % i==0){
res = node->val;
}
++i;
}
return res;
}
};
/**
* Your Solution object will be instantiated and called as such:
* Solution* obj = new Solution(head);
* int param_1 = obj->getRandom();
*/
----1、零钱兑换 (x)
刷题列表:322. 零钱兑换 - 力扣(LeetCode) (leetcode-cn.com)
1、dp含义:
dp[i]代表i元需要最小硬币个数
2、dp数组初始化
全部元素初始化为amount+1,代表当前硬币无法组合得到i元
dp[0]=0 0元购
3、状态转移方程
if(coins[j] <= i) dp[i]=min(dp[i],dp[i-coins[j]]+1);
遍历硬币数组,若该硬币面值不大于当前要找的钱i,则dp[i]可以等于dp[i-coins[j]]+1;即在剩余最小值,添加一枚当前面值硬币。
取最优解。
我的解法:
测试用例: [470,18,66,301,403,112,360] 8235 超时
容易忘掉初始化dp[0]=0;代表0元无需硬币个数。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n=coins.size();
//vector<vector<int>>dp(amount+1,vector<int>(n));
vector<int> dp(amount+1,INT_MAX);
dp[0]=0;
for(int i=1;i<=amount;++i)
{
for(int j=0;j<n;++j)
{
if(i-coins[j]<0 || dp[i-coins[j]]==INT_MAX) continue; //这里导致超时?
else dp[i]=min(dp[i],dp[i-coins[j]]+1);
cout<<dp[i]<<endl;
}
cout<<dp[i]<<endl;
}
return dp[amount]==INT_MAX?-1:dp[amount];
}
};
官方解法:
(1)该解法定义max为amount+1而不是INT_MAX; 根据规律找到最大值
(2)判断状态转移的方式更优
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int Max = amount + 1;
vector<int> dp(amount + 1, Max);
dp[0] = 0;
for (int i = 1; i <= amount; ++i) {
for (int j = 0; j < (int)coins.size(); ++j) {
if (coins[j] <= i) {
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
};
----2、零钱兑换 II(x)
刷题列表:518. 零钱兑换 II - 力扣(LeetCode) (leetcode-cn.com)
1、dp含义
dp[i]表示i元组合次数
2、dp数组初始化
全部初始化为0代表无解决方案
dp[0]=1,代表0元为0元组合一次
3、递推公式
该题需要注意的是避免重复:如方案2,1,1和1,2,1是一种解决方案,即不考虑排列顺序。
解决方法:
使用外层循环coins的做法不会重复计算不同的排列。因为外层循环是遍历数组 coins 的值,内层循环是遍历不同的金额之和,在计算dp[i] 的值时,可以确保金额之和等于 i 的硬币面额的顺序,由于顺序确定,因此不会重复计算不同的排列。
为什么外层要对coins循环:
假设coins = {1, 2, 3},amount = 5。 凑出5的方案有三类:
- 组合必须以硬币1结尾,且不能包含硬币1之后的其他硬币2, 3。假设这类方案数量为x1。
- 组合必须以硬币2结尾,且不能包含硬币2之后的其他硬币3。假设这类方案数量为x2。
- 组合必须以硬币3结尾。假设这类方案数量为x3。
第一轮,我们计算x1。
第二轮,我们计算x2。并将x2加到x1上,得到x1 + x2。
第三轮,我们计算x3。并将x2加到x1 + x2上,得到x1 + x2 + x2。
对amount为5而言
x1 有 {1, 1, 1, 1, 1}
x2 有 {1, 1, 1, 2}, {1, 2, 2}
x3 有 {1, 1, 3}, {2, 3}
最终返回x1 + x2 + x3。
class Solution {
public:
int change(int amount, vector<int>& coins) {
int n=coins.size();
//创建dp数组
vector<int>dp(amount+1,0);
//首元素初始化
dp[0]=1;
//进行递推,外层循环coins,避免包含后面的硬币面值导致重复
for(int coin:coins){
for(int i=0;i<=amount;++i){
//判断使用当前硬币面值是否会付多了
if(i>=coin){
dp[i]+=dp[i-coin];
}
}
}
//返回金额amount组合方案数量
return dp[amount];
}
};
----3、组合总和 Ⅳ (x)
刷题列表:377. 组合总和 Ⅳ - 力扣(LeetCode) (leetcode-cn.com)
本题与上一题零钱兑换II类似,区别是该题不会考虑因方式不同而导致的内部数据排序后重复。
需要注意的是题目中的:题目数据保证答案符合 32 位整数范围。
当测试用例为:
[10,20,30,40,50,60,70,80,90,100,110,120,130,140,150,160,170,180,190,200,210,220,230,240,250,260,270,280,290,300,310,320,330,340,350,360,370,380,390,400,410,420,430,440,450,460,470,480,490,500,510,520,530,540,550,560,570,580,590,600,610,620,630,640,650,660,670,680,690,700,710,720,730,740,750,760,770,780,790,800,810,820,830,840,850,860,870,880,890,900,910,920,930,940,950,960,970,980,990,111] 999
**会提示:**Line 12: Char 26: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type ‘int’ (solution.cpp) SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior prog_joined.cpp:21:26
翻译:
第12行:Char 26:运行时错误:有符号整数溢出:2147483647+1不能在类型“int”(solution.cpp)中表示
小结:未定义的行为程序:未定义的行为程序。cpp:21:26
解决方法:
答案与我的解法相比在if中多出的一个判断条件:
dp[i-nums[j]] < INT_MAX - dp[i]
这是避免dp[i]内保存的数据溢出,根据题目中描述的答案符合32位整数范围,故超出32位则代表数据无效
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
int n=nums.size();
vector<int>dp(target+1,0);
dp[0]=1;
for(int i=1;i<=target;++i)
{
for(int j=0;j<n;++j)
{
if(i>=nums[j] && dp[i-nums[j]] < INT_MAX - dp[i]){
dp[i]+=dp[i-nums[j]];
}
}
}
return dp[target];
}
};
----4、完全平方数(x)
刷题列表:279. 完全平方数 - 力扣(LeetCode) (leetcode-cn.com)
1、dp含义
dp[i]表示i由完全平方数组成的最少个数
2、dp数组初始化
dp[0]=0; 作为边界,0无法被完全平方数组成。
3、递推公式
我们可以依据题目的要求写出状态表达式:f[i]表示最少需要多少个数的平方来表示整数 i。
这些数必然落在区间[1, n]。我们可以枚举这些数,假设当前枚举到 j,那么我们还需要取若干数的平方,构成 i−j ^2。此时我们发现该子问题和原问题类似,只是规模变小了。这符合了动态规划的要求,于是我们可以写出状态转移方程。
dp[i]= 1+ min(j = 1~ sqrt(i) ) dp[i-j^2]
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n+1);
for(int i=1;i<=n;++i)
{
int min_tmp=INT_MAX;
for(int j=1;j*j<=i;++j)
{
min_tmp=min(min_tmp,dp[i-j*j]);
}
dp[i]=min_tmp+1;
}
return dp[n];
}
};
----5、整数拆分
刷题列表:343. 整数拆分 - 力扣(LeetCode) (leetcode-cn.com)
1、dp含义
dp[i]表示i被拆成整数的最大乘积
2、dp数组初始化
dp[0]=dp[1]=0; 0无法被拆故作为边界,1=1*0=0;
3、递推公式
当i>=2时,dp[i]可能会由两种方案得到:
(1)当i拆分成j与i-j后,i-j不再需要拆分成多个整数:dp[i] = j * (i - j);
(2)将i拆分成为i-j后,i-j仍需继续拆分成多个整数: dp[i] = j * dp[i-j]; (dp[i-j]即多次拆分后的最大乘积)
class Solution {
public:
int integerBreak(int n) {
vector<int>dp(n+1);
dp[0]=dp[1]=0;
for(int i=2;i<=n;++i)
{
int Max_tmp=0;
for(int j=1;j<i;++j)
{
Max_tmp=max(max(Max_tmp,j*(i-j)),j*dp[i-j]);
}
dp[i]=Max_tmp;
}
return dp[n];
}
};
1-17 统计元音字母序列的数目
今天的每日一题是:1220. 统计元音字母序列的数目 - 力扣(LeetCode) (leetcode-cn.com)
注意:由于答案可能会很大,所以请你返回 模 10^9 + 7
之后的结果。
1e9 + 7:10^9 + 7
class Solution {
public:
int countVowelPermutation(int n) {
long long mod = 1e9 + 7;
vector<long long> dp(5, 1);
vector<long long> ndp(5);
for (int i = 2; i <= n; ++i) {
/* a前面可以为e,u,i */
ndp[0] = (dp[1] + dp[2] + dp[4]) % mod;
/* e前面可以为a,i */
ndp[1] = (dp[0] + dp[2]) % mod;
/* i前面可以为e,o */
ndp[2] = (dp[1] + dp[3]) % mod;
/* o前面可以为i */
ndp[3] = dp[2];
/* u前面可以为i,o */
ndp[4] = (dp[2] + dp[3]) % mod;
dp = ndp;
}
return accumulate(dp.begin(), dp.end(), 0LL) % mod;
}
};
0 //int
0L //long
0LL //long long
0UL //unsigned long
0.0 //double
0.0f //float
0.0L //long double
1-18 最小时间差
今天的每日一题是:539. 最小时间差 - 力扣(LeetCode) (leetcode-cn.com)
if (timePoints.size() > 1440) {**
return 0;
}**
该部分代码有效避免了超时。因为24*60=1440,故当数组大小超过该值时必定会有重复元素。此时差值最小一定为0;
class Solution {
public:
int differ(const string &s1,const string &s2)
{
int t1,t2;
t1=((s1[0]-'0')*10+(s1[1]-'0'))*60+(s1[3]-'0')*10+s1[4]-'0';
t2=((s2[0]-'0')*10+(s2[1]-'0'))*60+(s2[3]-'0')*10+s2[4]-'0';
int res=abs(t1-t2);
return min(res,24*60-res);
}
int findMinDifference(vector<string>& timePoints) {
if (timePoints.size() > 1440) {
return 0;
}
int n = timePoints.size();
vector<int>dp(n+1);
dp[1]=24*60;
for(int i=2;i<=n;++i)
{
dp[i]=dp[i-1];
for(int j=0;j<i-1;++j)
{
dp[i]=min(dp[i],differ(timePoints[i-1],timePoints[j]));
}
}
return dp[n];
}
};
该代码整体而言类似官方题解—鸽巢原理
class Solution {
int getMinutes(string &t) {
return (int(t[0] - '0') * 10 + int(t[1] - '0')) * 60 + int(t[3] - '0') * 10 + int(t[4] - '0');
}
public:
int findMinDifference(vector<string> &timePoints) {
int n = timePoints.size();
if (n > 1440) {
return 0;
}
sort(timePoints.begin(), timePoints.end());
int ans = INT_MAX;
int t0Minutes = getMinutes(timePoints[0]);
int preMinutes = t0Minutes;
for (int i = 1; i < n; ++i) {
int minutes = getMinutes(timePoints[i]);
ans = min(ans, minutes - preMinutes); // 相邻时间的时间差
preMinutes = minutes;
}
ans = min(ans, t0Minutes + 1440 - preMinutes); // 首尾时间的时间差
return ans;
}
};
1-19 存在重复元素 II
今日的每日一题是:219. 存在重复元素 II - 力扣(LeetCode) (leetcode-cn.com)
创建一个哈希表,用来查重。
class Solution {
public:
bool containsNearbyDuplicate(vector<int>& nums, int k) {
unordered_map<int,int> map;
for(int i=0;i<nums.size();i++){
if(map.find(nums[i])==map.end()){
map.insert({nums[i],i});
}else{
if(i - map[nums[i]] <=k){
cout<<k<<endl;
return true;
}else{
map[nums[i]]=i;
}
}
}
return false;
}
};
1-20 石子游戏 IX
今天的每日一题是:2029. 石子游戏 IX - 力扣(LeetCode) (leetcode-cn.com)
博弈论
(1)A获胜条件:B移除后石子价值总和为3的倍数,且石子有剩余。
B获胜条件:A移除后石子总价值为3的倍数或石子被拿空。
(2)当cnt0 为偶数时,相当不存在。
当cnt0为奇数时,相当为1。
cnt0相当于与对手替换出手顺序,输—>赢
(3)
class Solution {
public:
bool stoneGameIX(vector<int>& stones) {
int cnt0 = 0, cnt1 = 0, cnt2 = 0;
for (int val: stones) {
if (int type = val % 3; type == 0) {
++cnt0;
}
else if (type == 1) {
++cnt1;
}
else {
++cnt2;
}
}
if (cnt0 % 2 == 0) {
return cnt1 >= 1 && cnt2 >= 1;
}
return cnt1 - cnt2 > 2 || cnt2 - cnt1 > 2;
}
};
1-21 跳跃游戏 IV
今天的每日一题是:1345. 跳跃游戏 IV - 力扣(LeetCode) (leetcode-cn.com)
图的广度优先搜索
class Solution {
public:
int minJumps(vector<int>& arr) {
//创建一个哈希表,用于保存数组中相同值的索引集合
unordered_map<int, vector<int>> idxSameValue;
//遍历更新哈希表:key:值 value:索引
for (int i = 0; i < arr.size(); i++) {
idxSameValue[arr[i]].push_back(i);
}
unordered_set<int> visitedIndex;//用于存放访问过的节点
queue<pair<int, int>> q;
q.emplace(0, 0);//先将首位置放入对列中
visitedIndex.emplace(0);
while (!q.empty()) {
auto [idx, step] = q.front();
q.pop();
if (idx == arr.size() - 1) {
return step;
}
int v = arr[idx];
step++;
//将当前元素的所有同值元素放入队列
if (idxSameValue.count(v)) {
for (auto & i : idxSameValue[v]) {
if (!visitedIndex.count(i)) {
visitedIndex.emplace(i);
q.emplace(i, step);
}
}
idxSameValue.erase(v);
}
//将当前元素的下一个元素放入
if (idx + 1 < arr.size() && !visitedIndex.count(idx + 1)) {
visitedIndex.emplace(idx + 1);
q.emplace(idx + 1, step);
}
//将当前元素的上一个元素放入
if (idx - 1 >= 0 && !visitedIndex.count(idx - 1)) {
visitedIndex.emplace(idx - 1);
q.emplace(idx - 1, step);
}
}
return -1;
}
};
1-22 删除回文子序列
今天的每日一题是:1332. 删除回文子序列 - 力扣(LeetCode) (leetcode-cn.com)
首先需要先确定什么是子序列:一个序列中的序列,如abcd中的ac就是一个子序列。
而相同字符组成的子序列也是回文的,由于该题中的字符串只由两种不同的字符组成,故最多删除两个子序列即可完成任务。
class Solution {
public:
int removePalindromeSub(string s) {
int n= s.size();
for(int i=0;i<n/2;++i){
if(s[i]!=s[n-i-1]){
return 2;
}
}
return 1;
}
};
1-23 股票价格波动
今天的每日一题是:2034. 股票价格波动 - 力扣(LeetCode) (leetcode-cn.com)
class StockPrice {
public:
StockPrice() {
this->maxTimestamp = 0;
}
void update(int timestamp, int price) {
maxTimestamp = max(maxTimestamp, timestamp);
int prevPrice = timePriceMap.count(timestamp) ? timePriceMap[timestamp] : 0;
timePriceMap[timestamp] = price;
if (prevPrice > 0) {
auto it = prices.find(prevPrice);
if (it != prices.end()) {
prices.erase(it);
}
}
prices.emplace(price);
}
int current() {
return timePriceMap[maxTimestamp];
}
int maximum() {
return *prices.rbegin();
}
int minimum() {
return *prices.begin();
}
private:
int maxTimestamp;
unordered_map<int, int> timePriceMap;
multiset<int> prices;
};
/**
* Your StockPrice object will be instantiated and called as such:
* StockPrice* obj = new StockPrice();
* obj->update(timestamp,price);
* int param_2 = obj->current();
* int param_3 = obj->maximum();
* int param_4 = obj->minimum();
*/
1-24 到达目的地的第二短时间(困难)
今天的每日一题是:2045. 到达目的地的第二短时间 - 力扣(LeetCode) (leetcode-cn.com)
BFS广度优先搜索
class Solution {
public:
int secondMinimum(int n, vector<vector<int>>& edges, int time, int change) {
//graph[0][1]代表从节点0能够到达的节点
vector<vector<int>> graph(n + 1); //横坐标为edge中的节点[0],纵坐标为edge中的节点[1]
for (auto &e: edges) {
graph[e[0]].push_back(e[1]);
graph[e[1]].push_back(e[0]);
}
// path[i][0] 表示从 1 到 i 的最短路长度,path[i][1] 表示从 1 到 i 的严格次短路长度
vector<vector<int>> path(n + 1, vector<int>(2, INT_MAX));
path[1][0] = 0;
queue<pair<int, int>> q;
q.push({1, 0});
while (path[n][1] == INT_MAX) {
auto [cur, len] = q.front();
q.pop();
for (auto next : graph[cur]) {
if (len + 1 < path[next][0]) {//假设第一次是最小的
path[next][0] = len + 1;
q.push({next, len + 1});
} else if (len + 1 > path[next][0] && len + 1 < path[next][1]) {//那么第二是就是第二最小
path[next][1] = len + 1;
q.push({next, len + 1});
}
}
}
int ret = 0;
for (int i = 0; i < path[n][1]; i++) {
if (ret % (2 * change) >= change) {
ret = ret + (2 * change - ret % (2 * change));
}
ret = ret + time;
}
return ret;
}
};
1-25 比赛中的配对次数
今天的每日一题是:1688. 比赛中的配对次数 - 力扣(LeetCode) (leetcode-cn.com)
使用模拟或数学规律的方法解决:
递归:
class Solution {
public:
int res=0;
int numberOfMatches(int n) {
if(n==1) return res;
res+=n/2;
if(n%2==1){
return numberOfMatches(n/2+1);
}else{
return numberOfMatches(n/2);
}
return res;
}
};
模拟:
class Solution {
public:
int numberOfMatches(int n) {
int ans = 0;
while (n > 1) {
if (n % 2 == 0) {
ans += n / 2;
n /= 2;
}
else {
ans += (n - 1) / 2;
n = (n - 1) / 2 + 1;
}
}
return ans;
}
};
数学:
class Solution {
public:
int numberOfMatches(int n) {
return n - 1;
}
};
----1、Sqrt(x)
二分查找:69. Sqrt(x) - 力扣(LeetCode) (leetcode-cn.com)
由
于
x
平
方
根
的
整
数
部
分
a
n
s
满
足
k
2
≤
x
的
最
大
k
值
,
因
此
我
们
可
以
对
k
进
行
二
分
查
找
,
从
而
得
到
答
案
。
由于x平方根的整数部分ans 满足 \ \ \ \ \ k^2 ≤x的最大k值,因此我们可以对k进行二分查找,从而得到答案。
由于x平方根的整数部分ans满足 k2≤x的最大k值,因此我们可以对k进行二分查找,从而得到答案。
二分查找的下界为0,上界可以粗略地设定为x。在二分查找的每一步中,我们只需要比较中间元素 mid的平方与x的大小关系,并通过比较的结果调整上下界的范围。由于我们所有的运算都是整数运算,不会存在误差,因此在得到最终的答案ans后,也就不需要再去尝试ans+1了。
由于测试用例存在溢出用例,故将mid平方强制转换成long long类型
class Solution {
public:
int mySqrt(int x) {
int l=0,r=x;
int ans=0;
while(l<=r){
int mid=l+(r-l)/2;
if((long long)mid*mid>x){
r=mid-1;
}else{
l=mid+1;
ans=mid;
}
}
return ans;
}
};
1-26 检测正方形(x)
今天的每日一题是:2013. 检测正方形 - 力扣(LeetCode) (leetcode-cn.com)
我的想法整体思路上没有问题,但在实现上出现了问题,若有时间可以思考导致错误发生的原因。
官方题解:
1、首先会想到使用哈希表来保存加入的全部点数。
这里定义如下哈希表:unordered_map<int,unordered_map<int,int>> cnt;
进行哈希嵌套,第一个键值代表纵坐标,第二个键值代表横坐标,值代表该坐标的个数
2、思考如何计算count函数返回的答案
设输入的点为( x, y) 先寻找到另一个点与其构成一条边(这里用上下边中的一条) 该点的纵坐标为y
然后枚举另一条边的纵坐标为col,那么正方形的边长d = |y - col|且大于0.
根据以上我们可以得到正方形四个点对应的坐标:
( x,y )、( x,col )、( x+d,y )、( x+d,col )
或为:( x,y )、( x,col )、( x-d,y )、( x-d,col )
因为构成正方形的点能够重复出现,故计算时需要把另外三个坐标出现的次数相乘。
class DetectSquares {
public:
//创建一个嵌套哈希表<y,<x,count>>;
unordered_map<int,unordered_map<int,int>> cnt;
DetectSquares() {
}
void add(vector<int> point) {
int x=point[0],y=point[1];
cnt[y][x]++;
}
int count(vector<int> point) {
int res=0;
int x=point[0],y=point[1];
//进行排除
if(!cnt.count(y)){
return 0;
}
//
unordered_map<int,int> &yCnt=cnt[y];
for (auto & [col, colCnt] : cnt) {
//排除掉与输入点重复的点
if (col != y) {
// 根据对称性,这里可以不用取绝对值
int d = col - y;
res += (colCnt.count(x) ? colCnt[x] : 0) * (yCnt.count(x + d) ? yCnt[x + d] : 0) *
(colCnt.count(x + d)? colCnt[x + d] : 0);
res += (colCnt.count(x) ? colCnt[x] : 0) * (yCnt.count(x - d) ? yCnt[x - d] : 0) *
(colCnt.count(x - d) ? colCnt[x - d] : 0);
}
}
return res;
}
};
/**
* Your DetectSquares object will be instantiated and called as such:
* DetectSquares* obj = new DetectSquares();
* obj->add(point);
* int param_2 = obj->count(point);
*/
思考一下为什么以下操作是错误的:
res += colCnt.count(x) * yCnt.count(x + d) * colCnt.count(x + d);
res += colCnt.count(x) * yCnt.count(x - d) * colCnt.count(x - d);
因为unordered_map是不重复哈希表,count方法只会返回0或1,即只起到了查找是否存在的功能。
而上面代码中若找到了则返回的是对应点的数量。
----1、删除链表的倒数第 N 个结点
百度面试题目:19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode) (leetcode-cn.com)
我使用的方法是利用栈先进后出的性质,循环n次进行删除,特别考虑删除头节点的情况。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* node=head;
ListNode* prenode;
stack<ListNode*> stk;
//将全部链表节点加入到栈中
while(node!=nullptr){
stk.push(node);
node=node->next;
}
//从栈中依次弹出
for(int i=n;i>0;--i){
node=stk.top();
stk.pop();
}
if(!stk.empty()){
prenode=stk.top();
prenode->next=node->next;
node->next=nullptr;
}else{
//如果栈空了则代表删除的是头节点
head=head->next;
node->next=nullptr;
}
return head;
}
};
该题目建议使用快慢指针:
我们也可以在不预处理出链表的长度,以及使用常数空间的前提下解决本题。
由于我们需要找到倒数第n个节点,因此我们可以使用两个指针 first和second同时对链表进行遍历,并且first 比 second超前n个节点。当first遍历到链表的末尾时,second就恰好处于倒数第n个节点。
具体地,初始时 first和second均指向头节点。我们首先使用first对链表进行遍历,遍历的次数为n。此时,first 和second之间间隔了n-1个节点,即first 比 second超前了n个节点。
在这之后,我们同时使用first和second对链表进行遍历。当first遍历到链表的末尾(即first为空指针)时,second恰好指向倒数第n个节点。
根据方法一和方法二,如果我们能够得到的是倒数第n个节点的前驱节点而不是倒数第n个节点的话,删除操作会更加方便。因此我们可以考虑在初始时将second.指向哑节点,其余的操作步骤不变。这样一来,当first遍历到链表的末尾时,second的下一个节点就是我们需要删除的节点。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
//创建一个哑节点,解决删除头节点问题
ListNode* dummy = new ListNode(0, head);
ListNode* first = head;
//让慢指针指向哑节点,当快指针遍历到空时,慢指针指向被删除节点的前驱节点
ListNode* second = dummy;
//快指针先走n步,与head差n-1
for (int i = 0; i < n; ++i) {
first = first->next;
}
//慢指针找到前驱节点
while (first) {
first = first->next;
second = second->next;
}
//将被删除节点的前驱节点和后继节点连接
second->next = second->next->next;
//将哑节点的后继节点作为head返回
ListNode* ans = dummy->next;
//释放内存,避免内存泄漏
delete dummy;
return ans;
}
};
时间复杂度:O(L),其中L是链表的长度。空间复杂度:O(1)。
1-27 句子中的有效单词数
今天的每日一题是:2047. 句子中的有效单词数 - 力扣(LeetCode) (leetcode-cn.com)
该题为字符串模拟题,也可用正则表达式解决。
首先将句子按空格分隔成单词,然后判断单词是否有效。
由题意知,单词不有效的条件为以下其中之一:
- 单词中包含数字;
- 单词中包含两个以上连字符;
- 连字符在单词头部或者单词末尾;
- 连字符的左/右边字符不是小写字母;
- 单词中的标点符号不在单词的末尾。
记录有效的单词的个数,即为答案。
class Solution {
public:
int countValidWords(string sentence) {
int n = sentence.length();
//单词左右边界,用来分割字符串
int l = 0, r = 0;
int ret = 0;
//访问避免复制与修改
string_view slice(sentence);
while (true) {
//跳过空格
while (l < n && sentence[l] == ' ') {
l++;
}
//越界终止
if (l >= n) {
break;
}
//重设右边界,设置r=l也可以,但是边界重合无意义
r = l + 1;
while (r < n && sentence[r] != ' ') {
r++;
}
//此时r为' '故需要-1
if (isValid(slice.substr(l, r - l))) { // 判断根据空格分解出来的 token 是否有效
ret++;
}
//重设左边界
l = r + 1;
}
return ret;
}
bool isValid(const string_view &word) {
//首先定义被分割单词的长度为n
int n = word.length();
bool has_hyphens = false;//有连字符
//进行遍历确认是否合法
for (int i = 0; i < n; i++) {
if (word[i] >= '0' && word[i] <= '9') {//含有数字
return false;
} else if (word[i] == '-') {//含有'-'
//位于头或尾或前后不为小写字母 或一个单词中遇到多个'-'
if (has_hyphens == true || i == 0 || i == n - 1 || !islower(word[i - 1]) || !islower(word[i + 1])) {
return false;
}
//用于验重
has_hyphens = true;
} else if (word[i] == '!' || word[i] == '.' || word[i] == ',') {//特殊符号不位于尾
if (i != n - 1) {
return false;
}
}
}
return true;
}
};
1-28 游戏中弱角色的数量
今天的每日一题是:1996. 游戏中弱角色的数量 - 力扣(LeetCode) (leetcode-cn.com)
方法一:排序
当存在攻防都大的角色时,该角色即为若角色。
选择攻防某一个(攻击)进行排序,并不断保存另一个最大属性(防御)maxDef;
不断进行遍历,当遍历到的元素防御小于保存值maxDef则认为该元素为若角色,res++;
需要考虑的特殊情况:
攻击值相等,若防御大在前则maxDef可能会被更新到该值,导致后者被误判为弱角色。
将攻击相等的元素分在同一个小组即可解决这个问题。
这里的处理方法是将攻击力相同的元素根据其防御力从小到大进行排序,这样就很好的避免了这种情况。
class Solution {
public:
int numberOfWeakCharacters(vector<vector<int>>& properties) {
//自定义排序
sort(properties.begin(),properties.end(), [](const vector<int> & a,const vector<int> & b){
return a[0] == b[0]? (a[1] < b[1]) : (a[0] > b[0]);
});
int maxDef = 0;
int res = 0;
for(auto &p:properties){
if(p[1] < maxDef){
res++;
}else{
maxDef = p[1];
}
}
return res;
}
};
方法二:单调栈
(1)将数组按照攻击从小到大排序,攻击相同则按照防御从大到小排序
(2)遍历数组,判断当前p[1]压入栈中,若p[1]大于栈顶元素则代表栈顶元素为弱角色,ans++,并弹出栈顶元素。直到不符合循环条件结束循环,将当前元素压入到栈中。
class Solution {
public:
int numberOfWeakCharacters(vector<vector<int>>& properties) {
//自定义排序
sort(begin(properties), end(properties), [](const vector<int> & a, const vector<int> & b) {
return a[0] == b[0] ? (a[1] > b[1]) : (a[0] < b[0]);
});
//创建一个用来压入防御的单调栈
stack<int> st;
int ans = 0;
for (auto & p: properties) {
while (!st.empty() && st.top() < p[1]) {
++ans;
st.pop();
}
st.push(p[1]);
}
return ans;
}
};
这里需要注意自定义排序之间的区别,begin(properties)效果等同于properties.begin();
1-29 地图中的最高点(x)
今天的每日一题是:1765. 地图中的最高点 - 力扣(LeetCode) (leetcode-cn.com)
该题是典型的多源bfs搜索,遍历到所有水域位置后向其四周扩展。
因为水域必须为0,而陆地由四个方向最小高度+1得到,故周围有水域的陆地高度一定为1,此时没有被扩展到的代表其四周没有水域;
处理完全部水域后,开始由高度为1的陆地向四周扩展,此时没有被扩展到的节点即代表周围最小高度不为1;
根据以上原理,当完成全部区域的扩展时,得到答案数组。
故创建一个大小m×n的数组,初始化为-1代表未被扩展。
创建一个队列,根据先入先出的规则来进行扩展,将遍历传入数组得到的水域位置压入队列中。每当成功扩展到一个新的位置,都将该位置压入队列。
//代表四个不同方向,用来遍历找到出最小高度
int dirs[4][2]={{-1,0},{1,0},{0,-1},{0,1}};
class Solution {
public:
vector<vector<int>> highestPeak(vector<vector<int>>& isWater) {
int m= isWater.size(),n=isWater[0].size();
vector<vector<int>> res(m,vector<int>(n,-1));//初始化为-1代表格子未被访问过
queue<pair<int,int>>q;//用来存放所有水域的坐标
//遍历数组,将所有水域坐标入队
for(int i=0;i<m;++i){
for(int j=0;j<n;++j){
if(isWater[i][j]){ //iswater中元素值为1代表水域
res[i][j]=0;
q.emplace(i,j); //将水域坐标入队
}
}
}
while(!q.empty()){
auto &p=q.front();
for(auto &dir:dirs){
int x=p.first + dir[0],y = p.second + dir[1];
if(0 <=x && x < m && 0 <= y && y < n && res[x][y] == -1){
res[x][y] = res[p.first][p.second] + 1;
//将由此得到的
q.emplace(x,y);
}
}
q.pop();
}
return res;
}
};
1-30 两句话中的不常见单词(?)
今天的每日一题是:884. 两句话中的不常见单词 - 力扣(LeetCode) (leetcode-cn.com)
方法一:模拟
将全部单词存入哈希表,遍历哈希表元素,个数唯一代表不常见加入到res中。
class Solution {
public:
unordered_map<string,int> _map;
vector<string> uncommonFromSentences(string s1, string s2) {
vector<string> res;
help(s1);
help(s2);
for(auto num:_map){
if(num.second == 1){
res.push_back(num.first);
}
}
return res;
}
void help(string str){
string s="";
for(auto ch:str){
if(ch!=' '){
s+=ch;
}else{
_map[s]++;
s="";
}
}
_map[s]++;
}
};
方法二:使用stringstream
class Solution {
public:
vector<string> uncommonFromSentences(string s1, string s2) {
unordered_map<string, int> freq;
auto insert = [&](const string& s) {
stringstream ss(s);
string word;
while (ss >> word) {
++freq[move(word)];
}
};
insert(s1);
insert(s2);
vector<string> ans;
for (const auto& [word, occ]: freq) {
if (occ == 1) {
ans.push_back(word);
}
}
return ans;
}
};
1-31 将数字变成 0 的操作次数
今天的每日一题是:1342. 将数字变成 0 的操作次数 - 力扣(LeetCode) (leetcode-cn.com)
直接模拟。
class Solution {
public:
int numberOfSteps(int num) {
int ret = 0;
while (num) {
ret += (num > 1 ? 1 : 0) + (num & 0x01);//判断是否未奇数,若为奇数+2
num >>= 1; //相当于除2
}
return ret;
}
};
十六进制 0x01和0x1有区别吗?
位数不一样
0x01-----00000001
0x1------0001
按位取反 赋给一字节变量
0x01--------------11111110
0x1—>1110–|---->算术扩展->00001110
|---->逻辑扩展->11111110
刷题总结
字符串
遇到字符串问题首先就要想用栈解决的方法
str.substr(index,size);
返回一个str的子串,从index开始,返回大小为size的字符串(包含index)。
若不指定size,则默认到最后一个字符。
str.erase(index,len);
对字符串str执行操作,从指定位置index开始,删除len个字符。(包含index位置字符)
str.erase();代表全部删除。
str.erase(3);代表删除索引3以及其后全部字符。
str.length()和str.size()
length()函数返回字符串的长度. 这个数字应该和size()返回的数字相同.
string_view slice(sentence);
C++17内容
当字符串数据的所有权已经确定(譬如由某个string对象持有),并且你只想访问(而不修改)他们时,使用 std::string_view 可以避免字符串数据的复制,从而提高程序效率,这(指程序效率)
reverse();
字符串旋转:reverse(str.begin(),str.end());
局部旋转:reverse(str.begin(),str.begin()+n);//从索引0到1这部分旋转。
vector:
创建一维数组初始化:
auto dp = vector (s.size() + 1);
vector f(n,3);
创建二维数组初始化:
vector<vector> f(n, vector(3));
vec.resize();
重设vector数组,使用方法与初始化相同。
*max_element(dp.begin(), dp.end());
枚举出数组中的最大元素
accumulate(dp.begin(), dp.end(), 0LL);
返回数组中全部的元素和
vec.erase();
使用迭代器进行删除:erase(location)或erase(first , last)
vector<int>::iterator it;
for (it = mark.begin(); *it != key; it++);
mark.erase(it);
math:
1、min(i,min(j,k));
返回i,j,k中最小的那个。
2、pow,sqrt,abs,fabs
#include <math.h>
//平方 pow()
int a = pow(4,2);// 4的平方=16
//开方
int b = pow(4,0.5);// 4的平方根=2
int c = sqrt(4);// 4的平方根=2
//整数绝对值
int c = abs(b-c);
//浮点数绝对值
double d = fabs(b-c);
3、 return vec[random() % vec.size()];
返回数组中0~n-1中的任意元素:
4、1e9 +7:
10^9+7
5、x ** 0.5
相当于pow(x,0.5);
哈希:
unordered_set seen;
unordered_map<int,int>map;
插入insert:
map.insert({1,2});//插入key为1,value为2的一个数据
查找find():
map.find(key) !=map.end();
删除erase():
map.erase(key); //等同于map.erase(map.find(key));
强制for循环:
for(auto num:_map){
if(num.second == 1){
res.push_back(num.first);
}
}
队列:
priority_queue<long, vector, greater> heap;
优先级队列:
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> pq(cmp);
pq(cmp);`因为lambda这种特殊的class没有默认构造函数,pq内部排序比较的时候要使用的是一个实例化的lambda对象,只能通过lambda的 copy构造进行实例化,从哪里copy呢,就需要pq构造函数的时候传入这个lambda对象。pq的自定义比较也可以使用struct或者class,因为struct和class都有默认构造函数,此时就不需要`pq(Type)` 而是直接`pq`即可。同理,自定义比较也可以使用函数,同样此时也需要提供函数对象进行实例化`pq(CmpFunc)`,假设比较函数为`bool CmpFunc(int x, int y) {return x < y;}
自定义比较、有序对:
auto cmp = [&nums1, &nums2](const pair<int, int> & a, const pair<int, int> & b) {
return nums1[a.first] + nums2[a.second] > nums1[b.first] + nums2[b.second];
};
链表
C++内置链表类型:
手写双向链表:
struct DLinkedNode {
int key, value;
DLinkedNode* prev;
DLinkedNode* next;
DLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr) {}
DLinkedNode(int _key, int _value): key(_key), value(_value), prev(nullptr), next(nullptr) {}
};
排序:
sort(vec.begin() , vec.end());
默认排序,将数组元素从小到大排列
自定义排序:
sort(properties.begin(),properties.end(), [](const vector<int> & a,const vector<int> & b){
return a[0] == b[0]? (a[1] < b[1]) : (a[0] > b[0]);
});
//排序一个二维数组,按照一位首元素从大到小进行排序,当首元素相等时按照次位元素从小到大进行排序
sort(begin(properties), end(properties), [](const vector<int> & a, const vector<int> & b) {
return a[0] == b[0] ? (a[1] > b[1]) : (a[0] < b[0]);
});
//排序一个二维数组,按照一位首元素从小到大进行排序,当首元素相等时按照次位元素从大到小进行排序
有序对pair<int,int>:
调用对应有序对的方法:
auto &[a,b] =xxx;
其中a为p.first,b为p.second;
广度优先搜索
1、向四周遍历:
为使代码简洁,创建一个数组保存搜索方向
int dirs[4][2]={{-1,0},{1,0},{0,-1},{0,1}};
从源开始向特定方向搜索
for(auto &dir:dirs){
int x=p.first + dir[0],y = p.second + dir[1];
if(0 <=x && x < m && 0 <= y && y < n && res[x][y] == -1){
res[x][y] = res[p.first][p.second] + 1;
//将由此得到的
q.emplace(x,y);
}
概念总结:
闰年
二叉搜索树
笛卡尔积
等差数列求和:Sn=n*a1+n(n-1)d/2或Sn=n(a1+an)/2
等比数列求和:Sn=a1(1-q^n)/(1-q) = (a1- an*q)/(1-q) (q≠1)
丑数:把只包含质因子2,3和5的数称作丑数(Ugly Number)。
例如6、8都是丑数,但7、14不是,因为它们包含质因子7。 习惯上我们把1当做是第一个丑数。
回文串:从前向后读与从后向前读一样
二维前缀和(前缀和数组)、容斥原理:元素和小于等于阈值的正方形的最大边长 - 元素和小于等于阈值的正方形的最大边长 - 力扣(LeetCode) (leetcode-cn.com)
子序列:序列中的序列,如abc的子序列可以是ac,注意需要按原来的顺序。
字符串的子序列:字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
公共子序列:两个字符串都有的子序列
水塘抽样
完全平方数:是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1
、4
、9
和 16
都是完全平方数,而 3
和 11
不是。
轴对齐正方形 是一个正方形,除四条边长度相同外,还满足每条边都与 x-轴 或 y-轴 平行或垂直。