AGC001 题解

A - BBQ Easy

先将 \(2N\) 个数排序,从大到小考虑,最大的数一定不会产生贡献,次大的数可以和最大的数捆绑在一起,并产生贡献,以此类推,这样的贪心正确性还是较为显然的。

#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll Read() {
	int sig = 1;
	ll num = 0;
	char c = getchar();
	while(!isdigit(c)) {
		if(c == '-') {
			sig = -1;
		}
		c = getchar();
	}
	while(isdigit(c)) {
		num = (num << 3) + (num << 1) + (c ^ 48);
		c = getchar();
	}
	return num * sig;
}
void Write(ll x) {
	if(x < 0) {
		putchar('-');
		x = -x;
	}
	if(x >= 10) {
		Write(x / 10);
	}
	putchar((x % 10) ^ 48);
}

const int N = 205;
int n, a[N];

int main() {
	int i;
	n = Read(), n <<= 1;
	for(i = 1; i <= n; i++) {
		a[i] = Read();
	}
	sort(a + 1, a + n + 1);
	int ans = 0;
	for(i = 1; i <= n; i += 2) {
		ans += a[i];
	}
	Write(ans), putchar('\n');
	return 0;
}

B - Mysterious Light

可以看到,从光反射第二次后开始后,以两轮为周期,光可能经过的范围总是为一个平行四边形,且光总是沿着平行四边形某个 \(120^\circ\) 的内角的角平分线射出。
比如样例的图:
image
设两条边长为 \(x, y\)。则对于一个平行四边形,只算它内部的光线总长,答案为:

\[f(x, y) = \begin{cases} x & \text{if } x = y \\ f(y, x) & \text{if } x < y \\ f(x - y, y) + 2y & \text{otherwise.} \end{cases}\]

显然初始两条光线总长为 \(N\),则答案为 \(N + f(X, N - X)\)\(O(N)\) 暴力计算可拿 \(300\) 分。
观察这个式子,首先可以令边界为 \(f(x, 0) = -x\),可以证明两个边界是等价的,很像辗转相减法,可以优化为辗转相除法,即:

\[f(x, y) = \begin{cases} -x & \text{if } y = 0 \\ f(y, x) & \text{if } x < y \\ f(x \bmod y, y) + 2 \cdot \lfloor \frac{x}{y} \rfloor \cdot y & \text{otherwise.} \end{cases}\]

观察到当 \(x < y\) 时,下面的式子等价于上面的式子,可得:

\[f(x, y) = \begin{cases} -x & \text{if } y = 0 \\ f(x \bmod y, y) + 2 \cdot \lfloor \frac{x}{y} \rfloor \cdot y & \text{otherwise.} \end{cases}\]

#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll Read() {
	int sig = 1;
	ll num = 0;
	char c = getchar();
	while(!isdigit(c)) {
		if(c == '-') {
			sig = -1;
		}
		c = getchar();
	}
	while(isdigit(c)) {
		num = (num << 3) + (num << 1) + (c ^ 48);
		c = getchar();
	}
	return num * sig;
}
void Write(ll x) {
	if(x < 0) {
		putchar('-');
		x = -x;
	}
	if(x >= 10) {
		Write(x / 10);
	}
	putchar((x % 10) ^ 48);
}

ll Solve(ll x, ll y) {
	return y ? Solve(y, x % y) + 2ll * (x / y) * y : -x;
}
int main() {
	ll n = Read(), x = Read();
	Write(n + Solve(n - x, x));
	return 0;
}

C - Shorten Diameter

