花惑う夏を待つ僕に差す月明かり

6 月 25 日 ~ 7 月 4 日

其实只准备考三科。

其实学点文化课挺好的。

LG P9111 [福建省队集训2019] 最大权独立集问题

给定一颗 \(n\) 个点的树,初始时每个点有点权 \(c_i\)\(S=0\)。进行 \(n\) 次操作,每次操作选择一个还在的点 \(u\),执行如下操作:

  • \(S \gets S + c'_u\),其中 \(c'_u\) 为点 \(u\) 当前的权值。
  • 对于所有与 \(u\) 直接相连的点 \(v\),令 \(c'_v \gets c'_v + c'_u\)
  • 删除点 \(u\)

\(S\) 的最大值。\(n \leq 400\)\(|c_i| \leq 10^9\)


考虑把贡献拆开,即计算每个点对答案的贡献。

设删除点 \(u\) 的时间是 \(t_u\)。对于一条连接 \(u,v\) 的边,若 \(t_u < t_v\),我们把它定向为 \(u \to v\),否则定向为 \(v \to u\)。容易发现,这样定向之后,\(d_u\) 会流向所有其可达的点。因此可以对题意做如下转化:给树上的每条边定向,设点 \(i\) 可以到达包括 \(i\) 在内的 \(k_i\) 个点,最大化 \(\sum\limits_{i=1}^n k_i \times d_i\) 的值。

考虑树形 DP。我们发现,对于当前 DP 的点 \(u\) 和它的一个儿子 \(v\) 而言,如果定向为 \(u \to v\),额外的贡献是好计算的,我们只需要在状态中记录下 \(v\) 可达的点数。但定向为 \(v \to u\) 时,事情就没有那么简单了:\(v\) 额外的贡献取决于 \(u\) 能够到达多少个点,但在合并完其它子树之前,我们其实并不知道这个确切的值。

注意到,本题的时间复杂度允许我们在背包之外再乘一个 \(\mathcal{O}(n)\),我们不妨枚举这个值为 \(k\) 并将其计入状态,即假设合并完之后 \(u\) 能够到达 \(k\) 个点,用这个 \(k\) 来提前计算 \(v\) 的额外贡献。具体来说,我们设 \(f_{u,i,k}\) 为考虑完 \(u\) 的子树,\(u\) 可达 \(i\) 个点,\(v\) 的额外贡献按照 \(k\) 来计算时,子树内答案的最大值。初始值 \(f_{u,1,k} = d_u\),转移枚举 \(k\),考虑边 \((u,v)\) 的方向:

  • 方向为 \(u \to v\)\(v\)\(u\) 没有额外的贡献,枚举 \(v\) 可达的点数 \(j\),有 \(f_{u,i,k}+f_{v,j,j}+ d_u \times j \to f_{u,i+j,k}\)
  • 方向为 \(v \to u\)\(v\)\(u\) 有额外 \(k\) 的贡献,枚举 \(v\) 可达的点数 \(j\),有 \(f_{u,i,k} + f_{v,j,j+k} \to f_{u,i,k}\)

转移完之后我们需要补上 \(u\) 的额外贡献,即 \(f_{u,i,k} + d_u \times (k-i) \to f_{u,i,k}\)。最后的答案即为 \(\max f_{1,i,i}\)

总时间复杂度 \(\mathcal{O}(n^3)\)。代码实现细节略有不同。

code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
constexpr int N = 4e2 + 5;
int n, a[N], c[N], sz[N];
LL f[N][N][N], g[N][N][N];
vector <int> e[N];
void dfs(int u) {
	sz[u] = 1;
	for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) f[u][i][k] = -1e18;  
	for (int k = 1; k <= n; k++) f[u][1][k] = 1LL * a[u] * k;
	for (auto v : e[u]) {
		dfs(v);
		static LL tmp[N][N];
		for (int k = 1; k <= n; k++) for (int i = 1; i <= sz[u]; i++) tmp[i][k] = f[u][i][k], f[u][i][k] = -1e18;
		for (int k = 1; k <= n; k++) {
			for (int i = 1; i <= sz[u]; i++) {
				for (int j = 1; j <= sz[v]; j++) {
					f[u][i + j][k] = max(f[u][i + j][k], tmp[i][k] + f[v][j][j]);
				}
				LL val = -1e18;
				for (int j = 1; j <= min(sz[v], n - k); j++) val = max(val, f[v][j][j + k]);
				f[u][i][k] = max(f[u][i][k], tmp[i][k] + val);	
			}
		} 
		sz[u] += sz[v];
	}
}
signed main() {
	ios :: sync_with_stdio(false);
	cin.tie(nullptr);
	cin >> n;
	for (int i = 1; i <= n; i++) cin >> a[i];
	for (int i = 2; i <= n; i++) {
		cin >> c[i];
		e[c[i]].push_back(i);
	}
	dfs(1);
	LL ans = -1e18;
	for (int i = 1; i <= n; i++) ans = max(ans, f[1][i][i]);
	cout << ans << "\n";
 	return 0;
}

