Loading

Qez 补题记录

7.10

T2

考虑一个 bool 的 \(f_{i,j,t=0/1/2}\) 表示是否存在至少一个点 \(c\) 满足 \(c \rightarrow i\) 的长度分别为 \(c \rightarrow j\) 的长度的两倍,两倍加一,两倍减一。转移考虑直接 bfs 即可,如果只设置 \(t = 0\) 那时间是会爆炸的。
时间复杂度 \(\mathcal{O}(n(n+m))\).

for (int i = 1; i <= m; i++) {
		int u, v;
		cin >> u >> v;
		e[u].push_back(v);
	}
	for (int i = 1; i <= n; i++) { vis[i][i][0] = 1; q.push({i, i, 0}); }
	int ans = n;
	while (!q.empty()) {
		Node nd = q.front(); q.pop();
		int u = nd.u, v = nd.v, t = nd.t;
		if (!t) {
			for (auto x : e[u]) {
				if (!vis[x][v][1]) {
					vis[x][v][1] = 1;
					q.push({x, v, 1});
				}
			}
		} else {
			int nxt = (t + 1) % 3;
			for (auto x : e[v]) {
				if (!vis[u][x][nxt]) {
					vis[u][x][nxt] = 1;
					if (nxt == 0) ans++;
					q.push({u, x, nxt});
				}
			}
		}
	}
	cout << ans << '\n';

T3

首先一定有解。
考虑一个不会证的感性做法:先倍长序列,然后 \(i \rightarrow 2i - 1/2i\) 连边,不取模,特别地,\(0\)\(2n-1\) 连边,然后易得每个点 \(m\) 的入度都是 \(1\), \(m\) 为奇数时入点是 \(\dfrac{m+1}{2}\), 偶数时是 \(\dfrac{m}{2}\), 所以现在知道起点 \(0\), 只需要知道终点就可以推出所有点,然后发现终点一定是 \(\dfrac{n}{2}\), 因为它只能到 \(n = 0\)\(n-1 = 2n - 1\)(模 \(n\) 意义下),但是后者显然开头就有,所以是前者,正好是回路。
细节:如果跳到访问过的点,直接让这个点 \(+n\), 主要这里不知道咋证的.

void solve(int n) {
	tot = 0;
	int cnt = n;
	for (int i = 0; i < n; i++) vis[i] = 0;
	int now = n / 2;
	while (cnt) {
		vis[now] = 1;
		cnt--;
		c[++tot] = now;
		if (vis[(now + 1) >> 1]) now = ((now + n + 1) >> 1) % n;
		else now = ((now + 1) >> 1) % n;
	}
	for (int i = tot; i >= 1; i--) cout << c[i] << ' ';
	cout << "\n";
}

T4

原:P6846
神秘题。
考虑到如果有一种定向的方式是 DAG, 那么所有边反着定向也会是 DAG, 两种加起来会改变 \(m\) 条边,所以平均改变 \(\dfrac{m}{2}\) 条边,方案数一定是偶数,即转化成求方案数。
考虑 dp, \(dp_s\) 表示 \(s\) 集合里的数定向成 DAG 的方案数,枚举子集 \(t\) 表示当前入度为 \(0\) 的点,显然要求是独立集,然后有可能会算重,所以要容斥,系数是 \((-1)^{|t|}\).
时间复杂度 \(\mathcal{O}(3^n+2^nm)\), 后者是预处理是否是独立集.

cin >> n >> m;
	for (int i = 1; i <= m; i++) {
		cin >> e[i].u >> e[i].v;
	}
	for (int s = 0; s < (1 << n); s++) {
		fac[s] = (__builtin_popcount(s) & 1) ? 1 : mod - 1;
		for (int i = 1; i <= m; i++) if ((s >> (e[i].u - 1) & 1) && (s >> (e[i].v - 1) & 1)) { b[s] = 1; break; }
	}
	dp[0] = 1;
	for (int s = 1; s < (1 << n); s++) {
		for (int t = s; t; t = s & (t - 1)) if (!b[t]) {
			add(dp[s], (1ll * fac[t] * dp[s ^ t] % mod + mod) % mod);
		}
	}
	cout << 1ll * dp[(1 << n) - 1] * m % mod * qpow(2, mod - 2) % mod << '\n';

7.11

T2

放在笛卡尔树上,两个点直接的最大值在 lca 处取到,直接树形背包即可。