没看数据范围,结果一看 \(N\) 只有 \(2000\)
考虑暴力枚举直径的中心,对 \(K\) 的奇偶性进行分类讨论。

  • \(K\) 为奇数时,枚举一条边,将与边的两个端点的距离最小值小于等于 \(\frac{K - 1}{2}\) 的点删去。
  • \(K\) 为偶数时,枚举一个点,将与点的距离小于等于 \(\frac{K}{2}\) 的点删去。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll Read() {
	int sig = 1;
	ll num = 0;
	char c = getchar();
	while(!isdigit(c)) {
		if(c == '-') {
			sig = -1;
		}
		c = getchar();
	}
	while(isdigit(c)) {
		num = (num << 3) + (num << 1) + (c ^ 48);
		c = getchar();
	}
	return num * sig;
}
void Write(ll x) {
	if(x < 0) {
		putchar('-');
		x = -x;
	}
	if(x >= 10) {
		Write(x / 10);
	}
	putchar((x % 10) ^ 48);
}
const int N = 2005;
int n, cnt = 0, k, o;
vector<int> e[N];
bool vis[N];
void Dfs(int u, int dis) {
	vis[u] = true;
	if(dis > k / 2) {
		cnt++;
	}
	for(auto v : e[u]) {
		if(vis[v] || v == o) {
			continue;
		}
		Dfs(v, dis + 1);
	}
}
int main() {
	int i;
	n = Read(), k = Read();
	vector<pair<int, int> > vec;
	for(i = 1; i < n; i++) {
		int u = Read(), v = Read();
		vec.emplace_back(u, v);
		e[u].push_back(v), e[v].push_back(u);
	}
	int ans = INT_MAX;
	if(k & 1) {
		for(auto p : vec) {
			int u = p.first, v = p.second;
			memset(vis, 0, sizeof(vis)), cnt = 0;
			o = v, Dfs(u, 0), o = u, Dfs(v, 0);
			ans = min(ans, cnt);
		}
	}
	else {
		for(i = 1; i <= n; i++) {
			memset(vis, 0, sizeof(vis)), cnt = 0;
			Dfs(i, 0);
			ans = min(ans, cnt);
		}
	}
	Write(ans);
	return 0;
}

D - Arrays and Palindrome

注意到:

  • 对于重叠的两个回文限制,长度分别为 \(x, x + 1\),且两个回文限制的初始位置(或结束位置,同理)相同,则可以推出这个长度为 \(x + 1\) 的序列相同。
  • 对于重叠的两个回文限制,长度均为 \(x\)\(x\) 是偶数,且两个回文限制的初始位置(或结束位置,同理)相差 \(1\),则可以推出这个长度为 \(x + 1\) 的序列相同。
  • 对于一个长度为 \(x\) 的回文限制,最多会产生 \(\lfloor \frac{x}{2} \rfloor\) 个两个数相等的限制条件,而判断 \(n\) 个数相等至少需要 \(n - 1\) 个限制条件,所以若 \(A\) 中有三个及以上的奇数一定无解。
  • 否则,把奇数尽量排在边缘是最优的。

可以构造:

\[b_i = \begin{cases} a_i - 1 & \text{if } i = 1 \\ a_M + 1 & \text{if } i = M \\ a_i & \text{otherwise.} \end{cases}\]

特判 \(a_1 = 1\) 的情况。

#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll Read() {
	int sig = 1;
	ll num = 0;
	char c = getchar();
	while(!isdigit(c)) {
		if(c == '-') {
			sig = -1;
		}
		c = getchar();
	}
	while(isdigit(c)) {
		num = (num << 3) + (num << 1) + (c ^ 48);
		c = getchar();
	}
	return num * sig;
}
void Write(ll x) {
	if(x < 0) {
		putchar('-');
		x = -x;
	}
	if(x >= 10) {
		Write(x / 10);
	}
	putchar((x % 10) ^ 48);
}

const int N = 100005;
int n, m, a[N];

int main() {
	int i, cnto = 0;
	n = Read(), m = Read();
	for(i = 1; i <= m; i++) {
		a[i] = Read();
		if(a[i] & 1) {
			cnto++;
		}
	}
	if(m == 1) {
		if(a[1] == 1) {
			printf("1\n1\n1");
		}
		else {
			printf("%d\n2\n%d %d", a[1], a[1] - 1, 1);
		}
		return 0;
	}
	if(cnto > 2) {
		printf("Impossible");
		return 0;
	}
	for(i = 1; i <= m; i++) {
		if(a[i] & 1) {
			swap(a[1], a[i]);
			break;
		}
	}
	for(i = m; i; i--) {
		if(a[i] & 1) {
			swap(a[m], a[i]);
			break;
		}
	}
	vector<int> b;
	if(a[1] > 1) {
		b.push_back(a[1] - 1);
	}
	for(i = 2; i < m; i++) {
		b.push_back(a[i]);
	}
	b.push_back(a[m] + 1);
	for(i = 1; i <= m; i++) {
		Write(a[i]), putchar(' ');
	}
	int k = b.size();
	putchar('\n'), Write(k), putchar('\n');
	for(i = 0; i < k; i++) {
		Write(b[i]), putchar(' ');
	} 
	return 0;
}

E - BBQ Hard

直接做是不好做的。
注意到组合数,因此考虑这个东西的组合意义。
首先有:

\[\sum_{i = 1}^N \sum_{j = i + 1}^N \binom{a_i + b_i + a_j + b_j}{a_i + a_j} = \dfrac{(\sum_{i = 1}^N \sum_{j = 1}^N \binom{a_i + b_i + a_j + b_j}{a_i + a_j}) - \sum_{i = 1}^N \binom{2a_i + 2b_i}{2a_i}}{2} \]

