AT AGC001 题解

Link

A

简单题,每次找两个最短的配对,取两者 \(\min\)。实现上,对 \(a\) 从小到大排序,\(1 \to n\) 遍历 \(i\),每次将 \(a_i\) 累计入答案并对 \(i\) 迭代 \(+ 2\)

#include <bits/stdc++.h>

using i64 = long long;

int main() {
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);

	int n;
	std::cin >> n; n = n * 2;
	std::vector<int> a(n + 1);
	for (int i = 1; i <= n; i++) {
		std::cin >> a[i];
	}
	std::sort(a.begin() + 1, a.end());
	i64 ans = 0;
	for (int i = 1; i <= n; i += 2) {
		ans += a[i];
	}
	std::cout << ans << "\n";
	return 0;
}

B

数学解,以样例为例,第一次走 \(x\),你会发现接下来走的就是 \(n - x, x, n - 2x, n - 2x \dots\),丢到几何上,路径之和就是 \((n - x) + x - \gcd(n, x) = n - gcd(n, x)\),记得乘上 \(3\)

#include <bits/stdc++.h>

using i64 = long long;

int main() {
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);

	i64 n, x;
	std::cin >> n >> x;
	std::cout << 3 * (n - std::__gcd(n, x)) << "\n";
	return 0;
}

C

比较好想的 C 了。肯定是要避免大量的 corner case,也就是不要尝试算直径出来之后去删直径。从确定直径这件事入手,对目标长度 \(k\) 分类讨论:

  • \(k\) 为奇数,枚举中心边的两个节点向下 DFS,距离中心边两端节点 \(\gt \frac{n - 1}{2}\) 的都要删掉
  • \(k\) 为偶数,更简单了,枚举中心点 \(u\) 向下 DFS,距离中心点 \(\gt \frac{n}{2}\) 的都要删掉
#include <bits/stdc++.h>

using i64 = long long;

constexpr int N = 2007;
constexpr int M = 4007;
constexpr int inf = 1e9;

int ecnt, head[N];
struct edge {
	int to, nxt;
} e[M];

void addedge(int u, int v, int w = 1) {
	e[++ecnt].nxt = head[u]; head[u] = ecnt; e[ecnt].to = v;
}

int n, d, ans;

int dfs(int u, int from, int dep, int lim) {
	int ret = 0;
	for (int i = head[u]; ~i; i = e[i].nxt) {
		int v = e[i].to;
		if (v != from)
			ret += dfs(v, u, dep + 1, lim);
	}
	return (ret + (dep > lim ? 1 : 0));
}

int main() {
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);

	memset(head, -1, sizeof(head));

	std::cin >> n >> d;
	for (int i = 1, x, y; i < n; i++) {
		std::cin >> x >> y;
		addedge(x, y);
		addedge(y, x);
	}
	ans = inf;
	if (d & 1) {
		for (int i = 1; i <= n; i++) {
			for (int j = head[i]; ~j; j = e[j].nxt) {
				int v = e[j].to;
				if (i < v)
					ans = std::min(ans, dfs(i, v, 0, (d - 1) / 2) + dfs(v, i, 0, (d - 1) / 2));
			}
		}
	} else {
		for (int i = 1; i <= n; i++) {
			ans = std::min(ans, dfs(i, 0, 0, d / 2));
		}
	}
	std::cout << ans << "\n";
	return 0;
}

D

好的构造。

题意依托。形式化题意如下,定义两个数列 \(a, b\) 满足:

  • \(a_i, b_i \geq 0\),且 \(\sum a = \sum b = n\)
  • 当一个长度为 \(k\) 的序列被 \(a, b\) 分别分段时,每个分段都是回文,要求通过这些回文的约束来限制整个序列的元素都相等

现在只知道 \(a\) 是另一个长度为 \(m\) 的序列 \(A\) 的排列,需要找到一对新的数列 \(a, b\) 使得上述性质仍然成立。比如题目给的样例 1,按 \(a\) 分块则 \([1, 1]\) 回文,\([2, 3]\) 回文,也就是说对于序列 \(c\)\(c_1 = c_1, c_2 = c_3\),再看 \(b\)\([1, 3]\) 回文,此时又有 \(c_1 = c_3\),综合得到 \(c_1 = c_2 = c_3\) 符合性质要求。

发现每个长度为 \(l\) 的回文块都会产生 \(\lfloor \frac{l}{2} \rfloor\) 条作限制的边,对于 \(n\) 个点,至少需要 \(n - 1\) 条边才能联通。对于奇数块,如果一个块的长度为奇数则相比偶数块少连了一条边,因为中间位置呈现一个自环,令 \(k_a, k_b\) 分别表示 \(a, b\) 中奇数块的个数,总边数为:\(\frac{n - k_a}{2} + \frac{n - k_b}{2} = n - \frac{k_a + k_b}{2}\)。同时要满足 \(n - \frac{k_a + k_b}{2} \geq n - 1\)\(k_a + k_b \leq 2\)。无解情况显然,当 \(a\) 中的奇数块数量超过 \(2\)