int build(int l, int r) {
	if (l > r) return 0;
	if (l == r) return l;
	int mx = 0, p = -1;
	for (int i = l; i <= r; i++) {
		if (a[i] > mx) mx = a[i], p = i;
	}
	ls[p] = build(l, p - 1), rs[p] = build(p + 1, r);
	return p;
}
inline void dfs(int u, int fa) {
	if (!u) return ;
	sz[u] = 1;
	f[u][0] = 0;
	dfs(ls[u], u);
	dfs(rs[u], u);
	sz[u] += sz[ls[u]];
	sz[u] += sz[rs[u]];
	for (int i = 0; i <= sz[ls[u]]; i++) {
		for (int j = 0; j <= sz[rs[u]]; j++) {
			f[u][i + j] = min(f[u][i + j], f[ls[u]][i] + f[rs[u]][j] + 1ll * i * j * a[u]);
			f[u][i + j + 1] = min(f[u][i + j + 1], f[ls[u]][i] + f[rs[u]][j] + (1ll * i * j + i + j + 1) * a[u]);
		}
	}
}

T3

正常写就是一个背包,按钮相当于一条连向 \(u \oplus s_i\) 的边,选或不选每个按钮跑 01 背包即可。
考虑人类智慧,由于数据随机,我们按照 \(t_i\) 升序排序,然后如果当前的 \(f_{s_i}\) 已经 \(\le t_i\) 了,说明这个按钮可以用其它按钮替换,直接跳过,然后不知道为啥飞快。根据老师的说法可以自己跑一下看看真正访问的频率是多少。

中间这一段我全鸽了。。

7.16

T1

考虑贪心。倒序之后对于 J/O/I 分别考虑,J 直接和 OI 匹配不劣,I 有两种情况:一种是作为开头,和 OI 匹配,一种是留着等 O 和其匹配成 OI, 然后发现如果留着的 I 和还存在的 OI 已经比前缀的 I 和 J 要多了,那么 I 再等待是没有意义的,有 OI 就让其成为开头,否则等着。

for (int i = 1; i <= n; i++) {
    	pre[i] = pre[i - 1] + (s[i] == 'I' || s[i] == 'J');
	}
    for (int i = n; i; i--) {
    	if (s[i] == 'J' && oi) oi--, ans++;
    	if (s[i] == 'O' && ci) ci--, oi++; // 这里的 I 已经没用了
		if (s[i] == 'I') {
			if (oi + ci >= pre[i] && oi) oi--, ans++; // 这时作开头没有任何意义
			else ci++; 
		} 
	}

T2

手玩阳历发现一个点如果出度 \(\ge 2\) 那么连出的点之间会两两连边,然后直接 bfs 更新连边即可,并查集维护,最后不在联通块的边记得加上。

cin >> n >> m;
	for (int i = 1; i <= n; i++) fa[i] = i, sz[i] = 1;
	for (int i = 1, u, v; i <= m; i++) {
		cin >> u >> v;
		edge[i] = {u, v};
		e[u].push_back(v);
		out[u]++;
	}
	for (int u = 1; u <= n; u++) if (out[u] >= 2) {
		for (auto v : e[u]) {
			merge(v, e[u][0]);
			if (!vis[v]) vis[v] = 1, q.push(v);
		}
	}
	while (!q.empty()) {
		int u = q.front(); q.pop();
		for (auto v : e[u]) {
			merge(u, v);
			if (!vis[v]) vis[v] = 1, q.push(v);
		}
	}
	for (int i = 1; i <= n; i++) if (fa[i] == i) {
		ans += 1ll * sz[i] * (sz[i] - 1);
	}
	for (int i = 1; i <= m; i++) {
		int u = edge[i].u, v = edge[i].v;
		if (find(u) != find(v)) ans++;
	}
	cout << ans << '\n';

T3

用完魔法之后,期望是不会变的,所以要稳扎稳打,每次只用一个,那如果最坏情况还是不行说明前 \(m\) 轮积攒的不够后面花了,即 \(-1\) 的条件为 \(2^m < n-m\), 接下来考虑有魔法,首先肯定要直接梭哈,如果钱花光了可以回溯,即变成 \(m \times 2\), 但是有 \(\dfrac{1}{2}\) 的概率消耗一次魔法。那么考虑第 \(i\) 轮把魔法用完,概率为

\[\frac{\dbinom{i}{m}-\dbinom{i-1}{m}}{2^i} \]

记为 \(p_i\), 那么对于前 \(n-1\) 轮,是可以直接用 \(2^i\times p_i\) 算出期望的,正好等于

\[\dbinom{i}{m}-\dbinom{i-1}{m}=\dbinom{i-1}{m-1} \]

但我们要在第 n 轮顺便把到最后也没用完魔法的概率算上,概率为

