刷题日志(2)——前缀和

本文介绍了如何通过构造前缀和数组、最早出现位置哈希表等方式解决一系列与数组相关的问题,包括子数组求和、区间操作、最长子串等,并展示了具体算法实现。
摘要由CSDN通过智能技术生成


前言


前缀和,主要用于解决子数组相关问题,比如:子数组求和,数组元素统计,区间操作等。其一般与哈希表一起使用。解决该类问题,最重要的是如何构造前缀信息,其中,构建某个前缀信息最早出现的位置是非常常用的技巧。

一、区域和检索 - 数组不可变——构造前缀和数组

在这里插入图片描述
思路:以 nums :[3,2,-5,6,2,1]为例
构造前缀和数组 sum:
0位置(0个数) ->s=0 //避免边界讨论
1位置(1个数) ->s=0+3
2位置(2个数) ->s=0+3+(-5)
、、、
6位置(6个数)->s=3+2+(-5)+6+2+1
以此类推。假定给定的 (left,right)为(3,5),即[6,2,1]
显然子数组累加和=前6个前缀和-前3个数的前缀和
sum[right+1]-sum[left],将区间累加和转化为两个前缀和

typedef struct {
    int* sum;
} NumArray;

NumArray* numArrayCreate(int* nums, int numsSize) {
    NumArray* p=malloc(sizeof(NumArray));
    p->sum=malloc(sizeof(int)*(numsSize+1));  //比元素组多出0位置,避免边界讨论
    p->sum[0]=0;   //sum[i]表示前i个数的和
    for(int i=1;i<numsSize+1;i++)
    {
        p->sum[i]=p->sum[i-1]+nums[i-1];
    }
    return p;
}
int numArraySumRange(NumArray* obj, int left, int right) 
{
    //给定区间如(3,4),数组0~4有5个元素,0~2有三个元素
    //故对应到sum中为 sum[4+1]-sum[3]
   return obj->sum[right+1]-obj->sum[left];
}

void numArrayFree(NumArray* obj) {
    free(obj->sum);
    obj=NULL;
}

二、表现良好的最长时间段——构造前缀和最早出现位置

在这里插入图片描述
思路:

由题意可知,数组hours只存在两种数据,即 >8 和 <=8,**则用1,-1分别表示这两种状态。

