塔

顾名思义,试炼塔。放的是一些认为很好的题。不保证难度递增。

AT_agc001_f [AGC001F] Wide Swap

这可能是我至今见过最智慧的题了。令 \(Q_{P_i} = i\) 为 \(P_i\) 的逆排列。那么,在 \(P\) 上的一次交换(\(1 \le i < j \le n \land j - i \ge k \land |P_j - P_i| = 1\))等价于在 \(Q\) 上交换相邻两个数(\(|Q_i - Q_{i + 1}| \ge k\))。

原题令 \(P\) 字典序最小,考虑在 \(Q\) 上代表什么。\(Q_i = 1\) 代表 \(P_1 = i\),\(P\) 字典序最小必然意味着 \(P_1\) 最小,然后 \(P_2\) 最小,以此类推,也就是说 \(Q\) 中 \(1\) 最靠前,然后 \(2\) 最靠前,以此类推。注意,这并不是说 \(Q\) 字典序最小,例如 \(Q = \{3, 1, 2\}\) 和 \(Q = \{2, 3, 1\}\),后者字典序更小,但前者 \(1\) 更靠前。

于是,需要贪心地令 \(Q\) 中 \(1\) 最靠前,然后 \(2\) 最靠前,以此类推。如果 \(Q_i < Q_{i + 1}\),那么交换它们必然是不优的。于是只能对 \(Q_i > Q_{i + 1}\) 的交换。这是只有两个数的情况。现在扩展到有两个序列 \(A, B\),假设其内部都已经最优排序,现在需要将其合并。也就是归并排序。

假设指针分别在 \(i, j\),当且仅当 \(B_j + k\) 小于或等于 \(A\) 中 \(i\) 处的后缀最小值时,\(B_j\) 可以不断交换到 \(A_i\) 前面,即放 \(B_j\);否则放 \(A_i\)。

点击查看代码
const int N = 500005;
int n, k, a[N], b[N], minn[N];
void merge(int l, int r){
	if(l == r) return ;
	int mid = (l + r) >> 1;
	merge(l, mid), merge(mid + 1, r);
	int p = l, q = mid + 1, cnt = l;
	minn[mid] = a[mid];
	for(int i = mid - 1; i >= l; i--) minn[i] = min(minn[i + 1], a[i]);
	while(p <= mid && q <= r){
		if(a[q] + k <= minn[p]) b[cnt++] = a[q++];
		else b[cnt++] = a[p++];
	}
	while(p <= mid) b[cnt++] = a[p++];
	while(q <= r) b[cnt++] = a[q++];
	for(int i = l; i <= r; i++) a[i] = b[i];
}
signed main(){
	n = read(), k = read();
	for(int i = 1; i <= n; i++) a[read()] = i;
	merge(1, n);
	for(int i = 1; i <= n; i++) b[a[i]] = i;
	for(int i = 1; i <= n; i++) print(b[i]), putchar('\n');
	return 0;
}

dmy 2026 CSP-S 模拟赛 Day 11 (Div 2) #C. 蜗蜗的反馈灯阵

点击查看题面

蜗蜗正在研究一组带有反馈控制器的指示灯。灯阵共有 \(n\) 盏灯,编号为 \(1\) 到 \(n\),每盏灯可以处于亮起或熄灭状态。

第 \(i\) 盏灯的控制器设有一个整数闭区间 \([l_i,r_i]\)。它只统计其他 \(n−1\) 盏灯中亮起的数量:如果这个数量在 \([l_i,r_i]\) 内,第 \(i\) 盏灯应当亮起;否则,它应当熄灭。

如果一种亮灭配置中的所有灯都符合各自控制器的要求,就称这种配置为稳定配置。对于整数 \(k\),若存在恰好有 \(k\) 盏灯亮起的稳定配置,就将 \(k\) 加入集合 \(S\)。因此,\(0 \le k \le n\),同一个 \(k\) 即使对应多种稳定配置,也只在 \(S\) 中计入一次。

蜗蜗给定了 \(n\) 个整数 \(d_1, d_2, \dots, d_n\)。第 \(i\) 个控制器的区间会从所有满足 \(0 \le l_i \le r_i \le d_i\) 的整数闭区间中等概率选择,各个控制器的选择相互独立。

请你求出随机选择所有区间后,集合 \(S\) 的大小的期望,答案对 \(998244353\) 取模。\(0 \le d_i < n \le 500\)。

这一块都是我自己的想法,赛时拿下 \(45\) 分,赛后改进拿下 \(70\) 分。

期望即为所有集合大小之和,除以区间选择的总方案数。总方案数为 \(\prod \frac{(d_i + 1) \times (d_i + 2)}{2}\),求所有集合大小之和。