考虑怎么在两个回文块之间沟通?一个波特的思路是,我们在每个块伸出来一条边连向下一个块,这个比较抽象,可以自己用草稿纸手玩一下 \(l = 5, l = 6\) 的块之间在开头、中间、结尾怎么联通的会直观很多,你会发现只有头尾才有可能是奇数,也印证了我们上面的证明。其实就是在做一个重新连接各个块之间的“延伸边”的操作,在这个过程中我们错位地限制了相邻的元素相等。

构造方法是容易的,将 \(a\) 中的块奇偶分类排序,两个奇数丢头部,偶数在中间连起来。对于目标 \(b\),构造 \(a_1 + 1, a_2, a_3, \dots a_{n - 1}\) 出来,如果 \(a\) 的结尾奇数块 \(\gt 1\),就在结尾丢一个 \(a_n - 1\),这样构造出来的 \(b\) 奇数块还会尽可能少,理想情况下为 \(0\)

#include <bits/stdc++.h>

using i64 = long long;

void solve() {
	int n, m;
	std::cin >> n >> m;
	std::vector<int> a(m + 1);
	int cot = 0;
	for (int i = 1; i <= m; i++) {
		std::cin >> a[i];
		cot += a[i] & 1;
	}
	if (cot > 2) { std::cout << "Impossible\n"; return; }
	if (m == 1) {
		if (a[1] == 1) {
			std::cout << "1\n1\n1\n";
		} else {
			std::cout << a[1] << "\n";
			std::cout << "2\n";
			std::cout << 1 << " " << a[1] - 1 << "\n";
		}
		return;
	}
	std::sort(a.begin() + 1, a.end(), [](int x, int y) { return (x & 1) > (y & 1); });
	std::cout << a[1] << " ";
	for (int i = 3; i <= m; i++) {
		std::cout << a[i] << " ";
	}
	std::cout << a[2] << "\n";
	std::vector<int> b;
	b.reserve(m);
	b.push_back(a[1] + 1);
	for (int i = 3; i <= m; i++) {
		b.push_back(a[i]);
	}
	if (a[2] > 1) {
		b.push_back(a[2] - 1);
	}
	std::cout << b.size() << "\n";
	for (auto i : b) {
		std::cout << i << " ";
	}
	std::cout << "\n";
}

int main() {
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);

	solve();
	return 0;
}

E

好的计数。转化一下题意为求:

\[\sum_{i = 1}^{n} \sum_{j = i + 1}^{n} \binom{a_i + b_i + a_j + b_j}{a_i + a_j} \]

这个式子的意义为将对应两包装中的肉和菜视为完全相同的若干个位置,然后钦定 \(a_i + a_j\) 的位置为肉,剩余的自动视为菜的方案数当然也可以钦定为 \(b_i + b_j\) 都是肉。

引入一个重要的推论:

\(\binom{x + y}{x}\) 的几何意义为 \((0, 0) \to (x, y)\) 的方案数,其中强制只能向上或者向右走。

证明略去,很有用而且很有名的关于杨辉三角与组合数意义的对应。

由此可以设计一个原点到 \((a_i + b_i, a_j + b_j)\) 的方案数的 DP,但是你注意到仍然是 \(O(n^2)\) 的,和裸着做没有区别。怎么去掉 \(i, j\) 中任意一维的限制呢?

将整个坐标平移 \((a_i, b_i)\),相当于 \((0, 0) \to (-a_i, b_i), (a_i + a_j, b_i + b_j) \to (a_j, b_j)\)。由于矩形的长宽不变,路径数不变,所以对答案是没有影响的。令 \(f(x, y)\) 表示从任意起点 \((-a_i, -b_i)\) 走到 \((x, y)\) 的方案数,我们有初始状态 \(f(-a_i, -b_i) = 1\),转移显然:

\[f(x, y) = f(x - 1, y) + f(x, y - 1) \]

再来去重。从所有 \(f(i, j)\) 中,减去 \(i = j\) 的情况 \(\binom{2a_i + 2b_i}{2a_i}\),剩下的就是 \(i \neq j\) 的情况,但是他们分别作为起点和终点被算了两次,所以还要除以二,最终:

\[\frac{(\sum_{j = 1}^{n} f(a_i, b_i)) - (\sum_{i = 1}^{n} \binom{2a_i + 2b_i}{2a_i})}{2} \]

