deaf的杂题选讲

放个deaf语录

deaf:建议大家平时练习找题不要找太简单的,比如CF就找3000+,洛谷的话就选黑紫题就好了

注: 在座的有新初三的不到省一水平的同学。

[TJOI2019]甲苯先生的线段树

这题题面简直就是刻在DNA里的点分治(路径为某个数的路径条数)。

我们考虑借鉴点分治的思想,枚举 lca,然后在考虑两个子树各引出一条链拼接起来。

然而发现十分的不可做。

如果想使得 \(p\) 的左右儿子分别引出了一条长度为 \(i,j\) 的链(链分别从左儿子和右儿子开始算起),且这两条链加上 \(p\) 的编号和为 \(s\),那么注意到若 \(s,i,j\) 已知,我们可以分别讨论这两条链全往左儿子走和全往右儿子走的编号和,那么 \(s\) 一定在这两者之间。解一下发现 \(p\) 可以直接唯一确定。(目前还是没搞懂这玩意儿是怎么注意到的)。

剩下的就非常简单了。枚举 \(i,j\) 后考虑在每个深度往左和往右对答案的贡献,不难发现我们就是要用 \(2-1,2^2-1...2^{i-1}-1,2-1,2^2-1...2^{j-1}-1\) 凑出一个数。枚举用了多少个数转化为数位 dp 模板就行了。

复杂度 \(O(n^5)\),但上界非常松,跑得飞快。

#include <cstdio>
#include <algorithm>
#include <cstring>

inline int min(const int x, const int y) {return x < y ? x : y;}
inline int max(const int x, const int y) {return x > y ? x : y;}
typedef long long ll;

int Log(ll n) {
	int x = 0;
	while (n != 1) n >>= 1, ++ x;
	return x;
}
ll getdis(ll a, ll b) {
	if (Log(a) < Log(b)) a ^= b ^= a ^= b;
	int tmp = Log(a) - Log(b);
	ll ans = 0;
	while (tmp --) ans += a, a >>= 1;
	if (a == b) return ans + a;
	while ((a >> 1) != (b >> 1)) ans += a + b, a >>= 1, b >>= 1;
	return ans + a + b + (a >> 1);
}
ll dp[55][105][2], c[105];
ll solve(ll n, int a, int b) {
	if (!n) return 1;
	if (a < 0) a = 0;
	if (b < 0) b = 0;
	memset(c, 0, sizeof c);
	for (int i = 1; i <= a; ++ i) ++ c[i];
	for (int i = 1; i <= b; ++ i) ++ c[i];
	ll ans = 0;
	for (int k = 0; k <= a + b; ++ k) if (!(n + k & 1ll)) {
		int lim = Log(n + k);
		dp[0][0][0] = 1;
		for (int i = 1; i <= lim; ++ i)
		for (int j = 0; j <= 2 * i && j <= a + b; ++ j) {
			dp[i][j][0] = dp[i][j][1] = 0;
			if (n + k & 1ll << i) {
				dp[i][j][0] = dp[i - 1][j][1];
				if (c[i] && j >= 1) dp[i][j][0] += dp[i - 1][j - 1][0] * c[i];
				if (c[i] == 2 && j >= 2) dp[i][j][1] = dp[i - 1][j - 2][1];
			} else {
				dp[i][j][0] = dp[i - 1][j][0];
				if (c[i] && j >= 1) dp[i][j][1] = dp[i - 1][j - 1][1] * c[i];
				if (c[i] >= 2 && j >= 2) dp[i][j][1] += dp[i - 1][j - 2][0];
			}
		}
		ans += dp[lim][k][0];
	}
	return ans;
}

int main() {
	int _;
	scanf("%d", &_);
	while (_ --) {
		int n, type;
		ll a, b, s = 0;
		scanf("%d%lld%lld%d", &n, &a, &b, &type);
		s = getdis(a, b);
		if (type == 1) {printf("%lld\n", s); continue;}
		ll ans = 0;
		for (int i = 0; i < n; ++ i)
		for (int j = 0; j < n; ++ j) {
			ll p = (s + 1 - (1ll << j)) / ((1ll << i + 1) + (1ll << j + 1) - 3);
			if (p <= 0) break;
			if (Log(p) + max(i, j) >= n) continue;
			if (((1ll << i + 1) + (1ll << j + 1) - 3) * p + (1ll << j + 1) - 3 - i - j + (1ll << i) < s) continue;
			ans += solve(s - ((1ll << i + 1) + (1ll << j + 1) - 3) * p - (1ll << j) + 1, i - 1, j - 1);
		}
		printf("%lld\n", ans - 1);
	}
}

