2025牛客暑期多校训练营9


A. AVL tree

题意:给你一棵二叉树,你每次可以删除一个点,或者对于一个缺少了某个左右儿子的节点补充一个子节点。求变成\(AVL\)树的最少操作。\(AVL\)树是指任意一个节点都有左右儿子高度差小于等于\(1\)。节点高度为左右儿子最大高度加一。空节点高度为\(0\)

考虑一颗高度为\(h\)\(AVL\)树最少要几个节点,那么左右儿子一个高度为\(h-2\),一个为\(h-1\),则\(cnt_h = cnt_{h-1} + cnt_{h-2} + 1\)。容易发现这个式子增长的很快。因为有一个解是删除所有\(n\)个点,所以答案不会劣于\(n\),所以最后树的高度不会很大,我们设个\(30\)
那么记\(f[u][i]\)表示\(u\)这棵子树高度为\(i\)的最少操作次数,转移就是讨论一下,两个子树高度差小于等于\(1\),一定有一个子树高度为\(i-1\)。如果 \(i=1\)那么意味着把\(u\)着棵子树除了\(u\)都删掉。

点击查看代码
#include <bits/stdc++.h>

using i64 = long long;

void solve() {
	int n;
	std::cin >> n;
	std::vector<int> l(n), r(n);
	for (int i = 0; i < n; ++ i) {	
		std::cin >> l[i] >> r[i];
		-- l[i];
		-- r[i];
	}

	std::vector<i64> f(40);
	f[0] = 0; f[1] = 1;
	for (int i = 2; i <= 30; ++ i) {
		f[i] = f[i - 1] + f[i - 2] + 1;
	}

	std::vector<int> size(n);
	std::vector dp(n, std::array<i64, 31>{});
	const i64 inf = 1e9;
	auto dfs = [&](auto & self, int u) -> void {
		size[u] = 1;
		dp[u].fill(inf);
		if (l[u] == -1 && r[u] == -1) {
			for (int i = 1; i <= 30; ++ i) {
				dp[u][i] = f[i] - 1;
			}
		} else if (l[u] == -1) {
			self(self, r[u]);
			size[u] += size[r[u]];
			dp[u][1] = size[u] - 1;
			for (int i = 2; i <= 30; ++ i) {
				dp[u][i] = std::min(dp[u][i], dp[r[u]][i - 1] + f[std::max(0, i - 2)]);
				dp[u][i] = std::min(dp[u][i], dp[r[u]][std::max(0, i - 2)] + f[i - 1]);
			}
		} else if (r[u] == -1) {
			self(self, l[u]);
			size[u] += size[l[u]];
			dp[u][1] = size[u] - 1;
			for (int i = 2; i <= 30; ++ i) {
				dp[u][i] = std::min(dp[u][i], dp[l[u]][i - 1] + f[std::max(0, i - 2)]);
				dp[u][i] = std::min(dp[u][i], dp[l[u]][std::max(0, i - 2)] + f[i - 1]);
			}
		} else {
			self(self, l[u]);
			self(self, r[u]);
			size[u] += size[l[u]] + size[r[u]];
			dp[u][1] = size[u] - 1;
			for (int i = 2; i <= 30; ++ i) {
				for (int j = i - 1; j >= std::max(0, i - 2); -- j) {
					for (int k = i - 1; k >= std::max(0, i - 2); -- k) {
						if (std::max(j, k) == i - 1 && std::abs(j - k) <= 1) {
							dp[u][i] = std::min(dp[u][i], dp[l[u]][j] + dp[r[u]][k]);
						}
					}
				}
			}
		}
	};

	dfs(dfs, 0);
	i64 ans = n;
	for (int i = 0; i <= std::min(30, n); ++ i) {
		ans = std::min(ans, dp[0][i]);
	}
	std::cout << ans << "\n";
}

int main() {
	std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
	int t = 1;
	std::cin >> t;
	while (t -- ) {
		solve();
	}
	return 0;
}

B. Date

题意:给你一个数字串,求其中合法的日期个数。

