刷CF #2100

CF207B3 Military Trainings

题目描述

\(n\) 个坦克,从 \(1\)\(n\) 编号,它们要进行消息传输。

每一次传输如下,列表中第一个坦克将信息传输到列表中的某个坦克。接收到该消息的坦克将其进一步发送到列表后的某个坦克。该过程将继续进行,直到最后一个坦克收到消息。可能不是列表中的所有坦克都会收到消息,但列表中的最后一个坦克必须收到消息。

当最后一个坦克收到消息时,它将挪到第一个位置,并发送一条消息。当信息到达最后一个坦克时,该坦克移动到列的开头,并将下一条信息发送到列表的末尾,依此类推。因此,当列中的坦克返回到其原始顺序时,练习就完成了。

在两个坦克之间传输信息需要一秒钟,然而,并非总是一个坦克可以将信息传输给另一个坦克。让我们考虑列中的两个坦克,使它们中的第一个是从开始计数的列中的第 \(i\) 个,第二个是列中的 \(j\) 个,并假设第二个坦克的编号为 \(x\)。然后,如果 \(i<j\)\(i\ge j-a_{x}\) 则可以传输。

你会得到坦克的数量,以及所有坦克的信息接收半径。您必须帮助 Smart Beaver 并组织消息传输,使所有消息的总传输时间尽可能短。

题解

首先我们容易得到 dp 转移方程: \(dp_i = \min\limits_{j = \max(1, i - a_i)}^{i - 1}dp_j + 1\),直接暴力转移是 \(\mathcal{O}(n^3)\) 的。

然后我们发现,对于每个 \(x\),如果起点不能直接发送消息给它,从 \(y = x - a_x \sim x - 1\)\(y - a_y\) 最小的 \(y\) 接收消息肯定是最优的。我们可以用 ST 表预处理 + 查询,时间复杂度优化到 \(\mathcal{O}(n^2)\)

接着对于每个 \(x\)\(y\),暴力的方法是一个一个找。但是这样太慢了,可以通过倍增来优化,预处理出 \(x\)\(2^k\) 次方步跳到的位置,时间复杂度优化到 \(\mathcal{O}(n \log n)\),可以通过。

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int M = 5e5 + 5;
int a[M], st[M][22], nxt[M][22], lg[M], n;
int query(int l, int r) {
	int k = lg[r - l + 1];
	int lf = st[l][k], rt = st[r - (1 << k) + 1][k];
	return a[lf] <= a[rt] ? lf : rt;
}
signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n;
	for (int i = 1; i <= n; i++) cin >> a[i], a[i + n] = a[i];
	n <<= 1;
	for (int i = 1; i <= n; i++) {
		a[i] = max(i - a[i], 1ll);
		st[i][0] = i;
	}
	for (int i = 2; i <= n; i++) lg[i] = lg[i / 2] + 1;
	for (int j = 1; (1 << j) <= n; j++) {
		for (int i = 1; i + (1 << j) - 1 <= n; i++) {
			int lf = st[i][j - 1];
			int rt = st[i + (1 << (j - 1))][j - 1];
			st[i][j] = a[lf] <= a[rt] ? lf : rt;
		}
	}
	for (int i = 1; i <= n; i++) nxt[i][0] = query(a[i], i);
	for (int j = 1; j <= 20; j++)
		for (int i = 1; i <= n; i++)
			nxt[i][j] = nxt[nxt[i][j - 1]][j - 1];
	int ans = 0;
	for (int i = 1; i <= n / 2; i++) {
		int j = i + n / 2 - 1;
		if (a[j] <= i) {
			ans++;
			continue;
		}
		for (int k = 20; k >= 0; k--)
			if (a[nxt[j][k]] > i) {
				ans += (1ll << k);
				j = nxt[j][k];
			}
		ans += 2;
	}
	cout << ans;
	return 0;
}

CF1252G Performance Review

题目描述

一个公司有 n 个员工,每个员工有一个能力值 \(a_i\), 每个 \(a_i\) 都不一样。公司采取末位淘汰制,我们考虑 m 年,每年年底都会补充 \(r_i\) 个新员工,每个新员工的能力值被表示为 \(b_{i_j}\), 即第 \(i\) 年新加入的第 \(j\) 个员工的能力值。每年,能力值最靠后的 \(r_i\) 个员工会被炒鱿鱼,取而代之的是新加入的这 \(r_i\) 个员工。你是第 \(1\) 号员工,你的能力值是 \(a_1\)

还有 \(q\) 次操作,每次操作的描述形如 \((x_i,y_i,z_i)\), 表示把第 \(x_i\) 年加入的第 \(y_i\) 个员工的能力值改为 \(z_i\)。 现在对于每个操作输出 \(0\)\(1\),表示如果进行完这个操作,\(m\) 年之后你会不会被炒鱿鱼。注意,操作是永久性的。

题解

注意到所有能力值小于 \(a_1\) 的员工显然先被炒鱿鱼。所以每年能力值小于 \(a_1\) 的人数只有 \(> r_i\) 时第一个人才会被炒。

我们令 \(f_i\) 表示第 \(i\) 年加入新员工之后能力小于第一个人的人数,对于第 \(i\) 年,以下信息可以预处理得到:

  • 淘汰前,有 \(f_{i - 1}\) 个能力小于第一个人。
    \(f_{i - 1} < r_i\),则能力小于第一个人的员工不足 \(r_i\) 人,\(1\) 号必然被淘汰;
    \(f_{i - 1} \ge r_i\),则淘汰的全是能力小于第一个人的员工,\(1\) 号存活。
  • 淘汰后,剩余 \(f_{i - 1} - r_i\) 个能力小于第一个人的员工。
  • 加入 \(r_i\) 名新员工,假设其中有 \(k_i\) 人能力小于第一个人,可以推出 \(f_i = f_{i - 1} + k_i - r_i\)

所以第一个人不被炒的条件为 \(\forall i \in [1, m], ~f_i - 1 \ge r_i\)。我们令 \(c_i = f_{i - 1} - r_i\),则只需要检查 \(c_i\) 的最小值是否 \(\ge 0\)

那么现在我们来考虑修改操作。显然这会影响 \(k_x\) 的取值,并且会影响 \(f_{x \sim m}\) 的取值。所以我们需要一个数据结构维护全局最小值和区间加。显然线段树维护即可。

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int M = 1e5 + 5;
int a[M], c[M], n, m, q;
vector<int>vec[M];
struct node {
	int laz, mn;
} Tree[M << 2];
void push_up(int p) {
	Tree[p].mn = min(Tree[p << 1].mn, Tree[p << 1 | 1].mn);
}
void down(int p) {
	int lz = Tree[p].laz;
	if(lz){
		Tree[p << 1].laz += lz;
		Tree[p << 1].mn += lz;
		Tree[p << 1 | 1].laz += lz;
		Tree[p << 1 | 1].mn += lz;
		Tree[p].laz = 0;
	}
}
void build(int p, int l, int r) {
	if (l == r) {
		Tree[p].laz = 0;
		Tree[p].mn = c[l];
		return;
	}
	int mid = (l + r) >> 1;
	build(p << 1, l, mid), build(p << 1 | 1, mid + 1, r);
	push_up(p);
}
void upd(int p, int l, int r, int ql, int qr, int v){
	if(ql <= l && r <= qr){
		Tree[p].laz += v;
		Tree[p].mn += v;
		return;
	}
	down(p);
	int mid = (l + r) >> 1;
	if(ql <= mid) upd(p << 1, l, mid, ql, qr, v);
	if(qr > mid) upd(p << 1 | 1, mid + 1, r, ql, qr, v);
	push_up(p);
}
void sol() {
	int x, y, z;
	cin >> x >> y >> z;
	int re = vec[x][y - 1];
	int d = (z < a[1]) - (re < a[1]);
	if (d != 0 && x + 1 <= m) upd(1, 1, m, x + 1, m, d);
	vec[x][y - 1] = z;
	cout << (Tree[1].mn >= 0) << '\n';
}
signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m >> q;
	for (int i = 1; i <= n; i++) cin >> a[i];
	int st = 0;
	for (int i = 2; i <= n; i++) st += (a[i] < a[1]);
	int cur = st;
	for (int i = 1, siz; i <= m; i++) {
		cin >> siz;
		int k = 0;
		for (int j = 1, x; j <= siz; j++) {
			cin >> x;
			vec[i].push_back(x);
			if (x < a[1]) ++k;
		}
		c[i] = cur - siz;
		cur = cur + k - siz;
	}
	build(1, 1, m);
	while (q--) sol();
	return 0;
}