\[\large{1-\sum_{i=m}^{n}p_i} \]

答案即为

\[\large{2^n \times (1-\sum_{i=m}^{n}p_i) + \binom{n}{m}} \]

接下来是一个逆天化简,我不太清楚,以后有时间再写吧

7.21

T3

提供一个爆标做法,可以过 \(\mathcal{O}(n \log n \log V)\), 思路源自 @A_zjzj.

首先同样的,要按 \(v_i\) 升序排序,然后考虑二分答案,这里不同的是,要记的是当前选择的物品最多能抵挡多少次魔法使得答案仍然满足二分的大小,那么反悔贪心,遇到不能的,可以把前面的一个换掉,显然不劣。

有一些细节,比如 int128.

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll, ll> pll;
const int N = 2e5 + 10, mod = 998244353;
template<typename T>
void dbg(const T &t) { cerr << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
    #ifdef ONLINE_JUDGE
        return ;
    #endif
    cerr << arg << ' ';
    dbg(args...);
}
#define fi first
#define se second
int T, n, m;
pll a[N];
bool cmp(const pll &A , const pll &B) {
	return A.se < B.se;
}
bool check(ll x) {
	int cnt = 0;
	__int128 sum = 0;
	priority_queue<ll>q;
	for (int i = 1; i <= n; i++) {
		if (a[i].se - a[i].fi - sum >= x) {
			cnt++;
			sum += a[i].fi;
			q.push(a[i].fi);
		} else {
			if (!q.empty() && q.top() > a[i].fi) {
				sum -= q.top() - a[i].fi;
				q.pop();
				q.push(a[i].fi);
			}
		}
	}
	return cnt > m;
}
int main() {
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
	cin >> T;
	while (T--) {
		cin >> n >> m;
		for (int i = 1; i <= n; i++) cin >> a[i].se >> a[i].fi; // 顺序与一般不同
		sort(a + 1, a + n + 1, cmp);
		ll L = 0, R = 1e18 + 1e10; // 这里如果写 1e18 + 1 要改成 (ll)1e18 + 1 否则精度问题会识别成 1e18
		while (L + 1 < R) {
			ll mid = (L + R) >> 1;
			if (check(mid)) L = mid;
			else R = mid;
		}
		cout << L << '\n';
	}
    return 0;
}

同步发表于 P4765 题解区。

7.22

T3

首先要用类似二分图染色的东西判断有无奇环/偶环,然后这样判偶环还是不够的,还需要跑一下 dfs 生成树,对于每条返祖边,覆盖边上的节点,显然当且仅当被覆盖次数 \(\ge 2\) 的点在偶环上,树上差分即可。
然后分讨,如果有偶环则看看是否有奇环,有答案为 \(1\), 否则观察图上最长路径点数,\(\ge 4\) 时可以连一条边构造偶环,\(\ge 3\) 时可以构造奇环,然后注意,先判奇环,如果有只要加偶环,否则先把奇环要加的边加上,比如有 \(\ge 3\) 朵花瓣的菊花,连完奇环要连的边就有点数 \(\ge 4\) 的路径了,这里在 \(\ge 3\) 时直接判了一下。点数 \(< 4\) 时需要先达到 \(4\) 再构造。
如何判断点数能否达到为 \(4\)? 考虑枚举中间的边,然后找到两点的出点,显然至多保留三个,暴力即可。