思路来自该题首杀队伍。
考虑从前往后记录方案数。一个年份是不是闰年,可以求出其千位和百位组成的数字是否是\(4\)的倍数,如果是说明这个年份是\(400\)的倍数,再求出其十位和个位组成的数是不是\(4\)的倍数,那么一个年份是闰年则十位个位组成的数是\(4\)的倍数且千位和百位都是\(0\)或者是\(4\)的倍数。
则可以记\(year1[i][j]\)表示前\(i\)个数字用一个数字组成年份的千位为\(j\)的方案个数,\(year2[i][2][2]\)表示前\(i\)个数字组成的年份的千位和百位是否是\(4\)的倍数以及是否全\(0\)的方案数,\(year3[i][j][k][l]\)表示前\(i\)个数字组成的年份里十位为\(j\)且千位和百位是否是\(4\)的倍数以及是否全\(0\)的方案数,\(year4[i][2]\)表示前\(i\)个数字组成的年份是否是闰年的方案数。
那么发现新加入一个数字\(x\)\(year1\)可以转移到\(year2\)\(year2\)可以转移到\(year3\)\(year3\)可以转移到\(year4\)
然后继续考虑在年份后面接月份,为了方便起见,记闰年的第\(2\)月为第\(13\)月。
那么同样的记\(month1[i][j][2]\)表示前\(i\)个数字组成的年-月里月份的十位数为\(j\)年份是否是闰年的方案数,\(month2[i][j]\)为前\(i\)个数里组成的月份里第\(j\)月的方案数,注意\(j \in [1, 13]\),第\(13\)月代表闰年的第二月。那么同样的一步一步转移。
最后就是记\(day1[i][j][k]\)为前\(i\)个数组成的年-月-日里月份是\(j\),日的十位数是\(k\)的方案数。
那么统计答案就是对于\(s_i = x\)\(ans = ans + \sum_{j=1}^{13} \sum_{k=0}^{3} day1[i - 1][j][k]\),需要满足\(10k + x\)小于等于该月的最大日期。

点击查看代码
#include <bits/stdc++.h>

using i64 = long long;

const int N = 1e5 + 5, mod = 998244353;
const int d[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 29};

int day1[14][4];
int month2[14], month1[2][2];
int year4[2], year3[10][2][2], year2[2][2], year1[10];

void add(int & x, int y) {
	x += y;
	if (x >= mod) {
		x -= mod;
	}
}

void solve() {
	int n;
	std::cin >> n;
	std::string s;
	std::cin >> s;
	s = " " + s;
	int ans = 0;
	for (int i = 1; i <= n; ++ i) {
		int x = s[i] - '0';
		for (int j = 1; j <= 13; ++ j) {
			for (int k = 0; k <= 3; ++ k) {
				if (k * 10 + x >= 1 && k * 10 + x <= d[j]) {
					add(ans, day1[j][k]);
				}
			}
		}

		if (x <= 3) {
			for (int j = 1; j <= 13; ++ j) {
				add(day1[j][x], month2[j]);
			}
		}

		for (int j = 0; j <= 1; ++ j) {
			for (int k = 0; k <= 1; ++ k) {
				if (k * 10 + x >= 1 && k * 10 + x <= 12) {
					int m = k * 10 + x;
					if (m == 2 && j) {
						m = 13;
					}
					add(month2[m], month1[j][k]);
				}
			}
		}

		if (x <= 1) {
			for (int j = 0; j <= 1; ++ j) {
				add(month1[j][x], year4[j]);
			}
		}

		for (int j = 0; j <= 9; ++ j) {
			for (int k = 0; k <= 1; ++ k) {
				for (int l = 0; l <= 1; ++ l) {
					if (x == 0 && j == 0 && l == 0) {
						continue;
					}

					int t = (j * 10 + x) % 4 == 0 && (k || j || x);
					add(year4[t], year3[j][k][l]);
				}
			}
		}

		for (int j = 0; j <= 1; ++ j) {
			for (int l = 0; l <= 1; ++ l) {
				add(year3[x][j][l], year2[j][l]);
			}
		}

		for (int j = 0; j <= 9; ++ j) {
			add(year2[(j * 10 + x) % 4 == 0][j || x], year1[j]);
		}

		add(year1[x], 1);
	}

	std::cout << ans << "\n";
}

int main() {
	std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
	int t = 1;
	// std::cin >> t;
	while (t -- ) {
		solve();
	}
	return 0;
}

C. Epoch-making

题意:给你一个有向无环图,每个点有点权。你每次可以选择一部分点,只要其中每个点的后继边的点都被选择过了,代价为这些的点最大点权。求选完所有点的最小代价。\(n\leq 24\)