对于 \(1 \le i, j \le N\)\(\binom{a_i + b_i + a_j + b_j}{a_i + a_j}\) 即为从 \((0, 0)\) 出发,每次只能向右、向上走一个单位长度,走到 \((a_i + a_j, b_i + b_j)\) 的方案数。
将点向左平移 \(a_i\) 个单位长度,向下平移 \(b_i\) 个单位长度,\(\binom{a_i + b_i + a_j + b_j}{a_i + a_j}\) 即为从 \((-a_i, -b_i)\) 出发,每次只能向右、向上走一个单位长度,走到 \((a_j, b_j)\) 的方案数。
因此考虑 DP,初始给每个 \((-a_i, -b_i)\)\(1\) 的系数,再在 \((a_i, b_i)\) 处统计贡献。

#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll Read() {
	int sig = 1;
	ll num = 0;
	char c = getchar();
	while(!isdigit(c)) {
		if(c == '-') {
			sig = -1;
		}
		c = getchar();
	}
	while(isdigit(c)) {
		num = (num << 3) + (num << 1) + (c ^ 48);
		c = getchar();
	}
	return num * sig;
}
void Write(ll x) {
	if(x < 0) {
		putchar('-');
		x = -x;
	}
	if(x >= 10) {
		Write(x / 10);
	}
	putchar((x % 10) ^ 48);
}

const int N = 200005, M = 4005;
const ll Mod = 1e9 + 7;
ll f[M][M], fact[N], invfact[N], inv[N];
void Init(int n) {
	int i;
	inv[1] = 1;
	for(i = 2; i <= n; i++) {
		inv[i] = Mod - Mod / i * inv[Mod % i] % Mod;
	}
	fact[0] = invfact[0] = 1;
	for(i = 1; i <= n; i++) {
		fact[i] = fact[i - 1] * i % Mod;
		invfact[i] = invfact[i - 1] * inv[i] % Mod;
	}
}
ll C(ll n, ll m) {
	if(n < m || n < 0) {
		return 0;
	}
	return fact[n] * invfact[m] % Mod * invfact[n - m] % Mod;
}
int n, a[N], b[N];
ll QuickPow(ll x, ll y) {
	if(y == 0) {
		return 1;
	}
	if(y == 1) {
		return x;
	}
	ll half = QuickPow(x, y >> 1);
	if(y & 1) {
		return half * half % Mod * x % Mod;
	}
	return half * half % Mod;
}

int main() {
	Init(N - 5);
	int i, j;
	n = Read();
	for(i = 1; i <= n; i++) {
		a[i] = Read(), b[i] = Read();
		f[2001 - a[i]][2001 - b[i]]++;
	}
	for(i = 1; i <= 4002; i++) {
		for(j = 1; j <= 4002; j++) {
			f[i][j] = (f[i][j] + f[i - 1][j] + f[i][j - 1]) % Mod;
		}
	}
	ll ans = 0;
	for(i = 1; i <= n; i++) {
		ans = (ans + f[2001 + a[i]][2001 + b[i]]) % Mod;
		ans = (ans - C(2 * a[i] + 2 * b[i], a[i] * 2) + Mod) % Mod;
	}
	Write(ans * QuickPow(2, Mod - 2) % Mod);
	return 0;
}

F - Wide Swap

参考 @linghuchong_ 的神仙题解
对于排列 \(P\),我们定义 \(Q_{P_i} = i\)
显然在排列 \(P\) 中,若有 \(|P_i - P_j| = 1\),则在排列 \(Q\) 中,\(i\)\(j\) 一定相邻。
问题转化为,每次可以交换 \(Q\) 中相邻的两个数 \(Q_i, Q_{i + 1}\),需满足 \(|Q_i - Q_{i + 1}| \ge K\)
对于字典序最小的排列 \(P\),一定有越小的数坐标越靠前,因此我们的目标还是让 \(Q\) 的字典序最小。
考虑如何对 \(Q\) 进行“排序”,显然可以有一个 \(O(N^2)\) 的类冒泡排序。
做到 \(O(N \log N)\) 则是归并的思想。
对于两个已经按上述规则“排序”的序列,假设为序列 \(a, b\),如下图,要将它们归并成序列 \(c\)
image
现在如果当前 \(a\) 的第一个数需要插到 \(c\) 的末尾,显然是可行的,所以当 \(a_i < b_j\) 时,也就是将 \(a_i\) 插到 \(c\) 的末尾更优时一定这么做。
否则,假设当前 \(b\) 的第一个数要插到 \(c\) 的末尾,考虑有哪些数会阻挡它,显然是 \(a\) 剩下的数:
image
\(a\) 剩下的数会阻挡它,当且仅当下面的条件成立:

  • \(\exists k \in [i, |a|]\)\(|a_k - b_j| < K\)