int n, m, vis[N], c[N], dep[N], odc, evc;
pii ed[M];
vector<int>e[N];
void dfs1(int u, int fa) {
	vis[u] = 1;
	for (auto v : e[u]) if (v != fa) {
		if (!vis[v]) {
			dep[v] = dep[u] + 1;
			dfs1(v, u);
		} else if (dep[u] > dep[v]) {
			if ((dep[u] & 1) == (dep[v] & 1)) odc = 1;
			else evc = 1;
			c[u]++; c[v]--;
		}
	}
}
void dfs2(int u, int fa) {
	vis[u] = 1;
	for (auto v : e[u]) if (v != fa) {
		if (!vis[v]) {
			dfs2(v, u);
			c[u] += c[v];
		}
	}
	if (c[u] > 1) evc = 1;
}
bool chk4() {
	for (int i = 1; i <= m; i++) {
		int u = ed[i].first, v = ed[i].second;
		for (int x = 0; x < 3 && x < e[u].size(); x++) for (int y = 0; y < 3 && y < e[v].size(); y++) 
			if (e[u][x] != v && e[v][y] != u && e[u][x] != e[v][y]) return 1;
	}
	return 0;
}
bool chk3() {
	for (int i = 1; i <= n; i++) if (e[i].size() >= 2) return 1;
	return 0;
}
int main() {
//	freopen("graph.in", "r", stdin);
//	freopen("graph.out", "w", stdout);
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
	clock_t c = clock();
	cin >> n >> m;
	for (int i = 1, u, v; i <= m; i++) {
		cin >> u >> v;
		ed[i] = {u, v};
		e[u].emplace_back(v);
		e[v].emplace_back(u);
	}
	if (n < 4) {
		cout << "-1\n"; return 0;
	}
	if (m <= 2) {
		cout << 5 - m << '\n'; return 0;
	}
	for (int i = 1; i <= n; i++) if (!vis[i]) dfs1(i, 0);
	fill(vis + 1, vis + n + 1, 0);
	for (int i = 1; i <= n; i++) if (!vis[i]) dfs2(i, 0);
	if (evc) {
		cout << (odc ^ 1) << '\n';
	} else {
		int len = chk4() ? 4 : (chk3() ? 3 : 2);
		if (odc) cout << 5 - len << '\n';
		else {
			if (len == 4) cout << 2 << '\n';
			else if (len == 3) {
				bool ok = 0;
				for (int i = 1; i <= n; i++) if (e[i].size() >= 3) { ok = 1; break; }
				cout << (ok ? 2 : 3) << '\n'; 
			} else cout << (m == 1 ? 4 : 3) << '\n';
		}
	}
	return 0;
}

7.23

T2

\(f_{i, j}\) 表示考虑到前 \(i\) 个点,深度为 \(j\) 的方案数。
首先根据期望的性质,期望的答案是 \(\frac{1}{(n-1)!}\sum f_{n, i}\), 分数正好约掉。
考虑 \(1, 2\) 肯定是固定的,于是 \(dp\) 时分讨加入的点是否在 \(2\) 为根的子树内。
时间复杂度 \(\mathcal{O}(n^3)\).

C[0][0] = 1;
for (int i = 1; i <= n; i++) {
	C[i][0] = C[i][i] = 1;
	for (int j = 1; j < i; j++) C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % p;
}
f[1][1] = f[2][2] = 1;
for (int i = 1; i <= n; i++) s[1][i] = 1, s[2][i] = (i > 1); 
ll ans = 0, fac = 1;
for (int i = 3; i <= n; i++) {
	for (int j = 2; j <= i; j++) {
		// 分别考虑在 2 为根的子树内/外
		for (int k = 1; k <= i - 2; k++) { // 2 为根的子树节点数 
			f[i][j] = (f[i][j] + s[k][min(k, j - 2)] * f[i - k][j] % p * C[i - 2][k - 1] % p) % p;
		}
		for (int k = 1; k < i; k++) {
			f[i][j] = (f[i][j] + f[k][j - 1] * s[i - k][j] % p * C[i - 2][k - 1] % p) % p;
		}
		s[i][j] = (s[i][j - 1] + f[i][j]) % p;
	}
	for (int j = i + 1; j <= n; j++) s[i][j] = s[i][j - 1];
}
for (int i = 1; i <= n; i++) ans = (ans + f[n][i] * i % p) % p;

T3

注意到每个数最多只会被前面四种不同数字转移到,否则可以任意插入一个,不劣。

7.24

T2

\(p_i\) 表示 \(i\) 之后,下一个 \(f_k=f_i\)\(k\) 之前,最后一个 \(f_i+1\) 的位置,考虑一些偏序关系:

\[\large{\forall k < i \land f_k = f_i, a_i < a_k} \]

\[\large{a_{p_i}>a_i} \]

然后就可以求出最大值了。

	for (int i = 1; i <= n; i++) {
		cin >> f[i];
		las[f[i]] = i;
		if (f[i] > 1) pre[i] = las[f[i] - 1];
		sz[i] = 1;
		cnt[i] = n;
	}
	for (int i = n; i >= 1; i--) if (pre[i]) {
		sz[pre[i]] += sz[i];
	} 
	for (int i = 1; i <= n; i++) {
		int ans = cnt[f[i]] - (sz[i] - 1); // 自己不算 
		cnt[f[i]] = ans - 1;
		cout << ans << ' '; 
	}
	cout << '\n';

T3

缺一分治。考虑 ban 掉的点在区间 \([l, r]\) 中的答案,每次使用 Floyd 可以 \(\mathcal{O}(n^2)\) 更新两点之间最短路,然后在递归左半区间的时候把右边更新了,右半同理,叶子节点计算答案。
时间复杂度 \(\mathcal{O}(n^3 \log n)\).

