今天可能给一长串字符串,并有多次区间查询,思路上是昨天题的延续。
思路
看到区间查询能想到前缀和、差分数组和线段树,这题涉及到了区间数字和所以应该能按前缀和去做。
前缀和求区间数字和是老生常谈了,生成前缀和后区间和就是 preSum[r+1]-preSum[l] (这里假设前缀和数组下标全从 1 开始),但是题目还涉及到要把 [l, r] 间的数字拼接成一个数字得到 x。
昨天的题目,我们在按位拼接数字的时候每次迭代会将前一个拼成的数 \times 10 再加上当前数,这题其实也是一样,我们可以递推前缀拼成的数字:
// if s[i]!='0'
preCat[i+1] = preCat[i]*10 + (s[i]-'0')
// else
preCat[i+1] = preCat[i]
可以看到同一个前缀,每一次递推都会 \times 10。能这样递推就可以计算区间结果了,如果要找 [l, r] 区间拼成的数字,那就是:
num = preCat[r+1] - preCat[l]*(10^(区间内非 0 字符的数量))
这里非 0 字符的数量可以用另一个前缀和去跟踪(这样写好像有点麻烦,应该有更优解),10 的幂也可以预先计算生成。
得出两个递推式后就差不多理清思路了,不过注意因为输入规模可能很大,preCat 和 10 的幂的预生成都要取模,计算拼成的 num 时也要取模。
尤其像是计算 num 时,上式算出来可能是负数,一些编程语言里 % 取余后仍是负数,因此要加上一份模数再取余,这样才能得到非负模数。
代码
写得有点丑陋了,应该有更优的写法。
class Solution {
public:
vector<int> sumAndMultiply(string s, vector<vector<int>>& queries) {
// 首先前缀和能解决 sum 的问题
// 问题是 x 的处理,因为 s 可能相当长,直接按位拼起来就算是 long long
// 也无法表示 x 肯定涉及到一个取余问题,如果 preCat
// 是前缀数字拼接得到的值 那么 preCat 的递推就是
// preCat[i+1]=(preCat[i]*10 + (s[i]-'0')) % modulus
// 相当于每次都把前缀构成的数字×10
// 给定 [l, r] 区间,要找到这个区间数字按位拼一起得到的值
// 那么应该是 preCat[r+1] - preCat[l]*(10^(r+1-l))
// 这里 10^(r+1-l) 可以另外开个数组预处理
int n = s.size();
// 下标从 1 开始
vector<int> preSum(n + 1, 0);
vector<int> preNonZeroCnt(n + 1, 0);
vector<long long> preCat(n + 1, 0);
vector<long long> pow10(n + 1, 1);
long long modulus = (long long)(1e9 + 7);
for (int i = 0; i < n; i++) {
preSum[i + 1] = preSum[i] + (s[i] - '0');
pow10[i + 1] = (pow10[i] * 10) % modulus;
if (s[i] != '0') {
preNonZeroCnt[i + 1] = preNonZeroCnt[i] + 1;
preCat[i + 1] = (preCat[i] * 10 + (s[i] - '0')) % modulus;
} else {
// 如果是 0,是不计入数字拼接的
preNonZeroCnt[i + 1] = preNonZeroCnt[i];
preCat[i + 1] = preCat[i];
}
}
vector<int> res;
for (vector<int>& q : queries) {
// 额外加上 modulus 来保证得到非负模数
int l = q[0], r = q[1];
int sum = preSum[r + 1] - preSum[l];
int powCnt = preNonZeroCnt[r + 1] - preNonZeroCnt[l];
long long result =
((preCat[r + 1] - preCat[l] * pow10[powCnt] % modulus +
modulus) %
modulus * sum) %
modulus;
res.emplace_back((int)result);
}
return res;
}
};