刷题笔记---7月
1512.好数对的数目(哈希)
cpp
class Solution {
public:
int numIdenticalPairs(vector<int>& nums) {
int cnt = 0;
unordered_map<int, int> hash;//nums[i]---个数
for(int i = 0; i < nums.size(); i++) {
cnt += hash[nums[i]];
hash[nums[i]]++;
}
return cnt;
}
};
类似于两数之和,这种题存在两个变量,这里可以用技巧:枚举右,维护左,对于a+b=t的问题,我们可以枚举b,然后查找b左边的是否有a=t-b,这可以用哈希表来维护。
go
func numIdenticalPairs(nums []int) int {
hash := make(map[int]int)
cnt := 0
for _, x := range nums {
cnt += hash[x]
hash[x]++
}
return cnt
}
go写法主要是记住hash怎么定义
2815数组中的最大数对和(哈希)
cpp
class Solution {
public:
int getMax(int x) {
int ans = 0;
while(x) {
ans = max(ans, x%10);
x /= 10;
}
return ans;
}
int maxSum(vector<int>& nums) {
unordered_map<int, int> hash;//最大数位---元素值
int ans = -1;
for(int i = 0; i < nums.size(); i++) {
int x = getMax(nums[i]);
cout << x << ' ';
if(hash[x]) {
ans = max(ans, nums[i]+hash[x]);
}
hash[x] = max(hash[x], nums[i]);
}
return ans;
}
};
仅仅只在上一题的基础上添加了一个数位上的最大值,数位上的最大值只需要对单个数进行取模运算拿到就行,这可以在外头写一个函数,也可以这样写
cpp
int x = 0;
for(int v = nums[i]; v > 0; v /= 10) {
x = max(x, v%10);
}
go
func maxSum(nums []int) int {
ans := -1
hash := make([]int, 10)
for _, x := range nums {
bit := 0
for v := x; v > 0; v /= 10 {
bit = max(bit, v%10)
}
//fmt.println(bit)
if hash[bit] != 0 {
ans = max(ans, hash[bit]+x)
}
hash[bit] = max(hash[bit], x)
}
return ans
}
3115.质数的最大距离(枚举右、维护左)
cpp
class Solution {
public:
bool isPrime(int x) {
if(x == 1) return false;
for(int i = 2; i < x; i++) {
if(x%i == 0) return false;
}
return true;
}
int maximumPrimeDifference(vector<int>& nums) {
int min_i = INT_MAX, max_i = 0;
for(int i = 0; i < nums.size(); i++) {
if(isPrime(nums[i])) {
min_i = min(min_i, i);
max_i = max(max_i, i);
}
}
return abs(max_i-min_i);
}
};
isPrime函数用来判断一个数是否为质数,这里wa了一次,就是1需要特判,1既不是质数也不是合数。min_i和max_i分别用来维护质数的最小和最大下标。
3033.修改矩阵(模拟)
cpp
class Solution {
public:
vector<vector<int>> modifiedMatrix(vector<vector<int>>& matrix) {
for(int j = 0; j < matrix[0].size(); j++) {
int mx = 0;
for(auto& row : matrix) {
mx = max(mx, row[j]);
}
for(auto& row : matrix) {
if(row[j] == -1) {
row[j] = mx;
}
}
}
return matrix;
}
};
先遍历列,再遍历行,先维护每一列的最大值,然后修改每一列为-1的值。注意auto为引用。
3101.交替子数组计数(数组、数学)
cpp
class Solution {
public:
long long countAlternatingSubarrays(vector<int>& nums) {
long long ans = 0;
int l = 0, r = 1;
for(; r < nums.size(); r++) {
if(nums[r] != nums[r-1]) {
ans += r-l;
//l = r;
} else {
l = r;
}
}
return ans+nums.size();
}
};
这里用到了一个子数组个数的技巧,前提是[l,r)区间内是符合题目条件的,这样其子数组计数为r-l个,在不符合条件的时候需要重新维护左边界的位置。
cpp
func countAlternatingSubarrays(nums []int) int64 {
ans := len(nums)
l, r := 0, 1
for r < len(nums) {
if nums[r] != nums[r-1] {
ans += r-l
} else {
l = r
}
r++
}
return int64(ans)
}
返回值是int64类型,最后一定要类型转换
2342.数位和相等数对的最大和(哈希)
cpp
class Solution {
public:
int maximumSum(vector<int>& nums) {
unordered_map<int, int> hash;//数位和---最大值
int mx = -1;
for(int i = 0; i < nums.size(); i++) {
int x = nums[i], digit = 0;
while(x > 0) {
digit += x%10;
x /= 10;
}
if(hash.count(digit)) {
mx = max(mx, hash[digit]+nums[i]);
}
hash[digit] = max(hash[digit], nums[i]);
}
return mx;
}
};
方法还是枚举右,维护左,维护的操作可以用哈希表或者哈希数组来实现。
go
func maximumSum(nums []int) int {
hash := make(map[int]int)//数位和---最大值
mx := -1
for _, num := range nums {
digit := 0
for x := num; x > 0; x /= 10 {
digit += x%10
}
if hash[digit] > 0 {
mx = max(mx, hash[digit]+num)
}
hash[digit] = max(hash[digit], num)
}
return mx
}
1679.k和数对的最大数目(双指针、哈希表)
哈希表解法:
cpp
class Solution {
public:
int maxOperations(vector<int>& nums, int k) {
unordered_map<int, int> hash;//值---个数
int ans = 0;
for(int x : nums) {
if(hash[k-x] > 0) {
hash[k-x]--;
cout << x;
ans++;
} else {
hash[x]++;
}
}
return ans;
}
};
go
func maxOperations(nums []int, k int) int {
hash := make(map[int]int)
ans := 0
for _, num := range nums {
if hash[k-num] > 0 {
ans++
hash[k-num]--
} else {
hash[num]++
}
}
return ans
}
哈希表做法其实就是对应两数之和这一题的解法,这一题完全可以把题目意思转换为两数之和为k的数对有多少个?一个位置的元素只能出现一次。
双指针解法:
cpp
class Solution {
public:
int maxOperations(vector<int>& nums, int k) {
sort(nums.begin(), nums.end());
int ans = 0;
for(int i = 0, j = nums.size()-1; i < j; ) {
if(nums[i]+nums[j] > k) {
j--;
} else if(nums[i]+nums[j] < k) {
i++;
} else {
i++;
j--;
ans++;
}
}
return ans;
}
};
go
func maxOperations(nums []int, k int) int {
sort.Ints(nums)
ans := 0
i, j := 0, len(nums)-1
for i < j {
if nums[i]+nums[j] < k {
i++
} else if nums[i]+nums[j] > k {
j--
} else {
i++
j--
ans++
}
}
return ans
}
双指针第一时间是没有想到的,是用完哈希写法后发现时间性能很低,看了一眼标签发现是双指针,我第一步思路就是看看能不能排序,如果排序完之后不会影响结果,那么这一题就很符合双指针的特点。
1010.总持续时间可被60整除的歌曲(哈希)
cpp
class Solution {
public:
int numPairsDivisibleBy60(vector<int>& time) {
unordered_map<int, int> hash;//时间%60---个数
int ans = 0;
for(int t : time) {
int temp = (60-t%60)%60;
if(hash.count(temp)) {
ans += hash[temp];
}
hash[t%60]++;
}
return ans;
}
};
突然发现这号线和我之前某次周赛写的题好像!周赛那个题就是被k整除,这个题也就是将k变成了60,力扣也是没题出了在这种方面改。。。
go
func numPairsDivisibleBy60(time []int) int {
hash := make(map[int]int)
ans := 0
for _, t := range time {
ans += hash[(60-t%60)%60]
hash[t%60]++
}
return ans
}
2971.找到最大周长的多边形(前缀和)
cpp
class Solution {
public:
long long largestPerimeter(vector<int>& nums) {
//1 1 2 3 5 12 50
long long sum = 0, mx = -1;
sort(nums.begin(), nums.end());
sum += nums[0]+nums[1];
for(int i = 2; i < nums.size(); i++) {
if(sum > nums[i]) {
mx = max(mx, sum+nums[i]);
}
sum += nums[i];
}
return mx;
}
};
go
func largestPerimeter(nums []int) int64 {
mx, sum := -1, 0
//sort.Ints(nums)
slices.Sort(nums)
sum += nums[0]+nums[1]
for i := 2; i < len(nums); i++ {
if sum > nums[i] {
mx = max(mx, sum+nums[i])
}
sum += nums[i]
}
return int64(mx)
}
很久没有独立做出来一道中等题啦哈哈哈,这一题也是分析出用前缀和来写了,mx用来维护符合多边形的最大周长,这一题我看灵神用的从后往前遍历,其实都一样吧。
2874.有序三元组中的最大值II(枚举、贪心)
枚举j
cpp
class Solution {
public:
long long maximumTripletValue(vector<int>& nums) {
int n = nums.size();
vector<int> surfmax(n+1, 0);
for(int k = n-1; k > 1; k--) {
surfmax[k] = max(surfmax[k+1], nums[k]);
}
long long ans = 0;
int premax = nums[0];
for(int j = 1; j < nums.size()-1; j++) {
ans = max(ans, (long long)(premax-nums[j])*surfmax[j+1]);
premax = max(premax, nums[j]);
}
return ans;
}
};
go
func maximumTripletValue(nums []int) int64 {
n := len(nums)
surfmax := make([]int, n+1)
for k := n-1; k > 1; k-- {
surfmax[k] = max(surfmax[k+1], nums[k])
}
ans, premax := 0, nums[0]
for j := 1; j < n-1; j++ {
ans = max(ans, (premax-nums[j])*surfmax[j+1])
premax = max(premax, nums[j])
}
return int64(ans)
}
做题的时候有这么一个思想,就是找i的最大,j的最小和k的最大,但是写法还是过于粗糙了,写了三个for循环,样例能过一部分,但是有一部分样例得不到正确结果,看了题解之后还是很有感触的,其实整体的方法还是枚举右维护左(对应的是枚举k的方法,这里写的枚举j,后面会写枚举k),枚举j呢也就是要找到j左边和右边的最大值,要找到j右边的最大值需要一个后缀数组sufmax来维护后缀最大值,并且在初始化长度的时候一定是要n+1,因为递推式surfmax[k] = max(surfmax[k+1], nums[k])
,否则可能会导致数组越界错误。而前缀最大值不需要数组来维护,只需要在枚举j的过程中维护前缀最大值即可。
枚举k
cpp
class Solution {
public:
long long maximumTripletValue(vector<int>& nums) {
int diffmax = 0, premax = 0;
long long ans = 0;
for(int x : nums) {
ans = max(ans, (long long)diffmax*x);
premax = max(premax, x);
diffmax = max(diffmax, premax-x);
cout << diffmax << '\t' << premax << '\n';
}
return ans;
}
};
go
func maximumTripletValue(nums []int) int64 {
diffmax, premax, ans := 0, 0, 0
for _, k := range nums {
ans = max(ans, diffmax*k)
diffmax = max(diffmax, premax-k)
premax = max(premax, k)
}
return int64(ans)
}
这一题枚举k就很像前面做过的121.买卖股票的最佳时机,也是枚举右边,左边用max或min来维护变量。
1814.统计一个数组中好对子的数目(哈希)
cpp
class Solution {
public:
// int rev(int x) {
// int res = 0;
// while(x > 0) {
// res = res*10+x%10;
// x /= 10;
// }
// return res;
// }
int countNicePairs(vector<int>& nums) {
//nums[i]-rev(nums[i]) = nums[j]-rev(nums[j]);
unordered_map<int, int> hash;//nums[i]-rev(nums[i]) --- 个数
int ans = 0, mod = 1e9+7;
auto rev = [](int x) {
int res = 0;
while(x > 0) {
res = res*10+x%10;
x /= 10;
}
return res;
};
for(int i = 0; i < nums.size(); i++) {
ans = (ans+hash[nums[i]-rev(nums[i])])%mod;
hash[nums[i]-rev(nums[i])]++;
}
return ans;
}
};
go
func countNicePairs(nums []int) int {
hash := make(map[int]int)
ans, mod := 0, int(1e9+7)
rev := func(x int) int {
res := 0
for x > 0 {
res = res*10+x%10
x /= 10
}
return res
}
for i := range nums {
t := nums[i]-rev(nums[i])
ans = (ans+hash[t])%mod
hash[t]++
}
return ans
}
// func rev(x int) int {
// res := 0
// for x > 0 {
// res = res*10 + x%10
// x /= 10
// }
// return res
// }
首先进行式子的变换,第一直觉就是将i和j放在一起,所以可以将题干中的条件转化成nums[i]-rev(nums[i]) = nums[j]-rev(nums[j])
可以发现等式两边的式子很像,这样可以利用枚举右维护左,维护的时候用哈希表来维护,来完成这一题,然后就是巩固了一下lambda表达式的写法,和学习了一下go语言lambda表达式的写法,对于go语言的话,lambda表达式其实就是在正常的函数定义前加了一个rev :=
然后其它地方就是函数的定义。然后就是go语言书写的时候mod的定义是mod := int(1e9+7)
,如果直接写mod:=1e9+7
是会报错的。因为在go语言中1e9+7是浮点类型,需要强制转换成整型。
2559.统计范围内的元音字符数(前缀和)
go
func vowelStrings(words []string, queries [][]int) []int {
n := len(words)
prefix := make([]int, n+1)
for i := 1; i < n+1; i++ {
prefix[i] = prefix[i-1]
s := words[i-1]
if isTrue(s[0]) && isTrue(s[len(s)-1]) {
prefix[i]++
}
}
ans := make([]int, len(queries))
for i, q := range queries {
ans[i] = prefix[q[1]+1]-prefix[q[0]]
}
return ans
}
func isTrue(c byte) bool {
if c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' {
return true
}
return false
}
go
func vowelStrings(words []string, queries [][]int) []int {
sum := make([]int, len(words)+1)
for i, w := range words {
sum[i+1] = sum[i]
if strings.Contains("aeiou", w[:1]) && strings.Contains("aeiou", w[len(w)-1:]) {
sum[i+1]++
}
}
ans := make([]int, len(queries))
for i, q := range queries {
ans[i] = sum[q[1]+1] - sum[q[0]]
}
return ans
}
这个题吧,之前做过,还是算流畅的写出来了这一题,wa了一发主要在于题目没有看清楚,按照题意写了之后还是正常运行的。这一题主要是学习一下go语言的前缀和写法吧,这里也是暴露出了很多学习漏洞。首先是go语言中没有char类型,这里需要借助type类型,就是将函数中的char改成type即可。然后就是将C++中的auto遍历二维数组的写法也是学到了,当时也是脑袋短路了吧怎么就没想到哈哈哈。第二段代码是灵神的写法,也是长见识了,它没有写isTrue函数,它好像是调用了strings.Contains("abc", w)
函数,通过调用也可以很容易知道这个函数的意思是啥,也就是w这个字符串是否包含"abc"中的一个,如果有,则返回true,否则返回false。这里它对w使用了切片,那么可以表明w字符串也能是其中的一个子串。个人对切片的理解和记忆方法:冒号就代表省略号,冒号在前那么就代表省略了前面的位置,数字就代表下标,如果是右边界它一定是开区间的,也就是遵循了左闭右开的原则。
2389.和优先的最长子序列(前缀和、二分查找)
cpp
class Solution {
public:
vector<int> answerQueries(vector<int>& nums, vector<int>& queries) {
sort(nums.begin(), nums.end());
for(int i = 1; i < nums.size(); i++) {
nums[i] = nums[i-1]+nums[i];
}
for(auto& q : queries) {
q = upper_bound(nums.begin(), nums.end(), q)-nums.begin();
}
return queries;
}
};
go
func answerQueries(nums []int, queries []int) []int {
//slices.Sort(nums)
sort.Ints(nums)
for i := 1; i < len(nums); i++ {
nums[i] = nums[i]+nums[i-1]
}
for i, q := range queries {
queries[i] = sort.SearchInts(nums, q+1)
}
return queries
}
这是一道二刷的简单题,我竟然没写出来!?不知道是写疲倦了还是脑子又短路了?问题就在于没有想到用sort排序一下数组来将子序列的问题转换成子数组的问题,这一题和下标没有关系,所以可以用排序将子序列长度问题转化为子数组长度问题
3152.特殊数组II(前缀和)
cpp
class Solution {
public:
vector<bool> isArraySpecial(vector<int>& nums, vector<vector<int>>& queries) {
vector<int> v(nums.size());
for(int i = 1; i < nums.size(); i++) {
if(nums[i]%2 != nums[i-1]%2) v[i] = v[i-1];
else v[i] = v[i-1]+1;
}
vector<bool> ans;
for(auto q : queries) {
ans.push_back(v[q[0]] == v[q[1]]);
}
return ans;
}
};
go
func isArraySpecial(nums []int, queries [][]int) []bool {
terminal := make([]int, len(nums))
for i := 1; i < len(nums); i++ {
terminal[i] = terminal[i-1]
if nums[i]%2 == nums[i-1]%2 {
terminal[i]++
}
}
ans := make([]bool, len(queries))
for i, q := range queries {
// if terminal[q[0]] == terminal[q[1]] {
// ans[i] = true
// } else {
// ans[i] = false
// }
ans[i] = (terminal[q[0]] == terminal[q[1]])
}
return ans
}
为什么二刷的题写不出!?这一题其实映像还是挺深刻的,当时周赛自己独立写出来的一题,结果现在忘记了怎么写!我怎么这么蠢。当时还是自己总结出来的方法,现在竟然写不出,当时自己总结的是区间染色法,其实也就类似于前缀和预处理。
2438.二的幂数组中查询范围内的乘积(位运算)
cpp
class Solution {
public:
vector<int> productQueries(int n, vector<vector<int>>& queries) {
const int mod = 1e9+7;
vector<int> power;
int lowbit;
while(n) {
lowbit = n&(-n);//-n也就是n^1
power.push_back(lowbit);
n ^= lowbit;//减去最低位
//n -= lowbit;
}
vector<int> ans;
for(auto q : queries) {
long long res = 1;
for(int i = q[0]; i <= q[1]; i++) {
res = (res*power[i])%mod;
}
ans.push_back(res);
}
return ans;
}
};
这一题又是学到了新东西了!首先是将一个数拆分成二的幂和的形式,这里是用power数组来保存,并且这个power数组是递增的,所以是从这个数的最低位开始取的。下面来介绍一下这个过程:首先是取出最末尾1,把这个数存入lowbit,也就是n&(-n)
,-n的由来是n^1+1
,具体是计组的知识,也就是负数是用补码来表示的,负数的补码的源码取反加一,所以也就对应的是-n,然后就是这里的res如果不用long long的话会报错,但是这里我觉得ans.push_back(res);
会报错,实际上没报错,也不知道为啥。
go
func productQueries(n int, queries [][]int) []int {
mod := int(1e9+7)
power := []int{}
for n > 0 {
lowbit := n&(-n)
power = append(power, lowbit)
n ^= lowbit
}
ans := make([]int, len(queries))
for i, q := range queries {
mul := 1
for _, x := range power[q[0] : q[1]+1] {
mul = mul*x%mod
}
ans[i] = mul
}
return ans
}
新知识!对数组元素的添加,也就类似于C++中的push_back,这里用的是power = append(power, lowbit)
,也就是当我定义了这个power数组的时候无法初始化它的长度,那么在添加元素的时候就用append函数。还有就是二层for循环内,遍历power函数的时候,由于已经知道了左右边界,所以for循环体内写的语句是for _, x := range power[q[0] : q[1]+1]
注意左闭右开,冒号左边是左边界,冒号右边是右边界
2974.最小数字游戏(排序、模拟)
go
func numberGame(nums []int) []int {
sort.Ints(nums)
for i := 0; i < len(nums); i+=2 {
nums[i], nums[i+1] = nums[i+1], nums[i]
}
return nums
}
go语言的新知识,交换的语法,其实就类似于初始化,直接一行解决了
53.最大子数组和(前缀和)
cpp
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int ans = INT_MIN, presum = 0, min_presum = 0;
for(int x : nums) {
presum += x;
ans = max(ans, presum-min_presum);
min_presum = min(min_presum, presum);
}
return ans;
}
};
go
func maxSubArray(nums []int) int {
ans, presum, min_presum := math.MinInt, 0, 0
for _, x := range nums {
presum += x
ans = max(ans, presum-min_presum)
min_presum = min(min_presum, presum)
}
return ans
}
这一题之前做过,当时是学动态规划的时候做的,那个时候还看不懂前缀和题解,现在至少还是看得懂前缀和解法了,其实转化成前缀和之后就和121买卖股票的最佳时机是一个意思了,只不过就是在枚举右维护左的同时多了一个前缀和预处理的操作,只能说发明灵神的人是个天才。写这一题的时候也解决了我一个前缀和遇到的问题,就是为什么一定要规定前缀和数组s[0]=0,就是如果计算的一个子数组恰好是一个从nums[0]开始的前缀和数组,如果我没有s[0]=0,那么我如何表示出左边界呢?这也就是为什么前缀和数组要开辟+1的空间,就是预留给s[0]=0的位置。由于这一题中结果不能是空数组,所以ans的操作一定要在min_presum之前,这也就是股票那一题不能在同一天买卖股票
的意思
3011.判断一个数组是否可以变为有序(位运算、分组循环)
cpp
class Solution {
public:
bool canSortArray(vector<int>& nums) {
int n = nums.size(), i = 0, premax = 0;
while(i < n) {
int mx = 0, ones = __builtin_popcount(nums[i]); //分组条件 设置位数目相同
while(i < n && __builtin_popcount(nums[i]) == ones) {
if(nums[i] < premax) {
return false;
}
mx = max(mx, nums[i++]); //维护当前组的最大值
}
premax = mx; //维护前一组的最大值
}
return true;
}
};
go
func canSortArray(nums []int) bool {
premax, i, n := 0, 0, len(nums)
for i < n {
ones := bits.OnesCount(uint(nums[i]))
mx := 0
for i < n && bits.OnesCount(uint(nums[i])) == ones {
if nums[i] < premax {
return false
}
mx = max(mx, nums[i])
i++
}
premax = mx
}
return true
}
其实除了用到了位运算、分组循环的做法,还用到了这一段时间刷的枚举右维护左的方法,看起来比较隐晦,实际上这是将时间复杂度降下来的一个思想,不然是做不到On的时间复杂度的。再来讲讲这一题学到的新知识:位运算函数求一个数(x)的二进制中有多少个1---C++用__builtin_popcount(x)
,go语言用bits.OnesCount(uint(x))
,另外一个新知识就是分组循环,下面是分组循环的模板
cpp
int i = 0, n = nums.size();
while(i < n) {
...
while(i < n && 分组条件) {
...
i++;
}
...
}
807.保持城市天际线(数组、矩阵)
cpp
class Solution {
public:
int maxIncreaseKeepingSkyline(vector<vector<int>>& grid) {
int n = grid.size(), sum = 0, ans = 0;
vector<int> max_i(n, 0), max_j(n, 0);
for(int i = 0; i < n; i++) {
for(int j = 0; j < n; j++) {
max_i[i] = max(max_i[i], grid[i][j]);
max_j[j] = max(max_j[j], grid[i][j]);
sum += grid[i][j];
}
}
for(int i = 0; i < n; i++) {
for(int j = 0; j < n; j++) {
ans += min(max_i[i], max_j[j]);
cout << min(max_i[i], max_j[j]) << " ";
}
}
return ans-sum;
}
};
go
func maxIncreaseKeepingSkyline(grid [][]int) int {
n := len(grid)
max_i, max_j := make([]int, n), make([]int, n)
ans := 0
for i := range grid {
for j := range grid[i] {
max_i[i] = max(max_i[i], grid[i][j])
max_j[j] = max(max_j[j], grid[i][j])
//ans += grid[i][j]
}
}
for i := range grid {
for j := range grid[i] {
ans += min(max_i[i], max_j[j])-grid[i][j]
}
}
return ans
}
顺利ac!我最开始还以为我这个方法是模拟法,我以为是最白痴的写法,没想到题解也是这样写的!方法也就是将
437.路径总和III(前缀和、哈希)
go
func pathSum(root *TreeNode, targetSum int) int {
hash := make(map[int]int)
hash[0] = 1
ans := 0
var dfs func(*TreeNode, int)
dfs = func(node *TreeNode, sum int) {
if node == nil {
return
}
sum += node.Val
ans += hash[sum-targetSum]
hash[sum]++
dfs(node.Left, sum)
dfs(node.Right, sum)
hash[sum]--
}
dfs(root, 0)
return ans
}
go语言的递归写法,首先需要var来定义一下dfs,然后就是lambda表达式的写法dfs = func(node *TreeNode, sum int)
需要注意的是这里的指针用法和结构体使用,*是写在类型前面的,然后所有的节点数据都是通过.来指出的
3216.交换后字典序最小的字符串(模拟)
go
func getSmallestString(s string) string {
t := []byte(s)
for i := 0; i < len(s)-1; i++ {
x, y := t[i], t[i+1]
if x > y && x%2 == y%2 {
t[i], t[i+1] = y, x
break
}
}
return string(t)
}
在go语言中,无法直接对字符串进行操作,这里的步骤是先将s转换成byte类型,中间的for循环就是解题的逻辑,最后返回的时候也需要将变量转换回string类型
3217.从链表中移除在数组中存在的节点(哈希表、链表)
cpp
class Solution {
public:
ListNode* modifiedList(vector<int>& nums, ListNode* head) {
unordered_set<int> s(nums.begin(), nums.end());
ListNode* dummy = new ListNode(0, head);
ListNode* pre = dummy, *cur = dummy->next;
while(cur != nullptr) {
if(s.count(cur->val)) {
pre->next = cur->next;
auto t = cur;
cur = cur->next;
delete t;
} else {
pre = cur;
cur = cur->next;
}
}
return dummy->next;
}
};
这是自己写的版本,没有什么特点,最开始用的是枚举nums,但是会超时,解决办法是将nums转化为set容器,这样查找nums就是常数的时间复杂度了。
cpp
class Solution {
public:
ListNode* modifiedList(vector<int>& nums, ListNode* head) {
unordered_set<int> s(nums.begin(), nums.end());
ListNode* dummy = new ListNode(0, head);
ListNode* cur = dummy;
while(cur->next) {
auto t = cur->next;
if(s.count(cur->next->val)) {
cur->next = cur->next->next;
delete t;
} else {
cur = cur->next;
}
}
return dummy->next;
}
};
这是灵神的版本,它这里只用到了cur数组,也就是我写的pre指针,他这个方法感觉也不错,我写的两个指针在释放内存方面就有点复杂了。
go
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func modifiedList(nums []int, head *ListNode) *ListNode {
has := make(map[int]bool, len(nums)) //预分配空间
for _, x := range nums {
has[x] = true
}
dummy := &ListNode{Next: head}//定义哨兵写法
cur := dummy//cur用于遍历
for cur.Next != nil {
if has[cur.Next.Val] {
cur.Next = cur.Next.Next
} else {
cur = cur.Next
}
}
return dummy.Next
}
go语言指针写法,没见过的写法还是太多了空用nil
表示,哨兵的开辟dummy := &ListNode{Next: head}
3218.切蛋糕的最小总开销(贪心)
cpp
class Solution {
public:
int minimumCost(int m, int n, vector<int>& horizontalCut, vector<int>& verticalCut) {
sort(horizontalCut.begin(), horizontalCut.end(), greater());
sort(verticalCut.begin(), verticalCut.end(), greater());
int cntH = 1, cntV = 1, i = 0, j = 0, ans = 0;
while(i < m-1 || j < n-1) {
if(j == n-1 || i < m-1 && horizontalCut[i] > verticalCut[j]) {
ans += horizontalCut[i++]*cntH;
cntV++;
} else {
ans += verticalCut[j++]*cntV;
cntH++;
}
}
return ans;
}
};
go
func minimumCost(m int, n int, horizontalCut []int, verticalCut []int) int64 {
slices.SortFunc(horizontalCut, func(a, b int) int {return b-a})
slices.SortFunc(verticalCut, func(a, b int) int {return b-a})
i, j, cntH, cntV, ans := 0, 0, 1, 1, 0
for i < m-1 || j < n-1 {
if j == n-1 || i < m-1 && horizontalCut[i] > verticalCut[j] {
ans += horizontalCut[i]*cntH
cntV++
i++
} else {
ans += verticalCut[j]*cntV
cntH++
j++
}
}
return int64(ans)
}
这一题在周赛中是两小问,其中第一问是能用dp暴力解决的,但是第二问增加数据范围之后只能用贪心来解决,这一题贪心的思路主要是要找到横切竖切的系数关系,当前如果是横切的话需要知道前面有多少次竖切,然后用ans += horizontalCut[i]*cntH
累加代价和。当然,这里还有一个最重要的贪心前提,就是当前横切和竖切哪个大就先切哪一个,这样才会使得ans的总开销是最小的,这在正常的惯性思路中是可以想到的,证明起来但很难说明,这可能也就是贪心题的难点所在吧。
go语言的语法知识:slices.SortFunc(horizontalCut, func(a, b int) int {return b-a})
这是一个排序操作,目的是将horizontalcut数组变为降序排列,而让其变成降序排列的语句就是func(a, b int) int {return b-a})
,这是一个匿名函数。
2588.统计美丽子数组数目(前缀和、位运算)
cpp
class Solution {
public:
long long beautifulSubarrays(vector<int>& nums) {
unordered_map<int, int> hash;//前缀异或和 --- 个数
hash[0] = 1;//异或初始化为1,任何数异或0就是它本身
long long ans = 0, presum = 0;
for(int x : nums) {
presum ^= x;
ans += hash[presum];
hash[presum]++;
}
return ans;
}
};
go
func beautifulSubarrays(nums []int) int64 {
hash := make(map[int]int)
presum, ans := 0, 0
hash[0] = 1
for _, x := range nums {
presum ^= x
ans += hash[presum]
hash[presum]++
}
return int64(ans)
}
第一次做到前缀和+位运算的运用,总的来说其实和加法前缀和很像,关键是要发掘出这一题要用到位运算,也就是说对位运算还不是很敏感的地步吧。对于题中给出的条件,都减去2^k,我们把所有数都拆成二进制来看的话就比较清晰了,以子数组[3,1,2]为例它们的异或和是为0的,那么就要用到异或运算,这也恰恰和题中变成一个全为0的数组
相对应。或者从另一方面来看,也是从灵神那里得到的启发,就是这个子数组各个元素转化为二进制的时候,每一位的二进制1的个数一定要是偶数,这样才能使其异或为0
525.连续数组(前缀和、哈希)---523
cpp
class Solution {
public:
int findMaxLength(vector<int>& nums) {
unordered_map<int, int> hash;
hash[0] = -1;
int presum = 0, ans = 0;
for(int i = 0; i < nums.size(); i++) {
nums[i] == 0 ? presum-- : presum++;
if(hash.count(presum)) ans = max(ans, i-hash[presum]);
else hash[presum] = i;
}
return ans;
}
};
go
func findMaxLength(nums []int) int {
presum, ans := 0, 0
hash := make(map[int]int)
hash[0] = -1
for i, x := range nums {
if x == 0 {
presum--
} else {
presum++
}
if _, has := hash[presum]; has {
ans = max(ans, i-hash[presum])
} else {
hash[presum] = i
}
}
return ans
}
这和之前做过的523很像,是同一个模板,而且都是求的最大子数组长度,当时在那一题讨论的else的问题在这里也是存在的,只有当前前缀和没有找到的时候才需要更新前缀和下标,而不是求一个前缀和就记录到哈希表中,主要是跟下标牵扯到了关系,以前做的那种子数组个数之类的就不需要考虑这种问题。再就是解决这个题的方法,遇到0,presum--,遇到1,presum++
3026.最大好子数组和(前缀和、哈希表)
cpp
class Solution {
public:
long long maximumSubarraySum(vector<int>& nums, int k) {
unordered_map<int, long long> hash; //nums[i] --- presum
long long ans = LLONG_MIN, sum = 0;
for(int x : nums) {
auto it = hash.find(x-k);
//找到了x-k的值
if(it != hash.end()) {
ans = max(ans, sum+x-it->second);
}
//找到了x-k的值
it = hash.find(x+k);
if(it != hash.end()) {
ans = max(ans, sum+x-it->second);
}
//找到了x的值
it = hash.find(x);
if(it == hash.end() || sum < it->second) {
hash[x] = sum;
}
sum += x;
}
return ans == LLONG_MIN ? 0 : ans;
}
};
go
func maximumSubarraySum(nums []int, k int) int64 {
ans := math.MinInt
hash := make(map[int]int)
sum := 0
for _, x := range nums {
s, ok := hash[x-k]
if ok {
ans = max(ans, sum+x-s)
}
s, ok = hash[x+k]
if ok {
ans = max(ans, sum+x-s)
}
s, ok = hash[x]
if !ok || sum < s {
hash[x] = sum
}
sum += x
}
if ans == math.MinInt {
return 0
}
return int64(ans)
}
难怪这一题这么高的分!这一题和之前做过的前缀和+哈希完全不同!看了题解真是妙阿,和以前做过的前缀和题目不同的是在于这一题并不是预处理前缀和,而是动态的维护前缀和,想想如果提前预处理了前缀和的话其实会出现问题,就是前缀和会出现多个相同的值,这样在查询的时候其实会出现问题的。那么现在来介绍这一题的独特之处:定义哈希表---nums[i]到以nums[i]为结尾(不包括nums[i])的最小前缀和,为什么要是最小前缀和?因为s[i]越小,那么最后求得的ans=s[j+1]-s[i]
是最大的。还有一点就是:题解是如何处理这个绝对值的---将绝对值去掉也就是nums[i]-nums[j]= +-k
然后分类讨论+k和-k。再然后就是维护这个前缀和sum,这一题还是很有讲究的,拿第一遍循环为例,它是在最末尾才加上x的,我大致总结了一个特点,要是在遍历前对哈希表初始化了的话就在循环开始累加presum,要是没有初始化就在循环末尾累加presum。
对于go语言:
go
//这行代码从 hash 哈希表中查找键为 x-k 的值,并将结果赋给变量 s。ok 是一个布尔值,指示是否成功找到该键。
s, ok := hash[x-k]
if ok {
ans = max(ans, sum+x-s)
}
1546.和为目标值且不重叠的非空子数组的最大数目(前缀和、哈希表)
cpp
class Solution {
public:
int maxNonOverlapping(vector<int>& nums, int target) {
unordered_map<int, int> hash; //前缀和到个数的映射
hash[0] = 1;
int sum = 0, ans = 0;
for(int x : nums) {
sum += x;
if(hash.count(sum-target)) {
ans++;
sum = 0;
hash.clear();
hash[0] = 1;
}
else hash[sum]++;
}
return ans;
}
};
go
func maxNonOverlapping(nums []int, target int) int {
hash := make(map[int]int)
ans, sum := 0, 0
hash[0] = 1
for _, x := range nums {
sum += x
if _, ok := hash[sum-target]; ok {
ans++
sum = 0
hash = make(map[int]int)
hash[0] = 1
} else {
hash[sum]++
}
}
return ans
}
就这个ac爽!说一下解题过程:先是自己思考步骤,但是自己有一点不是很确定,然后看了一眼提示,大致意思就是贪心每当遇到一个可以构成和为target的子数组时就将sum清零,哈希表清空
,这里唯一不确定的一点就是哈希表的清空操作hash.clear()
,go语言就是重新开辟一个哈希表hash = make(map[int]int)
1310.子数组异或查询(前缀和、位运算)
cpp
class Solution {
public:
vector<int> xorQueries(vector<int>& arr, vector<vector<int>>& queries) {
int n = arr.size();
vector<int> prefix(n+1);
for(int i = 1; i <= n; i++) {
prefix[i] = prefix[i-1]^arr[i-1];
}
vector<int> ans;
for(auto q : queries) {
ans.push_back(prefix[q[1]+1]^prefix[q[0]]);
}
return ans;
}
};
go
func xorQueries(arr []int, queries [][]int) []int {
ans, prefix := make([]int, len(queries)), make([]int, len(arr)+1)
//prefix[0] = 0
for i := range arr {
prefix[i+1] = prefix[i]^arr[i]
}
for i, q := range queries {
//ans[i] = prefix[q[1]+1]^prefix[q[0]]
ans[i] = prefix[q[0]]^prefix[q[1]+1]
}
return ans
}
前缀异或和,算是没有一点改变的一道题吧,求子区间异或和用ans.push_back(prefix[q[1]+1]^prefix[q[0]]);
也就是将中间的-变成了^
1124.表现良好的最长时间段(单调栈、前缀和)
cpp
class Solution {
public:
int longestWPI(vector<int>& hours) {
int n = hours.size(), ans = 0, presum[n+1];
stack<int> st;//左闭右开区间
presum[0] = 0;
st.push(presum[0]);
for(int j = 1; j <= n; j++) {
presum[j] = presum[j-1] + (hours[j-1] > 8 ? 1 : -1);
if(presum[j] < presum[st.top()]) st.push(j);
}
for(int i = n; i > 0; i--) {
while(!st.empty() && presum[i] > presum[st.top()]) {
ans = max(ans, i-st.top());
st.pop();
}
}
return ans;
}
};
go
func longestWPI(hours []int) int {
ans := 0
n := len(hours)
s := make([]int, n+1) //前缀和
st := []int{0} //s[0] = 0
for j, h := range hours {
j++
s[j] = s[j-1]
if h > 8 {
s[j]++
} else {
s[j]--
}
if s[j] < s[st[len(st)-1]] {
st = append(st, j) //感兴趣的j
}
}
for i := n; i > 0; i-- {
for len(st) > 0 && s[i] > s[st[len(st)-1]] {
ans = max(ans, i-st[len(st)-1]) //[栈顶, i)可能使最长子数组
st = st[:len(st)-1] //出栈操作
}
}
return ans
}
这个题确实有点难想到,首先是前缀和,它将良好和劳累分为了两个对立面,良好就+1,劳累就-1,那么就将问题转化成求最长子数组长度,使得这个数组的前缀和大于0即可,然后就是如何求这个长度,这里的方法就是用到了单调栈,也就是用单调栈来维护左边界。最后对于go语言,它是没有栈这个数据结构的,它使用数组来维护的,入栈用的是st = append(st, j)
,出栈用的是st = st[:len(st)-1]
,加强切片的理解
1685.有序数组中差绝对值之和(距离和)
cpp
class Solution {
public:
//24.7.23
vector<int> getSumAbsoluteDifferences(vector<int>& nums) {
int n = nums.size();
vector<int> presum(n+1), ans;
for(int i = 1; i <= n; i++) {
presum[i] = presum[i-1]+nums[i-1];
}
for(int i = 0; i < n; i++) {
int left = i*nums[i]-presum[i];
int right = presum[n]-presum[i]-(n-i)*nums[i];
ans.push_back(left+right);
}
return ans;
}
};
go
func getSumAbsoluteDifferences(nums []int) []int {
n := len(nums)
ans, presum := make([]int, n), make([]int, n+1)
for i := 1; i <= n; i++ {
presum[i] = presum[i-1]+nums[i-1]
}
for i := range nums {
left := i*nums[i]-presum[i]
right := presum[n]-presum[i+1]-(n-1-i)*nums[i]
ans[i] = left+right
}
return ans
}
二刷这一题了,竟然没有一点思路了,还是废了。关键在于前缀和的灵活运用。首先初始化前缀和presum,然后枚举nums,由于nums是有序的这一前提下,前缀和就能很好的引用了。当前nums左边的差绝对值之和就为int left = i*nums[i]-presum[i];
,右边的差绝对值之和就为right = presum[n]-presum[i+1]-(n-1-i)*nums[i];
,正是因为nums的有序,使得i*nums[i]
和presum
存在大小关系,这样就将前缀和应用到了求差绝对值之和了。这一题的关键也就是需要发现差绝对值之和能够这样转换,再就是需要注意的是前缀和的下标吧,这一块也经常出错。
2602.使数组元素全部相等的最少操作次数(距离和)
cpp
class Solution {
public:
vector<long long> minOperations(vector<int>& nums, vector<int>& queries) {
sort(nums.begin(), nums.end());
int n = nums.size();
vector<long long> presum(n+1);
for(int i = 1; i <= n; i++) presum[i] = presum[i-1]+nums[i-1];
vector<long long> ans;
for(auto q : queries) {
long long bound = ranges::lower_bound(nums, q)-nums.begin();
long long left = q*bound-presum[bound];
long long right = presum[n]-presum[bound]-(n-bound)*q;
ans.push_back(left+right);
}
return ans;
}
};
go
func minOperations(nums []int, queries []int) []int64 {
n := len(nums)
sort.Ints(nums)
presum, ans := make([]int, n+1), make([]int64, len(queries))
for i := 1; i <= n; i++ {
presum[i] = presum[i-1]+nums[i-1]
}
for i, q := range queries {
bound := sort.SearchInts(nums, q)
left := q*bound-presum[bound]
right := presum[n]-presum[bound]-(n-bound)*q
ans[i] = int64(left+right)
}
return ans
}
这也是二刷的题目了,好像对这种距离和这种题目有点感觉了,首先就是排序,一定是有序的序列,才能有left和right的恒等式,前一题为什么没有用到二分查找呢?因为前一题就是直接枚举下标的,这里是枚举值,需要二分查找来找到这个值是第几个数
2615.等值距离和(距离和)
cpp
class Solution {
public:
vector<long long> distance(vector<int>& nums) {
//24.7.23
int n = nums.size();
unordered_map<int, vector<int>> groups;//nums[i]---i的映射 有多个所以为数组
for(int i = 0;i < n; i++) groups[nums[i]].push_back(i);
vector<long long> ans(n), presum(n+1);
for(auto &[_, a] : groups) {
int m = a.size();//表示那一组vector的大小
for(int i = 0; i < m; i++) presum[i+1] = presum[i]+a[i];
for(int i = 0; i < m; i++) {
long long target = a[i];//那个值的下标
long long left = target*i-presum[i];
long long right = presum[m]-presum[i]-target*(m-i);
ans[target] = left+right;
}
}
return ans;
}
};
go
func distance(nums []int) []int64 {
groups := map[int][]int{}
for i, x := range nums {
groups[x] = append(groups[x], i) //相同元素分到一组,记录下标
}
ans := make([]int64, len(nums))
for _, a := range groups {
n := len(a)
s := make([]int, n+1)
for i, x := range a {
s[i+1] = s[i]+x //求前缀和
}
for i, target := range a {
left := target*i-s[i]
right := s[n]-s[i]-target*(n-i)
ans[target] = int64(left+right)
}
}
return ans
}
可以说是第一次做这种类型的题目了,首先就是哈希表的运用,它将nums数组中相同值放在同一个哈希表中,这个哈希表是以nums[i]---所有下标的映射,也就是unordered_map<int, vector<int>> groups;
,然后在前两题的基础上先要对哈希表进行遍历,这里遍历也就是分组遍历,在C++中遍历的方法是这样写的for(auto &[_, a] : groups)
,这里的a也就是每一组的数组(里面存储的是下标序列)
1177.构建回文串检测(前缀和、位运算)
优化前
cpp
class Solution {
public:
vector<bool> canMakePaliQueries(string s, vector<vector<int>>& queries) {
//24.7.24
int n = s.size(), q = queries.size();
vector<array<int, 26>> v(n+1);
//vector<vector<int>> v(n+1, vector<int>(26));
//初始化前缀和
for(int i = 1; i <= n; i++) {
v[i] = v[i-1];
v[i][s[i-1]-'a']++;
}
vector<bool> ans;
for(int i = 0; i < queries.size(); i++) {
auto qu = queries[i];
int left = qu[0], right = qu[1], k = qu[2], m = 0;
for(int j = 0; j < 26; j++) {
m += (v[right+1][j]-v[left][j])%2; //奇数加一,偶数不变
}
ans.push_back(m/2 <= k);
}
return ans;
}
};
go
func canMakePaliQueries(s string, queries [][]int) []bool {
presum := make([][26]int, len(s)+1)
for i, c := range s {
presum[i+1] = presum[i]
presum[i+1][c-'a']++
}
ans := make([]bool, len(queries))
for j, q := range queries {
left, right, k, m := q[0], q[1], q[2], 0
for j := 0; j < 26; j++ {
m += (presum[right+1][j]-presum[left][j])%2
}
ans[j] = m/2 <= k
}
return ans
}
涨见识了,这个回文串也是从未见过的解决方式,判断能否经过修改后子串能否变成回文串,看如下分类讨论:
对于C++中,内层的数组使用array要比vector更快,当然它们之间的初始化就有所不同了,需要注意一下,这里的有26个前缀和数组哦!有一点二维的意思了,下一层由上一层复制过来,再进行初始化
由于只需要考虑字母个数的奇偶性,所以这里可以用到异或运算来实现时间上的优化
cpp
class Solution {
public:
vector<bool> canMakePaliQueries(string s, vector<vector<int>>& queries) {
//24.7.24
int n = s.size(), q = queries.size();
vector<array<int, 26>> v(n+1);
//vector<vector<int>> v(n+1, vector<int>(26));
//初始化前缀和
for(int i = 0; i < n; i++) {
v[i+1] = v[i];
v[i+1][s[i]-'a'] ^= 1;
}
vector<bool> ans;
for(int i = 0; i < queries.size(); i++) {
auto qu = queries[i];
int left = qu[0], right = qu[1], k = qu[2], m = 0;
for(int j = 0; j < 26; j++) {
m += v[right+1][j]^v[left][j];
}
ans.push_back(m/2 <= k);
}
return ans;
}
};
除此之外,长为26的数组中只存储了0和1,那么就可以把它压缩到一个二进制数中,二进制数从低到高第s[i]-'a'
个比特存储着0(偶)和1(奇)的信息,优化后的代码如下
cpp
class Solution {
public:
vector<bool> canMakePaliQueries(string s, vector<vector<int>>& queries) {
//24.7.24
int n = s.size(), q = queries.size();
vector<int> v(n+1);
//vector<vector<int>> v(n+1, vector<int>(26));
//初始化前缀和
for(int i = 0; i < n; i++) {
int bit = 1 << (s[i]-'a');
v[i+1] = v[i]^bit;
}
vector<bool> ans;
for(int i = 0; i < queries.size(); i++) {
auto qu = queries[i];
int left = qu[0], right = qu[1], k = qu[2], m = 0;
m = __builtin_popcount(v[right+1]^v[left]);//每个子串的二进制中有多少个1,也就是多少个奇数字母
ans.push_back(m/2 <= k);
}
return ans;
}
};
go
func canMakePaliQueries(s string, queries [][]int) []bool {
presum := make([]int, len(s)+1)
for i, c := range s {
bit := 1 << (c-'a')
presum[i+1] = presum[i]^bit
}
ans := make([]bool, len(queries))
for j, q := range queries {
left, right, k, m := q[0], q[1], q[2], 0
m = bits.OnesCount(uint(presum[right+1]^presum[left]))
ans[j] = m/2 <= k
}
return ans
}
这也是终于看懂了这个bit的意思了int bit = 1 << (s[i]-'a');
,表示bit左移s[i]-'a'位,这样也就对应二进制位与字母的位置了
304.二维区域和检索-矩阵不可变(二维前缀和)
cpp
class NumMatrix {
public:
vector<vector<int>> presum;
NumMatrix(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
presum.resize(m+1, vector<int>(n+1));
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
presum[i+1][j+1] = presum[i+1][j]+presum[i][j+1]-presum[i][j]+matrix[i][j];
}
}
}
int sumRegion(int row1, int col1, int row2, int col2) {
return presum[row2+1][col2+1]-presum[row2+1][col1]-presum[row1][col2+1]+presum[row1][col1];
}
};
go
type NumMatrix [][]int
func Constructor(matrix [][]int) NumMatrix {
m, n := len(matrix), len(matrix[0])
sum := make([][]int, m+1)
sum[0] = make([]int, n+1)
for i, row := range matrix {
sum[i+1] = make([]int, n+1)
for j, x := range row {
sum[i+1][j+1] = sum[i+1][j] + sum[i][j+1] - sum[i][j] + x
}
}
return sum
}
// 返回左上角在 (r1,c1) 右下角在 (r2,c2) 的子矩阵元素和
func (s NumMatrix) SumRegion(r1, c1, r2, c2 int) int {
return s[r2+1][c2+1] - s[r2+1][c1] - s[r1][c2+1] + s[r1][c1]
}
刷惯了一维前缀和,现在开始二维前缀和!二维前缀和顾名思义题目所给的数组一定是二维的,当用到求和的时候就可以想到二维前缀和,我自己ac这一题的时候并不是灵神的写法,感觉我的想法更倾向于大众思维,就是一行一行的求前缀和,计算ans的时候也是枚举行累加每一行的前缀和最后返回答案。这样的时间复杂度肯定是线性的,看到灵神的题解之后发现是真的妙啊!它的写法完全就是通项公式,要想到真的很难!
1314.矩阵区域和(二维前缀和)
cpp
class Solution {
public:
// 1 2 3
// 4 5 6
// 7 8 9
int get(const vector<vector<int>>& pre, int m, int n, int x, int y) {
x = max(min(x, m), 0);
y = max(min(y, n), 0);
return pre[x][y];
}
vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) {
int m = mat.size(), n = mat[0].size();
vector<vector<int>> presum(m+110, vector<int>(n+110));
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
presum[i+1][j+1] = presum[i+1][j]+presum[i][j+1]-presum[i][j]+mat[i][j];
}
}
vector<vector<int>> ans(m, vector<int>(n));
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
ans[i][j] = get(presum, m, n, i + k + 1, j + k + 1) - get(presum, m, n, i - k, j + k + 1) - get(presum, m, n, i + k + 1, j - k) + get(presum, m, n, i - k, j - k);
}
}
return ans;
}
};
昨天的二维前缀和的步骤还是写对了哈哈哈,但是这一题呢其实就是多在了边界的判断上,因为这里没有明确规定k是小于n或m的,所以需要对各个边界进行判断,这里官解是给出了一个函数,用来限定矩阵的范围的,确实可以学习学习,先于右、下边界取最小值,再与左上边界取最大值,这样就将x和y分别限定在了[0,m]和[0,n]之间了
3070.元素和小于等于k的子矩阵的数目(二维前缀和、维护列)
二维前缀和
cpp
class Solution {
public:
int countSubmatrices(vector<vector<int>>& grid, int k) {
int m = grid.size(), n = grid[0].size(), ans = 0;
vector<vector<int>> presum(m+1, vector<int>(n+1));
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
presum[i+1][j+1] = presum[i+1][j]+presum[i][j+1]-presum[i][j]+grid[i][j];
ans += presum[i+1][j+1]-presum[0][j+1]-presum[i+1][0]+presum[0][0] <= k;
}
}
// int ans = 0;
// for(int i = 0; i < m; i++) {
// for(int j = 0; j < n; j++) {
// ans += presum[i+1][j+1]-presum[0][j+1]-presum[i+1][0]+presum[0][0] <= k;
// }
// }
return ans;
}
};
go
func countSubmatrices(grid [][]int, k int) int {
m, n, ans := len(grid), len(grid[0]), 0
presum := make([][]int, m+1)
presum[0] = make([]int, n+1)
for i := range grid {
presum[i+1] = make([]int, n+1)
for j := range grid[i] {
presum[i+1][j+1] = presum[i+1][j]+presum[i][j+1]-presum[i][j]+grid[i][j]
if presum[i+1][j+1] <= k {
ans++
}
}
}
return ans
}
C++中注释的代码是自己写的一部分,其实这一题完全就可以在初始化preusm的时候就将ans累加,这样可以节省一点时间,这一题比上一题要简单一些,每个子矩阵都确定了是从左上角开始的,这样就限定了子矩阵的范围,做起来还是比较顺手的,相当于是二维前缀和的模板题了。
维护每列的元素和
cpp
class Solution {
public:
int countSubmatrices(vector<vector<int>>& grid, int k) {
int ans = 0, n = grid[0].size();
vector<int> colsum(n);
for(auto g : grid) {
int s = 0;
for(int i = 0; i < n; i++) {
colsum[i] += g[i];
s += colsum[i];
if(s > k) break;
ans++;
}
}
return ans;
}
};
go
func countSubmatrices(grid [][]int, k int) int {
ans, n := 0, len(grid[0])
colsum := make([]int, n)
for _, g := range grid {
s := 0
for i, x := range g {
colsum[i] += x
s += colsum[i]
if s > k {
break
}
ans++
}
}
return ans
}
也是一个很好的方法!它是将每一列的前缀和维护起来了,这个s是一列一列的累加的,这个s的初始化也是很有讲究的,它一定要是在一层循环后初始化,每一行的开始s是0,然后一列一列的累加,当s > k的时候就直接break掉因为这个矩阵的元素规定就是大于0的,所以colsum的和一定是大于0的,所以可以放心break
1738.找出第k大的异或坐标值(二维前缀和)
cpp
class Solution {
public:
int kthLargestValue(vector<vector<int>>& matrix, int k) {
int m = matrix.size(), n = matrix[0].size();
vector<int> colsum(n), t;
for(auto ma : matrix) {
int s = 0;
for(int i = 0; i < n; i++) {
colsum[i] ^= ma[i];
s ^= colsum[i];
t.push_back(s);
}
}
sort(t.begin(), t.end(), greater());
//for(int a : t) cout << a << " ";
return t[k-1];
}
};
go
func kthLargestValue(matrix [][]int, k int) int {
n := len(matrix[0])
presum, ans := make([]int, n), []int{}
for _, row := range matrix {
s := 0
for i, x := range row {
presum[i] ^= x
s ^= presum[i]
ans = append(ans, s)
}
}
sort.Ints(ans)
return ans[len(ans)-k]
}
这一题和上一题的方法二一样,它是维护每一列的前缀和来实现的,在写go的时候出了一点错误,也就是go没有将数组排序成降序的函数,sort.Ints(ans)
只能将ans升序排序。
17.05.字母与数字(前缀和、哈希表)
cpp
class Solution {
public:
vector<string> findLongestSubarray(vector<string>& array) {
//24.7.25
int n = array.size();
vector<int> presum(n+1);
for(int i = 0; i < n; i++) {
presum[i+1] = presum[i]+(array[i][0] >> 6 & 1)*2-1; //数字+1 英文-1
}
int start = 0, end = 0; //符合要求的数组[start, end)左闭右开
unordered_map<int, int> first; //s[i] --- i
for(int i = 0; i <= n; i++) {
auto it = first.find(presum[i]);
//第一次出现
if(it == first.end()) {
first[presum[i]] = i;
} else if(i-it->second > end-start) { //第二次出现且长度大于之前就更新
start = it->second;
end = i;
}
}
return {array.begin()+start, array.begin()+end};
}
};
go
func findLongestSubarray(array []string) []string {
n := len(array)
presum := make([]int, n+1)
for i, val := range array {
presum[i+1] = presum[i]+int(val[0])>>6&1*2 - 1
}
start, end := 0, 0
first := make(map[int]int)
for i, val := range presum {
if x, ok := first[val]; !ok {
first[val] = i
} else if i-x > end-start {
start, end = x, i
}
}
return array[start:end]
}
搞定一道面试题!虽然是看完题解之后写出来的,但是感觉又学到了新东西就很爽!首先是前缀和的使用,遇到数字就+1,遇到字母就-1,然后问题转化成找前缀和为0的子数组,它的最大长度是多少。关键是转化问题这一点要想到!前面已经刷过类似的题了,多的就不说了,然后就是对返回该子数组,若存在多个最长子数组,返回左端点下标值最小的子数组。若不存在这样的数组,返回一个空数组。
的转化,很巧妙!它就是用哈希表first来实现的那么如何返回左端点最小的子数组呢?当时就是被这个给吓到了,感觉会很麻烦,其实就是一句else if(i-it->second > end-start)
就能解决的事!也就是当长度等于当前长度的话那么就不更新start和end,我当时怎么就想不出来?当且大于当前长度才能更新,这样就符合题目意思了
2740.找出分区值(排序、贪心)
cpp
class Solution {
public:
int findValueOfPartition(vector<int>& nums) {
sort(nums.begin(), nums.end());
int ans = INT_MAX;
for(int i = 1; i < nums.size(); i++) {
ans = min(ans, abs(nums[i-1]-nums[i]));
}
return ans;
}
};
go
func findValueOfPartition(nums []int) int {
ans := math.MaxInt
sort.Ints(nums)
for i := 1; i < len(nums); i++ {
ans = min(abs(nums[i-1]-nums[i]), ans)
}
return ans
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
这个题我怎么感觉可以划分成简单题呢?这只要想到了排序,然后枚举数组维护相邻差绝对值的最小值就行了
2488.统计中位数为k的子数组(前缀和、哈希表、等价转换)
cpp
class Solution {
public:
int countSubarrays(vector<int>& nums, int k) {
//奇数情况: 左大+右大 = 左小+右小 <==> 左小-左大 = 右大-右小 分别为 1 -1 1 -1
int pos = find(nums.begin(), nums.end(), k)-nums.begin();//返回k的下标位置
unordered_map<int, int> cnt; //与k大小关系---个数的映射
cnt[0] = 1; //起始x=0,所以cnt[0] = 1
for(int i = pos-1, x = 0; i >= 0; i--) {
x += nums[i] < k ? 1 : -1;
cnt[x]++;
}
int ans = cnt[0]+cnt[-1];
cout << ans;
for(int i = pos+1, x = 0; i < nums.size(); i++) {
x += nums[i] > k ? 1 : -1;
ans += cnt[x]+cnt[x-1];
}
return ans;
}
};
go
func countSubarrays(nums []int, k int) int {
pos := 0
for i, x := range nums {
if x == k {
pos = i
break
}
}
// fmt.Print(pos)
// return 0
cnt, x := make(map[int]int), 0
cnt[0] = 1
for i := pos-1; i >= 0; i-- {
if nums[i] < k {
x++
} else {
x--
}
cnt[x]++
}
ans, x := cnt[0]+cnt[-1], 0
for _, v := range nums[pos+1:] {
if v > k {
x++
} else {
x--
}
ans += cnt[x]+cnt[x-1]
}
return ans
}
这是一道子数组统计问题的套路题,主要方法就是等价转换成前缀和+哈希表的模型,相当于是将k作为一个分界线(分为大于k的数和小于k的数),与以往的题目不同的是遍历方式,以前都是从头往后初始化哈希表,而这一题需要从k的下标分别往左和往右遍历,题解是往左初始化哈希表往右计算ans,这一题关键就是要想到等价转化和遍历顺序吧,分奇偶求ans也是很妙的一点ans += cnt[x]+cnt[x-1]
按照题目的意思话,如果nums是偶数个数,那么等式关系就变成了左小-左大-1 = 右大-右小
1854.人口最多的年份(差分数组)
cpp
class Solution {
public:
int maximumPopulation(vector<vector<int>>& logs) {
//24.7.26
vector<int> diff(103);
for(auto l : logs) {
diff[l[0]-1949]++;
diff[l[1]-1949]--;
}
int m = 0, sum = 0, ans = 0;
for(int i = 0; i < diff.size(); i++) {
sum += diff[i];
if(m < sum) {
m = sum;
ans = i+1949;
}
}
return ans;
}
};
go
func maximumPopulation(logs [][]int) int {
diff := make([]int, 103)
for _, l := range logs {
diff[l[0]-1949]++
diff[l[1]-1949]--
}
sum, m, ans := 0, 0, 0
for i, s := range diff {
sum += s
if m < sum {
m = sum
ans = i+1949
}
}
return ans
}
二刷!回顾一下差分数组,同前缀和数组一样,定义长度要至少+1,下面给出通项公式,这样便于理解
针对于这一题,首先是要将范围转化到差分数组熟悉的形式,也就是左边界变为1,操作时将左右边界同时减去1949,再就是最近做过的维护最小下标的方法,就是对应这里的if(m < sum)
,有待加强有待加强!
3106.满足距离约束且字典序最小的字符串(贪心)
cpp
class Solution {
public:
string getSmallestString(string s, int k) {
for(char& c : s) { //一定要是引用
int dis = min('z'-c+1, c-'a');
if(dis > k) {
c -= k;
break;
}
c = 'a';
k -= dis;
}
return s;
}
};
go
func getSmallestString(s string, k int) string {
ss := []byte(s)
for i, c := range ss {
dis := int(min(c-'a', 'z'-c+1)) //一定要转化成int
if dis > k {
ss[i] -= byte(k) //一定要转成byte
break
}
k -= dis
ss[i] = 'a'
}
return string(ss)
}
贪心思路:优先将前面的字母变为a,主要是代码要看的简洁,自己写的就是一坨狗屎,果然贪心无模板说的还是很对的,有的贪心想到容易实现起来可能有点困难。再就是go语言的写法有点陌生,想到了string不能直接修改要用byte
1590.使数组和能被p整除(前缀和、哈希表)
cpp
class Solution {
public:
int minSubarray(vector<int>& nums, int p) {
int n = nums.size();
vector<int> presum(n+1);
for(int i = 0; i < n; i++) {
presum[i+1] = (presum[i]+nums[i])%p;
}
int x = presum[n], ans = n;
if(x%p == 0) return 0;
unordered_map<int, int> hash;
for(int i = 0; i <= n; i++) {
hash[presum[i]] = i;
auto it = hash.find(((presum[i]-x)%p+p)%p);
if(it != hash.end()) {
ans = min(ans, i-it->second);
}
}
return ans < n ? ans : -1;
}
};
go
func minSubarray(nums []int, p int) int {
n := len(nums)
presum := make([]int, n+1)
for i, v := range nums {
presum[i+1] = (presum[i]+v)%p
}
x := presum[n]
if x%p == 0 {
return 0
}
hash := make(map[int]int)
ans := n
for i, v := range presum {
hash[v] = i
if h, ok := hash[((v-x)%p+p)%p]; ok {
ans = min(ans, i-h)
}
}
if ans < n {
return ans
} else {
return -1
}
}
这一题吧,和之前做过的有一题有点像,但是呢细细一想还是有一点区别的,这一题实现起来确实要难一点,这一题也是将取模运算应用的很多了,涉及到数组之间取模的关系和模运算的交换律
这一题的前缀和使用的也很新颖,这里的presum数组保存的是前缀和的模,嘶确实没用过,长见识了长见识了
56.合并区间(排序、差分、合并区间模板)---3169
cpp
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
ranges::sort(intervals); //按照左端点从小到大排序
vector<vector<int>> ans;
for(auto i : intervals) {
if(!ans.empty() && ans.back()[1] >= i[0]) {
ans.back()[1] = max(ans.back()[1], i[1]);
} else {
ans.push_back(i);
}
}
return ans;
}
};
go
func merge(intervals [][]int) [][]int {
slices.SortFunc(intervals, func(p, q []int) int {return p[0]-q[0]}) //按照左端点从小到大排序
ans := [][]int{}
for _, p := range intervals {
m := len(ans)
if m > 0 && p[0] <= ans[m-1][1] {
ans[m-1][1] = max(ans[m-1][1], p[1])
} else {
ans = append(ans, p)
}
}
return ans
}
第一次刷的时候是因为一次周赛出现的题目,当时因为数据范围大,使用差分数组的时候内存超限了,其实合并区间的思路还是很容易想到的,主要是实现起来比较复杂,要熟悉对二维数组的操作。
57.插入区间(模拟)
cpp
class Solution {
public:
vector<vector<int>> insert(vector<vector<int>>& intervals, vector<int>& newInterval) {
int index = 0, n = intervals.size();
vector<vector<int>> ans;
while(index < n && intervals[index][1] < newInterval[0]) {
ans.push_back(intervals[index++]);
}
while(index < n && intervals[index][0] <= newInterval[1]) {
newInterval[0] = min(newInterval[0], intervals[index][0]);
newInterval[1] = max(newInterval[1], intervals[index][1]);
index++;
}
ans.push_back(newInterval);
while(index < n) {
ans.push_back(intervals[index++]);
}
return ans;
}
};
go
func insert(intervals [][]int, newInterval []int) [][]int {
index, n := 0, len(intervals)
ans := [][]int{}
for index < n && intervals[index][1] < newInterval[0] {
ans = append(ans, intervals[index])
index++
}
for index < n && intervals[index][0] <= newInterval[1] {
newInterval[0] = min(newInterval[0], intervals[index][0])
newInterval[1] = max(newInterval[1], intervals[index][1])
index++
}
ans = append(ans, newInterval)
for index < n {
ans = append(ans, intervals[index])
index++
}
return ans
}
这一题题解吧,更倾向于模拟题,其实思路也是想到的的,就是不知道怎么实现出来,也是在题解区看到了自己想要的答案,卡尔不愧是卡尔,题解还是很亲民的。
732.我的日程安排表III(差分)
cpp
class MyCalendarThree {
public:
MyCalendarThree() {
}
int book(int startTime, int endTime) {
hash[startTime]++;
hash[endTime]--;
int ans = 0, maxbook = 0;
for(auto& [_, val] : hash) {
maxbook += val;
ans = max(ans, maxbook);
cout << val << " ";
}
cout << endl;
return ans;
}
map<int, int> hash;
};
这算是一道和之前完全不一样的差分!以前都是用数组写的差分,这一题用的是哈希表来写的差分!而且这个哈希表用的还是map!而不是unordered_map,也是第一次写题用到map,它们两者的区别就是是否重复、是否有序,而这一题呢,的确要讲究有序,这样才能算出正确的区间的最大并发预定值
682.棒球比赛(模拟)
cpp
class Solution {
public:
int calPoints(vector<string>& operations) {
vector<int> ans;
for(string s : operations) {
if(s == "C") ans.pop_back();
else if(s == "D") ans.push_back(ans.back()*2);
else if(s == "+") ans.push_back(ans.back()+ans[ans.size()-2]);
else ans.push_back(stoi(s));
}
return reduce(ans.begin(), ans.end(), 0);
}
};
go
func calPoints(operations []string) int {
ans := []int{}
for _, op := range operations {
n := len(ans)
if op == "+" {
ans = append(ans, ans[n-1]+ans[n-2])
} else if op == "D" {
ans = append(ans, 2*ans[n-1])
} else if op == "C" {
ans = ans[:n-1]
} else {
num, _ := strconv.Atoi(op)//字符串转整型
ans = append(ans, num)
}
}
sum := 0
for _, x := range ans {
sum += x
}
return sum
}
简单的模拟题,主要是学习学习go的写法,字符串转整型、切片的使用
2406.将区间分为最少组数(差分、优先队列、贪心)
差分法
cpp
class Solution {
public:
int minGroups(vector<vector<int>>& intervals) {
map<int, int> diff;
for(auto i : intervals) {
diff[i[0]]++;
diff[i[1]+1]--;
}
int ans = 0, sum = 0;
for(auto& [_, val] : diff) {
ans = max(ans, sum+=val);
}
return ans;
}
};
优先队列---小根堆
cpp
class Solution {
public:
int minGroups(vector<vector<int>>& intervals) {
ranges::sort(intervals);
priority_queue<int, vector<int>, greater<>> pq;
for(auto p : intervals) {
if(!pq.empty() && pq.top() < p[0]) pq.pop();
pq.push(p[1]);
}
return pq.size();
}
};
差分法转化问题的解法其实和732是一样的,就是区间的右边界不同其它都是一样的。然后就是小根堆的使用,小根堆的声明priority_queue<int, vector<int>, greater<>> pq;
,不加greater<>
默认是大根堆,小根堆的特点是每个节点的值要比孩子节点的值要小,这一题就是用小根堆来实现分组
的一个算法
2961.双模幂运算(快速幂、模运算)
cpp
class Solution {
public:
int power(int x, int n, int mod) {
int ans = 1;
while(n) {
if(n&1) {
ans = ans*x%mod;
}
x = x*x%mod;
n >>= 1;
}
return ans;
}
vector<int> getGoodIndices(vector<vector<int>>& variables, int target) {
vector<int> ans;
for(int i = 0; i < variables.size(); i++) {
auto v = variables[i];
if(power(power(v[0], v[1], 10), v[2], v[3]) == target) {
ans.push_back(i);
}
}
return ans;
}
};
go
func getGoodIndices(variables [][]int, target int) []int {
ans := []int{}
for i, v := range variables {
if power(power(v[0], v[1], 10), v[2], v[3]) == target {
ans = append(ans, i)
}
}
return ans
}
func power(x, n, mod int) int {
ans := 1
for n > 0 {
if n&1 == 1 {
ans = ans*x%mod
}
x = x*x%mod
n >>= 1
}
return ans
}
这一题主要用到的算法是快速幂,之前学快速幂咋看咋都看不懂,现在看感觉好简单。。。然后就是模运算,在快速幂的过程中加上模运算,这样得到的值是不会变的。
50.Pow(x,n)(快速幂)
cpp
class Solution {
public:
double myPow(double x, int n) {
//24.7.30
double ans = 1;
long long n1 = n;
if(n1 < 0) {
x = 1/x;
n1 = -n1;
}
while(n1) {
if(n1&1) {
ans *= x;
}
x *= x;
n1 >>= 1;
}
return ans;
}
};
go
func myPow(x float64, n int) float64 {
ans := 1.0
if n < 0 {
n = -n
x = 1/x
}
for n > 0 {
if n&1 == 1 {
ans *= x
}
x *= x
n >>= 1
}
return ans
}
这里主要是考虑到n是负数的情况,需要讨论一下,这一点很重要。当n为负数时,将n变为其相反数,然后将底数x变成1/x,n变为相反数的时候会出现int溢出的情况,所以n的类型要是long long
2381.字母移位II(差分)
cpp
class Solution {
public:
string shiftingLetters(string s, vector<vector<int>>& shifts) {
int n = s.size();
vector<int> diff(n+1);
for(auto s : shifts) {
if(s[2] == 1) diff[s[0]]++, diff[s[1]+1]--;
else diff[s[0]]--, diff[s[1]+1]++;
}
string ans;
for(int i = 0, sum = 0; i < n; i++) {
sum += diff[i];
int t = s[i]-'a';
t = ((t+sum)%26+26)%26;
ans.push_back('a'+t);
}
return ans;
}
};
go
func shiftingLetters(s string, shifts [][]int) string {
n := len(s)
diff := make([]int, n+1)
for _, s := range shifts {
if(s[2] == 1) {
diff[s[0]]++
diff[s[1]+1]--
} else {
diff[s[0]]--
diff[s[1]+1]++
}
}
t, shift := []byte(s), 0
for i, c := range t {
shift = (shift+diff[i])%26+26
t[i] = (c-'a'+byte(shift))%26+'a'
}
return string(t)
}
查漏补缺了一下,原来差分数组可以从1开始啊,我还以为只能从1开始呢,自己写的时候还给搞麻烦了。这一题确实挺容易看出来是差分数组的,重要的是怎么将这个字符串正确的变换过来,前天牛客做过的也是类似的题,只不过是求的操作最小次数:min(op, 26-op)
,就类似于将这个字母表变成一个环形的,a向前移位就变成了z,z向后移位变成了a这样子的。而这里呢就是求的字符变换结果,变换方式也就是先将这个字符相对于a的位置求出来t
,最终的偏移量就是t = ((t+sum)%26+26)%26;
就类似于实数求余,考虑正数和负数的情况。
1441.用栈操作构建数组(模拟)
cpp
class Solution {
public:
vector<string> buildArray(vector<int>& target, int n) {
vector<string> ans;
int s = target.size();
for(int i = 1; i <= n && s > 0; i++) {
auto flag = find(target.begin(), target.end(), i);
if(flag != target.end()) ans.push_back("Push"), s--;
else ans.push_back("Push"), ans.push_back("Pop");
}
return ans;
}
};
说是栈的题单,咋看不出来呢?反正很简单,就这么过去了吧。。。
3111.覆盖所有点的最少矩形数目(贪心、排序)
cpp
class Solution {
public:
int minRectanglesToCoverPoints(vector<vector<int>>& points, int w) {
ranges::sort(points);
int ans = 0, maxx = -1;
for(auto p : points) {
if(p[0] > maxx) {
ans++;
maxx = p[0]+w;
}
}
return ans;
}
};
go
func minRectanglesToCoverPoints(points [][]int, w int) int {
slices.SortFunc(points, func(p, q[]int) int {return p[0]-q[0]})
maxx, ans := -1, 0
for _, p := range points {
if p[0] > maxx {
ans++
maxx = p[0]+w
}
}
return ans
}
关键在于转化问题,这一题其实和y坐标没有任何关系,只需要看x坐标的范围就行了,然后排序、贪心解决这一题。
3264.k次乘运算后的最终数组I(模拟、堆)
cpp
class Solution {
public:
vector<int> getFinalState(vector<int>& nums, int k, int multiplier) {
while(k--) {
int mi = 0;
for(int i = 1; i < nums.size(); i++) {
if(nums[i] < nums[mi]) mi = i;
}
nums[mi] *= multiplier;
}
return nums;
}
};
go
func getFinalState(nums []int, k int, multiplier int) []int {
for ; k > 0; k-- {
mi := 0
for i := 1; i < len(nums); i++ {
if nums[i] < nums[mi] {
mi = i
}
}
nums[mi] *= multiplier
}
return nums
}
这一题在堆的题单中,但是用堆来解决这道题好像挺复杂的,主要是要把堆又重新变回数组,这个操作相当麻烦,这竟然是上一次周赛的题,看了一下感觉这周赛的难度越来越大了,我学习的速率好像完全赶不上它难度增加的速率啊,由于这一题数据范围比较小,所以还是简单模拟过了。
7月刷题小结
这一个月开始跟着灵神的题单(常用的数据结构题单)开始刷题了,这期间也包括每日一题和某些每日一题的扩展题(也可以说是相似的题)、周赛双周赛、牛客周赛。牛客周赛打的比较少,主要是时间上冲突了。
这一个月刷题效率个人感觉不算慢的了,应该日均有2-3道新题,已解决的题目总数也是达到了297,马上突破300大关了,这期间吧,主要是学习和张见识为主,主要是了解题型和总结模板、巩固C++语法、学习go语言语法和各种库函数的使用规则。对于go语言也基本掌握了几个库函数的使用:排序、二分查找、最大最小值,熟悉使用哈希表、数组、栈等数据结构。
对于练习赛:上一次的周赛双周赛上了50分!在做题的时候感觉也很深刻,主要是能够将题目转化成自己熟悉的题型,这样做起来会很顺手,还有就是要减少自己wa的次数,记得那次ac了3题,但是罚时了20分钟,不然那一次能加更多的分的!
知识点和算法技巧总结:枚举右、维护左,这一思想通常搭配着哈希表使用,这种题掌握的还算熟练了,一般出现在简单、中等题型中。在中等题中这个方法就是用来降低时间复杂度的,通常能将平方复杂度降为线性的复杂度。然后就是对哈希表的使用,我觉得对哈希表的定义很重要,这个月出现过很多次不同的哈希定义:值到下标的映射、值到个数的映射、前缀和%k到...的映射。这种哈希的定义取决于题目的意思,所以哈希的定义要灵活的根据题目意思来定义。下一步就是初始化,在有的题解中初始化和遍历放在了一起,这样的话对哈希表的操作要在循环体的最前面,如果是先初始化之后再遍历,那么对哈希表的操作放在循环体的末尾,这也算是自己发现的一个小结论吧!对于这个方法,经典的题目有121.买卖股票、2874.有序三元组最大值II,这种类型的题在循环题内维护左的过程中某些步骤是不能更换顺序的!这一点很重要!
最大数位、数位和
cpp
//最大数位
//写法一:
int getMax(int x) {
int ans = 0;
while(x) {
ans = max(ans, x%10);
x /= 10;
}
return ans;
}
//写法二:
int x = 0;
for(int v = nums[i]; v > 0; v /= 10) {
x = max(x, v%10);
}
//数位和
int x = nums[i], digit = 0;
while(x > 0) {
digit += x%10;
x /= 10;
}
质数判断
cpp
bool isPrime(int x) {
if(x == 1) return false;
for(int i = 2; i < x; i++) {
if(x%i == 0) return false;
}
return true;
}
列先遍历数组
有时候在某些简单、中等题中先遍历数组的列,再遍历行会使代码简洁很多,正常都是先遍历行,这样写习惯了之后突然让你从列遍历可能会比较生疏,这一写法具体可以看3033题解。
子数组计数问题
个人认为这种类型的题是用双指针来解的,以3101为例,题解是利用左右边界l和r来确定子数组个数的,它们之间存在数量上的一个关系,这在之前的周赛也遇到过。假设nums=[1,0,1,0],l=0,r=2,那么r-l+1=3,这3个符合条件的子数组分别是以nums[r]为结尾的3个子数组,分别是[1],[0,1]和[1,0,1],由于这一题中每个长为1的子数组都符合交替子数组,所以在遍历的时候就没有把+1算进去,在最后返回的时候加上了nums.size(),在遍历的过程中无需考虑会出现子数组重复的情况,这体现了这个等式的重要性。
前缀和
前缀和分为两种:动态维护前缀和、预处理前缀和。两种要因题目而异,关键是要发掘这个题是要用前缀和来处理,比如:题干中字眼出现"和"的字眼、求某个区间符合...的个数等等,都是能用前缀和来求的。前缀和的下标一定要是从1开始,而差分不需要
维护后缀最大值
cpp
vector<int> surfmax(n+1, 0);
for(int k = n-1; k > 1; k--) {
surfmax[k] = max(surfmax[k+1], nums[k]);
}
数位反转
cpp
auto rev = [](int x) {
int res = 0;
while(x > 0) {
res = res*10+x%10;
x /= 10;
}
return res;
};
子序列和子数组长度的转化
这要在一定的条件下才能够转化,转化后题目就会变得简单很多。以2389为例,将数组排序之后求最长子序列长度就会变成最长子数组的长度了
将n转化成二进制和的形式存在数组中
cpp
vector<int> power;
int lowbit;
while(n) {
lowbit = n&(-n);//-n也就是n^1
power.push_back(lowbit);
n ^= lowbit;//减去最低位
//n -= lowbit;
}
分组循环---例题3011
cpp
int i = 0, n = nums.size();
while(i < n) {
...
while(i < n && 分组条件) {
...
i++;
}
...
}
快速幂
cpp
while(n1) {
if(n1&1) {
ans *= x;
}
x *= x;
n1 >>= 1;
}
补充一下位运算吧:任何数和0异或值为其本身,任何数和1异或值为其相反数
更多算法:合并区间、差分、二维前缀和