[十二省联考 2019] 皮配

如果没有学校有偏好,就可以把阵营与派系分开考虑。

对于有偏好的学校,注意到可以首先确定无限制学校城市的阵营与派系,然后确定无限制学校的派系,这些都可以 \(O(nm)\) 地完成。再用暴力 dp 算出被限制学校的方案即可。总复杂度 \(O(nm+mk^2s)\)

其实是个很板的背包

#include <cstdio>
#include <cstring>
#include <vector>

const int mod = 998244353;
inline int min(const int x, const int y) {return x < y ? x : y;}
inline int max(const int x, const int y) {return x > y ? x : y;}
inline void add(int &x, const int y) {
	if ((x += y) >= mod) x -= mod;
}
int f[2505], g[2505], b[1005], s[1005], dislike[1005], sum[2505][2505], dp[2505][305], tmp[2505][305];
std::vector<int> sch[1005];
std::vector<std::pair<int, int> > dsch[1005];
inline int getsum(int x1, int y1, int x2, int y2) {
	x1 = max(x1, 0), y1 = max(y1, 0);
	if (x1 > x2 || y1 > y2) return 0;
	++ x1, ++ y1, ++ x2, ++ y2;
	return (1ll * sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1]) % mod;
}

int main() {
	int _;
	scanf("%d", &_);
	while (_ --) {
		memset(f, 0, sizeof f);
		memset(g, 0, sizeof g);
		memset(dp, 0, sizeof dp);
		memset(dislike, -1, sizeof dislike);
		int n, m, c0, c1, d0, d1, tot = 0, ans = 0;
		scanf("%d%*d%d%d%d%d", &n, &c0, &c1, &d0, &d1);
		m = max(c0, max(c1, max(d0, d1)));
		for (int i = 1; i <= n; ++ i) sch[i].clear(), dsch[i].clear();
		for (int i = 1; i <= n; ++ i) scanf("%d%d", b + i, s + i), tot += s[i];
		int k;
		scanf("%d", &k);
		for (int i = 1; i <= k; ++ i) {
			int id, p;
			scanf("%d%d", &id, &p);
			dislike[id] = p;
		}
		for (int i = 1; i <= n; ++ i)
			if (dislike[i] == -1) sch[b[i]].push_back(s[i]);
			else dsch[b[i]].push_back(std::make_pair(s[i], dislike[i]));
		f[0] = g[0] = 1;
		for (int i = 1; i <= n; ++ i)
			if (dsch[i].empty()) {
				if (sch[i].empty()) continue;
				int sum = 0;
				for (int j : sch[i]) {
					sum += j;
					for (int k = d0; k >= j; -- k) add(g[k], g[k - j]);
				}
				for (int k = c0; k >= sum; -- k) add(f[k], f[k - sum]);
			} else
				for (int j : sch[i])
					for (int k = d0; k >= j; -- k) add(g[k], g[k - j]);
		dp[0][0] = 1;
		int tot2 = 0;
		for (int i = 1, now = 0; i <= n; ++ i) if (dsch[i].size()) {
			int sum = 0, sum2 = 0;
			for (int j : sch[i]) sum += j;
			for (auto j : dsch[i]) sum += j.first, sum2 += j.first;
			now += sum, tot2 += sum2;
			int limc = min(c0, now), limd = min(d0, tot2);
			for (int j = 0; j < sum; ++ j) memset(tmp[j], 0, limd + 2 << 2);
			for (int j = sum; j <= limc; ++ j) memcpy(tmp[j], dp[j - sum], limd + 2 << 2);
			for (auto j : dsch[i])
			for (int x = limc; x >= 0; -- x)
			for (int y = limd; y >= 0; -- y) {
				if (j.second == 3) dp[x][y] = 0;
				if (j.second != 2 && y >= j.first) add(dp[x][y], dp[x][y - j.first]);
			}
			for (auto j : dsch[i])
			for (int x = limc; x >= sum; -- x)
			for (int y = limd; y >= 0; -- y) {
				if (j.second == 1) tmp[x][y] = 0;
				if (j.second != 0 && y >= j.first) add(tmp[x][y], tmp[x][y - j.first]);
			}
			for (int i = 0; i <= limc; ++ i)
				for (int j = 0; j <= limd; ++ j) add(dp[i][j], tmp[i][j]);
		}
		for (int i = 1; i <= m + 1; ++ i)
		for (int j = 1; j <= m + 1; ++ j)
			sum[i][j] = (1ll * sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + 1ll * f[i - 1] * g[j - 1]) % mod;
		for (int i = 0; i <= c0 && i <= tot; ++ i)
		for (int j = 0; j <= d0 && j <= tot2; ++ j) if (dp[i][j])
			ans = (ans + 1ll * dp[i][j] * getsum(tot - c1 - i, tot - d1 - j, c0 - i, d0 - j)) % mod;
		printf("%d\n", (ans + mod) % mod);
	}
	return 0;
}

