E
贡献法 扫描线 二阶差分
求一个数组的所有子数组的平均数之和。
等价于求所有子数组的加权和。对于长度iii的子数组,权重就是1/i1/i1/i
考虑贡献法,有两种,一种是每个元素的贡献,一种是每个前缀的贡献。先来说第一个,这个比较麻烦,每个元素的贡献,考虑对于每个长度iii的划窗,划过整个数组,每一步给窗口内加上1/i1/i1/i。
对于一个iii只用考虑每个位置被加了多少次1/i1/i1/i。这个东西打表,或者手玩可以发现,贡献基本是一个梯形,开始前缀部分单增的等差数列,中间一段平台,最后后缀是一个单减的等差数列。并且根据窗口长度是否超过nnn的一半,中间平台区的高度不一样。
但总之都是区间加等差数列,这可以线段树,也可以二阶差分。这里选择二阶差分做法,所谓二阶差分就是需要做两次前缀和才能还原。以下代码封装了一个区间加等差数列的二阶差分更新函数,传入区间l,r,首项s,公差d。
具体根据窗口长度分讨两种情况,加的等差数列值这里就不说了,可以作为一个结论,也可以手玩。
另外这里有一堆乘法,除法,加减法,为了取模简单用了modint
c
void solve() {
int n;
cin >> n;
vector<Mint> a(n + 10);
auto add = [&](int l, int r, Mint s, Mint d)->void{
if (l > r)return;
a[l] += s;
a[l + 1] += d - s;
Mint L = l, R = r;
a[r + 1] -= s + (R - L + 1) * d;
a[r + 2] += s + (R - L) * d;
};
rep(i, 1, n) {
int cur = inv(i, M2);
if (i <= (n + 1) / 2) {
add(1, i - 1, cur, cur);
add(n - i + 2, n, (i - 1)*cur, -cur);
add(i, n - i + 1, 1, 0);
} else {
int h = n - i + 1;
add(1, n - i, cur, cur);
add(i + 1, n, (h - 1)*cur, -cur);
add(n - i + 1, i, h * cur, 0);
}
}
rep(i, 1, n) {
a[i] += a[i - 1];
}
rep(i, 1, n) {
a[i] += a[i - 1];
}
Mint ans = 0;
rep(i, 1, n) {
int x;
cin >> x;
ans += Mint(x) * a[i];
}
cout << ans.val << '\n';
}
另一个简单一点的做法是分析每个前缀的贡献,每个区间的贡献实际上可以看成(si−sj)/(i−j)(s_i-s_j)/(i-j)(si−sj)/(i−j),那么对于前缀si,sjs_i,s_jsi,sj,分别有1/(i−j),−1/(i−j)1/(i-j),-1/(i-j)1/(i−j),−1/(i−j)的贡献。
考虑一个sks_ksk的贡献,他作为sis_isi的时候,是对于j∈0,kj∈0,kj∈0,k,这些时候的贡献之和是∑j=0k1/j\sum_{j=0}^k 1/j∑j=0k1/j。他作为−sj-s_j−sj的时候同理,是对于j∈k,nj∈k,nj∈k,n,这些时候的贡献之和是∑j=kn1/j\sum_{j=k}^n 1/j∑j=kn1/j。注意到这两个贡献都是1/j1/j1/j的区间和,维护一个1/j1/j1/j的前缀和即可快速计算贡献。
c
void solve() {
int n;
cin >> n;
vector<Mint> a(n + 10), b(n + 10);
rep(i, 1, n) {
int x;
cin >> x;
b[i] += b[i - 1] + inv(i, M2);
a[i] += a[i - 1] + x;
}
Mint ans = 0;
rep(i, 1, n) {
ans += (b[i] - b[n - i]) * a[i];
}
cout << ans.val << '\n';
}
F
dp 线段树
手上两个变量x=y=0,扫一个排列p,对每个pip_ipi,可以决定使用x或y中的一个,令使用的这个变量t变成max(pi,t)\max(p_i,t)max(pi,t),如果t在这一步变大了,答案计数器+1。问答案最大多少。
看到这个朴素的想法就是f(i,x,y)f(i,x,y)f(i,x,y)表示考虑前i个,两个变量的值分别为x,y,能得到的最大答案。这状态太多了,考虑压缩。
注意到前缀里的每个元素都必须操作,那么对于前缀最大值mximx_imxi,一定也被x或y操作了,那么我们永远可以确定,第i步后,max(x,y)=mxi\max(x,y)=mx_imax(x,y)=mxi。
于是x,y中较大元素永远是确定的,只需要在状态里维护较小元素即可,f(i,j)f(i,j)f(i,j)表示考虑前i个,x,y里较小值为j时的最大答案。
这还是太多了,转移会是O(n)O(n)O(n)的,总复杂度O(n2)O(n^2)O(n2)。仔细分析转移看看能不能数据结构优化。
- 如果pip_ipi大于x,y的较大值,那么让x,y哪个来都能答案+1,并且操作的那个会变成pip_ipi。那么贪心的思考,一定让较大变量变,这样较小值还能保持很小,后面变大的次数更多,答案更大。
- 如果pip_ipi位于x,y之间,那么可以让x来,也可以让y来。如果让较大值来,答案不变,x,y也都不变,无事发生。如果让较小值来,较小值会变大为pip_ipi,答案+1
- 如果pip_ipi小于较小值,也是无事发生。
发现对于上面第一个情况,就是对于所有较小值,答案都会加+1,也就是
f(i,j)=f(i−1,j)+1,1≤j≤nf(i,j)=f(i-1,j)+1,1\le j\le nf(i,j)=f(i−1,j)+1,1≤j≤n
对于第二个情况,可以从较小变量小于pip_ipi的状态转移到pip_ipi,并且答案+1,也就是
f(i,pi)=maxf(i−1,j)+1,j≤pif(i,p_i)=\max f(i-1,j)+1,j\le p_if(i,pi)=maxf(i−1,j)+1,j≤pi
可以发现这两个情况就是区间加,区间查询最值,可以用线段树优化转移,复杂度为O(nlogn)O(n\log n)O(nlogn)。对于第二种情况,计算出f(i,pi)f(i,p_i)f(i,pi)后还需要插入线段树,也就是还需要实现一个单点赋值操作。这和前面的全局+1操作并不冲突。
c
struct Tree {
#define ls u<<1
#define rs u<<1|1
struct Node {
int l, r;
ll mx, add;
} tr[N << 2];
void pushup(int u) {
tr[u].mx = max(tr[ls].mx, tr[rs].mx);
}
void pushdown(int u) {
if (tr[u].add) {
tr[ls].mx += tr[u].add;
tr[rs].mx += tr[u].add;
tr[ls].add += tr[u].add;
tr[rs].add += tr[u].add;
tr[u].add = 0;
}
}
void build(int u, int l, int r) {
tr[u] = {l, r, 0, 0};
if (l == r) {
tr[u].mx = -inf;
return;
}
int mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
pushup(u);
}
void modify(int u, int l, int r, int val) {
if (tr[u].l >= l && tr[u].r <= r) {
tr[u].mx += val;
tr[u].add += val;
return ;
} else {
int mid = (tr[u].l + tr[u].r) >> 1;
pushdown(u);
if (mid >= l)modify(ls, l, r, val);
if (r > mid)modify(rs, l, r, val);
pushup(u);
}
}
void modify1(int u, int l, int r, int val) {
if (tr[u].l >= l && tr[u].r <= r) {
tr[u].mx = max(tr[u].mx, val);
return ;
} else {
int mid = (tr[u].l + tr[u].r) >> 1;
pushdown(u);
if (mid >= l)modify1(ls, l, r, val);
if (r > mid)modify1(rs, l, r, val);
pushup(u);
}
}
ll query(int u, int l, int r) {
if (l <= tr[u].l && tr[u].r <= r)return tr[u].mx;
pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
if (r <= mid)return query(ls, l, r);
if (l > mid)return query(rs, l, r);
return max(query(ls, l, r), query(rs, l, r));
}
} t;
void solve() {
int n;
cin >> n;
int mx = 0;
t.build(1, 0, n);
int x;
cin >> x;
t.modify1(1, 0, 0, 1);
mx = x;
rep(i, 2, n) {
int x;
cin >> x;
if (x > mx) {
t.modify(1, 0, n, 1);
} else {
int res = t.query(1, 0, x);
t.modify1(1, x, x, res + 1);
}
mx = max(mx, x);
}
cout << t.query(1, 0, n) << '\n';
}