ABC468 扫描线|贡献法|二阶差分|线段树优化DP

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)=max⁡f(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(nlog⁡n)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';
}
相关推荐
happyprince1 小时前
篇2-bitsandbytes-具体观-算法与实现剖析
算法
普通攻击往后拉11 小时前
Leetcode 206. 反转链表
算法·leetcode·链表
@syh.11 小时前
【贪心】矩阵消除游戏
算法·游戏·矩阵
可编程芯片开发12 小时前
基于零极点配置的PID控制系统simulink建模与仿真
算法
徐小夕12 小时前
开源!我用SQLite + DuckDB打造了一款可视化AI问数平台
前端·算法·github
Hrain-AI12 小时前
2026 企业 AI 智能体平台横评:8 大主流平台 7 维度实测对比
人工智能·算法·机器学习
Angel Q.13 小时前
因子分析和生成模型有什么关系?从“幕后因素”到“生成数据”
算法
FBI HackerHarry浩15 小时前
AI大模型开发V2第四阶段线性回归
人工智能·算法·线性回归