CF1207F Remainder Problem

题目描述

给你一个长度为 \(500000\) 的序列,初值为 \(0\) ,你要完成 \(q\) 次操作,操作有如下两种:

  1. 1 x y : 将下标为 \(x\) 的位置的值加上 \(y\)
  2. 2 x y : 询问所有下标模 \(x\) 的结果为 \(y\) 的位置的值之和

题解

根号分治。

对于操作 \(2\),如果 \(x \ge \sqrt x\) 就直接暴力枚举就可以了,对于 \(x < \sqrt x\) 我们记 \(f_{i,j}\) 表示下表模 \(i\)\(j\) 的位置的和,直接预处理即可。对于操作 \(1\) 直接暴力在 \(f\) 数组上面更新就可以了。

Code

#include<bits/stdc++.h>
using namespace std;
const int N = 5e5;
int ans[710][710], a[N + 5];
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	int m;
	cin >> m;
	int siz = sqrt(N);
	while (m--) {
		int op, x, y;
		cin >> op >> x >> y;
		if (op == 1) {
			for (int j = 1; j <= siz; ++j) {
				if (x >= j) ans[j][x % j] += y;
				else ans[j][x] += y;
			}
			a[x] += y;
		} else {
			if (x <= siz) {
				cout << ans[x][y] << '\n';
			} else {
				int sum = 0;
				for (int i = y; i <= N; i += x) sum += a[i];
				cout << sum << '\n';
			}
		}
	}
	return 0;
}

CF2029E Common Generator

题目描述

对于两个整数 \(x\)\(y\)\(x,y\ge 2\)),如果且仅如果可以通过若干次(可能为零次)以下操作将 \(x\) 变换为 \(y\),则称 \(x\)\(y\) 的一个生成器

  • 选取一个 \(x\) 的约数 \(d\)\(d\ge 2\)),然后将 \(x\) 增加 \(d\)

现在,Kevin 给你一个长度为 \(n\) 的数组 \(a\),其中包含两两不同的整数(\(a_i \ge 2\))。

你需要寻找一个整数 \(x\ge 2\),使得对每个 \(1\le i\le n\)\(x\) 都是 \(a_i\) 的生成器;如果不存在这样的整数,则输出 \(-1\)

题解

首先注意到 \(2\) 开始怎么做,首先显然 \(2k\) 都可做了。变成 \(6\) 以后,所有的 \(3k\) 都可做了。同理,变形成 \(2p\) 以后,所有的 \(kp\) 都可做了,这里的 \(p\) 是任意质数。于是只要目标数字不是质数都可以通过 \(2\) 获得。

由于 \(x \to x + d\) 需要保证 \(d \mid x\),于是必然有 \(d \mid x + d\),所以质数最多一个,因为必定从这个数出发去构造其他数字。

首先对于一个质数 \(p\),第一步就是变成 \(2p\),所以如果数列中有一个数 \(\in (p, 2p)\) 就无解。然后 \(\ge 2p\) 的偶数就可以构造出来了。

然后我们假设要合成 \(q\),其中 \(q\) 是奇数,我们必须得获得一个不小于 \(2p\) 的数字满足其与 \(q\) 的最大公约数不为 \(1\)。假设这个数为 \(g\),那么 \(q-g\) 必定是偶数。那么我们就可以先不断加 \(2\) 得到 \(q-g\),然后再操作一步得到 \(q\)。做完了。

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int M = 1e5 + 5;
const int N = 4e5 + 5;
int a[M], prime[N], ck[N], tot, lp[N];
void init(){
	memset(ck, 1, sizeof(ck));
	ck[0] = ck[1] = 0;            
	tot = 0;
	for(int i = 2; i < N; i++){
		if(ck[i]){
			prime[tot++] = i;
			lp[i] = i;            
		}
		for(int j = 0; j < tot && i * prime[j] < N; j++){
			ck[i * prime[j]] = 0;
			lp[i * prime[j]] = prime[j];
			if(i % prime[j] == 0) break;
		}
	}
}
void sol(){
	int n;
	cin >> n;
	vector<int> vec;
	for(int i = 1; i <= n; i++){
		cin >> a[i];
		int x = a[i];
		if(x % 2 == 1 && ck[x]) vec.push_back(x);  
	}
	if(vec.size() > 1){
		cout << "-1\n";
		return;
	}
	if(vec.empty()){
		cout << "2\n";
		return;
	}
	int p = vec[0];
	for(int i = 1; i <= n; i++){
		if(a[i] < p){
			cout << "-1\n";
			return;
		}
	}
	for(int i = 1; i <= n; i++){
		int x = a[i];
		if(x == p) continue;
		if(x % 2 == 0){
			if(x < 2 * p){
				cout << "-1\n";
				return;
			}
		} else {
			int d = lp[x];          
			if(x - d < 2 * p){
				cout << "-1\n";
				return;
			}
		}
	}
	cout << p << "\n";
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	init();          
	int T;
	cin >> T;
	while(T--) sol();
	return 0;
}

CF455C Civilization

题目描述

给出一个由 \(n\) 个点,\(m\) 条边组成的森林,有 \(q\) 组询问。

  1. 给出点 \(x\),输出点 \(x\) 所在的树的直径。
  2. 给出点 \(x,y\),如果 \(x,y\) 在同一棵树中则忽略此操作。否则选择任意两点 \(u,v\),使得 \(u\)\(x\) 在同一棵树中且 \(v\)\(y\) 在同一棵树中。将 \(u,v\) 之间连一条边,使得连边后的到的新树的直径最小。

题解

结论题。

首先我们发现要维护森林,所以考虑并查集。

