题目描述
难度分:2600
输入T(≤103)表示T组数据。所有数据的n之和≤2×103。
每组数据输入n(1≤n≤2×103)和长为n的数组a(1≤a[i]≤105)。
你可以执行如下两类操作:
- 合并相邻元素。例如把[2,2,3]的前两个数合并成4,得到[4,3]。
- 把一个大于1的数v分解为两个正整数x和y,满足x+y=v。例如把[2,2,3]的第一个数分解为两个1,得到[1,1,2,3]。
定义f(b)表示把b变成回文数组的最少操作次数。对于a的所有非空连续子数组b,计算f(b),输出所有f(b)之和。
输入样例
4
3
2 1 3
4
1 1 1 1
5
4 2 3 1 5
4
1 2 1 2
输出样例
3
0
14
5
算法
贡献法
思维难度很高的一道贡献计算题,很不好想。
合并操作,每次可以减少一个数字。
分解操作,比如最左边是2,最右边是5,我们分解大的那个数,得到3和2。这时候在两端的两个2就可以配对,把两边的2去掉,接下来考虑把中间部分变为回文。此时中间部分的最右端就是一个3,相比之前左边是2右边是5,这次操作就等价于去掉最左边的2,并修改最右边的5为3,所以每次操作也可以减少一个数字。
因此,这两种操作效果是等效的,不妨只考虑好理解的合并操作。
如果按照每次减少一个数的思路,那么长为m的子数组b操作m−1次可以变成一个数,此时一定是回文的。
接下来我们减少操作次数,比如b=[2,2,1,3],左右都可以合并成4,最后得到[4,4],这样只需操作2次而不是3次,减少了1次操作。
又比如b=[2,2,9,1,3],左右都可以合并成4,最后得到[4,9,4],这样只需操作2次而不是4次,减少了2次操作。
考虑一般情况,发现如果前缀[0,i]与后缀[j,m−1]的元素和相同,那么可以减少1次操作。注意前后缀可以相交,在b=[2,2,9,1,3]这个例子中,我们有2+2=1+3,同时还有2+2+9=9+1+3,所以可以减少2次操作(有多少个相同的和就可以减少几次操作,看上去好像是这个规律)。
这时候就可以考虑贡献法,如果有两个非空子数组[i1,j1],[i2,j2]的元素和相同,那么对于下标从i1到j2的子数组b来说,可以减少1次操作,对答案的贡献就是−1。
所以只需统计非空子数组和的个数cnt_s。遍历到子数组[i,j]时,设其元素和为s,那么它与之前统计过的cnt_s个子数组和相同,对答案的贡献就是j−i−cnt_s。其中j−i是不考虑相同前后缀时,长为j−i+1的子数组所需的操作次数(即一次减少一个数的合并操作)。
复杂度分析
时间复杂度
双重循环遍历每个子数组[i,j]就能计算出答案,时间复杂度为O(n2)。
空间复杂度
空间消耗主要在于哈希表counter,最差情况下每个子数组[i,j]都有不同的累加和,这样就会存O(n2)级别的键值对。因此,额外空间复杂度为O(n2)。
C++ 代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2010;
int T, n, a[N];
struct custom_hash {
static uint64_t splitmix64(uint64_t x) {
x += 0x9e3779b97f4a7c15;
x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
x = (x ^ (x >> 27)) * 0x94d049bb133111eb;
return x ^ (x >> 31);
}
size_t operator()(uint64_t x) const {
static const uint64_t FIXED_RANDOM = chrono::steady_clock::now().time_since_epoch().count();
return splitmix64(x + FIXED_RANDOM);
}
};
int main() {
scanf("%d", &T);
while(T--) {
scanf("%d", &n);
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
LL ans = 0;
unordered_map<int, int, custom_hash> counter;
for(int i = 1; i <= n; i++) {
int s = 0;
for(int j = i; j <= n; j++) {
s += a[j];
ans += j - i - counter[s];
counter[s]++;
}
}
printf("%lld\n", ans);
}
return 0;
}