利用前缀信息解决子数组问题(下)

P r o b l e m 6 Problem6 Problem6 构建前缀和余数最早出现的位置,移除的最短子数组长度,使得剩余元素的累加和能被 p p p整除

LeetCode 1590 使数组和能被整除

给你一个正整数数组 n u m s nums nums,请你移除最短子数组(可以为空),使得剩余元素的和能被 p p p整除。不允许将整个数组都移除。请你返回你需要移除的最短子数组的长度,如果无法满足题目的要求,返回-1。

子数组 定义为原数组中连续的一组元素。

示例1:

输入: nums = [3,1,4,2], p = 6
输出: 1
问题分析:

如果 n u m s nums nums数组内所有元素累加和 s u m sum sum p p p的整数倍,那需要移除的最短子数组的长度自然为0。如果 n u m s nums nums数组的累加和不能被 p p p整除,我们记
s u m   m o d   p = a sum~mod ~p = a sum mod p=a
要想使得移除子数组后的剩余元素累加和能被 p p p整除,那被我们移除的子数组的累加和,记为 s u m D r o p e d sumDroped sumDroped,需满足:
s u m D r o p e d   m o d   p = a sumDroped ~ mod ~ p = a sumDroped mod p=a
所以我们要找累加和为 s u m D r o p e d sumDroped sumDroped的子数组,怎么找呢?

和之前的方式一样,我们直接利用前缀和来找 s u m D r o p e d sumDroped sumDroped即可,会得到以下公式:
( p r e x [ j ] − p r e x [ i − 1 ] )   m o d   p = a (prex[j] - prex[i-1]) ~mod ~p = a (prex[j]prex[i1]) mod p=a
【注意】: 这里的 i , j i,j i,j都是指数字的编号,从1开始。

对上述公式进行转化求 p r e x [ i − 1 ] prex[i-1] prex[i1],我们得到:
p r e x [ i − 1 ] = ( p r e x [ j ] − a )   m o d   p prex[i-1] = (prex[j] - a) ~mod ~p prex[i1]=(prex[j]a) mod p
分析到这里,其实就已经可以给出解题代码了,但是我们发现,我们只看重前缀和数组 p r e x [ ] prex[] prex[]关于 p p p的余数,所以我们可以将前缀和数组中的所有数变为其关于 p p p的余数,这样也就得到了前缀和余数数组,记为 p r e x M o d P [ ] prexModP[] prexModP[]。这样找 p r e x [ i − 1 ] prex[i-1] prex[i1]的公式就变为:
p r e x [ i − 1 ] = p r e x [ j ] − a    i f   p r e x [ j ] > = a p r e x [ i − 1 ] = p r e x [ j ] − a + p    i f   p r e x [ j ] < a prex[i-1] = prex[j] - a ~~if ~prex[j] >= a \\ prex[i-1] = prex[j] - a + p ~~ if~ prex[j] <a prex[i1]=prex[j]a  if prex[j]>=aprex[i1]=prex[j]a+p  if prex[j]<a
i f if if去掉,就等价于
p r e x [ i − 1 ] = ( p r e x [ j ] − a + p )   m o d   p prex[i-1] = (prex[j] - a + p) ~ mod ~ p prex[i1]=(prex[j]a+p) mod p

解决代码:
int solutionProblem6(int nums[], int N, int p) {
    int prexModP[N + 1];
    prexModP[0] = 0;
    unordered_map<int, int> map; // prex[i-1] : 出现的最晚位置
    map[0] = 0;
    // 先找出sum%p == a来
    for (int j = 0; j < N; j++) {
        prexModP[j + 1] = (prexModP[j] + nums[j]) % p;
    }

    int a = prexModP[N];
    int result = INT_MAX; // 存储结果
    for (int j = 0; j < N; j++) {
        map[prexModP[j + 1]] = j + 1;
        int aim = (prexModP[j + 1] - a + p) % p;
        if (map.count(aim) != 0) {
            result = min(result, j + 1 - map[aim]);
        }
    }

    return result == N ? -1 : result;
}

【提醒】for循环的 j j j指的是 被删除数组 的最右下标,对应的位置编号是 j + 1 j+1 j+1,我们的思路是这样的:

​ 遍历每个位置 j j j,在每次遍历时去寻找有没有 p r e x [ i − 1 ] prex[i-1] prex[i1],满足
p r e x [ i − 1 ] = ( p r e x [ j ] − a + p )   m o d   p prex[i-1] = (prex[j] - a + p) ~ mod ~ p prex[i1]=(prex[j]a+p) mod p
找到则更新result,如果最终的result是数组长度,说明我们只有删除整个数组才能使得剩余元素,也就是0,被 p p p整除,这时我们就要返回-1。