LG P7740 [NOI2021] 机器人游戏

\(m\) 个长度为 \(n\) 的序列,序列上每个位置可以是 01 或空格子。第 \(i\) 个序列上有一个机器人,其操作序列为 \(S_i\),所有机器人的起点都是其所在序列的第 \(p\) 个格子。每个机器人会执行如下操作对序列进行修改:

  • 如果它所在序列全是空格子,则不会执行后面的任何操作。

  • 否则它会按顺序执行操作序列:R 表示往右走一步,如果走出去了就爆炸;01 表示把脚下格子改成 01* 表示把当前格子异或 1。对于修改操作,如果脚下是空格子则不会产生任何影响。

对于一个初始序列状态、最终序列状态的二元组 \((A,B)\),如果存在一个 \(p\) 使得机器人在初始状态 \(A\) 上进行修改之后,最终达到状态 \(B\) 且中途没有爆炸,则称之合法方案。

求合法方案总数对 \(10^9 + 7\) 取模后的结果。\(n \leq 32\)\(m \leq 10^3\)\(|S_i| \leq 100\)


考虑容斥,先转化成求不合法的方案数,接着转化为计算钦定若干个位置是起点,剩下随便的方案数。当然样例解释已经教会了我们该怎么容斥:直接枚举钦定的起点集合 \(S\),把方案数带上容斥系数 \((-1)^{|S|}\)

考虑怎么计算这个方案数。容易发现,在一个机器人操作完之后,对于初始状态某个位置上的值 \(x\),在终止状态中一定可以被表示为 \(0\)\(1\)\(x\)\(1-x\) 的其中之一。分别考虑每个纸带,对于钦定为起点的那些位置,它们会给之后的所有位置一个限制。那么对于每个位置而言,它会可能会受到若干个不同的限制,我们需要计算满足所有限制的输入输出的方案数。

分类讨论:

  • 如果限制中既有 \(0\) 又有 \(1\),或者既有 \(x\) 又有 \(1-x\):显然唯一可能的合法情况就是该位置为空,方案数为 \(1\)
  • 否则,如果限制中有 \(0\)\(1\),且有 \(x\)\(1-x\):那么要么格子为空,否则输入输出就确定了,方案数为 \(2\)
  • 对于剩下的情况,每一种输入恰对应一种输出,加上空格子的情况,方案数为 \(3\)

这样每个位置就是独立的了。特判一下机器人爆炸的情况,直接计算的时间复杂度为 \(\mathcal{O}(n^2m2^n)\)

注意到 \(n \leq 32\) 非常折半,我们考虑怎么把指数砍掉一半。关键的性质是:如果存在 \(\geq k\) 的起点,那么要使得机器人不爆炸,最大位移必然 \(\leq n-k\)。这启发我们枚举最后一个起点的位置 \(P\),对于第 \(i\) 个机器人,设其中有 \(c_i\)R,那么对于当前的 \(P\),有且仅有 \(c_i > n-P\) 的机器人会爆炸。

然后我们对于前 \(P\) 个位置 DP,注意到由于最大位移 \(\leq n-P\),我们只需要记录与当前位置不超过 \(n-P\) 的起点状态和与当前位置距离超过 \(n-P\) 的位置中有没有起点即可。具体来说是设 \(f_{i,0/1,S}\) 表示做到第 \(i\) 位,前面若干位的起点状态是 \(S\),更前面有没有起点的方案数带上容斥系数的和,转移时枚举所有序列的第 \(i\) 个位置计算贡献。最后类似处理一下超过 \(P\) 的元素即可。这样可以得到 \(\mathcal{O}(n^2m2^{\frac{n}{2}})\) 的时间复杂度,但还是无法通过。