一个比较暴力的做法是,先预处理每个点集的代价,然后枚举每个集合的子集,从这些子集转移过来。这样是\(3^n\)的。
继续观察性质,考虑哪些选法一定不是最优,因为是取最大值,那么如果选择了一个点,对于其它能选的点且点权小于等于这个点的,都选上不会导致答案变差。那么把每个点按点权从小到大排序,然后枚举集合,每次尝试扩展这个集合,那么应该对于这个集合能选的点里从小到大选一个前缀。
循环转移是\(n2^n\),但有很多状态是非法的,可以用记忆化搜索通过。

点击查看代码
#include <bits/stdc++.h>

using i64 = long long;

void solve() {
	int n, m;
	std::cin >> n >> m;
	std::vector<std::pair<int, int>> a(n);
	for (int i = 0; i < n; ++ i) {
		int w;
		std::cin >> w;
		a[i] = {w, i};
	}

	std::ranges::sort(a);
	std::vector<int> id(n), w(n);
	for (int i = 0; i < n; ++ i) {
		w[i] = a[i].first;
		id[a[i].second] = i;
	}

	std::vector<int> adj(n);
	for (int i = 0; i < m; ++ i) {
		int u, v;
		std::cin >> u >> v;
		-- u, -- v;
		u = id[u];
		v = id[v];
		adj[v] |= 1 << u;
	}

	constexpr int inf = 1e9;
	std::vector<int> f(1 << n, -1);
	auto dfs = [&](auto & self, int s) -> int {
		if (s == (1 << n) - 1) {
			return 0;
		}

		if (f[s] != -1) {
			return f[s];
		}

		int res = inf, cur = s;
		for (int i = 0; i < n; ++ i) {
			if ((~s >> i & 1) && (s & adj[i]) == adj[i]) {
				cur |= 1 << i;
				res = std::min(res, self(self, cur) + w[i]);
			}
		}

		return f[s] = res;
	};

	std::cout << dfs(dfs, 0) << "\n";
}

int main() {
	std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
	int t = 1;
	// std::cin >> t;
	while (t -- ) {
		solve();
	}
	return 0;
}

F. Military Training

题意:两个长度为\(1\)的线段,你要通过每次让一个线段绕它的一个点旋转90$度来使得两个线段重合,求最少次数。

题解是直接求中点然后转换为曼哈顿距离,代码就几行。
赛时分类讨论调了半天,我的做法是先把线段都变成竖着的或者横着的。然后一个点可以通过两次旋转使得横纵坐标都变化\(1\),这些操作先使得两个线段处在一条直线上,然后在旋转过去。然后还要讨论一下哪个点对应哪个点。

点击查看代码
#include <bits/stdc++.h>

using i64 = long long;

struct P {
	i64 x, y;
};

i64 get(P a, P b, P c, P d) {
	i64 t = std::min(std::abs(b.x - d.x), std::abs(b.y - d.y));
	i64 ans = t * 2;
	t = std::max(std::abs(b.x - d.x), std::abs(b.y - d.y)) - t;
	ans += t / 2 * 4;
	t %= 2;
	ans += t * 2;
	return ans;
}

void solve() {
	P a, b, c, d;
	std::cin >> a.x >> a.y >> b.x >> b.y >> c.x >> c.y >> d.x >> d.y;
	if (a.x > b.x || a.y > b.y) {
		std::swap(a, b);
	}
	if (c.x > d.x || c.y > d.y) {
		std::swap(c, d);
	}

	i64 ans = 1e18;
	if ((a.x == b.x) != (c.x == d.x)) {	
		if (a.x == b.x) {
			auto x = a;
			auto y = a;
			x.x -= 1; y.x += 1;
			ans = std::min({ans, get(x, a, c, d) + 1, get(a, y, c, d) + 1});
			x = b;
			y = b;
			x.x -= 1; y.x += 1;
			ans = std::min({ans, get(x, b, c, d) + 1, get(b, y, c, d) + 1});
		} else {
			auto x = a;
			auto y = a;
			x.y -= 1; y.y += 1;
			ans = std::min({ans, get(x, a, c, d) + 1, get(a, y, c, d) + 1});
			x = b;
			y = b;
			x.y -= 1; y.y += 1;
			ans = std::min({ans, get(x, b, c, d) + 1, get(b, y, c, d) + 1});
		}
	} else {
		ans = get(a, b, c, d);
	} 
	std::cout << ans << "\n";
}