然后我们思考连接两棵树的时候,假设两棵树的根为 \(u\)\(v\),则 \(ans_v = \max(ans_v, ans_u, 1 + \lceil\frac {ans_v}2\rceil + \lceil\frac {ans_u}2\rceil)\)。原因是要想直径最短,我们选择加边的点一定要在直径上,因为其他的点走到直径还要一段距离,从而增长了路径。那么直径就被选择的点分成了两段。因为我们要最小化较长的那一段,所以要让选择的点尽量靠近直径的中点。最后的答案就是直径长度的一半向上取整。

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int M = 3e5 + 5;
vector<int>vec[M];
int vis[M], d[M], fa[M], res[M], maxn = 0;
int find(int x) {
	return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void merge(int x, int y) {
	int f1 = find(x), f2 = find(y);
	if(f1 != f2) {
		fa[f1] = f2;
		res[f2] = max(max(res[f2], res[f1]), (res[f2] + 1) / 2 + (res[f1] + 1) / 2 + 1);
	} 
}
void unite(int x, int y) {
	int f1 = find(x), f2 = find(y);
	if (f1 != f2) fa[f1] = f2;
}
void dfs(int u, int fa) {
	vis[u] = 1;
	for(auto v : vec[u]) {
		if(v == fa) continue;
		dfs(v, u);
		maxn = max(maxn, d[v] + d[u] + 1);
		d[u] = max(d[u], d[v] + 1);
	}
}
int read() {
	int sgn = 1, ans = 0;
	char c = getchar();
	while (!isdigit(c)) {
		if (c == '-') sgn = -sgn;
		c = getchar();
	}
	while (isdigit(c)) ans = ans * 10 + c - '0', c = getchar();
	return sgn * ans;
}
signed main() {
	int n = read(), m = read(), q = read();
	for(int i = 1; i <= n; i++) fa[i] = i;
	for (int i = 1; i <= m; i++) {
		int u = read(), v = read();
		vec[u].push_back(v);
		vec[v].push_back(u);
		unite(u, v);
	}
	for(int i = 1; i <= n; i++) {
		if(!vis[i]){	
			maxn = 0;
			dfs(i, 0);
			res[find(i)] = maxn;
		}
	}
	while(q--) {
		int op = read(), x = read();
		if(op == 1) cout << res[find(x)] << '\n';
		else {
			int y = read();
			merge(x, y);
		}
	}
	return 0;
}

CF1912B Blueprint for Seating

题目描述

形式化地说,一种布局是一个正整数序列 $ a_1, a_2, \ldots, a_{k+1} $,其中第 $ i $ 组有 $ a_i $ 个座位,共有 $ k $ 条过道,每条过道位于相邻的两组之间。也就是说,过道必须夹在两个座位之间,无法紧挨窗户,也不能有两条相邻的过道。一种座位布局的不便程度定义为一个排中每个座位到最近过道的距离总和。座位到过道的距离是指它们之间的座位数。

公司希望设计一个单排有 $ n $ 个座位和 $ k $ 条过道的布局,以达到最小的不便程度。请帮助他们找出所有可能布局中不便程度最小的一种,并计算出所有达到最小不便程度的布局数量,并将其结果对 $ 998,244,353 $ 取模。

题解

首先我们发现挨着窗户的很难处理,所以我们可以想到把靠窗的两块合成一块正常的,于是一个长度为 \(l\) 的块的贡献 \(f(l) = \lfloor\frac{(l-1)^2}{4}\rfloor\)。显然我们每一块都要越平均越好(严谨证明是琴生不等式)。所以第一小问的答案为 \(n \bmod k \times f(\lceil \frac n k\rceil) + (k - n \bmod k) \times f(\lfloor\frac n k \rfloor)\)

接下来考虑第二小问。这里要分两种情况:

  • \(n \le 2k\) 时每一块不是 \(1\) 就是 \(2\),此时两端的组长度只能为 \(1\),所以情况数显然为 \(\binom{k - 1}{n - k - 1}\)

  • \(m = n \bmod 2k\)。此时的最优布局等价于将 \(m\) 个“超出平均”的座位分配到 \(k + 1\) 个特定位置上,情况数为:

    \[\sum\limits_{i=0}^{\lfloor\frac n 2 \rfloor} \binom{k - 1}{i} \binom{k - i - 1}{m - 2i} \]

    其中有 \(i\) 个内部组被分配了 \(2\) 个额外座位,剩余 \(m−2i\) 个额外座位分配给包含两端在内的 \(k−i+1\) 个位置,每个位置至多 \(1\) 个。

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int M = 1e6 + 6;
const int mod = 998244353;
int fac[M], inv[M];
int qpow(int x, int y) {
	int res = 1;
	while(y) {
		if(y & 1) res = res * x % mod;
		x = x * x % mod;
		y >>= 1;
	}
	return res;
}
int read(){
	int sgn = 1, ans = 0;
	char c = getchar();
	while(!isdigit(c)){	
		if(c == '-') sgn = -sgn;
		c = getchar();
	}
	while(isdigit(c)) ans = ans * 10 + c - '0', c = getchar();
	return sgn * ans;
}
void init(int T) {
	fac[0] = inv[0] = 1;
	for(int i = 1; i <= T; i++) fac[i] = fac[i - 1] * i % mod;
	inv[T] = qpow(fac[T], mod - 2);
	for(int i = T; i > 1; i--) inv[i - 1] = inv[i] * i % mod;
}
int f(int x) {
	if(x <= 0) return 0;
	else return (x - 1) * (x - 1) / 4;
}
int C(int x, int y) {
	if(x < y) return 0;
	return fac[x] * inv[y] % mod * inv[x - y] % mod;
}
void sol(){
	int n = read(), k = read();
	int tm = n / k, r = n % k, m = n % (2 * k);
	cout << r * f(tm + 1) + (k - r) * f(tm) << ' ';
	if(n <= 2 * k) {
		cout << C(k - 1, n - k - 1) << '\n';
		return;
	} else {
		int res = 0;
		for(int i = 0; i <= m / 2; i++) {
			res += C(k - 1, i) * C(k - i + 1, m - 2 * i) % mod;
			res %= mod;
		}
		cout << res << '\n';
	}
}
signed main() {
	int T = read();
	init(M - 3);
	while(T--) sol();
	return 0;
}

CF1312E Array Shrinking

题目描述

给你一个长度为 \(n(1 \le n \le 500)\) 的数组 \(a\),每次你可以进行以下两步操作:

  1. 找到 \(i \in [1, n)\),使得 \(a_i = a_{i + 1}\)

  2. 它们 替换为 \(a_i + 1\)

每轮操作之后,显然数组的长度会减小 \(1\),问剩余数组长度的最小值。

题解

很明显的区间 dp。设 \(f_{i,j}\) 表示 \([i,j]\) 合并之后的数是多少,如果无法合并则为 \(-1\)。首先套路地 \(f_{i,i} = a_i\)。然后递推式子显然为 \(f_{i,j} = f_{i,k} + 1\),如果 \(f_{i,k} = f_{k + 1, j} \neq -1\)

然后向最长上升子序列一样令 \(g_i\) 表示前 \(i\) 个数能够合成的最小长度,显然只要区间 \([i,j]\) 合法就有转移 \(g_i = \min(g_i, g_j + 1)\)。答案即为 \(g_n\)

Code

#include <bits/stdc++.h>
using namespace std;
const int N = 505;
int dp[N][N], f[N], a[N];
int read() {
	int sgn = 1, ans = 0;
	char c = getchar();
	while (!isdigit(c)) {
		if (c == '-') sgn = -sgn;
		c = getchar();
	}
	while (isdigit(c)) ans = ans * 10 + c - '0', c = getchar();
	return sgn * ans;
}
int main() {
	int n = read();
	memset(dp, -1, sizeof(dp));
	for (int i = 1; i <= n; i++) a[i] = read(), dp[i][i] = a[i];
	for (int len = 2; len <= n; len++) {
		for (int l = 1; l + len - 1 <= n; l++) {
			int r = l + len - 1;
			for (int k = l; k < r; k++) {
				if (dp[l][k] != -1 && dp[l][k] == dp[k + 1][r]) {
					dp[l][r] = dp[l][k] + 1;
					break;
				}
			}
		}
	}
	f[0] = 0;
	for (int i = 1; i <= n; i++) {
		f[i] = i; 
		for (int j = 1; j <= i; j++) {
			if (dp[j][i] != -1) f[i] = min(f[i], f[j - 1] + 1);
		}
	}
	cout << f[n];
	return 0;
}

CF838B Diverging Directions

题目描述

给定一个有 \(n\) 个点、\(2n-2\) 条边的有向带权图。点编号为 \(1\)\(n\),边编号为 \(1\)\(2n-2\)。这些边可以分为两部分:

  • \(n-1\) 条边形成一棵以 \(1\) 号点为根的有根生成树,所有这些边都从根指向子节点。
  • \(n-1\) 条边均为从节点 \(i\) 到节点 \(1\) 的边,\(2\leq i\leq n\)

你将会得到 \(q\) 组询问。每种询问有两类:

  • \(1\ i\ w\):将第 \(i\) 条边的权值修改为 \(w\)
  • \(2\ u\ v\):输出从点 \(u\) 到点 \(v\) 的最短路径长度。

请你对于每一个第 \(2\) 种类型的询问,输出最短路径的长度。

题解

一道比较好的练手题。

我们令前 \(n-1\) 条边为树边,后 \(n-1\) 条边为回边。

首先我们发现,如果 \(u\)\(v\) 的祖先,那么最短路显然是走树边,最短路为 \(d_v - d_u\),其中 \(d_i\) 表示 \(i\) 到根节点的距离。如果 \(v\)\(u\) 的祖先,那么答案显然为 \(d_v - d_u + \min\limits_{k \in son_u} (d_k + val_k)\),其中 \(val_i\) 表示 \(i\) 的回边的权值。

具体的,我们令 \(c_i = d_i + val_i\),那么如果修改的是树边 \(u \to v\) 的权值,那么对于 \(v\) 以及其子树的 \(c\) 都会改变;如果修改的是回边 \(v \to 1\) 的权值,那么只有 \(c_v\) 会发生变化。所以直接考虑 dfs 序 + 线段树维护 \(c_i\) 的最小值即可。

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int M = 4e5 + 5;
const int inf = 1e18;
int d[M], bk[M], dfn[M], siz[M], tot;
int rk[M], eu[M], ev[M], ew[M], nd[M];
vector<pair<int, int>> vec[M];
struct node {
	int mn, minn, lz;
} Tree[M << 2];
void pushup(int x) {
	Tree[x].mn = min(Tree[x << 1].mn, Tree[x << 1 | 1].mn);
	Tree[x].minn = min(Tree[x << 1].minn, Tree[x << 1 | 1].minn);
}
void tg(int x, int t) {
	Tree[x].mn += t;
	Tree[x].minn += t;
	Tree[x].lz += t;
}
void down(int x) {
	int laz = Tree[x].lz;
	if (laz) {
		tg(x << 1, laz);
		tg(x << 1 | 1, laz);
		Tree[x].lz = 0;
	}
}
void build(int x, int l, int r) {
	if (l == r) {
		int u = rk[l];
		Tree[x].mn = d[u] + bk[u];
		Tree[x].minn = d[u];
		Tree[x].lz = 0;
		return;
	}
	int mid = (l + r) >> 1;
	build(x << 1, l, mid);
	build(x << 1 | 1, mid + 1, r);
	pushup(x);
}
void upd(int x, int l, int r, int ql, int qr, int v) {
	if (ql <= l && r <= qr) {
		tg(x, v);
		return;
	}
	down(x);
	int mid = (l + r) >> 1;
	if (ql <= mid) upd(x << 1, l, mid, ql, qr, v);
	if (qr > mid) upd(x << 1 | 1, mid + 1, r, ql, qr, v);
	pushup(x);
}
void upd2(int x, int l, int r, int pos, int v) {
	if (l == r) {
		Tree[x].mn += v;
		return;
	}
	down(x);
	int mid = (l + r) >> 1;
	if (pos <= mid) upd2(x << 1, l, mid, pos, v);
	else upd2(x << 1 | 1, mid + 1, r, pos, v);
	pushup(x);
}
int qry(int x, int l, int r, int ql, int qr) {
	if (ql <= l && r <= qr) return Tree[x].mn;
	down(x);
	int mid = (l + r) >> 1;
	int res = inf;
	if (ql <= mid) res = min(res, qry(x << 1, l, mid, ql, qr));
	if (qr > mid) res = min(res, qry(x << 1 | 1, mid + 1, r, ql, qr));
	return res;
}
int qry2(int x, int l, int r, int pos) {
	if (l == r) return Tree[x].minn;
	down(x);
	int mid = (l + r) >> 1;
	if (pos <= mid) return qry2(x << 1, l, mid, pos);
	else return qry2(x << 1 | 1, mid + 1, r, pos);
}
int read() {
	int sgn = 1, ans = 0;
	char c = getchar();
	while (!isdigit(c)) {
		if (c == '-') sgn = -sgn;
		c = getchar();
	}
	while (isdigit(c)) ans = ans * 10 + c - '0', c = getchar();
	return sgn * ans;
}
void dfs(int u, int pre, int val) {
	siz[u] = 1;
	dfn[u] = ++tot;
	rk[tot] = u;
	d[u] = d[pre] + val;
	for (auto [v, w] : vec[u]) {
		dfs(v, u, w);
		siz[u] += siz[v];
	}
}
signed main() {
	int n = read(), q = read();
	for (int i = 1; i < n; i++) {
		int u = read(), v = read(), w = read();
		vec[u].push_back(make_pair(v, w));
		eu[i] = u; ev[i] = v; ew[i] = w;
	}
	for (int i = n; i <= 2 * n - 2; i++) {
		int u = read(), v = read(), w = read();
		bk[u] = w;
		nd[i] = u;
		ew[i] = w;
	}
	bk[1] = inf;
	dfs(1, 0, 0);
	build(1, 1, n);
	while (q--) {
		int op = read(), x = read(), y = read();
		if (op == 1) {
			if (x <= n - 1) {
				int v = ev[x];
				int delta = y - ew[x];
				ew[x] = y;
				upd(1, 1, n, dfn[v], dfn[v] + siz[v] - 1, delta);
			} else {
				int u = nd[x];
				int delta = y - ew[x];
				ew[x] = y;
				bk[u] = y;
				upd2(1, 1, n, dfn[u], delta);
			}
		} else {
			int du = qry2(1, 1, n, dfn[x]), dv = qry2(1, 1, n, dfn[y]);
			if (dfn[x] <= dfn[y] && dfn[y] <= dfn[x] + siz[x] - 1) {
				cout << dv - du << '\n';
			} else {
				int mn = qry(1, 1, n, dfn[x], dfn[x] + siz[x] - 1);
				cout << mn - du + dv << '\n';
			}
		}
	}
	return 0;
}

CF762C Two strings

题目描述

给你两个字符串 \(a\)\(b\)。你需要从字符串 \(b\) 中删除最少数量的连续(位置相邻)字符,使得它变为字符串 \(a\) 的一个子序列。你可能不需要删除任何字符,也可能需要将 \(b\) 的所有字符都删除,使其变为空串。

字符串 \(s\) 的子序列是指可以通过删除若干(可以为零)个字符(不要求连续)从 \(s\) 得到的任意字符串。

题解

我们可以发现删除 \(b\) 的一个子串相当于保留 \(b\) 的一段前缀 + 后缀。于是问题转化为找一对前缀 \(b_{1\cdots i}\) 和后缀 \(b_{j\cdots len}\),使得它们分别是 \(a\) 的子序列,且在 \(a\) 中的匹配位置不重叠。

于是我们令 \(pre_i\)\(b\) 的前 \(i\) 个字符能够被匹配的最左位置, \(suf_i\) 为后缀 \(i \sim len\) 能够被匹配的最右位置,显然这个可以 \(\mathcal{O}(n)\) 预处理。然后我们双指针枚举合法的 \((i,j)\),贪心地让 \(j\) 尽可能靠近 \(i\),统计答案即可。

Code

#include<bits/stdc++.h>
using namespace std;
const int M = 1e5 + 5;
int pre[M], suf[M];
bool ok[M];               
int main() {
	string a, b;
	cin >> a >> b;
	int l1 = a.size(), l2 = b.size();
	a = " " + a, b = " " + b;
	pre[0] = 1;  ok[0] = true; 
	for (int i = 1; i <= l2; i++) {
		int pos = pre[i - 1];
		while (pos <= l1 && a[pos] != b[i]) pos++;
		if (pos <= l1) {       
			pre[i] = pos + 1;
			ok[i] = true;
		} else {              
			pre[i] = l1 + 1;
			ok[i] = false;
		}
	}
	suf[l2 + 1] = l1 + 1;
	for (int j = l2; j >= 1; j--) {
		int pos = suf[j + 1] - 1;
		while (pos >= 1 && a[pos] != b[j]) pos--;
		if (pos >= 1) suf[j] = pos;
		else suf[j] = 0;        
	}
	int ans = 0, p1 = 0, p2 = l2 + 1;
	int j = 1;
	for (int i = 0; i <= l2; i++) {
		if (!ok[i]) break;       
		if (j <= i) j = i + 1;
		while (j <= l2 + 1 && suf[j] < pre[i]) j++;
		if (j <= l2 + 1) {
			int len = i + (l2 - j + 1);
			if (len > ans) {
				ans = len;
				p1 = i;
				p2 = j;
			}
		}
	}
	if (ans == 0) cout << "-";
	else cout << b.substr(1, p1) << b.substr(p2);
	return 0;
}

CF1864E Guess Game

题目描述

Carol 有一个长度为 \(n\) 的非负整数序列 \(s\)。她想和 Alice 以及 Bob 一起玩“猜数游戏”。

游戏规则如下:Carol 会随机选择两个整数下标 \(i_a\)\(i_b\),范围为 \([1, n]\),并令 \(a=s_{i_a}\)\(b=s_{i_b}\)。注意 \(i_a\)\(i_b\) 可以相同。

Carol 会告知:

  • Alice \(a\) 的值;
  • Bob \(b\) 的值;
  • Alice 和 Bob 都会知道 \(a \mid b\) 的值。

接下来开始猜数。两位玩家轮流猜测,Alice 先手。两人的目标是判断以下哪种情况成立:\(a < b\)\(a > b\),或 \(a = b\)

每一轮,玩家可以选择以下两种操作之一:

  • 说“我不知道”,并将回合交给另一位玩家;
  • 说“我知道”,并给出答案“\(a<b\)”、“\(a>b\)”或“\(a=b\)”;此时游戏结束。

Alice 和 Bob 都能听到对方的发言,并可以利用这些信息进行推理。两人都足够聪明,只有在完全确定时才会说“我知道”。你需要计算游戏中玩家所用回合数的期望值。输出答案对 \(998\,244\,353\) 取模。

题解

首先按位从高到低逐位比较 \(a\)\(b\)。设 \(c = a \mid b\)。对于二进制表示的每一位:

  • \(c\) 的某位为 \(0\),则无需额外回合。
  • \(c\) 的某位为 \(1\),则可能为 \((0,1), (1,0), (1,1)\)
    • 若这是第一个不同位,看到 \(0\) 的玩家可以立刻确定大小关系并宣布。我们直接合并统计 \((i,j)\)\((j,i)\) 即可。
    • 若该位两人均为 \(1\),则先手和后手都会说“我不知道”,两人由此确认该位都是 \(1\),然后继续检查下一位。

因此可以发现,若 \(a<b\),设 \(a\) 最高 \(0\) 的位是第 \(i\) 位,会经过 \(i + [i \bmod 2 = 0]\) 轮;同理,若 \(a>b\),设 \(b\) 最高 \(0\) 的位是第 \(i\) 位,会经过 \(i + [i \bmod 2 = 1]\) 轮。

那么我们只需要批量统计任意两个数二进制下第一个不同的位置即可。这个可以用 Trie 树轻松解决。

Code

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int mod = 998244353;
int qpow(int x, int y) {
	int res = 1;
	while (y) {
		if (y & 1) res = res * x % mod;
		x = x * x % mod;
		y >>= 1;
	}
	return res;
}
struct node {
	int ch[2], sz;
};
vector<node> tr;
int ans;
int clone() {
	tr.push_back({{0, 0}, 0});
	return tr.size() - 1;
}
void dfs(int u, int d, int cnt) {
	if (d < 0) {
		ans = (ans + tr[u].sz * tr[u].sz % mod * (cnt + 1)) % mod;
		return;
	}
	int lf = tr[u].ch[0], rt = tr[u].ch[1];
	int sz1 = lf ? tr[lf].sz : 0, sz2 = rt ? tr[rt].sz : 0;
	if (sz1 && sz2) {
		ans = (ans + sz1 * sz2 % mod * (2LL * cnt + 3)) % mod;
	}
	if (lf) dfs(lf, d - 1, cnt);
	if (rt) dfs(rt, d - 1, cnt + 1);
}
int read(){
	int sgn = 1, ans = 0;
	char c = getchar();
	while(!isdigit(c)){
		if(c == '-') sgn = -sgn;
		c = getchar();
	}
	while(isdigit(c)) ans = ans * 10 + c - '0', c = getchar();
	return sgn * ans;
}
void solve() {
	int n = read();
	vector<int> s(n);
	for (int i = 0; i < n; i++) s[i] = read();
	tr.clear();
	clone();
	for (int x : s) {
		int p = 0;
		for (int i = 29; i >= 0; i--) {
			int b = (x >> i) & 1;
			if (!tr[p].ch[b]) {
				int nxt = clone();
				tr[p].ch[b] = nxt;
			}
			p = tr[p].ch[b];
			tr[p].sz++;
		}
	}
	ans = 0;
	dfs(0, 29, 0);
	int inv = qpow(n, mod - 2);
	cout << ans * inv % mod * inv % mod << '\n';
}
signed main() {
	int T = read();
	while (T--) solve();
	return 0;
}

CF1886D Monocarp and the Set

题目描述

Monocarp 有 \(n\) 个数字 \(1, 2, \dots, n\) 和一个集合(初始为空)。他以某种顺序将这些数字依次加入集合中,每次加入一个新的、之前未出现过的数字。换句话说,加入数字的序列是 \(1\)\(n\) 的一个排列。

每当 Monocarp 往集合中加入一个元素(除了第一次)时,他会写下一个字符:

  • 如果当前加入的元素成为集合中的最大值,Monocarp 写下字符 \(>\)
  • 如果当前加入的元素成为集合中的最小值,Monocarp 写下字符 \(<\)
  • 如果都不是,Monocarp 写下字符 \(?\)

现在给定一个长度为 \(n-1\) 的字符串 \(s\),它表示 Monocarp 按顺序写下的字符。你需要处理 \(m\) 个关于该字符串的查询。每个查询格式如下:

  • \(i\) \(c\) —— 将 \(s_i\) 替换为字符 \(c\)

你需要在处理所有查询之前,以及每次查询后,计算有多少种不同的排列方式,使得 Monocarp 按这种顺序插入数字后,得到的字符串正好是 \(s\)。由于答案可能很大,请对 \(998244353\) 取模后输出。

题解

不是为什么结论这么简单??

首先我们发现每次操作第二个数字必须不是问号,在每次操作时候需要特判。

然后我们将插入过程看作逐步构建一个有序序列。设已插入 \(k - 1\) 个数,它们的大小关系已经确定,可以看作排好序的一列数。现在插入第 \(k\) 个数:

  • 若记录 >,则新数比之前所有数都大,它只能放在当前序列的最右端,有 \(1\) 种放法。
  • 若记录 <,则新数比之前所有数都小,它只能放在当前序列的最左端,有 \(1\) 种放法。
  • 若记录 ?,则新数大小在原有最小值与最大值之间,它可以放在除两端以外的任意两个相邻数之间,有 \(k - 2\) 种放法。

因为每个位置相互独立所以直接相乘就是答案。然后询问的时候询问时除去这一位原有的贡献,再乘上修改后的贡献即可。

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod = 998244353;
string s;
int read(){
	int sgn = 1, ans = 0;
	char c = getchar();
	while(!isdigit(c)){
		if(c == '-') sgn = -sgn;
		c = getchar();
	}
	while(isdigit(c)) ans = ans * 10 + c - '0', c = getchar();
	return sgn * ans;
}
int qpow(int x, int y) {
	int res = 1;
	while(y) {
		if(y & 1) res = res * x % mod;
		x = x * x % mod;
		y >>= 1;
	}
	return res;
}
int inv(int x) {return qpow(x, mod - 2);}
signed main(){
	int n = read(), m = read();
	getline(cin, s);
	s = "  " + s;
	int res = 1;
	for(int i = 3; i <= n; i++) {
		if(s[i] == '?') res = res * (i - 2) % mod;
	}
	cout << res * (s[2] != '?') << '\n';
	while(m--) {
		int p = read() + 1;
		char c;
		cin >> c;
		if(p > 2 && s[p] == '?') res = res * inv(p - 2) % mod;
		s[p] = c;
		if(p > 2 && s[p] == '?') res = res * (p - 2) % mod;
		cout << res * (s[2] != '?') << '\n';
	}
	return 0;
}

CF1370E Binary Subsequence Rotation

题目描述

给定两个长度为 \(n\)\(01\)\(a\)\(b\)

对于一次“旋转”操作我们这样定义:如果我们要旋转的序列为 \(c_1,c_2,c_3,c_4,c_5...c_n\),那么旋转之后的序列为 \(c_n,c_1,c_2,c_3,c_4...c_{n-1}\)

求至少进行多少次“旋转”操作,能够把串 \(a\) 变成串 \(b\)。如果不可能将 \(a\) 转换成 \(b\) 请输出 \(-1\)

题解

首先如果 \(a\)\(b\)\(0\)\(1\) 的数量不同,显然无解,输出 \(-1\)

然后我们发现,对于 \(a_i = b_i\) 的位置,它们已经满足要求,不需要参与任何操作;我们只需要考虑所有 \(a_i \neq b_i\) 的位置。在这些位置上,要么是 \(a_i=0,\;b_i=1\)(下称 \(A\) 类位置),要么是 \(a_i=1,\;b_i=0\)(下称 \(B\) 类位置)。显然 \(A\) 类位置的数量必然等于 \(B\) 类位置的数量。

一次“旋转”操作选择一个子序列,将其元素循环右移一位。如果这个子序列只包含 \(A\) 类或只包含 \(B\) 类位置,旋转后不会产生任何匹配;只有在一个操作中交替选择 \(A\)\(B\) 位置,才能在一次旋转后让这些位置全部变成正确的值。具体来说,如果把选中的子序列写成 \(c_1,c_2,\dots,c_k\),旋转后 \(c_1\) 得到原来的 \(c_k\)\(c_2\) 得到原来的 \(c_1\),……,\(c_k\) 得到原来的 \(c_{k-1}\)。为了让所有选中位置都匹配,这个序列的类型必须形如 \(A,B,A,B,\dots\) 或者 \(B,A,B,A,\dots\),且最后一位的类型与第一位相反,从而形成闭环。

因此,问题转化为:将剩下的 \(A,B\) 序列(保持原顺序)划分成最少数量的交替子序列,每个子序列内部类型交替。一次操作恰好处理一个这样的交替子序列。

我们可以用贪心算法求出最少划分数。从左到右扫描所有 \(a_i \neq b_i\) 的位置,维护两个计数器:

  • \(\text{cnt}_A\):当前以 \(A\) 结尾的未完结交替子序列数量;
  • \(\text{cnt}_B\):当前以 \(B\) 结尾的未完结交替子序列数量。
    当遇到一个 \(A\) 类位置时,如果 \(\text{cnt}_B > 0\),就可以把它接在一个以 \(B\) 结尾的子序列后面,令 \(\text{cnt}_B\)\(1\)\(\text{cnt}_A\)\(1\);否则必须新开一个子序列,\(\text{cnt}_A\)\(1\)。遇到 \(B\) 类位置时同理:若 \(\text{cnt}_A > 0\),则 \(\text{cnt}_A\)\(1\)\(\text{cnt}_B\)\(1\);否则新开一个,\(\text{cnt}_B\)\(1\)
    扫描结束后,所有未完结的子序列就是最少需要的交替子序列,答案即为 \(\text{cnt}_A + \text{cnt}_B\)。这个过程与计算前缀和最大值与最小值之差是等价的,将 \(A\) 看作 \(+1\)\(B\) 看作 \(-1\),答案也等于最大前缀和减去最小前缀和。

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
int read() {
	int sgn = 1, ans = 0;
	char c = getchar();
	while (!isdigit(c)) {
		if (c == '-') sgn = -sgn;
		c = getchar();
	}
	while (isdigit(c)) ans = ans * 10 + c - '0', c = getchar();
	return sgn * ans;
}
signed main() {
	int n = read();
	string s1, s2;
	cin >> s1 >> s2;
	s1 = " " + s1, s2 = " " + s2;
	int cnt1 = 0, cnt2 = 0;
	for (int i = 1; i <= n; i++) {
		if (s1[i] == '1') cnt1++;
		if (s2[i] == '1') cnt2++;
	}
	if (cnt1 ^ cnt2) {
		cout << "-1\n";
		return 0;
	}
	cnt1 = cnt2 = 0;
	for (int i = 1; i <= n; i++) {
		if (s1[i] == s2[i]) continue;
		if (s1[i] == '0') {
			if (cnt1) cnt1--;
			cnt2++;
		} else {
			if (cnt2) cnt2--;
			cnt1++;
		}
	}
	cout << cnt1 + cnt2 << "\n";
	return 0;
}

CF859E Desk Disorder

题目描述

\(n\) 位工程师,每位工程师要么留在原座位 \(a_i\),要么搬到目标座位 \(b_i\)。不允许两位工程师坐在同一张桌子上,求分配方案有多少种。对 \(10^{9}+7\) 取模。

题解

套路地连边 \(a_i \to b_i\),我们发现每个点出度为 \(0\)\(1\),所以每个连通块有 \(4\) 种情况:

  • 自环:显然动不了,方案数为 \(1\)
  • 环:显然分顺时针和逆时针 \(2\) 种方案。
  • 树:我们以出度为 \(0\) 的点为根,考虑对于树上的每一个点,如果它想到它父亲的位置,它的父亲就也得往前,那么它到根这条路径上的点都得往前挪。每个点都可以这样,所以方案数为树的 \(siz\) (注意,此时根的方案就是每个点都不动)。
  • 基环树:树 + 环的答案乘起来。

最终答案就是所有连通块乘起来。

Code

#include<bits/stdc++.h>
using namespace std;
const int M = 2e5 + 5;
const int mod = 1e9 + 7;
vector<int> vec[M];
int vis[M], ex[M];
bool ck[M];
long long res = 1;  
int read() {
	int sgn = 1, ans = 0;
	char c = getchar();
	while (!isdigit(c)) {
		if (c == '-') sgn = -sgn;
		c = getchar();
	}
	while (isdigit(c)) {
		ans = ans * 10 + c - '0';
		c = getchar();
	}
	return sgn * ans;
}
void dfs(int x, int fa) {
	stack<int> st;
	st.push(x);
	vis[x] = 1;
	int k = 0, cnt = 0; 
	long long deg = 0;
	while (!st.empty()) {
		int u = st.top(); st.pop();
		++k;
		deg += (int)vec[u].size();
		if (ck[u]) ++cnt;
		for (int v : vec[u]) {
			if (!vis[v]) {
				vis[v] = 1;
				st.push(v);
			}
		}
	}
	int m = (deg / 2) + cnt;
	if (m == k - 1) {
		res = res * k % mod;
	} else if (m == k) {
		if (cnt > 0) {
			res = res * 1 % mod;
		} else {
			res = res * 2 % mod;
		}
	}
}
signed main() {
	int n = read();
	for (int i = 1; i <= n; ++i) {
		int u = read(), v = read();
		if (u == v) {
			ck[u] = true;
			ex[u] = 1;
		} else {
			vec[u].push_back(v);
			vec[v].push_back(u);
			ex[u] = ex[v] = 1;
		}
	}
	for (int i = 1; i <= 2 * n; i++) {
		if (!vis[i] && ex[i]) {
			dfs(i, 0);
		}
	}
	cout << res % mod;
	return 0;
}

CF2183E LCM is Legendary Counting Master

题目描述

给定一个长度为 \(n~(n \le 3000)\) 的序列 \(a\) 和一个正整数 \(m~(m \le 3000)\)。序列 \(a\) 的每个元素都是 \([0, m]\) 范围内的整数。

当且仅当以下两个条件都满足时,序列 \(a\) 被认为是好的:

  • \(a_1 < a_2 < a_3 < \ldots < a_n\)
  • \(\frac{1}{\operatorname{lcm}(a_1,a_2)}+\frac{1}{\operatorname{lcm}(a_2,a_3)}+\ldots+ \frac{1}{\operatorname{lcm}(a_{n-1},a_n)}+{\color{red}\frac{1}{\operatorname{lcm}(a_n,a_1)}}\ge 1\)

你需要将序列 \(a\) 中所有的零替换成 \([1, m]\) 范围内的整数。求有多少种不同的替换方式,使得最终的序列 \(a\) 是好的。请输出答案对 \(998\,244\,353\) 取模。

题解

这谁注意得到啊QwQ

首先我们注意到 \(\frac 1 {\operatorname{lcm}(x, y)} = \frac{\gcd(x, y)}{xy}\),所以:

\[\begin{aligned} \frac{1}{\operatorname{lcm}(a_1,a_2)}+\frac{1}{\operatorname{lcm}(a_2,a_3)}+\ldots+ \frac{1}{\operatorname{lcm}(a_{n-1},a_n)}+{\frac{1}{\operatorname{lcm}(a_n,a_1)}}\\ = \frac{\gcd(a_1,a_2)}{a_1 a_2}+\frac{\gcd(a_2,a_3)}{a_2a_3} + \ldots + \frac{\gcd(a_{n - 1},a_n)}{a_{n - 1} a_n}+\frac{\gcd(a_n,a_1)}{a_na_1} \end{aligned} \]

\(\gcd\) 的性质,得:\(\gcd(x_i, x_{i + 1}) = \gcd(x_{i+1} - x_i, x_i) \le x_{i + 1} - x_i\),所以有:

\[\begin{aligned} &\le \frac{a_2 - a_1}{a_1 a_2}+\frac{a_3 - a_2}{a_2a_3} + \ldots + \frac{a_n - a_{n - 1}}{a_{n - 1} a_n}+\frac{\color{red}a_1}{a_na_1} \\ &= (\frac{1}{a_1} - \frac{1}{a_2}) + (\frac{1}{a_2} - \frac{1}{a_3}) + \ldots +(\frac{1}{a_{n - 1}} - \frac{1}{a_n}) + \frac 1 {a_n} \\ &= \frac{1} {a_1} \le 1. \end{aligned} \]

而题目要求 \(\ge 1\),所以得出 \(\forall i \in [1, n),\gcd(a_{i + 1} - a_i) = a_{i + 1} - a_i\),且 \(a_1 = 1\)。题目即转化为求满足该条件的数列个数。

考虑 dp。我们令 \(f_{i,j}\) 表示第 \(i\) 个数换成 \(j\) 的方案数,首先 \(f_{1,1} = 1\)。 而 \(\gcd(x, y) = y - x\) 相当于 \(\exist d \mid x\) 使得 \(y = d + x\),所以显然有转移方程:

\[f_{i,j} = \sum\limits_{j = 1, x | j}^m f_{i - 1, j - x} \]

最终的答案就是 \(\sum\limits_{i = 1}^m f_{n, i}\)。需要注意的是有些数字是填好了的,所以要去除 \(j \neq a_i\) 的贡献。

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int M = 3005;
const int mod = 998244353;
int f[M][M], a[M];
vector<int>vec[M];
int read(){
	int sgn = 1, ans = 0;
	char c = getchar();
	while(!isdigit(c)){
		if(c == '-') sgn = -sgn;
		c = getchar();
	}
	while(isdigit(c)) ans = ans * 10 + c - '0', c = getchar();
	return sgn * ans;
}
void init(int T) {
	for(int i = 1; i <= T; i++) {
		for(int j = i; j <= T; j += i) {
			vec[j].push_back(i);
		}
	}
}
void sol() {
	int n = read(), m = read();
	//memset(f, 0, sizeof(f));
	for(int i = 1; i <= n; i++) a[i] = read();
	if(a[1] > 1) {
		cout << "0\n";
		return;
	}
	f[1][1] = 1;
	for(int i = 2; i <= n; i++) {
		for(int j = 1; j <= m; j++) {
			f[i][j] = 0;
			for(auto v : vec[j]) {
				f[i][j] = (f[i][j] + f[i - 1][j - v]) % mod;
			}
		}
		if(a[i]) {
			for(int j = 1; j <= m; j++) {
				if(j ^ a[i]) f[i][j] = 0;
			}
		}
	}
	int ans = 0;
	for (int j = 1; j <= m; j++) {
		(ans += f[n][j]) %= mod;
	}
	cout << ans << '\n';
}
signed main(){
	init(M - 2);
	int T = read();
	while(T--) sol();
	return 0;
}

CF1876C Autosynthesis

题目描述

Chaneka 写下了一个包含 \(n\) 个正整数元素的数组 \(a\)。最初,所有元素都没有被圈出。在一次操作中,Chaneka 可以圈出一个元素。可以多次圈出同一个元素。

在完成所有操作后,Chaneka 会构造一个序列 \(r\),该序列由所有未被圈出的 \(a\) 的元素按索引顺序组成。

Chaneka 还会构造另一个序列 \(p\),其长度等于操作次数,第 \(i\) 个元素 \(p_i\) 表示第 \(i\) 次操作中被圈出的元素的索引。

Chaneka 想要通过若干次操作,使得序列 \(r\) 等于序列 \(p\)。请你帮助她实现这一目标,或者报告如果无法实现。

题解

对于这种和下标相关的题目,首先我们应该想到建图。

我们对 \(i \to a_i\) 建边,此时应该形成的是一个内向基环树森林。不难发现,最终答案合法当且仅当:所有未选择出边的点,它的入边最少有一条被选择了;所有选择了出边的点,它所有的入边都没被选择。

由于所有叶节点只能选择它的出边,于是考虑对于基环树自底向上贪心,最终对于剩下的环进行处理。无解的情况,就是在我们处理之后发现有节点并未在过程中处理成功。

Code

#include <bits/stdc++.h>
using namespace std;
const int M = 200005;  
int a[M], d[M], st[M];
int read() {
    int sgn = 1, ans = 0;
    char c = getchar();
    while (!isdigit(c)) {
        if (c == '-') sgn = -sgn;
        c = getchar();
    }
    while (isdigit(c)) ans = ans * 10 + c - '0', c = getchar();
    return sgn * ans;
}
int main() {
    int n = read();
    for (int i = 1; i <= n; i++) st[i] = -1;
    for (int i = 1; i <= n; i++) {
        a[i] = read();
        ++d[a[i]];
    }
    queue<int> q;
    for (int i = 1; i <= n; i++) {
        if (d[i] == 0) {
            st[i] = 0;         
            q.push(i);
        }
    }
    while (!q.empty()) {
        int u = q.front(); q.pop(); 
        int v = a[u];
        if (st[v] == -1) {
            st[v] = 1;               
            int w = a[v];
            if (--d[w] == 0 && st[w] == -1) {
                st[w] = 0;           
                q.push(w);
            }
        }
    }
    for (int i = 1; i <= n; i++) {
        if (st[i] == -1) {
            int cur = i, col = 0;
            while (st[cur] == -1) {
                st[cur] = col;
                col ^= 1;
                cur = a[cur];
            }
            if (st[cur] != col) {
                cout << -1 << '\n';
                return 0;
            }
        }
    }
    int tot = 0;
    for (int i = 1; i <= n; i++) {
        if (st[i] == 1) ++tot;
    }
    cout << n - tot << '\n';  
    for (int i = 1; i <= n; i++) {
        if (st[i] == 0) cout << a[i] << ' ';
    }
    return 0;
}

CF703D Mishka and Interesting sum

题目描述

给定 \(n\) 个元素组成的非负整数数组 \(a_{1},a_{2},...,a_{n}\)\(m\) 个查询。每个查询方式如下:

每个查询的处理方式如下:

  1. 指定两个整数 \(l\)\(r\)\(1\leq l\leq r\leq n\)),表示查询区间的左右边界。
  2. 把区间 \([l,r]\)(即序列 \(a_{l},a_{l+1},...,a_{r}\))中出现次数为偶数次的整数写下来。
  3. 计算这些写下来的整数的异或和,作为本次查询的答案。形式化地说,若写下来的整数为 \(x_{1},x_{2},...,x_{k}\),则米什卡想要知道 \(\displaystyle x_{1}\oplus x_{2}\oplus\dots\oplus x_{k}\) 的值,这里 \(\oplus\) 表示按位异或运算。