ll f[10][N][N], ans[500010];
struct Query {
	int id, s, t;
};
vector<Query>q[N];
void solve(int l, int r, int d) {
	if (l == r) {
		for (auto [id, s, t] : q[l]) ans[id] = f[d - 1][s][t];
		return ;
	}
	int mid = (l + r) >> 1;
	for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) f[d][i][j] = f[d - 1][i][j];
	for (int k = mid + 1, k <= r; k++) {
		for (int i = 1; i <= n; i++) {
			ll t = f[d][i][k];
			for (int j = 1; j <= n; j++) f[d][i][j] = min(f[d][i][j], t + f[d][k][j]);
		}
	}
	solve(l, mid, d + 1);
	for (int k = l, k <= mid; k++) {
		for (int i = 1; i <= n; i++) {
			ll t = f[d][i][k];
			for (int j = 1; j <= n; j++) f[d][i][j] = min(f[d][i][j], t + f[d][k][j]);
		}
	}
	solve(mid + 1, r, d + 1);
}
int main() {
	freopen("war.in", "r", stdin);
	freopen("war.out", "w", stdout);
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
	cin >> n >> Q;
	for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) cin >> f[0][i][j];
	for (int i = 1; i <= Q; i++) {
		int s, t, u;
		q[u].push_back({i, s, t});
	}
	solve(1, n, 1);
    for (int i = 1; i <= n; i++) cout << ans[i] << '\n';
	return 0;
}

T4

个人认为非常好的一道题,有多步转化。
首先显然修改一次 \(c\) 可以改成修改 \(c\)\(1\), 然后每个点的操作次数就固定了,记为 \(g_i\).
考虑两个数组相同的情况,\(a_i\) 最后会变成 \(a'_i + (n - 1)g_i - \sum_{j \neq i} g_j = a_i + ng_i - \sum g_j\), 如果所有 \(g_i\)\(-1\), \(a\) 显然不变,所以可以钦定一个 \(g_i\)\(0\).
然后观察,发现这种情况下,\(g\)\(a\) 是一一对应的,因为存在 \(g_p=0\), 所以 \(a_p-a'_p=\sum g_i\) 为最大值,那么就可以每次找到最大,还原出所有的 \(g\).
考虑判断一个 \(g\) 是否合法,转化成初始 \(\forall g_i=0\), 每次 \(g_i \leftarrow g_i+1\), 然后要求 \(\forall a'_i=a_i - ng_i + \sum g_j \ge 0\).
设操作了 \(t = \sum g_i\) 次, 就要满足 \(\forall a_i - ng_i \ge t\), 每次肯定选当前最小的 \(a_i-ng_i\)\(g_i \leftarrow g_i + 1\), 这些操作是必须要做的,可以用排序和双指针实现,然后需要保证剩下的 \(g_i\) 存在 \(0\), 可以考虑容斥,减掉不存在 \(0\) 的情况,即先给剩下的 \(g_i\)\(+1\), 再插板法。
时间复杂度 \(\mathcal{O}(n \log n + \max a_i)\)

// N = n + V = 1.1e6
int n, x, a[N], b[N];
ll fac[N], ifac[N];
ll qpow(ll a, int b) {
    ll res = 1;
    for (; b; b >>= 1, a = a * a % mod) if (b & 1) res = res * a % mod;
    return res;
}
ll C(int n, int m) {
    return fac[n] * ifac[n - m] % mod * ifac[m] % mod;
}
int calc(int m) {
    return (C(m + n - 1, n - 1) - (m >= n - x ? C(m - (n - x) + n - 1, n - 1) : 0) + mod) % mod; // 先让剩下 (n - x) 个至少是 1
}
int main() {
	freopen("rebirth.in", "r", stdin);
	freopen("rebirth.out", "w", stdout);
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
	cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    sort(a + 1, a + n + 1);
    fac[0] = 1;
    int m = n + a[n];
    for (int i = 1; i <= m; i++) fac[i] = fac[i - 1] * i % mod;
    ifac[m] = qpow(fac[m], mod - 2);
    for (int i = m; i; i--) ifac[i - 1] = ifac[i] * i % mod;
    int l = 1, r = 0, ans = 0;
    for (int i = 0; ; i++) {
        while (r <= i && x < n && a[x + 1] < i) b[++r] = a[++x] + n;
        while (r <= i && r && b[l] < i) b[++r] = b[l++] + n;
        if (r > i || x == n) break;
        ans = (ans + calc(i - r)) % mod;
    }
    cout << ans << '\n';
    return 0;
}
posted @ 2025-07-10 18:30  循环一号  阅读(37)  评论(0)    收藏  举报