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\) 的内角的角平分线射出。
比如样例的图:
设两条边长为 \(x, y\)。则对于一个平行四边形,只算它内部的光线总长,答案为:
显然初始两条光线总长为 \(N\),则答案为 \(N + f(X, N - X)\),\(O(N)\) 暴力计算可拿 \(300\) 分。
观察这个式子,首先可以令边界为 \(f(x, 0) = -x\),可以证明两个边界是等价的,很像辗转相减法,可以优化为辗转相除法,即:
观察到当 \(x < y\) 时,下面的式子等价于上面的式子,可得:
#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\) 中有三个及以上的奇数一定无解。
- 否则,把奇数尽量排在边缘是最优的。
可以构造:
特判 \(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
直接做是不好做的。
注意到组合数,因此考虑这个东西的组合意义。
首先有:
对于 \(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\):
现在如果当前 \(a\) 的第一个数需要插到 \(c\) 的末尾,显然是可行的,所以当 \(a_i < b_j\) 时,也就是将 \(a_i\) 插到 \(c\) 的末尾更优时一定这么做。
否则,假设当前 \(b\) 的第一个数要插到 \(c\) 的末尾,考虑有哪些数会阻挡它,显然是 \(a\) 剩下的数:
\(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;
}