我们发现瓶颈无非是对于一个给定的起点集合,在所有序列中求出对某个位置 \(x\) 的限制是什么,这个总状态是 \(\mathcal{O}(n2^{\frac{n}{2}})\) 的。注意到我们只需要判定是否存在某个限制,考虑使用 bitset 优化,一个 bitset 中每个位置代表一个纸袋的限制。预处理 \(Z_i\) 表示距离起点为 \(i\) 的位置拥有的限制,进而由于只需要算子集并,容易用类似递推的方式求出 \(F_S\) 表示起点集合为 \(S\) 的限制,单次转移 \(\mathcal{O}(\frac{m}{\omega})\),总时间复杂度 \(\mathcal{O}(\frac{nm2^{\frac{n}{2}}}{\omega})\)

code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef vector <int> vi;
constexpr int N = 35, M = 1e3 + 5, mod = 1e9 + 7;
int n, m, L[M], oper[M][N], c[M], inall;
char str[M][N * 3];
int f[N][2][1 << 17], pw2[M], pw3[M];
bitset < M > all, rc[N][4], pc[N], Z[1 << 17][4];
vi st[N];
signed main() {
	ios :: sync_with_stdio(false);
	cin.tie(nullptr);
	cin >> n >> m;
	for (int i = 1; i <= m; i++) cin >> (str[i] + 1), L[i] = strlen(str[i] + 1);
	pw2[0] = pw3[0] = 1;
	for (int i = 1; i <= m; i++) pw2[i] = 1LL * pw2[i - 1] * 2 % mod, pw3[i] = 1LL * pw3[i - 1] * 3 % mod; 
	for (int i = 1; i <= m; i++) {
		for (int j = 1; j <= L[i]; j++) {
			if (str[i][j] == 'R') ++c[i], oper[i][c[i]] = 0;
			if (str[i][j] == '0') oper[i][c[i]] = 2;
			if (str[i][j] == '1') oper[i][c[i]] = 3;
			if (str[i][j] == '*') oper[i][c[i]] ^= 1;
		}
		st[c[i]].push_back(i);
	}
	for (int i = 1; i <= m; i++) all.set(i);
	int ans = 1;
	for (int i = 1; i <= n * m; i++) ans = 1LL * ans * 3 % mod;
	for (int r = n; r >= 0; r--) {
		for (auto t : st[n - r]) {
			for (int i = 0; i <= n + 2; i++) rc[i][oper[t][i]].set(t);
			for (int i = c[t] + 1; i <= n + 2; i++) pc[i].set(t);
			++inall; 
		}
		f[0][0][0] = 1;
		for (int i = 1; i <= r; i++) {
			int up = 1 << min(n - r + 1, i - 1), xup = 1 << min(n - r + 1, i), msk = xup - 1;
			for (int s = 0; s <= xup - 1; s++) f[i][0][s] = f[i][1][s] = 0;
			for (int z = 0; z <= 1; z++) {
				for (int s = 0; s <= up - 1; s++) {
					int _z = z | ((s << 1) > msk);
					if (i < r) f[i][_z][(s << 1) & msk] = (f[i][_z][(s << 1) & msk] + f[i - 1][z][s]) % mod;
					f[i][_z][(s << 1 | 1) & msk] = (f[i][_z][(s << 1 | 1) & msk] + mod - f[i - 1][z][s]) % mod;
				}
			}
			for (int z = 0; z <= 1; z++) {
				int _z = z | (i < r);
				for (int j = 0; j <= min(n - r + 1, i) - 1; j++) {
					for (int k = _z; k <= 3; k++) Z[1 << j][k] = rc[j][k];
					if (_z == 0) Z[1 << j][0] |= pc[j];
				}
				for (int s = 0; s <= xup - 1; s++) {
					if (s) for (int k = _z; k <= 3; k++) Z[s][k] = Z[s & -s][k] | Z[s ^ (s & -s)][k];
					if (_z) {
						bitset < M > v1 = Z[s][1] | (Z[s][2] & Z[s][3]), v2 = (Z[s][2] | Z[s][3]) & (all ^ v1);
						int px = v1.count(), py = v2.count();
						f[i][z][s] = 1LL * f[i][z][s] * pw2[py] % mod * pw3[inall - px - py] % mod; 
					} else {
						bitset < M > v1 = (Z[s][0] & Z[s][1]) | (Z[s][2] & Z[s][3]), v2 = (Z[s][0] | Z[s][1]) & (Z[s][2] | Z[s][3]) & (all ^ v1);
						int px = v1.count(), py = v2.count();
						f[i][z][s] = 1LL * f[i][z][s] * pw2[py] % mod * pw3[inall - px - py] % mod; 		
					}
				}
			}
		}
		int dis = min(n - r + 1, r), xup = 1 << dis;
		for (int z = 0; z <= 1; z++) {
			for (int d = 1; d <= n - r; d++) {
				for (int j = 0; j <= dis - 1; j++) for (int k = z; k <= 3; k++) Z[1 << j][k] = rc[j + d][k];
				for (int s = 0; s <= xup - 1; s++) {
					if (s) for (int k = z; k <= 3; k++) Z[s][k] = Z[s & -s][k] | Z[s ^ (s & -s)][k];
					if (z) {
						bitset < M > v1 = Z[s][1] | (Z[s][2] & Z[s][3]), v2 = (Z[s][2] | Z[s][3]) & (all ^ v1);
						int px = v1.count(), py = v2.count();
						f[r][z][s] = 1LL * f[r][z][s] * pw2[py] % mod * pw3[inall - px - py] % mod; 
					} else {
						bitset < M > v1 = (Z[s][0] & Z[s][1]) | (Z[s][2] & Z[s][3]), v2 = (Z[s][0] | Z[s][1]) & (Z[s][2] | Z[s][3]) & (all ^ v1);
						int px = v1.count(), py = v2.count();
						f[r][z][s] = 1LL * f[r][z][s] * pw2[py] % mod * pw3[inall - px - py] % mod; 		
					}
				}
			}
		}
		for (int z = 0; z <= 1; z++) {
			for (int s = 0; s <= xup - 1; s++) {
				ans = (ans + mod - f[r][z][s]) % mod;
			}
		}
	}
	cout << ans << "\n";
 	return 0;
}

