给定整数 p 和 m ,一个长度为 k 且下标从 0 开始的字符串 s 的哈希值按照如下函数计算:
hash(s, p, m) = (val(s[0]) * p0 + val(s[1]) * p1 + ... + val(s[k-1]) * pk-1) mod m.
其中 val(s[i]) 表示 s[i] 在字母表中的下标,从 val('a') = 1 到 val('z') = 26 。
给你一个字符串 s 和整数 power,modulo,k 和 hashValue 。请你返回 s 中 第一个 长度为 k 的 子串 sub ,满足 hash(sub, power, modulo) == hashValue 。
测试数据保证一定 存在 至少一个这样的子串。
子串 定义为一个字符串中连续非空字符组成的序列。
自己写显示简单计算哈希值WA了一次,考虑到有可能是数据量大的问题,采用了快速幂算法,但是还是WA,这就让我十分疑惑了,怎么回事呢?周赛到此打铁。
class Solution {
public String subStrHash(String s, int power, int modulo, int k, int hashValue) {
int n=s.length();
for(int i=0;i<=n-k;i++){
String str=s.substring(i,i+k);
System.out.println(str);
if(hash(str,power,modulo)==hashValue){
return str;
}
}
return null;
}
int hash(String s,int power,int modulo){
int h=0;
for(int i=0;i<s.length();i++){
char c=s.charAt(i);
h+=(int)(c-'a'+1)*pow(power,i,modulo);
h%=modulo;
}
return h;
}
int pow(int a,int n,int m){
if(n==0) return 1;
int ans=pow(a,n/2,m);
ans=ans*ans%m;
if(n%2==1) ans=ans*a%m;
return ans;
}
}
那这个题目错在什么地方呢?
查到的解释是这样的, 需要使用反向滑窗,而我使用了正向滑窗,这会导致每个新值减去头多一个尾,这样会导致除法求模,很难运算,所以采取逆向滑窗,用乘法代替除法
这样可以利用上前面计算的哈希值,并且防止运算乘法逆元,十分优雅
class Solution {
public String subStrHash(String s, int power, int modulo, int k, int hashValue) {
char[] cs = s.toCharArray();
int len = cs.length;
long hash = 0;
long pp = 1;
for (int i = len - k; i < len; i++) {
hash += (cs[i] - 'a' + 1) * pp;
hash %= modulo;
pp = pp * power % modulo;
}
int ans = len - k;
int p = len - k - 1;
while (p >= 0) {
hash *= power;
hash -= (cs[p + k] - 'a' + 1) * pp % modulo;
hash += (cs[p] - 'a' + 1) % modulo;
hash = (hash + modulo) % modulo;
if (hash == hashValue) {
ans = p;
}
p--;
}
return new String(cs, ans, k);
}
}
给你一个下标从 0 开始的字符串数组 words 。每个字符串都只包含 小写英文字母 。words 中任意一个子串中,每个字母都至多只出现一次。
如果通过以下操作之一,我们可以从 s1 的字母集合得到 s2 的字母集合,那么我们称这两个字符串为 关联的 :
往 s1 的字母集合中添加一个字母。
从 s1 的字母集合中删去一个字母。
将 s1 中的一个字母替换成另外任意一个字母(也可以替换为这个字母本身)。
数组 words 可以分为一个或者多个无交集的 组 。如果一个字符串与另一个字符串关联,那么它们应当属于同一个组。
注意,你需要确保分好组后,一个组内的任一字符串与其他组的字符串都不关联。可以证明在这个条件下,分组方案是唯一的。
请你返回一个长度为 2 的数组 ans :
ans[0] 是 words 分组后的 总组数 。
ans[1] 是字符串数目最多的组所包含的字符串数目
方法:状态压缩+BFS
状态压缩:因为每个字符串只有小写字母,且没有重复,所以我们可以用一个26位数的int数值来存储字符串,1对应这个字母存在,0对应不存在,这样一来,若两个字符串s1和s2相关联有四种情况(对应bit为i1,i2):
1.i1==i2 需要枚举1个字符串
2.i1某一位为0,i2同一位为1 需要枚举26个字符串
3.i1某一位为1,i2同一位为2 需要枚举26个字符串
4.i1某一位为0,i2同一位为1;同时i1另一位为1,i2同一位为2 需要枚举26*26个字符串
完成存储后,我们将字符串看成图的节点,words看成一个图,关联关系保存为一条边,这样我们统计图中的连通分量数,并返回最大连通分量点数即可,想要实现这样的目标,我们可以使用BFS/DFS/并查集,我不会并查集,所以使用BFS
注意到结点数有2*10^4,如果逐个枚举,绝对会TLE,所以我们要考虑,对于每个结点考虑其可能的关联结点是否在图中即可,关联节点最多有26*26个,再*2*10^4也不会TLE,这样就可以AC了
class Solution {
public int[] groupStrings(String[] words) {
//记录结点的个数(考虑相同节点)
Map<Integer, Integer> cnt = new HashMap<>();
for (String word : words) {
int xor = getBits(word);
cnt.put(xor, cnt.getOrDefault(xor, 0) + 1);
}
// 记录当前bit值是否被访问
Set<Integer> vis = new HashSet<>();
//统计连通分量数目和最大连通分量大小
int count = 0,maxn = 0;
//遍历节点,建图
for (int node : cnt.keySet()) {
//开始访问一个新的连通分量
if(vis.add(node)){
//该分量大小要考虑一个字符串出现多次
int size=cnt.get(node);
//BFS所用队列
Deque<Integer>queue=new LinkedList<>();
queue.offer(node);
//枚举一个连通分量
while(!queue.isEmpty()){
int cur=queue.poll();
//枚举可能的邻居
for(int neigh:getNeighbours(cur)){
//存在且未访问过,更新数据并入列
if(cnt.containsKey(neigh)&&!vis.contains(neigh)){
vis.add(neigh);
queue.offer(neigh);
size+=cnt.get(neigh);
}
}
}
//更新数据
maxn=Math.max(maxn,size);
count++;
}
}
return new int[]{count, maxn};
}
//对每个结点,计算其所有可能的邻居
private List<Integer> getNeighbours(int root) {
List<Integer> adj = new ArrayList<>();
// 改变任意一个0或1
// 和1异或即为改变
for (int i = 0; i < 26; i++) {
adj.add(root ^ (1 << i));
}
// 任意一对0 1对换
for (int i = 0; i < 26; i++) {
if ((root & (1 << i)) > 0) {
for (int j = 0; j < 26; j++) {
if ((root & (1 << j)) == 0) {
adj.add(root ^ (1 << i) ^ (1 << j));
}
}
}
}
return adj;
}
//状态压缩,将字符串压缩为int
private int getBits(String word) {
int ans = 0;
for (char c : word.toCharArray()) {
int index = c - 'a';
ans |= 1 << index;
}
return ans;
}
}