做题!

Tags:

【树】【博弈】【匹配】【构造】【贪心】【映射】【Trie】【组合数学】【计数 dp】【距离】【随机化】【期望】【拉格朗日插值】【数论】


P5801 [SEERC 2019] Game on a Tree

\(\color{#3498db}9.4\) tag:【树】【博弈】【匹配】

Trick:树上博弈,考虑完美匹配。

以防读题读错了,这个题中的棋子可以移动到任意没走过的祖先节点,不只是父亲节点。

首先有一个类似的问题:假如棋子只能移动到相邻节点,考虑完美匹配的存在性:

  • 如果存在完美匹配,先手不管选哪个点,后手都可以选它的匹配点,这样后手一直有点选,所以后手必胜;
  • 否则先手可以选择一个非匹配点,显然其它点都是匹配点(不然就存在增广路,可以多一条匹配边),那么就变成了第一种情况,只不过先后手交换了,因此先手必胜。

这个题也差不多,区别在于 \(u\) 不仅可以和其相邻点匹配,还可以和它的任意祖先匹配,树形 dp,设 \(f_u\) 表示 \(u\) 子树内最少还剩多少个未匹配的点,转移是容易的。

#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int n, f[N]; vector<int> G[N];
void dfs(int u, int fa) {
	for (int v : G[u]) if (v != fa) dfs(v, u), f[u] += f[v];
	if (f[u]) f[u]--; else f[u] = 1;
}
int main() {
	scanf("%d", &n);
	for (int i = 1, u, v; i < n; i++) scanf("%d%d", &u, &v), G[u].push_back(v), G[v].push_back(u);
	dfs(1, 0), printf("%s", f[1] ? "Alice" : "Bob");
}

P8976 「DTOI-4」排列

\(\color{#52c41a}8.4\) tag:【构造】【贪心】

整个序列的和是固定的,那么只需要让前一半的和不小于且尽可能接近 \(a\) 即可。

实际上只要有解,那就一定可以使得前一半之和恰好等于 \(a\)。使用调整法构造,一开始让前一半是 \(\left[1,\dfrac n2\right]\) 的数,从右往左依次调整,设 \(\text{mx}\) 表示当前最大的可用数,现在枚举到了 \(i\),还差 \(d\),如果 \(\text{mx}-i\ge d\),那就直接将 \(p_i\) 设为 \(i+d\) 即可,完事;否则将 \(p_i\) 设为 \(\text{mx}\),这时 \(\text{mx}\leftarrow\text{mx}-1\)。这样可以保证和恰好为 \(a\),并且每个数不重复。

最后剩下的数就放在右半边,记得判无解。

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5 + 5;
int T, n, a, b, p[N], h, sum, mx, now, d;
bool vis[N];
signed main() {
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin >> T;
	while (T--) {
		cin >> n >> a >> b;
		h = n >> 1, sum = n * (n + 1) >> 1, mx = n, now = h * (h + 1) >> 1, d = a - now;
		for (int i = 1; i <= h; i++) p[i] = i;
		for (int i = h; i && d > 0; i--) {
			if (mx - i < d) now += mx - i, d -= mx - i, p[i] = mx--;
			else now += d, p[i] = i + d, d = 0;
		}
		if (d > 0 || sum - now < b) { cout << -1 << '\n'; continue; }
		memset(vis + 1, 0, n);
		for (int i = 1; i <= h; i++) cout << p[i] << ' ', vis[p[i]] = 1;
		for (int i = 1; i <= n; i++) if (!vis[i]) cout << i << ' ';
		cout << '\n';
	}
}

P10753 [COI 2022/2023] Bliskost

\(\color{#ffc116}7\) tag:【映射】

Trick:判断是否本质相同,找不变量/找最小状态。

找最小状态这个思想比较常用,例如判断两个字符串是否循环同构,可以把它们的最小表示法求出来,就能直接判断是否相等了。

这个题中,一个字符串显然可以被变成 \(\texttt{aa...ax}\) 的形式,例如 \(\texttt{abc}\rightarrow\texttt{aab}\)\(\texttt{ced}\rightarrow\texttt{acd}\rightarrow\texttt{aab}\),只需要判断最后一个字符是否相等即可。

\(\texttt a=0\)\(\texttt b=1\),以此类推,容易得到最后一个字母为:

\[\sum_{i=1}^n(-1)^{n-i}s_i\pmod{26} \]

直接算即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
int n, q, a1, a2;
char s1[N], s2[N];
int main() {
	scanf("%d%d%s%s", &n, &q, s1 + 1, s2 + 1);
	for (int i = 1; i <= n; i++) (n - i & 1) ? a1 -= s1[i] - 'a' : a1 += s1[i] - 'a';
	for (int i = 1; i <= n; i++) (n - i & 1) ? a2 -= s2[i] - 'a' : a2 += s2[i] - 'a';
	puts((a1 % 26 + 26) % 26 == (a2 % 26 + 26) % 26 ? "da" : "ne");
	int x; char y;
	while (q--) {
		scanf("%d %c", &x, &y);
		(n - x & 1) ? a1 += s1[x] - 'a' : a1 -= s1[x] - 'a';
		s1[x] = y;
		(n - x & 1) ? a1 -= s1[x] - 'a' : a1 += s1[x] - 'a';
		puts((a1 % 26 + 26) % 26 == (a2 % 26 + 26) % 26 ? "da" : "ne");
	}
}

P6018 [Ynoi2010] Fusion tree

\(\color{#9d3dcf}10.4\) tag:【Trie】

首先考虑如果没有操作 1 怎么做,对于每个节点 \(u\),将其儿子节点的权值建出 01-Trie,操作 2 时在父节点处删除旧值并插入新值即可,操作 3 查询的是 Trie 上的全局异或值,记录一下有多少个数第 \(i\) 位是 \(1\) 即可。

现在有操作 1,对于父亲做单点修改,会影响祖父的 Trie;对于儿子,在 Trie 上做全局 \(+1\) 即可。时间复杂度为 \(\mathcal O(n\log V)\)

#include <bits/stdc++.h>
using namespace std;
#define il inline
#define IOSIZE (1 << 20)
char buf[IOSIZE], *p1 = buf, *p2 = buf;
#define gc() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, IOSIZE, stdin), p1 == p2) ? EOF : *p1++)
il int read() { int x = 0; char c = '%'; while (c < '0' || c > '9') c = gc(); while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = gc(); return x; }
const int N = 5e5 + 5;
int n, m, a[N], fa[N], tag[N], rt[N], tot, ch[N * 20][2], cnt[N * 20], num[N][20];
vector<int> G[N];
il void ins(int u, int x) {
	int p = rt[u]; cnt[p]++;
	for (int i = 0, o; i < 20; i++) {
		o = x >> i & 1, num[u][i] += o;
		if (!ch[p][o]) ch[p][o] = ++tot;
		p = ch[p][o], cnt[p]++;
	}
}
il void del(int u, int x) {
	int p = rt[u]; cnt[p]--;
	for (int i = 0, o; i < 20; i++) {
		o = x >> i & 1, num[u][i] -= o;
		p = ch[p][o], cnt[p]--;
	}
}
il void upd(int u) {
	int p = rt[u];
	for (int i = 0; i < 20; i++) {
		if (!cnt[p]) return;
		num[u][i] += cnt[ch[p][0]] - cnt[ch[p][1]];
		swap(ch[p][0], ch[p][1]), p = ch[p][0];
	}
}
il int qry(int u) {
	int sum = 0;
	for (int i = 0; i < 20; i++) if (num[u][i] & 1) sum |= 1 << i;
	return sum;
}
il int val(int u) { return a[u] + tag[fa[u]]; }
il void dfs(int u, int f) {
	fa[u] = f, rt[u] = ++tot;
	for (int i = 0, s = G[u].size(), v; i < s; i++) if ((v = G[u][i]) != f) dfs(v, u), ins(u, a[v]);
}
int main() {
	n = read(), m = read();
	for (int i = 1, u, v; i < n; i++) u = read(), v = read(), G[u].push_back(v), G[v].push_back(u);
	for (int i = 1; i <= n; i++) a[i] = read();
	dfs(1, 0);
	int op, x, y;
	while (m--) {
		op = read(), x = read();
		if (op == 1) {
			tag[x]++, upd(x);
			int f = fa[x], ff = fa[f];
			if (f) {
				if (ff) del(ff, val(f)), a[f]++, ins(ff, val(f));
				else a[f]++;
			}
		} else if (op == 2) {
			y = read();
			int f = fa[x];
			if (f) del(f, val(x)), a[x] -= y, ins(f, val(x));
			else a[x] -= y;
		} else {
			int f = fa[x];
			if (f) cout << (val(f) ^ qry(x)) << '\n';
			else cout << qry(x) << '\n';
		}
	}
}

P4456 [CQOI2018] 交错序列

\(\color{#ffc116}7\) tag:【组合数学】

插空法,答案即为:

\[\sum_{i=0}^{\lceil n/2\rceil}i^a(n-i)^b\binom{n-i+1}{i} \]

线性筛筛出 \(i^a\)\(i^b\),组合数需要用卢卡斯定理算。

#include <bits/stdc++.h>
using namespace std;
#define il inline
typedef long long ll;
const int N = 1e7 + 5;
struct Mod {
    ll m, p;
    il void init(int pp) { m = ((__int128)1 << 64) / pp; p = pp; }
    il ll operator()(ll x) { return x - ((__int128(x) * m) >> 64) * p; }
} mod;
int n, a, b, M, fac[N], inv[N], tot, pri[N], pwa[N], pwb[N];
bool vis[N];
il int qpow(int a, int b) {
	int c = 1;
	while (b) { if (b & 1) c = mod((ll)c * a); a = mod((ll)a * a), b >>= 1; }
	return c;
}
il void init(int n) {
	fac[0] = 1;
	for (int i = 1; i <= n; i++) fac[i] = mod((ll)fac[i - 1] * i);
	inv[n] = qpow(fac[n], M - 2);
	for (int i = n - 1; ~i; i--) inv[i] = mod((ll)inv[i + 1] * (i + 1));
	pwa[0] = (a == 0), pwb[0] = (b == 0), pwa[1] = 1, pwb[1] = 1;
	for (int i = 2, k, p; i <= n; i++) {
		if (!vis[i]) pri[++tot] = i, pwa[i] = qpow(i, a), pwb[i] = qpow(i, b);
		for (int j = 1; j <= tot && (k = i * (p = pri[j])) <= n; j++) {
			vis[k] = 1, pwa[k] = mod((ll)pwa[i] * pwa[p]), pwb[k] = mod((ll)pwb[i] * pwb[p]);
			if (!(i % p)) break;
		}
	}
}
il int C(int n, int m) { return n < m || m < 0 ? 0 : mod(mod((ll)fac[n] * inv[n - m]) * inv[m]); }
il int Lucas(int n, int m) { return n < M && m < M ? C(n, m) : mod((ll)C(mod(n), mod(m)) * C(n / M, m / M)); }
int main() {
	scanf("%d%d%d%d", &n, &a, &b, &M);
	mod.init(M), init(n + 1); int nn = (n + 1) >> 1, ans = 0;
	for (int i = 0; i <= nn; i++) ans = mod(ans + Lucas(n - i + 1, i) * mod((ll)pwa[n - i] * pwb[i]));
	printf("%d", (ans % M + M) % M);
}

P4977 毒瘤之神异之旅

\(\color{#3498db}9.8\) tag:【计数 dp】

Trick:分拆类问题,转成 Ferrers 图求解。

Ferrers 图是这样一个东西:

1
11
1111
1111
11111
1111111

代表一个无序分拆方案。上图代表的方案为 \(1+2+4+4+5+7\)

将一个 Ferrers 图沿着对角线翻转,得到的新 Ferrers 图称为原图的共轭,新分拆称为原分拆的共轭。上图的共轭如下所示:

1
1
11
1111
1111
11111
111111

其共轭代表的方案为 \(1+1+2+4+4+5+6\)

本题中,直接决策第 \(i\) 个数是多少不太好做,将其对称后,去掉了行数的限制,并且增加了每行宽度的上界 \(K\),较容易求解。设 \(f_{i,j}\) 表示使用了 \(i\)\(1\),最后一行有 \(j\)\(1\) 的方案数,可以得到转移方程:

\[f_{i,j}=f_{i-1,j-1}+f_{i-j,j} \]

统计答案的时候,枚举末尾有多少个 \(j\) 即可,答案为:

\[\sum_{j=0}^{k}\sum_{i=1}^ni^mf_{n-i(k-j),j} \]

滚动数组一下。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 10005, p = 1e9 + 7;
int n, k, m, pw[N], f[2][N], ans;
int qpow(int a, int b) {
	int c = 1;
	while (b) { if (b & 1) c = (ll)c * a % p; a = (ll)a * a % p, b >>= 1; }
	return c;
}
int main() {
	scanf("%d%d%d", &n, &k, &m);
	for (int i = 1; i <= n; i++) pw[i] = qpow(i, m);
	f[0][0] = 1;
	for (int j = 0, x, y; j < k; j++) {
		x = j & 1, y = x ^ 1;
		memset(f[y], 0, sizeof(f[y]));
		for (int i = j; i <= n; i++) f[y][i] = (f[x][i - 1] + f[y][i - j - 1]) % p;
		for (int i = 1; i <= n; i++) {
			if (i * (k - j) <= n) ans = (ans + (ll)f[x][n - i * (k - j)] * pw[i]) % p;
		}
	}
	cout << ans;
}

P6143 [USACO20FEB] Equilateral Triangles P

\(\color{#3498db}9.4\) tag:【距离】

先画出一个曼哈顿等边三角形 \(ABC\)

找到一个点 \(O\) 使得 \(\text{dis}(A,O)=\text{dis}(B,O)=\text{dis}(C,O)\),称其为曼哈顿外心。

枚举点 \(A\)\(\text{dis}(A,O)\),这样可以在一个方向上确定点 \(B\)。可以发现 \(C\) 的合法取值在一条斜线上,做一个斜向的前缀和即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 605, M = N * N;
int n, s[N][N], tot, x[M], y[M];
char a[N][N], t[N][N];
long long ans;
void turn() {
	memcpy(t, a, sizeof(t));
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++) a[j][i] = t[n - i + 1][j];
	}
}
void calc() {
	memset(s, 0, sizeof(s));
	for (int i = 1; i < n << 1; i++) {
		for (int j = 1; j < n << 1; j++) s[i][j] = s[i - 1][j + 1] + a[i][j];
	}
	tot = 0;
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++) if (a[i][j]) x[++tot] = i, y[tot] = j;
	}
	for (int i = 1, x1, y1, x2, y2; i <= tot; i++) {
		for (int j = 1; j <= n; j++) {
			x1 = x[i], y1 = y[i], x2 = x1 - j, y2 = y1 + j;
			if (x2 >= 0 && y2 <= n && a[x2][y2]) ans += s[x1 + j][y1 + j] - s[x1][y1 + (j << 1)];
		}
	}
}
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) {
		scanf("%s", a[i] + 1);
		for (int j = 1; j <= n; j++) a[i][j] = (a[i][j] == '*');
	}
	for (int i : {0, 1, 2, 3}) calc(), turn();
	printf("%lld", ans);
}

P4581 [BJOI2014] 想法

\(\color{#0e1d69}{11.0}\) tag:【随机化】【期望】

好像有什么神秘的叫 Hyperloglog 的东西,不会。

有向图可达点数不可做的原因在于会算重,那就整一些不会算重的,例如 \(\min\)

有一个比较好猜的结论:在 \([0,1]\) 中均匀随机选 \(n\) 个数,其最小值期望为 \(\dfrac{1}{n+1}\)

给每个点随机 \([0,1]\) 中的实数权值,先把每个点可达的点中的权值最小值算出来,多次试验取平均值,然后直接用上面的式子计算出估计值即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5, T = 200;
int n, m, u[N], v[N];
double w[N], s[N];
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m;
	for (int i = m + 1; i <= n; i++) cin >> u[i] >> v[i];
	for (int t = 1; t <= T; t++) {
		for (int i = 1; i <= m; i++) w[i] = 1.0 * rand() / RAND_MAX;
		for (int i = m + 1; i <= n; i++) s[i] += (w[i] = min(w[u[i]], w[v[i]])) / T;
	}
	for (int i = m + 1; i <= n; i++) cout << int(1.0 / s[i] - 0.5) << '\n';
}

P4463 [集训队互测 2012] calc

\(\color{#9d3dcf}10.6\) tag:【计数 dp】【拉格朗日插值】

很 educational 的一道题。

要互不相等,那就让它递增吧,最后再乘个 \(n!\)

\(f_{i,j}\) 表示前 \(i\) 个数,第 \(i\) 个数 \(\le j\) 的贡献和,有:

\[f_{i,j}=f_{i,j-1}+jf_{i-1,j-1} \]

初始值为 \(f_{0,j}=1\)

不幸的是 \(k\le10^9\),于是这个做法就死了。但 \(n\) 很小,这启示我们使用复杂度在 \(\mathcal O(n^2)\)\(\mathcal O(n^3)\) 之间的做法。

手动写出 \(n=0,1,2,\dots\) 的表达式:

\[\begin{aligned} f_{0,j}&=1\\ f_{1,j}&=\dfrac12j^2+\dfrac12j\\ f_{2,j}&=\dfrac18j^4+\dfrac{1}{12}j^3-\dfrac18j^2-\dfrac{1}{12}j\\ &\vdots \end{aligned} \]

发现 \(f_{n,j}\) 是关于 \(j\)\(2n\) 次多项式。实际上有:

\[f_{i,j}-f_{i,j-1}=jf_{i-1,j-1}\\ \Downarrow\\ \deg f_n-1=\deg f_{n-1}+1\\ \Downarrow\\ \deg f_n=\deg f_{n-1}+2,\deg f_0=0\\ \Downarrow\\ \deg f_n=2n \]

那就暴算出 \(f_{n,1}\sim f_{2n+1}\) 的值,拉格朗日插值即可,时间复杂度为 \(\mathcal O(n^2)\)

#include <bits/stdc++.h>
using namespace std;
#define il inline
typedef long long ll;
const int N = 505;
il int qpow(int a, int b, const int P) {
	int c = 1;
	while (b) { if (b & 1) c = (ll)c * a % P; a = (ll)a * a % P, b >>= 1; }
	return c;
}
int k, n, p, fac = 1, f[N][N << 1];
int main() {
	scanf("%d%d%d", &k, &n, &p); const int P = p;
	for (int i = 1; i <= n; i++) fac = (ll)fac * i % P;
	int u = min(n << 1 | 1, k);
	for (int i = 0; i <= u; i++) f[0][i] = 1;
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= u; j++) f[i][j] = (f[i][j - 1] + (ll)j * f[i - 1][j - 1]) % P;
	}
	if (k <= u) return printf("%lld", (ll)f[n][k] * fac % P), 0;
	int ans = 0;
	for (int i = 1, up, dn; i <= u; i++) {
		up = f[n][i], dn = 1;
		for (int j = 1; j <= u; j++) {
			if (i != j) up = (ll)up * (k - j + p) % P, dn = (ll)dn * (i - j + p) % P;
		}
		ans = (ans + (ll)up * qpow(dn, P - 2, P)) % P;
	}
	printf("%lld", ((ll)ans * fac) % P);
}

P6060 [加油武汉] 传染病研究

\(\color{#9d3dcf}10.4\) tag:【数论】

这个 \(T\le10^4\) 很容易误导你去想 \(\mathcal O(T\sqrt n)\),其实也可以做不过挺麻烦,有一种 \(\mathcal O((T+n)\omega(n))\) 的简洁做法,所以为什么不把 \(T\) 也开到 \(10^7\) 呢?

首先把 \(\sigma(n^k)\) 的式子写出来,假设 \(n=\prod\limits_{i=1}^mp_i^{c_i}\)

\[\sigma(n^k)=\prod_{i=1}^m(c_ik+1) \]

可以发现这是一个关于 \(k\) 的低次多项式,\(10^7\) 以内次数不超过 \(8\)

线性筛可以直接把每个多项式求出来,查询的时候直接算即可。

#include <bits/stdc++.h>
using namespace std;
#define il inline
typedef long long ll;
const int N = 1e7 + 5, Mod = 998244353;
int T, n, k, ans, tot, pri[N], fac[N], cnt[N], md[N], f[N][9];
il int mod(int x) { return x >= Mod ? x - Mod : x; }
il void sieve(int n) {
	f[1][0] = 1;
	for (int i = 2, k, p, x, ck; i <= n; i++) {
		if (!fac[i]) pri[++tot] = i, fac[i] = i, cnt[i] = 1, md[i] = i, f[i][0] = f[i][1] = 1;
		for (int j = 1; j <= tot && (k = i * (p = pri[j])) <= n; j++) {
			fac[k] = p;
			if (fac[i] == p) {
				ck = cnt[k] = cnt[i] + 1, md[k] = md[i] * p, x = i / md[i], f[k][0] = 1;
				for (int i1 = 1; i1 <= 8; i1++) f[k][i1] = (f[x][i1] + (ll)f[x][i1 - 1] * ck) % Mod;
				break;
			}
			cnt[k] = 1, md[k] = p, f[k][0] = 1;
			for (int i1 = 1; i1 <= 8; i1++) f[k][i1] = mod(f[i][i1] + f[i][i1 - 1]);
		}
	}
	for (int i = 2; i <= n; i++) for (int j = 0; j <= 8; j++) f[i][j] = mod(f[i][j] + f[i - 1][j]);
}
int main() {
	sieve(N - 1);
//	return 0;
	scanf("%d", &T);
	while (T--) {
		scanf("%d%d", &n, &k);
		ans = 0;
		for (int i = 8; ~i; i--) ans = ((ll)ans * k + f[n][i]) % Mod;
		printf("%d\n", ans);
	}
}

P9406 [POI 2020/2021 R3] Nawiasowania

\(\color{#3498db}9.6\) tag:【构造】【贪心】

将左括号作为 \(1\),右括号作为 \(-1\),合法条件是任意位置的前缀和不小于 \(0\)。那么在保证排列后的括号串合法的情况下,原串中一定是左括号越靠左越容易合法。令字典序最小的合法串为“最优串”。

考虑由 \(n-2\) 的构造得到 \(n\) 的构造,先在末尾新增两个右括号,然后在序列中选择一个右括号变成左括号,要求修改后仍然合法。

可以发现除了 \(s_n\) 外的右括号都可以改成左括号,那用一个堆维护目前可以改成左括号的最小位置即可。

小吐槽:这个题如果说“输出字典序最小的解”就会更好想一些,至少对我这种乐色来说/kk。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
int n, a[N], c; char s[N];
priority_queue<int, vector<int>, greater<int> > Q;
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) scanf("%d", &a[i]), s[i] = ')';
	Q.push(a[1]);
	for (int i = 1; i <= n >> 1; i++) {
		s[Q.top()] = '('; Q.pop();
		if (i < n >> 1) Q.push(a[i << 1]), Q.push(a[i << 1 | 1]);
	}
	for (int i = 1; i <= n; i++) {
		s[i] == '(' ? c++ : c--;
		if (c < 0) return puts("NIE"), 0;
	}
	puts(c ? "NIE" : s + 1);
}
posted @ 2025-02-12 09:21  Pentimentqwq  阅读(83)  评论(0)    收藏  举报