LG P6072『MdOI R1』Path

给定一棵 \(n\) 个点的树,边有边权。求两条点集不交的路径,使得边权异或和之和最大。你只需要求出这个最大值。\(n \leq 3\times 10^4\)\(w \leq 10^9\)


一个简单的转化:我们以 \(1\) 为根,定义 \(a_x\)\(x\) 到根的路径上所有边边权的异或和,那么一条链 \((x,y)\) 的权值即为 \(a_x \oplus a_y\)。对于每个点 \(x\),设 \(p_x,q_x\) 分别为其子树内和子树外 \(a_x \oplus a_y\) 的最大值,那么答案即为 \(\{ p_i + q_i \}_{\max}\)

\(p_x\) 只需要用 01-trie 维护子树内异或最大值即可,利用启发式合并或者 DSU on Tree 等技巧,在加入一个元素的时候更新答案,这样能做到 \(\mathcal{O}(n \log n \log V)\)

但求 \(q_x\) 似乎有些困难。考虑原树中使得 \(a_x \oplus a_y\) 最大的两个点 \(x,y\),此时 \(q\) 可能不为 \(a_x \oplus a_y\) 的只有 \(x\) 到根,以及 \(y\) 到根上的所有点。于是我们只需要解决对一条链求出 \(q\),这很容易:从根往下每次加子树即可,这部分时间复杂度为 \(\mathcal{O}(n \log V)\)

进一步发现求 \(p_x\) 的过程也能够优化:对于 \(x\) 到根以及 \(y\) 到根上的 \(p\),用类似的方法容易求出。而对于其他挂在链上的子树中的点,由于 \(q\) 都是 \(a_x \oplus a_y\),而我们的目标是最大化 \(p_i+q_i\),因此只需要考虑 \(p_i\) 最大的点,即子树的根。又由于这些子树不交,这部分的时间复杂度也是 \(\mathcal{O}(n \log V)\)

总时间复杂度 \(\mathcal{O}(n \log V)\)