反之,\(a\) 剩下的数不会阻挡它,当且仅当下面的条件成立:

  • \(\forall k \in [i, |a|]\)\(|a_k - b_j| \ge K\)

这一条件可以拆成两个条件(两个条件成立一个即可):

  • \((\min_{k = i}^{|a|} a_k) - b_j \ge K\)
  • \(b_j - (\max_{k = i}^{|a|} a_k) \ge K\)

其中,第二种情况若满足,则 \(a_i < b_j\) 必然成立,不满足 \(a_i > b_j\) 的前提,所以只要满足第一种情况就可以了。
等一下,这个条件貌似漏了一些情况,若满足上述条件,\(a\) 剩下的数一定不会阻挡它,但是反过来就不对了。
即,\(a\) 可以不满足上述情况,但是 \(b_j\) 在转移的也可以不受阻挡,并且 \(a_i\) 还是可以大于 \(b_j\)(存在一个 \(k\)\(k \not = i\)\(a_k < b_j - K\))。
但实际上我们可以证明,不存在不满足上述情况的且已被“排序”的 \(a\),证明如下:

假设存在满足上述情况且已被“排序”的 \(a\),那么 \(a\) 可以被一定分成两个集合 \(S_1,S_2\),使得 \((\max_{x \in S_1} x) + K \le b_j\),且 \((\min_{x \in S_2} x) - K \ge b_j\)
这样的话,\(\forall x \in S_1, y \in S_2\)\(y - x \ge 2K > K\)
因此 \(S_1\) 的数不能被 \(S_2\) 中的数阻挡,而 \(S_1\) 的数一定小于 \(S_2\) 的数,所以 \(S_1\) 的数应该被交换到最前面,所以 \(a\) 并未被完全排序,矛盾。

因此,只要 \((\min_{k = i}^{|a|} a_k) - b_j \ge K\) 满足,\(b_j\) 就一定可以被插入到 \(c\) 的末尾,且 \(b_j\) 插入到 \(c\) 的末尾一定是最优的(此时 \(a_i > b_j\) 显然成立)。
否则,我们只能把 \(a_i\) 插入到 \(c\) 的末尾。
代码实现时,可以预处理 \(a\) 的后缀最小值,来判断 \(b_j\) 是否应该被插入到 \(c\) 的末尾。

#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll Read() {
	int sig = 1;
	ll num = 0;
	char c = getchar();
	while(!isdigit(c)) {
		if(c == '-') {
			sig = -1;
		}
		c = getchar();
	}
	while(isdigit(c)) {
		num = (num << 3) + (num << 1) + (c ^ 48);
		c = getchar();
	}
	return num * sig;
}
void Write(ll x) {
	if(x < 0) {
		putchar('-');
		x = -x;
	}
	if(x >= 10) {
		Write(x / 10);
	}
	putchar((x % 10) ^ 48);
}

const int N = 500005;
int n, K, p[N], q[N], t[N], minn[N];
void Merge(int l, int mid, int r) {
	minn[mid + 1] = N;
	int i, j, k;
	for(i = mid; i >= l; i--) {
		minn[i] = min(minn[i + 1], q[i]);
	}
	i = l, j = mid + 1, k = l;
	while(i <= mid && j <= r) {
		if(minn[i] - K >= q[j]) {
			t[k++] = q[j++];
		}
		else {
			t[k++] = q[i++];
		}
	}
	while(i <= mid) {
		t[k++] = q[i++];
	}
	while(j <= r) {
		t[k++] = q[j++];
	}
	for(i = l; i <= r; i++) {
		q[i] = t[i];
	}
}
void Solve(int l, int r) {
	if(l >= r) {
		return ;
	}
	int mid = (l + r) >> 1;
	Solve(l, mid), Solve(mid + 1, r), Merge(l, mid, r);
}

int main() {
	int i;
	n = Read(), K = Read();
	for(i = 1; i <= n; i++) {
		p[i] = Read(), q[p[i]] = i;
	}
	Solve(1, n);
	for(i = 1; i <= n; i++) {
		p[q[i]] = i;
	}
	for(i = 1; i <= n; i++) {
		Write(p[i]), putchar('\n');
	}
	return 0;
}
posted @ 2024-08-09 21:42  Include_Z_F_R_qwq  阅读(17)  评论(2编辑  收藏  举报