拆贡献就是,枚举每个 \(k\),求它合法的方案数,累加起来就是所有集合大小之和。

考虑知道所有区间,判断 \(k\) 是否合法。

  • \(k = 0\),\((\sum [l_i = 0]) = 0\)。

  • \(k > 0\),\((\sum [l_i = k]) = 0 \ \operatorname{and}\ (\sum [l_i \le k - 1 \land k \le r_i(必亮)]) \le k \ \operatorname{and}\ (\sum [l_i > k \lor r_i < k - 1(必暗)]) \le n - k\)。

\(k = 0\) 要求每个 \(l_i, r_i\) 在 \([1, d_i]\) 里任选,方案数为 \(\prod \frac{d_i \times (d_i + 1)}{2}\)。

\(k > 0\) 考虑 DP。设 \(dp[j][a][b]\) 表示前 \(j\) 盏灯,\((\sum[l_i \le k - 1 \land k \le r_i]) = a, (\sum[l_i > k \lor r_i < k - 1]) = b\) 的方案数。

  • \(d_j < k - 1\):

    \(dp[j - 1][a][b] \times \frac{(d + 1) \times (d + 2)}{2} \rightarrow dp[j][a][b + 1]\)

  • \(d_j = k - 1\):

    \(dp[j - 1][a][b] \times k \rightarrow dp[j][a][b]\)

    \(dp[j - 1][a][b] \times \frac{(k - 1) \times k}{2} \rightarrow dp[j][a][b + 1]\)

  • \(d_j = k\):

    \(dp[j - 1][a][b] \rightarrow illegal\)

    \(dp[j - 1][a][b] \times k \rightarrow dp[j][a][b]\)

    \(dp[j - 1][a][b] \times k \rightarrow dp[j][a + 1][b]\)

    \(dp[j - 1][a][b] \times \frac{(k - 1) \times k}{2} \rightarrow dp[j][a][b + 1]\)

  • \(d_j > k\):

    \(dp[j - 1][a][b] \times (d - k + 1) \rightarrow illegal\)

    \(dp[j - 1][a][b] \times k \rightarrow dp[j][a][b]\)

    \(dp[j - 1][a][b] \times k \times (d - k + 1) \rightarrow dp[j][a + 1][b]\)

    \(dp[j - 1][a][b] \times (\frac{(k - 1) \times k}{2} + \frac{(d - k) \times (d - k + 1)}{2}) \rightarrow dp[j][a][b + 1]\)

整合一下,就是如下三个式子:

\(dp[j - 1][a][b] \times k \times [d \ge k - 1] \rightarrow dp[j][a][b]\)

\(dp[j - 1][a][b] \times k \times (\max(d, k - 1) - k + 1) \rightarrow dp[j][a + 1][b]\)

\(dp[j - 1][a][b] \times (\frac{(\min(d, k - 2) + 1) \times (\min(d, k - 2) + 2)}{2} + \frac{(\max(d, k) - k) \times (\max(d, k) - k + 1)}{2}) \rightarrow dp[j][a][b + 1]\)

最终答案为 \(\sum dp[n][a \le k][b \le n - k]\)。暴力做的话是 \(O(n^4)\) 的,也是我的赛时做法,部分分 \(30\) 分。对于 \(\max d_i \le 1\) 发现,当 \(k = 1\) 时,\(b\) 只可能是 \(0\);否则,\(a\) 只可能是 \(0\)。此时转移是 \(O(n^3)\) 的,部分分 \(15\) 分,也是我赛时的全部分数。赛后发现 \(\max d_i \le 20\) 同理呀,当 \(k \le \max d_i\) 时暴力做;否则,\(a\) 只可能是 \(0\)。此时转移是 \(O(\max d_i n^3)\) 的,另有部分分 \(25\) 分,也是我认为我能得到的最高分 \(70\) 分。

正解是容斥,\(a \le k \land b \le n - k\) 的方案数,就是总方案数减 \(a > k\) 的方案数再减 \(b > n - k\) 的方案数。

设 \(f[j][i]\) 表示前 \(j\) 盏灯,\(a = i\) 方案数,\(g[j][i]\) 表示前 \(j\) 盏灯,\(b = i\) 方案数。再用 \(all\) 表示总方案数。转移是类似上面的。注意所有方案都要合法。时间复杂度 \(O(n^3)\)。

