文章目录
前言
前缀和,主要用于解决子数组相关问题,比如:子数组求和,数组元素统计,区间操作等。其一般与哈希表一起使用。解决该类问题,最重要的是如何构造前缀信息,其中,构建某个前缀信息最早出现的位置是非常常用的技巧。
一、区域和检索 - 数组不可变——构造前缀和数组
思路:以 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 ]
,之后遍历数组,这个过程中,始终记录前缀和。当来到某个位置 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;
}
四.最大好子数组和——构造最小前缀和
思路:
- 首先同样遍历数组,根据条件,好子数组满足nums[i]=nums[j]-k 或者nums[i]=nums[j]+k,故向左延伸时,只需要找满足条件的nums[i]即可,因此哈希表中key 存放数组元素nums[i],而value存放最小的前缀和,因为要是子数组 [k,i] 累加和最大,则其前缀和[0,k-1] 显然应该最小。
- 每次查找,若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;
}
五.每个元音包含偶数次的最长子字符串——构造前缀奇偶状态
思路:
- 假设一个状态 state,由题知, 对于每个元英字母 a e i o u,认为若出现了偶数次,则对应位为0,若出现奇数次,则对应位为1;显然这些状态只有5为,故一个int 即可表达。
- 之后,遍历数组,统计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;
}
总结
对于前缀和,最关键的就是构建前缀信息,处理以上的构建方式,仍然有许多其他方式,比如:构建最晚出现位置,构建前缀和余数最晚出现位置,构建次数等等,进一步掌握需要做更多的题目来巩固!!!