[HNOI2019]校园旅行

神仙题。

在一个回文串两端加上同等数量的 1 可以变成另一个回文串。而一条边可以重复经过,所以如果左边可以加两个 \(1\),右边可以加 \(4\)\(1\),那我只需要再左边多走一遍就可以使得两边加的 \(1\) 个数一样了。

这提示我们只需要考虑 \(1\) 个数的奇偶性。

暴论:看到奇偶性,直接想到二分图。

考虑把两端都是 \(1\) 的点的边全部提出来,对于它们构成的每个连通块,如果这个连通块是一个二分图,不管怎么走,则只需要保留一颗生成树。正确性显然。如果不是二分图,那么不管走奇数还是偶数条边,我都可以随意跳到图上的任何一个点,所以我们加一个自环即可。

两端都是 \(0\) 的同理。两端不同的由于建出来一定是一个二分图,根本不需要考虑上面的东西,直接保留一颗生成树即可。

这样最多只有 \(3n\) 条无向边。大力 bfs 即可。

#include <cstdio>
#include <numeric>
#define gc (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1 ++)

char buf[100000], *p1, *p2;
inline int read() {
	char ch;
	int x = 0;
	while ((ch = gc) < 48);
	do x = x * 10 + ch - 48; while ((ch = gc) >= 48);
	return x;
}
char str[5005];
struct Edge {int to, nxt;} e[30005];
struct node {int x, y;} Q[12505005];
int head[5005], hd, tl, n, m, q, tot;
bool mark[5005][5005];
inline void AddEdge(int u, int v) {
	e[++ tot].to = v, e[tot].nxt = head[u], head[u] = tot;
}
struct Connector {
	int from[500005], to[500005], fa1[5005], fa2[10005], cnt;
	bool mark[500005];
	int find1(int x) {return fa1[x] == x ? x : fa1[x] = find1(fa1[x]);}
	int find2(int x) {return fa2[x] == x ? x : fa2[x] = find2(fa2[x]);}
	inline void merge1(int u, int v) {
		u = find1(u), v = find1(v);
		if (u != v) fa1[u] = v;
	}
	inline void merge2(int u, int v) {
		u = find2(u), v = find2(v);
		if (u != v) fa2[u] = v;
	}
	inline void add(int u, int v) {from[++ cnt] = u, to[cnt] = v;}
	void connect() {
		std::iota(fa1 + 1, fa1 + n + 1, 1);
		for (int i = 1; i <= cnt; ++ i)
			if (find1(from[i]) != find1(to[i]))
				merge1(from[i], to[i]), AddEdge(from[i], to[i]), AddEdge(to[i], from[i]);
		std::iota(fa2 + 1, fa2 + 2 * n + 1, 1);
		for (int i = 1; i <= cnt; ++ i) if (!mark[find1(from[i])]) {
			if (find2(from[i]) == find2(to[i])) mark[find1(from[i])] = true, AddEdge(from[i], from[i]);
			merge2(from[i], to[i] + n), merge2(from[i] + n, to[i]);
		}
	}
} b, w, bw;
void bfs() {
	hd = 1, tl = 0;
	for (int i = 1; i <= n; ++ i) mark[i][i] = true, Q[++ tl] = node{i, i};
	for (int i = 1; i <= n; ++ i)
	for (int j = head[i]; j; j = e[j].nxt)
		if (i < e[j].to && !mark[i][e[j].to] && str[i] == str[e[j].to])
			mark[i][e[j].to] = true, Q[++ tl] = node{i, e[j].to};
	while (hd <= tl) {
		int x = Q[hd].x, y = Q[hd].y;
		++ hd;
		for (int i = head[x]; i; i = e[i].nxt)
		for (int j = head[y]; j; j = e[j].nxt) if (str[e[i].to] == str[e[j].to]) {
			int u = e[i].to, v = e[j].to;
			if (u > v) u ^= v ^= u ^= v;
			if (!mark[u][v]) mark[u][v] = true, Q[++ tl] = node{u, v};
		}
	}
}