点击查看代码
int n, sum, ans, maxd, d[N];
int f[N][N], g[N][N];
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr), cout.tie(nullptr);
	n = read(), sum = 1ll, ans = 1ll;
	for(int i = 1; i <= n; i++){
		maxd = max(maxd, d[i] = read());
		MUL(sum, d[i] * (d[i] + 1) / 2);
		MUL(ans, inv((d[i] + 1) * (d[i] + 2) / 2));
	}
	for(int k = 1; k <= n; k++){
		for(int j = 0; j <= n; j++)
			for(int i = 0; i <= n; i++)
				f[j][i] = g[j][i] = 0ll;
		int all = f[0][0] = g[0][0] = 1ll;
		for(int j = 1; j <= n; j++){
			int A = k * (max(d[j], k - 1) - k + 1);  // 必亮。
			int B = (min(d[j], k - 2) + 1) * (min(d[j], k - 2) + 2) / 2 + (max(d[j], k) - k) * (max(d[j], k) - k + 1) / 2;  // 必暗。
			int C = k * (d[j] >= k - 1);  // 随意。
			for(int i = 0; i <= n; i++)
				f[j][i] = (f[j - 1][i] * (B + C) + (i ? f[j - 1][i - 1] * A : 0)) % mod, 
				g[j][i] = (g[j - 1][i] * (A + C) + (i ? g[j - 1][i - 1] * B : 0)) % mod;
			MUL(all, A + B + C);
		}
		for(int a = k + 1; a <= n; a++) ADD(all, mod - f[n][a]);
		for(int b = n - k + 1; b <= n; b++) ADD(all, mod - g[n][b]);
		ADD(sum, all);
	}
	cout << sum * ans % mod;
	return 0;
}

P11149 [THUWC 2018] 城市规划

简述就是树上求有至多两种颜色的连通块数量。我当然是不会了,但是 Air 切了。

树、计数、连通块,可以考虑树形 DP。钦定树以 \(1\) 为根。为防止计重,令一个连通块只在其根节点处被计算。

考虑以点 \(u\) 为根的子树,由于要求连通块的颜色种类至多为二,其中一个必然是 \(a_u\),另一个写进 DP 状态里。

那么 DP 状态就很好想到了。设 \(dp_{u, c}\) 表示以 \(u\) 为根、除了 \(a_u\) 的另一种颜色为 \(c\) 的连通块数。特别的,当 \(c = a_u\) 时表示只有一种颜色。

