2026牛客寒假营2 H题O(n)力大砖飞
读题+分析
总共的子数组显然有\(n^2\)个,暴力显然是\(n^3\),解决这道题我考虑了两个方面:1.如何更快速的计算权值 2.能否用类似dp的方法把遍历所有子数组的过程从\(n^2\)压缩到\(n\)?
对于问题1:
及其粗略的分析 伪代码的意思就是从l到r,如果\(a_k\)在之前存在过,total += cur_cnt;如果没有存在过,那么cur_cnt ++ total += cur_cnt。很容易看出total相当于cur_cnt的前缀和,那么分析cur_cnt即可。对于任意一个子数组进行cur_cnt的分析:
我们发现如果每个元素都是不一样的(记为情况\(H\)),所有的cur_cnt很显然就是自然数的升序,total就是自然数的求和,那么对于这道题来说,算出所有子数组情况\(H\)下的total,再减去相等元素的负贡献就可了
| index | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| array | 1 | 1 | 4 | 5 | 1 | 4 |
| cur_cnt | 1 | 1 | 2 | 3 | 3 | 3 |
| 情况\(H\)下 | 1 | 2 | 3 | 4 | 5 | 6 |
| 差值 | 0 | 1 | 1 | 1 | 2 | 3 |
| 负贡献 | 0 | 5 | 0 | 0 | 2 | 1 |
比较tricky的一点是摘清每个元素的负贡献,比如说这里a[2]的负贡献是5,因为自从cur_cnt在他这里少加了1之后,往后的每一个cur_cnt都比distinct时少1
对于问题2:
可以这样想,假如我们已经有了\(a_1 - a_k\)的序列,此时在末尾再加上\(a_{k+1}\)就会多上1--k+1,2--k+1,...k--k+1,k+1--k+1这k+1个子数组,由于在末尾加,为了方便我们从右往左看,就是把原题目伪代码中的从l到r改为从r到l,为确保题目等价,只需要输入时把整个数组倒过来存储就ok了(为什么会这么做呢?其实正着当然也可,只不过我想出该思路时是倒着看的,写题解时懒得再反过来了Orz)
将上述例子倒过来存储并从右往左逐个添加元素分析:
| 各轮负贡献/索引 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| array | 4 | 1 | 5 | 4 | 1 | 1 |
| r1 | 0 | |||||
| r2 | 0 | 0 | ||||
| r3 | 0 | 0 | 0 | |||
| r4 | 1 | 0 | 0 | 0 | ||
| r5 | 1 | 1+2 | 0 | 0 | 0 | |
| r6 | 1 | 1+2 | 0 | 0 | 1+2+3+4+5 | 0 |
在round4时,[415]加入4,子数组为[4] [54] [154] [4154] ;a[1]在[4154]中负贡献为1
round5中,[4154]中加入1,子数组为[1] [41] [541] [1541] [41541] ; a[1]在[41541]中负贡献1,a[2]在[1541]负贡献1,、[41541]中负贡献2
round5中,[41541]中加入1,子数组为[1] [11] [411] [5411] [15411] [415411] ; a[1]在[415411]中负贡献1,a[2]在[15411]负贡献1,、[415411]中负贡献2,a[5]在[11] [411] [5411] [15411] [415411] 中分别负贡献1、2、3、4、5
明显的,我们会发现一个数的负贡献与它的 索引 和它 右侧第一个与他相同的数的索引 有关,下证明:
设这个数为\(a_k\)(k from 1 to n),由于是倒序存储,他的输入顺序是\(i\) (from 0 to n-1) 显然有\(k = n - i\)
由于我们是从右向左不断延长来遍历子数组,所以子数组的范围总会先到达\(a_k\)再到达\(a_{k-1}\),直至\(a_1\),在这个过程中,\(a_k\)的负贡献从1,再到2,最后到了k
所以每延长一次,\(a_k\)所具有的负贡献是\(\sum_{i=1}^k = \frac{k(k+1)} 2\),记作\(point_k\)
那么\(point_k\)在整个延长过程中一共会出现几次呢?我们知道,只有它的右侧有相同元素时才会出现负贡献,所以第一次出现就是延长到右侧第一个与他相同的数时,在这之后直至所有元素都已涉及到,他都会一直进行负贡献(显然,因为一直有和他一样的数),假设右侧第一个与\(a_k\)相同的数为\(a_h\),输入顺序为\(j\)(\(h > k,j < i,h = n - j\))那么我们可以得到总共出现次数为\(n - h + 1 = j + 1\),记作\(advent_k\)
那么在整个过程中对于某个不属于\(distinct\)的元素\(a_k\)来说,他的负贡献就是\(point_k * advent_k\)
实现思路
1.求出情况\(H\)下的\(total\)
长度为\(1\)的子数组有\(n\)个,每个子数组的\(total\)为\(1\)
长度为\(2\)的子数组有\(n-1\)个,每个子数组的\(total\)为\(1+2\)
长度为\(i\)的子数组有\(n-i+1\)个,每个子数组的\(total\)为\(\frac{i(1+i)}2\)
\(TOTAL = \sum^n_{i=1}\frac{(n+1-i)i(i+1)}2 = \sum^n_{i=1}(\frac{n}{2}i^2 + \frac{n+1}2 i - \frac{i^3}2) = \frac 1 {24}n(n+1)(n+2)(n+3)\)
中间涉及的\(i^2,i^3\)的累加和可以参考之前的博客
要注意的是,这里出现了4次方,所以用long long也会溢出,要用__int128
2.求出\(point_k\) 和 \(advent_k\)
将数组倒序输入存储,并用哈希表(unordered_map来充当)存储每一个值的输入顺序+1,当输入到\(a_k\)时,mp.find(a[k]) != mp.end()就代表他的右侧有一个与他相同的元素,由上面分析可知,\(advent_k = j+ 1\),也就是mp中存储的数,那么advent[n-k]=mp[a[k]]
由于\(point_k= \frac{k(k+1)} 2\),所以最后算结果时,只需要遍历数组(等价于倒序遍历题目中给的原数组),执行total -= (__int128)advent[i] * (1 + i) * i / 2即可
主要代码如下
#include <bits/stdc++.h>
#define fr =fastRead()
#define fl =fastRead(1)
using ll = long long;
using namespace std;
inline void print_128(__int128 x){
if(x < 0){
putchar('-');
x = -x;
}
if(x > 9) print_128(x / 10);
putchar(x % 10 + '0');
}
void funH()
{
int cnt fr,t;
__int128 total;
while(cnt --){
ll n fl;
vector<int>a(n+1);
vector<int>advent(n+1,0);
unordered_map<int,int>mp;
total = (__int128)(n * (n + 1) / 2 * (n + 2)) * (__int128)(n + 3) / 12;
//n(n+1)(n+2)(n+3)/24
for(int i = 0;i < n;++i){
t fr;
a[n-i] = t;
if(mp.find(t) != mp.end()){
advent[n-i] = mp[t];
}
mp[t] = i + 1;
}
for(int i = 1;i <= n;++i){
// printf("a[%d] advent = %d\n",i,advent[i]);
if(advent[i]){
total -= (__int128)advent[i] * (1 + i) * i / 2;
}
}
print_128(total);putchar('\n');
}
}
浙公网安备 33010602011771号