LeetCode第278周赛回顾II

2156. 查找给定哈希值的子串

给定整数 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);
	}

}

2157. 字符串分组

给你一个下标从 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;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值