code
#include <bits/stdc++.h>
using namespace std;
typedef pair <int, int> pi;
constexpr int N = 3e4 + 5;
int n, a[N], p[N], q[N], ans; // p : in subtree, q : out of subtree
vector <pi> e[N];
int par[N], key[N];
struct Trie_with_position {
	int ch[N << 6][2], pos[N << 6];
	int tot = 1;
	int ns; pi ans;
	void ins(int x, int p) {
		int u = 1, ret = 0;
		for (int i = 30; i >= 0; i--) {
			int c = (x >> i) & 1;
			if (ch[u][c ^ 1]) ret |= (1 << i), c ^= 1;
			u = ch[u][c]; 
		}
		if (ret > ns) ns = ret, ans = make_pair(pos[u], p);
		u = 1;
		for (int i = 30; i >= 0; i--) {
			int c = (x >> i) & 1;
			if (!ch[u][c]) ch[u][c] = ++tot;
			u = ch[u][c];		
		}
		pos[u] = p;
	}
	void erase(int x) {
		pos[x] = 0;
		if (ch[x][0]) erase(ch[x][0]), ch[x][0] = 0;
		if (ch[x][1]) erase(ch[x][1]), ch[x][1] = 0;
	}
	void clr() {
		tot = 1, ns = 0, erase(1);
	} 
} all, tr;
void dfs0(int u, int ff) {
	for (auto [v, w] : e[u]) if (v != ff) {
		a[v] = a[u] ^ w, par[v] = u;
		dfs0(v, u);
	} 
}
void add_tree(int u, int ff) {
	tr.ins(a[u], u);
	for (auto [v, w] : e[u]) if (v != ff) add_tree(v, u);
}
void calc_q(int x) {
	tr.clr();
	static int seq[N], c;
	c = 0;
	while (x > 0) seq[++c] = x, key[x] = 1, x = par[x];
	if (c == 1) return;
	reverse(seq + 1, seq + c + 1);
	tr.ins(a[1], 1);
	for (int i = 2; i <= c; i++) {
		int u = seq[i - 1];
		for (auto [v, w] : e[u]) if (key[v] == 0) add_tree(v, u);
		q[seq[i]] = tr.ns;
		tr.ins(a[seq[i]], seq[i]);
	}
	for (int i = 1; i <= c; i++) key[seq[i]] = 0;
}
void calc_p(int x) {
	tr.clr();
	static int seq[N], c;
	c = 0;
	while (x > 0) seq[++c] = x, key[x] = 1, x = par[x];
	if (c == 1) return;
	add_tree(seq[1], seq[2]);
	p[seq[1]] = tr.ns;
	for (int i = 2; i <= c; i++) {
		int u = seq[i];
		for (auto [v, w] : e[u]) if (key[v] == 0) add_tree(v, u);
		tr.ins(a[u], u);
		p[u] = tr.ns;
	}
	for (int i = 1; i <= c; i++) key[seq[i]] = 0;
} 
signed main() {
	ios :: sync_with_stdio(false);
	cin.tie(nullptr);
	cin >> n;
	for (int i = 1, x, y, z; i < n; i++) {
		cin >> x >> y >> z;
		e[x].emplace_back(y, z);
		e[y].emplace_back(x, z);
	}
	dfs0(1, 0);
	for (int i = 1; i <= n; i++) all.ins(a[i], i);
	int tx, ty; 
	tie(tx, ty) = all.ans;
	calc_q(tx);
	calc_q(ty);
	int _tx = tx, _ty = ty;
	while (_tx > 0) key[_tx] = 1, _tx = par[_tx];
	while (_ty > 0) key[_ty] = 1, _ty = par[_ty];
	int seq[N], c = 0;
	for (int i = 1; i <= n; i++) if (key[i] == 0) {
		q[i] = tr.ns;
		bool ok = false;
		for (auto [v, w] : e[i]) if (key[v] == 1) { 
			ok = true; break;
		}
		if (ok == true) seq[++c] = i;
	}
	for (int i = 1; i <= c; i++) { 
		tr.clr();
		add_tree(seq[i], par[seq[i]]);
		p[seq[i]] = tr.ns;
	}
	for (int i = 1; i <= n; i++) key[i] = 0;
	calc_p(tx);
	calc_p(ty);
	for (int i = 2; i <= n; i++) ans = max(ans, p[i] + q[i]);
	cout << ans << "\n";
  	return 0;
}

LOJ #3968. 「JOISC 2023 Day1」护照

\(n\) 个国家,在第 \(i\) 个国家可以获得能够进入 \(i \in [l_i,r_i]\) 内所有国家的护照。\(q\) 次询问,每次求从第 \(x_i\) 个国家出发,至少需要获得多少护照,才能够到达所有国家。\(n,q \leq 2 \times 10^5\)


关键的性质是,由于每次拓展的是包含 \(i\) 的一个区间,因此任意时刻,能到达的点集都是一个区间。因此能够到达所有国家,等价于能够到达 \(1\)\(n\) 这两个国家。

不过这好像也没有想象中那么好做,不妨再考虑一个弱化:如果只要求能到达 \(1\) 怎么做。这很容易,建反图从 \(1\) 开始做 01-BFS,连边用线段树优化一下就行了。只要求能到达 \(n\) 也是类似的。

我们发现,要求某个点能够同时到达 \(1\)\(n\),分别考虑它到 \(1\)\(n\) 的最短路径,这两条路径会有一些重合的部分,把这部分缩起来就是答案了。具体来说,是先走一段共同的部分到达某个中继点 \(x\),然后从 \(x\) 开始分道扬镳去 \(1\)\(n\)。于是做法就呼之欲出了:先对每个点求出若以这个点为中继点,但不要求之后的路径没有重合,后续部分的最小代价。然后再跑一次 01-BFS 更新掉路径重合的那部分即可。

