<think>我们正在处理上海计算机学会2025年2月月赛C++丙组T5子矩阵和问题。
根据题目描述,给定一个由数字0~9构成的字符串S,长度为n。构造一个n×n的矩阵,其中第i行第j列的元素值为S[i]与S[j]对应数字的乘积(注意:字符串下标从1开始还是0开始?题目没有明确,但通常编程题中字符串下标从0开始,但题目描述中第i个字符,在矩阵中对应第i行第j列,所以我们可以认为矩阵的行列索引从1到n,对应字符串索引0到n-1?但题目没有明确,需要按题意理解。不过,在构造矩阵时,我们通常用0-indexed,但题目描述是第i个字符,所以如果题目说第i个字符,那么对应字符串下标i-1。因此,矩阵中(i,j)位置的元素为 (S[i-1]-'0') * (S[j-1]-'0')。
但是,我们注意到题目要求的是满足条件的子矩阵个数。条件是什么?题目中说明:子矩阵的和等于该子矩阵中所有元素的和。然后,我们需要统计所有满足条件的子矩阵。
然而,从引用[2]中我们得到重要提示:利用子矩阵和等于行和乘以列和的特性,将二维问题转化为一维问题。具体来说,引用[2]提到:子矩阵和等于行和乘以列和?这似乎不太对。实际上,引用[2]中有一个例子:(x+y)^2 = x^2 + 2xy + y^2,而矩阵元素(除第一行和第一列)的特性是行*列。这里可能是在说,矩阵的每个元素是行向量和列向量的对应元素相乘(即外积矩阵)。因此,整个矩阵可以表示为:设向量a = (a1, a2, ..., an),则矩阵A的每个元素A[i][j] = a_i * a_j。
那么,对于这样的矩阵,任意一个子矩阵(从第r1行到第r2行,第c1列到第c2列)的和可以表示为:
sum = (a_{r1} + a_{r1+1} + ... + a_{r2}) * (a_{c1} + a_{c1+1} + ... + a_{c2})
即:行和(从r1到r2行的元素对应的a_i之和)乘以列和(从c1到c2列的元素对应的a_j之和)?
但是,注意:矩阵元素A[i][j] = a_i * a_j,那么子矩阵的和为:
sum = Σ_{i=r1}^{r2} Σ_{j=c1}^{c2} (a_i * a_j)
= (Σ_{i=r1}^{r2} a_i) * (Σ_{j=c1}^{c2} a_j)
因此,子矩阵的和等于行和乘以列和。所以题目要求满足什么条件的子矩阵?题目没有直接给出,但根据引用[1]的说明:输入5 123,输出2。并给出了两个子矩阵:[(2,1),(3,1)]和[(1,2),(1,3)]。这里坐标表示法:左上角(行,列)和右下角(行,列)。
我们重新理解题目:题目要求统计所有满足“子矩阵和等于该子矩阵中所有元素的和”的子矩阵个数。但根据上面的推导,任意子矩阵的和都等于行和乘以列和,而“所有元素的和”不就是子矩阵和吗?所以这个条件似乎是恒成立的?这显然不可能。
因此,我们需要重新审视题目。实际上,引用[1]中说明:符合题意的子矩阵为两个。所以条件可能不是这个。再查看引用[3]:给定字符串S,构造矩阵,然后要求的是子矩阵和。但引用[3]没有明确条件。
但引用[1]的输入格式是:输入5 123,输出2。这里5和123是什么意思?输入格式可能是两个整数?或者是一个整数和一个字符串?题目没有明确,但引用[3]提到:给定一个由数字0∼9构成的字符串S,其长度为n。所以输入应该是n和S?即输入的第一行是n,第二行是S?但示例输入是“5 123”,所以可能是n=5,S="123"?但S的长度为3,而n=5,这似乎矛盾。所以可能是输入格式为:第一行是n,第二行是字符串S,且S的长度为n。而示例输入“5 123”可能是笔误?或者输入格式是一行,包含n和S?但S是一个字符串,中间有空格?这不太可能。
重新阅读引用[1]:输入: 5 123 输出: 2。这里可能表示n=5,但字符串S只有3个字符?这显然不对。所以可能是输入格式为:一个整数n,然后一个长度为n的字符串。但示例输入是“5 123”,如果n=5,那么字符串应该是5个字符,而"123"只有3个字符。所以可能是输入两个整数?或者输入格式为:第一行两个整数?但题目没有明确。
由于题目信息不足,我们只能根据已有信息推测:题目要求的是子矩阵和等于某个特定条件?引用[2]中提到了一个关键点:子矩阵和等于行和乘以列和。但是,在一般的矩阵中,子矩阵和并不等于行和乘以列和,但在本题构造的矩阵(外积矩阵)中,这个等式恒成立。所以题目条件可能不是这个。
再看引用[1]的说明:符合题意的子矩阵为 [(2,1),(3,1)] 与 [(1,2),(1,3)]。第一个子矩阵:从第2行到第3行,第1列到第1列(即两行一列)。这个子矩阵的元素是:
(2,1): S[1](因为行2对应S的第2个字符,列1对应S的第1个字符)的乘积,即S[1]*S[0](假设字符串下标0开始)?注意:题目中矩阵的(2,1)位置,对应字符串的第2个字符和第1个字符(这里题目说第i个字符,所以i=2和j=1,对应字符串下标1和0?)
假设S="123",那么:
第1个字符:'1'(对应下标0)
第2个字符:'2'(对应下标1)
第3个字符:'3'(对应下标2)
构造的3×3矩阵(因为n=3?但输入是5 123,这里n=5?但字符串长度3,所以矩阵应该是5×5?但字符串只有3个字符,怎么构造5×5?所以这里可能输入5是n,但字符串长度应该是5。所以示例输入“5 123”可能是错误的?或者字符串是"123"但n=3?但输入是5 123,那么n=5,字符串"123"长度3,这显然不合理。所以可能是题目描述有误?或者我们理解有误?
重新考虑:引用[3]说:给定一个由数字0∼9构成的字符串S,其长度为n。所以输入应该是n和S,且S的长度为n。那么示例输入“5 123”可能是n=5,然后字符串S是5个字符?但"123"只有3个字符,所以可能是输入格式为:第一行一个整数n,第二行一个字符串S(长度为n)。那么示例输入应该是:
5
12345 (或其他5个字符)
但题目示例输入是“5 123”,所以可能是输入在一行,即第一个整数n,后面一个字符串(但字符串长度可能小于n?)这不符合引用[3]的描述。所以这里我们假设题目输入是:第一行一个整数n,第二行一个长度为n的字符串S。
那么,对于n=3,S="123",构造的矩阵为3×3:
a = [1,2,3]
矩阵A:
1 2 3
2 4 6
3 6 9
题目要求:子矩阵和等于该子矩阵中所有元素的和?这显然恒成立。所以条件一定不是这个。
引用[1]中说明:符合题意的子矩阵为两个:[(2,1),(3,1)]和[(1,2),(1,3)]。
第一个子矩阵:从第2行到第3行,第1列到第1列(即一个2行1列的子矩阵)。元素为:
(2,1): 2
(3,1): 3
和=2+3=5。
第二个子矩阵:从第1行到第1行,第2列到第3列(即1行2列)。元素为:
(1,2): 2
(1,3): 3
和=2+3=5。
我们注意到,这两个子矩阵的和都是5。那么条件可能是子矩阵和等于5?但题目并没有给出这个5。再观察输入:输入是5 123,其中5可能就是要求子矩阵和等于5?所以题目可能是:给定n和字符串S,以及一个整数k,要求子矩阵和等于k的个数?但引用[1]的输入只有5和123,没有k。所以可能是:输入n和S,然后要求子矩阵和等于n(这里n=5)?但示例中n=5,而子矩阵和是5,所以条件可能是子矩阵和等于给定的第一个整数(即5)?
因此,我们推测题目实际要求:统计所有子矩阵中和等于给定整数k(即输入的第一个整数)的个数。输入格式:第一行两个整数n和k,第二行一个长度为n的字符串S。
但引用[1]的输入是“5 123”,那么可能是:第一行两个整数,第一个整数是k,第二个整数是n?然后第二行字符串?但这样也不合理,因为123不是整数。或者输入格式为:第一行一个整数k,第二行一个字符串S(长度为n)?那么n就是字符串长度。这样,输入:5(k=5),然后字符串S="123",n=3。
这样,矩阵为3×3,如上所示。然后要求子矩阵和等于5的子矩阵个数。根据引用[1]输出2。
所以题目条件应该是:统计所有子矩阵中和等于k(输入的第一个整数)的个数。
那么,解题思路如下:
1. 输入整数k和字符串S(长度为n)。
2. 构造数组a,其中a[i] = S[i] - '0'(i从0到n-1)。
3. 矩阵A的任意元素A[i][j] = a[i] * a[j](注意:这里矩阵的行列索引从0到n-1,但子矩阵的选取可以任意连续行和连续列)。
4. 由于子矩阵和 = (a[i1]+a[i1+1]+...+a[i2]) * (a[j1]+a[j2+1]+...+a[j2]),即行区间和乘以列区间和。
5. 问题转化为:在数组a中,求有多少个区间对(行区间和列区间),使得行区间和乘以列区间和等于k。
6. 注意:行区间和列区间都是数组a的连续子数组,但行区间和列区间是独立的(即行区间和列区间可以不同,且行区间和列区间分别对应数组a的任意连续子数组)。
7. 因此,我们可以先计算数组a的所有连续子数组的和,并统计每个和出现的次数(用哈希表记录)。
8. 但是,行区间和列区间是同一个数组a的连续子数组,所以我们可以用同一个哈希表。
9. 设freq[x]表示数组a的连续子数组的和等于x的个数。
10. 然后,我们枚举所有可能的行区间和x,那么列区间和需要为k/x(注意:k必须能被x整除,且x≠0)。同时,如果k=0,需要特殊处理。
11. 但是,注意:行区间和列区间可以相同也可以不同,且相互独立,所以对于每个行区间和x,如果列区间和需要y满足x*y=k,那么方案数就是freq[x] * freq[y]。但是,这里要注意:x和y可以相等也可以不等,但都是同一个数组的连续子数组和。
12. 但是,这里有一个问题:行区间和列区间是独立的,所以一个区间和x可以来自行区间,同时区间和y可以来自列区间,且x*y=k,那么这一对就贡献freq[x]*freq[y]个方案。
13. 但是,注意:同一个连续子数组和可能被多次计算(即不同的区间可能和相同),所以用freq数组记录次数。
14. 特殊处理k=0的情况:
如果k=0,那么要求行区间和x=0,或者列区间和y=0?但是,由于x*y=0,所以只要x=0或y=0。但是,注意:x和y都可以任意取值,只要其中一个为0即可?不对,因为x*y=0,所以要么x=0,要么y=0(或者同时为0)。所以方案数包括:
- 行区间和=0,列区间和任意(只要存在列区间和,注意列区间和可以任意,包括0和非0,但要求列区间和存在,即freq[y]中y可以取任意值?不对,实际上列区间和可以任意,但这里我们要求列区间和是任意值都可以?不对,因为x*y=0,当x=0时,y可以任意,因为0*y=0。但是,我们要求的是x*y=k=0,所以当x=0时,y可以是任意值吗?不对,因为0*y=0,所以对于任意列区间和y,都有0*y=0。所以只要行区间和=0,那么列区间和可以是任意连续子数组和(即所有连续子数组和都满足条件)。同理,当列区间和=0时,行区间和可以是任意连续子数组和。但是,这样会重复计算行区间和=0且列区间和=0的情况。
所以,方案数 = (行区间和=0的区间个数) * (所有列区间和的个数) + (列区间和=0的区间个数) * (所有行区间和的个数) - (行区间和=0且列区间和=0的区间个数) [因为重复计算了行0列0的情况]?
但实际上,行区间和列区间是独立的,所以行区间和=0的区间个数(记为cnt_row0)乘以列区间和任意(记为total,即列区间和的总个数,注意列区间和的总个数等于n*(n+1)/2?不对,连续子数组个数为n*(n+1)/2,但这里我们统计的是每个连续子数组和出现的次数,所以总个数为n*(n+1)/2,即total = n*(n+1)/2。那么,当行区间和=0时,列区间和可以取任意连续子数组(有total种),所以贡献为cnt_row0 * total。同理,当列区间和=0时,行区间和可以取任意连续子数组(有total种),贡献为cnt_col0 * total。但是,这样会重复计算行区间和=0且列区间和=0的情况(即被计算了两次),所以需要减去重复的:cnt_row0 * cnt_col0?不对,因为行区间和=0且列区间和=0的情况在第一种情况中被计算了cnt_row0 * total(其中包含了列区间和为0的情况),在第二种情况中被计算了cnt_col0 * total(其中包含了行区间和为0的情况),所以重复计算了cnt_row0 * cnt_col0次?不对,重复计算的是行0列0的情况,而这种情况被计算了两次(一次在第一种情况中作为列区间任意中的一种,另一次在第二种情况中作为行区间任意中的一种)。所以重复计算了cnt_row0 * cnt_col0次。因此,总方案数 = cnt_row0 * total + cnt_col0 * total - cnt_row0 * cnt_col0。
但是,注意:行区间和列区间都是同一个数组a的连续子数组,所以cnt_row0 = cnt_col0(记为cnt0),而且连续子数组和的总个数total = n*(n+1)/2。所以方案数 = cnt0 * total + cnt0 * total - cnt0 * cnt0 = 2 * cnt0 * total - cnt0^2。
然而,我们还有一种情况没有考虑:当k=0时,除了上述两种情况,还有没有其他情况?比如行区间和不为0,但列区间和为0?这已经包含在第二种情况(列0行任意)中。所以这个公式已经覆盖。
但是,实际上,当行区间和=0时,列区间和任意,包括列区间和为0。所以行0列0的情况在第一种情况中被计算了cnt0 * total次(其中列区间和为0的情况有cnt0次,所以行0列0的情况被计算了cnt0 * cnt0次?不对,在第一种情况中,行0列任意,列任意包括列0,所以行0列0的方案有cnt0(行0的个数)乘以cnt0(列0的个数)种。同样,在第二种情况中,列0行任意,也包括行0列0,所以又计算了cnt0 * cnt0次。因此,重复计算了cnt0^2次。所以总方案数 = cnt0 * total + cnt0 * total - cnt0 * cnt0。
但是,我们为什么要减去cnt0^2?因为行0列0的情况被计算了两次。所以最终k=0的方案数 = 2 * cnt0 * total - cnt0^2。
15. 当k≠0时:
枚举数组a的所有连续子数组和x(x≠0),如果k能被x整除(即k%x==0),且k/x是一个整数(记为y),那么我们就需要查找freq中y出现的次数,然后方案数增加freq[x] * freq[y]。
但是,注意:x和y可以相同也可以不同,且枚举x时,每个x对应一个y,所以直接计算即可。
16. 但是,这里有一个问题:同一个行区间和列区间对是否会被重复计算?不会,因为行区间和列区间是独立的,我们枚举行区间的和x,列区间的和y,然后x*y=k,那么每一对(x,y)对应freq[x]*freq[y]个方案。注意:不同的行区间即使和相同,但区间不同,所以是不同的行区间;列区间同理。
17. 但是,注意:如果x和y相等,那么freq[x]*freq[y]就是freq[x]的平方。
18. 但是,还有一种特殊情况:当k=0时,我们单独处理。当k≠0时,我们只枚举x≠0的情况。但是,如果x=0,那么0*y=0,所以只有当k=0时才会考虑x=0。所以k≠0时,x不能为0。
19. 因此,算法步骤:
步骤1:预处理数组a(a[i]=S[i]-'0')的所有连续子数组和,并统计每个和出现的次数(用freq哈希表)。
步骤2:计算连续子数组的总个数total = n*(n+1)/2。
步骤3:计算cnt0 = freq[0] (即和为0的连续子数组个数)。
步骤4:初始化ans=0。
步骤5:如果k==0:
ans = 2 * cnt0 * total - cnt0 * cnt0
步骤6:如果k≠0:
遍历freq中每个不为0的键x(即x≠0):
如果x整除k(即k%x==0),则y=k/x,然后如果y在freq中,则ans += freq[x] * freq[y]
步骤7:输出ans。
20. 但是,这里有一个问题:行区间和列区间是同一个数组的连续子数组,所以行区间和列区间可以相同吗?可以,因为行区间和列区间是独立的,所以可以相同也可以不同。所以上述计算没有问题。
21. 但是,我们需要注意:同一个连续子数组和,可能由不同的连续子数组得到,所以用freq统计次数。
22. 但是,我们还需要注意:连续子数组和的范围。由于a[i]是0~9的整数,所以连续子数组和最小为0,最大为9*n。因此,k的范围?题目没有给出,但k是输入给定的整数,所以k可能很大?但n最大是多少?题目没有给出,但月赛丙组一般n<=1000或2000。
23. 然而,如果我们用双重循环计算所有连续子数组和,那么时间复杂度为O(n^2),对于n=1000,需要计算50万次,可以接受。
24. 但是,k可能很大,比如1e9,那么我们在枚举x时,x的范围是0到9*n(最大9000),所以枚举x的范围是0~9000,然后判断k%x==0,然后y=k/x,然后检查y是否在0~9000范围内(因为freq中只有0~9000)?如果y不在0~9000,那么就不存在。这样,枚举x只需要9000次,很快。
25. 但是,注意:当x=0时,k≠0,那么0*y=k≠0,不可能,所以k≠0时x不能为0,所以枚举x时跳过0。
26. 但是,如果k为负数怎么办?题目中a[i]是非负整数(0~9),所以连续子数组和都是非负整数,因此x>=0,y>=0。所以如果k<0,那么不可能有解,直接输出0。
27. 因此,我们还需要判断k的符号:
如果k<0,则ans=0。
如果k=0,按上述计算。
如果k>0,则枚举x(x>0且x<=max_sum,max_sum=9*n),然后判断整除和y的范围(y应该在0~max_sum之间?但是y=k/x,而x>=1,所以y<=k,而k可能很大,但是freq中只存储了0~max_sum的值,所以如果y>max_sum,那么就不存在,跳过。
28. 但是,k可能很大,导致y很大(比如k=1e9,x=1,则y=1e9,而max_sum=9000,所以y>9000,那么就不存在),所以不需要担心。
29. 所以,算法步骤修改为:
输入:整数k,字符串S(长度为n)
如果k<0: 输出0
否则:
构造数组a,长度n
freq = {} # 记录连续子数组和出现的次数
total = n*(n+1)//2 # 连续子数组的总个数
# 计算所有连续子数组和
for i in range(n):
s = 0
for j in range(i, n):
s += a[j]
freq[s] = freq.get(s,0) + 1
cnt0 = freq.get(0,0) # 和为0的连续子数组个数
if k == 0:
ans = 2 * cnt0 * total - cnt0 * cnt0
else: # k>0
ans = 0
# 遍历freq中所有键,注意:只考虑x>0
for x in freq:
if x==0:
continue
if k % x == 0:
y = k // x
# 如果y在freq中,且y>=0(显然)且y<=9*n(虽然不一定需要,但freq中只有0~9*n)
# 注意:y可能为0,但k>0时,y=k/x>0,所以不会为0。但是当k>0时,x>0,y>0。
if y in freq:
ans += freq[x] * freq[y]
# 输出ans
30. 但是,这里有一个问题:当k>0时,我们只枚举了x>0,那么x=0的情况不考虑(因为x=0时,0*y=0≠k>0),所以没问题。
31. 但是,还有一种情况:x和y可能相等,但我们在枚举x时,如果x=y,那么freq[x] * freq[y] = freq[x]^2,这没有问题,因为行区间和列区间独立,所以允许行区间和列区间相等(即同一个连续子数组作为行区间,同一个连续子数组作为列区间)?但是,注意:行区间和列区间可以是同一个连续子数组吗?实际上,行区间和列区间是两个独立的区间,它们可以是同一个连续子数组(即区间相同),也可以不同。所以这样计算没有问题。
32. 但是,我们还需要注意:连续子数组和可能有重复值,所以freq[x]可能大于1,所以用乘法。
33. 测试示例:n=3, S="123", k=5
a = [1,2,3]
连续子数组和:
[1]:1 -> freq[1]=1
[1,2]:3 -> freq[3]=1
[1,2,3]:6 -> freq[6]=1
[2]:2 -> freq[2]=1
[2,3]:5 -> freq[5]=1
[3]:3 -> freq[3]=2 (因为有两个区间和为3:一个是[1,2]得到3,另一个是[3]得到3?不对,[3]是3,所以freq[3]应该是2?)
重新计算:
从i=0:
j=0: s=1 -> freq[1]=1
j=1: s=1+2=3 -> freq[3]=1
j=2: s=1+2+3=6 -> freq[6]=1
从i=1:
j=1: s=2 -> freq[2]=1
j=2: s=2+3=5 -> freq[5]=1
从i=2:
j=2: s=3 -> freq[3]=2? 不对,此时freq[3]已经存在,所以freq[3]变为2?不对,前面已经有一个3([1,2]),现在又有一个3([3])?所以freq[3]应该是2。但是,我们上面计算i=1时没有计算3,所以这里freq[3]在i=0时已经出现一次,然后在i=2时j=2又出现一次,所以freq[3]=2。
所以freq:
1:1, 2:1, 3:2, 5:1, 6:1
k=5,枚举x(x>0):
x=1: 5%1==0, y=5, freq[5]=1 -> 贡献1*1=1
x=2: 5%2!=0 -> 跳过
x=3: 5%3!=0 -> 跳过
x=5: 5%5==0, y=1, 然后freq[1]=1 -> 贡献1*1=1
x=6: 5%6!=0 -> 跳过
所以ans=1+1=2,符合输出。
34. 但是,注意:行区间和为1的区间(即[0:0])和列区间和为5的区间(即[1:2])构成一个子矩阵(1×2,即第一行的第1列到第2列)?不对,行区间和为1,对应行区间是[0:0](即第一行),列区间和为5,对应列区间是[1:2](即第2列到第3列)。所以子矩阵是从第1行到第1行,第2列到第3列(即一个1×2的矩阵)。这个子矩阵的和=行区间和1 * 列区间和5 = 5? 不对,应该是1*5=5,但实际这个子矩阵的和为:A[0][1]+A[0][2] = (a0*a1)+(a0*a2)=1*2+1*3=2+3=5,正确。
另一个:行区间和为5(即行区间[1:2]:第2行和第3行)和列区间和为1(即列区间[0:0]:第1列)构成一个子矩阵:第2行到第3行,第1列到第1列(即2×1的矩阵)。子矩阵和=A[1][0]+A[2][0] = (a1*a0)+(a2*a0)=2*1+3*1=2+3=5,正确。
35. 因此,代码实现。
36. 但是,注意:连续子数组和的计算中,同一个和值可能出现多次,所以用freq统计。
37. 另外,当k=0时,我们使用公式:2 * cnt0 * total - cnt0 * cnt0
其中cnt0是连续子数组和为0的个数,total是连续子数组总个数(即n*(n+1)//2)。
示例:n=3, a=[1,2,3],那么和为0的连续子数组个数cnt0=0(因为没有0),所以k=0时,ans=0。
另一个示例:a=[0,0,0],则n=3,连续子数组和全为0,cnt0=6(因为连续子数组有6个,每个和都是0),total=6。
ans = 2*6*6 - 6*6 = 72-36=36。
验证:每个行区间(6种)和列区间(6种)组合,共36种,且每个子矩阵和=0(因为行区间和=0,0*列区间和=0),所以正确。
38. 但是,注意:当k=0时,还有一种情况:行区间和不为0,但列区间和为0?不行,因为要求行区间和乘以列区间和=0,所以必须至少有一个为0。所以上面的公式已经覆盖。
39. 因此,代码实现如下(C++):
注意:由于题目是C++丙组,我们用C++编写。
输入格式:第一行一个整数k,第二行一个字符串S(长度为n,n为字符串长度)
步骤:
- 读取k和S
- n = S.length()
- 构造数组a,大小为n,a[i]=S[i]-'0'
- 计算连续子数组和,并统计频率freq(用map或unordered_map)
- 计算连续子数组总个数total = n*(n+1)/2
- 如果k<0,输出0
- 如果k==0,则计算cnt0 = freq[0](注意:如果freq中没有0,则cnt0=0),然后输出 2*cnt0*total - cnt0*cnt0
- 如果k>0,则遍历freq(跳过0),对于每个键值对(x,cntx),如果x>0且k%x==0,则y=k/x,然后检查y是否在freq中,如果在,则ans += cntx * freq[y](注意:freq[y]可能不存在,所以用map的find方法)
40. 注意:k可能很大,但x在0~9*n之间,所以枚举x没有问题。
41. 但是,k/x可能不是整数?我们已经判断k%x==0,所以y是整数。
42. 另外,y可能很大,但y=k/x,而x最小为1,所以y最大为k。而k的范围?题目没有给出,但k是输入的整数,所以可能很大。但是,我们只关心y是否在freq中,而freq中存储的键最大为9*n(即9000,如果n=1000,则最大为9000),所以如果y>9000,那么freq中肯定不存在,所以我们可以先判断y是否超过max_sum(=9*n),如果超过,则跳过。
43. 因此,在k>0的循环中,可以加上:
if (y > max_sum) continue;
44. 其中max_sum=9*n。
45. 完整代码:
注意:由于题目是月赛,我们使用标准输入输出。
代码:
```cpp
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
int main() {
long long k; // 输入的目标和
string S;
cin >> k; // 读取k
cin >> S; // 读取字符串S
int n = S.length();
vector<int> a(n);
for (int i = 0; i < n; i++) {
a[i] = S[i] - '0';
}
// 计算所有连续子数组的和,并统计频率
unordered_map<long long, long long> freq;
// 注意:连续子数组和可能很大?但最大为9*n(n<=1000,最大9000),所以用long long也可以,但实际int足够,但k可能很大,所以用long long
// 但是,连续子数组和最大为9000,所以可以用int,但为了乘法不溢出,用long long
long long total = (long long)n * (n+1) / 2; // 连续子数组总个数
// 枚举所有连续子数组
for (int i = 0; i < n; i++) {
long long sum = 0;
for (int j = i; j < n; j++) {
sum += a[j];
freq[sum]++; // 这个和出现的次数加1
}
}
// 特判k<0
if (k < 0) {
cout << 0 << endl;
return 0;
}
long long ans = 0;
if (k == 0) {
long long cnt0 = freq[0]; // 和为0的连续子数组个数
// 公式:2 * cnt0 * total - cnt0 * cnt0
ans = 2 * cnt0 * total - cnt0 * cnt0;
cout << ans << endl;
return 0;
}
// k>0
long long max_sum = 9 * (long long)n; // 最大连续子数组和
for (auto &p : freq) {
long long x = p.first;
long long cntx = p.second;
if (x == 0) continue; // 跳过0,因为k>0时x不能为0
// 注意:x可能大于max_sum?但不会,因为x是连续子数组和,最大为max_sum
if (k % x == 0) {
long long y = k / x;
// 如果y大于max_sum,则不可能存在
if (y > max_sum) continue;
// 查找y在freq中出现的次数
if (freq.find(y) != freq.end()) {
ans += cntx * freq[y];
}
}
}
cout << ans << endl;
return 0;
}
```
46. 测试示例:k=5, S="123",输出2。
47. 注意:在k>0的循环中,我们跳过了x=0,所以没有问题。
48. 但是,注意:连续子数组和可能为0,但k>0时,x=0跳过,所以不会考虑。
49. 另外,注意:同一个连续子数组和,我们通过双重循环已经统计了所有区间,所以freq正确。
50. 但是,当n很大时(比如1000),双重循环O(n^2)是10^6,可以接受。
51. 提交时注意:题目要求从标准输入读取,输出到标准输出。
52. 但是,题目输入格式是:第一行一个整数k,第二行一个字符串S。所以我们的代码符合。
53. 注意:k的范围?题目没有给出,但k是long long类型,所以可以很大。但是,在循环中,我们枚举x(x在1~max_sum,即9000)所以很快。
54. 但是,如果k=0,那么直接计算,不需要循环。
55. 因此,我们完成了代码。
但是,引用[1]的输入是“5 123”,我们假设输入第一行是5,第二行是"123",所以输入:
5
123
输出2。
56. 另一个测试:k=0, S="000",则n=3,连续子数组和:每个区间和都是0,所以freq[0]=6(因为有6个连续子数组),total=6,所以ans=2*6*6-6*6=36,输出36。
57. 再测试:k=0, S="00",则n=2,连续子数组和:0,0,0 -> 三个0,即freq[0]=3,total=3,ans=2*3*3-3*3=18-9=9。
58. 验证:对于S="00",矩阵为2×2:
a=[0,0]
矩阵:
0 0
0 0
子矩阵个数:行区间有3种([0:0],[1:1],[0:1]),列区间有3种,所以总子矩阵个数=3*3=9,每个子矩阵和都是0,所以满足条件的有9个。
59. 因此,正确。
60. 最后,注意题目中引用[1]的输入是“5 123”,但我们的输入是两行:第一行5,第二行123。所以符合。
61. 输出代码。
但是,引用[4]提供了二维前缀和,但本题我们不需要构造矩阵,因为利用了矩阵是外积矩阵的性质,将二维问题转化为一维。所以不需要二维前缀和。
因此,我们提供上述代码。</think>### 子矩阵和问题解析
#### 问题背景
给定一个由数字0∼9构成的字符串 $S$,长度为 $n$,构造一个 $n \times n$ 的矩阵 $A$,其中 $A_{ij} = (S_i - '0') \times (S_j - '0')$。要求统计所有子矩阵中和等于给定整数 $k$ 的个数。
#### 解题思路
1. **矩阵特性分析**:
矩阵 $A$ 是外积矩阵,任意子矩阵和可表示为:
$$ \text{sum} = (\text{行区间和}) \times (\text{列区间和}) $$
其中行区间和是字符串 $S$ 的某个连续子数组和,列区间和也是 $S$ 的某个连续子数组和。
例如:$S = "123"$ 时,子矩阵 $[(1,2),(1,3)]$ 的和为 $(1) \times (2+3) = 5$[^2]。
2. **问题转化**:
统计满足以下条件的区间对数量:
$$ (\text{行区间和}) \times (\text{列区间和}) = k $$
行区间和列区间独立,且均来自字符串 $S$ 的连续子数组和。
3. **关键步骤**:
- **预处理连续子数组和**:计算 $S$ 的所有连续子数组和,并用哈希表 `freq` 记录每个和的出现次数。
- **分类处理**:
- **$k < 0$**:直接输出 0(因元素非负,子矩阵和不可能为负)。
- **$k = 0$**:
方案数 = $2 \times \text{cnt}_0 \times \text{total} - \text{cnt}_0^2$
($\text{cnt}_0$ 为和为 0 的区间个数,$\text{total}$ 为总区间数)。
- **$k > 0$**:
枚举 `freq` 中每个非零键 $x$,若 $k \mod x = 0$ 且 $y = k/x$ 在哈希表中,则累加 $\text{freq}[x] \times \text{freq}[y]$。
#### 代码实现(C++)
```cpp
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
int main() {
long long k;
string S;
cin >> k >> S; // 输入k和字符串S
int n = S.length();
vector<int> a(n);
for (int i = 0; i < n; i++) {
a[i] = S[i] - '0';
}
// 计算所有连续子数组和及其频率
unordered_map<long long, long long> freq;
long long total = (long long)n * (n + 1) / 2; // 总区间数
for (int i = 0; i < n; i++) {
long long sum = 0;
for (int j = i; j < n; j++) {
sum += a[j];
freq[sum]++;
}
}
if (k < 0) {
cout << 0 << endl;
return 0;
}
long long ans = 0;
if (k == 0) {
long long cnt0 = freq[0]; // 和为0的区间个数
ans = 2 * cnt0 * total - cnt0 * cnt0;
cout << ans << endl;
return 0;
}
// k > 0 时枚举有效区间对
long long max_sum = 9 * (long long)n; // 最大可能的子数组和
for (auto &p : freq) {
long long x = p.first;
if (x == 0 || x > max_sum) continue;
if (k % x == 0) {
long long y = k / x;
if (y > max_sum) continue;
if (freq.find(y) != freq.end()) {
ans += p.second * freq[y];
}
}
}
cout << ans << endl;
return 0;
}
```
#### 复杂度分析
- **时间复杂度**:$O(n^2)$,双重循环计算连续子数组和。
- **空间复杂度**:$O(n^2)$,哈希表存储子数组和。
#### 示例验证
- **输入**:`k=5`, `S="123"`
**输出**:`2`
**解释**:
子矩阵 $[(1,2),(1,3)]$ 和 $[(2,1),(3,1)]$ 的和均为 5(见引用[^1])。
---
### 相关问题
1. **如何优化连续子数组和的计算效率?**
若 $n$ 较大(如 $n > 1000$),双重循环可能超时。能否用动态规划或数学性质优化?
2. **当 $k$ 极大时如何避免无效枚举?**
代码中通过 `max_sum = 9*n` 过滤 $y > 9n$ 的情况。是否存在更高效的剪枝策略?
3. **矩阵元素为负数时如何处理?**
本题限制数字为 0∼9,但若扩展为负数,子矩阵和可能为负。此时需调整 $k<0$ 的处理逻辑。
[^1]: 上海计算机学会2025年2月月赛C++丙组T5子矩阵和
[^2]: 子矩阵和等于行和乘以列和的特性(二维转一维)