#include <bits/stdc++.h>

using i64 = long long;

constexpr int N = 200010;
constexpr int M = 2000;
constexpr int D = 2010;
constexpr int P = 1e9 + 7;
constexpr int I = 500000004;

int n;
int a[N], b[N];
i64 fac[4 * M + 10], inv[4 * M + 10];
i64 dp[2 * D + 10][2 * D + 10];

i64 expow(i64 a, i64 b) {
    i64 res = 1;
    for (; b; b >>= 1) {
        if (b & 1) res = res * a % P;
        a = a * a % P;
    }
    return res;
}

void init() {
    int maxn = 4 * M;
    fac[0] = 1;
    for (int i = 1; i <= maxn; i++) {
        fac[i] = fac[i - 1] * i % P;
    }
    inv[maxn] = expow(fac[maxn], P - 2);
    for (int i = maxn - 1; i >= 0; i--) {
        inv[i] = inv[i + 1] * (i + 1) % P;
    }
}

i64 C(int n, int m) {
    if (m < 0 || m > n) return 0;
    return fac[n] * inv[m] % P * inv[n - m] % P;
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    init();
    
    std::cin >> n;
    for (int i = 1; i <= n; i++) {
        std::cin >> a[i] >> b[i];
        dp[D - a[i]][D - b[i]]++;
    }
    for (int i = 1; i <= 2 * D; i++) {
        for (int j = 1; j <= 2 * D; j++) {
            dp[i][j] = (dp[i][j] + dp[i-1][j] + dp[i][j-1]) % P;
        }
    }
    i64 ans = 0;
    for (int i = 1; i <= n; i++) {
        ans = (ans + dp[D + a[i]][D + b[i]]) % P;
        ans = (ans - C(2 * a[i] + 2 * b[i], 2 * a[i]) + P) % P;
    }
    ans = ans * I % P;
    std::cout << ans << "\n";
    return 0;
}

F

好的脑电波。

对于某个排列 \(p\) 的相关问题,如果难以考虑,不妨试试将位置和值交换一下?

定义新排列 \(p'\) 满足 \(p'_{p_i} = i\),通过这样的转换得到原题中的 \(j - i \geq k, |p_i - p_j| = 1\) 变成了可以交换的两个元素 \(p'_x, p'_{x + 1}\) 当且仅当 \(|p'_x - p'_{x + 1}| \geq k\),此时“一段距离”在这里变成了邻项交换。

一个 key observation 是,在 \(p'\) 中大小相差 \(\lt k\) 的元素相对顺序永远无法改变,对于这一系列的顺序约束,我们考虑找到满足这些所有约束的字典序最小排列。

考虑分治,对 \(p'\) 作归并排序,合并左右序列时,要决定的是取左半部分的第一个元素还是右半部分的第一个元素,计算左半部分后缀最小值 \(m_i\),如果 \(m_{l} \geq p'_{r} + k\),取右半部分的元素,否则取左半部分的。由于 \(m_l\) 表示左半部分剩余所有元素的最小值,如果这个元素的最小值都大于等于 \(a_r + k\),意味着右半部分的当前元素比左半部分所有剩余元素都小至少 \(k\),显然这些元素都可以放到左半部分元素之前不产生矛盾,通过这样确保尽可能将较小的元素提前得到更小的字典序。

#include <bits/stdc++.h>

using i64 = long long;

constexpr int N = 5e5 + 7;

int n, k;
int a[N], p[N], b[N], mn[N];

void merge(int l, int r) {
	if (l == r)
		return;
	int mid = (l + r) >> 1;
	merge(l, mid); merge(mid + 1, r);
	int mnv = n;
	for (int i = mid; i >= l; i--) {
		mnv = std::min(mnv, a[i]);
		mn[i] = mnv;
	}
	int cnt = l, p1 = l, p2 = mid + 1;
	while (p1 <= mid && p2 <= r) {
		if (mn[p1] >= a[p2] + k)
			b[cnt++] = a[p2++];
		else
			b[cnt++] = a[p1++];
	}
	while (p1 <= mid)
		b[cnt++] = a[p1++];
	while (p2 <= r)
		b[cnt++] = a[p2++];
	for (int i = l; i <= r; i++)
		a[i] = b[i];
}

int main() {
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);

	std::cin >> n >> k;
	for (int i = 1, x; i <= n; i++) {
		std::cin >> x;
		a[x] = i;
	}
	merge(1, n);
	for (int i = 1; i <= n; i++) {
		p[a[i]] = i;
	}
	for (int i = 1; i <= n; i++) {
		std::cout << p[i] << "\n";
	}
	return 0;
}
posted @ 2025-11-14 16:31  夢回路  阅读(5)  评论(0)    收藏  举报