187. Repeated DNA Sequences
思路1 滑窗+字符串hash
数据范围只有
1
0
5
10^5
105 ,一个朴素的想法是:从左到右处理字符串 s
,使用滑动窗口得到每个以 s[i]
为结尾且长度为 10
的子串,同时使用哈希表记录每个子串的出现次数,如果该子串出现次数超过一次,则加入答案。
为了防止相同的子串被重复添加到答案,而又不使用常数较大的 Set 结构。我们可以规定:当且仅当该子串在之前出现过一次(加上本次,当前出现次数为两次)时,将子串加入答案。
public List<String> findRepeatedDnaSequences(String s) {
int end=10;
HashMap<String,Boolean> map=new HashMap<>();
List<String> res=new ArrayList<>();
while (end<=s.length()){
String sub=s.substring(end-10,end);
if (map.containsKey(sub)&& !map.get(sub)){
res.add(sub);
map.put(sub,true);
}
if (!map.containsKey(sub)){
map.put(sub,false);
}
end++;
}
return res;
}
*思路2 整数hash
子串长度为 101010,因此上述解法的计算量为 1 0 6 10^6 106。
若题目给定的子串长度大于 100
时,加上生成子串和哈希表本身常数操作,那么计算量将超过
1
0
7
10^7
107 ,会 TLE。
因此一个能够做到严格 O ( n ) O(n) O(n) 的做法是使用「字符串哈希 + 前缀和」。
具体做法为,我们使用一个与字符串 sss 等长的哈希数组h[]
,以及次方数组p[]
。
由字符串预处理得到这样的哈希数组和次方数组复杂度为
O
(
n
)
O(n)
O(n)。当我们需要计算子串 s[i...j]
的哈希值,只需要利用前缀和思想
h
[
j
]
−
h
[
i
−
1
]
∗
p
[
j
−
i
+
1
]
h[j]−h[i−1]∗p[j−i+1]
h[j]−h[i−1]∗p[j−i+1] 即可在
O
(
1
)
O(1)
O(1) 时间内得出哈希值(与子串长度无关)。
到这里,还有一个小小的细节需要注意:如果我们期望做到严格 O ( n ) O(n) O(n),进行计数的「哈希表」就不能是以 String 作为 key,只能使用 Integer(也就是 hash 结果本身)作为 key。因为 Java 中的 String 的 hashCode 实现是会对字符串进行遍历的,这样哈希计数过程仍与长度有关,而 Integer 的 hashCode 就是该值本身,这是与长度无关的。
代码2
关于h[i]的递推形式证明:
对于 h[i]=h[i-1]*PRIME+s.charAt(i-1)
;
(Prime简写为P)
h
[
0
]
=
0
h[0]=0
h[0]=0
h
[
1
]
=
h
[
0
]
∗
P
+
s
0
=
s
[
0
]
h[1]=h[0]*P+s_0=s[0]
h[1]=h[0]∗P+s0=s[0]
h
[
2
]
=
h
[
1
]
∗
P
+
s
1
=
s
0
∗
P
+
s
1
h[2]=h[1]*P+s_1=s_0*P+s_1
h[2]=h[1]∗P+s1=s0∗P+s1
h
[
3
]
=
h
[
2
]
∗
P
+
s
2
=
s
0
∗
P
2
+
s
1
∗
P
+
s
2
h[3]=h[2]*P+s_2=s_0*P^2+s_1*P+s_2
h[3]=h[2]∗P+s2=s0∗P2+s1∗P+s2
.
.
.
...
...
h
[
j
]
=
h
[
j
−
1
]
]
∗
P
+
s
j
−
1
=
s
0
∗
P
j
−
1
+
s
1
∗
P
j
−
2
+
.
.
.
+
s
j
−
1
h[j]=h[j-1]]*P+s_{j-1}=s_0*P^{j-1}+s_1*P^{j-2}+...+s_{j-1}
h[j]=h[j−1]]∗P+sj−1=s0∗Pj−1+s1∗Pj−2+...+sj−1
对于hash=h[j]-h[i-1]*p[j-i+1]
:
h
[
i
]
=
s
0
∗
P
i
−
1
+
s
1
∗
p
i
−
2
+
.
.
.
+
s
i
−
1
h[i]=s_0*P^{i-1}+s_1*p^{i-2}+...+s_{i-1}
h[i]=s0∗Pi−1+s1∗pi−2+...+si−1
h
[
i
+
10
]
=
s
0
∗
P
i
+
9
+
s
1
∗
p
i
−
2
+
.
.
.
+
s
i
+
9
h[i+10]=s_0*P^{i+9}+s_1*p^{i-2}+...+s_{i+9}
h[i+10]=s0∗Pi+9+s1∗pi−2+...+si+9
h
a
s
h
=
h
[
i
+
10
]
−
h
[
i
]
∗
P
10
=
s
i
∗
P
9
+
s
i
+
1
∗
P
8
+
.
.
.
+
s
i
+
9
hash=h[i+10]-h[i]*P^{10}=s_{i}*P^9+s_{i+1}*P^8+...+s_{i+9}
hash=h[i+10]−h[i]∗P10=si∗P9+si+1∗P8+...+si+9
code
class Solution {
public static Integer N= (int) (1e5+10);
public static Integer PRIME= 131313;
public List<String> findRepeatedDnaSequences(String s) {
int n=s.length();
List<String> ans=new ArrayList<>();
int[] h=new int[N];
int[] p=new int[N];
p[0]=1;
for (int i = 1; i <= n; i++) {
h[i]=h[i-1]*PRIME+s.charAt(i-1);
p[i]=p[i-1]*PRIME;
}
Map<Integer,Integer>map=new HashMap<>();
for (int i = 1; i+10-1 <=n ; i++) {
int j=i+10-1;
int hash=h[j]-h[i-1]*p[j-i+1];
int cnt=map.getOrDefault(hash,0);
if (cnt==1){
ans.add(s.substring(i-1,i+10-1));
}
map.merge(hash,1,Integer::sum);
}
return ans;
}
}
820. Short Encoding of Words
思路
Hashset有个特点remove的元素不在set里面的话,是删除不了什么东西的。例如题目中的样例,time me bell,删除ime 的话是什么都不会发生的。利用这一点,我们可以把每个string元素从第一位开始从set中删除。
代码
public int minimumLengthEncoding(String[] words) {
Set<String> set = new HashSet<>(Arrays.asList(words));
for (String word : words) {
for (int i = 1; i < word.length(); i++) {
set.remove(word.substring(i));
}
}
System.out.println(set);
int ans = 0;
for (String word : set) {
ans += word.length() + 1;
}
return ans;
}
869. Reordered Power of 2
思路
预先将 1 < = k < = 1 0 9 1 <= k <= 10^9 1<=k<=109范围内2的幂的字符串表示的长度存入HashMap中,并将其具体值存入HashSet中。将新输入的数字转化为字符串,首先判断hashmap中是否存在长度为k的2的幂,若无则一定不可以重组为2的幂。若存在,则对长度等于字符串的2的幂的每个字符串逐个比较字符数量,若一致则存在返回ture。
代码
class Solution {
static HashMap<Integer, HashSet<String>> map=new HashMap<>();
static {
int base=1;
while (base<1000000000){
int digits=String.valueOf(base).length();
if (!map.containsKey(digits)){
map.put(digits,new HashSet<>());
map.get(digits).add(String.valueOf(base));
}
else{
map.get(digits).add(String.valueOf(base));
}
base*=2;
}
}
public boolean reorderedPowerOf2(int n) {
char[] digits=String.valueOf(n).toCharArray();
int[] cnts=new int[10];
for (char ch:digits){
cnts[ch-'0']++;
}
if (!map.containsKey(digits.length)){
return false;
}
continue2: for (String key:map.get(digits.length)){
int[] cnts_cpy=new int[10];
System.arraycopy(cnts,0,cnts_cpy,0,10);
for (char ch:key.toCharArray()){
cnts_cpy[ch-'0']--;
if (cnts_cpy[ch-'0']<0){
continue continue2;
}
}
return true;
}
return false;
}
}
890. Find and Replace Pattern
思路
创建两个HashMap 实现1对1双射
代码
class Solution {
public List<String> findAndReplacePattern(String[] words, String _pattern) {
List<String> res=new ArrayList<>();
char[] pattern=_pattern.toCharArray();
continue2: for (String word:words){
HashMap<Character,Character> mapping=new HashMap<>();
HashMap<Character,Character> reverseMapping=new HashMap<>();
for (int i=0;i<word.length();i++){
if (!mapping.containsKey(word.charAt(i))&&!reverseMapping.containsKey(pattern[i])){
mapping.put(word.charAt(i),pattern[i]);
reverseMapping.put(pattern[i],word.charAt(i));
}
else if (mapping.containsKey(word.charAt(i))&& mapping.get(word.charAt(i))==pattern[i]){
continue;
}
else{
continue continue2;
}
}
res.add(word);
}
return res;
}
}
911. Online Election
思路
首先定义counter
,用于保存当前时间t的票型,定义curr_idx
为历史最高得票者、定义curr_cnt
为历史最高票数。
定义TreeMaptimeSeries
用于存储在不同时间下的领先者。
对时间从1-n进行遍历,在counter中对当前的票数以及最高者进行判断:
- 若当前票数大于历史最高票
curr_cnt
对其进行更新。 - 若当前最高票者票数大于或等于
curr_cnt
。判断是否与历史最高票者是否一致,不一致则在timeSeries插入最高票数为其的时间戳(t,curr_idx
)。 - 对于任意输入t,通过timeSeries.get(timeSeries.floorKey(t)方法获取最接近t的最高票人即可。
代码
public class TopVotedCandidate {
TreeMap<Integer,Integer> timeSeries=new TreeMap<>();
HashMap<Integer,Integer> counter=new HashMap<>();
public TopVotedCandidate(int[] persons, int[] times) {
init(persons,times);
}
public void init(int[] persons, int[] times){
int curr_idx=-1;
int curr_cnt=0;
for (int i = 0; i < times.length; i++) {
counter.merge(persons[i],1,Integer::sum);
if (counter.get(persons[i])==curr_cnt){
if (curr_idx!=persons[i]){
curr_idx=persons[i];
timeSeries.put(times[i],curr_idx);
}
}
if (counter.get(persons[i])>curr_cnt){
curr_cnt=counter.get(persons[i]);
if (curr_idx!=persons[i]){
curr_idx=persons[i];
timeSeries.put(times[i],curr_idx);
}
}
}
}
public int q(int t) {
return timeSeries.get(timeSeries.floorKey(t));
}
}
954. Array of Doubled Pairs
思路 排序+hashmap
首先对数组从小到大排序,然后遍历每一个数字val
。
对于val
,我们进行如下考虑
- 如果
val
大于0- 如果hashmap中
val
不存在,则我们要寻找2*val
是否存在,因此将hashmap[2*val]+1; - 如果hashmap中
val
存在,则我们要寻找2*val
存在,因此将hashmap[2val]-1且如果hashmap[2val]为0则移除。
- 如果hashmap中
- 如果
val
小于0- 如果hashmap中
val
不存在,则我们要寻找val/2
是否存在,因此将hashmap[val/2]+1; - 如果hashmap中
val
存在,则我们要寻找val/2
存在,因此将hashmap[val/2]-1且如果hashmap[2*val]为0则移除。
最终如果hashmap为空则说明所有元素配对。
- 如果hashmap中
代码
class Solution {
public:
bool canReorderDoubled(vector<int>& arr) {
std::sort(arr.begin(), arr.end());
unordered_map<double,int> set1;
for (int val:arr) {
if (set1.count(val)){
set1[val]--;
if (set1[val]==0){
set1.erase(val);
}
}
else{
if (val<0){
set1[(double )val/2]++;
}
else{
set1[val*2]++;
}
}
}
return set1.empty();
}
};
973. K Closest Points to Origin
思路
使用TreeMap或者ProiorQueue均可,计算距离,存入结构中。最后取前k个。
代码
public int[][] kClosest(int[][] points, int k) {
TreeMap<int[],Integer> map=new TreeMap<>((o1,o2)->{
int first_val = o1[0]*o1[0]+o1[1]*o1[1];
int second_val=o2[0]*o2[0]+o2[1]*o2[1];
if (first_val==second_val){
if (o1[0]!=o2[0]){
return o1[0]-o2[0];
}
return o1[1]-o2[1];
}
return Integer.compare(first_val,second_val);
});
for (var key:points){
map.merge(key,1,Integer::sum);
}
int[][] ans=new int[k][2];
for (int i = 0; i <k ; i++) {
ans[i]=map.firstKey();
map.merge(map.firstKey(),-1,Integer::sum);
if (map.get(map.firstKey())==0){
map.remove(map.firstKey());
}
}
return ans;
}
974. Subarray Sums Divisible by K
思路
对于数组nums
从0到n,我们统计每一个nums[0]到nums[i[的和sum
。我们知道,如果想要子数组的和被k整除,则和除以k的余数必然相等。例如子数组
n
u
m
s
=
{
4
,
5
,
0
,
−
2
,
−
3
,
1
}
nums=\{4,5,0,-2,-3,1\}
nums={4,5,0,−2,−3,1}。
s
u
m
0
=
4
sum_0=4
sum0=4,
s
u
m
1
=
9
%
5
=
4
sum_1=9\%5=4
sum1=9%5=4。
s
u
m
2
=
9
%
5
=
4
sum_2=9\%5=4
sum2=9%5=4,
s
u
m
4
=
9
%
5
=
4
sum_4=9\%5=4
sum4=9%5=4因此对于
{
0
,
1
,
2
,
4
}
\{0,1,2,4\}
{0,1,2,4}任意为开始,任意为结束的子数组(开始坐标<结束坐标),必然可以被k整除。使用求和公式求和即可。
因此整体采用hashMap存储不同的和出现的次数,然后对每一个出现的次数使用求和公式计算。
代码
public int subarraysDivByK(int[] nums, int k) {
HashMap<Integer, Integer> sums = new HashMap<>();
int curr_sum = 0;
sums.put(0, 1);
int ans = 0;
for (int num : nums) {
curr_sum += num;
curr_sum %= k;
curr_sum+=k;
curr_sum %= k;
sums.merge(curr_sum, 1, Integer::sum);
}
for (var pair : sums.entrySet()) {
int n = pair.getValue();
ans += (n) * (n - 1) / 2;
}
return ans;
}
981. Time Based Key-Value Store
思路
以时间为key,设计二维HashMap。
代码
public class TimeMap {
HashMap<String, TreeMap<Integer,String>> map=new HashMap<>();
public TimeMap() {
}
public void set(String key, String value, int timestamp) {
if (!map.containsKey(key)) {
map.put(key, new TreeMap<>());
}
map.get(key).put(timestamp,value);
}
public String get(String key, int timestamp) {
if (!map.containsKey(key)){
return "";
}
var res=map.get(key).floorKey(timestamp);
if (res==null){
return "";
}
return map.get(key).get(res);
}
}
*1001. Grid Illumination
思路 Hash(注意斜向的处理)
棋盘大小的数据范围为 n
n
=
1
0
9
n = 10^9
n=109 ,硬模拟「亮灯」的操作必然会 TLE,而 lamps
和 queries
数据范围为 20000
是一个较为常见的突破口。
由于点亮每一盏灯,可以使得当前 行、列 和 对角线 的位置被照亮,行列可直接使用棋盘坐标的 (x,y)(x, y)(x,y) 来代指,而对角线则可以使用「截距」来进行代指,即使用x+y
和 x−y
进行代指。
分别使用四个「哈希表」row
、col
、left
和 right
来记录 行、列 和 对角线 的点亮情况(key 为线编号,value 为点亮次数)。
这样我们可以在
O
(
1
)
O(1)
O(1) 的复杂度处理掉所有的 lamps[i]
,某个位置被照亮的充要条件为:「当前位置所在行被点亮」或「当前位置所在列被点亮」或「当前位置所处的对角线被点亮」。
同时,由于每次查询后要进行「灭灯」操作(注意:灭灯只能灭有灯的位置,而不是灭有光的位置 🤣),因此我们还需要另外记录每个灯的位置,可以使用利用二维转一维的技巧进行编号:
i
d
x
=
x
∗
n
+
y
idx=x∗n+y
idx=x∗n+yi,并使用 HashSet 进行记录(忽略重复的 lamps[i]
)。
由于询问次数最多为 20000
,因此直接在查询完成后模拟「灭灯」即可(访问自身及相邻格子,共 999 个),计算量为 2∗1052 * 10^52∗10
5
以内,可以接受。若某个位置存在灯,将其从 HashSet 中移除,并更新对应线的点亮情况。
代码
class Solution {
static int[][] dirs = new int[][]{{0, 0}, {0, -1}, {0, 1}, {-1, 0}, {-1, -1}, {-1, 1}, {1, 0}, {1, -1}, {1, 1}};
static final long N = 20001;
HashMap<Integer, Integer> rows = new HashMap<>();
HashMap<Integer, Integer> columns = new HashMap<>();
HashMap<Integer, Integer> left = new HashMap<>();
HashMap<Integer, Integer> right = new HashMap<>();
HashSet<Long> set = new HashSet<>();
public int[] gridIllumination(int n, int[][] lamps, int[][] queries) {
for (var lamp : lamps) {
int r = lamp[0];
int c = lamp[1];
int sum = r + c;
int diff = r - c;
if (set.contains(r * N + c)) {
continue;
}
rows.merge(r, 1, Integer::sum);
columns.merge(c, 1, Integer::sum);
left.merge(sum, 1, Integer::sum);
right.merge(diff, 1, Integer::sum);
set.add(r * N + c);
}
int m = queries.length;
int[] ans = new int[m];
for (int i = 0; i < m; i++) {
int[] q = queries[i];
int r = q[0];
int c = q[1];
int sum = r + c;
int diff = r - c;
if (rows.containsKey(r)||columns.containsKey(c)||left.containsKey(sum)||right.containsKey(diff)){
ans[i]=1;
}
for(var d:dirs){
int next_r = r + d[0], next_c = c + d[1];
int next_sum = next_r + next_c, next_diff = next_r - next_c;
if (next_r < 0 || next_r >= n || next_c < 0 || next_c >= n) continue;
if (set.contains(next_r * N + next_c)) {
set.remove(next_r * N + next_c);
if (rows.get(next_r)==1){
rows.remove(next_r);
}
else{
rows.merge(next_r,-1,Integer::sum);
}
if (columns.get(next_c)==1){
columns.remove(next_c);
}
else{
columns.merge(next_c,-1,Integer::sum);
}
if (left.get(next_sum)==1){
left.remove(next_sum);
}
else{
left.merge(next_sum,-1,Integer::sum);
}
if (right.get(next_diff)==1){
right.remove(next_diff);
}
else{
right.merge(next_diff,-1,Integer::sum);
}
}
}
}
return ans;
}
}
1010. Pairs of Songs With Total Durations Divisible by 60
思路 hash
想要两个值的和为60的整数倍,则这两个值分别对60取余得值得和应为60。例如:
80
+
220
=
300
%
60
=
0
80+220=300\%60=0
80+220=300%60=0
=
80
%
60
+
220
%
60
=80\%60+220\%60
=80%60+220%60
=
20
+
40
=20+40
=20+40
而对于非0以及30的情形,只要在相加=60的两个集合
s
1
,
s
2
s1,s2
s1,s2中各取一个元素,即可组成一个有效组合。因此方案数=
∣
s
1
∣
∗
∣
s
2
∣
|s1|*|s2|
∣s1∣∗∣s2∣。对于0以及30,因为可以组成组合的元素在本集合
s
s
s中,方案数=
(
n
2
)
\binom{n}{2}
(2n)。最终将所有方案数相加即可。
代码
class Solution {
TreeMap<Integer,Long> map=new TreeMap<>();
public int numPairsDivisibleBy60(int[] time) {
for (int val:time){
map.merge(val%60, 1L,Long::sum);
}
Integer upper=map.floorKey(30);
if (upper==null){
return 0;
}
long ans=0;
for (int key:map.keySet()){
if (key>30){
return (int) ans;
}
if (key==0){
ans+=combinations(map.get(key));
}
else if (key==30){
ans+=combinations(map.get(key));
break;
}
else {
ans+=map.get(key)*(map.getOrDefault(60 - key, 0L));
}
}
return (int) ans;
}
public static long combinations(long n){
return (n)*(n-1)/2;
}
}
思路 三Hash
创建两个HashMap和一个HashSet、其中两个HashMap分别是
HashMap<Integer,HashSet<String>> mapper
: key 为每个字符串的长度,Value为长度为key的字符串的集合
HashMap<String,Integer> counter
:key为字符串,Value为前面连续字符串的数量
,HashSet为
HashSet<String> curr
:当前循环中的字符串集合(后续说明)
定义函数
check1DigitDiff(String a,String b)
:确认a和b是不是只差一个字符
我们知道,如果
代码
class Solution {
public int longestStrChain(String[] words) {
HashMap<String,Integer> counter=new HashMap<>();
HashSet<String> curr=new HashSet<>();
HashMap<Integer,HashSet<String>> mapper=new HashMap<>();
for (String word:words){
if (!mapper.containsKey(word.length())){
mapper.put(word.length(),new HashSet<>());
}
mapper.get(word.length()).add(word);
}
int curr_len=1;
while (curr_len<=16){
if (!mapper.containsKey(curr_len)){
curr.clear();
curr_len++;
continue;
}
if (curr.isEmpty()){
for(String word:mapper.get(curr_len)){
counter.put(word,1);
curr.add(word);
}
}
else{
for(String word:mapper.get(curr_len)){
counter.put(word,1);
for (String prev:curr){
if (check1DigitDiff(prev,word)){
counter.put(word,Math.max(counter.get(prev)+1,counter.get(word)));
}
}
}
curr.clear();
curr.addAll(mapper.get(curr_len));
}
curr_len++;
}
int ans=1;
for ( var pair:counter.entrySet()){
ans=Math.max(pair.getValue(),ans);
}
return ans;
}
public static boolean check1DigitDiff(String a,String b){
boolean used=false;
int b_idx=0;
for (int i = 0; i < a.length(); i++) {
if (a.charAt(i)==b.charAt(b_idx)){
b_idx++;
continue;
}
if (a.charAt(i)!=b.charAt(b_idx)&&!used){
used=true;
b_idx++;
i--;
}
else{
return false;
}
}
return true;
}
}
2829 Determine the Minimum Sum of a k-avoiding Array
思路
采取hashset的方式,从小到大逐渐遍历每一个数,并将k关于起的互补数即 a + b = k a+b=k a+b=k置入结果中。直到放满n个元素则结果数+1。
代码
public int minimumSum(int n, int k) {
HashSet<Integer> set=new HashSet<>();
int sum=0;
int elements=0;
int base=1;
while (elements<n){
if (!set.contains(base)){
set.add(k-base);
sum+=base;
elements++;
}
base++;
}
return sum;
}