int main() {
	scanf("%d%d%d%s", &n, &m, &q, str + 1);
	for (int i = 1, u, v; i <= m; ++ i) {
		u = read(), v = read();
		if (str[u] == '0' && str[v] == '0') w.add(u, v);
		else if (str[u] == '1' && str[v] == '1') b.add(u, v);
		else bw.add(u, v);
	}
	b.connect(), w.connect(), bw.connect();
	bfs();
	while (q --) {
		int u = read(), v = read();
		if (u > v) u ^= v ^= u ^= v;
		puts(mark[u][v] ? "YES" : "NO");
	}
	return 0;
}

CF1517F Reunion

如果能求半径不超过 \(r\) 的方案数,就可以得到半径恰好为 \(r\) 的方案数。

对于半径不超过 \(r\) 这个条件,我们考虑将限制条件转为所有没空的节点与它距离不超过 \(r\) 的点覆盖了整棵树。

注意到,当子树内有点没有被覆盖的时候,子树内的某些点即使能覆盖到子树外的点,也是可以被其它点取代的。因此状态记下当前最深的没有被覆盖的节点/最浅的没空的节点,然后树形背包随便数。

#include <cstdio>
#include <cstring>

const int mod = 998244353, inv2 = 499122177;
inline int min(const int x, const int y) {return x < y ? x : y;}
inline int max(const int x, const int y) {return x > y ? x : y;}
struct Edge {int to, nxt;} e[605];
int head[305], f[305][305], g[305][305], tmpf[305], tmpg[305], a[305], dep[305], tot, r;
inline void AddEdge(int u, int v) {
	e[++ tot].to = v, e[tot].nxt = head[u], head[u] = tot;
}
void dfs(int u, int fa) {
	f[u][0] = g[u][0] = 1, dep[u] = 0;
	for (int i = head[u]; i; i = e[i].nxt) if (e[i].to != fa) {
		dfs(e[i].to, u);
		int v = e[i].to;
		memset(tmpf, 0, sizeof tmpf);
		memset(tmpg, 0, sizeof tmpg);
		for (int j = 0; j <= dep[u]; ++ j)
		for (int k = 0; k <= dep[v]; ++ k) {
			tmpf[max(j, k + 1)] = (tmpf[max(j, k + 1)] + 1ll * f[u][j] * f[v][k]) % mod;
			if (j + k + 1 <= r) {
				tmpg[k + 1] = (tmpg[k + 1] + 1ll * f[u][j] * g[v][k]) % mod;
				tmpg[j] = (tmpg[j] + 1ll * g[u][j] * f[v][k]) % mod;
			} else {
				tmpf[j] = (tmpf[j] + 1ll * f[u][j] * g[v][k]) % mod;
				tmpf[k + 1] = (tmpf[k + 1] + 1ll * g[u][j] * f[v][k]) % mod;
			}
			tmpg[min(j, k + 1)] = (tmpg[min(j, k + 1)] + 1ll * g[u][j] * g[v][k]) % mod;
		}
		dep[u] = max(dep[u], dep[e[i].to] + 1);
		memcpy(f[u], tmpf, sizeof f[u]), memcpy(g[u], tmpg, sizeof g[u]);
	}
}

int main() {
	int n, ans = 0;
	scanf("%d", &n);
	for (int i = 1, u, v; i < n; ++ i)
		scanf("%d%d", &u, &v), AddEdge(u, v), AddEdge(v, u);
	for (r = 1; r < n; ++ r) {
		memset(f, 0, sizeof f), memset(g, 0, sizeof g);
		dfs(1, -1);
		for (int i = 0; i < n; ++ i) ans = (ans + f[1][i]) % mod;
	}
	for (int i = 1; i <= n; ++ i) ans = 1ll * ans * inv2 % mod;
	printf("%d", ans);
	return 0;
}

「ZJOI2019 Day1」线段树

显然我们只需要算每个节点 tag 期望之和就行了。设节点的 tag 期望为 \(p\)