int main() {
	std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
	int t = 1;
	std::cin >> t;
	while (t -- ) {
		solve();
	}
	return 0;
}

G. Permutation

题意:给你一个排列\(p\),和一个空数组\(a\),每次你可以删掉\(p\)最左或者最右的元素,或者把\(p\)当前最小值加入\(a\)。需要操作\(n\)次,求不同\(a\)的个数。

考虑枚举\(a\)中的最后一个元素是什么。
可以求出笛卡尔树,记\([l_u, r_u]\)为以\(u\)最小的区间,那么想要\(u\)出现在\(a\)里,则必须把\([l_u, r_u]\)之外的元素都删掉。那么总共需要删\(n - (r_u - l_u + 1)\)次,则留给\(a\)的元素最多\(r_u - l_u + 1\)个。如果\(u\)可以出现在\(a\)里,则可以出现在\(a\)里的元素为笛卡尔树里\(u\)的所有祖先节点。记\(u\)深度为\(k = dep_u\),也就是说总共可以出现\(k\)个不同的元素,而最多出现\(size = r_u - l_u + 1\)个元素,可以记第\(i\)个元素出现次数为\(x_i\),也就是\(x_1 + x_2 + ... + x_k \leq size\),其中\(x_k \geq 1\),其它都是大于等于\(0\)。这是个经典问题,考虑转为等于的形式,记\(x_1 + x_2 + ... + x_k + z = size\),其中\(z \geq 0\)。可以证明任意满足条件的序列都对应一个\(a\)
那么可以插板法求解。答案就是\(C(size + k - 1,k)\)
代码省略取模类。

点击查看代码
constexpr int P = 998244353;
using Z = MInt<P>;

struct Comb {
    int n;
    std::vector<Z> _fac;
    std::vector<Z> _invfac;
    std::vector<Z> _inv;
    
    Comb() : n{0}, _fac{1}, _invfac{1}, _inv{0} {}
    Comb(int n) : Comb() {
        init(n);
    }
    
    void init(int m) {
        if (m <= n) return;
        _fac.resize(m + 1);
        _invfac.resize(m + 1);
        _inv.resize(m + 1);
        
        for (int i = n + 1; i <= m; i++) {
            _fac[i] = _fac[i - 1] * i;
        }
        _invfac[m] = _fac[m].inv();
        for (int i = m; i > n; i--) {
            _invfac[i - 1] = _invfac[i] * i;
            _inv[i] = _invfac[i] * _fac[i - 1];
        }
        n = m;
    }
    
    Z fac(int m) {
        if (m > n) init(2 * m);
        return _fac[m];
    }
    Z invfac(int m) {
        if (m > n) init(2 * m);
        return _invfac[m];
    }
    Z inv(int m) {
        if (m > n) init(2 * m);
        return _inv[m];
    }
    Z binom(int n, int m) {
        if (n < m || m < 0) return 0;
        return fac(n) * invfac(m) * invfac(n - m);
    }
} comb;

void solve() {
    int n;
    std::cin >> n;
    std::vector<int> a(n + 1);
    for (int i = 1; i <= n; ++ i) {
        std::cin >> a[i];
    }

    std::vector<int> stk(n + 10);
    int top = 0;
    std::vector<int> l(n + 1), r(n + 1);
    for (int i = 1; i <= n; ++ i) {
        while (top && a[stk[top]] > a[i]) {
            l[i] = stk[top];
            -- top;
        }

        if (top) {
            r[stk[top]] = i;
        }

        stk[ ++ top] = i;
    }

    Z ans = 1;
    auto dfs = [&](auto & self, int u, int L, int R, int d = 1) -> void {
        ans += comb.binom(R - L + 1 + d - 1, d);
        if (l[u]) {
            self(self, l[u], L, u - 1, d + 1);
        }

        if (r[u]) {
            self(self, r[u], u + 1, R, d + 1);
        }
    };

    dfs(dfs, stk[1], 1, n);
    std::cout << ans << "\n";
}

int main() {
    std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
    int t = 1;
    std::cin >> t;
    while (t -- ) {
        solve();
    }
    return 0;
}

J. Too many catgirls nya

题意:每一行的输入后面加上\(nya\)

点击查看代码
#include <bits/stdc++.h>

using i64 = long long;