总时间复杂度 \(\mathcal{O}(n \log n)\)

code
#include <bits/stdc++.h>
using namespace std;
typedef pair <int, int> pi;
constexpr int N = 2e5 + 5;
int n, q, L[N], R[N];
vector <pi> e[N << 2]; int id[N << 2], tot, a[N << 2], b[N << 2], d[N << 2];
#define m ((l + r) >> 1)
void build(int x, int l, int r) {
	id[x] = ++tot;
	if (l == r) return e[l].emplace_back(id[x], 0), void();
	build(x << 1, l, m), build(x << 1 | 1, m + 1, r);
	e[id[x << 1]].emplace_back(id[x], 0);
	e[id[x << 1 | 1]].emplace_back(id[x], 0);
}
void add(int x, int l, int r, int ql, int qr, int v) {
	if (ql <= l && qr >= r) return e[id[x]].emplace_back(n + v, 0), void();
	if (ql <= m) add(x << 1, l, m, ql, qr, v);
	if (qr > m) add(x << 1 | 1, m + 1, r, ql, qr, v);
}
#undef m 
signed main() {
	ios :: sync_with_stdio(false);
	cin.tie(nullptr);
	cin >> n;
	tot = 2 * n;
	build(1, 1, n);
	for (int i = 1; i <= n; i++) {
		cin >> L[i] >> R[i];
		add(1, 1, n, L[i], R[i], i), e[n + i].emplace_back(i, 1);
	}
	for (int i = 1; i <= tot; i++) a[i] = 1e9;
	a[1] = 0;
	deque <int> que;
	que.push_front(1);
	while (!que.empty()) {
		int u = que.front(); que.pop_front();
		for (auto [v, w] : e[u]) {
			if (a[v] > a[u] + w) {
				a[v] = a[u] + w;
				if (w == 0) que.push_front(v);
				if (w == 1) que.push_back(v);
			}
		}
	}
	for (int i = 1; i <= tot; i++) b[i] = 1e9;
	b[n] = 0;
	que.push_front(n);
	while (!que.empty()) {
		int u = que.front(); que.pop_front();
		for (auto [v, w] : e[u]) {
			if (b[v] > b[u] + w) {
				b[v] = b[u] + w;
				if (w == 0) que.push_front(v);
				if (w == 1) que.push_back(v);
			}
		}
	}
	for (int i = 1; i <= tot; i++) d[i] = 1e9;
	for (int i = 1; i <= n; i++) d[i] = a[i] + b[i] - (1 < i && i < n);
	int seq[N];
	for (int i = 1; i <= n; i++) seq[i] = i;
	sort(seq + 1, seq + n + 1, [&](int i, int j) {
		return d[i] < d[j];
	});
	for (int i = 1; i <= n; i++) que.push_back(seq[i]);
	while (!que.empty()) {
		int u = que.front(); que.pop_front();
		for (auto [v, w] : e[u]) {
			if (d[v] > d[u] + w) {
				d[v] = d[u] + w;
				if (w == 0) que.push_front(v);
				if (w == 1) que.push_back(v);
			}
		}
	}
	cin >> q;
	while (q--) {
		int x; cin >> x;
		if (d[x] > n) cout << "-1\n";
		else cout << d[x] << "\n";
	}
   	return 0;
}

LOJ #3970. 「JOISC 2023 Day2」议会

\(n\) 个人给 \(m\) 项提案投票,\(a_{i,j}\)\(1\) 表示赞成,为 \(0\) 表示反对。议会的过程如下:

  • 抽签选一个主席。
  • 在剩下的人里抽签选一个副主席。
  • 在剩下的人中,如果有 \(\geq \lfloor \frac{n}{2} \rfloor\) 名议员同意某项提案,那么该提案就会被通过。

对于每个人,求出:若其被选为主席,能够通过的最大提案数。\(n \leq 3 \times 10^5\)\(m \leq 20\)


考虑当主席确定的时候该怎么做:容易发现此时副主席选谁只会影响得票数恰好是 \(\lfloor \frac{n}{2} \rfloor\) 的那些提案。设这些提案的集合为 \(S\)\(a_i\)\(i\) 号议员投票的状态,我们的目标是对每个 \(i\) 求出 \(\min_{j \neq i} |a_j \cap S|\)