P r o b l e m 7 Problem7 Problem7 构建奇偶状态 最早出现的位置,每个元音包含偶数次的最长子串长度

LeetCode1371 每个元音包含偶数次的最长字符串

给你一个字符串s,请你返回满足以下条件的最长子字符串的长度:每个元音字母,即‘a’, ‘e’, ‘i’, ‘o’, ‘u’在子字符串中都恰好出现了偶数次。

示例 1:

输入:s = "eleetminicoworoep"
输出:13
解释:最长子字符串是 "leetminicowor" ,它包含 e,i,o 各 2 个,以及 0 个 a,u 。

示例 2:

输入:s = "leetcodeisgreat"
输出:5
解释:最长子字符串是 "leetc" ,其中包含 2 个 e 。

示例 3:

输入:s = "bcbcbc"
输出:6
解释:这个示例中,字符串 "bcbcbc" 本身就是最长的,因为所有的元音 a,e,i,o,u 都出现了 0 次。
问题分析:

这道题目并不能直接转换成累加和的形式,所以前缀和是用不了了,我们得考虑其他方式来构建前缀信息。

同学们不难想到,奇数+1就变成了偶数,偶数+1就变成了奇数,这可以用异或运算来表示元音个数在奇数和偶数之间的转换。有五个元音,我们就用五个位来表示,‘_ _ _ _ _’。五个为分别对应‘a’, ‘e’, ‘i’, ‘o’, ‘u’,其中0表示元音字母出现次数为偶数,1表示出现字母为奇数。

现在我们假设符合条件的最长子串的最右下标为 j j j,为了保证子串更长,那我们就要尽量使子串的最左下标 i i i越小越好,同时还需要满足s[i,...,j]中元音字母出现次数为偶数。

很好,现在只需要弄明白 j j j i i i之间的关系就好了。如果s[0,...,j]中的‘a’出现了奇数次,那我们就要保证s[0,...,i-1]也出现奇数次,并且 i − 1 i-1 i1要尽可能地小。‘e’, ‘i’, ‘o’, ‘u’也是如此。我们借助之前的”五个位“进行分析,

如果s[0,...,j]中各元音字母的个数为’10011‘,那么我们就要找最小的 i − 1 i-1 i1,并使其满足s[0,...,i-1]中各元音字母的个数为’10011‘。

分析到现在,我们总结一下:

重点分析:

​ 每个子数组s[0,...,m]都有一个状态‘_ _ _ _ _’来存储数组中各元音的奇偶数,对于状态为‘aabbb’(a,b = 0 或 1)的子数组s[0,...,j],我们要找到 i − 1 i-1 i1最小的并且状态同样为‘aabbb’的子数组s[0,...,i-1],这样就能获得最长的 每个元音字母包含偶数次 的子数组s[i,...,j]

​ 所以此问题的哈希表应当是**{‘_ _ _ _ ’: 出现的最早位置 }**,但是状态‘ _ _ _ _’应该是五位的二进制数,我们直接用32位的int去表示,会浪费很多空间,同时哈希表也占挺大空间的。既然就五个位,并且【键】状态 对应的【值】是出现的最早位置,那我就直接用int map[32]去存储前缀信息,这样非常方便,如果哈希表的键是int型数据并且连续的话,我推荐大家使用int []去存储前缀信息。

解决代码:
//将字符变成相应的数 
// 'a' -> 4, 'e' -> 3, 'i' -> 2, 'o' -> 1, 'u' -> 0, else -> -1 
int move(char a) {
    switch (a)
    {
    case 'a': return 4;
    case 'e': return 3;
    case 'i': return 2;
    case 'o': return 1;
    case 'u': return 0;
    default:
        return -1;
    }
}

int solutionProblem6(string s) {
    int map[32];
    fill(map, map + 32, -2); //-2表示此状态从未出现过
    map[0] = -1; //一个元素都不考虑时,位置下标可以看成-1,其对应的状态自然是 '00000',也就是0

    int result = 0; //存储结果
    int status = 0; //存储s[0,...,j]当前状态
    for (int j = 0;j < s.length();j++) {
        int m = move(s[j]);
        if (m != -1) {
            status ^= (1 << m);
            //更新map
            if (map[status] == -2)   map[status] = j;
        }
        //寻找状态同为status的s[0,...,i-1]
        if (map[status] != -2) {
            result = max(result, j - map[status]);
        }
    }
    return result;
}
  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fresher.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值