本专栏开启,目的在于帮助大家更好的掌握学习Java
,特别是一些Java学习者
难以在网上找到系统地算法学习资料帮助自身入门算法,同时对于专栏内的内容有任何疑问都可在文章末尾添加我的微信给你进行一对一的讲解。
但最最主要的还是需要独立思考,对于本专栏的所有内容,能够进行完全掌握,自己完完全全将代码写过一遍,对于算法入门肯定是没有问题的。
算法的学习肯定不能缺少总结,这里我推荐大家可以到高校算法社区将学过的知识进行打卡,以此来进行巩固以及复习。
学好算法的唯一途径那一定是题海战略,大量练习的堆积才能练就一身本领。专栏的任何题目我将会从【题目描述】【解题思路】【模板代码】【代码解析】等四板块进行讲解。
专栏上章讲了前缀和数组的应用,它能帮助我们在 O ( 1 ) O(1) O(1) 的复杂度求出任意子数组的和。而前缀异或则能够帮我们在 O ( 1 ) O(1) O(1) 的复杂度求出任意子数组的异或值,这个需求场景在很多地方都很常见,我们来推导一下原理:
定义 s [ i ] s[i] s[i] 为 a a a 数组前 i i i 个元素的异或值,也就是说:
s [ i ] = a [ 1 ] ∧ a [ 2 ] ∧ a [ 3 ] . . . . ∧ a [ i ] s[i]=a[1] \land a[2] \land a[3]....\land a[i] s[i]=a[1]∧a[2]∧a[3]....∧a[i]
如果想求出区间 [ l , r ] [l,r] [l,r] 的异或值,我们只需要 s [ r ] ∧ s [ l − 1 ] s[r] \land s[l-1] s[r]∧s[l−1]即可,因为专栏前面也讲过异或性质——x^x=0
和x^0=x
。这样显然区间 [ 1 , l − 1 ] [1,l-1] [1,l−1]的元素出现了两次,达到了抵消的效果。即可得:
s [ r ] ∧ s [ l − 1 ] = a [ l ] ∧ a [ l + 1 ] ∧ a [ l + 2 ] . . . ∧ a [ r ] s[r] \land s[l-1]=a[l] \land a[l+1] \land a[l+2] ...\land a[r] s[r]∧s[l−1]=a[l]∧a[l+1]∧a[l+2]...∧a[r]
有一个正整数数组 arr
,现给你一个对应的查询数组 queries
,其中 queries[i] = [Li, Ri]
。
对于每个查询 i
,请你计算从 Li 到 Ri
的 XOR 值(即 arr[Li] xor arr[Li+1] xor ... xor arr[Ri]
)作为本次查询的结果。
并返回一个包含给定查询 queries
所有结果的数组。
异或前缀的模板题,我们直接得到前缀异或数组来处理查询。
class Solution {
public int[] xorQueries(int[] a, int[][] q) {
int n = a.length;
int[] s = new int[n + 1];
for (int i = 0; i < n; i++) {
s[i + 1] = s[i] ^ a[i];
}
int m = q.length;
int[] w = new int[m];
for (int i = 0; i < m; i++) {
int l = q[i][0], r = q[i][1];
w[i]=s[l]^s[r+1];
}
return w;
}
}
注意到题目给定的数组和查询都是下标从0
开始的,同样为了避免边界问题,我们前缀异或数组的下标也从1
开始。时间复杂度 O ( n ) O(n) O(n)。
给你一个下标从 0 开始的整数数组nums
。每次操作中,你可以:
选择两个满足 0 <= i, j < nums.length
的不同下标 i
和 j
。
选择一个非负整数 k
,满足 nums[i]
和 nums[j]
在二进制下的第 k
位(下标编号从 0 开始)是 1
。
将 nums[i]
和 nums[j]
都减去 2 k 2^k 2k 。
如果一个子数组内执行上述操作若干次后,该子数组可以变成一个全为 0
的数组,那么我们称它是一个 美丽 的子数组。
请你返回数组 nums
中 美丽子数组 的数目。
子数组是一个数组中一段连续 非空 的元素序列。
对于所有的位运算操作,我们都可以把每一个比特位单独来考虑,每次去掉两个1
,如果某个数组的美丽子数组,那么这个子数组在该比特位上1
出现的频率一定是偶数次,而根据异或的性质,该位最终的异或值一定为0
。
经过上述推导,可以知道这个子数组每一个比特位的异或值都为0
,那么说明这个子数组的异或值就为0
,所以问题转换为原数组存在多少个异或值为0
的子数组,这个问题等价于我们之前前缀和中做过的题——和为k
的子数组数目,只不过我们这是前缀异或。使用哈希表和前缀异或数组来解决。如果在前缀异或数组 s s s 中存在 s [ l − 1 ] = = s [ r ] s[l-1]==s[r] s[l−1]==s[r],则说明区间 [ l , r ] [l,r] [l,r]是一个美丽子数组,求出 s s s 有多少对相同的值则为答案。
class Solution {
public long beautifulSubarrays(int[] a) {
int n = a.length;
int[] s = new int[n+1];
for (int i = 0; i < n; i++) {
s[i + 1] = s[i] ^ a[i];
}
long ans = 0;
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i <= n; i++) {
ans += map.getOrDefault(s[i], 0);
map.put(s[i], map.getOrDefault(s[i], 0) + 1);
}
return ans;
}
}
注意细节,下标变换成1
开始。
字符串20230322
可以重新被排列为02320232
,它是0232
重复两次得到的。如果一个由数字组成的字符串可以通过重新排列成某个字符串的重复两次时,它被认为是快乐的,给定一个字符串 S S S, 请你求出它有多少个子字符串是快乐的。 S S S 的长度不超过 5 × 1 0 5 5 \times 10^5 5×105
思路其实类似于上题,显然对于一个快乐字符串,其 [ 0 , 9 ] [0,9] [0,9] 的字符出现的个数都为偶数次。对于每个字符,我们可以将其映射到比特位上,比如字符i
,它的值就为 2 i 2^i 2i,如果一个子数组的异或值为0
,则说明其是快乐字符串,做法同上一题类似。
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
char[] a = sc.next().toCharArray();
int n = a.length;
Map<Integer, Integer> map = new HashMap<>();
int[] s = new int[n + 1];
for (int i = 0; i < n; i++) {
int x = 1 << (a[i] - '0');
s[i + 1] = s[i] ^ x;
}
long ans = 0;
for (int i = 0; i <= n; i++) {
ans += map.getOrDefault(s[i], 0);
map.put(s[i], map.getOrDefault(s[i], 0) + 1);
}
System.out.println(ans);
}
}
易懂
序号 | 题目链接 | 难度评级 |
---|---|---|
1 | 子数组异或查询 | 2 |
2 | 统计美丽子数组 | 3 |
3 | Three Days Ago | 4 |