\(j\neq i\) 的限制比较烦,一个套路是直接求出最大和次大值,查询的时候只需要检查一下取哪个就行了。由于 \(a_j \cap S \subseteq a_j,S\),因此一个想法是把 \(a_j\) 挂上去做高位后缀和,然后再做一遍高维前缀和,但是好像不是很好维护 \(\min\)。正难则反,把每个 \(a_i\) 取反然后求 \(\max\) 就行了。

具体来说,我们先高维后缀和求出可能最优的,满足 \(S \subseteq a'_j\) 的两个 \(j\),分别记作 \(g1_S,g2_S\),再做高维后缀和,对每个 \(S\) 求出 \(g1_S,g2_S\) 表示 \(|a'_j \cap S|\) 最大的两个 \(j\)

总时间复杂度 \(\mathcal{O}(m2^m + nm)\)

code
#include <bits/stdc++.h>
using namespace std;
typedef pair <int, int> pi;
constexpr int N = 3e5 + 5, M = 21;
int n, m, a[N], g1[1 << M], g2[1 << M], c[M];
signed main() {
	ios :: sync_with_stdio(false);
	cin.tie(nullptr);
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		for (int j = 1, x; j <= m; j++) {
			cin >> x;
			if (x) ++c[m - j];
			a[i] = (a[i] << 1) | (x ^ 1);
		}
		if (g1[a[i]]) g2[a[i]] = i;
		else g1[a[i]] = i;
	}
	for (int s = 1 << m; s >= 0; s--) {
		for (int i = 0; i < m; i++) if (!((s >> i) & 1)) { int t = s | (1 << i);
			if (!g1[s]) {
				g1[s] = g1[t], g2[s] = g2[t];
			} else {
				if (g1[t]) {
					if (g1[s] != g1[t]) g2[s] = g1[s], g1[s] = g1[t];
				} 
				if (g2[t]) {
					if (g2[s] != g2[t]) g2[s] = g2[t];
				}
			}
		}
	}
	for (int s = 0; s < 1 << m; s++) {
		for (int i = 0; i < m; i++) if ((s >> i) & 1) { int t = s ^ (1 << i);
			vector <int> v;
			v.push_back(g1[s]);
			v.push_back(g2[s]);
			v.push_back(g1[t]);
			v.push_back(g2[t]);
			sort(v.begin(), v.end(), [&](int i, int j) {
				return __builtin_popcount(a[i] & s) > __builtin_popcount(a[j] & s);
			});
			v.erase(unique(v.begin(), v.end()), v.end());
			g1[s] = v[0];
			if (v.size() > 1) g2[s] = v[1];
		}
	}
	for (int i = 1; i <= n; i++) {
		int ret = 0, s = 0;
		for (int j = 0; j < m; j++) {
			int nc = c[j];
			if (!((a[i] >> j) & 1)) nc--;
			if (nc > n / 2) ret++;
			if (nc == n / 2) s |= 1 << j;
		}
		if (g1[s] == i) {
			ret += __builtin_popcount(a[g2[s]] & s);
		} else {
			ret += __builtin_popcount(a[g1[s]] & s);
		}
		cout << ret << "\n";
	}
  	return 0;
}

LOJ #3972. 「JOISC 2023 Day3」合唱

给定一个长为 \(2n\) 的,仅包含 AB 的字符串 \(S\)。定义一个子序列是好的,当且仅当其中 AB 的数量相等,且所有 A 在所有 B 之前。每次操作可以交换相邻两个字符,求至少需要多少次操作才能使得 \(S\) 能够划分为恰好 \(k\) 个好子序列。\(k \leq n \leq 10^6\)


感觉完全做不来这种题.

首先发现 \(=k\)\(\leq k\) 其实是等价的,先考虑对于一个确定的串,怎样划分能够使得子序列的数量尽量少。这比较简单,只需要每次贪心的往后取就行了。然后我们考虑一下大概会怎么交换,好像每次是给一个区间做排序状物,但似乎没有什么好的刻画方法,于是突然就做不下去了。

考虑一个神秘观察:把 \(S\) 看成在 \(n \times n\) 的网格中的一个路径,其中 A 表示向右一步,B 表示向上一步,显然一次操作只会是把一个上右改成右上。当然有解的一个必要条件是它必须在对角线下方,我们把在对角线上方的部分改到对角线下方,代价提前计算掉,现在只考虑路径在对角线之下的情况。