状态数肯定是爆空间了,那就先写出来 \(O(n^2)\) 的暴力转移吧。

  • 若 \(a_u \ne a_v\),发现只可能对 \(dp_{u, a_v}\) 产生贡献。\(dp'_{u, a_v} \leftarrow dp_{u, a_v} + (dp_{u, a_u} + dp_{u, a_v}) \times (dp_{v, a_v} + dp_{v, a_u})\)。

  • 若 \(a_u = a_v\),枚举 \(x \ne a_u\)。\(dp'_{u, x} \leftarrow dp_{u, x} \times (1 + dp_{v, x} + dp_{v, a_v}) + dp_{u, a_u} \times dp_{v, x}\)。注意 \(dp_{u, a_u} \times dp_{v, a_v}\) 不能贡献到 \(dp'_{u, x}\) 上,而是应该贡献到 \(dp'_{u, a_u}\) 上,即 \(dp_{u, a_u} = dp_{u, a_u} \times (1 + dp_{v, a_v})\)。

初值显然是 \(dp_{u, a_u} = 1\),表示只有 \(u\) 的连通块。最后对答案贡献 \(\sum\limits_{x = 1}^n dp_{u, x}\)。

$O(n^2)$ 转移代码
int dp[5005][5005];
void dfs(int u, int father){
	dp[u][a[u]] = 1;
	for(int i = head[u]; i; i = o[i].nxt){
		int v = o[i].ver;
		if(v != father){
			dfs(v, u);
			if(a[u] != a[v]){
				int A = dp[u][a[u]], B = dp[u][a[v]];
				int C = dp[v][a[v]], D = dp[v][a[u]];
				dp[u][a[v]] = (B + (A + B) * (C + D)) % mod;
			}
			else{
				int A = dp[u][a[u]], B = dp[v][a[v]];
				for(int x = 1; x <= n; x++) if(x ^ a[u]){
					int U = dp[u][x], V = dp[v][x];
					dp[u][x] = (U * (1 + V + B) + A * V) % mod;
				}
				dp[u][a[u]] = A * (1 + B) % mod;
			}
		}
	}
	for(int x = 1; x <= n; x++) if(dp[u][x]) ADD(ans, dp[u][x]);
}

对于 \(a_u \ne a_v\) 是 \(O(1)\) 转移,但是对于 \(a_u = a_v\) 的 \(O(n)\) 转移要怎么优化呢?

写成代码中的形式会比较自然地想到线段树合并。\(a_u \ne a_v\) 时,进行单点查询、单点修改;\(a_u = a_v\) 时,\(A, B\) 单点查询,\(dp_{u, x \ne a_u}\) 线段树合并,\(dp_{u, a_u}\) 单点修改。

难点在于合并,当 \(u\) 在这段区间的 \(dp\) 为空时,即代码中的 \(U = 0\),有 \(dp'_{u, x} \leftarrow dp_{u, a_u} \times dp_{v, x}\)。当 \(v\) 在这段区间的 \(dp\) 为空时,即代码中的 \(V = 0\),有 \(dp'_{u, x} \leftarrow dp_{u, x} \times (1 + dp_{v, a_v})\)。当都不为空时,继续递归,到叶子节点时就正常求 \(dp\) 值即可。

最后对答案的贡献就是全局和。因此线段树需要维护区间和、支持区间乘。

此外,建议先写一下 \(O(n^2)\) 的暴力 DP,然后对照着改成线段树合并。

点击查看代码
#define int long long
const int N = 500005, mod = 998244353;
int n, ans, a[N];
struct SON{
	int ver, nxt;
}o[N << 1];
int tot, head[N];
inline void add_edge(int u, int v){
	o[++tot] = SON{v, head[u]};
	head[u] = tot;
}
inline void ADD(int &x, int y){
	(x += y) >= mod && (x -= mod);
}
inline void MUL(int &x, int y){
	(x *= y) >= mod && (x %= mod);
}
int dp[5005][5005];
void dfs(int u, int father){
	dp[u][a[u]] = 1;
	for(int i = head[u]; i; i = o[i].nxt){
		int v = o[i].ver;
		if(v != father){
			dfs(v, u);
			if(a[u] != a[v]){
				int A = dp[u][a[u]], B = dp[u][a[v]];
				int C = dp[v][a[v]], D = dp[v][a[u]];
				dp[u][a[v]] = (B + (A + B) * (C + D)) % mod;
			}
			else{
				int A = dp[u][a[u]], B = dp[v][a[v]];
				for(int x = 1; x <= n; x++) if(x ^ a[u]){
					int U = dp[u][x], V = dp[v][x];
					dp[u][x] = (U * (1 + V + B) + A * V) % mod;
				}
				dp[u][a[u]] = A * (1 + B) % mod;
			}
		}
	}
	for(int x = 1; x <= n; x++) if(dp[u][x]) ADD(ans, dp[u][x]);
}
struct segment_tree{
	int tot, rt[N];
	struct node{
		int ls, rs;
		int lz = 1;
		int sum;
	}o[N * 50];
	#define ls(x) o[x].ls
	#define rs(x) o[x].rs
	#define lz(x) o[x].lz
	#define sum(x) o[x].sum
	inline void push_down(int x){
		if(lz(x) != 1){
			MUL(lz(ls(x)), lz(x));
			MUL(sum(ls(x)), lz(x));
			MUL(lz(rs(x)), lz(x));
			MUL(sum(rs(x)), lz(x));
			lz(x) = 1;
		}
	}
	inline void push_up(int x){
		sum(x) = sum(ls(x));
		ADD(sum(x), sum(rs(x)));
	}
	void change(int &x, int l, int r, int t, int val){
		if(!x) x = ++tot;
		if(l == r){
			sum(x) = val;
			return ;
		}
		push_down(x);
		int mid = (l + r) >> 1;
		if(t <= mid) change(ls(x), l, mid, t, val);
		else change(rs(x), mid + 1, r, t, val);
		push_up(x);
	}
	int ask(int x, int l, int r, int t){
		if(!x) return 0;
		if(l == r) return sum(x);
		push_down(x);
		int mid = (l + r) >> 1;
		if(t <= mid) return ask(ls(x), l, mid, t);
		else return ask(rs(x), mid + 1, r, t);
	}
	void merge(int &x, int y, int l, int r, int A, int B){
		if(!x){
			x = y;
			MUL(lz(x), A);
			MUL(sum(x), A);
			return ;
		}
		if(!y){
			MUL(lz(x), 1 + B);
			MUL(sum(x), 1 + B);
			return ;
		}
		if(l == r){
			int U = sum(x), V = sum(y);
			sum(x) = (U * (1 + V + B) + A * V) % mod;
			return ;
		}
		push_down(x);
		push_down(y);
		int mid = (l + r) >> 1;
		merge(ls(x), ls(y), l, mid, A, B);
		merge(rs(x), rs(y), mid + 1, r, A, B);
		push_up(x);
	}
}tree;
#define rt(x) tree.rt[x]
void DP(int u, int father){
	tree.change(rt(u), 1, n, a[u], 1);
	for(int i = head[u]; i; i = o[i].nxt){
		int v = o[i].ver;
		if(v != father){
			DP(v, u);
			if(a[u] != a[v]){
				int A = tree.ask(rt(u), 1, n, a[u]), B = tree.ask(rt(u), 1, n, a[v]);
				int C = tree.ask(rt(v), 1, n, a[v]), D = tree.ask(rt(v), 1, n, a[u]);
				tree.change(rt(u), 1, n, a[v], (B + (A + B) * (C + D)) % mod);
			}
			else{
				int A = tree.ask(rt(u), 1, n, a[u]), B = tree.ask(rt(v), 1, n, a[v]);
				tree.merge(rt(u), rt(v), 1, n, A, B);
				tree.change(rt(u), 1, n, a[u], (A * (1 + B)) % mod);
			}
		}
	}
	ans = (ans + tree.sum(rt(u))) % mod;
}
signed main(){
	n = read();
	for(int i = 1; i <= n; i++) a[i] = read();
	for(int i = 1; i < n; i++){
		int u = read(), v = read();
		add_edge(u, v), add_edge(v, u);
	}
	// dfs(1, 0);
	DP(1, 0);
	print(ans);
	return 0;
}

P15762 [JAG 2025 Summer Camp #1] Inside Yamanote

下文所有变量的角标都对 \(n\) 取模,如 \(i + 1\) 代表 \((i + 1) \bmod n\)。

设首都为 \(pos\),每个点到首都的距离为 \(d_i\),若满足 \(|d_{i + 1} - d_i| \le l_i\) 且 \(d_i \ge 0\) 且存在 \(d_i = 0\),则必然存在一种合法的建边方案。后两个条件分别代表距离非负、存在首都;第一个条件是因为可以让 \(pos\) 向 \(i(i \ne pos)\) 连边,边权为 \(d_i\),当且仅当满足 \(|d_{i + 1} - d_i| \le l_i\) 时,\(pos, i, i + 1\) 在几何平面上构成三角形(或三点共线),则 \(pos\) 到 \(i\) 的距离为 \(d_i\)。

最后要求的声誉,是所有 \(d_{X_i} - d_{Y_i}\) 的总和。考虑每个 \(d_i\) 的贡献,必然是一个系数乘上 \(d_i\),设其为 \(s_i\)。那么每次让 \(s_{X_i}\) 加一,\(s_{Y_i}\) 减一即可。由于每次都加一个、减一个,必然有 \(\sum\limits_{i = 0}^{n - 1} s_i = 0\)。

则答案为:\(\sum\limits_{i = 0}^{n - 1} s_i \times d_i\)。

然后就是人类智慧了。发现对于一种只满足 \(|d_{i + 1} - d_i| \le l_i\) 的 \(d_i\) 方案,必然可以令其变成合法方案(即变得同时满足之前所说的三个条件)。因为令 \(c = -\min d_i\),令全体 \(d_i\) 加上 \(c\),则此时最小的 \(d_i\) 变成 \(0\),其余 \(d_i\) 同样 \(\ge 0\),同时仍然满足 \(|d_{i + 1} - d_i| \le l_i\)(因为都加上同一个值,差还是不变)。

于是可以抛开首都,\(d_i\) 方案只需要满足 \(|d_{i + 1} - d_i| \le l_i\),求 \(\sum\limits_{i = 0}^{n - 1} s_i \times d_i\) 的最大值。

\(d_i\) 还是有点难入手。令 \(w_i = d_{i + 1} - d_i\),则 \(|w_i| \le l_i \Longleftrightarrow -l_i \le w_i \le l_i\)。抛开 \(d_i\),只构造 \(w_i\) 的合法方案,仍然有对应的 \(d_i\) 方案。因此可以从 \(w_i\) 入手。显然还需要有 \(\sum\limits_{i = 0}^{n - 1} w_i = 0\),因为要满足环状。

推式子:

\[\begin{aligned} &\sum\limits_{i = 0}^{n - 1} s_i \times d_i\\ =& s_0 \times d_0 + \sum\limits_{i = 1}^{n - 1} s_i \times (d_0 + \sum\limits_{j = 0}^{i - 1} w_j)\\ =& d_0 \times \sum\limits_{i = 0}^{n - 1} s_i + \sum\limits_{i = 1}^{n - 1} s_i \times \sum\limits_{j = 0}^{i - 1} w_j\\ =& \sum\limits_{j = 0}^{n - 1} w_j \times \sum\limits_{i = j + 1}^{n - 1} s_i \end{aligned} \]

设 \(a_j = \sum\limits_{i = j + 1}^{n - 1} s_i\),\(a_i\) 是不变的。则答案为 \(\sum\limits_{i = 0}^{n - 1} w_i \times a_i\)。

因此,问题转化为:求满足 \(-l_i \le w_i \le l_i\) 且 \(\sum\limits_{i = 0}^{n - 1} w_i = 0\) 的 \(w_i\) 方案中,\(\sum\limits_{i = 0}^{n - 1} w_i \times a_i\) 的最大值。

先抛开 \(\sum\limits_{i = 0}^{n - 1} w_i = 0\) 的限制。由于 \(a_i\) 有正有负,肯定是贪心地令 \(a_i < 0\) 的 \(w_i = -l_i,a_i \ge 0\) 的 \(w_i = l_i\),设该答案为 \(A\)。

然后考虑这个限制。设 \(neg\) 表示 \(a_i < 0\) 的 \(-w_i\) 之和,\(pos\) 表示 \(a_i \ge 0\) 的 \(w_i\) 之和。

若 \(a_i < 0\) 的 \(w_i\) 加一,会使答案减少 \(-a_i\),该 \(w_i\) 可以从 \(-l_i\) 加到 \(l_i\),最多加 \(2 \times l_i\) 次。

若 \(a_i \ge 0\) 的 \(w_i\) 减一,会使答案减少 \(a_i\),该 \(w_i\) 可以从 \(l_i\) 减到 \(-l_i\),最多减 \(2 \times l_i\) 次。

如果 \(neg > pos\),希望选择一些 \(a_i < 0\) 的 \(w_i\) 增加一些值,增加的总次数为 \(k = neg - pos\)。

等价于有 \(2 \times l_i\) 个 \(-a_i(a_i < 0)\),前 \(k\) 小的数之和为 \(B\),则答案为 \(A - B\)。

如果 \(neg \le pos\),希望选择一些 \(a_i \ge 0\) 的 \(w_i\) 减少一些值,减少的总次数为 \(k = pos - neg\)。

等价于有 \(2 \times l_i\) 个 \(a_i(a_i \ge 0)\),前 \(k\) 小的数之和为 \(B\),则答案为 \(A - B\)。

开权值线段树,以 \(a_i\) 为下标(或者 \(-a_i\))。线段树上二分求前 \(k\) 小之和即可。注意 \(0 \le a_i \le m\),线段树开到 \(m\) 不是 \(n\)。

点击查看代码
const int N = 200005;
int n, m, q;
int l[N], s[N], a[N];
struct segment_tree{
	int sum[N << 2], cnt[N << 2];
	#define ls x << 1
	#define rs x << 1 | 1
	void add(int x, int l, int r, int t, int c, int val){
		if(l == r){
			sum[x] += c * val;
			cnt[x] += c;
			return ;
		}
		int mid = (l + r) >> 1;
		if(t <= mid) add(ls, l, mid, t, c, val);
		else add(rs, mid + 1, r, t, c, val);
		sum[x] = sum[ls] + sum[rs];
		cnt[x] = cnt[ls] + cnt[rs];
	}
	int ask(int x, int l, int r, int k){
		if(l == r) return min(cnt[x], k) * l;
		int mid = (l + r) >> 1;
		if(k <= cnt[ls]) return ask(ls, l, mid, k);
		else return sum[ls] + ask(rs, mid + 1, r, k - cnt[ls]);
	}
}NEG, POS;
int A, B;
int neg, pos;
inline void update(int i, int k){
	if(a[i] < 0){
		NEG.add(1, 0, m, -a[i], k * 2 * l[i], -a[i]);
		A += k * a[i] * (-l[i]);
		neg += k * l[i];
	}
	else{
		POS.add(1, 0, m, a[i], k * 2 * l[i], a[i]);
		A += k * a[i] * l[i];
		pos += k * l[i];
	}
}
signed main(){
	n = read(), m = read(), q = read();
	for(int i = 0; i < n; i++) l[i] = read();
	for(int i = 0; i < m; i++){
		int x = read(), y = read();
		++s[x], --s[y];
	}
	for(int i = n - 2; i >= 0; i--) a[i] = a[i + 1] + s[i + 1];
	for(int i = 0; i < n; i++) update(i, 1);
	for(; q; q--){
		int i = read();
		update(i, -1);
		l[i] = read();
		update(i, 1);
		if(neg > pos) B = NEG.ask(1, 0, m, neg - pos);
		else B = POS.ask(1, 0, m, pos - neg);
		print(A - B), putchar('\n');
	}
	return 0;
}

十一校联考 asdfz 2026 NOIP 模拟赛 #C. 深搜

点击查看题面

对于一个有根树,我们记 \(f(v)\) 为对以点 \(v\) 为根的子树 DFS(可以以任意顺序访问子节点)能够得到的本质不同的“节点遍历序列去除非前缀最大值后形成的序列”的数量对 \(10^{9} + 7\) 取模后的结果。

给定一个 \(n\) 个点的,编号从 \(1\) 开始且以点 \(r\) 为根的树。共 \(t\) 组测试数据:

  • 当 \(o = 1\) 时,请求出 \(f(r)\);

  • 当 \(o = 2\) 时,请求出 \(f(1), f(2), \dots, f(n)\)。

对于所有数据,\(o \in \{1,2\}, t \le 10^{4}, n \le 5 \times 10^{5}, \sum n \le 2 \times 10^{6}\)。

\(f(u)\) 本质上是对以点 \(u\) 为根的子树 DFS(可以以任意顺序访问子节点)能够得到的本质不同的“前缀最大值序列”的数量。

设 \(\text{subtree}(u)\) 表示以点 \(u\) 为根的子树内的点编号的集合,\(sz_u\) 表示 \(\text{subtree}(u)\) 的大小。

设 \(m_u = \max\limits_{v \in \text{subtree}(u)} v\) 表示 \(\text{subtree}(u)\) 的最大值。若 \(m_u\) 没有作为前缀最大值,则 \(\text{subtree}(u)\) 内不会有任何点编号作为前缀最大值。

设 \(V = \{v_1, v_2, \dots, v_k\}\) 为点 \(u\) 的子节点编号集合。进入点 \(v_i\) 并遍历完 \(\text{subtree}(v_i)\) 后,若再进入 \(m_{v_j} < m_{v_i}\) 的点 \(v_j\),则 \(m_{v_j}\) 不会作为前缀最大值,\(\text{subtree}(v)\) 不会对 \(f(u)\) 产生贡献。因此,将 \(v_i\) 按 \(m_{v_i}\) 从大到小排序,依次考虑 \(v_i\)。若要让 \(\text{subtree}(v_i)\) 对 \(f(u)\) 产生贡献,需要将 \(v_i\) 放置在最靠前的位置,即优先进入点 \(v_i\)。

然后考虑求贡献。比较显然的 DP 状态是,设 \(f_{u,i}\) 表示之前的前缀最大值为 \(i\),然后进入点 \(u\),\(\text{subtree}(u)\) 内部的本质不同的“前缀最大值序列”的数量。那么,按照如上所述,设考虑到 \(v\),对于 \(i \ge u\) 的转移:\(f'_{u, i} = f_{u, i} + f_{v, i} \times f_{u, m_v}\);对于 \(i < u\) 的转移:\(f'_{u, i} = f'_{u, u}\)。暴力 DP 是 \(O(n^2)\) 的。答案为 \(f_{u, u}\)。

设 \(g_{u, i} = f_{u, i} - f_{u, i + 1}\),对于 \(i \ge u\) 的转移:

\[\begin{aligned} g'_{u, i} &= f'_{u, i} - f'_{u, i + 1}\\ &= f_{u, i} + f_{v, i} \times f_{u, m_v} - f_{u, i + 1} - f_{v, i + 1} \times f_{u, m_v}\\ &= (f_{u, i} - f_{u, i + 1}) + (f_{v, i} - f_{v, i + 1}) \times f_{u, m_v}\\ &= g_{u, i} + g_{v, i} \times f_{u, m_v}\\ &= g_{u, i} + g_{v, i} \times \sum\limits_{j = m_v}^n g_{u, j} \end{aligned} \]

对于 \(i < u\) 的转移:\(g'_{u, i} = 0\)。

这样,\(g_u\) 中有值的位置不会超过 \(sz_u\) 个,就可以上线段树合并了,需要支持区间求和(求 \(\sum\limits_{j = m_v}^n g_{u, j}\))、区间乘(乘 \(\sum\limits_{j = m_v}^n g_{u, j}\))、区间推平(推平为 \(0\),改为区间乘 \(0\) 也行)。答案为 \(\sum g_{u, i}\)。

点击查看代码
#define int long long
inline int read(){
	int x;
	cin >> x;
	return x;
}
const int N = 500005, mod = 1000000007;
int test, type, T;
int n, r;
struct SON{
	int ver, nxt;
}o[N << 1];
int tot, head[N];
inline void add_edge(int u, int v){
	o[++tot] = SON{v, head[u]};
	head[u] = tot;
}
int m[N];
void dfs(int u, int father){
	m[u] = u;
	for(int i = head[u]; i; i = o[i].nxt){
		int v = o[i].ver;
		if(v != father) dfs(v, u), m[u] = max(m[u], m[v]);
	}
}
inline void ADD(int &x, int y){
	(x += y) >= mod && (x -= mod);
}
inline void MUL(int &x, int y){
	(x *= y) >= mod && (x %= mod);
}
struct segment_tree{
	int node_tot, root[N];
	struct node{
		int ls, rs;
		int sum, lz;
	}o[N * 20];
	#define ls(x) o[x].ls
	#define rs(x) o[x].rs
	#define sum(x) o[x].sum
	#define lz(x) o[x].lz
	inline void push_down(int x){
		if(lz(x) != 1){
			if(ls(x)) MUL(sum(ls(x)), lz(x)), MUL(lz(ls(x)), lz(x));
			if(rs(x)) MUL(sum(rs(x)), lz(x)), MUL(lz(rs(x)), lz(x));
			lz(x) = 1;
		}
	}
	inline void push_up(int x){
		if(ls(x)) sum(x) = sum(ls(x));
		else sum(x) = 0;
		if(rs(x)) ADD(sum(x), sum(rs(x)));
	}
	void build(int &x, int l, int r, int t, int val){
		x = ++node_tot;
		sum(x) = val, lz(x) = 1;
		ls(x) = rs(x) = 0;
		if(l == r) return ;
		int mid = (l + r) >> 1;
		if(t <= mid) build(ls(x), l, mid, t, val);
		else build(rs(x), mid + 1, r, t, val);
	}
	int ask(int x, int l, int r, int tl, int tr){
		if(!x) return 0;
		if(tl <= l && r <= tr) return sum(x);
		push_down(x);
		int mid = (l + r) >> 1, res = 0;
		if(tl <= mid) ADD(res, ask(ls(x), l, mid, tl, tr));
		if(tr > mid) ADD(res, ask(rs(x), mid + 1, r, tl, tr));
		return res;
	}
	void mul(int x, int l, int r, int tl, int tr, int val){
		if(!x) return ;
		if(tl <= l && r <= tr){
			MUL(sum(x), val);
			MUL(lz(x), val);
			return ;
		}
		push_down(x);
		int mid = (l + r) >> 1;
		if(tl <= mid) mul(ls(x), l, mid, tl, tr, val);
		if(tr > mid) mul(rs(x), mid + 1, r, tl, tr, val);
		push_up(x);
	}
	void del(int x, int l, int r, int tl, int tr){
		if(!x) return ;
		if(tl <= l && r <= tr){
			sum(x) = 0, lz(x) = 1;
			ls(x) = rs(x) = 0;
			x = 0;
			return ;
		}
		push_down(x);
		int mid = (l + r) >> 1;
		if(tl <= mid) del(ls(x), l, mid, tl, tr);
		if(tr > mid) del(rs(x), mid + 1, r, tl, tr);
		push_up(x);
	}
	void merge(int &x, int y, int l, int r){
		if(!x){
			x = y;
			return ;
		}
		if(!y) return ;
		if(l == r){
			ADD(sum(x), sum(y));
			return ;
		}
		push_down(x);
		push_down(y);
		int mid = (l + r) >> 1;
		merge(ls(x), ls(y), l, mid);
		merge(rs(x), rs(y), mid + 1, r);
		push_up(x);
	}
	#undef ls
	#undef rs
	#undef sum
	#undef lz
}tree;
#define root(x) tree.root[x]
int ans[N];
void DP(int u, int father){
	vector<int> son;
	for(int i = head[u]; i; i = o[i].nxt){
		int v = o[i].ver;
		if(v != father) DP(v, u), son.push_back(v);
	}
	if(u == m[u]){
		tree.build(root(u), 1, n, u, 1);
		ans[u] = 1;
		return ;
	}
	sort(son.begin(), son.end(), [](const int &i, const int &j){
		return m[i] > m[j];
	});
	for(auto v : son){
		if(v == son[0]) root(u) = root(v);
		else{
			int val = tree.ask(root(u), 1, n, m[v], n);
			tree.mul(root(v), 1, n, 1, n, val);
			tree.merge(root(u), root(v), 1, n);
		}
	}
	if(u - 1) tree.del(root(u), 1, n, 1, u - 1);
	ans[u] = tree.ask(root(u), 1, n, 1, n);
}
inline void solve(){
	n = read(), r = read();
	tot = 0, tree.node_tot = 0;
	for(int u = 1; u <= n; u++) head[u] = root(u) = 0;
	for(int i = 1; i < n; i++){
		int u = read(), v = read();
		add_edge(u, v), add_edge(v, u);
	}
	dfs(r, 0);
	DP(r, 0);
	if(type == 1) cout << ans[r] << '\n';
	else{
		for(int i = 1; i < n; i++) cout << ans[i] << ' ';
		cout << ans[n] << '\n';
	}
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr), cout.tie(nullptr);
	for(test = read(), type = read(), T = read(); T; T--) solve();
	return 0;
}
posted @ 2026-09-23 18:19  manboo  阅读(20)  评论(0)    收藏  举报