那么分情况讨论:

  1. 完全被 \([l,r]\) 覆盖的点。\(p\rightarrow (p+1)/2\)
  2. \([l,r]\) 有交的点。\(p\rightarrow p/2\)
  3. \([l,r]\) 无交且父亲与 \([l,r]\) 有交的点。……只要祖先有一个的 tag 是 \(1\),就会一直下放到它这个位置。

那我们直接记下 \(q\) 表示跟到它的路上有节点 tag 为 \(1\) 的概率。看起来很粗暴但没准行得通。

  1. 完全被 \([l,r]\) 覆盖的点。\(p\rightarrow (p+1)/2\)。子树内所有节点 \(q\rightarrow (q+1)/2\)
  2. \([l,r]\) 有交的点。\(p\rightarrow p/2,q\rightarrow q/2\)
  3. \([l,r]\) 无交且父亲与 \([l,r]\) 有交的点。\(p\rightarrow (p+q)/2\)\(q\) 不变。

竟然可以成功转移了,然后就没了。

#include <cstdio>
#define gc (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1 ++)

const int mod = 998244353, inv2 = 499122177;
inline void add(int &x, const int y) {
	if ((x += y) >= mod) x -= mod;
}
inline void mns(int &x, const int y) {
	if ((x -= y) < 0) x += mod;
}
inline void div2(int &x) {
	if (x & 1) x = (x >> 1) + inv2;
	else x >>= 1;
}
inline void adddiv(int &x) {
	if (x & 1) x = x + 1 >> 1;
	else x = (x >> 1) + inv2;
}
char buf[100000], *p1, *p2;
inline int read() {
	char ch;
	int x = 0;
	while ((ch = gc) < 48);
	do x = x * 10 + ch - 48; while ((ch = gc) >= 48);
	return x;
}
struct node {
	int l, r, p, q, add, mul;
} tree[400005];
int ans, t = 1;
void build(int p, int l, int r) {
	tree[p].l = l, tree[p].r = r, tree[p].mul = 1;
	if (l != r) {
		build(p << 1, l, l + r >> 1);
		build(p << 1 | 1, (l + r >> 1) + 1, r);
	}
}
inline void pushdown(int p) {
	if (tree[p].mul == 1 && tree[p].add == 0) return;
	tree[p << 1].q = (1ll * tree[p << 1].q * tree[p].mul + tree[p].add) % mod;
	tree[p << 1 | 1].q = (1ll * tree[p << 1 | 1].q * tree[p].mul + tree[p].add) % mod;
	tree[p << 1].add = (1ll * tree[p << 1].add * tree[p].mul + tree[p].add) % mod;
	tree[p << 1 | 1].add = (1ll * tree[p << 1 | 1].add * tree[p].mul + tree[p].add) % mod;
	tree[p << 1].mul = 1ll * tree[p << 1].mul * tree[p].mul % mod;
	tree[p << 1 | 1].mul = 1ll * tree[p << 1 | 1].mul * tree[p].mul % mod;
	tree[p].mul = 1, tree[p].add = 0;
}
void update(int p, int l, int r) {
	if (l <= tree[p].l && tree[p].r <= r) {
		mns(ans, tree[p].p), adddiv(tree[p].p), add(ans, tree[p].p), adddiv(tree[p].q);
		div2(tree[p].mul), adddiv(tree[p].add);
		return;
	}
	pushdown(p);
	mns(ans, tree[p].p), div2(tree[p].p), add(ans, tree[p].p), div2(tree[p].q);
	int mid = tree[p].l + tree[p].r >> 1;
	if (l <= mid) update(p << 1, l, r);
	else {
		mns(ans, tree[p << 1].p);
		add(tree[p << 1].p, tree[p << 1].q), div2(tree[p << 1].p);
		add(ans, tree[p << 1].p);
	}
	if (mid < r) update(p << 1 | 1, l, r);
	else {
		mns(ans, tree[p << 1 | 1].p);
		add(tree[p << 1 | 1].p, tree[p << 1 | 1].q), div2(tree[p << 1 | 1].p);
		add(ans, tree[p << 1 | 1].p);
	}
}

int main() {
	int n = read(), q = read();
	build(1, 1, n);
	while (q --) {
		int op = read();
		if (op == 1) {
			int l = read(), r = read();
			update(1, l, r), add(t, t);
		} else printf("%d\n", (int)(1ll * ans * t % mod));
	}
	return 0;
}
posted @ 2022-07-31 19:47  zqs2020  阅读(72)  评论(0)    收藏  举报