考虑在路径上做上面那个贪心,相当于每次沿着路径往右走,碰到一个向上就一直向上直到碰到对角线再向右,两次拐弯衔接的地方可能会平移一部分路径,但这个是合法的。

于是要求 \(k\) 个子序列,相当于是拐弯了 \(k\) 次,每个拐弯对应一个区间。现在假设已经确定了最后的这 \(k\) 个拐弯长成什么样,我们发现达到这个状态的代价恰好就是夹在钦定的路径上方,原路径下方的格子数量。具体可以看下面这个从官方题解偷来的图。

\(w(l,r)\) 为区间 \([l,r]\) 对应拐弯,即从 \((l,l)\) 走到 \((r,r)\) 的代价,假设原点坐标是 \((1,1)\)。那么现在问题相当于是要划分成 \(k\) 个区间,最小化代价和。老套路了,容易验证它满足四边形不等式,所以整个问题是凸的。考虑 WQS 二分,之后用利用决策单调性分治 / SMAWK / LARSCH 等等优化转移技巧可以做到 \(\mathcal{O}(n \log^3 n)\)\(\mathcal{O}(n \log n)\) 不等的时间复杂度。但它们要么复杂度太高,要么代码太难写,都不是我们想要的。

让我们暂时忘掉决策单调性。考虑给 \(w(l,r)\) 一个更好的表征:设 \(t_i\) 表示第 \(i\) 向上前向右的次数,\(pre_i\) 表示 \(t_i\) 的前缀和,\(cnt_i\)\(sum_i\) 分别表示满足 \(t_j \leq i\)\(j\) 的个数和 \(t_j\) 的和,这些都容易 \(\mathcal{O}(n)\) 预处理。那么 \(w(l,r) = (cnt_{r-1} - l + 1) \times (r - 1) - sum_{r-1} + pre_{l-1}\)。拆开用斜率优化即可。

总时间复杂度 \(\mathcal{O}(n)\)。有时候知道得太多也不一定是一件好事。

code
#include <bits/stdc++.h>
using namespace std;
typedef pair <int, int> pi;
typedef long long LL;
constexpr int N = 2e6 + 5;
constexpr LL inf = 1e18;
int n, k, t[N], g[N]; LL cnt[N], sum[N], pre[N], f[N], ans, ns;
int q[N], hd, tl;
LL X(int i) { return i; }
LL Y(int i) {
	return f[i] + pre[i - 1] + i;
}
bool chk(LL m) {
	fill(f + 1, f + n + 2, inf);
	fill(g + 1, g + n + 2, k + 1);
	f[1] = g[1] = 0;
	q[hd = tl = 1] = 1;
	for (int i = 2; i <= n + 1; i++) {
		while (hd < tl && i * (X(q[hd + 1]) - X(q[hd])) > (Y(q[hd + 1]) - Y(q[hd]))) hd++;
		LL val = -X(q[hd]) * i + Y(q[hd]);
		f[i] = val + (cnt[i - 1] + 1) * (i - 1) - sum[i - 1] - m, g[i] = g[q[hd]] + 1;
		while (hd < tl && (Y(i) - Y(q[tl])) * (X(q[tl]) - X(q[tl - 1])) < (Y(q[tl]) - Y(q[tl - 1])) * (X(i) - X(q[tl]))) tl--;
		q[++tl] = i;
	} 
	ans = f[n + 1];
	return g[n + 1] <= k;
}
signed main() {
	ios :: sync_with_stdio(false);
	cin.tie(nullptr);
	cin >> n >> k;
	string str;
	cin >> str;
	str = ' ' + str;
	int cA = 0, cB = 0;
	for (int i = 1; i <= 2 * n; i++) {
		if (str[i] == 'A') ++cA;
		else ++cB, t[cB] = cA;
	}
	for (int i = 1; i <= n; i++) if (t[i] < i) ns += i - t[i], t[i] = i;
	for (int i = 1; i <= n; i++) pre[i] = pre[i - 1] + t[i];
	for (int i = 1; i <= n; i++) ++cnt[t[i]], sum[t[i]] += t[i];
	for (int i = 1; i <= n; i++) cnt[i] += cnt[i - 1], sum[i] += sum[i - 1];
	LL l = -5e11, r = 0, p = 0;
	while (l <= r) {
		LL m = (l + r) >> 1;
		if (chk(m)) p = m, l = m + 1;
		else r = m - 1;
	}
	chk(p);
	cout << ns + ans + 1LL * p * k << "\n";
  	return 0;
}
posted @ 2023-07-05 13:33  came11ia  阅读(58)  评论(0编辑  收藏  举报