=》这样,题目就转化为 求解子数组累加和>0的最大长度。

  1. 首先 假定一数组 [ 1 , -1 , -1, -1 , 1 , -1 ,1 , 1 , 1, -1 , 1 , 1 ]
    ,之后遍历数组,这个过程中,始终记录前缀和。当来到某个位置 i时,讨论以该位置起,往左延伸多长可以满足条件要求(这也是一般的处理步骤,即到某个位数,则往左延伸子数组,看其能最远延伸到哪里以满足题目要求并且该过程一直记录前缀和,只不过,不同的题目记录的内容可能不同
    在这里插入图片描述
    2.如图所示, 当i来到6位置时,其前缀和为 sum=-1不满足要求,而满足题目调节为sum>0,故只需找到 前缀和为sum<=-2的位置即可满足条件。更进一步,若sum<0,则只需找到其sum-1 的前缀和最早出现位置即可。
    3.为什么只需找到sum-1的前缀和最早出现位置?由于该数组只有1和-1,意味着其累加和增大/减小只能一点一点的增大/减小。如图,当0~j的前缀和为-5时,能够到达-5,其肯定是由于-4减小1得到,其他<sum-1的数同理,最终得到只需找前缀和为sum-1即可,而为了使子数组长度最长,对于相同的前缀和,肯定下标越早,长度越长。
    在这里插入图片描述
    4.若sum>0,则0~i即为最长。
    5.设置初始值,在一开始最好在哈希表的第一条记录插入一个信息,比如 0的前缀和下标即为 -1,这样做可以避免0处前缀和的讨论,虽然本题可以不加
struct HashEntry {
    int key;         // 前缀和
    int val;         // 前缀和出现的最早位置
    UT_hash_handle hh;         
};

void hashAddItem(struct HashEntry **obj, int key, int val) {
    struct HashEntry *pEntry;
    pEntry = malloc(sizeof(struct HashEntry));
    pEntry->key = key;
    pEntry->val = val;
    HASH_ADD_INT(*obj, key, pEntry);
} 

struct HashEntry *hashFindItem(const struct HashEntry **obj, int key)
{
    struct HashEntry *pEntry = NULL;
    HASH_FIND_INT(*obj, &key, pEntry);
    return pEntry;
}

void hashEraseItem(struct HashEntry **obj, int key)
{   
    struct HashEntry *pEntry = NULL;
    HASH_FIND_INT(*obj, &key, pEntry);
    if (NULL != pEntry) {
        HASH_DEL(*obj, pEntry);
        free(pEntry);
    } 
}

void hashFreeAll(struct HashEntry **obj)
{
    struct HashEntry *curr, *next;
    HASH_ITER(hh, *obj, curr, next)
    {
        HASH_DEL(*obj,curr);  
        free(curr);      
    }
}
int longestWPI(int* hours, int hoursSize) {
    int* nums=malloc(sizeof(int)*hoursSize);
    for(int i=0;i<hoursSize;i++)   //hours转化
    {
        nums[i]=hours[i]>8?1:-1;
    }
    int sum=0;  //计算前缀和
    int ans=0;  //符合条件的最大长度
    struct HashEntry* cnt=NULL;
    hashAddItem(&cnt,0,-1); //前缀和为0出现在-1位置,如果不写,不同题目可能出错,所以最好带上
    for(int i=0;i<hoursSize;i++)
    {
        sum+=nums[i];
        if(sum>0)     //如果该前缀和>0,说明0~i即为最大长度,ans更新
        {
            ans=i+1;
        }
        else
        {
            //sum<0,则寻找sum-1最早出现位置
            struct HashEntry* node=hashFindItem(&cnt,sum-1);
            if(node!=NULL)
            {
                ans=ans<i-(node->val)?i-(node->val):ans;
            }
            struct HashEntry* s=hashFindItem(&cnt,sum); 
            if(s==NULL)   //更新哈希表且保证存放的是最早前缀和下标
            {
                hashAddItem(&cnt,sum,i);
            }
        }
    }
    return ans;
}

三. 和为 K 的子数组——构建前缀和出现的次数

在这里插入图片描述
思路:
变量由题转换 和为K的子数组=子数组累加和=K=sum[i]-sum[j]
故只需要找到值为sum-k的前缀和即可,而每出现一次,就统计一次,最后累加返回即可。

struct HashEntry {
    int key;         //前缀和       
    int val;         //前缀和出现次数
    UT_hash_handle hh;         
};

void hashAddItem(struct HashEntry **obj, int key, int val) {
    struct HashEntry *pEntry;
    pEntry = malloc(sizeof(struct HashEntry));
    pEntry->key = key;
    pEntry->val = val;
    HASH_ADD_INT(*obj, key, pEntry);
} 

struct HashEntry *hashFindItem(const struct HashEntry **obj, int key)
{
    struct HashEntry *pEntry = NULL;
    HASH_FIND_INT(*obj, &key, pEntry);
    return pEntry;
}

void hashEraseItem(struct HashEntry **obj, int key)
{   
    struct HashEntry *pEntry = NULL;
    HASH_FIND_INT(*obj, &key, pEntry);
    if (NULL != pEntry) {
        HASH_DEL(*obj, pEntry);
        free(pEntry);
    } 
}

void hashFreeAll(struct HashEntry **obj)
{
    struct HashEntry *curr, *next;
    HASH_ITER(hh, *obj, curr, next)
    {
        HASH_DEL(*obj,curr);  
        free(curr);      
    }
}
int subarraySum(int* nums, int numsSize, int k) {
    struct HashEntry* map=NULL;
    int sum=0;
    int ans=0;
    hashAddItem(&map,0,1);    //0前缀和在没有数字时就已经出现了
    for(int i=0;i<numsSize;i++)
    {
        sum+=nums[i];
        struct HashEntry* pEntry=hashFindItem(&map,sum-k); 
        if(pEntry!=NULL)     //ans更新
        {
            ans+=pEntry->val;
        }
        struct HashEntry* qEntry=hashFindItem(&map, sum);
        if(qEntry==NULL)   //记录前缀和
        {
            hashAddItem(&map,sum,1);
        }
        else
        {
            qEntry->val++;
        }
    }
    return ans;
}

四.最大好子数组和——构造最小前缀和

在这里插入图片描述
思路:

  1. 首先同样遍历数组,根据条件,好子数组满足nums[i]=nums[j]-k 或者nums[i]=nums[j]+k,故向左延伸时,只需要找满足条件的nums[i]即可,因此哈希表中key 存放数组元素nums[i],而value存放最小的前缀和,因为要是子数组 [k,i] 累加和最大,则其前缀和[0,k-1] 显然应该最小。
  2. 每次查找,若nums[i]已经存在,则判断其是否为最小前缀和,若是则更新。若nums[i]不存在,则将其加入哈希表。整个过程中,最大好子数组和不断更新。
struct HashEntry {
    int  key;             // nums[i]
    long long val;        //值为nums[i]的最小前缀和
    UT_hash_handle hh;         
};

void hashAddItem(struct HashEntry **obj, int key, long long val) {
    struct HashEntry *pEntry;
    pEntry = malloc(sizeof(struct HashEntry));
    pEntry->key = key;
    pEntry->val = val;
    HASH_ADD_INT(*obj, key, pEntry);
} 

struct HashEntry *hashFindItem(const struct HashEntry **obj, int key)
{
    struct HashEntry *pEntry = NULL;
    HASH_FIND_INT(*obj, &key, pEntry);
    return pEntry;
}

void hashEraseItem(struct HashEntry **obj, int key)
{   
    struct HashEntry *pEntry = NULL;
    HASH_FIND_INT(*obj, &key, pEntry);
    if (NULL != pEntry) {
        HASH_DEL(*obj, pEntry);
        free(pEntry);
    } 
}

void hashFreeAll(struct HashEntry **obj)
{
    struct HashEntry *curr, *next;
    HASH_ITER(hh, *obj, curr, next)
    {
        HASH_DEL(*obj,curr);  
        free(curr);      
    }
}
long long maximumSubarraySum(int* nums, int numsSize, int k) {
    struct HashEntry* cnt=NULL;
    long sum=0;
    long long ans=LLONG_MIN;
    for(int i=0;i<numsSize;i++)   //构造哈希表
    {
        sum+=nums[i];
        struct HashEntry* node=hashFindItem(&cnt,nums[i]+k);   //nums[i]=nums[j]+/- k  分别判断
        if(node!=NULL)            //如果满足条件
        {
            ans=ans<(sum-node->val+nums[i]+k)?sum-node->val+nums[i]+k:ans;  //更新ans值,ans=sum-(n-1位置的前缀和)
            //即  1   2   3   4   5  ,i=5,sum=15,满足条件的为 j=4位置,sum2=10,显然ans=15-(j=3位置前缀和)
        }
        struct HashEntry* node1=hashFindItem(&cnt,nums[i]-k);
        if(node1!=NULL)
        {
            ans=ans<(sum-node1->val+nums[i]-k)?sum-node1->val+nums[i]-k:ans;
        }
        struct HashEntry* a=hashFindItem(&cnt,nums[i]);
        if(a==NULL)    //维护相同元素的前缀和,始终保持哈希表记录的是最小的前缀和(如果相同)
            hashAddItem(&cnt,nums[i],sum);
        else
        {
            a->val=a->val>sum?sum:a->val;
        }
    }
    return ans==LLONG_MIN?0:ans;
}

五.每个元音包含偶数次的最长子字符串——构造前缀奇偶状态

在这里插入图片描述
思路:

  1. 假设一个状态 state,由题知, 对于每个元英字母 a e i o u,认为若出现了偶数次,则对应位为0,若出现奇数次,则对应位为1;显然这些状态只有5为,故一个int 即可表达。
  2. 之后,遍历数组,统计0~i上元英字母出现的次数,若(j+1 ~ i)中,某几个元英字母出现了奇数次怎么办(如 1 0 0 1 0)?则只有(0 ~ j)上前缀状态也是 (1 0 0 1 0)即可,即 奇+奇=偶
    3.所以,只要0 ~ i上维护的一个非0即1的状态,只需要找到同样状态最早出现在哪里,那么就可以满足条件且是最长的子串(通过位运算得到)
#define MAX_N 10000

int move(char cha) {
    switch (cha) {
        case 'a': return 0;
        case 'e': return 1;
        case 'i': return 2;
        case 'o': return 3;
        case 'u': return 4;
        default: return -1;
    }
}

int findTheLongestSubstring(char* s) {
    int n = 0;
    while (s[n] != '\0') 
    {
        n++;
    }
    int map[32];      // 只有5个元音字符,状态就5位
    for (int i = 0; i < 32; i++) 
    {
        map[i] = -2;
    }
    // map[0...31] = -2
	// map[01100] = -2, 这个状态之前没出现过
    map[0] = -1;
    int ans = 0;
    int status = 0;
    for (int i = 0; i < n; i++) 
    {
        // status : 0....i-1字符串上,aeiou的奇偶性
		// s[i] = 当前字符
		// 情况1 : 当前字符不是元音,status不变
		// 情况2 : 当前字符是元音,a~u(0~4),修改相应的状态
        int m = move(s[i]);
        if (m != -1) {
            status ^= 1 << m;
        }
        // status:  0....i字符串上,aeiou的奇偶性
		// 同样的状态,之前最早出现在哪
        if (map[status] != -2) 
        {
            ans = (ans > (i - map[status])) ? ans : (i - map[status]);
        } else 
        {
            map[status] = i;
        }
    }
    return ans;
}

总结

对于前缀和,最关键的就是构建前缀信息,处理以上的构建方式,仍然有许多其他方式,比如:构建最晚出现位置,构建前缀和余数最晚出现位置,构建次数等等,进一步掌握需要做更多的题目来巩固!!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值