void solve() {
	int n;
	std::cin >> n;
	std::cout << n << " nya\n";
	getchar();
	for (int i = 0; i < n; ++ i) {
		std::string s;
		char c;
		while ((c = getchar()) != '\n') {
			s += c;
		}
		std::cout << s;
		std::cout << " nya\n";
	}
}

int main() {
	// std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
	int t = 1;
	// std::cin >> t;
	while (t -- ) {
		solve();
	}
	return 0;
}

L. Ping Pong

题意:\(n\)个人打擂台,每个人有一个值,从前往后,一开始第一个人站在擂台上,然后依次和后面的人比,值小的排到最后,值大的留下。如果一个人连续赢了\(n-1\)轮,则第\(n\)轮这个人必败。求\(k\)轮后每个人打过几次擂台。每个人的值两两不同。

因为值两两不同,可以看作一个排列,考虑这样一个排列,以最大值开头,次大值结尾:\(n, ?, ?, ... ,n - 1\),如果此时\(n\)已经赢过一次了,你会发现\(2n - 2\)轮后又回到了这个状态,这个轮回中\(n\)\(n-1\)打了\(n\)次,其它都打了\(2\)次。
然后打表发现,每个初始状态最后都会变成这个状态,这个状态就是循环节。
想达到循环应该不需要很多轮操作。那么可以先把循环节模拟出来,然后就可以直接计算贡献。

点击查看代码
#include <bits/stdc++.h>

using i64 = long long;

void solve() {
	int n, k;
	std::cin >> n >> k;
	std::vector<int> a(n);
	for (int i = 0; i < n; ++ i) {
		std::cin >> a[i];
	}

	std::vector<int> ans(n);
	int K = std::min(4 * n, k);
	std::queue<int> q;
	for (int i = 1; i < n; ++ i) {
		q.push(i);
	}

	int cur = 0, cnt = 0;
	while (K -- ) {
		k -- ;
		int x = q.front(); q.pop();
		++ ans[cur]; ++ ans[x];
		++ cnt;
		if (cnt == n || a[x] > a[cur]) {
			q.push(cur);
			cur = x;
			cnt = 1;
		} else {
			q.push(x);
		}
	}
	int max = std::ranges::max(a);
	int p = 0;
	while (k) {
		k -- ;
		int x = q.front(); q.pop();
		++ ans[cur]; ++ ans[x];
		++ cnt;
		if (cnt == n || a[x] > a[cur]) {
			if (a[x] == max) {
				p = cur;
				q.push(cur);
				cur = x;
				cnt = 1;
				break;
			}
			q.push(cur);
			cur = x;
			cnt = 1;
		} else {
			q.push(x);
		}
	}

	int t = k / (2 * n - 2);
	k -= t * (2 * n - 2);
	for (int i = 0; i < n; ++ i) {
		if (a[i] == max || i == p) {
			ans[i] += t * n;
		} else {
			ans[i] += t * 2;
		}
	}

	while (k -- ) {
		int x = q.front(); q.pop();
		++ ans[cur]; ++ ans[x];
		++ cnt;
		if (cnt == n || a[x] > a[cur]) {
			q.push(cur);
			cur = x;
			cnt = 1;
		} else {
			q.push(x);
		}
	}

	for (int i = 0; i < n; ++ i) {
		std::cout << ans[i] << " \n"[i == n - 1];
	}
}

int main() {
	std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
	int t = 1;
	std::cin >> t;
	while (t -- ) {
		solve();
	}
	return 0;
}

M. Digit Sum

题意:记\(S(n)\)\(n\)的数位和,给出\(n\),求有没有一个\(a\)使得\(S(na) = aS(n)\)

\(n\leq 10^9\),最大数位和不超过\(81\),所以\(a\)也不会很大。枚举到\(100\)就行。

点击查看代码
#include <bits/stdc++.h>

using i64 = long long;

void solve() {
	i64 n;
	std::cin >> n;
	auto S = [&](i64 n) -> int {
		int res = 0;
		while (n) {
			res += n % 10;
			n /= 10;
		}
		return res;
	};

	for (int a = 1; a <= 100; ++ a) {
		if (S(n * a) == n * S(a)) {
			std::cout << a << "\n";
			return;
		}
	}
	std::cout << -1 << "\n";
}

int main() {
	std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
	int t = 1;
	std::cin >> t;
	while (t -- ) {
		solve();
	}
	return 0;
}
posted @ 2025-08-13 16:48  maburb  阅读(175)  评论(3)    收藏  举报