文章目录
刷题记录5.10-5.13
257.二叉树的所有路径
class Solution {
public:
//回溯函数
void func(TreeNode* root, vector<string>& res, vector<int>& path)
{
//一定要写在退出条件的前面
path.push_back(root->val);
//退出条件 将path数组转化为字符串传入res
if(root->left == nullptr && root->right == nullptr)
{
string result = to_string(path[0]);
for(int i = 1; i < path.size(); i++)
{
string temp = to_string(path[i]);
result = result+"->"+temp;
}
res.push_back(result);
return;
}
if(root->left)
{
func(root->left, res, path);
path.pop_back();
}
if(root->right)
{
func(root->right, res, path);
path.pop_back();
}
}
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> res;
vector<int> path;
func(root, res, path);
return res;
}
};
这一题有了回溯的味道,但是我尝试用之前回溯的模板去写这一题结果是写不出来,怎么办啊,学一点忘一点。。。
这一题要求路径,路径不难理解就是从根节点顺着到叶子节点的所有路径,最后需要输出的是一个字符串数组,里面需要添加->这种格式,对输出格式也有一定的要求。从根节点出发到叶子节点所以我们需要用前序遍历来写代码而不能是后序遍历。
经过这一题,其实递归回溯也不是完全不变的模板,特定情况有时也需要特殊情况处理,首先分析退出条件应该是root->left == nullptr && root->right == nullptr,而不是root==nullptr,叶子节点一定是左右两边都是空!因此这里就会有空指针异常的问题,这就关联到后面在每次递归前都需要判断root->left和root->right都是否为空的情况。而且,递归和回溯是捆绑起来的,所以都得写到对应的if语句里面。然后就是在退出之前需要将path数组转化为string存入res,这一段逻辑就不再赘述了。哦!对了,path数组的push_back操作一定要在递归条件判断之前,否则叶子节点的数据就无法存入!
精简版(省略回溯步骤、path改为string类型):
class Solution {
public:
void func(TreeNode* root, vector<string>& res, string path)
{
path +=to_string(root->val);
if(root->left == nullptr && root->right == nullptr)
{
res.push_back(path);
return;
}
if(root->left) func(root->left, res, path+"->");
if(root->right) func(root->right, res, path+"->");
}
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> res;
func(root, res, "");
return res;
}
};
为什么将path改为string类型之后就能省略pop_back()的操作?因为这里的path只是一个形式参数,仅能存在于当前递归函数,就达到了回溯的效果,如果将string path转化为引用类型的话会出错,这个地方得细细品。以前的模板也是习惯于将回溯函数里的path写为引用类型,这里就需要加强一下不加引用的写法和加引用的写法之间的异同。加上引用更能清楚的知道递归与回溯,省略的话自己也应当明白期间省略了什么,并且代表的什么。
牛客小白月赛93交换数字
#include <bits/stdc++.h>
using namespace std;
void solve()
{
int n;
string a, b;
cin >> n >> a >> b;
for(int i = 0; i < n; i++)
{
if(a[i] < b[i]) swap(a[i], b[i]);
}
long long proa = 0, prob = 0;
for(int i = 0; i < n; i++)
{
proa = (proa*10+(a[i]-'0'))%998244353;
prob = (prob*10+(b[i]-'0'))%998244353;
}
cout << (proa*prob)%998244353;
}
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
int t = 1;
//cin >> t;
while(t--) solve();
return 0;
}
这个题没写出来问题在于两个方面:一是题目条件看漏。二是看了题解后要是题目条件没看漏也写不出这道题。漏看的条件:可以对这两个数字进行任意次操作,我最开始以为只能改变同一个位置的两个字符串,主要是这样能过示例一,我就压根没有想过是我题目看错了,我真服了,看题还是得仔细一点啊!
题解给出的思路是这样的:想要得到任意次操作后a * b的最小值,就一定要让abs(a-b)尽可能的大,反之如果要求a * b的最大值,就一定要让abs(a-b)尽可能的小,也就是a尽可能接近b,这涉及到高中的均值不等式。计算方面的问题解决后就是取余的问题:我当时写的是int num1 = stoi(a)%mod;这样写有一种弊端,那就是如果字符串a很长,甚至超过了unsigned long long的范围是会导致溢出异常的,所以像这样长字符串转化为数字的时候需要一位一位地进行转化,并且每一位转化后就需要mod,这样才不会导致结果出错。
总结:学到了两点:1、对数字a和b任意数位交换任意次要得到a*b最小值需要abs(a-b)尽可能大,反之求a * b最大值需要abs(a-b)尽可能的小;2、长字符串转化为数字要逐个字符转化,每转化一个字符就进行取余以防止溢出
力扣周赛397—两个字符的排列差
class Solution {
public:
int findPermutationDifference(string s, string t) {
//采用哈希表解决,字符范围在a-z所以只需要一个哈希数组就行
vector<int> hash(26, 0);// key---字母 value---下标
int res = 0;
for(int i = 0; i < s.size(); i++) hash[s[i]-'a'] = i;
for(int i = 0; i < t.size(); i++) res += abs(hash[t[i]-'a']-i);
return res;
}
};
这也就是前两天刷到的哈希表题目,前前后后只用了4分钟ac了这一题,复习还是有用的!具体细节都在注释里了,其余的步骤就按照题目要求来写就行了。
力扣周赛397—从魔法师身上吸取的最大能量
class Solution {
public:
int maximumEnergy(vector<int>& energy, int k) {
int res = INT_MIN;
for(int i = energy.size()-1; i >= energy.size()-k; i--)
{
int s = 0;
for(int j = i; j >= 0; j -= k)
{
s += energy[j];
res = max(res, s);
}
}
return res;
}
};
这个题唯一疑惑在于我用前缀和的方式写会超时,而题解用后缀和的方式就不会超时,我真的很郁闷!题干中有个字眼:可以选择一个起点,而终点一定在序列的末端,所以这里用到后缀和,前后缀和这类题目还是做的比较少,还需要加强!
34.在排序数组中查找元素的第一个和最后一个位置
class Solution {
public:
int lowebound(vector<int>& nums, int target)
{
int left = 0, right = nums.size()-1;//左闭右闭区间
while(left <= right)
{
int mid = (right-left)/2+left;
if(nums[mid] >= target) right = mid-1;
else left = mid+1;
}
return left;
}
vector<int> searchRange(vector<int>& nums, int target) {
//如果数组为空,返回{-1, -1}
if(nums.size() == 0) return {-1, -1};
int start = lowebound(nums, target);
//两个条件分别是:
//1、数组中的所有元素都小于target
//2、找到了第一个大于等于target的值了,只不过不是target,而是第一个大于target的值,即数组中不存在target值。
if(start == nums.size() || nums[start] != target) return {-1, -1};
int end = lowebound(nums, target+1)-1;
return {start, end};
}
};
这一题已经是三刷了,结果还是能出错。。。感觉还是理解的不够透彻,每刷一遍都有新的理解,这一题实在是二分的经典!我写的lowerbound函数是求出第一个大于等于target的下标,这个函数里面的二分有几个需要注意的地方,一旦出错就是死循环或者结果不正确。1、区间定义贯穿整个函数,我习惯于定义为左闭右闭区间,所以while的退出条件要为left<=right,边界的修改分别为right=mid-1和left=mid+1;2、这一点也是我之前没有注意到的,就是nums[mid]>=target一定不能写成nums[mid]>target,因为最后的循环不变量是left,那么left就是我们第一个大于等于target的下标,那么left以左的都是小于target的值,所以在判断条件里面一定不能
是nums[mid] <= target。
再就是逻辑方面的问题,要考虑的几种特殊情况没有调理好,导致出错。详细步骤写在了注释上啦~
对比两个函数:
class Solution {
public:
//区分两个写法,仅仅在判断语句中进行了更改,关键在于循环不变量、退出条件之间的关系
//查找第一个大于等于target的下标
int lowebound(vector<int>& nums, int target)
{
int left = 0, right = nums.size()-1;//左闭右闭区间
while(left <= right)
{
int mid = (right-left)/2+left;
if(nums[mid] < target) left = mid+1;
else right = mid-1;
}
return left;
}
//查找第一个大于target的下标
int lowebound2(vector<int>& nums, int target)
{
int left = 0, right = nums.size()-1;//左闭右闭区间
while(left <= right)
{
int mid = (right-left)/2+left;
if(nums[mid] <= target) left = mid+1;
else right = mid-1;
}
return left;
}
vector<int> searchRange(vector<int>& nums, int target) {
//如果数组为空,返回{-1, -1}
if(nums.size() == 0) return {-1, -1};
int start = lowebound(nums, target);
if(start == nums.size() || nums[start] != target) return {-1, -1};
int end = lowebound2(nums, target)-1;
return {start, end};
}
};
这也是能够正确运行的代码,对比一下两个函数之间只在判断条件中进行了更改,这也使得到的结果产生了不同,关键是理清楚循环不变量和退出条件之间的关系,真的突然有一种醍醐灌顶的感觉!由于循环不变量是left,返回的下标也是left,判断条件中是nums[mid] < left,那么循环结束后left以左的都是小于target,所以当前left所指的就是第一个>=target的下标;判断条件中是nums[mid] <= left,那么循环结束后left以左的都是小于等于target的,所以当前left所指的就是第一个>target的下标。好好体会!如果依据题意改变边界条件也要会灵活的改变!建议:判断条件将left改变的那一行写在if中,这样便于理解,这个可以当作二分的模板记下来!
2529.正整数和负整数的最大计数
class Solution {
public:
int maximumCount(vector<int>& nums) {
int neg = ranges::lower_bound(nums, 0)-nums.begin();
int pos = nums.end()-ranges::upper_bound(nums, 0);
return max(neg, pos);
}
};
这里直接给出二分法内置的函数,为什么这里能用,而前面的几道题不能用呢?—>观察题干中的问题我发现前面几题都是求的具体下标或者是那个下标的数据、集合。而这一题呢它求的是区间的个数,并且与首尾都相关联,所以可以直接调用c++的内置函数ranges::lower_bound()和ranges::upper_bound()他们两个函数的返回值都是迭代器所以neg那行等价于neg = index-0;pos那行等价于pos = n-index;然后就是根据题意来判断哪里要加一哪里要减一,这个适情况而定,重要在于掌握求区间的方法。两个迭代器相减就求得了其之间包含的元素,也可以理解成左闭右开。[0,index)就包含index个元素,[index,n)就包含n-index个元素。
牛客周赛42
A.小红战小紫
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll N = 1e5;
void solve()
{
string s;
cin >> s;
cout << (s.size() == 1 ? "yukari" : "kou");
}
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t = 1;
//cin >> t;
while(t--) solve();
return 0;
}
我真的服了这个题,我当时还一直以为是题目出错了还是怎么的,一直在怀疑,可能是我做这种题做少了吧,脑子还是不太灵光,解析就是:只要s长度大于1小红先手小红都是赢,其次只需要考虑s长度为1的时候小红先手不能对s进行操作,小红输。就一个简单的if判断逻辑,我怎么就想不到???当然吧我觉得这种题其实也没什么意义。。。
C.小红的素数合并
#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll N = 1e5;
void solve()
{
int n;
cin >> n;
ll a[n+1];
for(int i = 0; i < n; i++) cin >> a[i];
sort(a, a+n);//普通数组的排序方法
ll Max = 0, Min = INT_MAX;
int left = 0, right = n-1-(n%2);//奇数情况-1
while(left < right)
{
Max = max(Max, a[left]*a[right]);
Min = min(Min, a[left]*a[right]);
left++;
right--;
}
//考虑奇数情况
if(n%2)
{
Max = max(Max, a[n-1]);
Min = min(Min, a[n-1]);
}
cout << Max-Min;
}
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t = 1;
//cin >> t;
while(t--) solve();
return 0;
}
这一题吧,其实当时有想到这个方法,但是只是思考了一下,并没有敲代码的想法,心里还是想着太麻烦了就不想写,唉,还是要摒弃这种思想吧,既然是比赛就要当比赛去打,还是要端正态度,当时心里想着就是没有考虑到奇数的情况,要是暴力写的话应该也是能混一点分的。当然对题干的理解也是差点意思,我以为会一直合并,其实仅仅只是合并素数而已,相乘之后一定是合数,合数之间不能相乘的,我还反而把问题相复杂了,总结一下把:态度需要放端正、读题要细心、该暴力的还是要硬着头皮暴力写一写。
D.小红树上删边
#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll N = 1e5;
//g为邻接表每个节点i的邻居保存再g[i]中
vector<int> g[202020];
//num[i]表示节点i为子树的节点个数,要包括节点i
int num[202020];
//符合条件的计数器 作为结果
int ans = 0;
//参数分别为当前节点x和父节点prarent
void dfs(int x, int prarent)
{
//初始化当前节点的节点数为1,即自身节点
num[x] = 1;
for(auto i : g[x])
{
if(i != prarent)
{
//向子树dfs
dfs(i, x);
//回溯当前节点的节点个数
num[x] += num[i];
}
}
//如果当前节点不是根节点 并且节点数是偶数 那么ans++ 因为根节点无法删除边
if(x != 1 && num[x]%2 == 0) ans++;
}
void solve()
{
int n, i, j;
cin >> n;
//构建无向图模板 n个节点n-1条边 所以从1开始 树型结构n个节点n-1条边
for(int i = 1; i < n; i++)
{
int x, y;
cin >> x >> y;
g[x].push_back(y);
g[y].push_back(x);
}
//参数分别为根 父节点
dfs(1, 0);
if(n%2 == 1) cout << -1;
else cout << ans;
}
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t = 1;
//cin >> t;
while(t--) solve();
return 0;
}
这个题涉及到树图的dfs,这里也是我相对薄弱的地方,没怎么做过这一类型的题目。先说一下解题思路,它说要删除多少条边能达到每个连通块都为偶数,删除的边要尽可能的多,那么其实就是遍历一棵树当遇到一个节点时,以该节点的子树的节点个数如果为偶数那么就让计数器加一(也就是删除的边数加一)再往下遍历的时候也一定还会在某个节点出现偶数个节点的子树然后计数器再累加一如此这样递归下去。准确来说不是递归的过程让计数器加一,应该说是回溯的过程中让计数器加一。解题思路差不多就是这样,这个题还有几个知识盲区:1、树型结构n个节点n-1条边,所以主函数的循环是从1至n,从1开始也是为了和题意同步,第一个节点标号就是1,这样不会因为节点标号而混乱;2、树型结构可以转换成一个无向图,无向图的初始化这个重点记忆!这个g[x].push_back(y)这种形式也是第一次遇到,以前都是v.push_back()这种之类的;3、这个dfs函数我感觉可以类比成回溯函数,要求得每个节点的节点个数就是用回溯来实现的,回溯的过程中来更新ans值;4、dfs函数的参数也很有特点,x表示当前节点parent表示父节点
2559统计范围内的元音字符串数
class Solution {
public:
vector<int> vowelStrings(vector<string>& words, vector<vector<int>>& queries) {
unordered_set<char> letters = {'a', 'e', 'i', 'o', 'u'};
vector<int> prefix(words.size()+1, 0);
for(int i = 0; i < words.size(); i++)
{
string s = words[i];
prefix[i+1] = prefix[i];
if(letters.count(s[0]) && letters.count(s[s.size()-1])) prefix[i+1]++;
}
vector<int> res;
for(int i = 0; i < queries.size(); i++)
{
auto t = queries[i];
res.push_back(prefix[t[1]+1]-prefix[t[0]]);
}
return res;
}
};
这一题用到的是前缀和与哈希表,主要是我找的前缀和题目,所以知道用前缀和来解题,不然我可能想不到这种题用前缀和来写。总结一下这一题的特点吧,首先这里有一个区间数组,然后我们得到的结果是询问这个区间所满足条件的个数,这个个数又在words数组中有连续的特点,所以可以用到前缀和。以后看到这种题至少会有个思路吧!确实前缀和肯定要省时间一点,否则就是for循环嵌套,这样太耗费时间了。这里还有个用的妙的地方就是letters哈希表,这样让判断条件里面简洁了很多,我第一遍写的时候if条件实在是太多了,看着就很不舒服哈哈哈。auto那一行就表示t是一个一维数组,就直接将t当成一个一维数组来用就是了,实在不行就定义变量l和r分别表示左右边界。
2389.和有限的最长子序列
class Solution {
public:
vector<int> answerQueries(vector<int>& nums, vector<int>& queries) {
sort(nums.begin(), nums.end());
//原地修改nums,nums就为prefix数组
for(int i = 1; i < nums.size(); i++) nums[i] += nums[i-1];
//q为引用
for(int& q : queries)
{
//求出[l,r)区间长度
q = ranges::upper_bound(nums.begin(), nums.end(), q)-nums.begin();
}
return queries;
}
};
这一题就是前缀和与二分法都用到了,刚好能够串联起这两天所学内容。由于这里求的是最长子序列长度,那么就与数组的顺序是没有关系的,所以我们可以先将数组排序。这里的nums数组充当了prefix数组,直接可以在当前nums数组进行修改,同样的在遍历queries数组的时候我们将求的长度也直接存入了进去,结果数组也就用queries数组代替了,使这一题的空间复杂度大大降低,具体实现在于for循环遍历时q是引用,对q的修改也就是对queries[i]的修改,实在是妙。再就是昨天总结到的区间长度和upper_bound函数,首先upper_bound函数先求出第一个大于q的迭代器再与初始迭代器相减就得出了区间的长度。