对于每个询问,依次输出每个查询的答案。

题解

我说是 HH 的项链有没有懂的。

首先出现偶数次的整数的异或和 \(=\) 区间内所有整数的异或和 \(\oplus\) 出现奇数次的整数的异或和,证明显然。

然后区间内所有整数的异或和显然前缀异或预处理即可,我们需要求出现奇数次的整数的异或和。

然后我们发现,异或两次等于不异或,所以出现奇数次的整数的异或和 \(=\) 区间内不同数的异或和。

区间内不同数的异或和这个就像 HH 的项链了,对于若干个询问的区间 \([l,r]\),如果他们的 \(r\) 都相等的话,那么出现的同一个数字,一定是只关心出现在最右边的那一个的。所以建一个树状数组,对所有查询的区间离线下来,按照 \(r\) 来排序,计算出从第一个位置起,到某个位置一共有多少不同的数字的贡献即可。

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int M = 1e6 + 6;
int a[M], pre[M], Tree[M], ans[M], tmp[M], val[M];
int vis[M], n, m, nw;
int read(){
	int sgn = 1, ans = 0;
	char c = getchar();
	while(!isdigit(c)){
		if(c == '-') sgn = -sgn;
		c = getchar();
	}
	while(isdigit(c)) ans = ans * 10 + c - '0', c = getchar();
	return sgn * ans;
}
int lowbit(int x){ return x & (-x); }
void upd(int p, int x){
	for (; p <= n; p += lowbit(p)) Tree[p] ^= x;
}
int qry(int p){
	int ret = 0;
	for (; p; p -= lowbit(p)) ret ^= Tree[p];
	return ret;
}
struct Q {
	int l, r, id;
} q[M];
bool cmp(Q a, Q b){ return a.r < b.r; }
signed main(){
	n = read();
	for(int i = 1; i <= n; i++) {
		a[i] = read();
		pre[i] = pre[i - 1] ^ a[i];
	}
	memcpy(tmp, a, sizeof(a));
	sort(tmp + 1, tmp + 1 + n);
	int cnt = unique(tmp + 1, tmp + 1 + n) - tmp - 1;
	for(int i = 1; i <= n; i++){
		a[i] = lower_bound(tmp + 1, tmp + 1 + cnt, a[i]) - tmp;
	}
	for(int i = 1; i <= n; i++) val[i] = tmp[a[i]];
	m = read();
	for(int i = 1; i <= m; i++){
		q[i].l = read();
		q[i].r = read();
		q[i].id = i;
	}
	sort(q + 1, q + 1 + m, cmp);
	nw = 1;
	for(int i = 1; i <= m; i++){
		for(int j = nw; j <= q[i].r; j++){
			int id = a[j];
			if(vis[id]) upd(vis[id], val[j]);
			upd(j, val[j]);              
			vis[id] = j;
		}
		nw = q[i].r + 1;
		ans[q[i].id] = (pre[q[i].r] ^ pre[q[i].l - 1]) ^ (qry(q[i].r) ^ qry(q[i].l - 1));
	}
	for(int i = 1; i <= m; i++) printf("%lld\n", ans[i]);
	return 0;
}
posted @ 2026-07-01 22:12  nick_zha  阅读(11)  评论(0)    收藏  举报